树形DP入门

树形DP入门

一· 引入

作为一个DP学的很渣的人,树形DP一开始对我很不友好(我连线性DP都没掌握更别说树形DP了)。

所以,为了造福自己,奉献上此篇水文(不喜可以在下方评论)。

我对树形DP的理解(勿喷):给定了一棵树,要求在其上以最少(最大)的代价(收益)完成给定的操作。所以,对于在树上进行状态转移就显得尤为的重要。

因为树本身是由树和它的子树构成的,所以,我们可以在树上进行递归。递归到叶子节点之后,可以一步一步返回其状态更新根节点。

反过来,因为搜索基本上都可以概括成一个状态的转移,由此,我们也可以在树上进行DP。不同的条件有着不同的状态转移方程式。

接下来,我们通过一道例题来讲解树形DP。

二·例题

例题1 二叉苹果树 洛谷P2015

题面

题目描述

有一棵苹果树,如果树枝有分叉,一定是分二叉(就是说没有只有一个儿子的结点)

这棵树共有 N N N 个结点(叶子点或者树枝分叉点),编号为 1 ∼ N 1 \sim N 1N,树根编号一定是 1 1 1

我们用一根树枝两端连接的结点的编号来描述一根树枝的位置。下面是一颗有 4 4 4 个树枝的树:

2   5
 \ / 
  3   4
   \ /
    1

现在这颗树枝条太多了,需要剪枝。但是一些树枝上长有苹果。

给定需要保留的树枝数量,求出最多能留住多少苹果。

输入格式

第一行 2 2 2 个整数 N N N Q Q Q,分别表示表示树的结点数,和要保留的树枝数量。

接下来 N − 1 N-1 N1 行,每行 3 3 3 个整数,描述一根树枝的信息:前 2 2 2 个数是它连接的结点的编号,第 3 3 3 个数是这根树枝上苹果的数量。

输出格式

一个数,最多能留住的苹果的数量。

样例 #1
样例输入 #1
5 2
1 3 1
1 4 10
2 3 20
3 5 20
样例输出 #1
21
提示

1 ⩽ Q < N ⩽ 100 1 \leqslant Q < N \leqslant 100 1Q<N100,每根树枝上的苹果 ⩽ 3 × 1 0 4 \leqslant 3 \times 10^4 3×104

分析

题目大家都应该理解了,我们接下来逐步分析。

这道题的第一个考点是考察我们对于树的存储。对于树的存储有两种方法(目前我只知道两种),一种是用vector实现的邻接表,一种是链式前向星。(本蒟蒻太菜了,不会链式前向星,只能用邻接表存QWQ)。

接下来,我们开始状态转移。

我们定义一个 d p [ x ] [ j ] dp[x][j] dp[x][j] 数组,来表示以 x x x 为根的儿子上留 j j j 条边时最多的苹果数量。由此,我们可以得出,最终的答案是 d p [ 1 ] [ q ] dp[1][q] dp[1][q]

状态转移代码如下,等会我们来分析。

for(int j=sum[x];j;j--){
    for(int k=0;k<j;k++){
        dp[x][j]=max(dp[x][j],dp[x][j-k-1]+dp[v][k]+w);
    }
}

其中, v v v x x x 的一个儿子结点,然后, d p [ x ] [ j ] dp[x][j] dp[x][j] 的计算方法有两部分。

1) d p [ v ] [ k ] dp[v][k] dp[v][k] 就是在以 v v v 为根结点的子树上留 k k k 条边

2) d p [ x ] [ j − k − 1 ] dp[x][j-k-1] dp[x][jk1] 就是在除了以 x x x 为根结点的子树上的 k k k 条边和 [ u , v ] [u,v] [u,v] 这一条边,一共是 k + 1 k+1 k+1 条边。这时,以 x x x 为根节点的子树上只有 j − k − 1 j-k-1 jk1 条边了。

接下来放出DP部分代码,带注释,好理解

void dfs(int x,int fath){
    for(int i=0;i<v[x].size();i++){//遍历x的所有子节点
        int vv=v[x][i].v;//x的子节点
        int ww=v[x][i].w;//该边的权
        if(vv==fath)continue;//就不用回去搜父节点啦,避免循环重复(一开始我就是没有注意到这个才导致我MLE)
        dfs(vv,x);//递归到最深的叶子结点,然后返回一步一步更新信息直到到根结点
        sume[x]+=sume[vv]+1;//sume[i]是来记录以i为根的子树的总边数,这里是累计子树上的总边数
        for(int j=sume[x];j;j--){
            for(int k=0;k<j;k++){
                dp[x][j]=max(dp[x][j],dp[x][j-k-1]+dp[vv][k]+ww);//状态转移,解释过啦,没什么好说的
            }
        }
    }
}

其实上面我们给的是多叉树的状态转移的方法,二叉树有其独特的转移方法,但是我们学了多叉树之后还有必要学二叉树吗(doge)。

上面一题我们也使用多叉树的方法处理的二叉树,那两部分计算方式大家都应该了解了吧。

其实上面的代码最关键的便是 d f s ( ) dfs() dfs() 中的 j j j 循环方向,不知道循环方向,我们就无法做出此题。 j j j 应该是从 s u m e [ x ] sume[x] sume[x] 开始慢慢递减,而不是从 0 0 0 开始递增。因为它此时的状态应该是以前用 x x x 的子树计算之后得来的结果,即排除当前的 v v v 的这个子树的计算结果。

k k k 的循环顺序是无所谓的,递增递减都可以 。

该代码的时间复杂度小于 O ( n 3 ) O(n^3) O(n3)

例题2.没有上司的舞会 洛谷P1352

题面

题目描述

某大学有 n n n 个职员,编号为 1 … n 1\ldots n 1n

他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。

现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 r i r_i ri,但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。

所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。

输入格式

输入的第一行是一个整数 n n n

2 2 2 到第 ( n + 1 ) (n + 1) (n+1) 行,每行一个整数,第 ( i + 1 ) (i+1) (i+1) 行的整数表示 i i i 号职员的快乐指数 r i r_i ri

( n + 2 ) (n + 2) (n+2) 到第 2 n 2n 2n 行,每行输入一对整数 l , k l, k l,k,代表 k k k l l l 的直接上司。

输出格式

输出一行一个整数代表最大的快乐指数。

样例 #1
样例输入 #1
7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5
样例输出 #1
5
提示
数据规模与约定

对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 6 × 1 0 3 1\leq n \leq 6 \times 10^3 1n6×103 − 128 ≤ r i ≤ 127 -128 \leq r_i\leq 127 128ri127 1 ≤ l , k ≤ n 1 \leq l, k \leq n 1l,kn,且给出的关系一定是一棵树。

分析

本题是树形DP的经典例题!!!

如题目所说,一个职员就是一个节点,如果一个节点参加宴会,那么它的子节点就不能参加宴会。如果这个节点不参加宴会,那么它的子节点参不参加宴会都可以。

状态转移分析:

我们定义一个 d p [ i ] [ 0 ] dp[i][0] dp[i][0] 数组表示不选择当前的节点,也就是不参加宴会的的最优解。

反之 d p [ i ] [ 1 ] dp[i][1] dp[i][1] 表示选择当前节点,也就是参加宴会的最优解。

状态转移也有两种情况,我们逐个分析

1)如果我们不选择当前的节点,那么它的子节点就是可以选可以不选,我们取其中的最大值,也就是 d p [ x ] [ 0 ] + = m a x ( d p [ x ] [ 0 ] , d p [ v ] [ 1 ] ) dp[x][0]+=max(dp[x][0],dp[v][1]) dp[x][0]+=max(dp[x][0],dp[v][1])

2)如果我们选择当前这个节点,那么它的子节点都不可以选了。也就是 d p [ x ] [ 1 ] + = d p [ v ] [ 0 ] dp[x][1]+=dp[v][0] dp[x][1]+=dp[v][0]

下面给出本题代码

#include
using namespace std;
int a[6005],fath[6005],dp[6005][2],n,r;
vector<int>s[6005];
void dfs(int x){
    dp[x][0]=0;//不参加宴会初始化
    dp[x][1]=a[x];//参加宴会初始化
    for(auto v:s[x]){
        dfs(v);//递归子节点
        dp[x][1]+=dp[v][0];//选父节点,子节点不可以选
        dp[x][0]+=max(dp[v][0],dp[v][1]);//不选父节点,子节点可以选可以不选
    }
}
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];//快乐指数
    }
    for(int i=1,x,y;i<n;i++){
        cin>>x>>y;
        s[y].push_back(x);//vector邻接表建树
        fath[x]=1;//标记,证明它不是根结点
    }
    for(int i=1;i<=n;i++){
        if(!fath[i]){
            r=i;
            break;
        }
    }//此处的for在查找树的根结点
    dfs(r);//r为此树的根结点,从根结点开始遍历这棵树
    cout<<max(dp[r][0],dp[r][1]);//选与不选的最大值
    return 0;
}

本代码的时间复杂度为 O ( n ) O(n) O(n)

以上就是我对树形DP的初步理解(终于理解DP的含义了

你可能感兴趣的:(c++教程,洛谷,深度优先,算法,树形DP,动态规划)