【算法】动态规划实战:从入门到精通
前言
动态规划(Dynamic Programming,简称DP)是算法领域中最重要也最常用的技术之一。它不是什么具体的算法,而是一种解决问题的思想和方法论。许多看似复杂的问题,如斐波那契数列、最长公共子序列、背包问题、编辑距离等,都可以用动态规划优雅地解决。然而,动态规划也是公认难以掌握的算法技术,很多初学者看了大量的理论讲解,却仍然不知道如何应用。
作为一名AI程序员,在日常工作中经常会遇到需要优化算法性能的场景:推荐系统中的序列建模、NLP中的序列标注、图像处理中的像素级决策等,这些问题的底层算法往往都离不开动态规划的支持。因此,深入理解动态规划不仅是应对算法面试的需要,更是提升编程能力的重要途径。
本文将从动态规划的基本概念入手,通过大量的实例和详细的代码演示,帮助读者真正掌握动态规划的核心思想和实战技巧。
一、动态规划基础理论
1.1 什么是动态规划
动态规划的核心思想是将一个复杂的问题分解成若干个子问题,先求解子问题,再利用子问题的解来构建原问题的解。与分治法不同的是,动态规划处理的子问题往往不是相互独立的,而是存在重叠子问题。如果每次都重新计算子问题,会导致大量的重复计算,效率极低。动态规划通过存储子问题的解(记忆化)或者自底向上地构建解(打表),避免了重复计算,将指数级的时间复杂度降低到多项式级别。
动态规划适用于两类问题:最优子结构 和重叠子问题。最优子结构意味着问题的最优解可以由其子问题的最优解构造而成;重叠子问题意味着在递归过程中会反复遇到相同的子问题。只有同时满足这两个条件的问题,才适合用动态规划来解决。
1.2 动态规划的核心要素
理解动态规划,需要掌握三个核心概念:状态 、转移方程 和边界条件。
**状态(State)**是动态规划中最核心的概念。状态描述了问题在某个时刻的具体情况,通常用一个或一组变量来表示。例如,在斐波那契数列问题中,状态就是第n个斐波那契数的值;在背包问题中,状态可以是前i个物品、容量为j时的最大价值。状态的选择直接决定了问题的规模和解决方案的复杂度。
**转移方程(Transition)**描述了状态之间的关系,即如何从一个或多个已知状态推导到目标状态。转移方程是动态规划的精髓,找到正确的转移方程往往意味着问题已经解决了一半。
**边界条件(Base Case)**是动态规划的起点,是不需要推导就能直接知道答案的状态。边界条件的正确性直接影响最终结果。
python
# 斐波那契数列 - 最简单的动态规划示例
# 方法1:朴素递归(指数级时间复杂度,有大量重复计算)
def fib_recursive(n):
if n <= 1:
return n
return fib_recursive(n - 1) + fib_recursive(n - 2)
# 方法2:记忆化递归(自顶向下动态规划)
from functools import lru_cache
@lru_cache(maxsize=None)
def fib_memoized(n):
if n <= 1:
return n
return fib_memoized(n - 1) + fib_memoized(n - 2)
# 方法3:动态规划(自底向上打表)
def fib_dp(n):
if n <= 1:
return n
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
# 方法4:空间优化的动态规划
def fib_dp_optimized(n):
if n <= 1:
return n
prev, curr = 0, 1
for _ in range(2, n + 1):
prev, curr = curr, prev + curr
return curr
1.3 动态规划的两种实现方式
自顶向下(记忆化递归):从原问题开始,递归地分解问题直到边界条件,过程中用字典或数组存储已解决的子问题,避免重复计算。这种方式符合人的自然思维,但递归调用有栈溢出风险。
自底向上(打表递推):从边界条件开始,逐步计算更复杂的状态,直到计算出目标状态。这种方式避免了递归开销,更适合生产环境,但需要先确定计算顺序。
python
# 对比两种方式的阶乘计算
# 自顶向下:记忆化递归
@lru_cache(maxsize=None)
def factorial_memo(n):
if n <= 1:
return 1
return n * factorial_memo(n - 1)
# 自底向上:打表递推
def factorial_dp(n):
dp = [1] * (n + 1)
for i in range(2, n + 1):
dp[i] = dp[i - 1] * i
return dp[n]
# 空间进一步优化
def factorial_optimized(n):
result = 1
for i in range(2, n + 1):
result *= i
return result
二、经典动态规划问题
2.1 爬楼梯问题
爬楼梯问题是最经典的入门级动态规划问题。假设有n级台阶,每次可以爬1级或2级,问有多少种不同的方法爬到楼顶。
python
# 问题分析:
# 到达第n级台阶的最后一步,要么是从第n-1级爬1步,要么是从第n-2级爬2步
# 因此:dp[n] = dp[n-1] + dp[n-2]
# 边界条件:dp[0] = 1(不爬也是一种方法),dp[1] = 1
def climb_stairs(n):
"""
爬楼梯问题
Args:
n: 台阶数量
Returns:
爬到楼顶的方法数
"""
if n <= 2:
return n
dp = [0] * (n + 1)
dp[1] = 1
dp[2] = 2
for i in range(3, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
# 空间优化版本
def climb_stairs_optimized(n):
if n <= 2:
return n
prev, curr = 1, 2
for _ in range(3, n + 1):
prev, curr = curr, prev + curr
return curr
# 变种:如果每次可以爬1、2或3级台阶
def climb_stairs_v2(n):
if n <= 2:
return n
if n == 3:
return 4
dp = [0] * (n + 1)
dp[1], dp[2], dp[3] = 1, 2, 4
for i in range(4, n + 1):
dp[i] = dp[i-1] + dp[i-2] + dp[i-3]
return dp[n]
# 测试
print(climb_stairs(5)) # 输出: 8
print(climb_stairs(10)) # 输出: 89
2.2 背包问题
背包问题是动态规划中最经典的问题类型,包括0-1背包、完全背包、多重背包等多种变体。
0-1背包问题:有n件物品,每件物品的重量为w[i],价值为v[i],有一个容量为W的背包,问如何装入物品使得总价值最大,每件物品只能选择装入或不装入。
python
def knapsack_01(weights, values, capacity):
"""
0-1背包问题
Args:
weights: 各物品的重量列表
values: 各物品的价值列表
capacity: 背包容量
Returns:
最大价值
"""
n = len(weights)
if n == 0 or capacity == 0:
return 0
# dp[i][j] 表示前i个物品、背包容量为j时的最大价值
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(capacity + 1):
# 不装入第i个物品
dp[i][j] = dp[i-1][j]
# 装入第i个物品(如果能装得下)
if j >= weights[i-1]:
dp[i][j] = max(
dp[i][j],
dp[i-1][j - weights[i-1]] + values[i-1]
)
return dp[n][capacity]
# 空间优化版本(滚动数组)
def knapsack_01_optimized(weights, values, capacity):
n = len(weights)
if n == 0 or capacity == 0:
return 0
dp = [0] * (capacity + 1)
for i in range(n):
# 必须倒序遍历,确保每个物品只被使用一次
for j in range(capacity, weights[i] - 1, -1):
dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
return dp[capacity]
# 完全背包问题(每种物品无限件)
def knapsack_complete(weights, values, capacity):
"""
完全背包问题
Args:
weights: 各物品的重量列表
values: 各物品的价值列表
capacity: 背包容量
Returns:
最大价值
"""
n = len(weights)
if n == 0 or capacity == 0:
return 0
dp = [0] * (capacity + 1)
# 正序遍历,允许重复使用物品
for i in range(n):
for j in range(weights[i], capacity + 1):
dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
return dp[capacity]
# 多重背包问题(每种物品有数量限制)
def knapsack_multiple(weights, values, quantities, capacity):
"""
多重背包问题
Args:
weights: 各物品的重量列表
values: 各物品的价值列表
quantities: 各物品的数量限制
capacity: 背包容量
"""
n = len(weights)
if n == 0 or capacity == 0:
return 0
dp = [0] * (capacity + 1)
for i in range(n):
# 对于每种物品,使用二进制优化将其拆分为多个0-1背包
k = 1
while k < quantities[i]:
w, v = weights[i] * k, values[i] * k
for j in range(capacity, w - 1, -1):
dp[j] = max(dp[j], dp[j - w] + v)
k *= 2
# 处理剩余数量
w, v = weights[i] * quantities[i], values[i] * quantities[i]
for j in range(capacity, w - 1, -1):
dp[j] = max(dp[j], dp[j - w] + v)
return dp[capacity]
# 测试
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
capacity = 8
print(knapsack_01(weights, values, capacity)) # 输出: 10
2.3 最长公共子序列
最长公共子序列(Longest Common Subsequence,LCS)是一个经典的问题,在文本比对、生物信息学等领域有广泛应用。
python
def longest_common_subsequence(s1, s2):
"""
最长公共子序列
Args:
s1: 第一个字符串
s2: 第二个字符串
Returns:
LCS的长度
"""
m, n = len(s1), len(s2)
if m == 0 or n == 0:
return 0
# dp[i][j] 表示s1前i个字符和s2前j个字符的LCS长度
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if s1[i-1] == s2[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]
# 空间优化版本
def lcs_optimized(s1, s2):
m, n = len(s1), len(s2)
if m == 0 or n == 0:
return 0
# 只需要两行空间
prev = [0] * (n + 1)
curr = [0] * (n + 1)
for i in range(1, m + 1):
for j in range(1, n + 1):
if s1[i-1] == s2[j-1]:
curr[j] = prev[j-1] + 1
else:
curr[j] = max(prev[j], curr[j-1])
prev, curr = curr, [0] * (n + 1)
return prev[n]
# 获取具体的LCS(而不仅仅是长度)
def longest_common_subsequence_with_string(s1, s2):
"""
返回LCS的具体字符串
"""
m, n = len(s1), len(s2)
if m == 0 or n == 0:
return ""
dp = [[0] * (n + 1) for _ in range(m + 1)]
# 构建dp表
for i in range(1, m + 1):
for j in range(1, n + 1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
# 回溯获取LCS字符串
lcs = []
i, j = m, n
while i > 0 and j > 0:
if s1[i-1] == s2[j-1]:
lcs.append(s1[i-1])
i -= 1
j -= 1
elif dp[i-1][j] > dp[i][j-1]:
i -= 1
else:
j -= 1
return ''.join(reversed(lcs))
# 测试
print(longest_common_subsequence("ABCD", "AEBD")) # 输出: 3
print(lcs_optimized("ABCD", "AEBD")) # 输出: 3
print(longest_common_subsequence_with_string("ABCD", "AEBD")) # 输出: "ABD"
2.4 编辑距离
编辑距离(Levenshtein Distance)是另一个经典问题,计算将一个字符串转换成另一个字符串所需的最少编辑操作数(插入、删除、替换)。
python
def edit_distance(word1, word2):
"""
编辑距离(Levenshtein Distance)
Args:
word1: 源字符串
word2: 目标字符串
Returns:
编辑距离
"""
m, n = len(word1), len(word2)
if m == 0:
return n
if n == 0:
return m
# dp[i][j] 表示word1前i个字符转换为word2前j个字符的最少操作数
dp = [[0] * (n + 1) for _ in range(m + 1)]
# 边界条件:空字符串到任意字符串的距离
for i in range(m + 1):
dp[i][0] = i # 删除i个字符
for j in range(n + 1):
dp[0][j] = j # 插入j个字符
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = 1 + min(
dp[i-1][j], # 删除
dp[i][j-1], # 插入
dp[i-1][j-1] # 替换
)
return dp[m][n]
# 空间优化版本
def edit_distance_optimized(word1, word2):
m, n = len(word1), len(word2)
prev = list(range(n + 1))
curr = [0] * (n + 1)
for i in range(1, m + 1):
curr[0] = i
for j in range(1, n + 1):
if word1[i-1] == word2[j-1]:
curr[j] = prev[j-1]
else:
curr[j] = 1 + min(prev[j], curr[j-1], prev[j-1])
prev, curr = curr, prev
return prev[n]
# 获取编辑路径
def edit_distance_with_path(word1, word2):
"""
返回编辑距离和具体的编辑操作序列
"""
m, n = len(word1), len(word2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m + 1):
dp[i][0] = i
for j in range(n + 1):
dp[0][j] = j
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
# 回溯获取编辑路径
operations = []
i, j = m, n
while i > 0 or j > 0:
if i > 0 and j > 0 and word1[i-1] == word2[j-1]:
operations.append(f"保持: {word1[i-1]}")
i -= 1
j -= 1
elif i > 0 and dp[i][j] == dp[i-1][j] + 1:
operations.append(f"删除: {word1[i-1]}")
i -= 1
elif j > 0 and dp[i][j] == dp[i][j-1] + 1:
operations.append(f"插入: {word2[j-1]}")
j -= 1
elif i > 0 and j > 0:
operations.append(f"替换: {word1[i-1]} -> {word2[j-1]}")
i -= 1
j -= 1
return dp[m][n], list(reversed(operations))
# 测试
print(edit_distance("horse", "ros")) # 输出: 3
print(edit_distance("intention", "execution")) # 输出: 5
distance, path = edit_distance_with_path("horse", "ros")
print(f"Distance: {distance}")
for op in path:
print(f" {op}")
三、进阶动态规划问题
3.1 最长递增子序列
最长递增子序列(Longest Increasing Subsequence,LIS)是一个经典问题,给定一个序列,找出其中最长的递增子序列的长度。
python
def longest_increasing_subsequence(nums):
"""
最长递增子序列
Args:
nums: 输入序列
Returns:
LIS的长度
"""
n = len(nums)
if n == 0:
return 0
# dp[i] 表示以nums[i]结尾的LIS长度
dp = [1] * n
for i in range(n):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
# 二分查找优化版本(时间复杂度O(nlogn))
import bisect
def lis_binary_search(nums):
"""
使用二分查找优化LIS算法
核心思想:维护一个有序数组tails,tails[i]表示长度为i+1的递增子序列的最小结尾元素
"""
tails = []
for num in nums:
# 找到第一个 >= num 的位置
pos = bisect.bisect_left(tails, num)
if pos == len(tails):
tails.append(num)
else:
tails[pos] = num
return len(tails)
# 获取具体的LIS
def lis_with_sequence(nums):
"""
返回LIS的长度和具体的序列
"""
n = len(nums)
if n == 0:
return 0, []
# dp[i] 表示以nums[i]结尾的LIS长度
# prev[i] 表示在LIS中nums[i]的前一个元素的索引
dp = [1] * n
prev = [-1] * n
for i in range(n):
for j in range(i):
if nums[j] < nums[i] and dp[j] + 1 > dp[i]:
dp[i] = dp[j] + 1
prev[i] = j
# 找到最长的递增子序列的结尾索引
max_len, max_idx = 1, 0
for i in range(n):
if dp[i] > max_len:
max_len = dp[i]
max_idx = i
# 回溯获取序列
lis = []
idx = max_idx
while idx != -1:
lis.append(nums[idx])
idx = prev[idx]
return max_len, list(reversed(lis))
# 测试
nums = [10, 9, 2, 5, 3, 7, 101, 18]
print(longest_increasing_subsequence(nums)) # 输出: 4
print(lis_binary_search(nums)) # 输出: 4
length, sequence = lis_with_sequence(nums)
print(f"Length: {length}, Sequence: {sequence}") # Length: 4, Sequence: [2, 3, 7, 101]
3.2 股票买卖问题
股票买卖问题是动态规划中的经典系列问题,通过这类问题可以深入理解如何将实际问题抽象为状态机模型。
python
def max_profit_unlimited(prices):
"""
最佳买卖股票时机(无限次交易)
只要今天价格比昨天高,就买入昨天、卖出今天
"""
profit = 0
for i in range(1, len(prices)):
if prices[i] > prices[i-1]:
profit += prices[i] - prices[i-1]
return profit
def max_profit_once(prices):
"""
最佳买卖股票时机(只交易一次)
状态定义:
- cash[i] 表示第i天结束时的最大现金(在不持有股票的情况下)
- hold[i] 表示第i天结束时的最大收益(在持有股票的情况下)
转移方程:
- cash[i] = max(cash[i-1], hold[i-1] + prices[i])
- hold[i] = max(hold[i-1], -prices[i])
"""
cash = 0
hold = float('-inf')
for price in prices:
cash = max(cash, hold + price)
hold = max(hold, -price)
return cash
def max_profit_k_transactions(prices, k):
"""
最佳买卖股票时机(最多交易k次)
"""
n = len(prices)
if n == 0 or k == 0:
return 0
# 优化:如果k >= n/2,相当于无限次交易
if k >= n // 2:
profit = 0
for i in range(1, n):
if prices[i] > prices[i-1]:
profit += prices[i] - prices[i-1]
return profit
# dp[t][d] 表示第d天结束、已完成t次交易后的最大收益
# 优化空间:用两行滚动
dp = [[0] * n for _ in range(k + 1)]
for t in range(1, k + 1):
max_diff = -prices[0]
for d in range(1, n):
max_diff = max(max_diff, dp[t-1][d-1] - prices[d])
dp[t][d] = max(dp[t][d-1], prices[d] + max_diff)
return dp[k][n-1]
def max_profit_with_cooldown(prices):
"""
最佳买卖股票时机(含冷冻期)
冷冻期:卖出股票后一天不能买入
"""
n = len(prices)
if n == 0:
return 0
# 状态定义:
# hold[i] = max profit on day i with stock held
# cash[i] = max profit on day i with no stock held (can buy today or before)
# cooldown[i] = max profit on day i, cooldown (just sold)
hold = float('-inf')
cash = 0
cooldown = 0
for price in prices:
prev_hold = hold
hold = max(hold, cooldown - price)
cooldown = cash
cash = max(cash, prev_hold + price)
return cash
# 测试
prices = [7, 1, 5, 3, 6, 4]
print(max_profit_unlimited(prices)) # 输出: 7
print(max_profit_once(prices)) # 输出: 5
print(max_profit_k_transactions(prices, 2)) # 输出: 7
print(max_profit_with_cooldown(prices)) # 输出: 7
3.3 戳气球问题
戳气球问题是一个比较复杂的动态规划问题,需要逆向思考。
python
def burst_balloons(nums):
"""
戳气球问题
有n个气球,编号0到n-1,每个气球上有一个数字。
戳破气球i可以获得nums[left] * nums[i] * nums[right]个硬币,
其中left和right是气球i左右两边最近的气球(如果不存在则视为1)。
求可以获得的最大硬币数。
关键洞察:
- 正向思考很难,因为戳破气球会改变邻居关系
- 逆向思考:假设最后戳破的是第k个气球,
那么在此之前,left和right区间内的气球都已经戳破了
"""
n = len(nums)
if n == 0:
return 0
# 扩展数组,两端添加虚拟气球(值为1)
points = [1] + nums + [1]
n = len(points)
# dp[i][j] 表示戳破开区间(i, j)中所有气球能获得的最大硬币数
# 最终答案是 dp[0][n-1]
dp = [[0] * n for _ in range(n)]
# 区间长度从2开始(只有一个气球的情况不需要计算,因为乘积为0)
for length in range(2, n):
for left in range(0, n - length):
right = left + length
# 尝试戳破开区间(left, right)中的最后一个气球k
for k in range(left + 1, right):
# k是最后一个被戳破的,所以[left, k]和[k, right]已经处理完毕
coins = dp[left][k] + dp[k][right] + points[left] * points[k] * points[right]
dp[left][right] = max(dp[left][right], coins)
return dp[0][n-1]
# 测试
nums = [3, 1, 5, 8]
print(burst_balloons(nums)) # 输出: 167
四、动态规划解题套路
4.1 解题步骤总结
解决动态规划问题通常遵循以下步骤:
- 确定问题是否适合动态规划:检查是否有最优子结构和重叠子问题
- 确定状态表示:明确用什么变量描述问题的当前状态
- 确定边界条件:最简单的情况是什么
- 推导转移方程:如何从子问题的解得到当前问题的解
- 确定计算顺序:自底向上时需要明确依赖顺序
- 实现并优化:编写代码,考虑空间优化
python
# 通用解题模板
def dp_template(problem_input):
"""
动态规划通用解题模板
1. 预处理输入(如有必要)
2. 初始化dp数组和边界条件
3. 自底向上计算(注意遍历顺序)
4. 返回结果
"""
# 步骤1:预处理
n = len(problem_input)
if n == 0:
return 0
# 步骤2:初始化
dp = [[0] * (some_size) for _ in range(some_size)]
# 设置边界条件
for i in range(...):
dp[i][0] = base_value
# 步骤3:状态转移
for i in range(1, n):
for j in range(1, m):
# 核心转移逻辑
dp[i][j] = max/min(
dp[i-1][j], # 不选
dp[i][j-1], # 选
dp[i-1][j-1] # 特殊情况
) + value
# 步骤4:返回结果
return dp[n-1][m-1]
4.2 空间优化技巧
动态规划中,空间优化是非常重要的技巧。很多情况下,我们可以将二维dp数组优化为一维,甚至使用更少的变量。
python
# 一维dp优化示例 - 0-1背包
def knapsack_1d_optimized(weights, values, capacity):
"""
一维dp是0-1背包的标准空间优化
关键点:必须倒序遍历容量
"""
dp = [0] * (capacity + 1)
for i in range(len(weights)):
# 倒序确保每个物品只被使用一次
for j in range(capacity, weights[i] - 1, -1):
dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
return dp[capacity]
# 完全背包的空间优化
def knapsack_complete_1d(weights, values, capacity):
"""
完全背包的一维dp优化
关键点:必须正序遍历容量
"""
dp = [0] * (capacity + 1)
for i in range(len(weights)):
# 正序允许重复使用物品
for j in range(weights[i], capacity + 1):
dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
return dp[capacity]
# 滚动数组 - 保留必要的行
def lcs滚动数组(s1, s2):
"""
LCS的空间优化
只需要保留前一行和当前行
"""
if len(s1) < len(s2):
s1, s2 = s2, s1 # 确保s2是较短的
prev = [0] * (len(s2) + 1)
curr = [0] * (len(s2) + 1)
for i in range(1, len(s1) + 1):
for j in range(1, len(s2) + 1):
if s1[i-1] == s2[j-1]:
curr[j] = prev[j-1] + 1
else:
curr[j] = max(prev[j], curr[j-1])
prev, curr = curr, [0] * (len(s2) + 1)
return prev[len(s2)]
五、综合实战案例
5.1 需求管理系统
假设我们需要开发一个需求优先级排序系统,需求有"紧急程度"和"重要性"两个维度,我们需要选择最佳的需求执行顺序以最大化总体价值。
python
def priority_scheduler(requirements, max_time):
"""
需求优先级调度器
Args:
requirements: 需求列表,每项为(紧急程度, 重要性, 预计工时)
max_time: 可用总工时
Returns:
最优的价值和选中的需求
"""
# 紧急程度和重要性合并为价值因子
# 这里简化为:价值 = 紧急程度 * 重要性 * 10
n = len(requirements)
values = []
weights = []
for urgency, importance, hours in requirements:
values.append(urgency * importance * 10)
weights.append(hours)
# 0-1背包:每个需求只能选择做或不做
capacity = max_time
dp = [0] * (capacity + 1)
choice = [[False] * (capacity + 1) for _ in range(n)]
for i in range(n):
for j in range(capacity, weights[i] - 1, -1):
if dp[j - weights[i]] + values[i] > dp[j]:
dp[j] = dp[j - weights[i]] + values[i]
choice[i][j] = True
# 回溯找出选择了哪些需求
selected = []
j = capacity
for i in range(n - 1, -1, -1):
if choice[i][j]:
selected.append(i)
j -= weights[i]
return dp[capacity], selected
# 测试
requirements = [
(5, 4, 8), # 紧急程度5, 重要性4, 工时8
(3, 5, 6), # 紧急程度3, 重要性5, 工时6
(4, 2, 4), # 紧急程度4, 重要性2, 工时4
(2, 4, 5), # 紧急程度2, 重要性4, 工时5
]
max_time = 15
max_value, selected = priority_scheduler(requirements, max_time)
print(f"最大价值: {max_value}")
print(f"选择的需求: {selected}")
5.2 AI对话窗口优化
在AI对话系统中,需要优化"token窗口管理",这是一个简化版的滑动窗口问题,可以用动态规划解决。
python
def token_window_optimize(messages, max_tokens):
"""
AI对话窗口Token优化
给定一系列历史消息,每条消息有token数和"价值分数",
需要保留最近总token数不超过max_tokens的消息序列,
使得总价值最大化。
状态定义:dp[i] = 前i条消息在token限制下的最大价值
"""
n = len(messages)
if n == 0:
return 0
# 提取token数和价值
tokens = [msg['tokens'] for msg in messages]
values = [msg['value'] for msg in messages]
# dp[i][t] = 前i条消息、使用t个token时的最大价值
# 空间优化:只需要上一行
dp = [0] * (max_tokens + 1)
for i in range(n):
# 倒序遍历,确保每条消息只被计算一次
for t in range(max_tokens, tokens[i] - 1, -1):
dp[t] = max(dp[t], dp[t - tokens[i]] + values[i])
return dp[max_tokens]
# 测试
messages = [
{'tokens': 100, 'value': 80}, # 系统提示
{'tokens': 50, 'value': 60}, # 用户问题
{'tokens': 30, 'value': 90}, # AI回答1
{'tokens': 80, 'value': 70}, # 用户追问
{'tokens': 40, 'value': 85}, # AI回答2
]
max_tokens = 150
result = token_window_optimize(messages, max_tokens)
print(f"最大价值: {result}")
六、总结
动态规划是算法领域最核心的技术之一,掌握动态规划对于提升编程能力和解决问题的能力至关重要。本文从动态规划的基础概念出发,通过大量的实例演示了动态规划的解题思路和技巧。
学习动态规划需要注意以下几点:
- 理解核心思想:分解问题、存储子问题的解、构建原问题的解
- 掌握状态定义:状态定义是动态规划的核心,直接影响问题复杂度
- 熟练推导转移方程:这是动态规划的灵魂,需要大量练习
- 学会空间优化:很多情况下可以将空间复杂度降低一个维度
- 多做练习:动态规划需要通过不断实践来加深理解
希望本文能够帮助读者真正掌握动态规划,在今后的工作和学习中能够灵活运用这一强大的算法技术。