一、动态规划
(一)相关定义
(1)定义与特点
动态规划(Dynamic Programming,DP)算法通常用于求解某种具有最优性质的问题。在这类问题中,可能会有许多可行解,每一个解都对应一个值,我们希望找到具有最优值的解。
动态规划算法与分治法类似,其基本思想也是将待求解的问题分解成若干个子问题,先求解子问题,然后从这些子问题的解中 得到原有问题的解。与分治法不同的是,动态规划经分解后得到的子问题往往 不是相互独立 的。
递归——递归调用过程中,有些子问题被反复计算多次;动态规划——如果用一个数组保存已解决的子问题的答案,而在需要的时候再从数组中查找出已求得子问题的答案,这样就可以避免大量的重复计算。这种就是动态规划算法。(动态规划算法需要存储各子问题的解,所以它的空间复杂度要大于其他算法,这是一种空间换时间的策略。)
(2)求解步骤
1.分析最优子结构性质(递推关系)
2.递归定义最优值(动态规划核心)
3.自底向上的方式计算出最优值(动态规划的执行过程)
4.根据计算最优值时得到的信息,构造最优解备注:动态规划其实是一种数学方法,是求解某类问题的一种方法,而不是一种特殊的算法,没有一个标准的数学表达式或明确定义的一种规则。
自底向上,逐层递推,把较大的数字与上一层相加,把得到的数字存到解的数组中(避免重复计算),加到最后就是本题最优解。
例如:
在图中,19+2>7+2,所以把19与上一层相加
自底向上,逐层向上相加,并把计算的结果存储到结果数组中。
动态规划问题的一般形式就是求最值。动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说让你求最长递增子序列呀,最小编辑距离呀等等。
既然是要求最值,核心问题是什么呢?求解动态规划的核心问题是穷举。
(3)其余一些特征
如何构造出子问题,每个子问题必须满足”重叠子结构“和”最优子结构“两个要求才能用动态规划来解决。
1)重叠子结构
前面我们说动态规划是将一个多阶段问题分成几个相互联系的单阶段问题。而这几个相互联系的单阶段问题一定要是一族同类型的子问题,这样分成多阶段决策才是有意义的,否则我们每次面对的决策都是不同的问题,也就失去了分成单阶段的意义。
在算法上,我们把这种同类型的子问题又叫做”重叠子结构“。重叠不是相同,而是同类型,我们必须得利用前面子结构计算出的结果,用递推不断地调用同一个问题。
所以说,《算法导论》中的重叠子结构其实就是对应了数学中将多阶段问题分成单阶段问题的过程,只不过强调了这个单阶段问题必须是”重叠的“,这样才能够用同一种算法递推地解决问题。
2)最优子结构
利用前面已经解决的子问题最优解来构造并解决后面的子问题。能够构造的前提就是要满足指标函数的分离性,也就是全局最优解一定能够拆成子问题的最优解,从而递推解决。
对应到算法中,就是按照自底向上的策略,首先找到子问题的最优解,解决子问题,然后逐步向上找到问题的一个最优解。也就是说,整体问题的最优解包括了子问题的最优解。我们又叫做”最优子结构“。
(二)动态规划的分类以及典型例题
以下是每种动态规划类型的定义、典型问题的解释以及解决方式:
1. 线性 DP
- 定义: 线性 DP 是一种适用于一维序列问题的动态规划方法。它通过将问题分解为一系列线性排列的子问题,并利用这些子问题的解来构建原问题的解.
- 典型问题 - 最长上升子序列 (300)
- 解释: 给定一个整数数组,找到其中最长严格递增子序列的长度。子序列是由原数组派生而来的序列,通过删除(或不删除)原数组中的元素而不改变其余元素的顺序得到.
- 解决方式:
- 动态规划: 定义 dp\[i\] 表示以第 i 个元素结尾的最长上升子序列的长度。状态转移方程为 dp\[i\] = max(dp\[i\], dp\[j\] + 1),其中 j < i 且 nums\[j\] < nums\[i\]。初始状态为 dp\[i\] = 1,表示每个元素自身可以构成长度为 1 的上升子序列.
- 贪心 + 二分: 维护一个单调递增的数组 d\[i\],表示长度为 i 的最长上升子序列的末尾元素的最小值。遍历原数组,对于每个元素,使用二分查找在 d 中找到合适的位置进行更新,从而实现更优的时间复杂度 O(nlogn).
2. 区间 DP
- 定义: 区间 DP 是一种适用于在某个区间内进行操作的问题的动态规划方法。它通过将问题分解为多个区间,并利用这些区间的解来构建原问题的解.
- 典型问题 - 最长回文子串 (5)
- 解释: 给定一个字符串,找到其中最长的回文子串。回文子串是指正读和反读都相同的子串.
- 解决方式: 定义 dp\[i\]\[j\] 表示字符串 s\[i:j\] 是否是回文子串。状态转移方程为 dp\[i\]\[j\] = dp\[i+1\]\[j-1\],当 s\[i\] == s\[j\] 时。初始状态为 dp\[i\]\[i\] = true,表示每个单个字符都是回文子串;dp\[i\]\[i+1\] = true,当 s\[i\] == s\[i+1\] 时,表示相邻的两个相同字符也是回文子串.
3. 背包 DP
- 定义: 背包 DP 是一种用于解决背包问题及其变种的动态规划方法。它通过在有限的容量内选择物品以最大化价值或最小化成本.
- 典型问题 - 01 背包 (416)
- 解释: 给定一组物品,每个物品都有一个重量和一个价值,以及一个背包的最大容量。要求选择若干物品放入背包,使得背包中物品的总重量不超过背包容量,且总价值最大.
- 解决方式: 定义 dp\[i\]\[j\] 表示前 i 个物品在容量为 j 的背包中能够获得的最大价值。状态转移方程为 dp\[i\]\[j\] = max(dp\[i-1\]\[j\], dp\[i-1\]\[j-w\[i\]\] + v\[i\]),其中 w\[i\] 和 v\[i\] 分别表示第 i 个物品的重量和价值。初始状态为 dp\[0\]\[j\] = 0,表示没有物品时背包的价值为 0.
4. 树形 DP
- 定义: 树形 DP 是一种适用于树结构问题的动态规划方法。它通过递归地处理树的每个节点,并利用子节点的信息来计算当前节点的解.
- 典型问题 - 二叉树中的最大路径和 (124)
- 解释: 给定一个二叉树,找到一条从任意节点出发,沿父节点-子节点连接,到达任意节点的路径,使得路径中各节点值的总和最大.
- 解决方式: 定义 dp\[x\] 表示以 x 为根节点的子树到叶子节点的最大路径和。对于每个节点 x,计算其左子树和右子树的最大路径和 left 和 right,然后更新当前节点的最大路径和 res = max(res, x.val + left + right)。同时,返回当前节点到叶子节点的最大路径和 max(left, right) + x.val.
5. 状态压缩 DP
- 定义: 状态压缩 DP 是一种通过压缩状态来减少存储空间需求的动态规划方法。它适用于状态数量较少的问题,通常使用位运算来表示状态.
- 典型问题 - 骑士拨号器 (935)
- 解释: 在一个电话拨号盘上,骑士可以按照国际象棋中骑士的移动方式在数字键之间跳跃。给定一个正整数 N,计算骑士从任意数字键出发,经过 N-1 步跳跃后,能够拨出的不同号码的数量.
- 解决方式: 定义 dp\[i\]\[k\] 表示在 k 位置走了 i 步的不同号码数。状态转移方程为 dp\[i\]\[k\] = sum(dp\[i-1\]\[j\]),其中 j 是从 k 位置可以跳跃到的位置。使用二维数组存储状态,并通过迭代的方式计算最终结果.
6. 数位 DP
- 定义: 数位 DP 是一种用于解决与数字位相关的计数问题的动态规划方法。它通过逐位分析数字的每一位,利用前导零和当前位的限制来计算满足条件的数字数量.
- 典型问题 - 数字 1 的个数 (233)
- 解释: 给定一个整数 n,计算所有小于等于 n 的非负整数中数字 1 出现的个数.
- 解决方式: 从高位到低位逐位分析数字 n 的每一位。对于每一位,根据当前位的值(0、1 或其他数)以及高位和低位的值,计算该位为 1 时的数字个数,并累加到结果中.
7. 计数型 DP
- 定义: 计数型 DP 是一种用于计算满足某种条件的方案数的动态规划方法。它通常可以结合组合数学的方法来求解.
- 典型问题 - 不同路径 (62)
- 解释: 一个机器人位于一个 m x n 网格的左上角,每次只能向下或者向右移动一步,计算机器人到达网格右下角的不同路径数量.
- 解决方式: 定义 dp\[i\]\[j\] 表示到达网格 (i, j) 的不同路径数量。状态转移方程为 dp\[i\]\[j\] = dp\[i-1\]\[j\] + dp\[i\]\[j-1\]。初始状态为 dp\[0\]\[j\] = 1 和 dp\[i\]\[0\] = 1,表示第一行和第一列的路径数量都为 1.
8. 递推型 DP
- 定义: 递推型 DP 是一种通过递推公式直接计算结果的动态规划方法。它适用于具有简单递推关系的问题.
- 典型问题 - 爬楼梯 (70)
- 解释: 假设你正在爬楼梯,需要 n 阶才能到达楼顶,每次可以爬 1 或 2 个台阶,计算爬到楼顶的不同方法数.
- 解决方式: 定义 dp\[i\] 表示爬到第 i 阶的不同方法数。状态转移方程为 dp\[i\] = dp\[i-1\] + dp\[i-2\]。初始状态为 dp\[1\] = 1 和 dp\[2\] = 2,表示爬到第 1 阶和第 2 阶的方法数分别为 1 和 2.
9. 概率型 DP
- 定义: 概率型 DP 是一种用于计算概率或期望值的动态规划方法。它通过分析状态之间的转移概率和期望值来求解问题.
- 典型问题 - 分汤 (808)
- 解释: 给定两个容量分别为 A 和 B 的汤锅,每次可以将一个锅中的汤倒入另一个锅中,直到其中一个锅为空。计算两个锅都为空的概率.
- 解决方式: 定义 dp\[i\]\[j\] 表示容量为 i 和 j 的两个锅都为空的概率。状态转移方程根据汤的转移方式来计算,通常需要考虑多种转移情况,并累加相应的概率.
10. 博弈型 DP
- 定义: 博弈型 DP 是一种用于解决博弈问题的动态规划方法。它通过分析博弈双方的策略和状态转移来求解问题.
- 典型问题 - Nim 游戏 (292)
- 解释: Nim 游戏是一种经典的博弈游戏,有若干堆石子,每次可以从任意一堆中取走若干个石子,最后取走最后一个石子的玩家获胜。判断先手玩家是否有必胜策略.
- 解决方式: 利用 Nim 游戏的数学性质,计算所有石子堆的异或和。如果异或和为 0,则先手玩家必败;否则,先手玩家有必胜策略.
11. 记忆化搜索
- 定义: 记忆化搜索是一种结合深度优先搜索和动态规划的方法,适用于状态的转移方向不确定的问题。它通过缓存已计算的状态来避免重复计算.
- 典型问题 - 矩阵中的最长递增路径 (329)
- 解释: 给定一个矩阵,找到其中最长的递增路径的长度。路径可以从任意单元格开始,每次只能向相邻的单元格移动,且相邻单元格的值必须大于当前单元格的值.
- 解决方式: 使用深度优先搜索遍历矩阵中的每个单元格,对于每个单元格,递归地计算其最长递增路径的长度,并将结果缓存起来。当再次访问同一个单元格时,直接从缓存中获取结果,避免重复计算.
二、动态规划解法
(一)动态规划的求解套路
核心是穷举,
tip:穷举与枚举的联系和区别?
- 概念本质相同:穷举法和枚举法从本质上来说,都是一种通过列举所有可能情况来求解问题的方法。它们都是在一个预先确定的范围内,将所有可能的元素、组合或者状态逐一列出,然后对每一种情况进行检查,以确定是否满足问题的要求。
- 算法思想类似:在算法实现的过程中,它们通常都需要利用循环结构来遍历所有可能的情况。例如,在对一个整数区间内的所有数字进行操作时,无论是穷举还是枚举,都可能会使用类似 “for 循环” 这样的结构来依次访问每个数字。
- 目的相同:都是为了找到满足特定条件的解,这个解可以是一个最优解(如在优化问题中找到最好的方案),也可以是所有符合条件的解(如找出所有满足某个方程的整数解)。
区别
- 范围界定
- 穷举:穷举法强调的是对所有可能情况的完全列举,不遗漏任何一种可能性。它通常会涉及一个比较大的、明确的范围,并且这个范围往往是根据问题的边界条件严格确定的。例如,在破解一个四位数字密码(每位数字是 0 - 9)时,穷举法会从 0000 开始,一直列举到 9999,总共 10000 种情况,涵盖了密码所有可能的组合。
- 枚举:枚举法的范围可能相对更灵活,它可能只是列举出一部分具有代表性的或者符合某种规则的情况。例如,在列举一周内的工作日时,我们枚举为 “周一、周二、周三、周四、周五”,这里没有列举周末,是根据 “工作日” 这个规则来列举的,而不是像穷举那样把一周七天所有可能的组合等都列出来。
-
(二)回溯算法
优化回溯算法只有剪枝一种方法,在回溯算法:组合问题再剪剪枝中把回溯法代码做了剪枝优化,树形结构如图:
(三)逆推法
逆推法定义与原理
- 逆推法是一种推理方法,它是从问题的结果出发,按照与事件发展过程相反的顺序,逐步向前推导条件。它是基于已知的最终状态,根据问题中的逻辑关系和规则,反推出初始状态或者中间过程所需要满足的条件。
参考文献
[1]超详细!动态规划详解分析(典型例题分析和对比,附源码)_动态规划案例解析-CSDN博客
[2]算法之动态规划总结(11种DP类型,70道全部搞懂)_dp算法-CSDN博客
[3]https://zhuanlan.zhihu.com/p/365698607
[4]深度好文:动态规划详解 - labuladong - 博客园