【洛谷】P8352 [SDOI/SXOI2022] 小 N 的独立集(dp套dp)
题意
给定一棵 \(n\) 个点的树,每个点的权值在 \([1,k]\) 之间,对于 \(\forall i \in [1,kn]\),求出有多少种权值分配方案,使得树的最大权独立集大小为 \(i\)。
\(n \leq 1000,k \leq 5\)。
思路
不难想到一种很暴力的方式,即枚举每个点的权值,然后树形 dp 求出最大权独立集。即,设 \(f_{u,0/1}\) 表示在子树 \(u\) 中不选/选 \(u\) 时的最大权独立集。时间复杂度为 \(O(nk^n)\)。
在本题中,注意到点权的范围实际上很小,一种理想的状态是令 \(g_{u,v}\) 为 \(u\) 子树内最大权独立集大小为 \(v\) 的方案数。而在转移中,由于需要对当前点 \(u\) 是否被选取在独立集中分类讨论,那么可以进一步想到令 \(g_{u,v_0,v_1}\) 表示在 \(u\) 子树中不选择 \(u\) 节点时最大权独立集大小为 \(v_0\),选择 \(u\) 节点时最大权独立集大小为 \(v_1\) 的方案数。枚举当前儿子 \(v\) 的时候,令 \(p\) 和 \(q\) 分别表示不选/选 \(v\) 节点的最大权独立集大小,可以得到状态转移方程:
不难发现这里的 \(v_0\) 和 \(v_1\) 实质上就是之前提到的 \(f_{u,0}\) 和 \(f_{u,1}\)。这种内层 dp 结果作为外层 dp 的状态的小 trick 就被称为 dp 套 dp。这样做的时间复杂度为 \(O(n^3k^4)\)。还需要考虑进一步优化。
注意到 \(f_{u,0}\) 和 \(f_{u,1}\) 的差值实际上不会很大,考虑把这个差值的范围确定下来。可以重新定义 \(f_{v,0}\) 和 \(f_{v,1}\) 的为是否强制不选择 \(u\) 节点时的最大权独立集大小,那么可以得到不等式 \(0 \leq f_{u,0}-f_{u,1} \leq a_u \leq k\)。于是就可以重新定义 \(g_{u,v_1,d}\) 表示 \(f_{u,0}=v_1+d,f_{u,1}=v_1\) 时的方案数。
由于状态重新定义,转移方程也发生了一定的变化。在枚举 \(f_{v,v_1,d}\) 时,同样令 \(p=v_1,q=d\),那么可以得到 \(v_1'=v_1+\max(p,p+q)=v_1+p+q\),\(v_0'=\max(v_1+p+q,v_1+d+p)\),于是就有 \(d'=v_0'-v_1'=\max(v_1+p+q,v_1+d+p)-v_1+p+q\),得到转移方程:
此时的时间复杂度就降低到了 \(O(n^2k^4)\),可以通过本题。
code
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1010,M=N*5,mod=1e9+7;
int tmp[M][6],f[N][M][6],siz[N],n,k,h[N],idx;
struct edge{int v,nex;}e[M];
void add(int u,int v){e[++idx]=edge{v,h[u]};h[u]=idx;}
void Add(int &a,int b){a+=b;if(a>=mod) a-=mod;}
void dfs(int u,int fa)
{
siz[u]=1;
for(int i=1;i<=k;i++) f[u][0][i]=1;
for(int i=h[u];i;i=e[i].nex)
{
int v=e[i].v;if(v==fa) continue;dfs(v,u);memset(tmp,0,sizeof(tmp));
for(int i=0;i<=k*siz[u];i++)
for(int j=0;j<=k;j++)
{
if(!f[u][i][j]) continue;
for(int p=0;p<=k*siz[v];p++)
for(int q=0;q<=k;q++)
Add(tmp[i+p+q][max(i+j+p,i+p+q)-(i+p+q)],1ll*f[u][i][j]*f[v][p][q]%mod);
}
memcpy(f[u],tmp,sizeof(tmp));
siz[u]+=siz[v];
}
}
int main()
{
scanf("%d%d",&n,&k);for(int u,v,i=1;i<n;i++) scanf("%d%d",&u,&v),add(u,v),add(v,u);dfs(1,0);
for(int i=1;i<=k*n;i++)
{
int ans=0;
for(int d=0;d<=min(i,k);d++) Add(ans,f[1][i-d][d]);
printf("%d\n",ans);
}
return 0;
}