相信很多小伙伴刷题的时候面对力扣上近两千道题目,感觉无从下手,我花费半年时间整理了Github项目:「力扣刷题攻略」https://github.com/youngyangyang04/leetcode-master。 里面有100多道经典算法题目刷题顺序、配有40w字的详细图解,常用算法模板总结,以及难点视频讲解,按照list一道一道刷就可以了!star支持一波吧!

337.打家劫舍 III

题目链接:https://leetcode-cn.com/problems/house-robber-iii/

在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。

计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。

思路

这道题目和 198.打家劫舍,213.打家劫舍II也是如出一辙,只不过这个换成了树。

如果对树的遍历不够熟悉的话,那本题就有难度了。

对于树的话,首先就要想到遍历方式,前中后序(深度优先搜索)还是层序遍历(广度优先搜索)。

本题一定是要后序遍历,因为通过递归函数的返回值来做下一步计算

与198.打家劫舍,213.打家劫舍II一样,关键是要讨论当前节点抢还是不抢。

如果抢了当前节点,两个孩子就不是动,如果没抢当前节点,就可以考虑抢左右孩子(注意这里说的是“考虑”

暴力递归

代码如下:

class Solution {
public:int rob(TreeNode* root) {if (root == NULL) return 0;if (root->left == NULL && root->right == NULL) return root->val;// 偷父节点int val1 = root->val;if (root->left) val1 += rob(root->left->left) + rob(root->left->right); // 跳过root->left,相当于不考虑左孩子了if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right,相当于不考虑右孩子了// 不偷父节点int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子return max(val1, val2);}
};
  • 时间复杂度:O(n^2) 这个时间复杂度不太标准,也不容易准确化,例如越往下的节点重复计算次数就越多
  • 空间复杂度:O(n) 算上递推系统栈的空间

当然以上代码超时了,这个递归的过程中其实是有重复计算了。

我们计算了root的四个孙子(左右孩子的孩子)为头结点的子树的情况,又计算了root的左右孩子为头结点的子树的情况,计算左右孩子的时候其实又把孙子计算了一遍。

记忆化递推

所以可以使用一个map把计算过的结果保存一下,这样如果计算过孙子了,那么计算孩子的时候可以复用孙子节点的结果。

代码如下:

class Solution {
public:unordered_map<TreeNode* , int> umap; // 记录计算过的结果int rob(TreeNode* root) {if (root == NULL) return 0;if (root->left == NULL && root->right == NULL) return root->val;if (umap[root]) return umap[root]; // 如果umap里已经有记录则直接返回// 偷父节点int val1 = root->val;if (root->left) val1 += rob(root->left->left) + rob(root->left->right); // 跳过root->leftif (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right// 不偷父节点int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子umap[root] = max(val1, val2); // umap记录一下结果return max(val1, val2);}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n) 算上递推系统栈的空间

动态规划

在上面两种方法,其实对一个节点 投与不投得到的最大金钱都没有做记录,而是需要实时计算。

而动态规划其实就是使用状态转移容器来记录状态的变化,这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。

这道题目算是树形dp的入门题目,因为是在树上进行状态转移,我们在讲解二叉树的时候说过递归三部曲,那么下面我以递归三部曲为框架,其中融合动规五部曲的内容来进行讲解

  1. 确定递归函数的参数和返回值

这里我们要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。

参数为当前节点,代码如下:

vector<int> robTree(TreeNode* cur) {

其实这里的返回数组就是dp数组。

所以dp数组(dp table)以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。

所以本题dp数组就是一个长度为2的数组!

那么有同学可能疑惑,长度为2的数组怎么标记树中每个节点的状态呢?

别忘了在递归的过程中,系统栈会保存每一层递归的参数

如果还不理解的话,就接着往下看,看到代码就理解了哈。

  1. 确定终止条件

在遍历的过程中,如果遇到空间点的话,很明显,无论偷还是不偷都是0,所以就返回

if (cur == NULL) return vector<int>{0, 0};

这也相当于dp数组的初始化

  1. 确定遍历顺序

首先明确的是使用后序遍历。 因为通过递归函数的返回值来做下一步计算。

通过递归左节点,得到左节点偷与不偷的金钱。

通过递归右节点,得到右节点偷与不偷的金钱。

代码如下:

// 下标0:不偷,下标1:偷
vector<int> left = robTree(cur->left); // 左
vector<int> right = robTree(cur->right); // 右
// 中
  1. 确定单层递归的逻辑

如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0]; (如果对下标含义不理解就在回顾一下dp数组的含义

如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]);

最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}

代码如下:

vector<int> left = robTree(cur->left); // 左
vector<int> right = robTree(cur->right); // 右 // 偷cur
int val1 = cur->val + left[0] + right[0];
// 不偷cur
int val2 = max(left[0], left[1]) + max(right[0], right[1]);
return {val2, val1};
  1. 举例推导dp数组

以示例1为例,dp数组状态如下:(注意用后序遍历的方式推导

最后头结点就是 取下标0 和 下标1的最大值就是偷得的最大金钱

递归三部曲与动规五部曲分析完毕,C++代码如下:

class Solution {
public:int rob(TreeNode* root) {vector<int> result = robTree(root);return max(result[0], result[1]);}// 长度为2的数组,0:不偷,1:偷vector<int> robTree(TreeNode* cur) {if (cur == NULL) return vector<int>{0, 0};vector<int> left = robTree(cur->left);vector<int> right = robTree(cur->right);// 偷curint val1 = cur->val + left[0] + right[0];// 不偷cur int val2 = max(left[0], left[1]) + max(right[0], right[1]);return {val2, val1};}
};
  • 时间复杂度:O(n) 每个节点只遍历了一次
  • 空间复杂度:O(n) 算上递推系统栈的空间

总结

这道题是树形DP的入门题目,通过这道题目大家应该也了解了,所谓树形DP就是在树上进行递归公式的推导。

所以树形DP也没有那么神秘!

只不过平时我们习惯了在一维数组或者二维数组上推导公式,一下子换成了树,就需要对树的遍历方式足够了解!

大家还记不记得我在讲解贪心专题的时候,讲到这道题目:贪心算法:我要监控二叉树!,这也是贪心算法在树上的应用。那我也可以把这个算法起一个名字,叫做树形贪心,哈哈哈

“树形贪心”词汇从此诞生,来自「代码随想录」(纯属娱乐)

PDF开放下载

以下资源由「代码随想录」原创出品!

  • 回溯算法学习手册开放下载!!
  • 二叉树学习手册开放下载!!
  • 贪心算法学习手册开放下载!!

(后续Carl将陆续整理各个专题的PDF下载版本)

我是程序员Carl,可以找我组队刷题,也可以在B站上找到我,关注公众号代码随想录来和上万录友一起打卡学习算法,来看看,你会发现相见恨晚!

如果感觉对你有帮助,不要吝啬给一个

「代码随想录」337.打家劫舍III 【动态规划】力扣详解!相关推荐

  1. 104. Leetcode 337. 打家劫舍 III (动态规划-打家劫舍)

    步骤一.确定递归函数的参数和返回值: dp数组及下标的含义 dp数组的长度为2, 下标为0记录不偷该节点所得到的的最大金钱,下标为1记 录偷该节点所得到的的最大金钱. 步骤二.确定终止条件: 在遍历的 ...

  2. c++时间片轮转rr进程调度算法_「学习笔记」时间片轮转(RR)调度算法(详解版)...

    关键词:时间, 进程, 调度, 队列, 切换 时间片轮转(RR)时间片轮转(RR)调度算法是专门为分时系统设计的.它类似于 FCFS调度,但是增加了抢占以切换进程. 该算法中,将一个较小时间单... ...

  3. 代码随想录Day48|198.打家劫舍、213.打家劫舍II、337.打家劫舍III

    文章目录 198.打0家劫舍 213.打家劫舍II 337.打家劫舍III 198.打0家劫舍 文章讲解:代码随想录 (programmercarl.com) 题目链接:198. 打家劫舍 - 力扣( ...

  4. 新功能又来啦!这次是「代码搜索」和视频直播!

    不知不觉又到周五,菌菌又带着新功能来啦! 代码搜索功能发布,提升开发效率 开发一个项目,配置参数是必不可少的步骤,而项目规模越大需要配置的参数就越多.怎么样?是不是已经开始头疼了?dengdengde ...

  5. 如何排版 微信公众号「代码块」之 MarkEditor

    前段时间写过一篇文章 如何排版微信公众号「代码块」,讲的是如何使用浏览器插件 Markdown Here 来排版代码块.虽然用 Markdown Here 排版出来的样式还不错,但存在一个问题,就是代 ...

  6. 「代码家」的学习过程和学习经验分享【转】

    图灵丛书的一句话说的很好,Standing on the shoulders of giants,是的,我们一直站在巨人的肩上,我们起步都在沿着他们的轨迹前行,之后慢慢的在前人的开发基础或者规范上写出 ...

  7. 如何排版 微信公众号「代码块」

    最近博主刚开通微信公众号「石佳劼的博客」,被微信公众平台的图文编辑器折腾的不轻,如果文章中包含「代码块」,怎么排版都显得杂乱无章.之前一直用 Markdown 写作,从来没有考虑过排版.样式问题,因为 ...

  8. day40|198.打家劫舍、213.打家劫舍II、337.打家劫舍III

    198.打家劫舍 你是一个专业的小偷,计划偷窃沿街的房屋.每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 ...

  9. Day48 | 198. 打家劫舍 | 213.打家劫舍Il |337. 打家劫舍 III

    198. 打家劫舍 注意点: 1. 递推公式,dp[j-2]+nums[j]表示的是加上nums[j]后dp[j]的值, dp[j-1]表示的是不加nums[j]后dp[j-1]的最大值 class ...

  10. 337.打家劫舍 III 121. 买卖股票的最佳时机

    337.打家劫舍 III 树形dp,采用后序遍历,用dp[0],dp[1] 分别表示不取当前物品,和取当前物品的所获最大利润 /*** Definition for a binary tree nod ...

最新文章

  1. 图像处理之基础---高斯低通滤波在指定区域画放大圆形图
  2. 拼团小程序源码_纯小白如何做一个摄影电商拼团小程序?
  3. javaweb 之javascript 结合
  4. 《Linux4.0设备驱动开发详解》笔记--第七章:Linux设备中的并发控制
  5. php 处理raw数据,PHP用HTTP_RAW_POST_DATA来接收post过来的数据
  6. Eclipse与JDK多版本兼容性设置
  7. java jnlp_使用Java 7启动JNLP时的NoSuchAlgorithmException
  8. UE4碰撞射线检测2
  9. rpiplay实现树莓派AirPlay投屏器
  10. /proc/sysrq-trigger
  11. java高级过滤器、baseServlet、log4j配置使用poi实现excel导入数据库
  12. Oracle免登录下载JDK(实测有效)
  13. IMAP协议RFC3501中文文档
  14. 全球与中国圆顶隔膜阀市场发展动态及投资趋向建议报告2021年版
  15. 域控无法同步OUTLOOK提示“该姓名与地址列表中的的姓名不匹配”
  16. 网络工程师的敲门砖,2022最新HCIA-Datacom题库H12-811首发分享
  17. hrbust 2188 星际旅行
  18. mongodb期末考试总结
  19. 神经网络应用实例 -- python
  20. 运用物理信息神经网络求解流体力学方程

热门文章

  1. jQuery中的阻止默认行为
  2. 一天一小段js代码(no.4)
  3. ak330s 置换内外sd
  4. 【Debug】IAR在线调试时报错,Warning: Stack pointer is setup to incorrect alignmentStack,芯片使用STM32F103ZET6...
  5. Vova and Train (codeforces 1066A)
  6. mysql8采用caching-sha2-password加密
  7. 在NodeJS中使用Redis缓存数据
  8. 正则表达式中符号的含义(可能不是很全)
  9. SharePoint2010内容类型剖析(三)
  10. matlab图片集成成视频