树形dp模板,ACM暑期集训
A - 没有上司的舞会
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<string>
#include<cstring>
#include<cmath>
#include<ctime>
#include<algorithm>
#include<utility>
#include<stack>
#include<queue>
#include<vector>
#include<set>
#include<map>
using namespace std;
typedef long long LL;
const int N = 6e3 + 5;
vector<int>G[N];
int f[N][2],w[N],s[N],v[N];
int n;
void dfs(int root) {
f[root][1] = w[root];
for (int i = 0; i < G[root].size();i++) {
if (v[G[root][i]])
continue;
int j = G[root][i];
dfs(j);
f[root][0] += max(f[j][0], f[j][1]);
f[root][1] += f[j][0];
}
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> w[i];
}
for (int i = 1, a, b; i < n; i++) {
cin >> a >> b;
G[b].push_back(a);
s[a] = 1;
}
int root = 1;
while (s[root])root++;
v[root] = 1;
dfs(root);
cout << max(f[root][0], f[root][1]) << endl;
return 0;
}
C - 有线电视网
P1273 有线电视网 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题目描述
某收费有线电视网计划转播一场重要的足球比赛。他们的转播网和用户终端构成一棵树状结构,这棵树的根结点位于足球比赛的现场,树叶为各个用户终端,其他中转站为该树的内部节点。
从转播站到转播站以及从转播站到所有用户终端的信号传输费用都是已知的,一场转播的总费用等于传输信号的费用总和。
现在每个用户都准备了一笔费用想观看这场精彩的足球比赛,有线电视网有权决定给哪些用户提供信号而不给哪些用户提供信号。
写一个程序找出一个方案使得有线电视网在不亏本的情况下使观看转播的用户尽可能多。
输入格式
输入文件的第一行包含两个用空格隔开的整数 N 和 M,其中 2≤N≤3000,1≤M≤N−1,N 为整个有线电视网的结点总数,M 为用户终端的数量。
第一个转播站即树的根结点编号为 1,其他的转播站编号为 2 到 N−M,用户终端编号为 N−M+1 到 N。
接下来的 N−M 行每行表示—个转播站的数据,第 i+1 行表示第 i 个转播站的数据,其格式如下:
K A1 C1 A2 C2 … Ak Ck
K 表示该转播站下接 K 个结点(转播站或用户),每个结点对应一对整数 A 与 C ,A 表示结点编号,C 表示从当前转播站传输信号到结点 A 的费用。最后一行依次表示所有用户为观看比赛而准备支付的钱数。单次传输成本和用户愿意交的费用均不超过 10。
输出格式
输出文件仅一行,包含一个整数,表示上述问题所要求的最大用户数。
输入输出样例
输入 #1复制
5 3 2 2 2 5 3 2 3 2 4 3 3 4 2
输出 #1复制
解析:树形dp,分组背包
所谓分组背包,即在选择物品的时候,一开始将物品分为好几组,在选择时,可以从每一组中至多选择一件物品,问如何获得最大的价值,所以我们每次可以枚举这个组数,用i表示第几组,用j表示体积,用k来表示选择的物品,伪代码如下
for (int i = 0到组数)
for (int j = max_v到0)
for (int k = 此组所有物品)
f[j] = max (f[j], f[j - v[k]] + w[k])
在树也是同样的道理,每个子树其实就是每一个组,在里面选择,但不同的是,在这里我们用dfs来实现
1.明确dp[i][j]含义,表示i节点,选j个用户,能得到的钱的最大值,然后对每个节点做分组背包。
2.怎么看出是分组背包?首先,背包的总容量相当于该点为根节点的子树中所有的用户数量(dp[i][j]的 j 不可能超过它连接的所有用户数)。然后,把该节点的每个儿子看成一组,每组中的元素为选一个,选两个...选n个用户。
3.转移方程 dp[i][j]=max(dp[i][j],dp[i][j-k]+dp[v][k]-这条边的花费) i,j不解释了,v表示枚举到这一组(即i的儿子),k表示枚举到这组中的元素:选k个用户。
4.最后输出dp[1][i]>=0的i的最大值,所以反向枚举。
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<string>
#include<cstring>
#include<cmath>
#include<ctime>
#include<algorithm>
#include<utility>
#include<stack>
#include<queue>
#include<vector>
#include<set>
#include<map>
using namespace std;
typedef long long LL;
const int N = 3e3 + 5;
int n, m;
int dp[N][N],w[N];
vector<pair<int, int>>G[N];
int dfs(int root) {
int sum = 0,t;
if (G[root].size() == 0) {
dp[root][1] = w[root];
return 1;
}
for (int i = 0; i < G[root].size(); i++) {
int v = G[root][i].first;
int b = G[root][i].second;
t = dfs(v);
sum += t;
for (int j = sum; j > 0; j--) {
for (int k = 1; k <= t; k++) {
if (j >= k)
dp[root][j] = max(dp[root][j], dp[root][j - k] + dp[v][k] - b);
}
}
}
return sum;
}
int main() {
cin >> n >> m;
for (int i = 1,t, a, b; i <= n - m; i++) {
scanf("%d", &t);
for (int j = 1; j <= t; j++) {
scanf("%d%d", &a, &b);
G[i].push_back({ a,b });
}
}
for (int i = n-m+1; i <= n; i++) {
scanf("%d", &w[i]);
}
memset(dp, -0x3f3f3f3f, sizeof(dp));
for (int i = 1; i <= n; i++) dp[i][0] = 0;
dfs(1);
for (int i = m; i > 0; i--) {
if (dp[1][i] >= 0) {
printf("%d\n", i);
break;
}
}
return 0;
}
D - 重建道路
P1272 重建道路 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
Description
一场可怕的地震后,人们用 N 个牲口棚(编号 1∼N)重建了农夫 John 的牧场。由于人们没有时间建设多余的道路,所以现在从一个牲口棚到另一个牲口棚的道路是惟一的。因此,牧场运输系统可以被构建成一棵树。
John 想要知道另一次地震会造成多严重的破坏。有些道路一旦被毁坏,就会使一棵含有 P 个牲口棚的子树和剩余的牲口棚分离,John 想知道这些道路的最小数目。
Input
第一行两个整数,N 和 P。
第二行到第 n 行,每行两个整数 I 和J,表示节点 I 是节点 J 的父节点。牧场运输系统可以被构建成一棵以 1 号节点为根的树
Output
单独一行,包含一旦被破坏将分离出恰含 P 个节点的子树的道路的最小数目。
Sample 1
Inputcopy | Outputcopy |
---|---|
11 6 1 2 1 3 1 4 1 5 2 6 2 7 2 8 4 9 4 10 4 11 | 2 |
Hint
样例解释
如果道路 1−4 和 1−5 被破坏,含有节点(1,2,3,6,7,81,2,3,6,7,8)的子树将被分离出来。
限制与约定
01≤N≤150,1≤P≤N,保证给出的是一棵树。
解析:树形dp,分组背包
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<string>
#include<cstring>
#include<cmath>
#include<ctime>
#include<algorithm>
#include<utility>
#include<stack>
#include<queue>
#include<vector>
#include<set>
#include<map>
using namespace std;
typedef long long LL;
const int N = 150 + 5;
int n,p;
vector<int>G[N];
int f[N][N],arr[N];
int dfs(int r) {
f[r][1] = G[r].size();
int sum = 1, t;
for (int i = 0; i < G[r].size(); i++) {
int v = G[r][i];
t = dfs(v);
sum += t;
for (int j = sum; j > 0; j--) {
for (int k = 1; k < j; k++) {
f[r][j] = min(f[r][j], f[r][j - k] + f[v][k] - 1);
}
}
}
return sum;
}
int main() {
cin >> n>>p;
for (int i = 1,a,b; i < n; i++) {
cin >> a >> b;
arr[b] = 1;
G[a].push_back(b);
}
int root = 1;
while (arr[root])root++;
memset(f, 0x3f3f3f3f, sizeof(f));
dfs(root);
int mn = f[root][p];
for (int i = 1; i <= n; i++) {
if (f[i][p] < mn)mn = f[i][p] + 1;
}
cout << mn << endl;
return 0;
}
H - Choosing Capital for Treeland
Choosing Capital for Treeland - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题意翻译
题目描述
Treeland国有n个城市,这n个城市连成了一颗树,有n-1条道路连接了所有城市。每条道路只能单向通行。现在政府需要决定选择哪个城市为首都。假如城市i成为了首都,那么为了使首都能到达任意一个城市,不得不将一些道路翻转方向,记翻转道路的条数为k。你的任务是找到所有满足k最小的首都。
输入输出格式
输入格式
输入包含多个测试点。对于每个测试点,每个测试点的第一行为一个正整数n(2<=n<=2e5)。接下来n-1行,每行两个正整数ai,bi,表示城市a到城市b有一条单向通行的道路。输入以空格分隔,以EOF结尾。
输出格式
对于每个测试点,第一行输出k,第二行升序输出所有满足条件的首都的编号。
输入输出样例
输入 #1复制
3 2 1 2 3
输出 #1复制
0 2
输入 #2复制
4 1 4 2 4 3 4
输出 #2复制
2 1 2 3
解析:树形dp
建图是见双向图,然后随便选择一个点作为根节点,进行两次dfs遍历:在第一次自底向上的dfs中,我们用dp[u]表示以u节点为首都时,u到所有子节点S需要逆转的边数;在第二次自顶向下的dfs中,dp[i]的定义变成了u到全树节点需要逆转的边数。具体见代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<string>
#include<cstring>
#include<cmath>
#include<ctime>
#include<algorithm>
#include<utility>
#include<stack>
#include<queue>
#include<vector>
#include<set>
#include<map>
using namespace std;
typedef long long LL;
const int N = 2e5 + 5;
int n, mn = 0x3f3f3f3f;
vector<pair<int, int>>G[N];
int f[N];
void dfs1(int r, int fa) {
for (int i = 0; i < G[r].size(); i++) {
int v = G[r][i].first;
if (v == fa)
continue;
dfs1(v, r);
if (G[r][i].second) {
f[r] += f[v];
}
else {
f[r] += f[v] + 1;
}
}
}
void dfs2(int r, int fa) {
mn = min(mn, f[r]);
for (int i = 0; i < G[r].size(); i++) {
int v = G[r][i].first;
if (v == fa)
continue;
if (G[r][i].second) {
f[v] = f[r] + 1;
}
else {
f[v] = f[r] - 1;
}
dfs2(v, r);
}
}
int main() {
while (scanf("%d", &n) != EOF) {
mn = 0x3f3f3f3f;
for (int i = 0; i <= n; i++) {
G[i].clear();
}
memset(f, 0, sizeof(f));
for (int i = 1, a, b; i < n; i++) {
scanf("%d%d", &a, &b);
G[a].push_back({ b,1 });
G[b].push_back({ a,0 });
}
dfs1(1, 0);
dfs2(1, 0);
cout << mn << endl;
for (int i = 1; i <= n; i++) {
if (mn == f[i]) {
printf("%d ", i);
}
}
cout << endl;
}
return 0;
}