动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的
例如:有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
动态规划中dp[j]是由dp[j-weight[i]]推导出来的,然后取max(dp[j], dp[j - weight[i]] + value[i])。
但如果是贪心呢,每次拿物品选一个最大的或者最小的就完事了,和上一个状态没有关系。
所以贪心解决不了动态规划的问题。
大家知道动规是由前一个状态推导出来的,而贪心是局部直接选最优的,对于刷题来说就够用了。
对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
找问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的!
做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果。
然后再写代码,如果代码没通过就打印dp数组,看看是不是和自己预先推导的哪里不一样。
如果打印出来和自己预先模拟推导是一样的,那么就是自己的递归公式、初始化或者遍历顺序有问题了。
如果和自己预先模拟推导的不一样,那么就是代码实现细节有问题。
这样才是一个完整的思考过程,而不是一旦代码出问题,就毫无头绪的东改改西改改,最后过不了,或者说是稀里糊涂的过了。
斐波那契数:
动规五部曲:
这里我们要用一个一维dp数组来保存递归的结果
1. 确定dp数组以及下标的含义
dp[i]的定义为:第i个数的斐波那契数值是dp[i]
2. 确定递推公式
为什么这是一道非常简单的入门题目呢?
因为题目已经把递推公式直接给我们了:状态转移方程 dp[i] = dp[i - 1] + dp[i - 2];
3. dp数组如何初始化
题目中把如何初始化也直接给我们了,如下:dp[0]=0, dp[1]=1
4. 确定遍历顺序
从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的
5. 举例推导dp数组
按照这个递推公式dp[i] = dp[i - 1] + dp[i - 2],我们来推导一下,当N为10的时候,dp数组应该是如下的数列:0 1 1 2 3 5 8 13 21 34 55
爬楼梯:
动规五部曲:
定义一个一维数组来记录不同楼层的状态
1. 确定dp数组以及下标的含义
dp[i]: 爬到第i层楼梯,有dp[i]种方法
2. 确定递推公式
如何可以推出dp[i]呢?
dp[i] = dp[i - 1] + dp[i - 2] 。
在推导dp[i]的时候,一定要时刻想着dp[i]的定义,否则容易跑偏。
这体现出确定dp数组以及下标的含义的重要性!
3. dp数组如何初始化
再回顾一下dp[i]的定义:爬到第i层楼梯,有dp[i]种方法。
那么i为0,dp[0]应该是多少呢,这个可以有很多解释,但基本都是直接奔着答案去解释的。
需要注意的是:题目中说了n是一个正整数,题目根本就没说n有为0的情况。
所以本题其实就不应该讨论dp[0]的初始化!
我相信dp[1] = 1,dp[2] = 2,这个初始化大家应该都没有争议的。
⭐所以我的原则是:不考虑dp[0]如何初始化,只初始化dp[1] = 1,dp[2] = 2,然后从i = 3开始递推,这样才符合dp[i]的定义。
4. 确定遍历顺序
从递推公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,遍历顺序一定是从前向后遍历的
5. 举例推导dp数组
746.使用最小花费爬楼梯
1. 确定dp数组以及下标的含义
dp[i]的定义:到达第i台阶所花费的最少体力为dp[i]
2. 确定递推公式
dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
3. dp数组如何初始化
dp[0]=0, dp[1]=0
4. 确定遍历顺序
因为是模拟台阶,而且dp[i]由dp[i-1]dp[i-2]推出,所以是从前到后遍历cost数组就可以了
62.不同路径
注意到 f(i,j)f(i, j)f(i,j) 仅与第 i 行和第 i−1 行的状态有关,因此我们可以使用滚动数组代替代码中的二维数组,使空间复杂度降低为 O(n)。此外,由于我们交换行列的值并不会对答案产生影响,因此我们总可以通过交换 m和 n 使得 这样空间复杂度降低至 O(min(m,n))
63.不同路径Ⅱ
「滚动数组思想」是一种常见的动态规划优化方法,在我们的题目中已经多次使用到,例如「剑指 Offer 46. 把数字翻译成字符串」、「70. 爬楼梯」等,当我们定义的状态在动态规划的转移方程中只和某几个状态相关的时候,就可以考虑这种优化方法,目的是给空间复杂度「降维」。
他们都以二维坐标作为状态,大多数都可以使用滚动数组进行优化。如果我们熟悉这类问题,可以一眼看出这是一个动态规划问题。当我们不熟悉的时候,怎么想到用动态规划来解决这个问题呢?我们需要从问题本身出发,寻找一些有用的信息,例如本题中:
动态规划的题目分为两大类,一种是求最优解类,典型问题是背包问题,另一种就是计数类,比如这里的统计方案数的问题,它们都存在一定的递推性质。前者的递推性质还有一个名字,叫做 「最优子结构」 ——即当前问题的最优解取决于子问题的最优解,后者类似,当前问题的方案数取决于子问题的方案数。所以在遇到求方案数的问题时,我们可以往动态规划的方向考虑。
通常如果我们察觉到了这两点要素,这个问题八成可以用动态规划来解决。读者可以多多练习,熟能生巧。
滚动数组的理解
1. 首先从第一行开始遍历,f[0]已经进行初始化,想到到达obstacleGrid[0][j]只能够从左往右走,上面没有路可以走,java整型数组初始化的时候默认值是零
也就是说f[j]的默认值都是零,经过f[j] += f[j - 1] (此时i=0也就是在第一行)的第一次循环完成后第一行的所有值就已经确定了,没有障碍物就是全为1,有障碍物就是为1或0。
现在f[i]数组中保存的值就是从起点出发到第一行第i个网格有多少条不同的路径。
2. 之后继续循环,当i=1也就是遍历到第二行,(每一行第一个网格的值如果有障碍就将其置0,如果无障碍的话值与上一行的f[j]保持一致,不用变化),第二个网格的值计算方式为上方的网格值加上左边网格的值,左边网格值已经确定,为f[j-1],而上方网格的值就是上一轮循环中的f[j],两个值相加得到第二行第二个网格的值并将f[j]中的数据覆盖,依次循环将f[j]中的数据全部替换,此时f[j]中就保存了从起点出发到第二行第j个网格有多少条不同的路径。
3. 继续循环直到完成,最后的结果就是从起点到终点有多少条不同的路径。
对于动态规划的两种解法,都是只需要上一层的解,而不需要上上一层的。也就是求第i
行时,只需要i-1
行已求解过的值,不需要i-2
行的了。所以这里可以用滚动数组进行优化,将二维数组改为一维数组。
计算当前值 = 以求出的左边值 + 上一次迭代同位置的值 dp[j] = dp[j - 1] + dp[j]