1. 动态规划算法精讲:从打家劫舍到股票交易
动态规划(Dynamic Programming)是算法领域中最重要也最实用的技术之一。作为一名刷过上百道动态规划题的算法工程师,我想分享一些实战经验和思考方式。动态规划之所以难,是因为它需要我们将问题分解成子问题,并找到状态转移的规律。今天我们就从经典的打家劫舍问题开始,逐步深入到更复杂的股票交易和子序列问题。
动态规划的核心在于定义状态和状态转移方程。以打家劫舍为例,我们需要明确"偷或不偷当前房屋"这个状态,以及如何从前面的状态推导出当前状态的最优解。这种思维方式可以推广到几乎所有DP问题中。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 打家劫舍系列问题解析
2.1 基础版:线性排列房屋
198题是最基础的打家劫舍问题。给定一个代表每个房屋存放金额的非负整数数组,相邻房屋不能同时被打劫。我们需要设计算法计算在不触动警报的情况下能够偷窃到的最高金额。
状态定义:dp[i]表示考虑前i个房屋(包括i)能偷窃到的最大金额
状态转移方程:
code复制dp[i] = max(dp[i-1], dp[i-2] + nums[i])
这个方程的含义是:对于第i个房屋,我们有两种选择:
- 不偷它,那么最大金额就是前i-1个房屋的最大金额dp[i-1]
- 偷它,那么就不能偷i-1房屋,最大金额是前i-2个房屋的最大金额加上当前房屋的金额
边界条件:
- dp[0] = nums[0](只有一间房屋时)
- dp[1] = max(nums[0], nums[1])(两间房屋时取较大的)
python复制def rob(nums):
if len(nums) == 1:
return nums[0]
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]
时间复杂度:O(n),只需遍历一次数组
空间复杂度:O(n),可以用滚动数组优化到O(1)
2.2 进阶版:环形排列房屋
213题将房屋排列成环形,即首尾相连。这意味着第一个和最后一个房屋不能同时被打劫。
解题思路:
我们可以将环形问题分解为两个线性问题:
- 不考虑最后一个房屋(即只考虑nums[0...n-2])
- 不考虑第一个房屋(即只考虑nums[1...n-1])
然后取这两个情况的最大值。
python复制def rob(nums):
if len(nums) == 1:
return nums[0]
def rob_range(start, end):
prev2, prev1 = 0, 0
for i in range(start, end+1):
curr = max(prev1, prev2 + nums[i])
prev2, prev1 = prev1, curr
return prev1
return max(rob_range(0, len(nums)-2), rob_range(1, len(nums)-1))
优化技巧:这里使用了滚动数组来优化空间复杂度,只保存前两个状态而不是整个dp数组。
2.3 终极版:二叉树房屋排列
337题将房屋排列成二叉树结构,相邻节点不能同时被打劫。这里的"相邻"指的是直接相连的父子节点。
状态定义:对于每个节点,我们需要记录两个状态:
- dp[0]:不偷当前节点时的最大金额
- dp[1]:偷当前节点时的最大金额
状态转移:
- 偷当前节点:不能偷子节点,金额 = 当前节点值 + 左子节点不偷的最大值 + 右子节点不偷的最大值
- 不偷当前节点:可以自由选择偷或不偷子节点,金额 = max(左子节点偷,不偷) + max(右子节点偷,不偷)
python复制def rob(root):
def dfs(node):
if not node:
return [0, 0]
left = dfs(node.left)
right = dfs(node.right)
#
