代码随想录二刷|第九章:动态规划

动规五部曲:

  1. 确定dp数组以及下标的含义
  2. 递推公式
  3. 初始化
  4. 遍历顺序
  5. 打印dp数组

509. 斐波那契数

只需要维护两个数值就可以了,不需要记录整个序列。

70. 爬楼梯

要正确处理n=1时的情况,因为当n为1时,vector dp(n + 1);将只初始化dp[0]和dp[1]。然而,代码中有dp[2] = 2。

62. 不同路径

so easy

63. 不同路径 II

初始化的写法,应该把条件判断放到for循环语句里,否则像我那种在for循环里面用if判断后break的写法只能使得第一个没有障碍的地方变为1:

	if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) //如果在起点或终点出现了障碍,直接返回0
            return 0;
        vector<vector<int>> dp(m, vector<int>(n, 0));
        for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1;
        for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) dp[0][j] = 1;

343. 整数拆分(难)

1、dp数组的定义:是一维的,不是二维的;但遍历的时候又是二维的
2、看到这道题目,都会想拆成两个呢,还是三个呢,还是四个…
dp数组的递推公式(难点):dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
j * (i - j) 是单纯的把整数拆分为两个数相乘,而j * dp[i - j]是拆分成两个以及两个以上的个数相乘。

96.不同的二叉搜索树(难)

dp数组的递推公式(难点):
dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量]
j相当于是头结点的元素,从1遍历到i为止。
所以递推公式:dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量
举例:

dp[3],就是 元素1为头结点搜索树的数量 + 元素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量

元素1为头结点搜索树的数量 = 右子树有2个元素的搜索树数量 * 左子树有0个元素的搜索树数量
元素2为头结点搜索树的数量 = 右子树有1个元素的搜索树数量 * 左子树有1个元素的搜索树数量
元素3为头结点搜索树的数量 = 右子树有0个元素的搜索树数量 * 左子树有2个元素的搜索树数量

有2个元素的搜索树数量就是dp[2]。
有1个元素的搜索树数量就是dp[1]。
有0个元素的搜索树数量就是dp[0]。
所以dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2]
在这里插入图片描述

01背包问题 二维

1、dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。

有两个方向推出来dp[i][j]:

  • 不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)
  • 放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值

2、初始化 weight 和 value 向量时,要指定它们的大小。不然不能通过索引填充它们。
3、在处理背包问题的循环中,需要确保不要访问负的索引。

    for(int i = 1; i < weight.size(); i++) { // 遍历科研物品
        for(int j = 0; j <= bagweight; j++) { // 遍历行李箱容量
            // 如果装不下这个物品,那么就继承dp[i - 1][j]的值
            if (j < weight[i]) dp[i][j] = dp[i - 1][j];
            // 如果能装下,就将值更新为 不装这个物品的最大值 和 装这个物品的最大值 中的 最大值
            // 装这个物品的最大值由容量为j - weight[i]的包任意放入序号为[0, i - 1]的最大值 + 该物品的价值构成
            else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
        }
    }

4、初始化 dp 数组时,只有当 weight[0] 小于等于 j 时,dp[0][j] 才应该等于 value[0]。
在这里插入图片描述

01背包问题 一维

1、遍历背包的顺序是不一样的!

二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小

为什么呢?

倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!

2、两个嵌套for循环的顺序,代码中是先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?

不可以!

因为一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。

倒序遍历的原因是,本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。

416. 分割等和子集

if (sum % 2 == 1) return false;

1049. 最后一块石头的重量 II

本题其实和416. 分割等和子集 (opens new window)几乎是一样的,只是最后对dp[target]的处理方式不同。
分割等和子集 (opens new window)相当于是求背包是否正好装满,而本题是求背包最多能装多少。

494. 目标和

回溯法:(超时)

        int sum = 0;
        for (int i = 0; i < nums.size(); i++) sum += nums[i];
        if (S > sum) return 0; // 此时没有方案
        if ((S + sum) % 2) return 0; // 此时没有方案,两个int相加的时候要各位小心数值溢出的问题
        int bagSize = (S + sum) / 2; // 转变为组合总和问题,bagsize就是要求的和

这里的bagSize就是39. 组合总和里的target(脑子也不知道转一下。。)
我想的是每个数都有两种取法:+或-,但不知道代码怎么写:

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int count = 0;
        backtrack(nums, target, 0, 0, count);
        return count;
    }

private:
    void backtrack(vector<int>& nums, int target, int index, int current_sum, int& count) {
        if (index == nums.size()) {
            if (current_sum == target) {
                count++;
            }
            return;
        }

        // 选择当前数字为正
        current_sum += nums[index];
        backtrack(nums, target, index + 1, current_sum, count);
        // 回溯,撤销选择
        current_sum -= nums[index];

        // 选择当前数字为负
        current_sum -= nums[index];
        backtrack(nums, target, index + 1, current_sum, count);
        // 回溯,撤销选择
        current_sum += nums[index];
    }
};

这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少。

本题则是装满有几种方法。其实这就是一个组合问题了。
求组合类问题的公式,都是类似这种:

dp[j] += dp[j - nums[i]]

初始化:dp[0] = 1 ,其他为0,为什么?(dp数组的含义)

474. 一和零

1、本题其实是01背包问题!

只不过这个背包有两个维度,一个是m 一个是n,而不同长度的字符串就是不同大小的待装物品。
2、dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。
3、递推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);

01背包的递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

对比一下就会发现,字符串的zeroNum和oneNum相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i])

完全背包

即每个物品可以取任意多次

518. 零钱兑换 II

本题和纯完全背包不一样,纯完全背包是凑成背包最大价值是多少,而本题是要求凑成总金额的物品组合个数!

377. 组合总和 Ⅳ

C++测试用例有两个数相加超过int的数据,所以需要在if里加上dp[i] < INT_MAX - dp[i - num[j]]。

57. 爬楼梯(进阶)

排列问题的完全背包

322. 零钱兑换

初始化:除了dp[0],其他都初始化为INT_MAX
当dp[j - nums[i]]是初始值时就跳过
如果最终还是初始值就返回-1
递推公式:dp[j] = min(dp[j], dp[j - i * i] + 1);
本题求钱币最小个数,那么钱币有顺序和没有顺序都可以,都不影响钱币的最小个数。
因此,外层for循环遍历物品,内层for遍历背包或者外层for遍历背包,内层for循环遍历物品都是可以的!

139. 单词拆分

回溯法:
会超时,需要使用记忆化递归
使用memory数组保存每次计算的以startIndex起始的计算结果,如果memory[startIndex]里已经被赋值了,直接用memory[startIndex]的结果。

class Solution {
private:
    bool backtracking (const string& s,
            const unordered_set<string>& wordSet,
            vector<bool>& memory,
            int startIndex) {
        if (startIndex >= s.size()) {
            return true;
        }
        // 如果memory[startIndex]不是初始值了,直接使用memory[startIndex]的结果
        if (!memory[startIndex]) return memory[startIndex];
        for (int i = startIndex; i < s.size(); i++) {
            string word = s.substr(startIndex, i - startIndex + 1);
            if (wordSet.find(word) != wordSet.end() && backtracking(s, wordSet, memory, i + 1)) {
                return true;
            }
        }
        memory[startIndex] = false; // 记录以startIndex开始的子串是不可以被拆分的
        return false;
    }
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
        vector<bool> memory(s.size(), 1); // 1 表示初始化状态
        return backtracking(s, wordSet, memory, 0);
    }
};

动态规划完全背包:
dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。
递推公式:if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true
初始化:dp[0] = true;
遍历顺序:排列问题,一定是 先遍历 背包,再遍历物品

56. 携带矿石资源

多重背包问题:第i种物品最多有Mi件可用
每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。但是这样处理会超时。
应该把每种商品遍历的个数放在01背包里面在遍历一遍。

198. 打家劫舍

当前状态和前面状态会有一种依赖关系,那么这种依赖关系都是动规的递推公式。
dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]。
递推公式:dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
初始化:
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
代码健壮性:
if (nums.size() == 0) return 0;
if (nums.size() == 1) return nums[0];

213. 打家劫舍 II

围成一圈了。就是首、尾元素不能同时取。
有两种情况:
考虑首元素、不考虑尾元素
考虑尾元素、不考虑首元素
把上一题的代码抽离出来。

337. 打家劫舍 III

变成二叉树了。就是如果抢了当前节点,两个孩子就不能动,如果没抢当前节点,就可以考虑抢左右孩子。
本题一定是要后序遍历,因为通过递归函数的返回值来做下一步计算。
暴力搜索法:
会超时,要使用记忆化递归,使用一个map把计算过的结果保存一下,这样如果计算过孙子了,那么计算孩子的时候可以复用孙子节点的结果。
动态规划法:
树形dp的难点:
dp数组如何定义?

121. 买卖股票的最佳时机

只能买一次,卖一次
贪心法:
左边取最小值,右边取最大值
动态规划法:
dp[i][0]与dp[i][1]

122. 买卖股票的最佳时机 II

可以买卖无限次
贪心法:
本题中理解利润拆分是关键点! 不要整块的去看,而是把整体利润拆为每天的利润。
一旦想到这里了,很自然就会想到贪心了,即:只收集每天的正利润,最后稳稳的就是最大利润了。
动态规划法:
和上一题只有在递推公式上的唯一区别。

123.买卖股票的最佳时机III

最多可以买卖两次
dp数组:
设置以下四个状态:
第一次持有股票
第一次不持有股票
第二次持有股票
第二次不持有股票

188.买卖股票的最佳时机IV

最多可以买卖k次

309.最佳买卖股票时机含冷冻期

可以买卖无限次,但有1天的冷冻期
状态划分:四个状态
状态一:持有股票状态(今天买入股票,或者是之前就买入了股票然后没有操作,一直持有)
不持有股票状态,这里就有两种卖出股票状态(因为本题有冷冻期,所以要分):
状态二:保持卖出股票的状态(两天前就卖出了股票,度过一天冷冻期。或者是前一天就是卖出股票状态,一直没操作)
状态三:今天卖出股票
状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!

714.买卖股票的最佳时机含手续费

可以买卖无限次

300.最长递增子序列

dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度
为什么一定表示 “以nums[i]结尾的最长递增子序” ?

674. 最长连续递增序列

要求连续
动态规划法:
和上一题的区别在于递推公式
贪心法:
遇到nums[i] > nums[i - 1]的情况,count就++,否则count为1,记录count的最大值就可以了。

718. 最长重复子数组

因为有两个数组,所以要用二维dp数组。
dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。 这样定义是为了方便初始化。
初始化:dp[i][0] 和dp[0][j]要初始值,因为 为了方便递归公式dp[i][j] = dp[i - 1][j - 1] + 1;
所以dp[i][0] 和dp[0][j]初始化为0。

1143.最长公共子序列

与上一题的区别在于:本题不要求连续。
dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
需要处理nums1[i] != nums2[j]的情况,并且不需要用res收集结果

1035. 不相交的线

就是求最长公共子序列

53. 最大子序和

贪心法:
使用result取区间累计的最大值
使用count重置最大子序起始位置,因为遇到负数一定是拉低总和
动态规划法:
dp[i]:包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]。

编辑距离问题:

392. 判断子序列

因为要保证顺序,所以不能使用哈希法。
dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]。

115.不同的子序列 (难)

上一题是是否出现,本题是求出现的个数。
因此对于dp数组的定义是不一样的。
就是模拟删除s中的元素,看有多少种删除方式可以使s变成t。
dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。
当s[i - 1] == t[i - 1]时,有使用s[i - 1](dp[i - 1][j - 1],因为s[i - 1] == t[i - 1],所以就不用考虑s[i - 1] 和 t[i - 1])和不使用s[i - 1](dp[i - 1][j])两种情况。

583. 两个字符串的删除操作

与上一题相比,就是两个字符串都可以删除了。
方法1:直接法
方法2:间接法,求出最长公共子序列,再用总长度去减

72. 编辑距离

插入、删除、替换
当word1[i - 1] != word2[j - 1]时,删除word1的,删除word2的,替换。(一个添加,相当于另一个删除)

回文子串与回文子序列

647. 回文子串

动态规划法:
如何定义dp数组?如果用直接法会发现很难找到递推关系。
布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串。
遍历顺序:一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的。
双指针法:
一个元素为中心点和两个元素为中心点

516.最长回文子序列

本题是回文子序列,可以是不连续的

纯01背包二维写法:
先遍历物品,还是先遍历背包,都可以。
逆序,还是顺序,都可以。

纯01背包一维写法:
先遍历物品,再遍历背包,并且背包逆序

纯完全背包一维写法:
先遍历物品,还是先遍历背包,都可以。
背包顺序。

多重背包:
即把每种商品遍历的个数放在01背包里面在遍历一遍

组合问题的公式:
01背包变种:
dp[j] += dp[j - nums[i]] 先遍历物品,再遍历背包,并且背包逆序
完全背包变种:
dp[j] += dp[j - nums[i]]
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
(如果求排列数就是外层for遍历背包,内层for循环遍历物品。)

  • 11
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值