P4516 [JSOI2018] 潜入行动 题解
P4516 [JSOI2018] 潜入行动
给定一棵无根树,要求给树上 \(k\) 个点标记,使得所有点都至少与一个被标记的点相邻。(注意自己被标记不代表与标记相邻)
思路
考虑树形DP。
套路地设 \(f_{u,i,0/1,0/1}\) 表示树上的点 \(u\) 及其子树内已经被标记了 \(i\) 个点,自己有没有被标记、自己有没有与标记相邻。
然后考虑转移。发现对于一棵子树的根 \(u\) 和 其某个儿子 \(v\),如果要转移那 \(u\) 本身就已经与标记或者 \(v\) 被标记或者二者兼而有之。
然后就对应转移即可。
复杂度的证明有些神秘,事实上这个东西与分块是类似的。我们将子树大小小于 \(k\) 的树叫做小块,大于等于 \(k\) 的叫做大块。
- 如果是一些大块合并(当然也可以间杂一些小块)成为一个更大的块,由于我么枚举的上限是 \(k\) ,因此大块间的合并一次是 \(k^2\) 的,但是大块间的合并是不超过 \(\frac{n}{k}\) 次的,因此大块间合并的总复杂度为 \(O(nk)\)。
- 如果是一些小块(其中没有大块)合并成为了大块,那么与1类似的分析,这种小块合成大块的出现次数也小于等于 \(\frac{n}{k}\)。
- 如果是小块与小块合并后还是小块,那这种情况的总时间复杂度就是 \(O(nk)\)。
然后就可以了。
code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll p=1e9+7;
const int N=1e5+7,M=105;
int n,k,tot[N],f[N][M][2][2],siz[N];
vector <int> q[N];
void dfs(int u,int fa){
ll tmp[M][2][2];memset(tmp,0,sizeof(tmp));siz[u]=1;
f[u][0][0][0]=f[u][1][1][0]=1;
for(int i=0;i<tot[u];i++){
int v=q[u][i];if(v==fa) continue;
dfs(v,u);
for(int a=0;a<=min(k,siz[u]);a++)
for(int b=0;b<=min(k,siz[v])&&a+b<=k;b++){
for(int p1=0;p1<2;p1++)for(int q1=0;q1<2;q1++)
for(int p2=0;p2<2;p2++)for(int q2=0;q2<2;q2++)
if(q2|p1)
tmp[a+b][p1][q1|p2]=(tmp[a+b][p1][q1|p2]+1ll*f[u][a][p1][q1]*f[v][b][p2][q2]%p)%p;
}
siz[u]+=siz[v];
for(int j=0;j<=k;j++) for(int p1=0;p1<2;p1++) for(int q1=0;q1<2;q1++) f[u][j][p1][q1]=tmp[j][p1][q1],tmp[j][p1][q1]=0;
}
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>k;
for(int i=1,u,v;i<=n-1;i++){cin>>u>>v;q[u].push_back(v),tot[u]++;q[v].push_back(u),tot[v]++;}
dfs(1,0);
cout<<(f[1][k][1][1]+f[1][k][0][1])%p;
return 0;
}

浙公网安备 33010602011771号