【算法】动态规划树形DP

一、树形DP思想介绍

在树这种数据结构上做DP很常见:给出一棵树,要求以最少代价(或最大收益)完成给定的操作。
并且在树上做DP显得非常自然,因为树本身有“子结构”性质(树和子树)。

基于树的解题步骤一般是先把树转换为有根树(如果是几棵互不连通的树,就加一个虚拟根,它连接所有孤立的树),然后在树上做DFS,递归到最底层的叶子节点,再一层层返回信息更新至根节点。显然,树上DP所操作的就是这一层层返回的信息。不同的题目需要灵活设计不同的DP状态和转移方程。


二、树形DP经典例题

2.1 二叉苹果树

洛谷p2015

题目描述

有一棵苹果树,如果树枝有分叉,一定是分二叉(就是说没有只有一个儿子的结点)
这棵树共有 N 个结点(叶子点或者树枝分叉点),编号为 1∼N,树根编号一定是 1。

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

2   5
 \ / 
  3   4
   \ /
    1

现在这颗树枝条太多了,需要剪枝。但是一些树枝上长有苹果。
给定需要保留的树枝数量,求出最多能留住多少苹果。

输入格式
第一行 2 个整数 N 和 Q,分别表示表示树的结点数,和要保留的树枝数量。
接下来 N−1 行,每行 3 个整数,描述一根树枝的信息:前 2 个数是它连接的结点的编号,第 3 个数是这根树枝上苹果的数量。

输出格式
一个数,最多能留住的苹果的数量。


解题步骤

  1. 状态表示
    f[x][i]表示以结点x为根的子树保留i个树枝所获得的最大边权值和。

  2. 状态计算
    树的父结点的状态由子结点转移而来,设以结点x为根的子树有一颗以结点u为根的子树,则状态转移方程为:
    f [ x ] [ i ] = m a x ( f [ x ] [ i ] , f [ x ] [ i − j − 1 ] + f [ u ] [ j ] + w [ i ] ) f[x][i] = max(f[x][i], f[x][i - j - 1] + f[u][j] + w[i]) f[x][i]=max(f[x][i],f[x][ij1]+f[u][j]+w[i])
    i − j − 1 i-j-1 ij1多减了1的原因是结点与子树连接需要用掉一根树枝,w[i]为该树枝的权值。

  3. 初始化
    根据本题题意,一颗树若没有树枝该树留住的苹果数量为0。

  4. 实现细节
    我们需要统计以x为根的子树的树枝总数sz[x],为了方便进行状态转移时的边界控制,确保在枚举保留的树枝数量时不超过子树的实际大小

第一层遍历的时候为什么要从大到小枚举树枝数?

与01背包问题dp优化的原理类似。我们希望状态转移时f[x][i-j-1]代表的是为与当前枚举的子树u没有连接的树x在拥有i-j-1个树枝保留的最多苹果数。若我们从小到大枚举树枝,则f[x][i-j-1]可能已经被更新了,相当于同一颗子树被考虑连接两次,不符合题意。

代码实现

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>

using namespace std;

const int N = 300, M = 2 * N;

int h[N], e[M], ne[M], w[M], idx;

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}

int f[N][N];
int n, m;
int sz[N];

void dfs(int x, int father)
{
    
    for(int i = h[x]; ~i; i = ne[i])
    {
        int j = e[i];
        if(j == father)continue;
        
        dfs(j, x);
        sz[x] += sz[j] + 1;
        
        for(int size_x = min(n-1 , sz[x]); size_x > 0; size_x --)
            for(int k = min(size_x-1, sz[j]); k >= 0; k --)
            {
                f[x][size_x] = max(f[x][size_x], f[x][size_x - k - 1] + f[j][k] + w[i]);
            }
    }
    
}


int main()
{
    scanf("%d %d", &n, &m);
    
    memset(h, -1, sizeof h);
    for(int i = 0; i < n - 1; i ++)
    {
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);
        add(a, b, c), add(b, a, c);
    }
    
    dfs(1, 0);
    
    printf("%d", f[1][m]);
	
	return 0;
}

2.2 没有上司的舞会

洛谷P1352

题目描述

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

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

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

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

解题步骤

  1. 状态表示
    f[x][0]表示以结点x为根的子树不选择该结点获得的最大happy值,f[x][1]表示以结点x为根的子树选择该结点获得的最大happy值.

  2. 状态计算

代码实现

#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

const int N = 6010;

int f[N][2];
int h[N], ne[N], e[N], idx;
int happy[N], has_fa[N];
int n;

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

void dfs(int u)
{
    f[u][1] = happy[u];
    
    for(int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        dfs(j);
        
        f[u][0] += max(f[j][0], f[j][1]);
        f[u][1] += f[j][0];
    }
}

int main()
{
    cin >> n;
    
    for(int i = 1; i <= n; i ++)
        scanf("%d", &happy[i]);
    
    memset(h, -1, sizeof(h));
    for(int i = 0; i < n - 1; i ++)
    {
        int a, b;
        scanf("%d %d", &a, &b);
        add(b, a);
        has_fa[a] = true;
    }
    
    int root = 1;
    while(has_fa[root])root++;
    
    dfs(root);
    
    printf("%d", max(f[root][0], f[root][1]));
    
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值