代码随想录算法训练营第46期Day37,38,39,41

这几天晚上看比赛,就把刷题耽误了。还好是开新章节,前面的题都比较简单。

然后周天做完了又忘记发了

动态规划

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数

Day37前两道题太简单了,我们从第三道题开始

leetcode.746.使用最小花费爬楼梯

cpp 复制代码
class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        vector<int> dp(cost.size()+8);//dp数组存储当前点位的最优解
        dp[0]=0;
        dp[1]=0;
        for(int i=2;i<=cost.size();i++){
            dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
            //这是dp公式,到达第i阶的最优解肯定是i-1阶的最优解+从i-1的花费和i-2阶的最有解+从i-2阶段的花费中小的那一个
        }
        return dp[cost.size()];
    }

};

leetcode.62.不同路径

cpp 复制代码
class Solution {
public:
    int uniquePaths(int m, int n) {
        int dp[105][105];
        dp[0][0]=0;
        for(int i=1;i<=m;i++){
            dp[i][1]=1;
        }
        for(int i=1;i<=n;i++){
            dp[1][i]=1;
        }
        for(int i=2;i<=m;i++){
            for(int j=2;j<=n;j++){
                dp[i][j]=dp[i][j-1]+dp[i-1][j];
            }
        }
        return dp[m][n];
    }
};

leetcode.63.不同路径Ⅱ

cpp 复制代码
class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();
 int n = obstacleGrid[0].size();
        vector<vector<int>> dp(m, vector<int>(n, 0));//注意最好用vector容器。都初始化为0
     /*if(obstacleGrid[0][0]==1||obstacleGrid[m-1][n-1]==1){//如果开始或终点有障碍那直接返回障碍
        return 0;
     }*/
        for(int i=0;i<m;i++){
             if(obstacleGrid[i][0]==1){//如果在边界的两条线上有障碍,那从障碍开始一下路径数都为0
               break;
            }
            dp[i][0]=1;
           
        }
        for(int i=0;i<n;i++){ 
            if(obstacleGrid[0][i]==1){
               break;
            }
            dp[0][i]=1;
            
        }


        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
            if(obstacleGrid[i][j]==0)dp[i][j]=dp[i][j-1]+dp[i-1][j];//如果碰不到障碍就进行dp,保证障碍处路径为0
            }
        }
        return dp[m-1][n-1];
    }
};

leetcode.416.分割等和子集

cpp 复制代码
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        sort(nums.begin(),nums.end());
        int sum=0;
        for(int i=0;i<nums.size();i++){
            sum+=nums[i];
        }
        vector<int> dp(11111,0);//这里数组尽量开大点,不然过不了
         if (sum % 2 == 1) return false;
         sum= sum / 2;//能够分成两个子集的元素和相等说明,有一部分元素相加的和是总和的一半
         //那么元素个数是种类,sum/2是容量的01背包问题就出现了
         for(int i=0;i<nums.size();i++){
            for(int j=sum;j>=nums[i];j--){
                dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
            }
         }
         if(dp[sum]==sum){
            return true;
         }
         return false;
    }
};

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

cpp 复制代码
class Solution {
public:
//和分割等和子列类似
    int lastStoneWeightII(vector<int>& stones) {
        vector<int> dp(10010,0);
        int sum=0;
        for(int i=0;i<stones.size();i++){
            sum+=stones[i];
        }
        int sum1=sum;
        sum=sum/2;
        for(int i=0;i<stones.size();i++){
            for(int j=sum;j>=stones[i];j--){
                dp[j]=max(dp[j],dp[j-stones[i]]+stones[i]);
            }
        }
        return sum1-2*dp[sum];




    }
};

leetcode.494.目标和

假设加法的总和为x,那么减法对应的总和就是sum - x。

所以我们要求的是 x - (sum - x) = target

x = (target + sum) / 2

此时问题就转化为,用nums装满容量为x的背包,有几种方法

  • 不放物品i:即背包容量为j,里面不放物品i,装满有dp[i - 1][j]中方法。

  • 放物品i: 即:先空出物品i的容量,背包容量为(j - 物品i容量),放满背包有 dp[i - 1][j - 物品i容量] 种方法。

  • if (nums[i] > j) dp[i][j] = dp[i - 1][j]; //说明背包容量装不下 物品i,所以此时装满背包的方法值 等于 不放物品i的装满背包的方法,

  1. 举例推导dp数组

输入:nums: [1, 1, 1, 1, 1], target: 3

bagSize = (target + sum) / 2 = (3 + 5) / 2 = 4

dp数组状态变化如下

那么最上行dp[0][j] 如何初始化呢?

dp[0][j]:只放物品0, 把容量为j的背包填满有几种方法。

只有背包容量为 物品0 的容量的时候,方法为1,正好装满。

其他情况下,要不是装不满,要不是装不下。

所以初始化:dp[0][nums[0]] = 1 ,其他均为0 。

表格最左列也要初始化,dp[i][0] : 背包容量为0, 放物品0 到 物品i,装满有几种方法。

都是有一种方法,就是放0件物品。

即 dp[i][0] = 1

如果有两个物品,物品0为0, 物品1为0,装满背包容量为0的方法有几种。

  • 放0件物品
  • 放物品0
  • 放物品1
  • 放物品0 和 物品1

此时是有4种方法。

其实就是算数组里有t个0,然后按照组合数量求,即 2^t

遍历顺序

在明确递推方向时,我们知道 当前值 是由上方和左上方推出。

那么我们的遍历顺序一定是 从上到下,从左到右。

因为只有这样,我们才能基于之前的数值做推导。

 for (int i = 0; i < nums.size(); i++) {
            if (nums[i] == 0) numZero++;
            dp[i][0] = (int) pow(2.0, numZero);
        }
cpp 复制代码
class Solution {
public:
int count=0;
   
    int findTargetSumWays(vector<int>& nums, int target) {
     int sum=0;
     for(int i=0;i<nums.size();i++){
        sum+=nums[i];  
             }
    if (abs(target) > sum) return 0; 
    
     sum+=target;
     if(sum%2==1){
        return 0;
     }       
     sum=sum/2;
     int bagSize=sum;
   vector<vector<int>> dp(nums.size(), vector<int>(10010, 0));
        
        // 初始化最上行
        if (nums[0] <= bagSize) dp[0][nums[0]] = 1; 

        // 初始化最左列,最左列其他数值在递推公式中就完成了赋值
        dp[0][0] = 1; 

        int numZero = 0;
        for (int i = 0; i < nums.size(); i++) {
            if (nums[i] == 0) numZero++;
            dp[i][0] = (int) pow(2.0, numZero);
        }

        // 以下遍历顺序行列可以颠倒
        for (int i = 1; i < nums.size(); i++) { // 行,遍历物品
            for (int j = 0; j <= bagSize; j++) { // 列,遍历背包
                if (nums[i] > j) dp[i][j] = dp[i - 1][j]; 
                else dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];
            }
        }
        return dp[nums.size() - 1][bagSize];
    }
};

一维数组版

二维DP数组递推公式: dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];

去掉维度i 之后,递推公式:dp[j] = dp[j] + dp[j - nums[i]] ,即:dp[j] += dp[j - nums[i]]

遍历物品放在外循环,遍历背包在内循环,且内循环倒序(为了保证物品只使用一次)。

cpp 复制代码
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for (int i = 0; i < nums.size(); i++) sum += nums[i];
        if (abs(target) > sum) return 0; // 此时没有方案
        if ((target + sum) % 2 == 1) return 0; // 此时没有方案
        int bagSize = (target + sum) / 2;
        vector<int> dp(bagSize + 1, 0);
        dp[0] = 1;
        for (int i = 0; i < nums.size(); i++) {
            for (int j = bagSize; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[bagSize];
    }
};

深搜遍历做法

cpp 复制代码
class Solution {
public:
int count=0;
    void backtrack(vector<int>& nums,int target,int index,int sum){
        if(index==nums.size()){
            if(sum==target){
                count++;
            }
        }else{
            backtrack(nums,target,index+1,sum+nums[index]);
            backtrack(nums,target,index+1,sum-nums[index]);
        }
        
    }
    int findTargetSumWays(vector<int>& nums, int target) {
      backtrack(nums,target,0,0);
      return count;  
    }
};

leetcode.474.一零和..

本题中strs 数组里的元素就是物品,每个物品都是一个!

而m 和 n相当于是一个背包,两个维度的背包

理解成多重背包的同学主要是把m和n混淆为物品了,感觉这是不同数量的物品,所以以为是多重背包。

但本题其实是01背包问题!

只不过这个背包有两个维度,一个是m 一个是n,而不同长度的字符串就是不同大小的待装物品。

开始动规五部曲:

  1. 确定dp数组(dp table)以及下标的含义

dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]

  1. 确定递推公式

dp[i][j] 可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum个0,oneNum个1。

dp[i][j] 就可以是 dp[i - zeroNum][j - oneNum] + 1。

然后我们在遍历的过程中,取dp[i][j]的最大值。

所以递推公式: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])。

这就是一个典型的01背包! 只不过物品的重量有了两个维度而已。

  1. dp数组如何初始化

01背包的dp数组初始化为0就可以。

因为物品价值不会是负数,初始为0,保证递推的时候dp[i][j]不会被初始值覆盖。

  1. 确定遍历顺序

01背包一定是外层for循环遍历物品,内层for循环遍历背包容量

那么本题也是,物品就是strs里的字符串,背包容量就是题目描述中的m和n。(相当于之前的一维dp,采用了滚动数组)

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

那么问题又来了,为什么二维dp数组遍历的时候不用倒序呢?

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

cpp 复制代码
class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector<vector<int>> dp(m + 1, vector<int> (n + 1, 0)); // 默认初始化0
        for (string str : strs) { // 遍历物品
            int oneNum = 0, zeroNum = 0;
            for (char c : str) {
                if (c == '0') zeroNum++;
                else oneNum++;
            }
            for (int i = m; i >= zeroNum; i--) { // 遍历背包容量且从后向前遍历!
                for (int j = n; j >= oneNum; j--) {
                    dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
                }
            }
        }
        return dp[m][n];
    }
};
相关推荐
WangMing_X14 分钟前
C# 单个函数实现各进制数间转换
java·开发语言·算法·c#·winform·软件
南宫生28 分钟前
贪心算法理论基础和习题【算法学习day.17】
java·学习·算法·leetcode·链表·贪心算法
海绵波波10728 分钟前
240. 搜索二维矩阵 II
数据结构·算法·矩阵
Death20035 分钟前
PCL库中的算法封装详解
c++·qt·算法·3d
weixin_3077791342 分钟前
复变函数解题技巧
算法
Qhumaing1 小时前
小白C/C++语言常见问题(持续更新)
c语言·c++·算法
嘻嘻啰啰1 小时前
代码随想录第十五天
c语言·数据结构·算法
懒惰才能让科技进步1 小时前
从零学习大模型(十一)-----Lottery Ticket Hypothesis剪枝
人工智能·深度学习·学习·算法·chatgpt·transformer·剪枝
自信人间三百年1 小时前
数据结构和算法-动态规划(3)-经典问题
java·数据结构·算法·leetcode·动态规划
啦啦啦!1 小时前
2024年10月30日(双指针算法)
c++·算法·leetcode