CF917D Stranger Trees
复盘 \(\color{black}{\text{n}}\color{red}{\text{ealchen}}\) 神仙讲的数数题。
Warning:如果你做过「WC2019」数树 的话你可以尝试先不看题解,直接去想 \(O(n ^ 2)\) 的做法。
这题还算是弱化版的呢(
UPDATE:“不少于”仅是便于理解,更严谨的说法就是“钦定”,因为二项式反演内存在系数。其他比如子集反演就是真正意义上的“不少于”。
(感谢 \(\color{black}{\text{C}}\color{red}{\text{onstant}}\) 的纠正)
Description
给定一棵有 \(n\) 个点的树,对于所有 \(k \in [0, n - 1]\) ,求有 \(k\) 条边和模板树重合的树有多少个。
答案对 \(10 ^ 9 + 7\) 取模。
原题:\(n \leq 100\) 。
加强版:\(n\leq 5000\) 。
Analysis
我是不会告诉你我连 \(O(n ^ 4)\) 都不会做的(
大概思路就是利用 矩阵树 ,因为考虑到一般的 矩阵树 处理不了恰好 \(k\) 条边的类型。
所以我们可以利用类似 子集卷积 的思维,我们单独令树边的权值带上未知数 \(x\) 。
那么最终只需要在 矩阵树 上找 \(x ^ k\) 的地方就行了。
(大概是 多项式插值+矩阵树 之类的牛逼算法,我显然不会)
显然还不够精髓,看起来有点科技暴力(
那么假如我们已经钦定好 \(k\) 条边重合,根据树的性质,就还会剩下 \(n - k - 2\) 个联通块。
一个常用 trick:对于这样的森林,假定它们的大小是 \(siz_i\) ,它存在的树的方案数:
(证明是用 矩阵树 或者 \(\text{Prufer}\) 序列,不会但是挺好记的)
那么其实对于有 \(k\) 条边重合的情况,需要知道的就是所有可能的 \(siz\) 。
复杂度直接上天。
考虑 DP。
Solution
(已经会和「WC2019」数树 sub2 大量相同了)
我们发现 DP 是能结合大量状态,但是却难以记录恰好 \(k\) 条边的限制。
有个比较自然的想法,恰好 \(k\) 条边不太行,但是改成钦定 \(k\) 条边呢?
这很二项式反演,令“恰好”是 \(f_k\) ,“钦定”是 \(g_k\) :
于是我们设 \(dp_{u, i, k}\) 表示在 \(u\) 为根的子树中, \(u\) 所在联通块大小为 \(i\) ,且钦定了 \(k\) 条边重合的答案。(因为其他联通块还有重合所以即为钦定 \(k\) 条边)
那么对于树上的每一条边 \((u, v)\) ,我们考虑选或者不选:
-
选:\(dp_{u, i + j, k + l + 1} = dp_{u, i, k} \cdot dp_{v, j, l}\)
-
不选:\(dp_{u, i, k + l} = dp_{u, i, k} \cdot dp_{v, j, l} \cdot l\)
因为枚举 \(u\) 和 \(v\) 的子树最终只是 \(O(n ^ 2)\) 的。
然后总共是 \(O(n ^ 4)\) 的。
然后一通观察发现转移性是比较单一,多项式插值可以减掉一个 \(n\) ,但是我们并不满足。
考虑 trick 中 \(\prod_{i} a_i\) 组合意义,大概就是每个联通块选择一个“特殊点”的总方案数。
那其实对于当前联通快,我们并不关心其他地方究竟选了那些“特殊点”,只关心目前的联通块选了“特殊点”没有。
所以可以把联通快那一维替换成 \(0/1\) 。
注意:
-
这样的话假如边 \((u, v)\) 要合并,两个联通块不能同时选过“特殊点”的。(可以理解为“加量”)
-
假如不合并,必须要求 \(v\) 所在的联通快已经选过“特殊点”了(可以理解为“结算”)
直接 \(O(4)\) 转移,总时间就变到 \(O(n ^ 2)\) 的了。
Code
Code
/*
*/
#include
using namespace std;
#define File(a) freopen(a".in", "r", stdin), freopen(a".out", "w", stdout);
#define Check(a) freopen(a".in", "r", stdin), freopen(a".ans", "w", stdout);
typedef long long ll;
typedef unsigned long long ull;
typedef std::pair pii;
#define fi first
#define se second
#define mp std::make_pair
const int mod = 1e9 + 7;
template
inline int M(A x) {return x;}
template
inline int M(A x, B ... args) {return 1ll * x * M(args...) % mod;}
const int N = 5e3 + 10;
int n, 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 f[N][N][2], g[N][N], si[N];
inline void dfs(int u, int fa) {
f[u][0][0] = f[u][0][1] = si[u] = 1;
for (int i = fst[u], v; i; i = e[i].nxt) {
v = e[i].to;
if (v == fa) continue;
dfs(v, u);
for (int j = 0; j <= si[u]; ++j) {
for (int k = 0; k <= n - si[u] && k <= si[v]; ++k) {
for (int x = 0; x < 2; ++x) {
for (int y = 0; x + y < 2; ++y) {
g[j + k + 1][x | y] += M(f[u][j][x], f[v][k][y]);
(g[j + k + 1][x | y] >= mod) && (g[j + k + 1][x | y] -= mod);
}
g[j + k][x] += M(f[u][j][x], f[v][k][1]);
(g[j + k][x] >= mod) && (g[j + k][x] -= mod);
}
}
}
si[u] += si[v];
for (int j = 0; j <= si[u]; ++j) {
for (int k = 0; k < 2; ++k) {
f[u][j][k] = g[j][k]; g[j][k] = 0;
}
}
}
}
int ret[N], res = 1, C[N][N];
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cin >> n;
for (int i = 1, u, v; i < n; ++i) {
std::cin >> u >> v; add(u, v);
}
dfs(1, 0); ret[n - 1] = 1;
for (int i = n - 2; ~i; --i) {
ret[i] = M(res, f[1][i][1]); res = M(res, n);
}
for (int i = 0; i < n; ++i) {
int ans = 0;
for (int j = i; j <= n; ++j) {
if (!i) C[j][i] = 1;
else {
C[j][i] = C[j - 1][i - 1] + C[j - 1][i];
(C[j][i] >= mod) && (C[j][i] -= mod);
}
res = M(C[j][i], ret[j]);
if ((j - i) & 1) ans += mod - res; else ans += res;
(ans >= mod) && (ans -= mod);
}
std::cout << ans << " ";
}
return 0;
}