打家劫舍:从线性到树形,彻底玩转动态规划
发表于更新于
字数总计:1.3k阅读时长:4分钟
哈喽,大家好呀,我是蔚蓝🥰!今天我们来聊聊 DP 专题里最像「犯罪现场模拟」的一个系列——打家劫舍 (House Robber)。
很多小伙伴刚接触动态规划时,觉得这题挺逗,怎么还教人「偷东西」呢?其实,这道题的精髓不在于「偷」,而在于如何处理邻居之间的制约关系。
今天这一篇,我把笔记里从简单的路边排房,到成环的社区,再到那棵让人抓狂的「老大的办公楼」,全部梳理清楚。核心代码都是最纯粹的 Java 版,保证你看完就能上手!
1. 关卡一:线性社区 (HR I) —— 别碰隔壁老王
如果你面前是一排房子,每家都有金币,但只要你连着偷两家,警报就会响。这就是最经典的一维 DP。
很多人第一反应是:我选奇数位还是偶数位?大错特错! 算法的世界里,你要做的是不断决策。

- 一眼识别状态:走到第
i 家房门口,我只有两个选择:
- 偷! 那我不仅能拿这家的钱,还得保证前一家没动身。
- 不偷! 那我能拿到的最大值,就是我走到前一家门口时的战果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class Solution { public int rob(int[] nums) { if (nums.length == 0) return 0; if (nums.length == 1) return nums[0];
int[] dp = new int[nums.length]; dp[0] = nums[0]; dp[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < nums.length; i++) { dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]); } return dp[nums.length - 1]; } }
|
2. 关卡二:社区成环 (HR II) —— 咬到尾巴的贪吃蛇
当这排房子首尾相连变成一个社区时,事情变邪乎了:你偷了第一家,最后一家就不能动。
这其实是一个 「破圈」 的过程。我们不需要复杂的联立方程,直接暴力拆解成两种情况:
- 不管最后一家:只算
[第一家, 倒数第二家]。
- 不管第一家:只算
[第二家, 最后一家]。
最后两者取大值,完美化解死循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class Solution { public int rob(int[] nums) { if (nums.length == 1) return nums[0]; int result1 = robRange(nums, 0, nums.length - 2); int result2 = robRange(nums, 1, nums.length - 1); return Math.max(result1, result2); }
private int robRange(int[] nums, int start, int end) { if (start == end) return nums[start]; int[] dp = new int[nums.length]; dp[start] = nums[start]; dp[start + 1] = Math.max(nums[start], nums[start + 1]); for (int i = start + 2; i <= end; i++) { dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]); } return dp[end]; } }
|
3. 关卡三:树形办公楼 (HR III) —— 擒贼先擒王
这是本系列的终极变体。房子不再是排成队,而是长成了一棵二叉树。邻居不再是左右,而是「父与子」。

这时候,线性遍历失效了,我们需要派侦察兵去深度优先搜索 (DFS),并且要按照 「由下至上」 的财务汇报逻辑(后序遍历)。
- :
每个节点都要向老板汇报两个数字:
{我不偷能拿多少, 我偷能拿多少}。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class Solution { public int rob(TreeNode root) { int[] res = robAction(root); return Math.max(res[0], res[1]); }
private int[] robAction(TreeNode root) { if (root == null) return new int[]{0, 0}; int[] left = robAction(root.left); int[] right = robAction(root.right); int v1 = root.val + left[0] + right[0]; int v0 = Math.max(left[0], left[1]) + Math.max(right[0], right[1]); return new int[]{v0, v1}; } }
|
4. 避坑指南
在刷这系列题时,我总结了 3 个血泪教训:
- 初始化的坑:别忘了处理
nums.length == 1。很多人一顿操作猛如虎,结果在 dp[1] 数组越界上翻了车。
- 树形 DP 的顺序:一定要是 「后序遍历」!你得先搞清楚手下人赚了多少,你这个当老大的才有资格做最后决定。
- 别想太多递归:原生的递归如果不用记忆化,那就是典型的「一人算错,全家累死」。能用数组存(DP),就别玩虚的。
🥰 碎碎念
算法其实就像生活里的资源配置。你不能既要又要,你必须在「收益」和「制约」之间找到那个微妙的平衡点。
如果你能一口气说清楚这三道题的区别,恭喜你,动态规划的状态方程对你来说已经不再是天书。
下一篇,咱们聊聊那个更硬核的「背包系列」?或者你还有哪道题还没绕出来,评论区告诉我!