[HNOI/AHOI2018]毒瘤

又是一道虚树好题

Description

给定 \(n\) 个点, \(m\) 条约束关系 \(a\)\(b\) ,表示 \(a\)\(b\) 不能同时被选择。

问有多少种选择方案,答案对 998244353 取模。

题意概括:求一张图上的独立集数量(为了凸显逼格。。)

image

\(n \leq 10 ^ 5\ ,\ n - 1\leq m \leq n + 10\)

Analysis

这个 \(m\) 的范围有点诡异,如果把图建出来,最多只会有 \(11\) 条非树边,或许可以以此作为切入点呢。

一个比较正常的想法,先去想对于一个树的话,怎么做这道题。

这应该是一个基础到家了的树形 DP 吧,\(F_{u, 0/1}\) 表示以 u 为根的子树里面, u 这个节点选 / 不选的方案数,最终答案就是 \(F_{1, 0} + F_{1, 1}\)

转移式子就是:

\[F_{u, 0} = \prod_{(u, v) \in S} {(F_{v, 0} + F_{v, 1})} \]

\[F_{u, 1} = \prod_{(u, v) \in S} {F_{v, 0}} \]

然后就去要去想怎么扩展到一个非树边不超过 \(11\) 个的图上。

Solution

Part1 \(\small O\big(2^{m - (n - 1)} \cdot n\big)\)

由于边很少,我们甚至可以直接枚举,对于每条边 \((u, v)\) (同时钦定 \(u\) 的 dfn 序小于 \(v\) )的状态就是 \((1, 0)\ ,\ (0, 1)\ ,\ (0, 0)\)\(1\) 表示选, \(0\) 表示不选),同时注意到后面两种并不排斥,可以一块计算,所以我们就可以以 \(u\) 选 / 不选作为一条边的状态, \(O\Big(2 ^ {m - (n - 1)}\Big)\) 的去枚举,然后 \(O(n)\) 的算。

总体时间复杂度就是 \(O\Big(2^{m - (n - 1)} \cdot n\Big)\) 的啦。

从数据上来看这完完全全能拿到 \(70pts\) 呀!

然后写完过后实测有 \(75pts\) 。。

(到这里的话,个人评价是道蓝以下的 DP 题)

Part2 \(\small O\big(n + 2^{m - (n - 1)} \cdot (m - (n - 1))\big)\)

可以感觉到每次都是进行的相同的操作,只是对于有些点可能确定了选 / 不选,不再存在贡献,就会影响了整个树整体的答案,然后我们就因此重新算了一遍,属实有点亏。

然后想起来非树边一共就 \(11\) 条,考虑把所有多余的边对应的点拎出来建成虚树,那么这样的话最多会有 \(22\) 个关键点,虚树大小也是 \(\leq (44 + 1)\)

接下来考虑在虚树上预处理出来一点什么。

想到一开始的那两个式子,很显然不能直接硬算,因为虚树两个有边的点之间可能有不少被省略的点,这样就相当于一条边在原树上可能是一条链。

因为中间的点能被省略掉,说明肯定不是关键点,所以可以正常转移,那么对于原树上一条链 \(u \Rightarrow x_1 \Rightarrow x_2 \Rightarrow ... \Rightarrow x_k \Rightarrow v\) ,假设 \(F_{i, 0 / 1}\) 转移前的值为 \(G_{i, 0 / 1}\) ,转移式子有:

\[\left\{ \begin{aligned} F_{x_k\ , 0}& = G_{x_k\ , 0} \cdot (F_{v, 0} + F_{v, 1}) \\ F_{x_k\ , 1}& = G_{x_k\ , 0} \cdot F_{v, 0} \end{aligned} \right. \left\{ \begin{aligned} F_{x_{k - 1}\ \ , 0}& = G_{x_{k - 1}\ \ , 0} \cdot (F_{x_k\ , 0} + F_{x_k\ , 1}) \\ F_{x_{k - 1}\ \ , 1}& = G_{x_{k - 1}\ \ , 0} \cdot F_{x_k\ , 0} \end{aligned} \right. \]

合并带入有:

\[\left\{ \begin{aligned} F_{x_{k - 1}\ \ , 0}& = G_{x_{k - 1}\ \ , 0} \cdot (G_{x_k\ , 0} \cdot (F_{v, 0} + F_{v, 1}) + G_{x_k\ , 0} \cdot F_{v, 0}) \\ F_{x_{k - 1}\ \ , 1}& = G_{x_{k - 1}\ \ , 0} \cdot (G_{x_k\ , 0} \cdot F_{v, 0}) \end{aligned} \right. \\ \Longrightarrow \\ \left\{ \begin{aligned} F_{x_{k - 1}\ \ , 0}& = (2\cdot G_{x_{k - 1}\ \ , 0} \cdot G_{x_k\ , 0}) \cdot F_{v, 0} + (G_{x_{k - 1}\ \ , 0} \cdot G_{x_k\ , 0}) \cdot F_{v, 1} \\ F_{x_{k - 1}\ \ , 1}& = (G_{x_{k - 1}\ \ , 0} \cdot G_{x_k\ , 0}) \cdot F_{v, 0} \end{aligned} \right. \]

总的来说,这会转化成一个带系数 \(coe\) 的式子,大概就是:

\[\left\{ \begin{aligned} F_{u, 0}& = coe_{0, 0} \cdot F_{v, 0} + coe_{0, 1} \cdot F_{v, 1} \\ F_{u, 1}& = coe_{1, 0} \cdot F_{v, 0} + coe_{1, 1} \cdot F_{v, 1} \end{aligned} \right. \]

那么对于虚树上任意两个相连的点,都有类似的式子,可以预处理出这个 \(coe\) ,由于这玩意真的特别像 \(F\) ,同样可以选择大法师处理,也是 \(O(n)\)

同样,树上所有点的 \(F\) 也不全都是 \(1\) 了,因为考虑到我们处理的系数什么的都是跟虚树上的点有关,所以只要子树内不包含虚树上的点的话,至始至终答案都是一样的,就把这些点的 \(F\) 按照正常的 DP 做法处理一下就可以了。

统计答案的话,还是先枚举所有非树边的选点的状态,然后按照这个把虚树上的点的初值赋好我们已经预处理好的系数,跑一遍虚树更新答案就行了。

这样按照我们前面设想的系数的含义,相当于是跳过了没有必要再次计算的非虚树点,直接一步到位,然后达到了节省时间的目的。

时间复杂度 \(O\Big(n + 2^{m - (n - 1)} \cdot (m - (n - 1))\Big)\)

(由于实现雀食有点麻烦我还是参考了 Soulist 的写法,雀食非常简洁易懂)

Code

/*

*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
const int mod = 998244353;
int n, m, num, lim, k, a[N], fst[N], tot, ifst[N], itot;
int _k, _a[N], vis[N], cnt, banx[N], bany[N];
int ft[N], fa[N][18], dfn[N], tim, dep[N], sta[N], top;
ll f[N][2], g[N][2], coe[2][N][2], ans;
struct edge {
	int nxt, to;
} e[N << 1], ie[N << 1];
inline int read() {
	char ch = getchar();
	int s = 0, w = 1;
	while (ch < '0' || ch > '9') {if(ch == '-') w = -1; ch = getchar();}
	while (ch >= '0' && ch <= '9') {s = (s << 3) + (s << 1) + ch - '0'; ch = getchar();}
	return s * w;
}
inline int find(int x) {
	if (ft[x] == x) return ft[x];
	return ft[x] = find(ft[x]);
}
inline void add(int u, int v) {
	e[++tot] = (edge) {fst[u], v};
	fst[u] = tot;
}
inline void iadd(int u, int v) {
	ie[++itot] = (edge) {ifst[u], v};
	ifst[u] = itot;
}
inline void dfs1(int u, int fth) {
	dep[u] = dep[fth] + 1;
	dfn[u] = ++tim; fa[u][0] = fth;
	for (int i = 1; i <= 17; ++i) {
		fa[u][i] = fa[fa[u][i - 1]][i - 1];
	}
	for (int i = fst[u]; i; i = e[i].nxt) {
		int v = e[i].to;
		if (v == fth) continue;
		dfs1(v, u);
		vis[u] += vis[v];//要记得互相影响但是不在虚树上的点也不能遍历
	}
}
inline int lca(int u, int v) {
	if (dep[u] < dep[v]) swap(u, v);
	for (int i = 17; ~i; --i) {
		if (dep[fa[u][i]] >= dep[v]) {
			u = fa[u][i];
		}
	}
	if (u == v) return v;
	for (int i = 17; ~i; --i) {
		if(fa[u][i] != fa[v][i]) {
			u = fa[u][i]; v = fa[v][i];
		}
	}
	return fa[u][0];
}
inline bool cmp(int x, int y) {
	return dfn[x] < dfn[y];
}
inline void IllusoryTree() {
	_k = k;
	for (int i = 1; i <= k; ++i) _a[i] = a[i];
	sort(a + 1, a + 1 + k, cmp);
	sta[top = 1] = 1; ifst[1] = 0;
	for (int i = 1; i <= k; ++i) {
		if (a[i] == 1) continue;
		int Lca = lca(sta[top], a[i]);
		if (sta[top] != Lca) {
			while (dfn[sta[top - 1]] > dfn[Lca]) {
				iadd(sta[top - 1], sta[top]); --top;
			}
			if (sta[top - 1] == Lca) {
				iadd(sta[top - 1], sta[top]); --top;
			}
			else {
				_a[++_k] = Lca;
				vis[Lca] = 1;
				ifst[Lca] = 0;
				iadd(Lca, sta[top]);
				sta[top] = Lca;
			}
		}
		sta[++top] = a[i]; ifst[a[i]] = 0;
	}
	for (int i = 1; i < top; ++i) {
		if (!vis[sta[i]]) {
			_a[++_k] = sta[i];
			vis[sta[i]] = 1;
		}
		iadd(sta[i], sta[i + 1]);
	}
	for (int i = 1; i <= _k; ++i) a[i] = _a[i];
	k = _k;
}
inline void dp_init(int u, int fth) {
	g[u][0] = g[u][1] = 1;
	for (int i = fst[u]; i; i = e[i].nxt) {
		int v = e[i].to;
		if (v == fth || vis[v]) continue;
		dp_init(v, u);
		(g[u][0] *= (g[v][0] + g[v][1])) %= mod;
		(g[u][1] *= g[v][0]) %= mod;
	}
}
inline void dp_help(int u, int son) {
	f[u][0] = f[u][1] = 1;
	for (int i = fst[u]; i; i = e[i].nxt) {
		int v = e[i].to;
		if (v == son || v == fa[u][0]) continue;
		dp_help(v, u);
		(f[u][0] *= (f[v][0] + f[v][1])) %= mod;
		(f[u][1] *= f[v][0]) %= mod;
	}
}
inline void calc_coe(int u, int fth) {
	int p = u;
	coe[0][u][0] = coe[0][u][1] = coe[1][u][0] = 1;
	while (fa[p][0] != fth) {
		int q = fa[p][0];
		dp_help(q, p);
		ll coe00 = coe[0][u][0], coe01 = coe[0][u][1];
		ll coe10 = coe[1][u][0], coe11 = coe[1][u][1];
		coe[0][u][0] = (coe00 * f[q][0] + coe10 * f[q][1]) % mod;
		coe[1][u][0] = coe00 * f[q][0] % mod;
		coe[0][u][1] = (coe01 * f[q][0] + coe11 * f[q][1]) % mod;
		coe[1][u][1] = coe01 * f[q][0] % mod;
		p = q;
	}
}
inline void dfs2(int u, int fth) {
	dp_init(u, fth); a[++k] = u; vis[u] = 1;
	if (u != fth) calc_coe(u, fth);
	for (int i = ifst[u]; i; i = ie[i].nxt) {
		int v = ie[i].to;
		if (v == fth) continue;
		dfs2(v, u);
	}
}
inline void dp_calc(int u, int fth) {
	for (int i = ifst[u]; i; i = ie[i].nxt) {
		int v = ie[i].to;
		if (v == fth) continue;
		dp_calc(v, u);
		(f[u][0] *= (coe[0][v][0] * f[v][0] + coe[0][v][1] * f[v][1]) % mod) %= mod;
		(f[u][1] *= (coe[1][v][0] * f[v][0] + coe[1][v][1] * f[v][1]) % mod) %= mod;
	}
}
int main() {
	n = read(); m = read();
	num = m - n + 1; lim = (1 << num);
	for (int i = 1; i <= n; ++i) ft[i] = i;
	for (int i = 1; i <= m; ++i) {
		int u = read(), v = read();
		int x = find(u), y = find(v);
		if (x != y) {
			add(u, v); add(v, u);
			ft[x] = y;
		}
		else {
			if (!vis[u]) a[++k] = u, vis[u] = 1;
			if (!vis[v]) a[++k] = v, vis[v] = 1;
			banx[++cnt] = u; bany[cnt] = v;
		}
	}
	dfs1(1, 1);
	IllusoryTree();
	k = 0;
	dfs2(1, 1);
	/*if (!k) a[++k] = 1, vis[1] = k;
	sort(a + 1, a + 1 + k, cmp);*/
	for (int i = 0; i < lim; ++i) {
		for (int j = 1; j <= k; ++j) {
			f[a[j]][0] = g[a[j]][0];
			f[a[j]][1] = g[a[j]][1];
		}
		for (int j = 1; j <= cnt; ++j) {
			if ((1 << (j - 1)) & i) {
				f[banx[j]][0] = 0;
				f[bany[j]][1] = 0;
			}
			else f[banx[j]][1] = 0;
		}
		dp_calc(1, 0);
		ans += f[1][0] + f[1][1];
	}
	printf("%lld\n", ans % mod);
	return 0;
}
posted @ 2021-10-29 21:24  Illusory_dimes  阅读(47)  评论(0编辑  收藏  举报