树形DP总结

树形DP

一、树形DP

1. 定义

树形DP即为在树形结构上的DP;

给定一个 N N N 个结点的树 (通常是无根树,即有 N − 1 N - 1 N1 条无向边) 可以通过根节点定义出每个节点,则在此树上设计动态规划;

2. 过程

由于树是由若干的子树递归定义的,所以树形 DP 通常在 DFS 的方式中实现,即在 DFS 遍历整棵树的过程中进行状态转移;

一般以结点从浅到深作为 DP 的 ”阶段“ ;

DP 状态的第一维通常为一个节点的编号,代表在以这个点为根节点的子树上的状态;

一般通过子节点与父节点的状态关系进行状态的转移;

即对于每个节点,先递归在它的每个子节点上进行DP,在回溯时,从子节点向根结点转移;

用邻接表存储下 N − 1 N - 1 N1 条无向边,在递归时,注意标记结点是否访问过,避免沿反向边父节点;

3. 例题

战略游戏

题目描述

Bob 喜欢玩电脑游戏,特别是战略游戏。但是他经常无法找到快速玩过游戏的方法。现在他有个问题。

现在他有座古城堡,古城堡的路形成一棵树。他要在这棵树的节点上放置最少数目的士兵,使得这些士兵能够瞭望到所有的路。

注意:某个士兵在一个节点上时,与该节点相连的所有边都将能被瞭望到。

请你编一个程序,给定一棵树,帮 Bob 计算出他最少要放置的士兵数。

输入格式

输入数据表示一棵树,描述如下。

第一行一个数 N ,表示树中节点的数目。

第二到第 N + 1 行,每行描述每个节点信息,依次为该节点编号 i ,数值 k ,k 表示后面有 k 条边与节点 i 相连,接下来 k 个数,分别是每条边的所连节点编号 r 1 , r 2 , . . . , r n r_1, r_2, ... , r_n r1,r2,...,rn

对于一个有 N 个节点的树,节点标号在 0 到 N - 1 之间,且在输入文件中每条边仅出现一次。

输出格式

输出仅包含一个数,为所求的最少士兵数。

数据范围与提示

对于 100% 的数据,有 0 < n < 1500 0 < n < 1500 0<n<1500

分析

此题意为 一个点可以覆盖与之连接的边,求用最少的点可以覆盖整棵树;

通过路形成一棵树与求最小的士兵数,想到树形DP;

状态

由于每个节点有两种情况,则站与不站士兵,分别对应两种不同的情况,则定义状态为

d p [ i ] [ 0 / 1 ] dp[i][0/1] dp[i][0/1] 表示覆盖完以 i i i 为根节点的子树,站/不站(1/0)士兵时,所需要的最少士兵数量;

转移

当以 i i i 为根节点的子树需要全部覆盖时;

i i i 结点不站士兵,则它的子节点必须全部放置士兵,才可以满足士兵看到所有的边,则

d p [ i ] [ 0 ] = d p [ v ] [ 1 ] dp[i][0] = dp[v][1] dp[i][0]=dp[v][1] ,其中 v v v i i i 的子节点;

i i i 结点站士兵,则其子节点可站可不站士兵,则选取站士兵最少的方案即可;

d p [ i ] [ 1 ] = m i n ( d p [ v ] [ 1 ] , d p [ v ] [ 0 ] ) dp[i][1] = min(dp[v][1], dp[v][0]) dp[i][1]=min(dp[v][1],dp[v][0]) ,其中 v v v i i i 的子节点;

又由于每个结点只需计算一次,所以不需要回溯;

答案

答案则为根结点站与不站士兵满足要求的总士兵数最小值;

代码
#include <cstdio>
#include <vector>
#include <algorithm>
#define MAXN 1505
using namespace std;
int n, dp[MAXN][2];
bool flag[MAXN];
vector < int > g[MAXN];
void dfs(int i) {
    dp[i][1] = 1;
    flag[i] = true;
    for (int t = 0; t < g[i].size(); t++) {
        int v = g[i][t];
        if (!flag[v]) {
            dfs(v);
            dp[i][1] += min(dp[v][1], dp[v][0]);
            dp[i][0] += dp[v][1];
        }
    }
}
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        int x, t, y;
        scanf("%d %d", &x, &t);
        for (int j = 1; j <= t; j++) {
            scanf("%d ", &y);
            g[x].push_back(y);
            g[y].push_back(x);
        }
    }
    dfs(0);
    printf("%d", min(dp[0][0], dp[0][1]));
    return 0;
}

二、树上背包

1. 说明

树上的背包问题,就是背包问题与树形 DP 的结合;

由于树由节点组成,将每个子树看作一组,则为每组物品中,选出 v v v 个,使得 c c c 最大,即为分组背包;

状态

即同滚动数组优化后的分组背包,将背包的容积当作状态中的第二维,第一位即为以 i i i 维根节点的子树;

d p [ i ] [ j ] dp[i][j] dp[i][j]: 以 i i i 为根节点的子树上,背包容量为 j j j 时的最大价值;

转移

在 DFS 遍历树时,枚举背包容量 j j j 与组内选择的物品数量 k k k

设这颗子树根结点为 i i i 其的一个子节点为 v v v

当选择 i , v i, v i,v 这条边时,要在以 i i i 为根结点的子树上选择 j j j 个,以 v v v 为根结点的子树上选择 k k k 个,则还应在以 i i i 为根结点的子树上选择 j − k − 1 j - k - 1 jk1 个;

状态转移方程
d p [ i ] [ j ] = max ⁡ k < j { d p [ v ] [ k ] + c [ i ] [ v ] + d p [ i ] [ j − k − 1 ] } dp[i][j] = \max_{k < j} \{ dp[v][k] + c[i][v] + dp[i][j - k - 1] \} dp[i][j]=k<jmax{dp[v][k]+c[i][v]+dp[i][jk1]}

由于转移根结点 i i i 时要用到子节点 u u u 的状态,所以先递归转移子节点 u u u 的状态,再在回溯时转移 i i i 的状态;

又由于转移 d p [ i ] [ j ] dp[i][j] dp[i][j] 时需要用到 d p [ i ] [ j − k − 1 ] dp[i][j - k - 1] dp[i][jk1] 的值,所以 j j j 应倒叙枚举;

2. 例题

二叉苹果树

题目描述

有一棵二叉苹果树,如果数字有分叉,一定是分两叉,即没有只有一个儿子的节点。这棵树共 N 个节点,标号 1 至 N ,树根编号一定为 1。

我们用一根树枝两端连接的节点编号描述一根树枝的位置。一棵有四根树枝的苹果树,因为树枝太多了,需要剪枝。但是一些树枝上长有苹果,给定需要保留的树枝数量,求最多能留住多少苹果。

输入格式

第一行两个数 N 和 Q, 表示树的节点数,Q 表示要保留的树枝数量。

接下来 N - 1 行描述树枝信息,每行三个整数,前两个是它连接的节点的编号,第三个数是这根树枝上苹果数量。

输出格式

输出仅一行,表示最多能留住的苹果的数量。

数据范围与提示

对于 100% 的数据, 1 ≤ Q ≤ N ≤ 100 , N ≠ 1 1\le Q \le N \le 100, N\neq 1 1QN100,N=1,每根树枝上苹果不超过 30000 个。

分析

由于剪掉树枝,使苹果数量最多,想到树形背包DP;

树枝数量为容量,苹果数量为价值;

状态

d p [ i ] [ j ] dp[i][j] dp[i][j] 表示以 i i i 号节点为根节点的子树上,保留 j j j 个树枝的最多苹果数量;

转移

分组背包,在DFS遍历树中,枚举背包容量,组内背包编号;

设这颗子树根结点为 i i i 其的一个子节点为 v v v

d p [ i ] [ j ] = max ⁡ k < j { d p [ v ] [ k ] + c [ i ] [ v ] + d p [ i ] [ j − k − 1 ] } dp[i][j] = \max_{k < j} \{ dp[v][k] + c[i][v] + dp[i][j - k - 1] \} dp[i][j]=k<jmax{dp[v][k]+c[i][v]+dp[i][jk1]}

由于是滚动数组优化后的分组背包,所以倒着枚举 j j j

代码
#include <cstdio>
#include <vector>
#include <algorithm>
#define MAXN 105
using namespace std;
int n, q, dp[MAXN][MAXN];
bool flag[MAXN];
struct edge {
    int to, tot;
};
vector < edge > g[MAXN];
void dfs(int i) {
    flag[i] = true;
    for (int t = 0; t < g[i].size(); t++) {
        int v = g[i][t].to, tot = g[i][t].tot;
        if (flag[v] == false) {
            dfs(v);
            for (int j = q; j >= 0; j--) {
                for (int k = 0; k < j; k++) {
                    dp[i][j] = max(dp[i][j], dp[v][k] + tot + dp[i][j - k - 1]);
                }
            }
        }
    }
    return; 
}
int main() {
    scanf("%d %d", &n, &q);
    for (int i = 1; i < n; i++) {
        int x, y, z;
        scanf("%d %d %d", &x, &y, &z);
        g[x].push_back( edge ( { y, z } ) );
        g[y].push_back( edge ( { x, z } ) );
    }
    dfs(1);
    printf("%d", dp[1][q]);
    return 0;
}

三、换根DP

1. 说明

给定一个无根树形结构,需要以每个结点为根进行统计,这类问题则为 “不定根” 的树形DP;

一般通过两次扫描来解决

  1. 第一次扫描,任选一点为根,预处理出转移根时需要的数据,以及以该点为根时的状态;
  2. 第二次扫描,从开始选的根开始搜索遍历,通过父节点与子节点的关系,进行状态转移,算出从换根后的解;

既可以在 O ( n ) O(n) O(n) 内解决;

此类问题定义状态时通常为一个节点的编号,代表在以这个点为根时树上的状态;

2. 例题

STA-Station

题目描述

给出一个N个点的树,找出一个点来,以这个点为根的树时,所有点的深度之和最大

输入格式

给出一个数字N,代表有N个点.N<=1000000 下面N-1条边.

输出格式

输出你所找到的点,如果具有多个解,请输出编号最小的那个.

分析

由于此题树为无根树,又要求结点为根时的深度之和,即为换根DP;

状态

d p [ i ] dp[i] dp[i] 表示以 i i i 为根结点时,树上的状态;

转移

在从 i i i 换根到 其子节点 v v v 时;

  1. u u u 为根结点的子树上的每个节点深度均减一,总共减去 s i z e [ u ] size[u] size[u] ;
  2. 其余的结点,深度均加1,总共加上 n − s i z e [ u ] n - size[u] nsize[u] ;

则有状态转移方程

d p [ v ] = d p [ i ] − s i z e [ v ] + n − s i z e [ v ] dp[v] = dp[i] - size[v] + n - size[v] dp[v]=dp[i]size[v]+nsize[v]

所以第一次扫描时,预处理出以 i i i 为根结点的子树的大小;

代码
#include <cstdio>
#include <vector>
#include <algorithm>
#define MAXN 1000005
using namespace std;
int n, ans1;
long long ans = -1, size[MAXN], dep[MAXN], dp[MAXN];
vector < long long > g[MAXN];
bool flag[MAXN];
void dfs1(int i) {
    size[i] = 1;
    flag[i] = true;
    for (int t = 0; t < g[i].size(); t++) {
        int v = g[i][t];
        if (!flag[v]) {
            dep[v] = dep[i] + 1;
            dfs1(v);
            size[i] += size[v];
        }
    }
    flag[i] = false;
    return;
}
void dfs2(int i) {
    flag[i] = true;
    for (int t = 0; t < g[i].size(); t++) {
        int v = g[i][t];
        if (!flag[v]) {
            dp[v] = dp[i] - size[v] + n - size[v];
            dfs2(v);
        }
    }
    return;
}
int main() {
    scanf("%d", &n);
    for (int i = 1; i < n; i++) {
        int x, y;
        scanf("%lld %lld", &x, &y);
        g[x].push_back(y);
        g[y].push_back(x);
    }
    dfs1(1);
    for (int i = 1; i <= n; i++) {
        dp[1] += dep[i];
    }
    dfs2(1);
    for (int i = 1; i <= n; i++) {
        if (dp[i] > ans) {
            ans = dp[i];
            ans1 = i;
        }
    }
    printf("%d", ans1);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值