@codeforces - 1097G@ Vladislav and a Great Legend
@description@
给定一棵 n 个点的树 T。对于每一个非空点集 X,定义 f(X) 为包含 X 内所有点的最小连通块的边数。
另给定一正整数 k,求:
模 10^9 + 7。
Input
第一行两个整数 n 与 k (2≤n≤10^5, 1≤k≤200),表示点数及指数。
接下来 n 行每行两个整数 ai, bi,描述一条树边 (1≤ai, bi≤n)。
Output
输出一个整数,表示结果 mod 10^9 + 7。
Examples
Input
4 1
1 2
2 3
2 4
Output
21
Input
4 2
1 2
2 3
2 4
Output
45
Input
5 3
1 2
2 3
3 4
4 5
Output
780
@solution@
如果以平常的思路,我们可以对于每一个 i 统计 f(X) = i 的个数。
但是我只能想到 O(n^2) 的做法,没办法进一步优化。
当 k = 1 时,我们可以想到对于每一条边,统计有多少 X 使得 X 对应的连通块包含这条边。这是很容易办到的。
当 k ≠ 1 时,注意到 k 的范围依然较小 (k <= 200) ,我们为什么不类比拓展一下:对于每个大小为 k 的边集,统计有多少 X 使得 X 对应的连通块包含这条边。
看起来好像不能够这样轻易类比。
我们考虑一个常见的套路,即斯特林反演:
其中 S 表示第二类斯特林数。这个证明网上挺多的,不再赘述。
注意到当 i > k 时 S(k, i) = 0,所以又有 \(m^k = \sum_{i=0}^{m}C(m, i)*S(k, i)*i!\)。
于是我们改写原式:
我们枚举 i,则 S(k, i)*i! 可以直接算。
后面那一堆的组合意义实际上是 “从树上选出一个大小为 i 的边集,有多少点集 X 对应的连通块包含这个边集”。
我们就实现了我们最初的目的。
我们可以通过树形 dp 来统计那个东西。
对于每个点集 X,我们在 X 内所有点的 LCA 处统计这个 X 的相关信息。
因此为了方便转移,我们定义 f[i][j] 表示以 i 为根的子树中,点集 X 的 LCA 高于或等于 i,已经选中 j 条边的方案数。
统计 LCA 为 i 时选出 j 条边的方案时,如果新加进来一个子树 p,以前的子树与根 i 构成连通块为 q,则新增的方案 del 等于 p 中、 q 中、p 与根 i 之间的连边选中共 j 条边的方案。
讨论一下就可以转移了。
f 的转移与上面大致相似,不过需要注意的是,f 可能会不选 p 中的点(即对应在 p 中选个空集)。需要单独拿出来讨论。
需要注意的是,为了不让时间复杂度退化成 O(nk^2),我们不能直接枚举到 k,而应该枚举到 min(size, k),其中 size 是子树的大小。这样复杂度是 O(nk)。
这个其实是一般的树形 dp 的一个优化的套路。不过一般的树形 dp 的证明只需要知道每个点对只会在 LCA 处统计即可得证,而这个证明要麻烦一些。
(以下转载自这一篇博客)。
首先考虑若干个 <k 的子树进行合并直到合并为一个 ≥k 的子树,代价是O(k^2)的,最多合并成n/k个这样的,于是复杂度是O(nk)。
再考虑<k的与≥k的合并,复杂度是O(size∗k)的,那么由于每一点最多经历一次这个过程,复杂度也是O(nk)。
最后是两个≥k的子树合并,由于只有最多n/k个这样的子树因此操作次数不超过nk,因此复杂度是O(nk)。
于是最终的复杂度是O(nk),降低复杂度的关键是合并的代价可以和子树大小取min。
@accepted code@
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXN = 100000;
const int MAXK = 200;
const int MOD = int(1E9) + 7;
inline int add(int a, int b) {return (a + b)%MOD;}
inline int mul(int a, int b) {return 1LL*a*b%MOD;}
struct edge{
int to; edge *nxt;
}edges[2*MAXN + 5], *adj[MAXN + 5], *ecnt = &edges[0];
void addedge(int u, int v) {
edge *p = (++ecnt);
p->to = v, p->nxt = adj[u], adj[u] = p;
p = (++ecnt);
p->to = u, p->nxt = adj[v], adj[v] = p;
}
int n, k;
int res[MAXK + 5], fct[MAXK + 5], S[MAXK + 5][MAXK + 5];
int func() {
int ans = 0;
for(int i=0;i<=k;i++)
ans = add(ans, mul(mul(S[k][i], fct[i]), res[i]));
return ans;
}
void init() {
S[0][0] = 1;
for(int i=1;i<=k;i++) {
S[i][0] = 0;
for(int j=1;j<=i;j++)
S[i][j] = add(S[i-1][j-1], mul(S[i-1][j], j));
}
fct[0] = 1;
for(int i=1;i<=k;i++)
fct[i] = mul(fct[i-1], i);
}
int f[MAXK + 5][MAXN + 5], siz[MAXN + 5];
void dfs(int x, int fa) {
f[0][x] = 1, res[0] = add(res[0], 1), siz[x] = 1;
for(edge *p=adj[x];p;p=p->nxt) {
if( p->to == fa ) continue;
dfs(p->to, x);
int t1 = min(siz[p->to] - 1, k), t2 = min(siz[x] - 1, k), t3 = min(siz[p->to] + siz[x] - 1, k);
for(int i=t2;i>=0;i--)
for(int j=min(t3-i, t1);j>=0;j--) {
int del = mul(f[i][x], f[j][p->to]);
res[i+j] = add(res[i+j], del), f[i+j][x] = add(f[i+j][x], del);
if( i+j+1 <= t3 )
res[i+j+1] = add(res[i+j+1], del), f[i+j+1][x] = add(f[i+j+1][x], del);
}
for(int i=0;i<=t1;i++) {
f[i][x] = add(f[i][x], f[i][p->to]);
if( i + 1 <= t3 )
f[i + 1][x] = add(f[i + 1][x], f[i][p->to]);
}
siz[x] += siz[p->to];
}
}
int main() {
scanf("%d%d", &n, &k), init();
for(int i=1;i<n;i++) {
int a, b; scanf("%d%d", &a, &b);
addedge(a, b);
}
dfs(1, 0);
printf("%d\n", func());
}
@details@
所以树形 dp 往往是听上去容易,代码细节很多的类型。
看代码要容易理解一些吧。。。