「WC2019」数树

\(\color{black}{\text{A}} \color{red}{\text{ThousandMoons}}\) \(\text{Round}\) 里面一道卡我不知道的一个结论的题来源(讲的时候提到了这个题)

非常非常非常好的计数题!!!

当然也真心感谢 \(\color{black}{\text{P}} \color{red}{\text{inkRabbit}}\) 的题解,很清晰易懂。

(同时也是我(多半)最后一次大改马蜂后第一篇题解)

Description

给定 \(n\)\(Y\)\(op\) ,对于一棵树,你需要给他染色,范围是 \([1, Y]\) ,要求是如果有一条路径同时存在于两棵树上,那么两个端点必须颜色相同。

  1. \(op = 0\) ,给定两个节点数为 \(n\) 的树,求染色方案数。

  2. \(op = 1\) ,给定其中一颗树,另一棵树形态任意(即 \(n ^ {n - 2}\) 种),求染色方案数之和。

  3. \(op = 2\), 仅给定上述三个数,求任意两棵形态任意的树的方案数之和。

\(998244353\) 取模。

\(n \leq 10 ^ 5,\ Y \leq 998244353,\ op \in \{0, 1, 2\}\)

Solution

Sol0(\(Y = 1\)

好说,无论怎么选都符合要求,所以:

  1. \(op = 0\) 时, \(ans = 1\)

  2. \(op = 1\) 时, \(ans = n ^ {n - 2}\)

  3. \(op = 2\) 时, \(ans = n ^ {2(n - 2)}\)

Sol0

namespace sub0 {
	inline int solve() {
		if (!op) return 1;
		if (op == 1) return ksm(n, n - 2);
		if (op == 2) return ksm(n, (n - 2) << 1);
		return 0;
	}
}

Sol1(\(op = 0\)

好说,树的形态都定了,直接按要求的来做就行了。

题目虽然说得是路径,但实际上跟单一条边是同理的,反正一个路径都被打通了,所有边肯定也是重合的。

所以拿个什么东西存一下其中一个,在另一个 find 一下就行了。

Sol1

namespace sub1 {
	std::set S;

	inline int solve() {
		for (int i = 1, u, v; i < n; ++i) {
			std::cin >> u >> v;
			if (u > v) std::swap(u, v);
			S.insert(mp(u, v));
		}

		int cnt = n;

		for(int i = 1, u, v; i < n; ++i) {
			std::cin >> u >> v;
			if (u > v) std::swap(u, v);
			cnt -= S.count(mp(u, v));
		}

		return ksm(Y, cnt);
	}
}

Sol2(\(op = 1\)

嘶,有点麻烦了。

假定两个树的边集分别是 \(E_1\)\(E_2\)

既然存在重边就会产生 1 的贡献,总结一手上面的计算方法就是:\(ans = Y ^ {n - |E_1\ \cap E_2\ |}\)

枚举树肯定是不现实的了,考虑换一个枚举方式,令 \(S = E_1\cap E_2\) ,那么就有:

\[\sum_{E_2} Y ^ {n - |E_1\ \cap E_2\ |} \]

\[=\sum_{E_2} Y ^ {n - |S|} \sum_{S = E_1\ \cap E_2} \]

到现在又行不通了,因为 \(S\) 对应的是一个交集,没有办法把两者分开计算。所以:

容斥。容斥。容斥。

我们这样考虑枚举子集和子集的子集(这个不知道推荐直接记住):

\[F(S) = \sum\limits_{T \subseteq S}\sum\limits_{R \subseteq T} (-1) ^ {|T| - |R|} \cdot F(R) \]

而在这里对应的要计算的函数 \(F\) 就是 \(Y ^ {n - |S|}\) ,所以但进去就可以继续化简了。

\[=\sum_{E_2} \sum_{T \subseteq S} \sum_{R \subseteq T} (-1) ^ {|T| - |R|} \cdot Y ^ {n - |R|} \]

我们发现前面两个 \(\sum\) 是完全可以横屏的,枚举的都是全部子集, \(E_2\) 这个未知量就没有用了。

不过发现还是需要 \(E_2\) 有多少存在于 \(T\) 中,用人话说,就是,包含边集 \(T\) 的树有多少种

好办,背结论:

  • 对于一个 \(n\) 个点的森林,假设有 \(k\) 个连通分量,每个连通分量大小事 \(a_i\) ,则包含这个森林的大树个数为 \(n ^ {k - 2} \prod\limits_{i = 1} ^ k a_i\)

(可以用 \(\text{Prufer}\) 序列或者矩阵树定理证明,不过我显然不会)

不对呀,他要已知的不是连通块数量的么,不慌,先假装不知道每个 \(a_i\) 的大小, \(k\) 我们是能通过 \(|T|\) 知道的。

(为了方便仍以 \(G\) 代替上述式子)

\[=\sum_{T \subseteq E_1} G(T) \sum_{R \subseteq T} (-1) ^ {|T| - |R|} \cdot Y ^ {n - |R|} \]

\[=\sum_{T \subseteq E_1} G(T) \cdot Y ^ {n - |T|} \sum_{R \subseteq T} (-Y) ^ {|T| - |R|} \]

虽然后面这坨看起来不是很熟悉,但是把它枚举的东西换成 \(|R|\) 的大小的时候,会因为枚举数量出现组合数:

\[=\sum_{T \subseteq E_1} G(T) \cdot Y ^ {n - |T|} \sum_{p = 0} ^ {|T|} C_{|T|} ^ {p} \cdot (-Y) ^ {|T| - p} \]

好熟悉,再加上一个 \(1 ^ p\) 就是标准的二项式了:

\[=\sum_{T \subseteq E_1} G(T) \cdot Y ^ {n - |T|} \cdot (1 - Y) ^ {|T|} \]

现在再把 \(G\) 带进去,注意,一个有 \(n\) 个点, \(l\) 条边的森林(不存在强连通分量)有 \(n - l\) 个连通块,所以:

\[=\sum_{T \subseteq E_1} \Big(n ^ {k - 2} \prod_{i = 1} ^ {k} a_i \Big) \cdot Y ^ {k} \cdot (1 - Y) ^ {n - k} \]

尝试利用前面的连通块数量把后面两大坨甩到外面: \(Y\) 可以直接甩进去, \(n\) 需要在外面加两个乘回来, \((1 - Y)\) 就只能甩到分母,然后在外面加:

\[=\frac{(1 - Y) ^ n}{n ^ 2}\sum_{T \subseteq E_1} \prod_{i = 1} ^ {k} \frac{nY}{1 - Y} \cdot a_i \]

其实两个分别写出来的分数都是常量了,现在着重考虑计算:\(\sum\limits_{\small T \subseteq E_1} \prod_{i = 1} ^ {k} a_i\)

所以我们可以用 \(\text{DP}\) 记录当前节点所属连通块然后计算答案,大概就是 \(f_{u, s}\) 表示遍历到 \(u\) ,当前 \(u\) 所属的连通块大小为 \(s\) ,但是很显然是要对于每个点枚举连通块大小,时间肯定是不允许的,所以考虑更好的方法。

我们可以这么想,一个节点的连通块做的贡献,假如这个连通块在没有结束,他的 \(siz\) 就一直在增长;如果结束了,那么 \(siz\) 就不会变,同时就会新增一个常量的乘积。

用人话说,就是,不会因为连通块大小的变化而存在不同的转移。

所以并不需要管当前的连通块大小,我们只需要分继续增长和强行结束两种情况分别计算就行了,非常简洁。

Sol2

namespace sub2 {
	int fst[N], tot;
	struct edge {int nxt, to;} e[N << 1];

	inline void add(int u, int v) {
		e[++tot] = (edge) {fst[u], v}; fst[u] = tot;
		e[++tot] = (edge) {fst[v], u}; fst[v] = tot;
	}

	int bef, sin, f[2][N];

	inline void dfs(int u, int fa) {
		f[1][u] = sin; f[0][u] = 1;

		for (int i = fst[u], v; i; i = e[i].nxt) {
			v = e[i].to;
			if (v == fa) continue;
			dfs(v, u);

			f[1][u] = (M(f[1][u], f[0][v] + f[1][v]) + M(f[0][u], f[1][v])) % mod;
			f[0][u] = M(f[0][u], f[0][v] + f[1][v]);
		}
	}

	inline int solve() {
		for (int i = 1, u, v; i < n; ++i) {
			std::cin >> u >> v; add(u, v);
		}

		bef = M(ksm(1 - Y + mod, n), ksm(n, mod - 3));
		sin = M(M(n, Y), ksm(1 - Y + mod, mod - 2));

		dfs(1, 0);

		return M(bef, f[1][1]);
	}
}

Sol3(\(op = 2\)

好家伙玩套娃呢是吧,连树的形态都不想给了。

\[\sum_{E_1} \sum_{E_2} Y ^ {n - |E_1\ \cap E_2\ |} \]

\[=\sum_{E_1} \sum_{E_2} Y ^ {n - |S|} \sum_{S = E_1\ \cap E_2} \]

\[=\sum_{E_1} \sum_{E_2} \sum_{T \subseteq S} \sum_{R \subseteq T} (-1) ^ {|T| - |R|} \cdot Y ^ {n - |R|} \]

虽然看上去这比上面那一个整整多了一个 \(\sum\) ,但是仔细思考 \(T\) 的意义,实际上对于一个 \(T\) ,但凡是包含 \(T\) 的树都会被 \(E_1\)\(E_2\) 个枚举到一次。所以相当于多枚举的 \(E_1\) 只不过是多枚举了,并不会牵连 \(E_2\) ,那多的系数也就很明显了,即多了一个 \(G\)

\[=\sum_{T \subseteq E_1} G(T) ^ 2 \sum_{R \subseteq T} (-1) ^ {|T| - |R|} \cdot Y ^ {n - |R|} \]

之后一大截其实很上面很像,不过为了连贯性还是都写上:

\[=\sum_{T \subseteq E_1} G(T) ^ 2 \cdot Y ^ {n - |T|} \sum_{R \subseteq T} (-Y) ^ {|T| - |R|} \]

\[=\sum_{T \subseteq E_1} G(T) ^ 2 \cdot Y ^ {n - |T|} \sum_{p = 0} ^ {|T|} C_{|T|} ^ {p} \cdot (-Y) ^ {|T| - p} \]

\[=\sum_{T \subseteq E_1} G(T) ^ 2 \cdot Y ^ {n - |T|} \cdot (1 - Y) ^ {|T|} \]

\[=\sum_{T \subseteq E_1} \Big(n ^ {k - 2} \prod_{i = 1} ^ {k} a_i \Big) ^ 2 \cdot Y ^ {k} \cdot (1 - Y) ^ {n - k} \]

\[=\frac{(1 - Y) ^ n}{n ^ 4}\sum_{T \subseteq E_1} \prod_{i = 1} ^ {k} \frac{n ^ 2 Y}{1 - Y} \cdot a_i ^ 2 \]

同样存在的两个常量可以暂时不考虑了,现在就是如何算:\(\sum\limits_{\small T \subseteq E_1} \prod_{i = 1} ^ {k} a_i ^ 2\)

这样的话假如还想上面那样 \(\text{DP}\) 转移就不行了,因为每种大小的连通块拓展到 \(siz + 1\) 就不是常量的增加,不能压缩状态了。(所以后面那坨常熟多半还是要考虑的,不过并不影响整个 Sol 的推进过程)

考虑换一种枚举方式,我们可以对于每种连通块考虑,发现因为不能强连通,所以连通块相当于小树,那对于一种 \(n\) 个点的树,它的总贡献就是 \(n ^ 2 \cdot n ^ {n - 2} = n ^ n\)

然后呢。然后呢。然后呢。

这样来想,对于每一种连通块,我们要求其内部的点是有标号的,但同时对于 \(k\) 个连通块,我们就必须要求它们都是无标号的。

我们可以把这个过程类化成:有 \(k\) 个无标号的盒子要放 \(n\) 个有标号小球且无空盒子。这个非常不伦不类,一会要标号一会又不要标号。

但是我们放松一点条件,假如盒子也有标号,那其实对应的就是单个盒子的 \(\text{EGF}\) 的幂次,大概就是这样:

\[f = \sum_{i = 1} ^ {\infty} \frac{n ^ 2 Y}{1 - Y} \cdot i ^ i\ \ \ \Rightarrow\ \ \ F(x) = \sum_{i = 1} ^ {\infty} \frac{n ^ 2 Y}{1 - Y} \cdot \frac{i ^ i}{i!} \]

\[ans = \sum_{k = 1} ^ {n} \bigg(\sum_{i = 1} ^ {\infty} \frac{n ^ 2 Y}{1 - Y} \cdot \frac{i ^ i}{i!}\bigg) ^ k \]

然后有标号转无标号也就是一个阶乘的事,但是转着转着,哟,这不是形如 \(\large\frac{x ^ k}{k!}\) 的形式么,那不是又是一个 \(\text{Exp}\) ,那只需要到时候的第 \(n\) 项??

那这样的话就可以稍微总结总结:

  • 单个盒子的贡献的 \(\text{Exp}\) 就是无标号盒子总方案的贡献,即可以理解成集合内的元素与集合的关系(而不是排列里的元素与排列的关系)

可以多项式爆算了,记得最后要把 \(\text{EGF}\)\(n\) 项本身除掉的 \(n!\) 乘回来。

Sol3

namespace sub3 {
	const int N = 4e5 + 10;

	int rev[N], f[N], g[N], inc[N];

	inline int M(int a, int b) {return 1ll * a * b % mod;}
	inline int ksm(int a, int b) {
		int tmp = 1;
		for (; b; b >>= 1, a = M(a, a)) if (b & 1) tmp = M(a, tmp);
		return tmp;
	}

	inline void NTT(int* NTT, int lim, int sig) {
		for (int i = 0; i < lim; ++i) {
			if (i < rev[i]) std::swap(NTT[i], NTT[rev[i]]);
		}

		for (int L = 2, mid = 1, ur; L <= lim; mid = L, L <<= 1) {
			ur = ksm(G[sig], (mod - 1) / L);

			for (int r = 0; r < lim; r += L) {
				for (int l = r, cm = 1; l < r + mid; ++l, cm = M(cm, ur)) {
					int but = NTT[l], fly = M(cm, NTT[l + mid]);

					NTT[l] = but + fly; (NTT[l] >= mod) && (NTT[l] -= mod);
					NTT[l + mid] = but - fly; (NTT[l + mid] < 0) && (NTT[l + mid] += mod);
				} 
			}
		}

		if (!sig) {
			int inv = ksm(lim, mod - 2);
			for (int i = 0; i < lim; ++i) NTT[i] = M(NTT[i], inv);
		}
	}

	inline void qd(int *F, int *G, int m) {
		for (int i = 1; i < m; ++i) G[i - 1] = M(i, F[i]);
		G[m - 1] = 0;
	}

	inline void jf(int *F, int *G, int m) {
		for (int i = 1; i < m; ++i) G[i] = M(inc[i], F[i - 1]);
		G[0] = 0;
	}

	int lim, fre, _f[N];

	inline void Init(int n) {
		lim = 1; fre = -1;
		for (; lim <= n; lim <<= 1) ++fre;
		for (int i = 0; i < lim; ++i) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << fre);
	}

	inline void Inv(int *F, int *G, int m) {
		if (m == 1) return void(G[0] = ksm(F[0], mod - 2));

		Inv(F, G, (m + 1) >> 1);

		Init(m << 1);
		for (int i = 0; i < m; ++i) _f[i] = F[i];
		for (int i = m; i < lim; ++i) _f[i] = 0;

		NTT(_f, lim, 1); NTT(G, lim, 1);
		for (int i = 0; i < lim; ++i) {
			G[i] = (G[i] << 1) - M(_f[i], M(G[i], G[i]));
			(G[i] >= mod) && (G[i] -= mod), (G[i] < 0) && (G[i] += mod);
		}
		NTT(G, lim, 0);
		for (int i = m; i < lim; ++i) G[i] = 0;
	}

	int inf[N], _g[N];

	inline void Ln(int *F, int *G, int m) {
		memset(inf, 0, sizeof(inf));
		Inv(F, inf, m);

		qd(F, _g, m);

		Init(m << 1);
		NTT(_g, lim, 1); NTT(inf, lim, 1);
		for (int i = 0; i < lim; ++i) _g[i] = M(_g[i], inf[i]);
		NTT(_g, lim, 0);

		jf(_g, G, m);
	}

	int lng[N];

	inline void Exp(int *F, int *G, int m) {
		if (m == 1) return void(G[0] = 1);

		Exp(F, G, (m + 1) >> 1);

		Ln(G, lng, m);

		Init(m << 1);
		for (int i = 0; i < m; ++i) _f[i] = F[i] - lng[i], (_f[i] < 0) && (_f[i] += mod);
		++_f[0];
		for (int i = m; i < lim; ++i) _f[i] = lng[i] = 0;

		NTT(_f, lim, 1); NTT(G, lim, 1);
		for (int i = 0; i < lim; ++i) G[i] = M(G[i], _f[i]);
		NTT(G, lim, 0);
		for (int i = m; i < lim; ++i) G[i] = 0;
	}

	int fac[N], inv[N], bef, sin;

	inline int solve() {
		inc[0] = inc[1] = 1;
		for (int i = 2; i < N; ++i) inc[i] = M(mod - mod / i, inc[mod % i]);

		fac[0] = inv[0] = 1;
		for (int i = 1; i <= n; ++i) {
			fac[i] = M(fac[i - 1], i);
			inv[i] = M(inv[i - 1], inc[i]);
		}

		bef = M(ksm(1 - Y + mod, n), ksm(n, mod - 5));
		sin = M(M(M(n, n), Y), ksm(1 - Y + mod, mod - 2));

		for (int i = 1; i <= n; ++i) f[i] = M(sin, M(ksm(i, i), inv[i]));
		Exp(f, g, n + 1);

		return M(bef, M(g[n], fac[n]));
	}
}

Code 就不放了,主要前面全给完了(

posted @ 2022-06-30 20:05  Illusory_dimes  阅读(64)  评论(0编辑  收藏  举报