动态规划

动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。

算法思想

基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。

由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。

与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。

算法特性

以下三个动态规划的性质可用于判断动态规划方法是否适用于给定的问题。

  1. 最优子结构:在自下而上的递推过程中,我们求得的每个子问题一定是全局最优解,既然它分解的子问题是全局最优解,那么依赖于它们解的原问题自然也是全局最优解。
  2. 无后效性:无后效性有两层含义,第一层含义是,在推导后面阶段的状态的时候,我们只关心前面阶段的状态值,不关心这个状态是怎么一步一步推导出来的。第二层含义是,某阶段状态一旦确定,就不受之后阶段的决策影响。无后效性是一个非常“宽松”的要求。只要满足前面提到的动态规划问题模型,其实基本上都会满足无后效性。
  3. 重叠子问题:在求解原问题的时候,我们往往需要依赖其子问题,子问题依赖其子子问题,甚至可能同时依赖多个子问题,因此子问题之间是有重叠关系的。

题目特征

特点示例
特点一:计数题目问:有多少种方法?有多少种走法?
特点二:最大值/最小值题目问:某种选择的大值是什么?完成任务的最小时间是什么?数组的长子序列时什么?达到目标少操作多少次?
特点三:可能性题目问:是否有可能出现某种情况?是否有可能在游戏中胜出?是否可以取出 k 个数满足条件?

设计思路

动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。如图所示。动态规划的设计都有着一定的模式,一般要经历以下几个步骤。

初始状态 -> 决策 1 -> 决策 2 -> ... -> 决策 n -> 结束状态
  1. 划分阶段
  2. 确定状态和状态变量
  3. 确定决策并写出状态转移方程
  4. 寻找边界条件

一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。

实际应用中可以按以下几个简化的步骤进行设计:

  1. 分析最优解的性质,并刻画其结构特征
  2. 递归的定义最优解
  3. 以自底向上或自顶向下的记忆方式(备忘录法)计算出最优解
  4. 根据计算最优值时得到的消息,构造问题的最优解

解题套路:

  1. 明确状态
  2. 明确选择
  3. 明确 dp 函数/数组的定义
  4. 明确 base case

状态转移表法

状态转移方程法

dp[n] = dn[n - 1] + dp[n - 2];

具备三要素,确认边界条件,初始化状态,开始切菜:

  • dp[0] = 1
  • dp[1] = 1
const climbStairs = function (n) {
const dp = [];
dp[0] = 1;
dp[1] = 1;
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
};

优化

const climbStairs = function (n) {
let a1 = 1;
let a2 = 1;
for (let i = 2; i <= n; i++) {
[a1, a2] = [a2, a1 + a2];
}
return a2;
};

经典问题

斐波那契数列

最长公共子串

0-1 背包问题

对于一组不同重量、不可分割的物品,我们需要选择一些装入背包,在满足背包最大重量限制的前提下,背包中物品总重量的最大值是多少呢?

// weights 物品重量
// weights[i] -> 第 i 件物品重量
// values 物品价值
// values[i] -> 第 i 件物品价值
// W 背包可承载重量
const knapsack = function (weights, values, W) {};

第一步:明确 状态选择

  • 状态
    • 背包的空余容量剩多少
    • 可选择的物品还有哪些
  • 选择
    • 把这个物品装进背包
    • 不把这个物品装进背包
// 初始化 base case
dp[0][0][...] = base;
// 进行状态转移
for(const 状态1 of 状态1的所有值) {
for(const 状态2 of 状态2的所有值) {
for(...) {
dp[状态1][状态2][...] = 求最值(选择1, 选择2, ...);
}
}
}

第二步:明确 dp 数组的定义

dp[i][w] 的含义为:对于前 i 个物品,当背包的容量为 w 时,可以装的最大价值是 dp[i][w]

比如说,dp[3][5] = 6 的含义为:对于给定的一系列物品中,若只对前 3 个物品进行选择,当背包容量为 5 时,最多可以装下的价值为 6。

根据此定义,还可得出:base case 为 dp[0][..] = dp[..][0] = 0,我们想计算的结果是 dp[N][w]

第三步:根据 选择 写出状态转移逻辑

const knapsack = function (weights, values, W) {
let dp = new Array().fill();
dp[0][..] = 0;
dp[..][0] = 0;
for(let i = 0; i < weights.length; i++) {
for (let j = 0; j < W; j++) {
dp[i][j] = max(把物品 i 装进背包, 不把物品 i 装进背包)
}
}
return dp[weights.length][W]
}
  • W 的约束下,把物品 i 装进背包,最大价值是多少?
  • W 的约束下,不把物品 i 装进背包,最大价值是多少?
    • 相当于把 i - 1 物品装进背包 dp[i - 1][w]

杨辉三角

相关问题

动态规划题型分类:

  • 线性 DP
    • 子序列(不连续)
    • 子序列(连续)
      • 674 最长连续递增序列
      • 718 最长重复子数组
      • 53 最大子序和
    • 经典问题
      • 120 三角形最小路径和
      • 152 乘积最大子数组
      • 392 判断子序列
      • 115 不同的子序列
      • 583 两个字符串的删除操作
      • 647 回文子串
      • 516 最长回文子序列
      • 887 鸡蛋掉落
      • 354 俄罗斯套娃信封问题
    • 打家劫舍系列
      • 198 打家劫舍
      • 213 打家劫舍 II
      • 337 打家劫舍 III(树形 DP)
    • 股票问题系列
      • 121 买卖股票的最佳时机(只能买卖一次)
      • 122 买卖股票的最佳时机 II(可以买卖多次)
      • 123 买卖股票的最佳时机 III(最多买卖两次)
      • 188 买卖股票的最佳时机 IV(最多买卖 K 次)
      • 309 最佳买卖股票的最佳时机含冷冻期(买卖多次,卖出有一天冷冻期)
      • 714 买卖股票的最佳时机含手续费(买卖多次,每次有手续费)
    • 字符串匹配系列
      • 72 编辑距离
      • 44 通配符匹配
      • 10 正则表达式匹配
  • 区间 DP
    • 516 最长回文子序列
    • 730 统计不同回文子字符串
    • 1039 多边形三角剖分的最低得分
    • 664 奇怪的打印机
    • 312 戳气球
  • 背包 DP
    • 01 背包
      • 416 分割等和子集
      • 1049 最后一块石头的重量
      • 494 目标和
      • 474 一和零
    • 完全背包
      • 322 零钱兑换
      • 518 零钱兑换 II
      • 70 爬楼梯
      • 279 完全平方数
      • 139 单词拆分
  • 树形 DP
    • 124 二叉树中的最大路径和
    • 1245 树的直径
    • 543 二叉树的直径
    • 333 最大 BST 子树
    • 337 打家劫舍 III
  • 状态压缩 DP
    • 464 我能赢吗
    • 526 优美的排列
    • 935 骑士拨号器
    • 1349 参加考试的最大学生数
  • 数位 DP
    • 233 数字 1 的个数
    • 902 最大为 N 的数字组合
    • 1015 可被 K 整除的最小整数
  • 计数型 DP:可组合数学方法写出组合数,然后 DP 求组合数
    • 62 不同路径
    • 63 不同路径 II
    • 96 不同的二叉搜索树(卡特兰数)
    • 1259 不相交的握手(卢卡斯定理求大组合数模质数)
  • 递推型 DP
    • 70 爬楼梯
    • 509 斐波那契数
    • 935 骑士拨号器
    • 957 N 天后的牢房
    • 1137 第 N 个泰波那契数
  • 概率型 DP
    • 808 分汤
    • 837 新 21 点
  • 博弈型
    • 293 翻转游戏
    • 294 翻转游戏 II
    • 292 Nim 游戏
    • 877 石子游戏
    • 1140 石子游戏 II
    • 348 判定井字棋胜负
    • 794 有效的井字游戏
    • 1275 找出井字棋的获胜者
  • 记忆化搜索
    • 329 矩阵中的最长递增路径
    • 576 出界的路径数

算法思想归纳总结

标准分治动态规划贪心算法
适用类型通用问题优化问题优化问题
子问题结构每个子问题不同很多子问题重复只有一个子问题
最有子结构不需要必须满足必须满足
子问题数全部子问题都要解决全部子问题都要解决只要解决一个子问题
子问题在最优解里全部部分部分
选择与求解次序先选择后解决子问题先解决子问题后选择先选择后解决子问题

参考资料