代码随想录算法训练营第36天 | LeetCode1049.最后一块石头的重量II、LeetCode494.目标和、LeetCode474.一和零

目录

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

LeetCode494.目标和  

LeetCode474.一和零


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

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

  • 如果 x == y,那么两块石头都会被完全粉碎;
  • 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x

最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0

思路:这里是给定背包容量,求尽可能将背包容量装满,看最多能装多少 。同样这里将stones[i]元素的大小作为元素所占空间(容量的大小)以及所拥有价值的大小。

之前其实讲过类似的一道题,LeetCode416.分割等和子集,在里面我分析过如何求解。

其实这里差不多,就是相当于将元素总和折半作为最大容量,要求的就是这个最大容量所能够装的尽可能多的值,因为题目中说当值相等时两者直接同时完全粉碎,也就是当这个最大容量的dp值就等于了总和的一半,所以比较时就直接粉碎了。

这里我没有使用单一两个元素进行比较,而是将它们看作一个整体,试图找到当对半这个容量装到最多的容量时,剩下的总和减去这个大小后,两者的差值能够达到最小的可能重量。

所以以整体角度来处理,相对来说更加方便,也能够很好使用动态规划。

    int lastStoneWeightII(vector<int>& stones) {
        int sum = 0;//统计总和
        for(int i = 0; i < stones.size(); i ++) sum += stones[i];
        int target = sum / 2;
        
        vector<int> dp(target + 1, 0);//dp[j]表示j容量所能拥有的最大价值
        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];
        //这里的dp[target]是小于等于target的,也小于等于sum/2
        //这里前半部分sum-dp[target]能保证是大于等于sum/2的
        //也就是说这是dp[target]能装的最大值相对于总值sum来说剩下的部分
        //两者相减即可得到石头的最小可能重量
    }

时间复杂度:O(m*n)

空间复杂度:O(m)

LeetCode494.目标和 

给你一个非负整数数组 nums 和一个整数 target

向数组中的每个整数前添加 '+''-' ,然后串联起所有整数,可以构造一个 表达式

  • 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1"

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

思路: 本题可以转换为在给定背包容量的情况下,找到将背包容量填充满的方案有多少。

那么如何转换呢?

首先我们假设将元素分为了两个子集,一个left,一个right,那么不管符号加在哪里,left+right=sum(sum为nums数组元素总和),这是始终成立的;现在我们假设在right上加上了负号,那么就有left-right=target,于是两个式子联立求解,有left=(sum+target)/2,sum可以求,是固定值,target也是固定值,题目中给定,所以相当于就将left的值给固定下来了,也就是left的值就为固定的背包容量了。

于是现在题目就转换成了背包容量固定,我们想要将其填满,因为填满就相当于有了一种方案(注意这里同样将nums[i]的大小当作了所占空间大小以及所拥有的价值),所以现在问题就明晰起来了。

我们创建dp[i][j]二维数组,表示在0到i个物品中,容量为j时将其装满的方案数,那么递归方程如何求呢,可以画个图,使用力扣中的测试用例,填充dp数组,可以比如nums[1,1,1,1,1],target=3,以求dp[2][2]为例,当容量为2的时候,如果没有装第2个物品,那么它的方案数就等于从0到1的物品,容量为2是的方案数,即dp[1][2]的方案数;当容量为2,装了第2个物品,那么就等于0到1的物品,容量为j-nums[2]时的方案数,因为这时空出来放入nums[2]大小的空间。能够将第2个物品放入。

于是乎,dp[2][2]=dp[1][2]+dp[1][1],所以抽象出来,递推公式为dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i]],但是要注意这里是在j>=nums[i]时才能这样更新,如果是小于的话,那么就直接dp[i][j]=dp[i-1][j]。

初始赋值的话对于dp[0][j]的元素,只有当j=nums[0]时,赋值为1,因为其他大小要么装不满,要么装不下,所以赋值为1即可。对于dp[i][0]的元素,也就是容量为0,一般来说就是不放物品,这也算一种方案数,所以统一赋值为1,但是因为题目中说nums[i]>=0,也就是可能为0,所以需要记录下0的个数,相应的元素赋值也成了求这些0元素的子集数量,所以从这个角度看,不放任何物品时,相当于就是一个空集,这样也合理。

    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;//求总和
        for(int i = 0; i < nums.size(); i ++) sum += nums[i];
        //这里的(sum+target)/2是这样推导而来的,首先假设nums中的元素分为两个子集
        //left为左边子集的和,right为右边子集的和,那么可以知道left+right=sum,并且可以有left-right=target
        //那么两式合并,即可得到左边子集left=(sum+target)/2
        if((sum + target) % 2 == 1) return 0;//这里sum+target为奇数,无法找到和为其一半的子集和,直接返回0
        if(abs(target) > sum) return 0;

        int N = (sum + target) / 2;
        vector<vector<int>> dp(nums.size(), vector<int>(N + 1, 0));//dp[i][j]表示0到i个物品中,将j个空间容量填满的方案数目
        if(N >= nums[0]) dp[0][nums[0]] = 1;//初始化,能够将第一个物品填满的只能是容量为nums[0]空间大下的容量,其他的要么填不满,要么就是填不下
        int count_zero = 0;
        for(int i = 0; i < nums.size(); i ++){
            if(nums[i] == 0) count_zero ++;
            dp[i][0] = (int)pow(2, count_zero);//这里其实就是在对最左边的一列进行初始化
            //因为背包容量为0,所以方案数初始化均可都为1,因为不放任何物品也是一种方法,
            //但是因为nums[i]可以为0,所以需要统计0元素的数量,而这个位置的初始化就有了所有0元素在一起组成集合的子集数量
            //子集数量有多少,那么就代表这里能够有多少种放入的情况,所以这里的不放任何物品可以看成是子集中的空集,这样就便于理解了
        }

        for(int i = 1; i < nums.size(); i ++){
            for(int j = 0; j < N + 1; j ++){
                //这里依然将nums[i]的值作为物品所占容量大小以及价值大小
                if(j < nums[i]) dp[i][j] = dp[i - 1][j];//当容量不够时,就等于不加i这个物品时,从0到i-1物品容量为j时的方案数
                else dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];//容量足够时就需要将前面0到i-1物品,容量为时的方案数加上0到i个物品并且留出i物品空间时的方案数
            }
        }
        return dp[nums.size() - 1][N];
    }

时间复杂度:O(m*n)

空间复杂度:O(m*n)

当然也可以使用一维滚动数组来优化一下空间复杂度,注意这里的两层for循环中的遍历顺序,不能弄反,否则会出错。

    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;//求总和
        for(int i = 0; i < nums.size(); i ++) sum += nums[i];
        if((sum + target) % 2 == 1) return 0;
        if(abs(target) > sum) return 0;

        int N = (sum + target) / 2;
        vector<int> dp(N + 1, 0);//dp[j]表示将j个空间容量填满的方案数目
        dp[0] = 1;//初始化,容量为0时的方案就是不放任何物品,方案数为1

        for(int i = 0; i < nums.size(); i ++){
            for(int j = N; j >= nums[i]; j --){
                dp[j] = dp[j] + dp[j - nums[i]];
                //可以简写成dp[j] += dp[j - nums[i]]
            }
        }
        return dp[N];
    }

时间复杂度:O(m*n)

空间复杂度:O(m)

LeetCode474.一和零

给你一个二进制字符串数组 strs 和两个整数 mn

请你找出并返回 strs 的最大子集的长度,该子集中 最多m0n1

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y子集

思路:这里可以转化为给定背包容量,然后求最多能够装入多少的物品数量。

但是这里的背包容量是二维的,所以有dp[i][j]表示最多为i个0,j个1的条件下,最大的子集数量。

我们这里是采用的背包的一维滚动数组的形式(只不过背包容量为二维,所以需要维护二维的数组),那么如何更新数组呢,dp[i][j]可以使用前面的已经遍历过的字符串的结果进行更新,也就是说,dp[i][j]=dp[i-count_zero][j-count_one]+1,这里是说dp[i][j]的数量就等于目前i个0减去当前字符串的0的个数、目前j个1减去当前字符串的1的个数的情况下的最大子集数量,加上把当前这个字符串加入其中的数量,所以这里有个加1。

当然了,dp[i][j]如果本身比dp[i-count_zero][j-count_one]+1大,那就取dp[i][j],所以递推公式就是dp[i][j]=max(dp[i][j],dp[i-count_zero][j-count_one]+1)。

初始赋值直接赋值为0即可,不会出现初始值将已有价值覆盖的情况。

同时这里还是需要强调大的两层for循环的顺序(实际本题有三层循环,第二层和第三层循环可以交换位置,相当于一个整体,进行背包容量的遍历),首先是遍历物品,然后是遍历容量,同时在遍历容量的时候一定是倒序的。

    int findMaxForm(vector<string>& strs, int m, int n) {
        //这里需要记录两个维度的背包容量,一个是装0字符的,一个是装1字符的
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));//dp[i][j]表示最多i个0、j个1的条件下最大的子集个数
        for(int i = 0; i < strs.size(); i ++){
            int count_zero = 0;//记录元素中0字符的数量
            int count_one = 0;//记录元素中1字符的数量
            for(char ch: strs[i]){
                if(ch == '0') count_zero ++;
                else count_one ++;
            }
            for(int j = m; j >= count_zero; j --){
                for(int k = n; k >= count_one; k --){
                    dp[j][k] = max(dp[j][k], dp[j - count_zero][k - count_one] + 1);//这里的加1代表满足条件的子集个数加1
                }
            }
        }
        return dp[m][n];
    }

时间复杂度:O(k*m*n)

空间复杂度:O(m*n)

感谢你的阅读,希望我的文章能够给你帮助,如果有帮助,麻烦点赞加收藏,或者点点关注,非常感谢。

如果有什么问题欢迎评论区讨论!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值