树形dp详解

树形dp,就是根据题意需要建一颗树并且在上面使用dp。树形dp一般采用递归的形式,因为树形dp一般需要用深度深的节点的状态来更新深度浅的的节点的状态。

基础树形dp

那么接下来就结合P1352 没有上司的舞会来讲解一下基础树形dp。

这个题目的意思还是比较清晰的,无疑采用树形dp求解。首先建立一颗树,将上司的节点连一条有向边到下属的节点,因为树形dp是从上而下的。点权另外存入一个数组中。

然后我们设 f ( u , 1 ) f(u,1) f(u,1)表示 u 职员来的情况下,在以ta为根的子树中最大快乐指数; f ( u , 0 ) f(u,0) f(u,0)表示 u 职员不来的情况下,在以ta为跟的子树中最大快乐指数。

根据题意:如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了 , 我们可以很容易地得出状态转移方程: f ( u , 1 ) f(u,1) f(u,1) = ∑ f ( v , 0 ) f(v,0) f(v,0) f ( u , 0 f(u,0 f(u,0 = ∑max{ f ( v , 0 ) f(v,0) f(v,0) f ( v , 1 ) f(v,1) f(v,1)}( v v v表示 u u u的子节点)。

解释一下,当 u 要来的时候,v 一定来不了,因为题意说了这个职员来了那么ta的直接下属不会来;那么当 u 不来的时候, 就可以取 v 来或者不来的最大值。

要注意这题需要找根节点,从根节点开始向下递归。

code:

#include <bits/stdc++.h>
using namespace std;
const int N = 7e3+10;
int e[N<<1],nex[N<<1],h[N],idx;
int f[N][2],n,a[N];
bool isf[N] , vis[N];
void add(int a,int b){
    e[++idx] = b;
    nex[idx] = h[a];
    h[a] = idx;
}
void tree_dp(int u){
    for(int i=h[u];i;i=nex[i]){
        int v = e[i];
        tree_dp(v);
        f[u][0] += max(f[v][0] , f[v][1]);
        f[u][1] += f[v][0];
    }
}
int main(){
    cin >> n;
    for(int i=1;i<=n;i++) cin >> f[i][1];// 直接存入当某个职员来的时候的初始值
    for(int i=1;i<n;i++){
        int a,b;
        cin >> a >> b;
        isf[a] = 1;// a 不可能是根节点
        add(b , a);
    }
    int root;
    for(int i=1;i<=n;i++){
        if(!isf[i]) {//找根
            root = i;
            break;
        }
    }
    tree_dp(root);
    cout << max(f[root][1] , f[root][0]);// 最后要取校长来或者不来的最大值
    return 0;
}

相关练习:

(1)P1122 最大子树和

这个题目还是比较水的。

首先我们设 f ( u ) f(u) f(u)表示以 u 为根的子树中经过“修剪”后的美丽指数最大值。

根据题意我们很容易得出状态转移方程: f [ u ] = ∑ f [ v ] ( f [ v ] ≥ 0 ) f[u] = ∑f[v](f[v]\ge0) f[u]=f[v](f[v]0)

ps:此处为了区分与小括号使用中括号

因为题意中有负数,所以很明显我们只需要使得算上这颗子树后和更大即可取这颗子树,否则直接减去不要。

(2)P2016 战略游戏
这个题目也算比较水。

我们设 f ( u , 1 ) f(u,1) f(u,1)表示在以 u u u点为根的子树上,在 u u u点上放置一个士兵时所放置士兵的最小数, f ( u , 0 ) f(u,0) f(u,0)表示在以 u u u点为根的子树上,在 u u u点上不放置士兵时所放置士兵的最小数。

可能有点绕但是仔细阅读理解一下

根据题目意思我们可以很容易地得出状态转移方程: f ( u , 1 ) = ∑ m i n f ( v , 1 ) , f ( v , 0 ) f(u,1) = ∑min{f(v,1),f(v,0)} f(u,1)=minf(v,1),f(v,0) f ( u , 0 ) = ∑ f ( v , 1 ) f(u,0) = ∑f(v,1) f(u,0)=f(v,1)

注意 f ( u , 1 ) f(u,1) f(u,1)要初始化为 1 1 1 ,不然全是 0 0 0

至于原因就是当 u u u节点有士兵的时候和它相邻的边可以有士兵也可以没有士兵,但是如果 u u u节点上没有士兵,那么它的儿子节点 v v v必须有士兵。

(3)P2458 [SDOI2006]保安站岗

这个题目比前面两题难亿点(

刚看完题目,你可能会觉得这就是个有权的战略游戏,但是码完之后发现假了。其实原因就在于战略游戏那题的士兵看的时,而这题的保安看的是

这就导致了很大的区别,如果在战略游戏中我们最多只能做到放 不放 放而在这题中可以放 不放 不放 放

所以我们的设的状态会复杂一点: f ( u , 0 ) f(u,0) f(u,0)表示为点 u u u被父节点看守, f ( u , 1 ) f(u,1) f(u,1)表示为点 u u u被子节点看守, f ( u , 2 ) f(u,2) f(u,2)表示为点 u u u被自己看守。

那么我们很容易得出状态转移方程: f ( u , 0 ) = ∑ m i n f ( v , 1 ) , f ( v , 2 ) f(u,0)=∑min{f(v,1),f(v,2)} f(u,0)=minf(v,1),f(v,2)
u u u被父亲节点看守的时候,它的儿子 v v v只要不被 u u u看守都没有问题。

f ( u , 1 ) = ∑ m i n f ( v , 1 ) , f ( v , 2 ) f(u,1)=∑min{f(v,1),f(v,2)} f(u,1)=minf(v,1),f(v,2) 但是保证有一个 v v v必须是自己看守自己

可以抽象为: f ( u , 1 ) = ∑ m i n f ( v , 1 ) , f ( v , 2 ) − m i n f ( v , 1 ) , f ( v , 2 ) + f ( v , 2 ) f(u,1)=∑min{f(v,1),f(v,2)}−min{f(v,1),f(v,2)}+f(v,2) f(u,1)=minf(v,1),f(v,2)minf(v,1),f(v,2)+f(v,2)

可能会有一点难理解,其实就是因为这个节点要被其中一个 v v v看守,所以必须保证取其中一个 v v v的状态为 f ( v , 2 ) f(v,2) f(v,2)即这个 v v v上有保安。之所以这么弄是重新遍历一次,保证一定有一次是取上了 f ( v , 2 ) f(v,2) f(v,2)

f ( u , 2 ) = ∑ m i n f ( v , 0 ) , f ( v , 1 ) , f ( v , 2 ) + v a l [ u ] f(u,2)=∑min{f(v,0),f(v,1),f(v,2)}+val[u] f(u,2)=minf(v,0),f(v,1),f(v,2)+val[u]

u u u被自己看守的时候,它的儿子 v v v可以是任意状态。但是注意最后要加上这个点的费用 v a l val val

dp函数部分代码:

void dp(int u){
    f[u][2] += w[u];
    int sum = 0;
    for(int i = h[u];i ;i = nex[i]){
        int v = e[i];
        dp(v);
        f[u][2] += min(f[v][2] , min(f[v][1] , f[v][0]));
        sum += min(f[v][1] , f[v][2]) ;
    }
    f[u][0] += sum;
    f[u][1] = INF;
    for(int i = h[u];i ;i = nex[i]){
        int v = e[i];
        f[u][1] = min(f[u][1] , sum - min(f[v][1] , f[v][2]) + f[v][2]);
    }
}

解释一下第二个循环部分:第一个循环中我们求出了 ∑ m i n f ( v , 1 ) , f ( v , 2 ) ∑min{f(v,1),f(v,2)} minf(v,1),f(v,2)存入 s u m sum sum,那么在第二个循环中直接遍历所有 v v v,然后找出当 v v v一定放置保安时和其他 v v v的状态和,这样的最小值就是 f ( u , 1 ) f(u,1) f(u,1)的结果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
树形动态规划(Tree DP)是一种常用的动态规划算法,用于解决树结构相关的问题。在Python中,可以使用递归或者迭代的方式实现树形DP树形DP的基本思想是,从树的叶子节点开始,逐层向上计算每个节点的状态,并利用已经计算过的节点状态来更新当前节点的状态。这样可以通过自底向上的方式,逐步计算出整个树的最优解。 下面是一个简单的示例,演示如何使用树形DP解决一个二叉树中节点权值之和的最大值问题: ```python class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def max_sum(root): if root is None: return 0 # 递归计算左右子树的最大权值和 left_sum = max_sum(root.left) right_sum = max_sum(root.right) # 当前节点的最大权值和为当前节点值加上左右子树中较大的权值和 return root.val + max(left_sum, right_sum) # 构建一个二叉树 root = TreeNode(1) root.left = TreeNode(2) root.right = TreeNode(3) root.left.left = TreeNode(4) root.left.right = TreeNode(5) # 计算二叉树中节点权值之和的最大值 result = max_sum(root) print(result) ``` 这段代码中,我们定义了一个`TreeNode`类来表示二叉树的节点,其中`val`表示节点的权值,`left`和`right`分别表示左子节点和右子节点。`max_sum`函数使用递归的方式计算二叉树中节点权值之和的最大值,通过比较左右子树的最大权值和来确定当前节点的最大权值和。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值