打家劫舍 图解题解
这道题到底在问什么
- 输入
- nums=[2,7,9,3,1,5,8,4,6,2]
- 输出
- 26
最优解:为什么这么做
一句话答案:LeetCode 198 打家劫舍的标准解是一维动态规划:dp[i] 表示考虑前 i+1 家能偷到的最大金额,每家只有「偷」与「不偷」两条路,dp[i] = max(dp[i-1], dp[i-2] + nums[i])。凭金额大小贪心会被「相邻不能同偷」锁死,必须两条路都算再取大。时间 O(n)、空间 O(1)。
打家劫舍的约束到底是什么
数组 nums 里每个数是一户人家的现金,你可以任选若干家下手,唯一的限制是不能偷相邻的两家,求能拿到的最大总金额。换句话说,这是在数组里选一批互不相邻的数,让它们的和最大。难点全在「相邻互斥」上:选了这家,左右两家就自动作废。
为什么挑钱多的先偷这种贪心不行
直觉做法是贪心:哪家钱多偷哪家。但金额大的那家可能正好卡在两户中等人家中间,偷了它就同时废掉两边,总和反而更小——局部最优会锁死全局最优。另一头,把所有「互不相邻的选法」全枚举出来又是指数级,算不动。
破局的观察是:站在第 i 家门口,无论前面怎么选,当下只有两种决策——偷这家,或不偷这家。而这两种决策的收益,都只取决于「前面几家的最优结果」,不取决于前面具体偷了哪几家。子问题重叠又能复用,动态规划顺势而出。
dp 状态怎么定义,初值为什么这么设
定义 dp[i] 为「只考虑前 i+1 家(下标 0 到 i)时能偷到的最大金额」,注意它的含义是最优值,不承诺第 i 家一定被偷。初值铺两个:dp[0] = nums[0],只有一家当然直接偷;dp[1] = max(nums[0], nums[1]),前两家相邻只能选一家,挑大的。dp[1] 容易被顺手写成 nums[1],那就漏了第 0 家更有钱的情形。
偷与不偷的转移方程为什么成立
对第 i 家做二选一:不偷,那么前 i+1 家的最优就是前 i 家的最优,直接沿用 dp[i-1];偷,则第 i-1 家必须放弃,收益是 dp[i-2] + nums[i]。第 i 家的所有选法必然落入这两类之一,所以 dp[i] = max(dp[i-1], dp[i-2] + nums[i]) 覆盖了全部可能,取大即最优。
这一步能成立还依赖一个隐含性质:一旦决定第 i 家偷不偷,前面子问题的最优解不会因为这个决定而失效——这就是无后效性,白话说就是「过去的最优怎么来的不重要,值对就行」。
复杂度怎么算,答案取哪一项
每家只做一次比较和一次加法,扫一遍数组即可,时间 O(n);dp[i] 只依赖前两项,用两个滚动变量轮替就够,空间 O(1),参考代码里 a, b = b, max(b, a + x) 一行完成。
两个易翻车点:一是答案取 dp[n-1] 而不是「最后一家偷的那条路」,因为最后一家完全可能不偷;二是姊妹题 LeetCode 213 把街道改成环形、首尾相邻,做法是拆成「不偷第一家」和「不偷最后一家」两条线性 DP,取两者较大者。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3记住这条二选一,下面每格都在套它。
- 4上行是每家金额(固定),下行 dp 待填。从最左开始。
- 5只有第 0 家,直接偷:dp[0]=2。
- 6前两家挑更有钱的那家:dp[1]=max(2,7)=7。
- 7算 dp[2]:不偷这家=沿用 7;偷这家=dp[0]+9=11(隔一家,避开 1)。
- 8取较大的 11 填进 dp[2]。偷这家更划算。
- 9算 dp[3]:不偷这家=沿用 11;偷这家=dp[1]+3=10(隔一家,避开 2)。
- 10取较大的 11 填进 dp[3]。不偷、保留前面更划算。
- 11算 dp[4]:不偷这家=沿用 11;偷这家=dp[2]+1=12(隔一家,避开 3)。
- 12取较大的 12 填进 dp[4]。偷这家更划算。
- 13算 dp[5]:不偷这家=沿用 12;偷这家=dp[3]+5=16(隔一家,避开 4)。
- 14取较大的 16 填进 dp[5]。偷这家更划算。
- 15算 dp[6]:不偷这家=沿用 16;偷这家=dp[4]+8=20(隔一家,避开 5)。
- 16取较大的 20 填进 dp[6]。偷这家更划算。
- 17算 dp[7]:不偷这家=沿用 20;偷这家=dp[5]+4=20(隔一家,避开 6)。
- 18取较大的 20 填进 dp[7]。两条一样。
- 19算 dp[8]:不偷这家=沿用 20;偷这家=dp[6]+6=26(隔一家,避开 7)。
- 20取较大的 26 填进 dp[8]。偷这家更划算。
- 21算 dp[9]:不偷这家=沿用 26;偷这家=dp[7]+2=22(隔一家,避开 8)。
- 22取较大的 26 填进 dp[9]。不偷、保留前面更划算。
- 23最右 dp[9]=26 就是不碰相邻、能偷到的最大金额。
⚠️ 容易写错的地方
✗ 错:凭金额大小贪心
✓ 对:必须两条路都算再取 max
大额可能挨着、反被锁死
✗ 错:dp[1] 写成 nums[1]
✓ 对:dp[1]=max(nums[0],nums[1])
前两家也要挑大的
✗ 错:答案取错项
✓ 对:答案是 dp[n-1]
最后一家也可能不偷
完整代码(Python / C++ / Java)
Python
def rob(nums):
a, b = 0, 0 # dp[i-2], dp[i-1]
for x in nums:
a, b = b, max(b, a + x)
return bC++
int rob(vector<int>& nums){
int a = 0, b = 0;
for(int x : nums){ int c = max(b, a + x); a = b; b = c; }
return b;
}Java
int rob(int[] nums){
int a = 0, b = 0;
for(int x : nums){ int c = Math.max(b, a + x); a = b; b = c; }
return b;
}复杂度
时间
O(n)
一遍线性递推
空间
O(1)
两个滚动变量
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 打家劫舍 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
环形版 LC213 怎么做?+
首尾相邻,拆成「不偷首」和「不偷尾」两条线性 DP,取两者最大。
为什么能滚动优化?+
dp[i] 只依赖前两项,两个变量轮替即可。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 打家劫舍 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。