「WC2019」数树
非常非常非常好的计数题!!!
当然也真心感谢
(同时也是我(多半)最后一次大改马蜂后第一篇题解)
Description#
给定
-
,给定两个节点数为 的树,求染色方案数。 -
,给定其中一颗树,另一棵树形态任意(即 种),求染色方案数之和。 -
, 仅给定上述三个数,求任意两棵形态任意的树的方案数之和。
对
Solution#
Sol0( )#
好说,无论怎么选都符合要求,所以:
-
时, 。 -
时, 。 -
时, 。
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( )#
好说,树的形态都定了,直接按要求的来做就行了。
题目虽然说得是路径,但实际上跟单一条边是同理的,反正一个路径都被打通了,所有边肯定也是重合的。
所以拿个什么东西存一下其中一个,在另一个 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( )#
嘶,有点麻烦了。
假定两个树的边集分别是
既然存在重边就会产生 1 的贡献,总结一手上面的计算方法就是:
枚举树肯定是不现实的了,考虑换一个枚举方式,令
到现在又行不通了,因为
容斥。容斥。容斥。
我们这样考虑枚举子集和子集的子集(这个不知道推荐直接记住):
而在这里对应的要计算的函数
我们发现前面两个
不过发现还是需要
好办,背结论:
- 对于一个
个点的森林,假设有 个连通分量,每个连通分量大小事 ,则包含这个森林的大树个数为
(可以用
不对呀,他要已知的不是连通块数量的么,不慌,先假装不知道每个
(为了方便仍以
虽然后面这坨看起来不是很熟悉,但是把它枚举的东西换成
好熟悉,再加上一个
现在再把
尝试利用前面的连通块数量把后面两大坨甩到外面:
其实两个分别写出来的分数都是常量了,现在着重考虑计算:
所以我们可以用
我们可以这么想,一个节点的连通块做的贡献,假如这个连通块在没有结束,他的
用人话说,就是,不会因为连通块大小的变化而存在不同的转移。
所以并不需要管当前的连通块大小,我们只需要分继续增长和强行结束两种情况分别计算就行了,非常简洁。
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( )#
好家伙玩套娃呢是吧,连树的形态都不想给了。
虽然看上去这比上面那一个整整多了一个
之后一大截其实很上面很像,不过为了连贯性还是都写上:
同样存在的两个常量可以暂时不考虑了,现在就是如何算:
这样的话假如还想上面那样
考虑换一种枚举方式,我们可以对于每种连通块考虑,发现因为不能强连通,所以连通块相当于小树,那对于一种
然后呢。然后呢。然后呢。
这样来想,对于每一种连通块,我们要求其内部的点是有标号的,但同时对于
我们可以把这个过程类化成:有
但是我们放松一点条件,假如盒子也有标号,那其实对应的就是单个盒子的
然后有标号转无标号也就是一个阶乘的事,但是转着转着,哟,这不是形如
那这样的话就可以稍微总结总结:
- 单个盒子的贡献的
就是无标号盒子总方案的贡献,即可以理解成集合内的元素与集合的关系(而不是排列里的元素与排列的关系)
可以多项式爆算了,记得最后要把
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 就不放了,主要前面全给完了(
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现