前言:本周来进行一次过去写过的动态规划内容的回顾,还是先列出动规的五部曲
本文适合对基本的动规和背包问题有一定了解的用户理解
动态规划五部曲
一 明确dp
数组下标以及其内值的含义
二 找出递推公式
三 如何对dp
数组进行初始化
四 确定遍历顺序
五 打印dp
数组找问题看是否符合预期
下面就直接开始了
63. 不同路径 II - 力扣(LeetCode)
相比于前置题目,该题目中增加了障碍,作为本体的核心
- 求解路径的数量,自然
dp
数组内存放值的含义就是路径数了,这道题目中是在一个二维地图中走,很明显i
和j
就是x轴方向和y轴方向了 - 机器人每次只能向下或者向右走一步,那到达每一个格子的方式就是到达其上面和左边方式的加和了,故有递推公式
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
- 此题的地图中存在障碍,那显现障碍势必是初始化的一部分,按题目中要求将二维
dp
数组中相对应的各自初始化为0,即没有办法到达该格子,初始的dp[0][0]
肯定是只有一种途径,即不动就可以到,所以要设为1,而在第一行和第一列的两组因为没有上面或者左边的途径,就只能一直向右或向下到达,所以要全部设为1 - 遍历的顺序其实无关紧要,换一下也是可行的,无非就是把地图转了90度进行操作
- 常规操作可以检查错误
注:在进行所有操作的时候都不要忘了障碍的存在,有可能障碍在起点或者终点,需要进行特判,在遍历dp
数组时也不要忘记处理障碍
int uniquePathsWithObstacles(int** obstacleGrid, int obstacleGridSize, int* obstacleGridColSize) {
int m = obstacleGridSize;
int n = *obstacleGridColSize;
if (obstacleGrid[0][0] == 1 || obstacleGrid[m - 1][n - 1] == 1) {
return 0;
}
int i, j;
int dp[m][n];
memset(dp, 0, sizeof(dp));
for (i = 0; i < m; i++ ) {
if (obstacleGrid[i][0] == 0) {
dp[i][0] = 1;
}
else {
break;
}
}
for (j = 0; j < n; j++ ) {
if (obstacleGrid[0][j] == 0) {
dp[0][j] = 1;
}
else {
break;
}
}
for (i = 1; i < m; i++ ) {
for (j = 1; j < n; j++ ) {
if(obstacleGrid[i][j] == 1) {
dp[i][j] = 0;
}
else {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
}
return dp[m - 1][n - 1];
}
122. 买卖股票的最佳时机 II - 力扣(LeetCode)
去年十二月贪心的那篇博客上我们已经见过这道题,但贪心的思路一般都不太好想,这里用动规的思路再进行解答
-
求解最大利润,则
dp
数组内放利润,题目中是根据天数进行操作的,遍历天数的责任落在了i
身上,那j
呢,天数和利润都名花有主了,剔除这两条,题目中还告诉我们可以在一天内选择出售或者购买股票,同时身上只能存在一张股票,好像依然没有 什么需要j
进行遍历的,那就需要换个思路了,身上只能同时有一张股票,遍历天数的时候就要考虑到底买还是不买,当你看重了某一天的股票,此时就出现了两种状态,之前到底有没有买过股票,这件事情就需要交给j
去做了,也不需要一个专门的变量,两个空间即可 -
由于
j
承担的职责只有两个空间,也就是说只需要遍历天数就可以了,j
的问题将应有的循环写两个式子,即前一天有股票和没有股票两种情况,两个递推式,那我们这里假设前一天没有票的状态在dp[i][0]
中,则dp[i][0] = fmax(dp[i - 1][0], dp[i - 1][1] + prices[i])
,有票的状态在dp[i][1]
中,则dp[i][1] = fmax(dp[i - 1][1], dp[i - 1][0] - prices[i])
,递推关系式管的是买不买今天的票,j
保存的是昨天有没有买票的情况,各位若是一下子没想通可以从逻辑上仔细看一看这两个递推关系式 -
初始化也是两部分,第一天没买票则初始化为0,买了票那此时是没有任何本钱的,但若执意要买减成负数其实也无关紧要,因为最终返回的结果一定是最后一天没有票,哪怕有票也要在最后一天卖了获取最大利润,返回的结果是
dp[i][0]
,与dp[i][1]
是没有关系的,所以初始化第一天时就按买了票减去相应的钱数即可 -
只有一层循环,此题不存在遍历顺序的问题
-
常规操作可以检查错误
int maxProfit(int* prices, int pricesSize) { int n = pricesSize; int dp[n][2]; dp[0][0] = 0, dp[0][1] = -prices[0]; for (int i = 1; i < pricesSize; i++) { dp[i][0] = fmax(dp[i - 1][0], dp[i - 1][1] + prices[i]); dp[i][1] = fmax(dp[i - 1][1], dp[i - 1][0] - prices[i]); } return dp[n - 1][0]; }
213. 打家劫舍 II - 力扣(LeetCode)
这道题比前置题目多了首尾房屋相连的条件,大致思路是差不错的,多了一个成环的处理
所以先展示未成环时的解决
- 看题可以知道
dp
数组内存放的应该是偷到的钱数,而i
去遍历房屋,j
似乎没有什么必要,用一维数组处理就好了 - 偷的关键在于偷不偷当前房间,如果不偷的话直接继承前一个房屋偷到的最大钱数即可,偷的话就不能偷前一个房间,继钱数只能从前前一个房间继承,则有
dp[i] = fmax(dp[i - 1], dp[i - 2] + nums[i])
- 根据2中的分析,遍历得从第三个屋子开始,那就要对前两个房屋初始化,自然
dp[0]
要有最大值就是偷当前屋子的钱,即dp[0] = nums[0]
,而为了算出三号房屋能偷到的最多的钱,则dp[1] = fmax(nums[0], nums[1])
- 只有一层循环,此题不存在遍历顺序的问题
- 常规操作可以检查错误
int rob(int* nums, int numsSize) {
if (numsSize == 0) return 0;
if (numsSize == 1) return nums[0];
int dp[110];
dp[0] = nums[0];
dp[1] = fmax(nums[0], nums[1]);
for (int i = 2; i < numsSize; i++) {
dp[i] = fmax(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp[numsSize - 1];
}
基础思路有了,现在再加上成环的条件,其实关键的因素在于首尾房屋,不偷首尾,那思路直接转化为少了两个房屋的前置问题了
而不考虑尾部房屋的情况其实是包含在其他两种情况之中的
int robRange(int* nums, int start, int end) {
int dp[200];
dp[start] = nums[start];
dp[start + 1] = fmax(nums[start], nums[start + 1]);
for (int i = start + 2; i <= end; i++) {
dp[i] = fmax(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp[end];
}
int rob(int* nums, int numsSize) {
if (numsSize == 1) {
return nums[0];
} else if (numsSize == 2) {
return fmax(nums[0], nums[1]);
}
return fmax(robRange(nums, 0, numsSize - 2), robRange(nums, 1, numsSize - 1));
}
在解决这类问题时也不能忘记特判长度为1和2的情况,毕竟dp
数组是从第三个房屋开始遍历的
1049. 最后一块石头的重量 II - 力扣(LeetCode)
这是本周博主才刷的题,看题面第一时间想到的应该是二分,排序加贪心,实现起来未免太过复杂
一般贪心的题都能用动规解,但这道用动规去看也很迷糊,如果一道能用动规去解的题目题面描述地完全想不到什么思路,那么它一定是要进行某种转化,转化成我们能看懂的样子
明明可以一块块石头选择碎还是不碎,这样子就很舒服了,但偏偏题目告诉我们要碎就两块一起碎,那能不能像上面股票那样分成两个状态,一次遍历石头总量,很显然不行,这两块石头是实际存在的,总不能爆掉第一块石头后去找第二块吧,这样就成暴力求解了
想不清这个问题,那我们先试一试动规五部曲(注:试错不一定所有地方都对)
-
- 说到遍历石头,这件事就交给
i
去做了,再想,题目要求解剩余的最小质量,dp
数组里装的自然是质量,那么j
应该承担什么任务,不妨先进行初始化,有剩余就有原先,而碎掉石头后质量会减小,那么初始化的问题就解决了,即最开始的dp[0][0]
应该装着这堆石块的质量总和 - 现在第0号位置装着所有的质量,遍历时是一块一块石头减去的,选择碎还是不碎,这是两种状态,而01背包中恰好也有着选与不选两种状态(对01背包问题没有了解的朋友建议先去学习其思想),既然股票的思路行不通,来试一试套01背包的模板,那这个时候
j
理所应当地要去遍历每一块石头的重量了 - 这个时候又有问题了,假设此题就是01背包的变式,01背包里面物品有体积和价值两个属性,而本体中只有质量一个属性,就算对应明显也是对应的体积,好像又碰壁了,博主在最开始突破此题的时候就卡死在这了,类比一下,01背包问的是价值,本体问的是质量,没错,本体中的质量同时充当了体积和价值两种角色,这就属于初见必死了,如果没见过一个属性充当两种效果,本题基本是做不出来的,还是要多见题
- 说到遍历石头,这件事就交给
-
下来只需要尝试将该问题转化为01背包的模板就行了,01背包是求解最大值,本题求解最小值,那可以先转化一下,求解一个可以碎掉的最大值,来获取剩余的最小值,这个思路转化了,那前面的分析基本可以宣告失败了,初始化需要重新考虑,每碎掉一个石头增加
dp
数组中的质量,那最初的dp[0][0]
就应该初始化为0 -
回到最开始就有的那个问题,一次碎要碎两块,套背包模板时每次加需要加两次相同的质量,属实是没有必要,让
j
在遍历质量时以最大质量的一半为极限即可,这个操作就相当于把石头自动分成了两堆,求解其中一堆能达到的最大质量就相当于求解出两堆一共能达到的最大质量,至于怎么分其实不需要考虑,在dp
遍历的过程中会自己分好的
既然都是背包的模板了,动规五部曲就不进行了,有问题的可以去看一看01背包的具体解决,结合上面对问题的转换即可解决此题
int lastStoneWeightII(int* stones, int stonesSize) {
int dp[15001];
memset(dp, 0, sizeof(dp));//这里的初始化因为直接套了01背包的一维解决,因其后面遍历时的倒叙遍历,所有值都应该初始化为0
int sum = 0;
for (int i = 0; i < stonesSize; i++) {
sum += stones[i];
}
int target = sum / 2;
for (int i = 0; i < stonesSize; i++) {
for (int j = target; j >= stones[i]; j--) {
dp[j] = fmax(dp[j], dp[j - stones[i]] + stones[i]);
}
}
return sum - dp[target] * 2;
}
动规的类型很多,解决此类问题的核心还是要多见题多做题,本文就到此结束了