P4516 [JSOI2018] 潜入行动 题解【树上动态规划入门】
问题简述
现有一个\(n\)个节点的树。你要在这个树上的恰好 \(k\) 个节点打上标记。
问有多少种不同的方案,使得对于任意一个点 \(u\) ,总有与 \(u\) 之间相邻的点且该点被打上标记。
输出答案对 \(10^9+7\) 取模。
\(n \in [1,10^5], k\in [1,100]\)
思考
树上计数问题,考虑动态规划。
由于存在限制条件:
使得对于任意一个点 \(u\) ,总有与 \(u\) 之间相邻的点且该点被打上标记。
所以,我们称一个点存在相连的被标记点为这个点被覆盖。反之。
考虑 \(dp\) 结构
很显然,我们的动态规划数组 \(dp\) 至少有两个维度
-
\(u\) 代表这是那个子树,\(u\) 为这个子树的根节点
-
\(i\) 代表在这个子树内,放了多少个标记
即 \(dp[u][i]\)
考虑如何转移。
注意到,我们在当前子树的根节点是否打上标记,取决于是否有子结点还没有被覆盖。因为诺如果有子结点还没有被覆盖,那么我们这个位置必须打上标记,所以我们要记录每个节点是否已经被覆盖
同时,根节点是否被覆盖,取决于是否有子结点被标记。
由此可得,我们需要增加两个 \(dp\) 维度
-
子树的根节点 \(u\) 是否被覆盖
-
子树的根节点 \(u\) 是否打上标记。
由此可得我们最终的 \(dp\) 状态
考虑如何转移
很显然,每个子树/结点的 \(dp\) 状态由其多个子树的状态合并。所以我们要从一个没有子树加入的初始状态,一步步扩展到所有子树均加入的最终状态。说白了就是背包dp
我们考虑初始状态。初始时,没有任何子树的加入。所以:
dp[u][1][1][0]=1;
dp[u][0][0][0]=1;
我们尝试之间在\(dp\)数组上跑背包,但是我们发现一个问题。如果只使用\(dp\)数组,那么我们在转移时,左值就同时有之前子树的计算结果和当前子树的目前计算结果。这导致重复的计算。
所以,对于每个子树\(v\),我们要先建立一个辅助动态规划数组 \(f\) , 然后利用现有的 \(dp[v],dp[u]\) 来计算 \(f\) 的状态,然后再将 \(f\) 的内容拷贝到 \(dp[u]\) 中
具体的转移式子自己推,没什么难得。
代码
#include <bits/stdc++.h>
using namespace std;
typedef unsigned int ll;
const ll inf=1e18;
const ll mod=1e9+7;
const ll maxn=1e5+5;
const ll maxk=105;
inline ll read() {
long long x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = x * 10 + (ch - '0');
ch = getchar();
}
return x * f;
}
struct Edge{
ll v,nxt;
};
ll f[maxk][2][2]; //f[i][j]代表不
Edge e[maxn<<1];
ll fa[maxn],sz[maxn];
ll head[maxn];
ll dp[maxn][maxk][2][2]; //在以i为根的子树,放了j个,i放没放,i有没有被覆盖
ll n,k,etot;
void add_edge(ll u,ll v) {
++etot;
e[etot]={v,head[u]};
head[u]=etot;
return;
}
void dfs(ll u,ll father) {
sz[u]=1;
fa[u]=father;
for(ll i=head[u];i;i=e[i].nxt) {
if(e[i].v==fa[u]) {
continue;
}
dfs(e[i].v,u);
sz[u]+=sz[e[i].v];
}
return;
}
void work(ll u) {
dp[u][1][1][0]=1;
dp[u][0][0][0]=1;
if(sz[u]==1) {
return;
}
memset(f,0,sizeof(f));
ll szsum=1;
for(ll i=head[u];i;i=e[i].nxt) {
if(e[i].v==fa[u]) {
continue;
}
ll v=e[i].v;
work(v);
//这个节点放or不放
//memset(f,0,sizeof(f));
ll oldsz=szsum;
szsum+=sz[v];
for(ll i=0;i<=min(k,szsum);++i) {
f[i][0][0]=f[i][0][1]=f[i][1][0]=f[i][1][1]=0;
}
for(ll j=0;j<=min(k,oldsz);++j) {
//总共k。
for(ll h=0;h<=min(k-j,sz[v]);++h) {
//这个节点放
//这个节点不放
//不放 覆盖
//v必须被覆盖
f[j+h][0][1]+=1LL * dp[v][h][0][1]*dp[u][j][0][1]%mod;
//f[j][0][1]%=mod;
f[j+h][0][1]+=(1LL * dp[v][h][1][1]*dp[u][j][0][1]%mod+1LL * dp[v][h][1][1]*dp[u][j][0][0]%mod)%mod;
f[j+h][0][1]%=mod;
//不放 不覆盖
f[j+h][0][0]+=1LL * dp[v][h][0][1]*dp[u][j][0][0]%mod;
f[j+h][0][0]%=mod;
//u可以放
//第一个0/1代表放没放 第二个代表是否被覆盖
//放且覆盖
f[j+h][1][1]+=1LL * dp[v][h][1][1]*dp[u][j][1][0]%mod;
//f[j][1][1]%=mod;
f[j+h][1][1]+=1LL * dp[v][h][1][1]*dp[u][j][1][1]%mod;
f[j+h][1][1]%=mod;
f[j+h][1][1]+=1LL * dp[v][h][1][0]*dp[u][j][1][0]%mod;
//f[j][1][1]%=mod;
f[j+h][1][1]+=1LL * dp[v][h][1][0]*dp[u][j][1][1]%mod;
f[j+h][1][1]%=mod;
f[j+h][1][1]+=1LL * dp[v][h][0][1]*dp[u][j][1][1]%mod;
//f[j][1][1]%=mod;
f[j+h][1][1]+=1LL * dp[v][h][0][0]*dp[u][j][1][1]%mod;
f[j+h][1][1]%=mod;
//放但是不覆盖
f[j+h][1][0]+=1LL * dp[v][h][0][0]*dp[u][j][1][0]%mod;
//f[j][1][0]%=mod;
f[j+h][1][0]+=1LL * dp[v][h][0][1]*dp[u][j][1][0]%mod;
f[j+h][1][0]%=mod;
}
}
for(ll j=0;j<=min(k,szsum);j++)
{
dp[u][j][0][0]=f[j][0][0];
dp[u][j][0][1]=f[j][0][1];
dp[u][j][1][0]=f[j][1][0];
dp[u][j][1][1]=f[j][1][1];
}
}
return;
}
int main() {
//freopen("input.txt","r",stdin);
n=read();k=read();
for(ll i=1;i<n;++i) {
ll u,v;
u=read();v=read();
add_edge(u,v);
add_edge(v,u);
}
dfs(1,0);
work(1);
printf("%lld\n",(dp[1][k][0][1]+dp[1][k][1][1])%mod);
return 0;
}
实现细节
这题的数据很恶心,而且实现细节非常多。
咕咕咕,太晚了,先放着。明天继续写

浙公网安备 33010602011771号