[洛谷-P1272] 重建道路(树形背包DP)
- 一、题目
- 重建道路
- 题目描述
- 输入格式
- 输出格式
- 样例 #1
- 样例输入 #1
- 样例输出 #1
- 提示
- 样例解释
- 限制与约定
- 二、思路
- 1、状态表示
- 2、转移方程
- 3、循环设计
- 4、初末状态
- 三、代码
一、题目
重建道路
题目描述
一场可怕的地震后,人们用 N N N 个牲口棚(编号 1 ∼ N 1\sim N 1∼N)重建了农夫 John 的牧场。由于人们没有时间建设多余的道路,所以现在从一个牲口棚到另一个牲口棚的道路是惟一的。因此,牧场运输系统可以被构建成一棵树。
John 想要知道另一次地震会造成多严重的破坏。有些道路一旦被毁坏,就会使一棵含有 P P P 个牲口棚的子树和剩余的牲口棚分离,John 想知道这些道路的最小数目。
输入格式
第一行两个整数, N N N 和 P P P。
第二行到第 n n n 行,每行两个整数 I I I 和 J J J,表示节点 I I I 是节点 J J J 的父节点。牧场运输系统可以被构建成一棵以 1 号节点为根的树
输出格式
单独一行,包含一旦被破坏将分离出恰含 P P P 个节点的子树的道路的最小数目。
样例 #1
样例输入 #1
11 6
1 2
1 3
1 4
1 5
2 6
2 7
2 8
4 9
4 10
4 11
样例输出 #1
2
提示
样例解释
如果道路 1 − 4 1-4 1−4 和 1 − 5 1-5 1−5 被破坏,含有节点( 1 , 2 , 3 , 6 , 7 , 8 1,2,3,6,7,8 1,2,3,6,7,8)的子树将被分离出来。
限制与约定
1 ≤ N ≤ 150 1\le N\le 150 1≤N≤150, 1 ≤ P ≤ N 1\le P\le N 1≤P≤N,保证给出的是一棵树。
二、思路
这道题简单的来说就是:
给出一棵含有 n 个节点的有根树,我们现在希望通过删除一些边,让他有一棵有 m 个节点的新树,求删除的最少边的数量。
我们需要注意的有一下几点:
第一,我们选择的点必须构成一棵树,即如果我们选择了一个点,那么就必须选择这个点的父节点。 从这里我们就能够发现这道题其实是一道树形背包DP的问题。
剩下的注意点将在DP的分析中给出。
1、状态表示
f [ u ] [ j ] f[u][j] f[u][j]表示在以 u u u为根节点的树中选择 j j j个点,删除的最小边的数量。
2、转移方程
f [ u ] [ s i z ( u ) ] [ j ] = m i n ( f [ u ] [ s i z ( u ) ] [ j ] , f [ u ] [ s i z ( u ) − 1 ] [ j − k ] + f [ s o n ] [ s i z ( s o n ) ] [ k ] − 2 ) f[u][siz(u)][j]=min(f[u][siz(u)][j], f[u][siz(u)-1][j-k]+f[son][siz(son)][k]-2) f[u][siz(u)][j]=min(f[u][siz(u)][j],f[u][siz(u)−1][j−k]+f[son][siz(son)][k]−2)
这里的 s o n son son指的是 u u u的子节点。 s i z ( u ) siz(u) siz(u)指的是 u u u的子树个数。 s i z ( s o n ) siz(son) siz(son)指的是其子树的子树个树。那么上述的转移方程可以描述为:
在以 u u u为根的子树中选 j j j个节点的最小代价等于在前 s i z ( u ) − 1 siz(u)-1 siz(u)−1个子树中选 j − k j-k j−k个,在第 s i z ( u ) siz(u) siz(u)个子树 s o n son son中选 k k k个的最小代价。
我们为什么要减 2 2 2呢?
我们在初始化的时候,我们是切除了和子节点之间相连的边,同时也切除了和父节点相连的边。
A是B的父节点,所以在算红色的联通块的时候,我们删除了一次A和B之间的边,B又是A的子节点,所以在算紫色连通块的时候,我们又删除了一次A和B之间的边。但实际上我们是需要在A和B之间连接一条边的。也就是说我们不应该删除这条边。我们的
f
[
i
]
[
j
]
f[i][j]
f[i][j]记录的是删除掉的边。所以我们应该-2。
这里我们还需要注意一个关键点:根据刚刚的分析,我们想要选择子树中的点就必须选择这个点所对的根节点。即如果我们总共想选择 j j j个点,那么我们在子树中至多选 j − 1 j-1 j−1个。这个即我们 k k k的范围。
而上述三维的DP是比较容易爆空间的,所以可以优化为两维:
f
[
u
]
[
j
]
=
m
i
n
(
f
[
u
]
[
j
]
,
f
[
u
]
[
j
−
k
]
+
f
[
s
o
n
]
[
k
]
−
2
)
f[u][j]=min(f[u][j], f[u][j-k]+f[son][k]-2)
f[u][j]=min(f[u][j],f[u][j−k]+f[son][k]−2)
3、循环设计
最外层肯定是去遍历树的节点,利用子节点去更新父节点,所以我们最外层的循环其实就是DFS。
那么第二层循环就是我们的选择的点的个数 j j j。但是由于我们优化掉了一维,所以我们需要倒序枚举。
第三层即去枚举在子树 s o n son son中选择的个数。
4、初末状态
f
[
u
]
[
1
]
f[u][1]
f[u][1]表示的是在以
u
u
u为根的树中保留一个点所需要的最小代价,即保留
u
u
u点本身。
那么我们只需要剪掉和
u
u
u直接相连的子节点即可剪去整棵子树,但是不要忘记和节点
u
u
u相连的父节点也要减去!
即 f [ u ] [ 1 ] = s i z ( u ) + 1 f[u][1]=siz(u) + 1 f[u][1]=siz(u)+1,其余初始化正无穷 I N F INF INF即可。
但是这里有一个特殊情况,如果我们的 u u u是整棵树的祖宗节点 r o o t root root。而祖宗节点是没有父节点的,所以我们不需要减去那个和父节点相连的边,此时我们只需要初始化为: f [ r o o t ] [ 1 ] = s i z ( r o o t ) f[root][1]=siz(root) f[root][1]=siz(root)。这是这道题的另外一个坑点。
末状态是
f
[
r
o
o
t
]
[
p
]
f[root][p]
f[root][p],这个想法对吗?
显然是不对的。因为题目中只说保留
p
p
p个,并没有说保留的是以祖宗节点
r
o
o
t
root
root为根。所以我们需要去枚举所有的
f
[
i
]
[
p
]
f[i][p]
f[i][p],找出一个最小值。
三、代码
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
#define endl '\n'
#define INF 0x3f3f3f3f
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
const int N = 300 + 10;
int f[N][N];
int siz[N];
int root = 0;
vector<int>edge[N];
int n, m;
void dp(int u, int M)
{
f[u][1] = edge[u].size() + (u != root);
for(int i = 0; i < edge[u].size(); i ++ )
{
int son = edge[u][i];
dp(son, M);
for(int j = M; j >= 1; j -- )
for(int q = 0; q <= (j - 1); q ++ )
f[u][j] = min(f[u][j - q] + f[son][q] - 2, f[u][j]);
}
}
void solve()
{
cin >> n >> m;
memset(f, INF, sizeof f);
for(int i = 0; i < n - 1; i ++ )
{
int a, b;
cin >> a >> b;
if(root == b || root == 0)
root = a;
edge[a].push_back(b);
}
dp(root, m);
int ans = f[root][m];
for(int i = 1; i <= n; i ++ )
{
if(f[i][m] < ans)
ans = f[i][m];
}
cout << ans << endl;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
solve();
}