题目描述
思路解析
一句话答案: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]。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
把环剪开成两条直线,难题就变回会做的老题。下面两段表分别算。
第①条 · 不偷最后一家:剪掉「户7」,强制不偷最后一家,对剩下这 7 户做普通打家劫舍。上行金额固定,下行 dp 待填。
只有第 1 户,直接偷:dp[0]=2。
前两户挑更有钱的:dp[1]=max(2,3)=3。
算 dp[2]:不偷这户=沿用 3;偷这户=dp[0]+9=11(隔一户,避开旁边那家)。
取较大的 11 填进 dp[2]。偷这户更划算。
算 dp[3]:不偷这户=沿用 11;偷这户=dp[1]+4=7(隔一户,避开旁边那家)。
取较大的 11 填进 dp[3]。不偷、保留前面更划算。
算 dp[4]:不偷这户=沿用 11;偷这户=dp[2]+1=12(隔一户,避开旁边那家)。
取较大的 12 填进 dp[4]。偷这户更划算。
算 dp[5]:不偷这户=沿用 12;偷这户=dp[3]+8=19(隔一户,避开旁边那家)。
取较大的 19 填进 dp[5]。偷这户更划算。
算 dp[6]:不偷这户=沿用 19;偷这户=dp[4]+6=18(隔一户,避开旁边那家)。
取较大的 19 填进 dp[6]。不偷、保留前面更划算。
第①条 · 不偷最后一家 这一条线性 DP 的最优结果是 19。
第②条 · 不偷第一家:剪掉「户0」,强制不偷第一家,对剩下这 7 户做普通打家劫舍。上行金额固定,下行 dp 待填。
只有第 1 户,直接偷:dp[0]=3。
前两户挑更有钱的:dp[1]=max(3,9)=9。
算 dp[2]:不偷这户=沿用 9;偷这户=dp[0]+4=7(隔一户,避开旁边那家)。
取较大的 9 填进 dp[2]。不偷、保留前面更划算。
算 dp[3]:不偷这户=沿用 9;偷这户=dp[1]+1=10(隔一户,避开旁边那家)。
取较大的 10 填进 dp[3]。偷这户更划算。
算 dp[4]:不偷这户=沿用 10;偷这户=dp[2]+8=17(隔一户,避开旁边那家)。
取较大的 17 填进 dp[4]。偷这户更划算。
算 dp[5]:不偷这户=沿用 17;偷这户=dp[3]+6=16(隔一户,避开旁边那家)。
取较大的 17 填进 dp[5]。不偷、保留前面更划算。
算 dp[6]:不偷这户=沿用 17;偷这户=dp[4]+2=19(隔一户,避开旁边那家)。
取较大的 19 填进 dp[6]。偷这户更划算。
第②条 · 不偷第一家 这一条线性 DP 的最优结果是 19。
两条线性结果:19 与 19,环形答案 = max(19, 19) = 19。
边界先想清,尤其首尾这一对。
两个高频追问。
参考代码
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:]))复杂度
- 时间:O(n),两遍线性递推,仍是 O(n)
- 空间:O(1),每遍只留前两项
易错点
面试追问把动画讲成自己的话
追问和打家劫舍 I 的唯一区别?
追问能不能只跑一遍处理环?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
最长回文子串
LeetCode 5 · 中等 · 沿着 一维动态规划 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题