代码随想录——动态规划31打家劫舍III(树状DP)

这道题目是 打家劫舍 III(House Robber III),是打家劫舍系列问题的变种。问题描述如下:

小偷发现了一个新的区域,这个区域的所有房屋排列类似于一棵二叉树。如果两个直接相连的房屋在同一晚被打劫,房屋会自动报警。给定这棵二叉树的根节点 root,求在不触发警报的情况下,小偷能够盗取的最高金额。
image


题目思路

1. 问题分析

  • 每个节点(房屋)有两个状态:不偷
  • 如果偷了当前节点,则不能偷其直接子节点(左子节点和右子节点)。
  • 如果不偷当前节点,则可以偷其子节点,也可以选择不偷子节点。
  • 目标是找到一种偷窃方案,使得总金额最大。

2. 解题方法

这道题有两种常见的解法:

  1. 记忆化搜索 + 递归
  2. 树形动态规划(树形DP)

不能用层序遍历化为一维数组再套用前面的打家劫舍。如以下情况不能解决
image


方法一:记忆化搜索 + 递归

思路

  • 对于每个节点,有两种选择:
    1. 偷当前节点:不能偷其左右子节点,但可以偷其孙子节点(左右子节点的子节点)。
    2. 不偷当前节点:可以考虑偷其左右子节点。
  • 使用递归计算这两种选择的最大值。
  • 记忆化搜索:每次偷爷爷节点时,又要重新计算孙子节点。但在此之前孙子节点已经计算过了。为了避免重复计算,使用 unordered_map<TreeNode*, int> 进行记忆化存储。

之前记忆化搜索一般使用数组(如斐波那契数列)缓存。但二叉树不适合拿数组当缓存,我们这次使用哈希表来存储结果

代码实现

class Solution {
public:
    unordered_map<TreeNode*, int> mp; // 记忆化存储

    int rob(TreeNode* root) {
        if (root == nullptr) return 0; // 空节点,返回0
        if (mp[root]) return mp[root]; // 如果已经计算过,直接返回

        // 偷当前节点
        int val1 = root->val;
        if (root->left) val1 += rob(root->left->left) + rob(root->left->right); // 偷孙子节点
        if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 偷孙子节点

        // 不偷当前节点
        int val2 = rob(root->left) + rob(root->right); // 偷子节点

        mp[root] = max(val1, val2); // 记录当前节点的最大值
        return mp[root];
    }
};

复杂度分析

  • 时间复杂度O(n),其中 n 是节点数量。每个节点只会被计算一次。
  • 空间复杂度O(n),用于存储记忆化结果和递归栈。

方法二:树形动态规划(树形DP)

方法一还要考虑孙子节点,能不能只考虑儿子——>记录每个节点选/不选的状态最大值

思路

  • 对于每个节点,返回一个长度为 2 的数组 dp
    • dp[0]:不偷当前节点时的最大金额。
    • dp[1]:偷当前节点时的最大金额。
  • 通过后序遍历(左右根)计算每个节点的 dp 值。
  • 最终结果是根节点的 dp[0]dp[1] 的最大值。

代码实现

  • 树形DP在树上进行状态转移,因此需要递归实现
class Solution {
public:
    int rob(TreeNode* root) {
        vector<int> result = robTree(root);
        return max(result[0], result[1]); // 返回偷或不偷根节点的最大值
    }

    vector<int> robTree(TreeNode* root) {
        if (root == nullptr) return {0, 0}; // 空节点,返回 {0, 0}

        vector<int> left = robTree(root->left); // 左子树的 dp 值
        vector<int> right = robTree(root->right); // 右子树的 dp 值

        // 不偷当前节点:左右子树可偷可不偷
        int val0 = max(left[0], left[1]) + max(right[0], right[1]);
        // 偷当前节点:左右子树不可偷
        int val1 = root->val + left[0] + right[0];

        return {val0, val1}; // 返回当前节点的 dp 值
    }
};

复杂度分析

  • 时间复杂度O(n),每个节点只会被访问一次。
  • 空间复杂度O(n),递归栈的深度最大为树的高度。

方法对比

方法 优点 缺点
记忆化搜索 + 递归 直观,易于理解 需要额外的空间存储记忆化结果
树形DP 空间效率更高,无需额外存储 需要理解状态转移方程

总结

  • 记忆化搜索 + 递归:通过递归遍历树,同时使用 unordered_map 存储中间结果,避免重复计算。
  • 树形DP:通过后序遍历,返回每个节点的状态值(偷或不偷),最终选择最大值。

两种方法的时间复杂度都是 O(n),但树形DP的空间效率更高,推荐使用树形DP。

posted @   NeroMegumi  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
点击右上角即可分享
微信分享提示