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]一定是有数值。 

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m = obstacleGrid.size();//定义整个数组的列数
        int n = obstacleGrid[0].size();//行数
	if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) //如果在起点或终点出现了障碍,直接返回0
            return 0;
        vector<vector<int>> dp(m, vector<int>(n, 0));//从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径
        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;
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (obstacleGrid[i][j] == 1) continue;//遇到障碍继续
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
        return dp[m - 1][n - 1];
    }
};
  • 时间复杂度: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个近似相同的子数相乘才是最大的

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

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

class Solution {
public:
    int integerBreak(int n) {
        if (n == 2) return 1;
        if (n == 3) return 2;
        if (n == 4) return 4;
        int result = 1;
        while (n > 4) {
            result *= 3;
            n -= 3;
        }
        result *= n;
        return result;
    }
};
  • 时间复杂度: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来遍历。

class Solution {
public:
    int numTrees(int n) {
        vector<int>dp(n + 1);//定义dp数组,其含义是i有不同种二叉搜索树
        dp[0] = 1;//初始化
        //注意遍历顺序,根据递推公式来实现,从前向后遍历,注意i的起始位置,以及边界
        for(int i = 1;i <= n;i++){
            //注意,j的起始和边界位置取值
            for(int j = 1;j <= i;j++){
                dp[i] += dp[j - 1] * dp[i - j];//递推公式
            }
        }
        return dp[n];
    }
};
  • 时间复杂度: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] 是由左上方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖。 


//二维dp数组实现
#include <bits/stdc++.h>
using namespace std;

int n, bagweight;// bagweight代表行李箱空间
void solve() {
    vector<int> weight(n, 0); // 存储每件物品所占空间
    vector<int> value(n, 0);  // 存储每件物品价值
    for(int i = 0; i < n; ++i) {
        cin >> weight[i];
    }
    for(int j = 0; j < n; ++j) {
        cin >> value[j];
    }
    // dp数组, dp[i][j]代表行李箱空间为j的情况下,从下标为[0, i]的物品里面任意取,能达到的最大价值
    vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));

    // 初始化, 因为需要用到dp[i - 1]的值
    // j < weight[0]已在上方被初始化为0
    // j >= weight[0]的值就初始化为value[0]
    for (int j = weight[0]; j <= bagweight; j++) {
        dp[0][j] = value[0];
    }

    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]);
        }
    }
    cout << dp[weight.size() - 1][bagweight] << endl;
}

int main() {
    while(cin >> n >> bagweight) {
        solve();
    }
    return 0;
}

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]就只会放入一个物品,即:背包里只放入了一个物品。

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

// 一维dp数组实现
#include <iostream>
#include <vector>
using namespace std;

int main() {
    // 读取 M 和 N
    int M, N;
    cin >> M >> N;

    vector<int> costs(M);
    vector<int> values(M);

    for (int i = 0; i < M; i++) {
        cin >> costs[i];
    }
    for (int j = 0; j < M; j++) {
        cin >> values[j];
    }

    // 创建一个动态规划数组dp,初始值为0
    vector<int> dp(N + 1, 0);

    // 外层循环遍历每个类型的研究材料
    for (int i = 0; i < M; ++i) {
        // 内层循环从 N 空间逐渐减少到当前研究材料所占空间
        for (int j = N; j >= costs[i]; --j) {
            // 考虑当前研究材料选择和不选择的情况,选择最大值
            dp[j] = max(dp[j], dp[j - costs[i]] + values[i]);
        }
    }

    // 输出dp[N],即在给定 N 行李空间可以携带的研究材料最大价值
    cout << dp[N] << endl;

    return 0;
}

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循环倒序遍历!

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;//定义总和
        vector<int>dp(10001,0);//定义dp数组,并且对其初始化,首先考虑0为0,其他非0位置初始化最小值0,
        //计算所有的值总和
        for(int i = 0;i < nums.size();i++){
            sum += nums[i];
        }
        //如果是奇数的话直接返回,采用的是一半取法,总和一半,剩下也肯定是一半相等
        if(sum % 2 == 1)return false;
        int target = sum / 2;
        //遍历顺序,一维不可以交换遍历顺序,首先背包,再物品,
        for(int i = 0;i < nums.size();i++){
            //遍历物品,从后向前是因为不可以有重复的,
            for(int j = target;j >= nums[i];j--){
                dp[j] = max(dp[j],dp[j-nums[i]]+nums[i]);//递推公式01背包推到出来的,一维的且根据dp定义nums[i]进行替换
            }
        }
        //最后进行判断
        if (dp[target] == target) return true;
        return false;
    }
};
  • 时间复杂度: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],

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

  • 21
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值