[树形dp] 没有上司的舞会(模板题+树形dp)

0. 前言

树形 dp 思维门槛高,但是跨过门槛之后,思维难度却不高。这点非常类似于

关键是要体会采用二进制数来表示状态的思想,要转变传统思维,学习接收并吸收这种思想。

1. 树形dp 模板题

285. 没有上司的舞会

在这里插入图片描述
简单模拟样例,便于理解:
在这里插入图片描述
样例的高兴度都是 1,那就是挑选没有相邻边的尽量多的点


树形 dp 思路:

重点: 状压 dp

思路:

  • 状态定义:
    • f[u][0]:所有从以 u 为根的子树中选择,并且不选 u 这个点的方案的最大高兴度
    • f[u][1]:所有从以 u 为根的子树中选择,并且选 u 这个点的方案的最大高兴度
  • 状态转移:
    • 分类依据:求解树形 dp 问题,大多都是递归来求解。即当求解根节点的时候,其所有儿子的状态都已经确定了。假设当前求解根节点为 u u u,其所有儿子节点为 s i s_i si。那么针对根节点的选法进行分类:
      • 不选根节点,即 f [ u ] [ 0 ] = Σ ( f [ s i ] [ 0 ] , f [ s i ] [ 1 ] ) f[u][0]=\Sigma (f[s_i][0],f[s_i][1]) f[u][0]=Σ(f[si][0],f[si][1])。其含义为:不选根节点时,其任一儿子节点选与不选均可,在两者中取最大值将其累加起来即可
      • 选根节点,即 f [ u ] [ 1 ] = Σ ( f [ s i ] [ 0 ] ) f[u][1]=\Sigma (f[s_i][0]) f[u][1]=Σ(f[si][0])。其含义为:选根节点时,其任一儿子节点均不能选,直接累加起来即可
  • 状态转移方程 f [ u ] [ 0 ] = Σ ( f [ s i ] [ 0 ] , f [ s i ] [ 1 ] ) f[u][0]=\Sigma (f[s_i][0],f[s_i][1]) f[u][0]=Σ(f[si][0],f[si][1]) f [ u ] [ 1 ] = Σ ( f [ s i ] [ 0 ] ) f[u][1]=\Sigma (f[s_i][0]) f[u][1]=Σ(f[si][0])
  • 状态初始化:首先需要建图,邻接表的头节点初始化为 -1。一开始当前节点去的话,则初始高兴度初始化为它自己 f[u][1]=happy[u],不去的话 f[u][0]=0
  • 返回答案f[root][0], f[root][1]
  • 时间复杂度: 一共有 2 n 2n 2n 个状态,每个状态在计算过程中需要枚举它所有的儿子,则所有节点儿子的数量总和就等于树中边的数量,边的数量是 n − 1 n-1 n1。则在状态计算时,故计算所有状态就是 O ( n ) O(n) O(n)

这个 dfs(root) 根节点是需要的,并不能像之前一样,随意找一个点进行 dfs。因为建图的时候采用的是单向边。

本题的物理结构相当于是一颗有根的树,其不适用线性 dp 的递推方式。而树形 dp 的这种递归求解的方式是恰好适用本题。

动态规划的本质是沿着一个拓扑序来确定状态的值的,对于一个树形结构而言,子节点的值远比根节点的值好确定,那么就是以叶子节点的值层层向上确定其父节点、根节点的值。这就是递归,且遵循拓扑序。所以采用 dfs 进行递归,且在回溯的时候进行状态计算因为在回溯的时候,其子节点的状态全部计算完毕了。

本题非常非常非常经典,是树形 dp 最经典题目。

代码:

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

using namespace std;

const int N = 6005;

int n;
int happy[N];
int h[N], e[N], ne[N], idx;
int f[N][2];
bool has_father[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 != -1; 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) cin >> happy[i];

    memset(h, -1, sizeof h);
    for (int i = 0; i < n - 1; ++i) {
        int a, b;
        cin >> a >> b;
        add(b, a);
        has_father[a] = true;
    }
    
    int root = 1;
    while (has_father[root]) root ++;
    
    dfs(root);
    
    cout << max(f[root][0], f[root][1]) << endl;
    
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值