第一章 人工智能基础
第三部分:算法分析与设计
第三节:动态规划
内容:基本概念,常见问题(如最长公共子序列、背包问题)的解法与优化
一、基本概念
动态规划是一种将复杂问题分解为子问题、通过保存子问题的解避免重复计算的算法策略。适用于具有**重叠子问题(overlapping subproblems)和最优子结构(optimal substructure)**的问题。
-
子问题重叠:相同的子问题会反复出现。
-
最优子结构:一个问题的最优解依赖于其子问题的最优解。
-
状态转移方程:描述当前状态如何由前一个状态转移而来。
-
记忆化搜索(自顶向下):递归 + 缓存。
-
递推(自底向上):从最小子问题一步步迭代到大问题。
二、经典问题讲解
1. 最长公共子序列(LCS)
问题描述:给定两个序列,找出它们的最长公共子序列的长度。
状态定义:
-
dp[i][j]
表示 A 的前 i 个字符与 B 的前 j 个字符的 LCS 长度。
状态转移方程:
if A[i-1] == B[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
代码示例:
def lcs(A, B):
m, n = len(A), len(B)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if A[i-1] == B[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
2. 0-1 背包问题
问题描述:给定一组物品,每个物品有重量和价值,在总重量不超过限制的情况下,选出若干物品使总价值最大。
状态定义:
-
dp[i][w]
表示前 i 件物品在容量为 w 的情况下的最大价值。
状态转移方程:
if w >= weight[i-1]:
dp[i][w] = max(dp[i-1][w], dp[i-1][w-weight[i-1]] + value[i-1])
else:
dp[i][w] = dp[i-1][w]
代码示例:
def knapsack(weights, values, capacity):
n = len(weights)
dp = [[0]*(capacity+1) for _ in range(n+1)]
for i in range(1, n+1):
for w in range(capacity+1):
if w >= weights[i-1]:
dp[i][w] = max(dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1])
else:
dp[i][w] = dp[i-1][w]
return dp[n][capacity]
三、优化技巧
-
滚动数组:空间优化。二维
dp[i][w]
可以优化为一维dp[w]
。 -
状态压缩:利用状态的可合并性简化空间。
-
路径恢复:通过记录路径重建实际方案(如 LCS 字符串、选中物品等)。
四、适用场景
-
序列匹配(LCS、编辑距离)
-
背包问题
-
股票买卖问题
-
数字拆分与计数(完全背包、分数问题)
-
博弈论问题(石子游戏)
股票买卖问题(Best Time to Buy and Sell Stock)
问题描述
给定一个数组 prices
,其中第 i 天的价格为 prices[i]
,可以进行一次买卖(先买后卖),求最大收益。
状态定义
-
min_price
:到目前为止的最低价格。 -
max_profit
:到目前为止的最大利润。
示例代码(买卖一次)
def max_profit(prices):
min_price = float('inf')
max_profit = 0
for price in prices:
min_price = min(min_price, price)
max_profit = max(max_profit, price - min_price)
return max_profit
股票买卖进阶(可进行 k 次交易)
状态定义:
-
dp[i][k][0]
:第 i 天,最多进行 k 次交易,不持股的最大收益。 -
dp[i][k][1]
:第 i 天,最多进行 k 次交易,持股的最大收益。
状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
该模型也可用于:
-
多次买卖(k 次)
-
冷却期
-
手续费
-
同时持有多股等情况(在 AI 中可用于强化学习环境建模)
博弈论问题:石子游戏(Stone Game)
问题描述
两人轮流从一堆石子中取走 1~3 个,取走最后一颗石子者胜。给定总石子数 n
,问先手是否必胜?
思路
对每一个 n
,我们都判断当前局面是否存在一个取法,使得对手处于“必败状态”。这是一种“胜负状态反推”的问题,适合用动态规划解决。
状态定义
-
dp[i]
表示面对i
个石子时,先手是否能赢(True
表示必赢,False
表示必败)。
状态转移方程
dp[i] = not dp[i-1] or not dp[i-2] or not dp[i-3]
即,只要存在一种取法,使对方进入失败状态,先手就能赢。
示例代码
def can_win(n):
dp = [False] * (n + 1)
for i in range(1, n + 1):
if i >= 1 and not dp[i-1]:
dp[i] = True
elif i >= 2 and not dp[i-2]:
dp[i] = True
elif i >= 3 and not dp[i-3]:
dp[i] = True
return dp[n]