树上DP
模版题重建道路
https://www.luogu.com.cn/problem/P1272
题意:
有n个点,n-1条边 , 求最少去掉多少条边会使使一颗含有P个点的子树和其他节点分离。
分析:
通过读题我们可以知道这个一颗树,含有根节点,无环,那么我们可以从上往下或从下往上的来思考。
我们可以维护一个二维数组dp [i] [j] ,i表示子树的根节点,j表示子树的节点数量, dp [i] [j] 表示获得节点数量为j且根节点为i的子树所需要删除的边的个数。
对于一个子树的根节点s,该子树含有n个点,我们用c(vi)表示在以s的子节点vi为根的子树内保留的点的数量。
那么有
n
=
∑
c
(
v
i
)
+
1
-
-
-
-
-
-
-
-
-
-
-
1
n = \sum c(v_i)+1 -----------1
n=∑c(vi)+1 -----------1
后面的加1是因为要包括根节点。
同时我们用d(vi) 表示在以s的子节点vi为根的子树内删除的边的数量
那么有
d
p
[
s
]
[
n
]
=
∑
d
(
v
i
)
-
-
-
-
-
-
-
-
2
dp[s][n] = \sum d(v_i) --------2
dp[s][n]=∑d(vi) --------2
因此,我们要做的就是在子树的根节点s上找到一种方式在满足1式的同时使2式最小。
这个就类似与背包问题。装满一定容量的背包使价值最大。那么类比1个背包的n个物件,这里有n个节点,也就是n颗子树,每个子树n_i含有k个子节点。
那么背包问题是 dp[i] [j]
同理以第n_i个节点为根的子树来说有 dp[n_i] [i] [j] , n_i表示根节点的编号,i表示这个根节点的第i个子树,j表示以n_i为节点的子树在第i个节点的子树上保留了j个点,dp[n_i] [i] [j]表示删除了多少条边。
为了和上面的dp [i] [j]保持一致,这里统一用 dp [i] [k] [j] , i 表示根节点的编号, k 表示这个根节点下的第 k个子树 , j表示在 第k个子树下保留了 j个节点 。 dp[i] [k] [j]表示删除了多少条边。
那么状态转移方程式就可以写出来了:
d
p
[
i
]
[
k
]
[
j
]
=
m
i
n
(
d
p
[
i
]
[
k
−
1
]
[
j
]
+
1
,
d
p
[
i
]
[
k
−
1
]
[
j
−
s
k
]
+
d
p
[
k
]
[
s
k
]
)
dp[i][k][j] = min(dp[i][k-1][j]+1 , dp[i][k-1][j-s_k] + dp[k][s_k])
dp[i][k][j]=min(dp[i][k−1][j]+1,dp[i][k−1][j−sk]+dp[k][sk])
解释:
dp[i] [k] [j]
以i为根节点的子树,它在第k个子节点生成的子树上保留j个点 ,要删除的边数
dp[i] [k-1] [j]
以i为根节点的子树,它在第k个子节点生成的子树上保留j个点 ,要删除的边数 。
这里为什么要加1呢, 类比背包问题,背包问题dp[i] [j] 可以等于 dp[i-1] [j] 表示第i件我不拿,这里也是一个意思,表示我不从第k个子树里保留节点 ,那么我就要把 i 和k之间的边断开,因此要加1
dp[i] [k-1] [j-s_k] + dp[k] [s_k]
这里的s_k是一个枚举量,总体的意思是,我从第k-1颗子树里取j-s_k 个点 并且从 第k颗子树里取s_k 个点凑出j个点。删除的边数也是两边凑出来。
同时我们可以从方程中看出第i个子树的状态要从它的子树转移来,因此整颗树要从低往上dp。
核心代码
void dfs(int now ,int fa){
sum[now] = 1 ,dp[now][0][1] = 0;
//sum[now]表示子树的节点数量,一开始只有根节点,所以1个
//dp[now][0][1] 表示dp过程的初始量,没有任何实际意义是一个过程量,它和只保留1个点要删除的边的数量不是一回事,这个应该是dp[now][cnt[now]][1] ,其中cnt[now]表示当前节点的子节点的数量
int k = 1;//第k颗子树
for(int i=0;i<edge[now].size();i++){
int to = edge[now][i];
if(to==fa)continue;
dfs(to,now);
sum[now]+=sum[to]; //累加子树的节点数量
for(int j = sum[now];j>0;j--){
dp[i][k][j] = dp[i][k-1][j]+1;//状态转移前者
for(int h =0;h<=min(sum[to],j-1);h++){
dp[i][k][j] = min(dp[i][k][j] , dp[i][k-1][j-h] +dp[to][cnt[to]][h]);
//状态转移后者
}
}
k++;
}
}
优化部分
既然树上dp可以类比背包问题,那么一定可以像背包减少一维,这里就可以减掉第二维,采用滚动数组的方式
void dfs(int now, int p)
{
sum[now] = 1, dp[now][1] = 0;
for (int i = 0; i < ve[now].size(); i++)
{
int to = ve[now][i];
if (to == p)
continue;
dfs2(to, now);
// debug(now);
sum[now] += sum[to];
// debug(sum[now]);
for (int j = sum[now]; j >0; j--)
{
dp[now][j]++;//k被滚动掉了
for (int k = 1; k <= min(sum[to], j - 1); k++)
{
dp[now][j] = min(dp[now][j], dp[now][j - k] + dp[to][k]);
}
}
}
}
完整的代码
#include <algorithm>
#include <cstring>
#include <iostream>
#include <stdlib.h>
#include <vector>
using namespace std;
typedef long long ll;
#define debug(x) cout << #x << " :" << x << endl;
const int maxn = 1e3;
const int INF = 1e9;
int n, p;
vector<int> ve[maxn];
int ru[maxn];
int sum[maxn];
int dp[maxn][maxn];
void dfs2(int now, int p)
{
sum[now] = 1, dp[now][1] = 0;
for (int i = 0; i < ve[now].size(); i++)
{
int to = ve[now][i];
if (to == p)
continue;
dfs2(to, now);
// debug(now);
sum[now] += sum[to];
// debug(sum[now]);
for (int j = sum[now]; j >0; j--)
{
dp[now][j]++;
for (int k = 1; k <= min(sum[to], j - 1); k++)
{
dp[now][j] = min(dp[now][j], dp[now][j - k] + dp[to][k]);
}
}
}
}
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(0);
int _ = 1;
//cin >> _;
while (_--)
{
cin >> n >> p;
int t1, t2;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
dp[i][j] = INF;
}
}
for (int i = 1; i < n; i++)
{
cin >> t1 >> t2;
ve[t1].push_back(t2);
ve[t2].push_back(t1);
ru[t2]++;
}
int root = 0;
for (int i = 1; i <= n; i++)
{
if (!ru[i])
{
root = i;
break;
}
}
// dfs1(1, 0);
// init();
dfs2(1, 0);
int ans = dp[1][p];
for (int i = 2; i <= n; i++)
{
ans = min(ans, dp[i][p] + 1);
}
cout << ans << endl;
}
}