Illublog我多想说再见啊

「WC2019」数树

Illu·2022-06-30 20:05·68 次阅读

「WC2019」数树

AThousandMoons Round 里面一道卡我不知道的一个结论的题来源(讲的时候提到了这个题)

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

当然也真心感谢 PinkRabbit 的题解,很清晰易懂。

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

Description#

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

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

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

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

998244353 取模。

n105, Y998244353, op{0,1,2}

Solution#

Sol0(Y=1#

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

  1. op=0 时, ans=1

  2. op=1 时, ans=nn2

  3. op=2 时, ans=n2(n2)

Sol0
Copy
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
Copy
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#

嘶,有点麻烦了。

假定两个树的边集分别是 E1E2

既然存在重边就会产生 1 的贡献,总结一手上面的计算方法就是:ans=Yn|E1 E2 |

枚举树肯定是不现实的了,考虑换一个枚举方式,令 S=E1E2 ,那么就有:

E2Yn|E1 E2 |

=E2Yn|S|S=E1 E2

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

容斥。容斥。容斥。

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

F(S)=TSRT(1)|T||R|F(R)

而在这里对应的要计算的函数 F 就是 Yn|S| ,所以但进去就可以继续化简了。

=E2TSRT(1)|T||R|Yn|R|

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

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

好办,背结论:

  • 对于一个 n 个点的森林,假设有 k 个连通分量,每个连通分量大小事 ai ,则包含这个森林的大树个数为 nk2i=1kai

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

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

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

=TE1G(T)RT(1)|T||R|Yn|R|

=TE1G(T)Yn|T|RT(Y)|T||R|

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

=TE1G(T)Yn|T|p=0|T|C|T|p(Y)|T|p

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

=TE1G(T)Yn|T|(1Y)|T|

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

=TE1(nk2i=1kai)Yk(1Y)nk

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

=(1Y)nn2TE1i=1knY1Yai

其实两个分别写出来的分数都是常量了,现在着重考虑计算:TE1i=1kai

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

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

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

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

Sol2
Copy
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#

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

E1E2Yn|E1 E2 |

=E1E2Yn|S|S=E1 E2

=E1E2TSRT(1)|T||R|Yn|R|

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

=TE1G(T)2RT(1)|T||R|Yn|R|

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

=TE1G(T)2Yn|T|RT(Y)|T||R|

=TE1G(T)2Yn|T|p=0|T|C|T|p(Y)|T|p

=TE1G(T)2Yn|T|(1Y)|T|

=TE1(nk2i=1kai)2Yk(1Y)nk

=(1Y)nn4TE1i=1kn2Y1Yai2

同样存在的两个常量可以暂时不考虑了,现在就是如何算:TE1i=1kai2

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

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

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

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

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

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

f=i=1n2Y1Yii      F(x)=i=1n2Y1Yiii!

ans=k=1n(i=1n2Y1Yiii!)k

然后有标号转无标号也就是一个阶乘的事,但是转着转着,哟,这不是形如 xkk! 的形式么,那不是又是一个 Exp ,那只需要到时候的第 n 项??

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

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

可以多项式爆算了,记得最后要把 EGFn 项本身除掉的 n! 乘回来。

Sol3
Copy
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 @   Illusory_dimes  阅读(68)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示
目录