力扣(LeetCode)上的"打家劫舍"(House Robber)系列问题,是动态规划(Dynamic Programming)领域的经典入门题目。这类问题通常描述为:假设你是一个专业的小偷,计划偷窃沿街的房屋,但相邻房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚被闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算在不触动警报的情况下,一夜之内能够偷窃到的最高金额。
这个问题的现实意义不仅限于算法练习,它实际上模拟了许多决策场景中的核心矛盾——在有限资源约束下做出最优选择。比如投资组合管理中选择不相邻的高收益项目,或者日程安排中避免时间冲突的高价值活动。
对于最基础的打家劫舍问题(LeetCode 198),房屋排列成一条直线。我们可以定义dp[i]表示偷窃到第i个房屋时能获得的最大金额。关键是要理解状态转移的两种可能性:
因此状态转移方程为:
code复制dp[i] = max(dp[i-1], dp[i-2] + nums[i])
基础实现代码示例(Python):
python复制def rob(nums):
if not nums:
return 0
if len(nums) <= 2:
return max(nums)
dp = [0] * len(nums)
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, len(nums)):
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
return dp[-1]
观察状态转移方程可以发现,dp[i]只依赖于前两个状态dp[i-1]和dp[i-2],因此可以用两个变量代替整个dp数组,将空间复杂度从O(n)降到O(1):
python复制def rob(nums):
prev_max = curr_max = 0
for num in nums:
temp = curr_max
curr_max = max(curr_max, prev_max + num)
prev_max = temp
return curr_max
提示:在实际面试中,面试官通常会期待你首先给出基础解法,然后主动提出空间优化方案,这展示了你的算法优化意识。
当房屋排列成环形时,第一间和最后一间房屋成为相邻关系。这种情况下,我们可以将问题拆解为两个子问题:
然后取这两个结果中的较大值:
python复制def rob(nums):
if len(nums) == 1:
return nums[0]
return max(rob_linear(nums[1:]), rob_linear(nums[:-1]))
def rob_linear(nums): # 普通线性解法
prev_max = curr_max = 0
for num in nums:
temp = curr_max
curr_max = max(curr_max, prev_max + num)
prev_max = temp
return curr_max
当房屋排列成二叉树结构时,每个节点表示一个房屋,相邻节点指直接相连的父子节点。这时需要采用树形DP的思路:
对于每个节点,有两种选择:
我们可以使用后序遍历,返回一个包含两个值的数组:[偷当前节点的最大值, 不偷当前节点的最大值]
python复制def rob(root):
def dfs(node):
if not node:
return [0, 0]
left = dfs(node.left)
right = dfs(node.right)
rob_current = node.val + left[1] + right[1]
skip_current = max(left) + max(right)
return [rob_current, skip_current]
return max(dfs(root))
新手常犯的错误是忽略边界条件:
注意:在动态规划问题中,边界条件往往决定了初始状态,处理不当会导致整个算法失败。
常见误区包括:
调试建议:
对于基础解法:
对于二叉树变种:
虽然问题设定是小偷场景,但这类算法在实际中有广泛用途:
进阶思考:
经过多次实践和教学,我总结出以下经验:
在面试中遇到此类问题时:
最后记住,这类问题的核心在于决策时的取舍——当前利益与长远收益的平衡,这不仅是算法精髓,也是许多现实决策的缩影。