树形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;
}

划重点时间到

小胖守皇宫

image

样例输出 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; 
}

完结撒花

posted @ 2022-01-25 17:38  kiritokazuto  阅读(231)  评论(0编辑  收藏  举报