「FJWC2020Day4-IJN」夕张的改造 题解
题意简述
给定一棵 \(n\) 个节点的树。一次操作为断边再连边,需保证仍为树。求 \(k\) 次操作后树的形态个数。
\(n \leq 5000\),\(0 \leq k \leq n\)。
被绕晕了,对我来说是一道超好的题,写一篇题解理清思路,也希望帮助到你。
题目分析
由于我们总能浪费步数,即断开连上同一条边,或连上断开同一条边,所以我们求的答案为 \(0 \sim k\) 次不浪费的操作后,树的形态个数。
考虑操作了 \(k\) 次,我们就断开了 \(k\) 条边,并且需要这些边不会被连上。如果不考虑后来连上的 \(k\) 条边,此时树被划分为了 \(k + 1\) 个联通块。答案就是用 \(k\) 条边,不连上断开的边,把 \(k + 1\) 个联通块连成一棵树的方案数。
对于不会浪费步数的证明:
假设断开了一条连上的边,那么剩余边数不足以将其连成一棵树,故每次连边都不会浪费。
对于 \(k\) 个操作一起考虑的正确性证明:
即需要证明,存在一种方案,过程中始终合法,断边之后再连边,仍为一棵树。
由于不能断开一条新连上的边,所以所有新连上的边会被保存到最后,即每次连上的边只能答案中选取。
考虑某一次断边,在这次断边之前的操作均合法。断边后会得到两个联通块,需要在最终答案中的 \(k\) 条新增的边中,使用一条未被使用的边。最终答案里肯定有联通这两个联通块的边,否则不是一棵树,矛盾。而在断边之前我们保证这是一棵树,所以这些边都是没有被使用的。所以我们一定能够选择一条边来连上。
我们只需要考虑的浪费就是连上一条断开的边。如果没有这个限制,答案怎么统计呢?
根据 Prufer 序列经典结论,用 \(k\) 条边把大小分别为 \(a_1, \ldots, a_{k + 1}\) 的联通块连成树的方案数为 \(n ^ {(k + 1) - 2} \prod \limits _ {i = 1} ^ {k + 1} a_i\)。前者对于每一种方案都是相同的,也很好求。重点在于求出每一种方案下,后者之和,即求 \(\sum \prod \limits _ {i = 1} ^ {k + 1} a_i\)。
考虑 \(\prod a_i\) 的意义,即为从每一个联通块中,选出一个关键点,求方案数。同时为了 \(\sum\),我们还要统计所有情况下的方案数之和。不妨记 \(F[u][x][0 / 1]\) 表示以 \(u\) 为根的子树中,得到 \(x\) 个联通块,并且根节点所在联通块有没有选出关键点,所有情况下的 \(\prod a_i\) 之和。边界 \(F[i][1][0 / 1] = 1\)。
考虑树形 DP 孩子 \(u\) 合并到 \(v\)。决策是否断边,分类讨论一下。
-
断开 \(u\) 和 \(v\) 之间的边。
即此时 \(u\) 和 \(v\) 不在同一联通块中,那么 \(u\) 中关键点必须选出。\[F[v][i + j][0 / 1] \gets F[v][i + j][0 / 1] + F[v][i][0 / 1] \times F[u][j][1] \]之所以两个 \(\sum\prod\) 直接相乘,是因为 \(u\) 中每一个 \(\prod\) 需要分别和 \(v\) 中的 \(\prod\) 相乘产生一种方案,最后再求和,两个 \(\sum \prod\) 直接相乘就满足了。
-
不断开 \(u\) 和 \(v\) 之间的边。
类似地得到如下转移方程。\[F[v][i + j][0] \gets F[v][i + j][0] + F[v][i][0] \times F[u][j][0] \]\[F[v][i + j][1] \gets F[v][i + j][1] + F[v][i][0] \times F[u][j][1] + F[v][i][1] \times F[u][j][0] \]
我们所求的 \(\sum \prod a_i\) 便是 \(F[1][k + 1][1]\)。也就是用 \(F[1][k + 1][1] \times n ^ {(k + 1) - 2}\) 表示操作了 \(k\) 次,可能连上一条断开的边,树的形态个数。注意到,这样统计是会重复的。不妨这样考虑,我们做的是钦定 \(n - 1 - k\) 条边不会改变,其他边可变可不变的方案数。这是二项式反演经典「至少」类问题,即「至少」\(n - 1 - k\) 条边不改变的方案数。我们要求的是不能连回去,恰好改变 \(k\) 条边,也就是恰好 \(n - 1 - k\) 条边不变的方案。
设 \(f(n - 1 - x) = dp[1][x + 1][1] \times n ^ {(x + 1) - 2}\),即 \(f(x) = dp[1][n - x][1] \times n ^ {(n - x) - 2}\) 表示「至少」\(x\) 条边不变。
考虑怎么表示出 \(g(x)\) 表示「恰好」\(x\) 条边不变。\(g(x)\) 在 \(f(x)\) 的基础上,需要去掉大于 \(x\) 条边不变的情况。对于一个 \(t > x\),\(g(t)\) 反复贡献了 \(\dbinom{t}{x}\) 次。
移项得:
反演得到:
对于操作了 \(k\) 次,即为保证「恰好」\(n - 1 - k\) 条边不变,答案为 \(g(n - 1 - k)\)。总答案即为 \(\sum \limits _ {i = 0} ^ k g(n - 1 - i)\)。
当然,你也可以直接使用我在《学习笔记》提到的模型求解。
时间复杂度:\(\Theta(n ^ 2)\),瓶颈在于树形 DP 和反演。
树形 DP 时间复杂度 \(\Theta(n ^ 2)\) 说明:
考虑一对 \((u, v)\) 只会在 \(\operatorname{lca}\) 处合并,共有 \(\Theta(n^ 2)\) 个点对。
注意 \(k\) 需要和 \(n - 1\) 取 \(\min\)。
代码
点击查看直接套用模型的代码
F[0] = 1;
for (int i = 1, pw = 1; i <= n - 1; ++i)
F[i] = mul(f[1][i + 1][1], pw), pw = mul(pw, n);
int ans = 0;
for (int i = 0; i <= k; ++i) {
for (int j = 0; j <= i; ++j) {
if ((i - j) & 1)
G[i] = sub(G[i], mul(F[j], C(n - 1 - j, n - 1 - i)));
else
G[i] = add(G[i], mul(F[j], C(n - 1 - j, n - 1 - i)));
}
ans = add(ans, G[i]);
}
#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;
const int N = 5010;
const int mod = 998244353;
inline int add(int a, int b) { return a + b >= mod ? a + b - mod : a + b; }
inline int sub(int a, int b) { return a - b < 0 ? a - b + mod : a - b; }
inline int mul(int a, int b) { return 1ll * a * b % mod; }
inline void toadd(int& a, int b) { a = add(a, b); }
int n, k, fa[N];
int frac[N], ifrac[N], Inv[N];
inline int C(int n, int m) { return mul(frac[n], mul(ifrac[m], ifrac[n - m])); }
int f[N][N][2]; // j 个联通块,根所在的联通块有没有被选中
int siz[N];
int F[N], G[N];
signed main() {
#ifndef XuYueming
freopen("kaisou.in", "r", stdin);
freopen("kaisou.out", "w", stdout);
#endif
scanf("%d%d", &n, &k), k = min(k, n - 1);
for (int i = 2; i <= n; ++i) scanf("%d", &fa[i]), ++fa[i];
frac[0] = ifrac[0] = 1;
for (int i = 1; i <= n; ++i) {
frac[i] = mul(frac[i - 1], i);
Inv[i] = i == 1 ? 1 : mul(mod - mod / i, Inv[mod % i]);
ifrac[i] = mul(ifrac[i - 1], Inv[i]);
f[i][1][0] = f[i][1][1] = 1;
siz[i] = 1;
}
for (int u = n; u >= 1; --u) {
int v = fa[u];
static int g[N][2];
memcpy(g, f[v], sizeof(g));
memset(f[v], 0x00, sizeof(f[v]));
for (int i = 1; i <= siz[v]; ++i)
for (int j = 1; j <= siz[u]; ++j) {
toadd(f[v][i + j][0], mul(f[u][j][1], g[i][0]));
toadd(f[v][i + j][1], mul(f[u][j][1], g[i][1]));
toadd(f[v][i + j - 1][0], mul(f[u][j][0], g[i][0]));
toadd(f[v][i + j - 1][1], add(mul(f[u][j][1], g[i][0]), mul(f[u][j][0], g[i][1])));
}
siz[v] += siz[u];
}
F[n - 1] = 1;
for (int i = n - 2, p = 1; i >= 0; --i) F[i] = mul(p, f[1][n - i][1]), p = mul(p, n);
// p 即为 n^{(n-x)-2}
int ans = 0;
for (int i = n - 1; i >= n - 1 - k; --i) {
for (int j = i; j <= n - 1; ++j) {
if ((j - i) & 1)
G[i] = sub(G[i], mul(C(j, i), F[j]));
else
G[i] = add(G[i], mul(C(j, i), F[j]));
}
ans = add(ans, G[i]);
}
printf("%d", ans);
return 0;
}
本文作者:XuYueming,转载请注明原文链接:https://www.cnblogs.com/XuYueming/p/18399260。
若未作特殊说明,本作品采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。

浙公网安备 33010602011771号