「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]\) ,要求是如果有一条路径同时存在于两棵树上,那么两个端点必须颜色相同。
-
\(op = 0\) ,给定两个节点数为 \(n\) 的树,求染色方案数。
-
\(op = 1\) ,给定其中一颗树,另一棵树形态任意(即 \(n ^ {n - 2}\) 种),求染色方案数之和。
-
\(op = 2\), 仅给定上述三个数,求任意两棵形态任意的树的方案数之和。
对 \(998244353\) 取模。
\(n \leq 10 ^ 5,\ Y \leq 998244353,\ op \in \{0, 1, 2\}\)
Solution
Sol0(\(Y = 1\))
好说,无论怎么选都符合要求,所以:
-
\(op = 0\) 时, \(ans = 1\) 。
-
\(op = 1\) 时, \(ans = n ^ {n - 2}\) 。
-
\(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\) ,那么就有:
到现在又行不通了,因为 \(S\) 对应的是一个交集,没有办法把两者分开计算。所以:
容斥。容斥。容斥。
我们这样考虑枚举子集和子集的子集(这个不知道推荐直接记住):
而在这里对应的要计算的函数 \(F\) 就是 \(Y ^ {n - |S|}\) ,所以但进去就可以继续化简了。
我们发现前面两个 \(\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\) 代替上述式子)
虽然后面这坨看起来不是很熟悉,但是把它枚举的东西换成 \(|R|\) 的大小的时候,会因为枚举数量出现组合数:
好熟悉,再加上一个 \(1 ^ p\) 就是标准的二项式了:
现在再把 \(G\) 带进去,注意,一个有 \(n\) 个点, \(l\) 条边的森林(不存在强连通分量)有 \(n - l\) 个连通块,所以:
尝试利用前面的连通块数量把后面两大坨甩到外面: \(Y\) 可以直接甩进去, \(n\) 需要在外面加两个乘回来, \((1 - Y)\) 就只能甩到分母,然后在外面加:
其实两个分别写出来的分数都是常量了,现在着重考虑计算:\(\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\) ,但是仔细思考 \(T\) 的意义,实际上对于一个 \(T\) ,但凡是包含 \(T\) 的树都会被 \(E_1\) 和 \(E_2\) 个枚举到一次。所以相当于多枚举的 \(E_1\) 只不过是多枚举了,并不会牵连 \(E_2\) ,那多的系数也就很明显了,即多了一个 \(G\) 。
之后一大截其实很上面很像,不过为了连贯性还是都写上:
同样存在的两个常量可以暂时不考虑了,现在就是如何算:\(\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}\) 的幂次,大概就是这样:
然后有标号转无标号也就是一个阶乘的事,但是转着转着,哟,这不是形如 \(\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 就不放了,主要前面全给完了(