题意:
给你一个
n
n
n个点的树,对于每一个非空点集
X
X
X,我们定义
f
(
X
)
f(X)
f(X)为点集是
X
X
X的最小连通子树的边数,再给你一个
k
k
k,你要对于所有点集,求出
(
f
(
X
)
)
k
(f(X))^k
(f(X))k之和。
n
<
=
1
e
5
,
k
<
=
200
n<=1e5,k<=200
n<=1e5,k<=200。
题解:
之前没做过这种套路的题,于是就不会做。
对于 x k x^k xk的答案的题,通常的做法是用第二类斯特林数展开或者用二项式定理展开,然后来考虑化简展开式,对展开式求值。
先讲一下这个展开的含义。我们设 S ( i , j ) S(i,j) S(i,j)表示第二类斯特林数,含义是 i i i个不同的球放到 j j j个相同的盒子里的方案数。那么我们有 n k = ∑ i = 0 k S ( k , i ) ∗ i ! ∗ C n i n^k=\sum_{i=0}^kS(k,i)*i!*C_{n}^{i} nk=i=0∑kS(k,i)∗i!∗Cni 从组合意义上来解释一下, n k n^k nk可以看作有 k k k个不同的球放到 n n n个不同的盒子里的方案数,等号右边是我们枚举有几个盒子是有球的,如果有 i i i个盒子有球,那么 k k k个不同的球放到 i i i个相同的盒子的方案数是 S ( k , i ) S(k,i) S(k,i),我们从 n n n个盒子中任意选取 i i i个,所以乘一个组合数,而盒子不同,所以还要乘一个 i ! i! i!。
我们考虑把这个组合意义的式子带到我们要求的东西里。
∑
X
(
f
(
X
)
)
k
=
∑
X
∑
i
=
0
k
C
f
(
X
)
i
∗
i
!
∗
S
(
k
,
i
)
\sum_{X}(f(X))^k=\sum_{X}\sum_{i=0}^kC_{f(X)}^{i}*i!*S(k,i)
X∑(f(X))k=X∑i=0∑kCf(X)i∗i!∗S(k,i)
=
∑
i
=
0
k
i
!
∗
S
(
k
,
i
)
∗
∑
X
C
f
(
X
)
i
=\sum_{i=0}^ki!*S(k,i)*\sum_{X}C_{f(X)}^{i}
=i=0∑ki!∗S(k,i)∗X∑Cf(X)i 前面的
i
!
i!
i!和
S
(
k
,
i
)
S(k,i)
S(k,i)都可以预处理出来,那么就是考虑后面这个
∑
X
C
f
(
X
)
i
\sum_{X}C_{f(X)}^{i}
∑XCf(X)i怎么求。我们考虑它的组合意义,其实可以看作每一种点集的最小连通生成树中选出
i
i
i条边的方案数之和。我们考虑用一个树形dp来算这个东西,我们强制要求这个生成树的根就是当前枚举到的所有点中深度最浅的,这样可以避免算重或者算漏。我们发现其实很类似一个树形背包,我们设
d
p
[
x
]
[
i
]
dp[x][i]
dp[x][i]表示以
x
x
x为根子树内的点集形成的所有生成树选了
i
i
i条边的方案数。你会发现这个转移比较类似一个树形背包。我这里的写法是,分三种情况,一种是所有的点都在以前的子树内,那么答案还是原来的答案,一种是点集全部在新来的子节点所在的子树内,那么就分选这个子节点到当前点的这条边和不选这条边两种情况直接加过来就行。另外就是原来已经有的一部分子树和新来的这个子树合并到一起,这个就类似一个树形背包了考虑两个子树各选了多少条边,并且考虑新来的子节点到
x
x
x的这条边选还是不选。这样就可以完成转移了,每次算完一个节点的时候顺边更新一个所有点集选
i
i
i条边的方案数之和的答案。有了这个所有点集的最小连通生成树选出
i
i
i条边的方案数之和我们就可以结合预处理出来的阶乘和第二类斯特林数求出答案了。
这个复杂度是 O ( n k ) O(nk) O(nk)的,并不是 O ( n k 2 ) O(nk^2) O(nk2)的,具体的证明的话大体是考虑合并的两个子树的size和 k k k的大小关系来分类讨论一下,发现每种情况的最终都是 O ( n k ) O(nk) O(nk)的。
代码:
#include <bits/stdc++.h>
using namespace std;
int n,k,hed[200010],cnt,fa[200010],sz[200010];
const long long mod=1e9+7;
long long dp[100010][210],s[210][210],jie[210],res[100010],ans,f[210];
struct node
{
int to,next;
}a[400010];
inline int read()
{
int x=0;
char s=getchar();
while(s>'9'||s<'0')
s=getchar();
while(s>='0'&&s<='9')
{
x=x*10+s-'0';
s=getchar();
}
return x;
}
inline void add(int from,int to)
{
a[++cnt].to=to;
a[cnt].next=hed[from];
hed[from]=cnt;
}
inline void dfs(int x)
{
sz[x]=1;
dp[x][0]=1;
for(int i=hed[x];i;i=a[i].next)
{
int y=a[i].to;
if(y==fa[x])
continue;
fa[y]=x;
dfs(y);
for(int j=0;j<=k;++j)
f[j]=dp[x][j];
for(int j=1;j<=min(k,sz[y]);++j)//只算新来的子树
dp[x][j]=(dp[x][j]+dp[y][j]+dp[y][j-1]);//可以选这条边也可以不选
dp[x][0]=(dp[x][0]+dp[y][0])%mod;
for(int j=0;j<=min(k,sz[x]);++j)//两个子树合并
{
for(int l=0;l<=min(k-j,sz[y]);++l)//可以选这条边也可以不选
{
dp[x][j+l]=(dp[x][j+l]+f[j]*dp[y][l]%mod)%mod;
res[j+l]=(res[j+l]+f[j]*dp[y][l]%mod)%mod;
dp[x][j+l+1]=(dp[x][j+l+1]+f[j]*dp[y][l]%mod)%mod;
res[j+l+1]=(res[j+l+1]+f[j]*dp[y][l]%mod)%mod;
}
}
sz[x]+=sz[y];
}
}
int main()
{
n=read();
k=read();
for(int i=1;i<=n-1;++i)
{
int x=read(),y=read();
add(x,y);
add(y,x);
}
jie[0]=1;
for(int i=1;i<=k;++i)
jie[i]=jie[i-1]*i%mod;
s[1][1]=1;
for(int i=2;i<=k;++i)
{
for(int j=1;j<=k;++j)
s[i][j]=(s[i-1][j-1]+s[i-1][j]*j)%mod;
}
dfs(1);
for(int i=1;i<=k;++i)
ans=(ans+jie[i]*s[k][i]%mod*res[i]%mod)%mod;
printf("%lld\n",ans);
return 0;
}