打家劫舍 II 图解题解
这道题到底在问什么
- 输入
- nums=[2,3,9,4,1,8,6,2](环形)
- 输出
- 19
最优解:为什么这么做
一句话答案:LeetCode 213 打家劫舍 II 房子围成一圈,首尾相邻不能同时偷,一遍线性 DP 管不住。解法是拆成两条直线各跑一次打家劫舍 I:去掉最后一家、去掉第一家,分别滚动求最大金额取 max,时间 O(n)、空间 O(1)。
打家劫舍 II 和 I 差在哪一句话
给一个数组 nums,每个值是一家现金,相邻两家不能同时偷,求最大总金额。和打家劫舍 I 唯一的区别:房子围成一圈,第一家和最后一家也相邻。就这句「首尾相邻」,原本离得最远的第一家和最后一家成了新邻居,直线题变成了环。
为什么直接套一遍线性 DP 会算错
打家劫舍 I 用线性 DP:DP 即动态规划,把算过的子问题存下来别重算;线性就是从头到尾扫一遍,靠递推(拿前面算好的值一步步推出后面的)往后推。它默认首尾两家离得远、互不影响。
环形里这个默认不成立:照直线跑一遍,dp 可能把下标 0 和 n-1 都选进最优,可它俩在环上是邻居,方案不合法。「首尾不能都偷」这条约束单条递推管不住。
为什么拆成两条直线取 max 就对了
麻烦全在首尾这对邻居上,就按它俩分情况。任何合法偷法,首尾至多占一家——至少有一端空着。两种情况合起来把所有合法方案兜住。
第一种把最后一家掐掉(只看 nums[0..n-2]),剩下是直线,第一家可偷;第二种把第一家掐掉(只看 nums[1..n-1]),最后一家可偷。每条上首尾不再相邻,就是打家劫舍 I,取更大那个。首尾都不偷的方案两条都算了,求 max 重复无妨,不漏就对。
每条直线里的 dp 怎么往前推
单条直线用 dp[i]=max(dp[i-1], dp[i-2]+nums[i]):不偷第 i 家就沿用前一家;偷第 i 家则前一家须空着,从 dp[i-2] 接过来加这家的钱。
参考代码用两个变量滚动:p 记 dp[i-2]、q 记 dp[i-1],每步 p, q = q, max(q, p+x),新 q 即当前最优。只留前两格,空间 O(1)。
手工演算:拿示例把两条直线都填出来
题面是环形数组 [2,3,9,4,1,8,6,2],拆成两条直线:掐掉最后一家的 [2,3,9,4,1,8,6]、掐掉第一家的 [3,9,4,1,8,6,2]。第一条递推:dp[0]=2;dp[1]=max(2,3)=3;dp[2]=max(3,2+9)=11;dp[3]=max(11,3+4)=11;dp[4]=max(11,11+1)=12;dp[5]=max(12,11+8)=19;dp[6]=max(19,12+6)=19,结果 19。
第二条递推:dp[0]=3;dp[1]=max(3,9)=9;dp[2]=max(9,3+4)=9;dp[3]=max(9,9+1)=10;dp[4]=max(10,9+8)=17;dp[5]=max(17,10+6)=17;dp[6]=max(17,17+2)=19,结果 19。
环形答案 max(19,19)=19,对上题面输出。两条同分是巧合,取大的才是解。
复杂度多少,n=1 和空端两个边界
两条各扫一遍,每遍 O(n)、合起来仍 O(n);每遍只留两个变量,空间 O(1)。
两处易错。一是区间切错:两条必须各漏一端、一条去尾一条去头,都留全就等于让首尾同时入选。二是 n==1:只有一家时去头去尾切完两条都空、各返回 0,会漏掉这家,得直接返回 nums[0]。
▶ 动画逐步走查(共 30 步)——想跟着动画一帧帧对照就展开
- 3把环剪开成两条直线,难题就变回会做的老题。下面两段表分别算。
- 4第①条 · 不偷最后一家:剪掉「户7」,强制不偷最后一家,对剩下这 7 户做普通打家劫舍。上行金额固定,下行 dp 待填。
- 5只有第 1 户,直接偷:dp[0]=2。
- 6前两户挑更有钱的:dp[1]=max(2,3)=3。
- 7算 dp[2]:不偷这户=沿用 3;偷这户=dp[0]+9=11(隔一户,避开旁边那家)。
- 8取较大的 11 填进 dp[2]。偷这户更划算。
- 9算 dp[3]:不偷这户=沿用 11;偷这户=dp[1]+4=7(隔一户,避开旁边那家)。
- 10取较大的 11 填进 dp[3]。不偷、保留前面更划算。
- 11算 dp[4]:不偷这户=沿用 11;偷这户=dp[2]+1=12(隔一户,避开旁边那家)。
- 12取较大的 12 填进 dp[4]。偷这户更划算。
- 13算 dp[5]:不偷这户=沿用 12;偷这户=dp[3]+8=19(隔一户,避开旁边那家)。
- 14取较大的 19 填进 dp[5]。偷这户更划算。
- 15算 dp[6]:不偷这户=沿用 19;偷这户=dp[4]+6=18(隔一户,避开旁边那家)。
- 16取较大的 19 填进 dp[6]。不偷、保留前面更划算。
- 17第①条 · 不偷最后一家 这一条线性 DP 的最优结果是 19。
- 18第②条 · 不偷第一家:剪掉「户0」,强制不偷第一家,对剩下这 7 户做普通打家劫舍。上行金额固定,下行 dp 待填。
- 19只有第 1 户,直接偷:dp[0]=3。
- 20前两户挑更有钱的:dp[1]=max(3,9)=9。
- 21算 dp[2]:不偷这户=沿用 9;偷这户=dp[0]+4=7(隔一户,避开旁边那家)。
- 22取较大的 9 填进 dp[2]。不偷、保留前面更划算。
- 23算 dp[3]:不偷这户=沿用 9;偷这户=dp[1]+1=10(隔一户,避开旁边那家)。
- 24取较大的 10 填进 dp[3]。偷这户更划算。
- 25算 dp[4]:不偷这户=沿用 10;偷这户=dp[2]+8=17(隔一户,避开旁边那家)。
- 26取较大的 17 填进 dp[4]。偷这户更划算。
- 27算 dp[5]:不偷这户=沿用 17;偷这户=dp[3]+6=16(隔一户,避开旁边那家)。
- 28取较大的 17 填进 dp[5]。不偷、保留前面更划算。
- 29算 dp[6]:不偷这户=沿用 17;偷这户=dp[4]+2=19(隔一户,避开旁边那家)。
- 30取较大的 19 填进 dp[6]。偷这户更划算。
- 31第②条 · 不偷第一家 这一条线性 DP 的最优结果是 19。
- 32两条线性结果:19 与 19,环形答案 = max(19, 19) = 19。
⚠️ 容易写错的地方
✗ 错:当成普通打家劫舍只算一遍
✓ 对:必须拆两条再取 max
首尾相邻,一遍管不住「不能都偷」
✗ 错:两条的区间切错
✓ 对:①nums[0..n-2] ②nums[1..n-1]
各漏一端才能保证首尾不同时入选
✗ 错:忘了 n==1 单独处理
✓ 对:一户时去头去尾都为空
切片后两条都空会返回 0,漏掉这户
完整代码(Python / C++ / Java)
Python
def rob(nums):
n = len(nums)
if n == 1: return nums[0]
def line(a):
p = q = 0
for x in a: p, q = q, max(q, p + x)
return q
return max(line(nums[:-1]), line(nums[1:]))C++
int line(vector<int>& v, int s, int e){
int p = 0, q = 0;
for(int i = s; i < e; i++){ int c = max(q, p + v[i]); p = q; q = c; }
return q;
}
int rob(vector<int>& nums){
int n = nums.size();
if(n == 1) return nums[0];
return max(line(nums, 0, n-1), line(nums, 1, n));
}Java
int line(int[] v, int s, int e){
int p = 0, q = 0;
for(int i = s; i < e; i++){ int c = Math.max(q, p + v[i]); p = q; q = c; }
return q;
}
int rob(int[] nums){
int n = nums.length;
if(n == 1) return nums[0];
return Math.max(line(nums, 0, n-1), line(nums, 1, n));
}复杂度
时间
O(n)
两遍线性递推,仍是 O(n)
空间
O(1)
每遍只留前两项
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 打家劫舍 II 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
打家劫舍 II 和打家劫舍 I 到底什么关系?+
II 就是把 I 的直线首尾接成了环。I 里第一家和最后一家隔得远、互不影响,一条 dp[i]=max(dp[i-1], dp[i-2]+nums[i]) 跑到底即可。II 里首尾相邻不能同时偷,一条递推管不住,于是拆成「去掉最后一家」和「去掉第一家」两条直线,各套用 I 的解法再取 max。会了 I,II 只是多包一层拆分。
为什么两条直线各漏一端就够,不能都保留全部房子?+
环形新增的约束只有首尾不能都偷。去掉最后一家那条覆盖了「不偷最后一家」的所有方案,去掉第一家那条覆盖了「不偷第一家」的所有方案,二者合起来兜住全部合法偷法。若两条都留全,就等于允许首尾同时入选,退化回一条线性 DP,环的约束直接失效。
能不能不拆两遍,一次遍历就把环处理掉?+
可以,本质是对「偷不偷第一家」做分类讨论,用一组状态同时维护两种情形往前推,一遍扫完。但那套状态转移更绕也更易错,和拆两条直线取 max 完全等价,时间同样 O(n)。拆开写最直观,面试里讲清思路优先选它。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 打家劫舍 II 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。