第五章 动态规划(7):树形DP模型

本文详细探讨了动态规划在解决树形结构问题中的应用,特别是最大独立集问题。通过实例分析了如何利用动态规划解决没有上司的舞会、树的最长路径(树的直径)、树的中心等经典问题。文章还讨论了网络延时、数字转换、二叉苹果树、战略游戏和皇宫看守等问题,展示了动态规划在树形结构中寻找最优解的方法。
摘要由CSDN通过智能技术生成

1、没有上司的舞会(最大独立集问题)

ACWing 285

集合
f(u,0)、f(u,1)
f(u,0):所有从以u为根的子树中选择的方案,选择并且不选u这个点的方案
f(u,1):所有从以u为根的子树中选择的方案,选择并且选u这个点的方案

集合划分
在这里插入图片描述

状态计算

f(u, 0) = sum(max(f(si, 0), f(si, 1)))
f(u, 1) = sum(f(si, 0))(因为选了根结点,子结点就不可以选择)

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

using namespace std;

const int N = 6010;

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]; // 选择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()
{
   
    scanf("%d", &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 ++ ) //读入n-1条边
    {
   
        int a, b;
        scanf("%d%d", &a, &b);
        has_father[a] = true;
        add(b, a);
    }
    
    // 因为这个题没有告诉我们根结点是谁,所以需要自己寻找
    int root = 1;
    while (has_father[root]) root ++ ;
    
    dfs(root);
    
    printf("%d\n", max(f[root][0], f[root][1]));
    
    return 0;
}

补充:LeetCode 337 打家劫舍 III

题目链接

这个题与上一题一模一样,但是要注意代码的实现。

2、树的最长路径 / 树的直径

ACwing 1072

最经典的问题是不带权值的树的直径,也就是边数最长的两个结点之间的距离。
常用做法是:

1. 任取一点作为起点,找到距离该点最远的一个点u。然后再BFS
  (能使用BFS就不使用DFS,因为考试C++栈仅有1M大小,如果DFS层数过多,会爆栈)
2. 再找到距离u最远的一个点v。然后继续BFS。
3. 那么u和v之间的路径就是一条最长直径。

结论:任取树中一个点a,找到距离a最远的点u,则u一定是某条直径上的端点。如果证明了u是某条直径的端点,从u开始求出来的距离u最远的点vuv之间的路径一定就是树最长路径 / 直径。
证明:
反证法:若距离任意一点a的最远点u不是任意一条直径上的某一个端点,假设树的真正直径为bc之间的连线。下面分情况讨论:

  1. 若连线au与连线bc之间没有交点。在aubc上各取一个点分别为xy,由于树是连通的,即a、u、b、c四个点之间可以相互连通,那么xy之间也可以相互连通,所以就有如下图。
    又由我们之前的假设可知: ① ≥ ② + ③ ①\ge②+③ +,即 ① + ② ≥ ① − ② ≥ ③ ①+②\ge①-②\ge③ +,所以路径byxu的长度大于等于路径bc的长度,与假设矛盾;
    在这里插入图片描述
  2. 若连线au与连线bc之间有交点。同理, ① ≥ ② ①\ge② ,那么路径bxu的长度大于等于路径bc的长度,与假设矛盾。在这里插入图片描述

证毕。

现在讨论扩展之后,即加上权值之后的树的最长路径问题。

按照定义,我们要求出树的所有直径,然后在这所有直径中取一个最小值即可。从集合角度思考,需要先将这些直接分成若干类,在这每一类中去寻找一个最大值,最后在所有最大之中取一个最大值即可。

路径分类

任取一个点,我们将其作为根结点,对于每一条直径,我们将其归类到这条路径中最高的点的这一类中。于是我们可以以每个点来分类,所以集合划分就是所有路径上最高的点是该点的路径的集合。

如何求出所有挂到该点上的所有路径长度的最大值呢?可以先将该结点的所有子结点往下走的所有路径长度最大值先求出来。对于挂在该点上的所有路径可分为两大类:

  • 一直往下走,直到走到叶子结点。它的路径最大长度就是枚举每一个子结点往下走的路径的最大距离,然后加上到根结点的1即可;
  • 路径穿过了该结点,往左子树或者右子树下面走。该结点的路径最大长度为从这两个方向下去的所有最长路径长度的最长的一条路径和第二长的一条路径的和。所以可以先求出每一个子结点的路径最大值,再将所有的最大值中取一个最大值即可。

题解参考

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

using namespace std;

const int N = 1e4 + 10, M = N << 1;  //初始不确定树的拓扑结构,因此要建立双向边

int n;
int h[N], e[M], w[M], ne[M], idx;
int f1[N], f2[N], res;

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

void dfs(int u, int father) //father表示u的父节点,因为该图为无向图,并且迭代过程中不能回到父节点,所以要特殊标记
{
   
    f1[u] = f2[u] = 0; //以节点u为根的子树中,从子树某个节点到u的最长路为f1,次长路径为f2

    for (int i = h[u]; i != -1; i = ne[i]) // 遍历邻边
    {
   
        int j = e[i];
        if (j == father) continue; //如果是父节点, 则不走
        dfs(j, u);

        if (f1[j] + w[i] >= f1[u]) f2[u] = f1[u], f1[u] = f1
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值