• C++算法学习心得八.动态规划算法(2)


    1.不同路径 II(63题)

    题目描述:

    一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

    机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

    现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

    动态规划:dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。

    dp[i][j] = dp[i - 1][j] + dp[i][j - 1]。无障碍的递推公式

    for循环的终止条件,一旦遇到obstacleGrid[i][0] == 1的情况就停止dp[i][0]的赋值1的操作,dp[0][j]同理

    从递归公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1] 中可以看出,一定是从左到右一层一层遍历,这样保证推导dp[i][j]的时候,dp[i - 1][j] 和 dp[i][j - 1]一定是有数值。 

    1. class Solution {
    2. public:
    3. int uniquePathsWithObstacles(vectorint>>& obstacleGrid) {
    4. int m = obstacleGrid.size();//定义整个数组的列数
    5. int n = obstacleGrid[0].size();//行数
    6. if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) //如果在起点或终点出现了障碍,直接返回0
    7. return 0;
    8. vectorint>> dp(m, vector<int>(n, 0));//从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径
    9. for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1;//初始化 ,条件增多,遇到障碍,
    10. for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) dp[0][j] = 1;
    11. for (int i = 1; i < m; i++) {
    12. for (int j = 1; j < n; j++) {
    13. if (obstacleGrid[i][j] == 1) continue;//遇到障碍继续
    14. dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
    15. }
    16. }
    17. return dp[m - 1][n - 1];
    18. }
    19. };
    • 时间复杂度:O(n × m),n、m 分别为obstacleGrid 长度和宽度
    • 空间复杂度:O(n × m)

    2.整数拆分(343题)

    题目描述:

    给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。

    示例 1:

    • 输入: 2
    • 输出: 1
    • 解释: 2 = 1 + 1, 1 × 1 = 1。

     动态规划:dp[i]:分拆数字i,可以得到的最大乘积为dp[i]。

    其实可以从1遍历j,然后有两种渠道得到dp[i].

    一个是j * (i - j) 直接相乘。

    一个是j * dp[i - j]

    递推公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));

    只初始化dp[2] = 1

    确定遍历顺序,先来看看递归公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));

    dp[i] 是依靠 dp[i - j]的状态,所以遍历i一定是从前向后遍历,先有dp[i - j]再有dp[i]。

    注意 枚举j的时候,是从1开始的。从0开始的话,那么让拆分一个数拆个0,求最大乘积就没有意义了。因为拆分一个数n 使之乘积最大,那么一定是拆分成m个近似相同的子数相乘才是最大的

    1. class Solution {
    2. public:
    3. int integerBreak(int n) {
    4. vector<int>dp(n+1);//dp[i]代表含义是拆分i达到乘积最大值
    5. dp[2] = 1;//初始化2,1和0都没意义
    6. //这里需要从i=3开始遍历,可以遍历到n
    7. for(int i = 3;i <= n;i++){
    8. //可以遍历到i,
    9. for(int j = 1;j <= i / 2;j++){
    10. dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));//递推公式
    11. }
    12. }
    13. return dp[n];
    14. }
    15. };
    • 时间复杂度:O(n^2)
    • 空间复杂度:O(n)

     贪心算法:每次拆成n个3,如果剩下是4,则保留4,然后相乘

    1. class Solution {
    2. public:
    3. int integerBreak(int n) {
    4. if (n == 2) return 1;
    5. if (n == 3) return 2;
    6. if (n == 4) return 4;
    7. int result = 1;
    8. while (n > 4) {
    9. result *= 3;
    10. n -= 3;
    11. }
    12. result *= n;
    13. return result;
    14. }
    15. };
    • 时间复杂度:O(n)
    • 空间复杂度:O(1)

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

    题目描述:

    给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种?

    动态规划:

    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]

    dp[i] : 1到i为节点组成的二叉搜索树的个数为dp[i]

    dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量]

    j相当于是头结点的元素,从1遍历到i为止。

    所以递推公式:dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量

    初始化dp[0] = 1

    首先一定是遍历节点数,从递归公式:dp[i] += dp[j - 1] * dp[i - j]可以看出,节点数为i的状态是依靠 i之前节点数的状态。

    那么遍历i里面每一个数作为头结点的状态,用j来遍历。

    1. class Solution {
    2. public:
    3. int numTrees(int n) {
    4. vector<int>dp(n + 1);//定义dp数组,其含义是i有不同种二叉搜索树
    5. dp[0] = 1;//初始化
    6. //注意遍历顺序,根据递推公式来实现,从前向后遍历,注意i的起始位置,以及边界
    7. for(int i = 1;i <= n;i++){
    8. //注意,j的起始和边界位置取值
    9. for(int j = 1;j <= i;j++){
    10. dp[i] += dp[j - 1] * dp[i - j];//递推公式
    11. }
    12. }
    13. return dp[n];
    14. }
    15. };
    • 时间复杂度:O(n^2)
    • 空间复杂度:O(n)

    4.01背包理论基础 (卡玛网46题)

    题目描述:

    动态规划:背包问题:

    01背包和完全背包就够用了 ,完全背包又是也是01背包稍作变化而来,即:完全背包的物品数量是无限的,背包问题的理论基础重中之重是01背包

    01 背包

    有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

    每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是$o(2^n)$,这里的n表示物品数量。

    所以暴力的解法是指数级别的时间复杂度。

    二维dp数组01背包

    对于背包问题,有一种写法, 是使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少

    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得到的最大价值

    所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

     首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。

    状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。

    dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。

    那么很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。

    当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。

    其实从递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出dp[i][j] 是由左上方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖。 

    1. //二维dp数组实现
    2. #include
    3. using namespace std;
    4. int n, bagweight;// bagweight代表行李箱空间
    5. void solve() {
    6. vector<int> weight(n, 0); // 存储每件物品所占空间
    7. vector<int> value(n, 0); // 存储每件物品价值
    8. for(int i = 0; i < n; ++i) {
    9. cin >> weight[i];
    10. }
    11. for(int j = 0; j < n; ++j) {
    12. cin >> value[j];
    13. }
    14. // dp数组, dp[i][j]代表行李箱空间为j的情况下,从下标为[0, i]的物品里面任意取,能达到的最大价值
    15. vectorint>> dp(weight.size(), vector<int>(bagweight + 1, 0));
    16. // 初始化, 因为需要用到dp[i - 1]的值
    17. // j < weight[0]已在上方被初始化为0
    18. // j >= weight[0]的值就初始化为value[0]
    19. for (int j = weight[0]; j <= bagweight; j++) {
    20. dp[0][j] = value[0];
    21. }
    22. for(int i = 1; i < weight.size(); i++) { // 遍历科研物品
    23. for(int j = 0; j <= bagweight; j++) { // 遍历行李箱容量
    24. // 如果装不下这个物品,那么就继承dp[i - 1][j]的值
    25. if (j < weight[i]) dp[i][j] = dp[i - 1][j];
    26. // 如果能装下,就将值更新为 不装这个物品的最大值 和 装这个物品的最大值 中的 最大值
    27. // 装这个物品的最大值由容量为j - weight[i]的包任意放入序号为[0, i - 1]的最大值 + 该物品的价值构成
    28. else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
    29. }
    30. }
    31. cout << dp[weight.size() - 1][bagweight] << endl;
    32. }
    33. int main() {
    34. while(cin >> n >> bagweight) {
    35. solve();
    36. }
    37. return 0;
    38. }

    5.01背包理论基础(滚动数组)

    上述题的另一种一维数组的解法,

    对于背包问题其实状态都是可以压缩的。

    在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

    其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);

    与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。

    这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。

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

    在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。

    dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。

    dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j])

    此时dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值,

    dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。那么非0下标都初始化为0就可以了。

    倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!(背包倒序遍历)所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了

    对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖!

    如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。

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

    1. // 一维dp数组实现
    2. #include
    3. #include
    4. using namespace std;
    5. int main() {
    6. // 读取 M 和 N
    7. int M, N;
    8. cin >> M >> N;
    9. vector<int> costs(M);
    10. vector<int> values(M);
    11. for (int i = 0; i < M; i++) {
    12. cin >> costs[i];
    13. }
    14. for (int j = 0; j < M; j++) {
    15. cin >> values[j];
    16. }
    17. // 创建一个动态规划数组dp,初始值为0
    18. vector<int> dp(N + 1, 0);
    19. // 外层循环遍历每个类型的研究材料
    20. for (int i = 0; i < M; ++i) {
    21. // 内层循环从 N 空间逐渐减少到当前研究材料所占空间
    22. for (int j = N; j >= costs[i]; --j) {
    23. // 考虑当前研究材料选择和不选择的情况,选择最大值
    24. dp[j] = max(dp[j], dp[j - costs[i]] + values[i]);
    25. }
    26. }
    27. // 输出dp[N],即在给定 N 行李空间可以携带的研究材料最大价值
    28. cout << dp[N] << endl;
    29. return 0;
    30. }

    6.分割等和子集(416题)

    题目描述:

    给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

    注意: 每个数组中的元素不会超过 100 数组的大小不会超过 200

    示例 1:

    • 输入: [1, 5, 11, 5]
    • 输出: true
    • 解释: 数组可以分割成 [1, 5, 5] 和 [11].

    01背包问题 :背包问题有多种背包方式,常见的有:01背包、完全背包、多重背包、分组背包和混合背包,

    即一个商品如果可以重复多次放入是完全背包,而只能放入一次是01背包,写法还是不一样的。

    要明确本题中我们要使用的是01背包,因为元素我们只能用一次。

    回归主题:首先,本题要求集合里能否出现总和为 sum / 2 的子集

    dp数组以及下标的含义:dp[j]表示 背包总容量(所能装的总重量)是j,放进物品后,背的最大重量为dp[j]

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

    本题,相当于背包里放入数值,那么物品i的重量是nums[i],其价值也是nums[i]。

    所以递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);

    初始化:dp[0]一定是0,那么非0下标都初始化为0就可以了

    如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!

    1. class Solution {
    2. public:
    3. bool canPartition(vector<int>& nums) {
    4. int sum = 0;//定义总和
    5. vector<int>dp(10001,0);//定义dp数组,并且对其初始化,首先考虑0为0,其他非0位置初始化最小值0,
    6. //计算所有的值总和
    7. for(int i = 0;i < nums.size();i++){
    8. sum += nums[i];
    9. }
    10. //如果是奇数的话直接返回,采用的是一半取法,总和一半,剩下也肯定是一半相等
    11. if(sum % 2 == 1)return false;
    12. int target = sum / 2;
    13. //遍历顺序,一维不可以交换遍历顺序,首先背包,再物品,
    14. for(int i = 0;i < nums.size();i++){
    15. //遍历物品,从后向前是因为不可以有重复的,
    16. for(int j = target;j >= nums[i];j--){
    17. dp[j] = max(dp[j],dp[j-nums[i]]+nums[i]);//递推公式01背包推到出来的,一维的且根据dp定义nums[i]进行替换
    18. }
    19. }
    20. //最后进行判断
    21. if (dp[target] == target) return true;
    22. return false;
    23. }
    24. };
    • 时间复杂度:O(n^2)
    • 空间复杂度:O(n),虽然dp数组大小为一个常数,但是大常数

     总结:

    不同路径II:题中有障碍物描述,这里我们设置二维dp数组来进行定义,定义从0,0出发到达i,j有dp(i,j)种方法,需要知道当前状态如何推出下一个状态,需要上和左来推出,自然定义递推公式 dp[i][j] = dp[i - 1][j] + dp[i][j - 1],根据递推公式我们知道从上到下从左到右的遍历方式进行遍历,这里初始化需要注意了,我们第一行和第一列都需要初始化为1,因为到达的话只有一条路径,但是需要注意加入条件,其他情况都设置为0,在第一列或者第一行的路径上有障碍物,则需要设置为0,最后需要注意遍历的时候边界问题,还有最后返回值

    整数拆分:一个正整数拆分成两个整数让其乘积最大化,得到最大乘积,首先我们需要推出递归公式,因为这个整数拆分,因为求最大值,所以必须考虑之前 一个状态和现在拆分之后状态的对比取最大值,这里我们需要一个变量去存一个边界,这个边界就是相当于拆分的边界,一次拆分i*i-j,与拆分一个j还可以继续拆分dp[i - j]*j进行取最大值,然后这个在与dp[i]进行比较,根据递推公式我们得需要从前向后遍历,初始化,因为1,0不用初始化,dp[2] = 1即可,注意遍历边界,外层i从3到n,j从1到i/2,这里边界因为对称,最后返回dp[n]即可

    不同的二叉搜索树:给整数节点,求1...n有几个二叉树,首先定义dp数组含义,dp[I]在1到i节点组成的二叉收索树的个数,dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量,需要初始化dp[0] = 1,根据递推公式来得知,遍历顺序,从前到后遍历,这里注意两个变量,i,j的取值范围,i需要从1-n,j则从1 - i,最后返回dp[n]即可

    01背包理论基础:有固定重量的背包,有n件物品,且每个物品有其重量和价值,在不超重的情况下求能装下的最大价值,只能取一次物品,因为物品只有两种状态取或者不取,所以可以使用回溯法,但是时间复杂度为n^2,dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少,当前可以选择放物品和不放物品两个选择,递推公式,前面就是不放物品i的价值,和后面需要知道前一个重量,去除物品重量,再加上价值,这两个放与不放之间取最大值则是递推公式,dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);初始化当j >= weight[0]时,dp[0][j] 应该是value[0],遍历顺序因为需要i-1状态,所以需要从前向后遍历,注意双循环的边界问题,判断条件需要设定如果当前放不下这个重量,就没必要进行递推,最后返回dp[weight.size() - 1][bagweight]即可

    01背包理论基础(滚动数组):使用一维数组滚动数组其实也可以实现,把dp[i - 1]那一层拷贝到dp[i]上,dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]),满足的条件是上一层可以重复利用,直接拷贝到当前层,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],这里初始化,我们设置dp[0]=0即可,背包容量为0所背的物品的最大价值就是0,非0下标都初始化为0。倒序遍历是为了保证物品i只被放入一次,可以这么理解就是相当于复制了上一个数组下来,然后我们需要上一个数组和前一个值来确定,所以是从后向前遍历,外层物品,内层容量,这里仍然需要考虑边界上下限

    分割等和子集:给定一个数组,将数组分为两个子集,使得两个子集和相等,要求集合里能否出现总和为 sum / 2 的子集,其实很巧妙的将其看成01背包问题,sum/2就是背包的容量,dp[j]表示 背包总容量(所能装的总重量)是j,放进物品后,背的最大重量为dp[j]。递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);相当于背包里放入数值,那么物品i的重量是nums[i],其价值也是nums[i]。dp[0]一定是0,那么非0下标都初始化为0,使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历,外层背包的遍历边界0-nums.size(),内层物品遍历边界容量的大小-nums[i],

    分割等和子集确实很好题值得思考,多想想

  • 相关阅读:
    UNI-APP 框架中解决打包后index.html文件中没有引号问题
    数据结构介绍
    关于vba代码运行时错误1004 应用程序定义或对象定义错误问题
    Go语言高级编程:深度挖掘
    NIO教程
    特征工程2之 . 时序值衍生的特征
    Java读和写文件
    PE 结构
    从入门到一位合格的爬虫师,这几点很重要
    Python+Selenium定位不到元素常见原因及解决办法(报:NoSuchElementException)
  • 原文地址:https://blog.csdn.net/weixin_55605689/article/details/135942741