树形DP学习笔记
树形dp
概念类
树形dp 是一种很让人喜爱的动态规划,真的可爱, 真的美丽,
当然,前提是在你学会它之后。
实现形式
- 树形dp的主要实现形式是
dfs
,
在dfs
中\(dp\),
主要的实现形式是\(dp[i][j][0/1]\),
\(i\)代表以\(i\)为根的子树,
\(j\)是表示在以\(i\)为根的子树中选择\(j\)个子节点,\(0\)表示这个节点不选,\(1\)表示选择这个节点。有的时候\(j\)或\(0\)或\(1\)这一维可以压掉
其实更多的时候是跑树上背包
基本的dp方程
- 选择节点类
\[dp[i][0] = dp[j][1]
\]
\[dp[i][1]=max/min(dp[j][0],dp[j][1]
\]
- 树形背包类
\[dp[v][k] = dp[u][k] + val
\]
\[dp[u][k] = max(dp[u][k], dp[v][k − 1]
\]
以上就是树形dp的基本理解,因为树形dp没有基本的形式,也没有固定的做法,一般一种题目有一种做法, 简称瞎做就完了, 所以,还是得看题目
- 题目推荐
- 树上染色
- 二叉苹果树
- 时态同步
- 小胖守皇宫
- 选课
- 三色二叉树
- 没有上司的舞会
- 访问美术馆
- 战略游戏
部分题目讲解
first one
二叉苹果树
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 10;
int kid[maxn][maxn];
int cnt[maxn];
int w[maxn][maxn];
int vis[maxn];
int dp[maxn][maxn];
int n, q;
void dfs(int x) {
vis[x] = 1;//标记, 防止死循环
for(int i = 1; i <= cnt[x]; i ++) {//枚举儿子
int y = kid[x][i];
if(vis[y] == 1)continue;//儿子竟是我爹
vis[y] = 1;
dfs(y);//以儿子搜索孙子
for(int zhi = q; zhi >= 1; zhi --) {//可选树枝个数
for(int er = zhi - 1; er >= 0; er --) {//给儿子留的
//自己得留着跟儿子连的枝, 所以zhi - 1
dp[x][zhi] = max(dp[x][zhi], dp[y][er] + dp[x][zhi - er - 1] + w[x][y]);
// 儿子的 自己用儿子剩的 自己连儿子的
}
}
}
return;
}
signed main () {
cin >> n >> q;
for(int i = 1, x, y, z; i < n; i ++) {
cin >> x >> y >> z;
w[x][y] = w[y][x] = z;//不知道谁是爹, 都当一下
kid[x][++cnt[x]] = y;
kid[y][++cnt[y]] = x;
}
dfs(1);
cout << dp[1][q] << endl;//以一为根节点
return 0;
}
(水题就不多讲, 看注释就好啦, 犇犇们都比我聪明(其实就是懒))
second one
没有上司的舞会
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 6e3 + 10;
/*
dp[x][0]表示以x为根的子树,且x不参加舞会的最大快乐值
dp[x][1]表示以x为根的子树,且x参加了舞会的最大快乐值
则dp[x][0] = sigma{max(dp[y][0], dp[y][1])} (y是x的儿子)
dp[x][1] = sigma{dp[y][0]} + happy[x] (y是x的儿子)
先找到树根root
则ans = max(dp[root][0], dp[root][1])
*/
int dp[maxn][2];
vector <int> a[maxn];
int happy[maxn];
int flag[maxn];
int root;
int n;
void dfs(int x) {
dp[x][0] = 0;
dp[x][1] = happy[x];
for(int i = 0 ; i < a[x].size(); i++) {
int y = a[x][i];
dfs(y);
dp[x][0] += max(dp[y][1], dp[y][0]);
dp[x][1] += dp[y][0];
}
}
signed main() {
cin >> n;
for(int i = 1; i <= n; i ++)cin >> happy[i];
for(int i = 1, x, y; i <= n; i ++) {
cin >> x >> y;
if(x != 0 && y != 0){
a[y].push_back(x);//y是x爹
flag[x] = 1;
}
}
for(int i = 1; i <= n; i ++ )if(!flag[i]){root = i; break;}
dfs(root);
cout << max(dp[root][1], dp[root][0]);
return 0;
}
third one
选课
这个题只要注意到建个虚点就好啦
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 10;
struct edge {
int to, next;
}a[maxn];
int len = 0;
int n, m;
int head[maxn];
int dp[maxn][maxn];
void Qian(int from, int to) {
a[++len].next = head[from];
a[len].to = to;
head[from] = len;
}
void WMXJJDP(int x) {
for (int i = head[x]; i; i = a[i].next) {
int y = a[i].to;
WMXJJDP(y);
for(int j = m + 1; j >= 1; j --) {//建了个虚点, 所以m + 1条边
for(int k = 0; k < j; k ++) {
dp[x][j] = max(dp[x][j], dp[y][k] + dp[x][j - k]);
}
}
}
}
signed main() {
cin >> n >> m;
for(int i = 1, opt; i <= n; i++) {
cin >> opt;
cin >> dp[i][1] ;
Qian(opt, i);
}
WMXJJDP(0);
cout << dp[0][m + 1];
return 0;
}
划重点时间到
小胖守皇宫
样例输出 25(
图截不到呜呜呜)
思路提供
-
\(dp_{i, 1}\)表示在\(i\)节点的子节点设一个侍卫的最小经费, 即被自己儿子守卫默认\(i\)节点自己不设
-
\(dp_{i, 2}\)表示在\(i\)节点设一个侍卫的最小经费
-
\(dp_{i, 3}\)表示在\(i\)节点的父节点设一个侍卫的最小经费, 即被自己爹守卫(默认\(i\)节点自己不设
-
\(dp_{i, 1}\)表示i节点被它的子节点守卫
- 考虑:子节点一定在自己那里设一个观测点吗?
- 当然不一定,当且仅当子节点 \((dp_{son_{i}, 2} < dp_{son_{i}, 1}\)时(即给自己设置更优))他才会在自己那里设。
- 那么需要找遍i的所有儿子,只要有一个儿子可以设就行了。
- 我们只需先把所有儿子的\(\ min(dp_{v,1},dp_{v,2})\)累加到\(dp_{i, 1}\)上,
- 再取所有儿子的\((dp_{v,2}-min(dp_{v,1},dp_{v,2}))\)最小值\(t\)
- 如果这个最小值 \(t\) 不是 \(0\),就说明没有一个儿子打算在自己那里设点,就必须强迫一个儿子改变最优解,来满足\(i\)节点的需求,即用\(dp_{i, 2}\)加上\(t\),若\(t\)是\(0\),不影响,加上也无妨, 不会有负数, 因为本来是最优解
- \(dp_{i, 1}+=min(dp_{v,1},dp_{v,2})\);既然默认自己不会设点,就不可能有\(dp_{v,3}\)这个状态
- \(dp_{i, 1}+=t\)
- \(dp_{i, 2}\)即\(i\)节点自己那里设了一个侍卫,既然自己已经设了一个,那么子节点的三种情况都要考虑。
- \(dp_{i, 2}+=min(dp_{v,1},dp_{v,2},dp_{v,3})\);同时把自己花的钱补上, 不能白嫖
- \(dp_{i, 3}\)表示\(i\)节点被它的父节点观测到,那么除了\(dp_{v,3}\)要被排除掉,其他的只要保证子节点可以被观测到即可。
- \(dp_{i, 3}+=min(dp_{v,1},dp_{v,2})\);
代码时间
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e3 + 10;
vector <int> son[maxn];
int vis[maxn];//标记, 找根
int sn[maxn];//son 撇s num
int w[maxn];//钱
int dp[maxn][4];//1 - > 儿子
//2 - > 自己
//3 - > 爹
int n;
#define Inf 2147483647
void dfs(int x) {
int ans = Inf;
for(int i = 0; i < sn[x]; i ++ ) {//vector从0存
int y = son[x]_{i}_;
dfs(y);
dp[x][1] += min(dp[y][1], dp[y][2]);
ans = min(ans, dp[y][2] - min(dp[y][1], dp[y][2]));
dp[x][2] += min(dp[y][1], min(dp[y][2], dp[y][3]));
dp[x][3] += min(dp[y][1], dp[y][2]);
}
dp[x][1] += ans;
dp[x][2] += w[x];
}
signed main() {
cin >> n;
for(int i = 1, x; i <= n; i ++) {
cin >> x;
cin >> w[x] >> sn[x];
for(int j = 1, y; j <= sn[x]; j ++ ) {
cin >> y;
son[x].push_back(y);
vis[y] = 1;
}
}
for(int i = 1; i <= n; i ++) {
if(vis_{i}_ == 0){dfs(i);cout << min(dp_{i}_[1], dp_{i}_[2]);break;}//根没有爹
}
return 0;
}
(最后一个题了, 坚持住)
三色二叉树
变量解释
- 定义\(dpmx_{x, 0}\) 为\(x\)节点染绿
, \(dpmx_{x, 1}\) 为\(x\)节点染红
, \(dpmx_{x, 2}\) 为\(x\)节点染蓝
的情况下子树中最多有几个点被染成绿色 - \(dpmi_{x, 0/1/2}\) 为最少有几个点
- \(max\)答案取\(dp_{1,0}\) 就是对的最大值时,显然 \(1\) 号点和两个(或一个)必放一个绿色如果在 \(1\) 号点不放绿色那就会在儿子处放,儿子的下一代不可以放绿色而如果在根节点直接放了,儿子不会影响下一代是否放绿色
Code
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
char s[maxn];
int tot;
int dpmx[maxn][4], dpmi[maxn][4];
void dfs(int x) {
if(s[x] == '0') {
//叶子
dpmi[x][0] = dpmx[x][0] = 1;
return;
}
dfs(++tot);
if(s[x] == '1') {//只有一个左儿子 x + 1;
dpmx[x][0] = max(dpmx[x + 1][1], dpmx[x + 1][2]) + 1;//自己染
dpmx[x][1] = max(dpmx[x + 1][0], dpmx[x + 1][2]);
dpmx[x][2] = max(dpmx[x + 1][1], dpmx[x + 1][0]);
//xiao
dpmi[x][0] = min(dpmi[x + 1][1], dpmi[x + 1][2]) + 1;//自己染
dpmi[x][1] = min(dpmi[x + 1][0], dpmi[x + 1][2]);
dpmi[x][2] = min(dpmi[x + 1][1], dpmi[x + 1][0]);
}else {
int r = ++tot;//右子树
dfs(r);
dpmx[x][0] = max(dpmx[x + 1][1] + dpmx[r][2], dpmx[x + 1][2] + dpmx[r][1]) + 1;
dpmx[x][1] = max(dpmx[x + 1][0] + dpmx[r][2], dpmx[x + 1][2] + dpmx[r][0]);
dpmx[x][2] = max(dpmx[x + 1][1] + dpmx[r][0], dpmx[x + 1][0] + dpmx[r][1]);
//xiao
dpmi[x][0] = min(dpmi[x + 1][1] + dpmi[r][2], dpmi[x + 1][2] + dpmi[r][1]) + 1;
dpmi[x][1] = min(dpmi[x + 1][0] + dpmi[r][2], dpmi[x + 1][2] + dpmi[r][0]);
dpmi[x][2] = min(dpmi[x + 1][1] + dpmi[r][0], dpmi[x + 1][0] + dpmi[r][1]);
}
}
signed main() {
scanf("%s", s + 1);
dfs(++tot);
cout << dpmx[1][0] << " " << min(min(dpmi[1][0], dpmi[1][1]), dpmi[1][2]);
return 0;
}
完结撒花
愿你在冷铁卷刃之前,得以窥见天光