树形动态规划——算法系列教程(c++版)

梦想不会自己发光,真正闪耀的是那个为梦狂奔的你。献给知行的孩子们!(Eric.He著)


  动态规划的本质:它是一种 “以空间换时间”的优化算法,核心是把复杂问题拆解为多个重叠的子问题,通过记录子问题的最优解(状态),避免重复计算,最终由子问题的最优解推导出原问题的最优解。

算法回顾

树形动态规划

  树形动态规划是解决树结构上的最优解问题,比如 “树的最大路径和”“打家劫舍 III(树形)”。这类问题的特点是问题定义在树上,子问题对应树的子树,状态转移依赖于节点的子节点状态,解题时通常采用后序遍历(先求解子树,再求解当前节点)。

教程目录导航

一、二叉树的直径(LeetCode 543)

问题描述:

算法解析:

  1. 定义状态:dp[root] 表示以root为根的子树中,从root向下延伸的最长路径长度(边数)。
  2. 边界条件:dp[null] = 0(空节点没有向下路径,长度为 0)
  3. 状态转移:dp[root] = max(dp[root.left], dp[root.right]) + 1(+1 是 root 到子节点的边)
  4. 遍历顺序:按树的递归顺序(后序遍历,先子后父)
  5. max_diameter 全局最优解

#include <iostream>
#include <vector>
#include <unordered_map>
#include"LinkedBSTree.h"
using namespace std;

int max_diameter; // 全局最优解(直径)
unordered_map<BSTNode<int>*, int> dp; // 显式存储dp[root]:key=节点,value=向下最长路径长度

// 计算dp[root],并更新全局直径
int calculateDP(BSTNode<int>* root) {
    // 边界条件:空节点,dp值为0
    if (root == nullptr) {
        dp[root] = 0;
        return 0;
    }

    // 记忆化:如果已经计算过当前节点的dp值,直接返回(避免重复计算)
    if (dp.find(root) != dp.end()) {
        return dp[root];
    }

    // 状态转移:计算左右子节点的dp值(子问题)
    int left_dp = calculateDP(root->left);  // dp[left]
    int right_dp = calculateDP(root->right); // dp[right]

    // 更新全局最优解:当前节点作为拐点的路径长度 = dp[left] + dp[right]
    max_diameter = max(max_diameter, left_dp + right_dp);

    // 计算当前节点的dp值,并存储(记忆化)
    dp[root] = max(left_dp, right_dp) + 1;

    return dp[root];
}

int main() {
    LinkedBSTree<int> tree;

        tree.insertNode(3);
        tree.insertNode(1);
        tree.insertNode(4);
        tree.insertNode(2);
        tree.insertNode(5);
    
        //tree.levelOrder();
        BSTNode<int>* root = tree.getRoot();
        calculateDP(root);
        cout << max_diameter ;

    return 0;
} 
        

输出结果


4
        

二、二叉树的最大路径和(LeetCode 124)

问题描述:

算法解析:

  1. 定义状态:dp[root] 表示以 root 为起点,向下延伸的最大路径和(路径只能向子节点延伸,不能分叉)。
  2. 状态转移:dp[root] = root.val + max(dp[root.left], dp[root.right], 0):
    • 加 root.val 是因为路径必须包含当前节点;
    • max(..., 0) 是因为如果子树的路径和为负,不如不选(选了反而拉低当前路径和);
  3. 全局最优解更新:按树的递归顺序(后序遍历,先子后父)
    • 对每个节点 root,以其为拐点的路径和 = root.val + dp[root.left] + dp[root.right];
    • 遍历所有节点,取该值的最大值即为全局最大路径和。

#include <iostream>
#include <vector>
#include <algorithm>
#include <climits> // 用于INT_MIN
#include"LinkedBSTree.h"

using namespace std;

int max_sum; // 全局最大路径和

// 递归函数:返回以root为起点向下延伸的最大路径和(局部状态dp[root])
int maxPathSum(BSTNode<int>* root) {
    if (root == nullptr) {
        return 0; // 空节点的局部路径和为0(无贡献)
    }

    // 计算左右子树的局部路径和(如果为负则取0,即不选该子树)
    int left_dp = max(maxPathSum(root->left), 0);
    int right_dp = max(maxPathSum(root->right), 0);

    // 更新全局最大路径和:当前节点作为拐点的路径和 = 根值 + 左局部 + 右局部
    max_sum = max(max_sum, root->data + left_dp + right_dp);

    // 返回当前节点的局部路径和:根值 + 左右局部的最大值(只能选一个方向延伸)
    return root->data + max(left_dp, right_dp);
}

int main() {
    max_sum = INT_MIN; // 初始化为最小整数(应对全负数的情况)
    LinkedBSTree<int> tree;

    tree.insertNode(2);
    tree.insertNode(1);
    tree.insertNode(3);

    //tree.levelOrder();
    BSTNode<int>* root = tree.getRoot();
    maxPathSum(root);
    cout << max_sum ;

    return 0;
}
        

输出结果


6
            

三、打家劫舍 III(LeetCode 337)

问题描述:

算法解析:

  1. 定义状态:对每个节点 root,定义一个长度为 2 的数组 dp[root]:
    • dp[root][0]:不选当前节点 root 时,以 root 为根的子树能盗取的最大金额;
    • dp[root][1]:选当前节点 root 时,以 root 为根的子树能盗取的最大金额。
  2. 状态转移:
    • 选当前节点 root(dp[root][1]):则左右子节点都不能选,即 dp[root][1] = root.val + dp[left][0] + dp[right][0];
    • 不选当前节点 root(dp[root][0]):则左右子节点可选可不选,取最大值,即 dp[root][0] = max(dp[left][0], dp[left][1]) + max(dp[right][0], dp[right][1]);
  3. 边界条件:空节点的两种状态都是 0(dp[null][0] = dp[null][1] = 0)。

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

// 递归函数:返回当前节点的dp数组({不选当前节点的最大值,选当前节点的最大值})
pair<int, int> rob(BSTNode<int>* root) {
    // 边界条件:空节点,两种状态都是0
    if (root == nullptr) {
        return {0, 0};
    }

    // 递归计算左右子节点的dp状态(后序遍历,先子后父)
    auto [left_not, left_choose] = rob(root->left);
    auto [right_not, right_choose] = rob(root->right);

    // 状态转移1:选当前节点 → 左右子节点都不能选
    int choose = root->data + left_not + right_not;

    // 状态转移2:不选当前节点 → 左右子节点可选可不选,取最大值相加
    int not_choose = max(left_not, left_choose) + max(right_not, right_choose);

    // 返回当前节点的两种状态
    return {not_choose, choose};
}

int main() {
    LinkedBSTree<int> tree;

    tree.insertNode(5);
    tree.insertNode(3);
    tree.insertNode(6);
    tree.insertNode(4);
    tree.insertNode(7);

    //tree.levelOrder();
    BSTNode<int>* root = tree.getRoot();
    auto [not_choose_root, choose_root] = rob(root);
    cout << max(not_choose_root, choose_root) ;

    return 0;
}
        

输出结果


16
            

返回顶部