Dynamic Programming
动态规划
动态规划(Dynamic Programming)算法是一种将大问题分解为子问题的优化算法,其基本思想是将问题分解为子问题,分别求解子问题的最优解,最后将子问题的最优解组合成原问题的最优解。知识点:
- 三要素:重叠子问题、最优子结构、状态转移方程
- 实现:动态规划算法通常采用自底向上或自顶向下的方式进行求解。自底向上的方法需要先求解小规模的子问题,再根据子问题的解求解大规模的子问题,直至求解原问题。自顶向下的方法则是从原问题开始,逐步将问题分解为子问题,并通过记忆化搜索的方式将已经求解过的子问题记录下来,避免重复计算。即带「备忘录」的递归算法,把一棵存在巨量冗余的递归树通过「剪枝」,改造成了一幅不存在冗余的递归图,极大减少了子问题(即递归图中节点)的个数。
- 应用:动态规划算法常用于求解具有重叠子问题和最优子结构的问题。如在路径规划、背包问题、编辑距离等领域。其中最经典的应用是背包问题,通过动态规划算法可以高效地求解背包问题的最优解。
- 优化:动态规划算法的时间复杂度往往较高。常用的优化方法包括状态压缩、空间优化、滚动数组等。状态压缩可以将问题的状态表示压缩成一个整数或者一个二进制数,从而减少状态的存储空间。空间优化可以通过仅保存部分状态,避免保存所有状态,从而减少空间使用。滚动数组则是通过循环利用数组来减少空间使用。另外,在实际应用中可以根据问题的特点进行算法的优化,例如在路径规划中,可以使用A*算法等算法加速求解过程。
- 类型题:都具有递推性质
- 求最优解,典型问题是背包问题,当前问题的最优解取决于子问题的最优解(最优子结构)。
- 计数类,如统计方案数的问题,当前问题的方案数取决于子问题的方案数。
遇到求最值的问题,基本都是由动态规划算法来解决
算法框架
- 解决两个字符串的动态规划问题,一般都是用两个指针 i, j 分别指向两个字符串的最后,然后一步步往前移动,缩小问题的规模
- 像子数组、子序列这类问题,你就可以尝试定义 dp[i] 是以 nums[i] 为结尾的最大子数组和/最长递增子序列,因为这样定义更容易将 dp[i+1] 和 dp[i] 建立起联系,利用数学归纳法写出状态转移方程
- 涉及两个字符串/数组时(比如最长公共子序列),dp 数组的含义如下:在子数组arr1[0..i]和子数组arr2[0..j]中,我们要求的子序列(最长公共子序列)长度为dp[i][j]。
- 只涉及一个字符串/数组时(比如本文要讲的最长回文子序列),dp 数组的含义如下:在子数组array[i..j]中,我们要求的子序列(最长回文子序列)的长度为dp[i][j]。
解题步骤:
- 确定 base case
- 确定「状态」,也就是原问题和子问题中会变化的变量
- 确定「选择」,也就是导致「状态」产生变化的行为
- 明确 dp 函数/数组的定义。根据 dp 数组的定义,运用数学归纳法的思想,假设 dp[0…i-1] 都已知,想办法求出 dp[i],一旦这一步完成,整个题目基本就解决了。如果无法完成这一步,很可能就是 dp 数组的定义不够恰当,需要重新定义 dp 数组的含义;或者可能是 dp 数组存储的信息还不够,不足以推出下一步的答案,需要把 dp 数组扩大成二维数组甚至三维数组
- 动态规划问题的最后一步优化,如果我们发现每次状态转移只需要 DP table 中的一部分,那么可以尝试缩小 DP table 的大小,只记录必要的数据,从而降低空间复杂度。
1 | # 自顶向下递归的动态规划 |
力扣指南
背包问题
变体:
- 0/1背包问题(0/1 Knapsack Problem):在给定的一组物品中,每个物品要么全部选取,要么不选取。每个物品只能选择一次放入背包中。
- 完全背包问题(Unbounded Knapsack Problem):在给定的一组物品中,每个物品的数量是无限的,可以重复选择放入背包中。每个物品可以选择多次放入背包中。
- 多重背包问题(Multiple Knapsack Problem):在给定的一组物品中,每个物品有一个限制数量。每个物品可以选择多次放入背包中,但是放入的数量不能超过其限制数量。
- 分组背包问题(Group Knapsack Problem):将物品分为若干组,每组中的物品只能选择一个放入背包中。每个物品只能选择一次放入背包中。
- 有限背包问题(Bounded Knapsack Problem):在给定的一组物品中,每个物品有一个限制数量。每个物品只能选择一次放入背包中,且放入的数量不能超过其限制数量。
- 带价值上限的背包问题(Knapsack Problem with Value Limit):在给定的一组物品中,每个物品有一个限制数量和一个价值上限。每个物品只能选择一次放入背包中,且放入的数量和总价值不能超过其限制。
0/1背包问题和完全背包问题是经典的NP问题。在求解背包问题时,需要遍历所有可能的物品组合来寻找最优解,而这个过程的时间复杂度通常是指数级的。因此,背包问题属于一类需要指数级时间复杂度的问题,即NP问题。
NP(Non-deterministic Polynomial time)是一个计算复杂性理论中的重要概念,它是指“非确定多项式时间”。一个问题属于NP,意味着对于该问题的一个解,可以在多项式时间内进行验证。然而,与确定性算法不同,NP问题目前没有已知的高效算法可以在多项式时间内找到解。
一个经典的NP问题是旅行商问题(Traveling Salesman Problem,TSP),即寻找最短的路径,使得一个旅行商可以依次访问一系列城市并返回起始城市,而且每个城市只能访问一次。
另一个重要的概念是NP完全性(NP-completeness),它指的是一类NP问题,被认为是计算上最难的问题之一。如果一个问题是NP完全的,那么它在多项式时间内可归约为任何其他的NP问题。
虽然目前尚未找到多项式时间的算法来解决所有的NP问题,但在实际应用中,我们可以采用启发式算法、近似算法和优化技巧等方法来求解NP问题的近似解或者找到满足特定要求的解。
算法框架
0/1背包:给定一个背包的容量C和n个物品,每个物品有两个属性:重量w和价值v。在限定的背包容量下选择物品,使得放入背包的物品总价值最大。
1 | def knapsack_01(weights, values, capacity): |
完全背包:给定一个背包的容量C和n个物品,每个物品有两个属性:重量w和价值v。在容量有限的背包中,可以选择任意数量的物品放入背包中,使得物品的总重量不超过背包容量,并且总价值最大。
1 | def knapsack_unbounded(weights, values, capacity): |
力扣指南
动态规划——背包 | ||
---|---|---|
题目 | 技巧 | 难度 |
✅416. 分割等和子集 | 【01背包】注意逻辑 | 🌟🌟 |
✅518. 零钱兑换 II | 【完全背包】好难 | 🌟🌟 |
✅494. 目标和 | 【01背包】 | 🌟🌟 |
✅474. 一和零 | 【01背包】 | 🌟🌟 |
✅279. 完全平方数 | 【完全背包】 | 🌟🌟 |
❌879. 盈利计划 | 【01背包】 | 🌟🌟 |
❌1049. 最后一块石头的重量 II | 【01背包】 | 🌟🌟 |
❌1230. 抛掷硬币 | 【01背包】 | 🌟🌟 |
❌1449. 数位成本和为目标值的最大数字 | 【完全背包】 | 🌟🌟 |
❌322. | 【完全背包】 | 🌟🌟 |