首页 > 其他分享 >代码随想录训练第三十一天|LeetCode1049.最后一块石头的重量II、LeetCode494.目标和、LeetCode474.一和零

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

时间:2024-07-31 18:56:27浏览次数:17  
标签:stones target nums int sum 随想录 II LeetCode474 dp

文章目录

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

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

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

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

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

示例 1:

输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

示例 2:

输入:stones = [31,26,33,21,40]
输出:5

提示:

  • 1 <= stones.length <= 30
  • 1 <= stones[i] <= 100

Related Topics

  • 数组

  • 动态规划

思路

本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了

是不是感觉和昨天讲解的416. 分割等和子集 (opens new window)非常像了。

本题物品的重量为stones[i],物品的价值也为stones[i]。

对应着01背包里的物品重量weight[i]和 物品价值value[i]。

接下来进行动规五步曲:

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

dp[j]表示容量(这里说容量更形象,其实就是重量)为j的背包,最多可以背最大重量为dp[j]

可以回忆一下01背包中,dp[j]的含义,容量为j的背包,最多可以装的价值为 dp[j]。

相对于 01背包,本题中,石头的重量是 stones[i],石头的价值也是 stones[i] ,可以 “最多可以装的价值为 dp[j]” == “最多可以背的重量为dp[j]”

  1. 确定递推公式

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

本题则是:dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);

一些同学可能看到这dp[j - stones[i]] + stones[i]中 又有- stones[i] 又有+stones[i],看着有点晕乎。

大家可以再去看 dp[j]的含义。

  1. dp数组如何初始化

既然 dp[j]中的j表示容量,那么最大容量(重量)是多少呢,就是所有石头的重量和。

而我们要求的target其实只是最大重量的一半,所以dp数组开到sum/2大小就可以了。

当然也可以把石头遍历一遍,计算出石头总重量 然后除2,得到dp数组的大小。

接下来就是如何初始化dp[j]呢,因为重量都不会是负数,所以dp[j]都初始化为0就可以了,这样在递归公式dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);中dp[j]才不会初始值所覆盖。

    int sum = 0;
    for (int i : stones) {
        sum += i;
    }
    int target = sum / 2;
    int[] dp = new int[target + 1];
  1. 确定遍历顺序

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

//外层遍历物品
    for (int i = 0; i < stones.length; i++) {
        //内层遍历背包(要放得下当前物品)
        for (int j = target; j >= stones[i]; j--) {
            int no = dp[j];
            int yes = dp[j - stones[i]] + stones[i];
            dp[j] = Math.max(no, yes);
        }
    }
  1. 举例推导dp数组

举例,输入:[2,4,1,1],此时target = (2 + 4 + 1 + 1)/2 = 4 ,dp数组状态图如下:

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

最后dp[target]里是容量为target的背包所能背的最大重量。

那么分成两堆石头,一堆石头的总重量是dp[target],另一堆就是sum - dp[target]。

在计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的

那么相撞之后剩下的最小石头重量就是 (sum - dp[target]) - dp[target]。

整体代码

一维数组

public int lastStoneWeightII(int[] stones) {
    int sum = 0;
    for (int i : stones) {
        sum += i;
    }
    int target = sum / 2;
    int[] dp = new int[target + 1];
    //外层遍历物品
    for (int i = 0; i < stones.length; i++) {
        //内层遍历背包(要放得下当前物品)
        for (int j = target; j >= stones[i]; j--) {
            int no = dp[j];
            int yes = dp[j - stones[i]] + stones[i];
            dp[j] = Math.max(no, yes);
        }
    }
    return sum - dp[target] * 2;
}
  • 时间复杂度:O(m × n) , m是石头总重量(准确的说是总重量的一半),n为石头块数
  • 空间复杂度:O(m)

二维数组

public int lastStoneWeightII(int[] stones) {
    int sum = 0;
    for (int i : stones) {
        sum += i;
    }
    int target = sum / 2;
    int[][] dp = new int[stones.length][target + 1];
    //初始化dp数组
    for (int i = stones[0]; i < target + 1; i++) {
        dp[0][i] = stones[0];
    }
    //外层遍历物品
    for (int i = 1; i < stones.length; i++) {
        for (int j = 1; j <= target; j++) {
            //如果放不下当前物品
            if (j < stones[i]) {
                dp[i][j] = dp[i - 1][j];
            } else {
                //不放入的情况
                int no = dp[i - 1][j];
                //放入的情况
                int yes = dp[i - 1][j - stones[i]] + stones[i];
                dp[i][j] = Math.max(no, yes);
            }
        }
    }
    return sum - dp[stones.length - 1][target] * 2;
}
  • 时间复杂度:O(m × n) , m是石头总重量(准确的说是总重量的一半),n为石头块数
  • 空间复杂度:O(m× n)

494.目标和

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

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

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

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

示例 1:

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3

示例 2:

输入:nums = [1], target = 1
输出:1

提示:

  • 1 <= nums.length <= 20
  • 0 <= nums[i] <= 1000
  • 0 <= sum(nums[i]) <= 1000
  • -1000 <= target <= 1000

Related Topics

  • 数组

  • 动态规划

  • 回溯

思路

这道题目咋眼一看和动态规划背包啥的也没啥关系。

本题要如何使表达式结果为target,

既然为target,那么就一定有 left组合 - right组合 = target。

left + right = sum,而sum是固定的。right = sum - left

公式来了, left - (sum - left) = target 推导出 left = (target + sum)/2 。

target是固定的,sum是固定的,left就可以求出来。

此时问题就是在集合nums中找出和为left的组合。

如何转化为01背包问题呢。

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

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

x = (target + sum) / 2

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

这里的x,就是bagSize,也就是我们后面要求的背包容量。

大家看到(target + sum) / 2 应该担心计算的过程中向下取整有没有影响。

这么担心就对了,例如sum 是5,target 是2的话其实就是无解的,所以:

    //此时没有方案,两个int相加的时候要格外小心数值溢出的问题
    if ((target + sum) % 2 == 1) {
        return 0;
    }

同时如果 S的绝对值已经大于sum,那么也是没有方案的。

    //如果目标值大于总和,则直接返回0;
    if (Math.abs(target) > sum) {
        return 0;
    }

再回归到01背包问题,为什么是01背包呢?

因为每个物品(题目中的1)只用一次!

这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少。

本题则是装满有几种方法。其实这就是一个组合问题了。

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

dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法

其实也可以使用二维dp数组来求解本题,dp[i][j]:使用 下标为[0, i]的nums[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种方法。

  1. 确定递推公式

有哪些来源可以推出dp[j]呢?

只要搞到nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。

例如:dp[j],j 为5,

  • 已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 容量为5的背包。
  • 已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 容量为5的背包。
  • 已经有一个3(nums[i]) 的话,有 dp[2]种方法 凑成 容量为5的背包
  • 已经有一个4(nums[i]) 的话,有 dp[1]种方法 凑成 容量为5的背包
  • 已经有一个5 (nums[i])的话,有 dp[0]种方法 凑成 容量为5的背包

那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。

所以求组合类问题的公式,都是类似这种:

dp[j] += dp[j - nums[i]];

这个公式在后面在讲解背包解决排列组合问题的时候还会用到!

  1. dp数组如何初始化

从递推公式可以看出,在初始化的时候dp[0] 一定要初始化为1,因为dp[0]是在公式中一切递推结果的起源,如果dp[0]是0的话,递推结果将都是0。

这里有录友可能认为从dp数组定义来说 dp[0] 应该是0,也有录友认为dp[0]应该是1。

其实不要硬去解释它的含义,咱就把 dp[0]的情况带入本题看看应该等于多少。

如果数组[0] ,target = 0,那么 bagSize = (target + sum) / 2 = 0。 dp[0]也应该是1, 也就是说给数组里的元素 0 前面无论放加法还是减法,都是 1 种方法。

所以本题我们应该初始化 dp[0] 为 1。

可能有同学想了,那 如果是 数组[0,0,0,0,0] target = 0 呢。

其实 此时最终的dp[0] = 32,也就是这五个零 子集的所有组合情况,但此dp[0]非彼dp[0],dp[0]能算出32,其基础是因为dp[0] = 1 累加起来的。

dp[j]其他下标对应的数值也应该初始化为0,从递推公式也可以看出,dp[j]要保证是0的初始值,才能正确的由dp[j - nums[i]]推导出来。

  1. 确定遍历顺序

动态规划:关于01背包问题,你该了解这些!(滚动数组) (opens new window)中,我们讲过对于01背包问题一维dp的遍历,nums放在外循环,target在内循环,且内循环倒序。

  1. 举例推导dp数组

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

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

dp数组状态变化如下:

img

一维数组解法

public int findTargetSumWays(int[] nums, int target) {
    //计算总和
    int sum = 0;
    for (int i : nums) {
        sum += i;
    }
    //如果目标值大于总和,则直接返回0;
    if (Math.abs(target) > sum) {
        return 0;
    }
    //此时没有方案,两个int相加的时候要格外小心数值溢出的问题
    if ((target + sum) % 2 == 1) {
        return 0;
    }
    //背包大小,转变为组合总和问题,bagsize就是要求的和
    int bigSize = (target + sum) / 2;
    int[] dp = new int[bigSize + 1];
    dp[0] = 1;
    for (int i = 0; i < nums.length; i++) {
        for (int j = bigSize; j >= nums[i]; j--) {
            dp[j] += dp[j - nums[i]];
        }
    }
    return dp[bigSize];
}
  • 时间复杂度:O(n × m),n为正数个数,m为背包容量
  • 空间复杂度:O(m),m为背包容量

二维数组解法

public int findTargetSumWays(int[] nums, int target) {
    //计算总和
    int sum = 0;
    for (int i : nums) {
        sum += i;
    }
    //如果目标值大于总和,则直接返回0;
    if (Math.abs(target) > sum) {
        return 0;
    }
    //此时没有方案,两个int相加的时候要格外小心数值溢出的问题
    if ((target + sum) % 2 == 1) {
        return 0;
    }
    //背包大小,转变为组合总和问题,bagsize就是要求的和
    int bigSize = (target + sum) / 2;
    // dp[i][j]:遍历到数组第i个数时, left为j时的能装满背包的方法总数
    int[][] dp = new int[nums.length][bigSize + 1];
    // 初始化最上行(dp[0][j]),当nums[0] == j时(注意nums[0]和j都一定是大于等于零的,因此不需要判断等于-j时的情况),有唯一一种取法可取到j,dp[0][j]此时等于1
    // 其他情况dp[0][j] = 0
    // java整数数组默认初始值为0
    if (nums[0] <= bigSize) {
        dp[0][nums[0]] = 1;
    }
    // 初始化最左列(dp[i][0])
    // 当从nums数组的索引0到i的部分有n个0时(n > 0),每个0可以取+/-,因此有2的n次方中可以取到j = 0的方案
    // n = 0说明当前遍历到的数组部分没有0全为正数,因此只有一种方案可以取到j = 0(就是所有数都不取)
    //当前列的0的个数
    int numZeros = 0;
    for (int i = 0; i < nums.length; i++) {
        if (nums[i] == 0) {
            numZeros++;
        }
        //计算2的numZeros次方
        dp[i][0] = (int) Math.pow(2, numZeros);
    }

    for (int i = 1; i < nums.length; i++) {
        for (int j = 1; j <= bigSize; 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.length - 1][bigSize];
}
  • 时间复杂度:O(n × m),n为正数个数,m为背包容量
  • 空间复杂度:O(n × m),m为背包容量

474.一和零

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

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

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

示例 1:

输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。

示例 2:

输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。

提示:

  • 1 <= strs.length <= 600
  • 1 <= strs[i].length <= 100
  • strs[i] 仅由 '0''1' 组成
  • 1 <= m, n <= 100

Related Topics

  • 数组

  • 字符串

  • 动态规划

思路

这道题目,还是比较难的,也有点像程序员自己给自己出个脑筋急转弯,程序员何苦为难程序员呢。

来说题,本题不少同学会认为是多重背包,一些题解也是这么写的。

其实本题并不是多重背包,再来看一下这个图,捋清几种背包的关系

416.分割等和子集1

多重背包是每个物品,数量不同的情况。

本题中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. 确定遍历顺序

那么本题也是,物品就是strs里的字符串,背包容量就是题目描述中的m和n。


都是物品重量的一个维度,先遍历哪个都行!

  1. 举例推导dp数组

以输入:[“10”,“0001”,“111001”,“1”,“0”],m = 3,n = 3为例

最后dp数组的状态如下所示:

474.一和零

public int findMaxForm(String[] strs, int m, int n) {
    //创建dp数组
    int[][] dp = new int[m + 1][n + 1];
    for (String s : strs) {
        int oneNum = 0;
        int zeroNum = 0;
        //先找到当前字符串的1 和 0 的个数
        for (char c : s.toCharArray()) {
            if (c == '0') {
                zeroNum++;
            } else {
                oneNum++;
            }
        }
        //倒序遍历
        for (int i = m; i >= zeroNum; i--) {
            for (int j = n; j >= oneNum; j--) {
                int no = dp[i][j];
                int yes = dp[i - zeroNum][j - oneNum] + 1;
                dp[i][j] = Math.max(no, yes);
            }
        }
    }
    return dp[m][n];
}
  • 时间复杂度: O(kmn),k 为strs的长度
  • 空间复杂度: O(mn)

标签:stones,target,nums,int,sum,随想录,II,LeetCode474,dp
From: https://blog.csdn.net/weixin_60364343/article/details/140785366

相关文章

  • 代码随想录训练第三十二天|完全背包理论基础、LeetCode518.零钱兑换II、LeetCode377.
    文章目录完全背包理论基础完全背包总结518.零钱兑换II思路一维数组二维数组377.组合总和Ⅳ思路卡码网70.爬楼梯(进阶版)思路完全背包理论基础完全背包有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i]。每件物品都有无......
  • 代码随想录训练第三十三天|LeetCode322. 零钱兑换、LeetCode279.完全平方数、LeetCode
    文章目录322.零钱兑换思路279.完全平方数思路139.单词拆分思路多重背包背包总结遍历顺序01背包完全背包总结322.零钱兑换给你一个整数数组coins,表示不同面额的硬币;以及一个整数amount,表示总金额。计算并返回可以凑成总金额所需的最少的硬币个数。如果......
  • IIS6.1+ASP+ACCESS网站迁移
    1.首先在源web服务器IIS管理器中将要迁移的网站停止,然后将要迁移的网站整个目录拷贝到目标服务器相同目录下;2.通过cmd命令进到源web服务器inetsrv目录:cd/dc:\windows\system32\inetsrv3.使用以下命令将源web服务器中IIS应用程序池配置信息导出:appcmdlistapppool/config/......
  • 第五十六天 第十一章:图论part06 108.冗余连接 109. 冗余连接II
    108.冗余连接继续使用查并集的方法,如果两个元素是在一个集合,那么我们就输出,反之加入集合。#include<iostream>#include<vector>usingnamespacestd;intN;vector<int>father=vector<int>(1001,0);voidinit(){for(inti=0;i<=N;i++){father[i]=i;......
  • Collecting Numbers II
    原题链接题解如果一个\(k\),其前面没有出现过\(k-1\),那么回合数+1,我们令这样的数叫做断点因此交换两个数\(l,r\)不会影响\([1,l-1],[r+1,n]\)内的断点code#include<bits/stdc++.h>#definelllonglong#definelowbit(x)((x)&(-x))usingnamespacestd;constll......
  • 代码随想录算法训练营第53天 | 图论2:岛屿数量相关问题
    99.岛屿数量https://kamacoder.com/problempage.php?pid=1171岛屿深搜https://www.programmercarl.com/kamacoder/0099.岛屿的数量深搜.html岛屿广搜https://www.programmercarl.com/kamacoder/0099.岛屿的数量广搜.html#思路100.岛屿的最大面积https://www.programmercar......
  • 代码随想录 day40 打家劫舍 及其变体
    打家劫舍打家劫舍解题思路动态规划解决问题,通过前两个值决定第三个值,需要注意的是初始值的选择,第二个的值是取前两个数中较大的,这样是为了保证跳过不需要取的值知识点动态规划心得初始值的选择没有考虑到,其余的都写出来了打家劫舍二打家劫舍二解题思路前一题的改进,只......
  • 代码随想录算法训练营Day0| LeetCode704: 二分查找
    LeetCode704二分查找先看了一下数组理论基础:数组基础题目链接:704.二分查找啥也没看,凭感觉直接上手:classSolution(object): defsearch(self,nums,target): fornuminnums: ifnum==target: returnnums.index(num) break return-1通过倒是......
  • 文件解析漏洞总结(IIS,NGINX,APACHE)
    目录一、IIS解析漏洞IIS6.X方式一:目录解析方式二:畸形文件解析IIS7.X利用条件环境配置下载链接:二、Nginx解析漏洞2.1:nginx_parsing利用条件利用姿势2.2:CVE-2013-4547影响版本利用姿势三、Apache解析漏洞3.1:apache_parsing利用姿势3.2:CVE-2017-15715影响版......
  • 代码随想录二刷(链表章节)
    代码随想录二刷(链表章节)链表就是通过指针串联在一起的线性结构,每个节点都是由一个数据域和指针域(存放下一个节点的指针)。双链表就是每个节点中既有指向前一个节点的,也有指向后一个节点的。循环链表就是把头和尾连起来。性能分析如下:下面来看下链表的具体题目:Leetcod......