NC20811 蓝魔法师 (树形DP/树上01背包)
NC20811 蓝魔法师 (树形DP/树上01背包)
题目:
给出一棵树,求有多少种删边方案,使得删后的图每个连通块大小小于等于k,两种方案不同当且仅当存在一条边在一个方案中被删除,而在另一个方案中未被删除,答案对998244353取模 。
题解:
没想到是树形DP题,做题太少了 。这类树上选一个子图/连通块,给一些限制问方案数/子图权值最大值一般可以先想想树形DP。
这道题就可以在处理u点时,把u看作一个树上连通块的根,它的大小来自选了多少子节点为根的联通块。所以相当于对u的每个子节点选与不选的考虑,来构成u为根一定大小的连通块,可以用树上01背包来做。
\(dp[u][j]\) 表示u为根的大小为 j 的连通块方案数。显然对每个子节点 v 有以选与不选两种更新情况。
-
选v:
\[dp[u][j] = dp[u][j-x]*dp[v][x] \] -
不选v: v的所有情况与u独立,直接乘上sum就行。
\[dp[u][j]=dp[u][j]*sum \\ 其中sum=\sum_{i=1}^{min(k,siz[v])}dp[v][i] \]
最后 \(res = \sum_{i=1}^{k}dp[1][i]\)
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<map>
#include<queue>
#include<vector>
#include<string>
#include<fstream>
using namespace std;
#define rep(i, a, n) for(int i = a; i <= n; ++ i)
#define per(i, a, n) for(int i = n; i >= a; -- i)
typedef long long ll;
const int N = 2e3 + 15;
const int mod = 998244353;
const double Pi = acos(- 1.0);
const int INF = 0x3f3f3f3f;
const int G = 3, Gi = 332748118;
ll qpow(ll a, ll b) { ll res = 1; while(b){ if(b & 1) res = (res * a) % mod; a = (a * a) % mod; b >>= 1;} return res; }
ll gcd(ll a, ll b) { return b ? gcd(b, a % b) : a; }
//
int n, k;
int dp[N][N], siz[N];
vector<int> g[N];
void dfs(int u, int pre){
siz[u] = 1; dp[u][1] = 1;
for(int i = 0; i < g[u].size(); ++ i){
int v = g[u][i];
if(v == pre) continue;
dfs(v, u);
int sum = 0;
for(int j = 1; j <= min(k, siz[v]); ++ j) sum = (sum + dp[v][j]) % mod;
for(int j = min(k, siz[u]); j >= 1; -- j){
for(int z = min(k, siz[v]); z >= 1; -- z){
if(j + z <= k)
dp[u][j + z] = (dp[u][j + z] + 1ll * dp[u][j] * dp[v][z]) % mod;
}
dp[u][j] = (1ll * dp[u][j] * sum) % mod;
}
siz[u] += siz[v];
}
}
int main()
{
scanf("%d%d",&n,&k);
for(int i = 1; i < n; ++ i){
int x, y; scanf("%d%d",&x,&y);
g[x].push_back(y); g[y].push_back(x);
}
dfs(1, 0);
int res = 0;
for(int i = 1; i <= k; ++ i) res = (res + dp[1][i]) % mod;
printf("%d\n",res);
return 0;
}