动态规划只能应用于有最优子结构的问题。
算法解释
动态规划只能应用于有最优子结构的问题。最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能 完全满足,故有时需要引入一定的近似)。简单地说,问题能够分解成子问题来解决。” 通俗一点来讲,动态规划和其它遍历算法(如深/广度优先搜索)都是将原问题拆成多个子问 题然后求解,他们之间最本质的区别是,动态规划保存子问题的解,避免重复计算。解决动态规 划问题的关键是找到状态转移方程,这样我们可以通过计算和储存子问题的解来求解最终问题。
例题
打家劫舍
题目描述
假如你是一个劫匪,并且决定抢劫一条街上的房子,每个房子内的钱财数量各不相同。如果 你抢了两栋相邻的房子,则会触发警报机关。求在不触发机关的情况下最多可以抢劫多少钱。
输入输出样例
输入是一个一维数组,表示每个房子的钱财数量;输出是劫匪可以最多抢劫的钱财数量。
1 | Input: [2,7,9,3,1] |
基本步骤
动态规划的的四个解题步骤是:
- 定义子问题
- 写出子问题的递推关系
- 确定 DP 数组的计算顺序
- 空间优化(可选)
步骤一:定义子问题
稍微接触过一点动态规划的朋友都知道动态规划有一个“子问题”的定义。什么是子问题?子问题是和原问题相似,但规模较小的问题。
可以看到,子问题是参数化的,我们定义的子问题中有参数 k。假设一共有 n 个房子的话,就一共有 n个子问题。动态规划实际上就是通过求这一堆子问题的解,来求出原问题的解。这要求子问题需要具备两个性质:
原问题要能由子问题表示。例如这道小偷问题中,k=n 时实际上就是原问题。否则,解了半天子问题还是解不出原问题,那子问题岂不是白解了。
一个子问题的解要能通过其他子问题的解求出。例如这道小偷问题中,f(k)可以由 f(k-1)和 f(k-2) 求出,具体原理后面会解释。这个性质就是教科书中所说的“最优子结构”。如果定义不出这样的子问题,那么这道题实际上没法用动态规划解。
小偷问题由于比较简单,定义子问题实际上是很直观的。
一些比较难的动态规划题目可能需要一些定义子问题的技巧。
在确定了子问题的递推关系之后,下一步就是依次计算出这些子问题了。在很多教程中都会写,动态规划有两种计算顺序,一种是自顶向下的、使用备忘录的递归方法,一种是自底向上的、使用 dp 数组的循环方法。不过在普通的动态规划题目中,99% 的情况我们都不需要用到备忘录方法,所以我们最好坚持用自底向上的 dp 数组。
矩阵中搜索正方形或长方形
对于在矩阵内搜索正方形或长方形的题型,一种常见的做法是定义一个二维 dp 数组,其中 dp[i][j] 表示满足题目条件的、以 (i, j) 为右下角的正方形或者长方形的属性。
分割类型题
对于分割类型题,动态规划的状态转移方程通常并不依赖相邻的位置,而是依赖于满足分割条件的位置。
子序列问题
对于子序列问题,第一种动态规划方法是,定义一个 dp 数组,其中 dp[i] 表示以 i 结尾的子序列的性质
对于子序列问题,第二种动态规划方法是,定义一个 dp 数组,其中 dp[i] 表示到位置 i 为止 的子序列的性质,并不必须以 i 结尾。这样 dp 数组的最后一位结果即为题目所求,不需要再对每 个位置进行统计。
注意:
子问题的定义真的很重要 好几次都是因为子问题没有定义好导致无法解题
背包问题
背包问题是一种组合优化的 NP 完全问题:有 N 个物品和容量为 W 的背包,每个物品都有 自己的体积 w 和价值 v,求拿哪些物品可以使得背包所装下物品的总价值最大。如果限定每种物 品只能选择 0 个或 1 个,则问题称为 0-1 背包问题;如果不限定每种物品的数量,则问题称为无 界背包问题或完全背包问题。
我们可以用动态规划来解决背包问题。以 0-1 背包问题为例。我们可以定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。在我们遍 历到第 i 件物品时,在当前背包总容量为 j 的情况下,如果我们不将物品 i 放入背包,那么 dp[i][j] = dp[i-1][j],即前 i 个物品的最大价值等于只取前 i-1 个物品时的最大价值;如果我们将物品 i 放 入背包,假设第 i 件物品体积为 w,价值为 v,那么我们得到 dp[i][j] = dp[i-1][j-w] + v。我们只需 在遍历过程中对这两种情况取最大值即可,总时间复杂度和空间复杂度都为 O(NW)。