【算法】动态规划实战:从入门到精通

【算法】动态规划实战:从入门到精通

前言

动态规划(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 解题步骤总结

解决动态规划问题通常遵循以下步骤:

  1. 确定问题是否适合动态规划:检查是否有最优子结构和重叠子问题
  2. 确定状态表示:明确用什么变量描述问题的当前状态
  3. 确定边界条件:最简单的情况是什么
  4. 推导转移方程:如何从子问题的解得到当前问题的解
  5. 确定计算顺序:自底向上时需要明确依赖顺序
  6. 实现并优化:编写代码,考虑空间优化
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}")

六、总结

动态规划是算法领域最核心的技术之一,掌握动态规划对于提升编程能力和解决问题的能力至关重要。本文从动态规划的基础概念出发,通过大量的实例演示了动态规划的解题思路和技巧。

学习动态规划需要注意以下几点:

  1. 理解核心思想:分解问题、存储子问题的解、构建原问题的解
  2. 掌握状态定义:状态定义是动态规划的核心,直接影响问题复杂度
  3. 熟练推导转移方程:这是动态规划的灵魂,需要大量练习
  4. 学会空间优化:很多情况下可以将空间复杂度降低一个维度
  5. 多做练习:动态规划需要通过不断实践来加深理解

希望本文能够帮助读者真正掌握动态规划,在今后的工作和学习中能够灵活运用这一强大的算法技术。

相关推荐
人工智能培训1 小时前
大模型与传统小模型、传统NLP模型的核心差异解析
人工智能·深度学习·神经网络·机器学习·生成对抗网络
沪漂阿龙1 小时前
面试题详解:智能客服 Agent 系统全栈拆解——Rasa Pro、对话管理、意图识别、GraphRAG、Qwen 与 RAG 优化实战
人工智能·架构
薛定猫AI1 小时前
【深度解析】Gemini Omni 多模态生成与 Agent 化创作工作流:从视频编辑到 UI 生成的技术演进
人工智能·ui·音视频
羊羊小栈1 小时前
AI赋能电力巡检:智能故障预警系统
人工智能·yolo·目标检测·毕业设计·大作业
Python私教1 小时前
视觉 Agent 爬取 vs Playwright 脚本:Browser Use 2026 选型表
人工智能
Python私教1 小时前
Crawlee StagehandCrawler:自然语言点 Load More 的工程化爬虫
人工智能
南屹川1 小时前
【容器化】Docker实战:从入门到生产环境部署
人工智能
海蓝可知天湛2 小时前
Agent&IELTS雅思口语专属语料库
人工智能·github·rag·ielts·skills
随身数智备忘录2 小时前
什么是设备管理体系?设备管理体系包含哪些核心模块?
网络·数据库·人工智能