当前位置: 首页 > news >正文

动态规划(DP)

简介:

动态规划(Dynamic Programming, DP)的起源可以追溯到 20 世纪 40 年代,其发展与美国数学家 ** 理查德・贝尔曼(Richard Bellman)** 的研究密切相关。以下是其起源的关键脉络:

1. 多阶段决策问题的研究

贝尔曼最初关注的是如何优化多阶段决策过程,例如水利资源的多级分配、库存管理等问题。这类问题的特点是:决策过程可分解为多个阶段,每个阶段的决策会影响后续阶段的状态,最终目标是找到全局最优解。

2. 最优化原理的提出(1949 年)

贝尔曼在研究中提出了最优化原理(Principle of Optimality):
“任何最优策略的子策略本身也必须是最优的。”
这一原理将复杂的多阶段问题分解为一系列子问题,通过递归求解子问题的最优解,最终得到全局最优解。

3. 动态规划的命名(1950 年)

为了避免当时美国国防部对 “数学研究” 的偏见,贝尔曼为这一方法命名为 **“动态规划”**(Dynamic Programming)。这一名称既模糊了数学本质,又暗示了其在动态过程优化中的应用。

4. 理论体系的建立(1957 年)

贝尔曼在 1957 年出版了专著《动态规划》(Dynamic Programming),系统阐述了动态规划的理论框架和应用方法。书中通过大量案例(如最短路径、资源分配等)展示了动态规划的核心思想:通过存储子问题的解避免重复计算,从而高效求解复杂问题

5. 应用领域的扩展

  • 早期应用:主要集中在运筹学、军事决策、生产调度等领域。
  • 计算机科学:20 世纪 60 年代后,动态规划被引入计算机算法设计,用于解决背包问题、字符串编辑距离、最长公共子序列等经典问题。
  • 现代发展:在机器学习(如强化学习)、生物信息学(如序列比对)、金融学(如期权定价)等领域得到广泛应用。

核心思想总结

动态规划的核心是将复杂问题分解为重叠子问题,通过记忆化存储递推计算避免重复工作,从而在多项式时间内找到最优解。其核心要素包括:

  • 状态定义:描述问题在各阶段的状态。
  • 状态转移方程:子问题之间的递推关系。
  • 边界条件:初始状态或最简子问题的解。

动态规划是一种强大的算法设计策略,用于解决具有最优子结构和子问题重叠性质的问题。下面将从多个方面详细介绍动态规划。

核心概念

  • 最优子结构:一个问题的最优解包含其子问题的最优解。也就是说,大问题的最优解可以由小问题的最优解组合而成。例如,在求解最短路径问题时,从起点到终点的最短路径必然包含了从起点到中间某个节点的最短路径。
  • 子问题重叠:在求解问题的过程中,很多子问题会被重复计算。动态规划通过记录子问题的解,避免了重复计算,从而提高了算法的效率。以斐波那契数列为例,计算 F(n) 时会多次用到 F(n−1) 和 F(n−2),如果不记录这些子问题的解,会导致大量的重复计算。

解题步骤

1. 定义状态

状态是动态规划中用来表示子问题的关键。要准确地定义状态,需要考虑问题的哪些方面是可变的,以及如何用这些变量来唯一确定一个子问题。

  • 单变量状态:在斐波那契数列问题中,我们用一个变量 i 来表示数列的第 i 项,状态 dp[i] 就表示斐波那契数列的第 i 项的值。
  • 多变量状态:在背包问题中,我们需要两个变量 i 和 w 来表示子问题,其中 i 表示前 i 个物品,w 表示背包的容量,状态 dp[i][w] 表示前 i 个物品放入容量为 w 的背包中所能获得的最大价值。
2. 确定状态转移方程

状态转移方程描述了状态之间的递推关系,即如何从已知的子问题的解推导出当前问题的解。这是动态规划的核心步骤,需要根据问题的具体规则来推导。

  • 斐波那契数列:状态转移方程为 dp[i]=dp[i−1]+dp[i−2](i≥2),表示第 i 项的值等于第 i−1 项和第 i−2 项的值之和。
  • 背包问题:当第 i 个物品的重量 weights[i−1] 小于等于当前背包容量 w 时,dp[i][w]=max(values[i−1]+dp[i−1][w−weights[i−1]],dp[i−1][w]);否则,dp[i][w]=dp[i−1][w]。这个方程表示在考虑是否放入第 i 个物品时,取放入和不放入两种情况下的最大值。
3. 初始化状态

初始化状态是为了确定最简单的子问题的解,这些解是后续推导的基础。

  • 斐波那契数列:dp[0]=0,dp[1]=1,这是斐波那契数列的初始定义。
  • 背包问题:dp[0][w]=0(没有物品时,价值为 0),dp[i][0]=0(背包容量为 0 时,价值为 0)。
4. 确定计算顺序

根据状态转移方程,确定计算子问题的顺序,确保在计算某个子问题时,其所依赖的子问题已经被计算过。

  • 斐波那契数列:由于 dp[i] 依赖于 dp[i−1] 和 dp[i−2],所以计算顺序是从 i=2 开始,依次递增计算到 i=n。
  • 背包问题:外层循环遍历物品(i 从 1 到 n),内层循环遍历背包容量(w 从 1 到 W),这样可以保证在计算 dp[i][w] 时,dp[i−1][w] 和 dp[i−1][w−weights[i−1]] 已经被计算出来。
5. 求解最终问题

根据状态转移方程和计算顺序,逐步计算出所有子问题的解,最终得到原问题的解。

  • 斐波那契数列:最终问题的解是 dp[n],表示斐波那契数列的第 n 项。
  • 背包问题:最终问题的解是 dp[n][W],表示前 n 个物品放入容量为 W 的背包中所能获得的最大价值。

动态规划的分类

  • 线性动态规划:状态的变化是线性的,通常可以用一维数组来表示状态。例如,斐波那契数列、最长上升子序列等问题都属于线性动态规划。
  • 区间动态规划:状态通常与区间有关,用二维数组来表示状态,状态转移方程涉及区间的合并和拆分。例如,矩阵链乘法问题。
  • 树形动态规划:问题的结构是树形的,状态通常定义在树的节点上,通过递归的方式进行状态转移。例如,树的最大独立集问题。
  • 状态压缩动态规划:当状态的维度很高,但状态的取值范围较小时,可以使用位运算等技术将状态进行压缩,用较小的空间来存储状态。例如,旅行商问题的一种优化解法。

动态规划与其他算法的比较

  • 与分治法的比较:分治法也会将问题分解为子问题,但分治法的子问题通常是相互独立的,不会有子问题重叠的情况,而动态规划的子问题是相互关联且有重叠的。
  • 与贪心算法的比较:贪心算法在每一步都做出当前看起来最优的选择,而不考虑整体的最优解。动态规划则会考虑所有可能的选择,并通过比较得到全局最优解。贪心算法不一定能得到最优解,而动态规划可以保证得到最优解。

代码&解释:

  1. 分析问题,确定状态
    • 仔细研究问题,找出能够描述问题不同阶段或情况的状态变量。例如,在背包问题中,可以用背包的剩余容量和已选物品的集合来定义状态;在最长公共子序列问题中,可以用两个字符串的长度作为状态变量,表示当前已比较的字符串长度。
  2. 推导状态转移方程
    • 这是动态规划的核心步骤。根据问题的性质,确定如何由已知状态推导出未知状态。例如,对于 0/1 背包问题,设f(i)(j)表示前i个物品放入容量为j的背包中所能获得的最大价值,状态转移方程为f(i)(j)=max(f(i−1)(j),f(i−1)(j−w(i))+v(i)),其中w(i)表示第i个物品的重量,v(i)表示第i个物品的价值。即考虑是否选择当前物品,若选择当前物品,则背包容量减少,价值增加;若不选择当前物品,则背包容量和价值不变3。
  3. 确定初始状态(边界条件)
    • 设定状态转移方程的初始值。例如,在 0/1 背包问题中,当背包容量为 0 时,f(i)(0)=0;当没有物品可选时,f(0)(j)=0。在最长公共子序列问题中,当两个字符串长度都为 0 时,dp(0)(0)=0。这些边界条件是状态转移方程正确求解的基础。
  4. 按照状态转移方程进行递推求解
    • 从已知的初始状态开始,根据状态转移方程逐步递推求解出其他状态的值。可以使用循环或递归的方式进行递推。例如,在计算斐波那契数列时,可以使用循环从初始状态dp[0]=0,dp[1]=1开始,逐步计算出后续的斐波那契数。
  5. 从最终状态回溯得到最优解(如果需要)
    • 在求解出最终状态的值后,有些问题可能需要通过回溯的方式找到最优解的具体路径或组合。例如,在背包问题中,可以根据状态转移方程的选择记录回溯找到选择的物品。但并非所有动态规划问题都需要回溯步骤,这取决于具体问题的要求。

下面以斐波那契数列为例,展示动态规划的代码实现:

#include <iostream>
#include <vector>
using namespace std;

int fib(int n) {
    if (n <= 1) return n;
    vector<int> dp(n + 1, 0);  // dp(i)表示第i个斐波那契数
    dp[0] = 0;
    dp[1] = 1;
    for (int i = 2; i <= n; ++i) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
}

int main() {
    int n;
    cout << "enter n: ";
    cin >> n;
    cout << "fibonacci of " << n << " is " << fib(n) << endl;
    return 0;
}
希望这些代码能帮助您理解并解决这个问题,如果有问题,请随时提问。
  蒟蒻文章,神犇勿喷,点个赞再走吧!QAQ

相关文章:

  • 聚类(Clustering)基础知识2
  • Web开发-JS应用WebPack构建打包Mode映射DevTool源码泄漏识别还原
  • Linux内核perf性能分析工具案例分析
  • 聚类(Clustering)基础知识3
  • Java-sort(自定义排序)
  • axios基础入门教程
  • 在训练和推理过程中 对 token 数量的处理方式的差异
  • Python-用户账户与应用程序样式
  • <em>5</em><em>0</em><em>0</em><em>彩</em><em>票</em><em>官</em><em>网</em>
  • 如何在VSCode 中采用CMake编译C++程序
  • 大模型架构记录13【hr agent】
  • Jest系列二之基础实践
  • python列表常用方法大全
  • C++ RTTI 详解:动态类型识别的奥秘
  • EF Core表达式树
  • ComfyUI发展全景:从AI绘画新星到多功能创意平台的崛起
  • 论文阅读:GS-Blur: A 3D Scene-Based Dataset for Realistic Image Deblurring
  • 如何衡量用静态库还是动态库?
  • LoRA技术全解析:如何用4%参数量实现大模型高效微调
  • 恐惧与贪婪指数数据获取及可视化