Luogu P4815 [CCO 2014] 狼人游戏 题解 [ 蓝 ] [ 树形背包 DP ] [ 计数 ]
狼人游戏:简单树形背包 DP,不懂为啥 \(n\le 200\),明明这题可以直接开到 \(n\le 5000\) 的。
观察题目给的特殊性质,把指认和给金水的操作转化为有向图,不难发现可以有如下性质:
- 该有向图无环。
- 每个点的入度最多为 \(1\)。
- 平民的出边没有任何作用,狼人的出边可以确定其他人的身份。
根据前两条性质,可以发现图的形态一定是一片外向树森林。
因为狼人恰好为 \(w\) 个,而父节点的身份决定了子节点可能的身份,于是设计树形背包 \(dp_{i,j,0/1}\) 表示考虑到节点 \(i\) 时,子树内出了 \(j\) 个狼人,且 \(i\) 为平民 / 狼人的总方案数。转移利用乘法原理:
- \(dp_{i,j,0}=\sum dp_{i,j-k,0}\times (dp_{v,k,0}+dp_{v,k,0})\)。
- \(dp_{i,j,1}=\sum dp_{i,j-k,1}\times dp_{v,k,type_v}\)。
其中 \(type_v\) 表示 \(i\) 确定为狼人后 \(v\) 的身份。
直接枚举 \(j,k\) 转移是 \(O(n^3)\) 的,考虑树形背包的常见优化,控制 \(j,k\) 的上下界,根据子树大小动态调整枚举范围,让每个点对都在 LCA 处合并即可做到 \(O(n^2)\) 的复杂度。具体上下界见代码。另外注意背包的转移限制,为了防止转移的变量被提前更新,每次计算 DP 值的时候可以开一个辅助变量进行统计。
注意,由于图是森林,所以可以建一个虚拟节点连接所有的树,答案即为 \(dp_{root,w,0}\),因为虚拟节点为平民时不会对原树的根有任何限制。
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi=pair<int,int>;
const ll mod=1000000007;
const int N=205;
int n,w,m,tp[N],rd[N],sz[N];
ll dp[N][N][2];
vector<int>g[N];
void dfs(int u)
{
sz[u]=1;dp[u][0][0]=dp[u][1][1]=1;
for(auto v:g[u])
{
dfs(v);
sz[u]+=sz[v];
for(int i=min(sz[u],w);i>=0;i--)
{
ll v0=0,v1=0;
for(int j=max(0,i-min(sz[u]-sz[v],w));j<=min(sz[v],i);j++)
{
v0=(v0+dp[u][i-j][0]*(dp[v][j][0]+dp[v][j][1]%mod)%mod)%mod;
v1=(v1+dp[u][i-j][1]*dp[v][j][tp[v]]%mod)%mod;
}
dp[u][i][0]=v0;
dp[u][i][1]=v1;
}
}
}
int main()
{
//freopen("sample.in","r",stdin);
//freopen("sample.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>w>>m;
while(m--)
{
int u,v;char c;
cin>>c>>u>>v;
if(c=='A')tp[v]=0;
else tp[v]=1;
g[u].push_back(v);
rd[v]++;
}
for(int i=1;i<=n;i++)
if(rd[i]==0)
g[n+1].push_back(i);
dfs(n+1);
cout<<dp[n+1][w][0];
return 0;
}

浙公网安备 33010602011771号