动态规划:从递归到最优决策的算法艺术

动态规划(Dynamic Programming,简称 DP)并非某种具体的算法,而是一种将复杂问题分解为简单子问题、并通过存储子问题的解来避免重复计算的算法设计思想 。其核心哲学在于用空间换时间,解决那些具有最优子结构 和重叠子问题 特性的难题。不同于贪心算法的局部最优选择,动态规划通过穷举所有可能的决策路径,确保找到全局最优解。

要理解动态规划,不妨从最朴素的递归说起。以经典的斐波那契数列为例,其数学定义为 F(n)=F(n−1)+F(n−2)F(n) = F(n-1) + F(n-2)F(n)=F(n−1)+F(n−2),其中 F(0)=0,F(1)=1F(0)=0, F(1)=1F(0)=0,F(1)=1。若直接翻译为递归代码,我们会发现计算过程产生了指数级的冗余运算。例如在计算 F(5)F(5)F(5) 时,F(3)F(3)F(3) 被重复计算了多次。这种重叠子问题 现象导致了巨大的性能浪费。动态规划介入的方式便是引入一个数组 dpdpdp 来存储已计算的结果,这被称为"记忆化搜索"。

在正式的文章中,我们通常使用"自底向上"的迭代方式来构建 DP。对于斐波那契数列,我们定义状态 dpidpidpi 表示第 iii 个数字的值。状态转移方程为:

dpi=dpi−1+dpi−2 dpi = dpi-1 + dpi-2 dpi=dpi−1+dpi−2

通过初始化 dp0dp0dp0 和 dp1dp1dp1,我们可以线性地推导出后续结果。以下是标准的动态规划实现:

python 复制代码
def fibonacci(n: int) -> int:
    if n <= 1:
        return n
    
    # 定义 dp 数组
    dp = [0] * (n + 1)
    # 初始化边界条件
    dp[0], dp[1] = 0, 1
    
    # 状态转移
    for i in range(2, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]
    
    return dp[n]

此代码的时间复杂度为 O(n)O(n)O(n),空间复杂度为 O(n)O(n)O(n)。进一步观察可以发现,dpidpidpi 仅依赖于前两个状态,因此我们可以将空间压缩至 O(1)O(1)O(1):

python 复制代码
def fibonacci_optimized(n: int) -> int:
    if n <= 1:
        return n
    prev, curr = 0, 1
    for _ in range(2, n + 1):
        prev, curr = curr, prev + curr
    return curr

动态规划的难点在于状态定义与转移方程的设计。以0-1 背包问题 为例,这是动态规划中的里程碑式问题。问题描述如下:给定 nnn 个物品,每个物品重量为 wiw_iwi,价值为 viv_ivi,以及一个容量为 WWW 的背包。求在不超过容量限制的前提下,背包能装下的物品最大总价值。

我们定义二维状态 dpijdpijdpij,表示前 iii 个物品放入容量为 jjj 的背包中所能获得的最大价值 。对于每个物品,我们有两种选择:放或不放。如果不放,则 dpij=dpi−1jdpij = dpi-1jdpij=dpi−1j;如果放(前提是 j≥wij \ge w_ij≥wi),则 dpij=dpi−1j−wi+vidpij = dpi-1j-w_i + v_idpij=dpi−1j−wi+vi。综合二者,状态转移方程为:

dpij=max⁡(dpi−1j,dpi−1j−wi+vi) dpij = \max(dpi-1j, dpi-1j-w_i + v_i) dpij=max(dpi−1j,dpi−1j−wi+vi)

完整的代码如下:

python 复制代码
def knapsack_01(W, weights, values):
    n = len(weights)
    # dp[i][j] 表示前 i 个物品,容量为 j 的最大价值
    dp = [[0] * (W + 1) for _ in range(n + 1)]
    
    for i in range(1, n + 1):
        for j in range(W + 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][W]

同样,该问题也可以进行空间优化。由于 dpidpidpi 只依赖于 dpi−1dpi-1dpi−1,我们可以使用一维数组并从后往前遍历容量 jjj,以避免数据覆盖。

除了背包问题,动态规划还广泛应用于字符串处理(如最长公共子序列 ,LCS)、区间决策(如石子合并 )以及树形结构(树形 DP )。掌握动态规划的关键在于"勤画表",通过在草稿纸上模拟 dpdpdp 数组的填充过程,往往能直观地发现状态之间的联系。

总而言之,动态规划不是一蹴而就的技巧,而是对问题本质的深度剖析。它要求我们明确"什么是状态"以及"状态如何流转"。当你面对一个问题时,若能将其抽象为一个多阶段的决策过程,并证明其满足无后效性,那么动态规划很可能就是你手中的利刃。

相关推荐
布莱克60513 小时前
C++ 中 map 的 insert 与 emplace:key 已存在时会覆盖吗?
开发语言·数据结构·c++·算法·map
D e re k ZH16 小时前
C++ 系统学习手记:从语法细节到工程实践
开发语言·数据结构·c++·算法
石小石Orz16 小时前
AI 正在蚕食我们的编码能力
前端·后端·算法
民乐团扒谱机18 小时前
【微实验】NSGA-II(非支配排序遗传算法)——解决多目标优化问题的“自然选择模拟器”
linux·人工智能·算法
青少儿编程课堂18 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:中缀表达式求值解析
c++·python·算法·bfs·信息学竞赛
武子康20 小时前
Qwen-Drive 到底是什么:4B 级 VLM 如何成为驾驶策略底座
人工智能·算法·机器学习
zh_xuan1 天前
LeeCode 496. 下一个更大元素I
数据结构·算法·栈·单调栈
机 _ 长1 天前
05 智算运维|8 张卡怎么才算「捆成一张」:NCCL 实测与拓扑
运维·人工智能·算法·智算运维
早起CaiCai1 天前
【2023-01 OM】利用深度学习算法融合异质观测实现海洋场降尺度
人工智能·深度学习·算法