【算法练习Day36】最后一块石头的重量 II&&目标和&&一和零

在这里插入图片描述

​个人主页:@Sherry的成长之路
学习社区:Sherry的成长之路(个人社区)
专栏链接:练题
长路漫漫浩浩,万事皆有期待

文章目录

  • 最后一块石头的重量 II
  • 目标和
  • 一和零
  • 总结:

最后一块石头的重量 II

1049. 最后一块石头的重量 II - 力扣(LeetCode)
【算法练习Day36】最后一块石头的重量 II&&目标和&&一和零_第1张图片

最后一块石头的重量II,这道题是将各个不同重量的石头相互碰撞,碰撞规则是如果两石头重量一样,那么两块石头均会撞碎,不会有剩余石头。如果其中一颗石头重量较大,那么会剩下较大重量石头减去较轻石头的重量,也就是说剩下一个新重量的石头,题目要求我们返回尽量小的剩余石头重量。

那么如何尽量使剩余的石头的重量最小呢?我们要做的就是将石头重量大致的平分成两堆,让两堆石头相撞,如果两堆石头总重量相等,那么剩余重量为0,如果不等,那么剩余的重量仍然为最小。这就是我们解题的一个思路。

动规五部曲的分析:

dp数组的含义:当前能承载的重量下能够存储的石头最大重量,我们假设将这一堆石头重量平分为两堆,所以最大的重量容量也就是总石头重量的一半。

递推公式:递推公式也就是借用01背包的模型,将该块石头放入背包或者不放入背包共两种不同的状态。和01背包的递推公式完全一样,这里的重量和价值均为石头的重量。

dp数组初始化:dp【0】初始化为0,因为能装总重量为0的背包,装不进石头。剩下的不为0的背包初始化为什么呢?根据之前的01背包的一维数组的初始化可知,依然初始化为0,因为我们要比较dp【j】的本身,如果初始化过大,就会导致本来应该赋值的量并不能赋值上。不懂得可以去看01背包的详解。

遍历顺序:遍历顺序就是正常的从前遍历石头,先遍历那一颗石头并不影响结果,背包是从后向前遍历的,不懂依然是往前翻01背包的一维数组解决方法,大多数例题我们都用一维数组解决。

打印:在未得到理想的答案时候,我们要使用打印dp数组的方法来辅助分析。

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        vector<int>dp(1501,0);
        dp[0]=0;int sum=0;
        for(auto i:stones)sum+=i;
        int target=sum/2;
        for(int i=0;i<stones.size();i++){
            for(int j=target;j>=stones[i];j--)
            dp[j]=max(dp[j],dp[j-stones[i]]+stones[i]);
        }
        return sum-dp[target]-dp[target];
    }
};

和我们上面的分析相同的思路,也就是说如果五部曲能完全懂,那么代码也十分容易,数组我们可以给定1501的大小,这是根据题意所得到的,+1是为了避免数组越界。

我们使用的方法是把全部重量的石头分为两半,最大的背包就是其总和一半,最后我们返回的是两堆石头碰撞剩余的部分,总和减去一半再减一半,有人会问了为什么要这样写呢?因为如果石头总重量不能均分两半,那么一定是会有非0的剩余,由于总和/2是向下取整所以一定是取得较小的数,我们这样做减法得到的就是剩余的新石头的重量。

目标和

494. 目标和 - 力扣(LeetCode)

【算法练习Day36】最后一块石头的重量 II&&目标和&&一和零_第2张图片

目标和这道题我一看就丝毫没有思路,它有一点像是用回溯算法解决的题目,给定数组的每一个数都有取正和取负两种,然后最后算出共可以有多少次不同的方法凑得目标值。

但事实上是有可以用上01背包的思路的!思路有些不好想,具体思路是将要变成正数的部分和要变成负数的部分分开,正数部分加上那些应该是负数的部分,就可以得到目标值了。那么关键的来了,如何知道我们的左边正数部分应该取多少呢?它为多少时候我们才能加上右边部分凑成target呢?有以下公式推出,left-right=target这也就是说左边数字和右边数字相加就是目标值,为什么是减去呢?因为我们只是将右边看成是负数,其实并不是负数,我们将数组中正数逐个变成负数再放进负数堆里是很麻烦的,不是明智之举,所有我们直接减掉右边的正数就可以达成目的了。而left+right=sum也就是数组本身的数字逐个相加起来,那肯定就是总和sum了。这就可以推出right=sum-left,将该等式回带第一个式子得left=(sum+target)/2。这是个重要的式子,它帮助我们确定左边部分到底放多大,也就是背包最大容量是多大!sum可以求target是题目给的。

dp数组的含义:填满容量为j的背包共有多少种填充方法。

递推公式:递推公式的分析略显复杂,若j=5的情况下,给定数组{1,2,3,4,5},那么我们如果已经加入一个1,则需要dp【4】种方法凑成容量为5的背包,如果已经加入的数字是2,那么则需要dp【3】种方法才能够凑成容量为5的背包,以此类推如果加入的是数字5那么凑成容量为5背包需要dp【0】种,那么我们要求构成dp【5】共多少方法,也就是容量为5共多少种方法,那一定是前面的几种加在一起的和,则为构建的dp【5】的方法。就是dp【j-nums【i】】

而由于是累加在一起故递推公式是dp【j】+=dp【j-nums【i】】。

dp数组初始化:j为0时候初始化为1,这一点是和其他01背包题目初始化不同的地方,我们可以理解为目标值为0则有一种方法,不放入数组元素。事实上经过调试我们也可以知道,如果第一个位置初始化为0,那么累加的一直就是0了,无论target为多少,都无法出来其他数字了,所以一定要初始化为1。而其他j不等于0时候给它们初始化为0,这样方便第一次累加,实际上这和遍历顺序也有关联。

遍历顺序:遍历顺序是从左向右遍历物品,从后向前遍历背包,不懂看之前的文章。

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum=0;
        for(auto i:nums)sum+=i;
        if((target+sum)%2==1)return 0;
        int left=(target+sum)/2;
        if(abs(target)>sum)return 0;
        vector<int>dp(10001,0);dp[0]=1;
        for(int i=0;i<nums.size();i++){
            for(int j=left;j>=nums[i];j--){
                dp[j]+=dp[j-nums[i]];
            }
        }
        return dp[left];
    }
};

最后我们将dp【left】返回即可知道,我们为了凑成target共能有多少种方法。

一和零

474. 一和零 - 力扣(LeetCode)
【算法练习Day36】最后一块石头的重量 II&&目标和&&一和零_第3张图片

这道题又是01背包的一种新的应用题型。这道题乍一看也不像是能用到01背包特性的。这道题有两个维度m和n控制着结果的产生,这意味着我们必须使用二维数组来表示,这和其他题目用一维数组是一样的,其他题目是用一维数组代替二维数组,这道题实际上使用二维数组代替三维数组。

dp数组及其含义:dp【i】【j】i个0j个1的最大子集大小为多少,该背包的最大容量是dp【m】【n】。

递推公式:递推公式是和一维数组的递推公式很类似,dp【i】【j】=max(dp【i】【j】,dp【i-x】【j-y】+1)。其中的x和y分别代表遍历当前物品时候,物品所含有的0数量和1数量。当当前背包的i和j对应的小于了x或者y中的任何一个,则跳出。

dp数组的初始化:初始化全都是0,第一个位置初始化,0个0,0个1装入不了任何的子集。

遍历顺序:仍然是物品从左到右依次遍历,这里我们是一个一个物品取,并且再嵌套一个循环来分解该物品有多少个0和多少个1。分解完毕了,我们走遍历背包,遍历背包依然是从后向前,并且这道题的二维背包,无论是先遍历0还是先遍历1效果都是一样的,没有先后之分。

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector<vector<int>>dp(m+1,vector<int>(n+1,0));
        for(string str:strs){
            int x=0,y=0;
            for(char ch:str){
                if(ch=='0')x++;
                else y++;}
                for(int i=m;i>=x;i--){
                    for(int j=n;j>=y;j--)
                    dp[i][j]=max(dp[i][j],dp[i-x][j-y]+1);
                }
            }  
            return dp[m][n];                                                     
        }
};

相当于持续的更新dp【m】【n】,遍历不同的物品,在最大背包的容量之内,我们尽可能地装入更多的物品,也就是拥有给定数组尽可能多的数据(子集)。这里还要对递推公式做进一步的解释,就是为什么我们dp【i-x】【j-y】之后还要有一个+1的操作?我们初始化本来都是0,这里递推公式就是比较当前容量最大应该是当前所包含子集多,还是倒退一次再装入的多,每次进行第二个运算都要+1,意义是加入一个子集,所以我们要使自己数量+1,这和dp数组的定义也是密切相关的,数组就表示了当前状态下的最大子集数量。

我们来做一个对于01背包这些题型的总结:
我们之前做的最开始01背包,是在当前的背包最大容量内能够携带的最大价值是多少。

分割等和子集这道题是给定背包容量,看能不能装满背包,如果不能装满说明不能分割成等和子集。

最后一块石头的重量II是给定背包容量,尽可能装,看能装多少,最后一做差,其实和分割等和子集有一点相像。

目标和是给定背包容量看有多少种装满的方法。

一和零是一个二维背包,给定背包容量看最多能装多少不同的物品。

这些都是01背包的不同维度上的解决问题的典例。其中以后两道题是有一些难度的。

总结:

这一期三道题目是对于01背包不同层次的应用,各有特色,也各有难点。同时也是01背包的最后一期,下一期我们来学习完全背包。希望我的文章和讲解能对大家的学习提供一些帮助。

当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~

在这里插入图片描述

你可能感兴趣的:(练题,算法)