@loj - 2496@ 「AHOI / HNOI2018」毒瘤


@description@

从前有一名毒瘤。

毒瘤最近发现了量产毒瘤题的奥秘。考虑如下类型的数据结构题:给出一个数组,要求支持若干种奇奇怪怪的修改操作(例如给一个区间内的数同时加上 c,或者将一个区间内的数同时开平方根),并且支持询问区间的和。毒瘤考虑了 n 个这样的修改操作,并将它们编号为 1...n。当毒瘤要出数据结构题的时候,他就将这些修改操作中选若干个出来,然后出成一道题。

当然了,这样出的题有可能不可做。通过精妙的数学推理,毒瘤揭露了这些修改操作之间的关系:有
m 对「互相排斥」的修改操作,第 i 对是第 ui 操作和第 vi 个操作。当一道题中同时含有 ui 和 vi 这两个操作时,这道题就会变得不可做。另一方面,当一道题中不包含任何「互相排斥」的操作时,这个题就是可做的。
此外,毒瘤还发现了一个规律: m - n 是一个很小的数字(参见「数据范围」中的说明),且任意两个修改操作都是连通的。两个修改操作 a, b 是连通的,当且仅当存在若干操作 t0, t1, ..., tl,使得 t0 = a, tl = b,且对任意 1 <= i <= l,t[i] 与 t[i-1] 都是「互相排斥」的修改操作。

一对「互相排斥」的修改操作称为互斥对。现在毒瘤想知道,给定值 n 和 m 个互斥对,他一共能出出多少道可做的不同的数据结构题。两个数据结构题是不同的,当且仅当其中某个操作出现在了其中一个题中,但是没有出现在另一个题中。

输入格式
第一行为正整数 n,m。
接下来 m 行,每行两个正整数 u, v,代表一对「互相排斥」的修改操作。

输出格式
输出一行一个整数,表示毒瘤可以出的可做的不同的数据结构题的个数。这个数可能很大,所以只输出模 998244353 后的值。

样例输入 1
3 2
1 2
2 3
样例输出 1
5
样例输入 2
6 8
1 2
1 3
1 4
2 4
3 5
4 5
4 6
1 6
样例输出 2
16

数据范围与提示
n <= 10^5, m <= n + 10。

@solution@

众所周知图的独立集问题是不可做的,所以我们需要对问题进行合理的暴力搜索。

注意到当 m = n - 1(即一棵树)时用 dp 随便做。
而 m - n 很小,这意味着整张图是一棵树 + 很少的非树边。
算了一下大概非树边最多 11 条,这 11 条边连着最多 22 个特殊点。

于是就有一个大胆的想法:暴力枚举特殊点是否被选中,然后这棵树再 O(n) 做一遍 dp。
暴力枚举的部分看上去是 2^22 种状态,实际上每条边只会对应 3 种状态(不可能一条边连着的两个点同时选),于是只会暴力枚举 3^11 种状态。这个范围小很多。
于是你就可以 O(3^11*n) 写出本题的暴力,约 70 分的好成绩。

要是我每次可以不重新算整棵树的 dp 就好了。
如果特殊点将原树分成了互不相关的若干连通块,且每个连通块只会受 1 或 2 个特殊点影响就好了。
这样我就可以预处理,就不用每次枚举完再重新做一遍 dp。

那我们就通过一些手段将这棵树分成若干连通块:使用虚树。
建出特殊点之间的虚树,虚树上的点将原图分成若干连通块。这样的话,要么是虚树上一条边对应一个连通块,要么一个连通块属于虚树上的某个点管辖。
这样只需要再在虚树上做一遍树形 dp,将预处理出来的连通块信息当作边权/点权即可。
虚树上只有最多 22*2 个点,所以可以轻松过。

@accepted code@

#include<map>
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
#define rep(G, x) for(Graph::edge *p=G.adj[x];p;p=p->nxt)
const int MAXN = 100000;
const int MOD = 998244353;
inline int add(int x, int y) {return (x + y)%MOD;}
inline int mul(int x, int y) {return 1LL*x*y%MOD;}
struct Graph{
	struct edge{
		int to, f[2][2];
		edge *nxt;
	}edges[2*MAXN + 5], *adj[MAXN + 5], *ecnt;
	Graph() {ecnt = &edges[0];}
	void addedge(int u, int v) {
		edge *p = (++ecnt);
		p->to = v, p->nxt = adj[u], adj[u] = p;
		p = (++ecnt);
		p->to = u, p->nxt = adj[v], adj[v] = p;
//		printf("! %d %d\n", u, v);
	}
}G1, G2;
int fa[20][MAXN + 5], dep[MAXN + 5], tid[MAXN + 5], dcnt = 0;
void dfs(int x, int f) {
	fa[0][x] = f, tid[x] = (++dcnt);
	for(int i=1;i<20;i++)
		fa[i][x] = fa[i-1][fa[i-1][x]];
	dep[x] = dep[f] + 1;
	rep(G1, x) {
		if( p->to == f ) continue;
		dfs(p->to, x);
	}
}
int lca(int u, int v) {
	if( dep[u] < dep[v] ) swap(u, v);
	for(int i=19;i>=0;i--)
		if( dep[fa[i][u]] >= dep[v] )
			u = fa[i][u];
	if( u == v ) return u;
	for(int i=19;i>=0;i--)
		if( fa[i][u] != fa[i][v] )
			u = fa[i][u], v = fa[i][v];
	return fa[0][u];
}
int sfa[MAXN + 5];
int find(int x) {
	return sfa[x] = (sfa[x] == x ? x : find(sfa[x]));
}
bool unite(int x, int y) {
	int fx = find(x), fy = find(y);
	if( fx == fy ) return false;
	else {
		sfa[fx] = fy;
		return true;
	}
}
bool tag[MAXN + 5];
bool cmp(int x, int y) {return tid[x] < tid[y];}
vector<int>arr;
int stk[MAXN + 5], tp;
void insert(int x) {
	if( tp ) {
		int z = lca(stk[tp], x);
		while( tp ) {
			int y = stk[tp--]; tag[y] = true;
			if( !tp || dep[stk[tp]] < dep[z] ) {
				if( y != z ) G2.addedge(z, y);
				break;
			}
			else G2.addedge(stk[tp], y);
		}
		stk[++tp] = z;
	}
	stk[++tp] = x;
}
int build_vtree() {
	sort(arr.begin(), arr.end(), cmp);
	for(int i=0;i<arr.size();i++)
		insert(arr[i]);
	int ret;
	while( tp ) {
		ret = stk[tp--], tag[ret] = true;
		if( tp ) G2.addedge(stk[tp], ret);
	}
	return ret;
}
int dp[2][MAXN + 5];
void dfs2(int x) {
	tag[x] = true, dp[0][x] = dp[1][x] = 1;
	rep(G1, x) {
		if( !tag[p->to] ) {
			dfs2(p->to);
			dp[0][x] = mul(dp[0][x], add(dp[0][p->to], dp[1][p->to]));
			dp[1][x] = mul(dp[1][x], dp[0][p->to]);
		}
	}
}
void dfs3(int x, int f) {
	dp[0][x] = dp[1][x] = 1;
	rep(G1, x) {
		if( p->to != f ) {
			if( !tag[p->to] ) dfs3(p->to, x);
			dp[0][x] = mul(dp[0][x], add(dp[0][p->to], dp[1][p->to]));
			dp[1][x] = mul(dp[1][x], dp[0][p->to]);
		}
	}
}
void func1(int x, int y, int f[][2]) {
	int p = fa[0][y];
	if( p == x ) {
		f[0][0] = f[0][1] = f[1][0] = 1;
		return ;
	}
	dp[0][x] = 1, dp[1][x] = 0;
	dfs3(p, y), f[0][0] = add(dp[0][p], dp[1][p]), f[0][1] = dp[0][p];
	dp[0][x] = 0, dp[1][x] = 1;
	dfs3(p, y), f[1][0] = add(dp[0][p], dp[1][p]), f[1][1] = dp[0][p];
	dfs2(p);
}
int g[2][MAXN + 5];
void get_value(int x, int f) {
	rep(G2, x) {
		if( p->to == f ) continue;
		func1(x, p->to, p->f), get_value(p->to, x);
	}
	g[0][x] = g[1][x] = 1;
	rep(G1, x) {
		if( !tag[p->to] ) {
			dfs2(p->to);
			g[0][x] = mul(g[0][x], add(dp[0][p->to], dp[1][p->to]));
			g[1][x] = mul(g[1][x], dp[0][p->to]);
		}
	}
}
int clr[MAXN + 5], c[MAXN + 5], root, ans;
vector<int>vec[MAXN + 5];
void check(int x, int fa) {
	rep(G2, x) {
		if( p->to != fa )
			check(p->to, x);
	}
	if( clr[x] != -1 )
		dp[clr[x]][x] = g[clr[x]][x], dp[!clr[x]][x] = 0;
	else dp[0][x] = g[0][x], dp[1][x] = g[1][x];
	rep(G2, x) {
		if( p->to != fa ) {
			dp[0][x] = mul(dp[0][x], add(mul(p->f[0][0], dp[0][p->to]), mul(p->f[0][1], dp[1][p->to])));
			dp[1][x] = mul(dp[1][x], add(mul(p->f[1][0], dp[0][p->to]), mul(p->f[1][1], dp[1][p->to])));
		}
	}
}
void search(int d) {
	if( d == arr.size() ) {
		check(root, 0);
		ans = add(ans, add(dp[0][root], dp[1][root]));
		return ;
	}
	clr[arr[d]] = 0, search(d + 1);
	if( !c[arr[d]] ) {
		for(int i=0;i<vec[arr[d]].size();i++)
			c[vec[arr[d]][i]]++;
		clr[arr[d]] = 1, search(d + 1);
		for(int i=0;i<vec[arr[d]].size();i++)
			c[vec[arr[d]][i]]--;
	}
}
map<int, int>mp;
int index(int x) {
	if( mp.count(x) ) return mp[x];
	else {
		arr.push_back(x);
		return mp[x] = arr.size() - 1;
	}
}
int main() {
	int n, m; scanf("%d%d", &n, &m);
	for(int i=1;i<=n;i++) sfa[i] = i;
	for(int i=1;i<=m;i++) {
		int u, v; scanf("%d%d", &u, &v);
		if( unite(u, v) ) G1.addedge(u, v);
		else {
			index(u), index(v);
			vec[u].push_back(v);
			vec[v].push_back(u);
		}
	}
	if( m == n - 1 ) {
		dfs2(1), printf("%d\n", add(dp[0][1], dp[1][1]));
		return 0;
	}
	dfs(1, 0), root = build_vtree(), get_value(root, 0);
	for(int i=1;i<=n;i++) c[i] = 0, clr[i] = -1;
	search(0);
	printf("%d\n", ans);
}

@details@

虽然说着挺简单,但还是写了 190+ 行的代码。
所以写暴力大概是最划算的选择。

所以注意区分 连通块属于虚树上的一条边 与 连通块属于虚树上一个点。

posted @ 2019-10-22 08:25  Tiw_Air_OAO  阅读(232)  评论(0编辑  收藏  举报