力扣474题、416题、1049题、474题·、494题(0-1背包)

416、分割等和子集

基本思想:

动态规划-01背包--一个商品只能放一次

  • 背包的体积为sum/2
  • 背包要放入的商品(集合里的元素)重量为元素的数值,价值也为元素的数值
  • 背包如果正好装满,说明找到了总和为sum/2的子集
  • 背包中每一个元素是不可重复放入

具体实现:

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

01背包中,dp[j] 表示: 容量为j的背包,所背的物品价值可以最大为dp[j]。

套入本题,dp[j]表示:容量为j的背包,最大可以凑成j的子集总和为dp[j]。

2、状态转移方程

01背包的递推公式为: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]);

3、初始状态

dp数组都初始化为0

4、确认遍历顺序

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

5、举例推导dp数组

dp[i]的数值一定是小于等于i的

如果dp[i] == i 说明,集合中的子集总和正好可以凑成总和i

 

 dp[11] == 11,说明可以将这个数组分割成两个子集,使得两个子集的元素和相等。

代码:

class Solution {
    public boolean canPartition(int[] nums) {
        if(nums == null || nums.length == 0) return false;
        int n= nums.length;
        int sum = 0;
        for(int num : nums){
            sum += num;
        }
        if(sum % 2 != 0) return false;
        int target = sum/2;
        int[] dp = new int[target + 1];
        for(int i = 0; i < n; i++){
            for (int j = target; j >= nums[i]; j--){
                dp[j] = Math.max(dp[j], dp[j-nums[i]] + nums[i]);
            }
        }
        return dp[target] == target;
    }
}

 

1049、最后一块石头的重量

基本思想:

尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,化解为01背包

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

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

具体实现:

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

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

2.确定递推公式

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

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

3.dp数组初始化

dp数组初始化为0

4.确定遍历顺序

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

5.举例推导dp数组

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

 

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

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

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

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

代码:

class Solution {
    public int lastStoneWeightII(int[] stones) {
        int sum = 0;
        for (int i : stones){
            sum += i;
        }
        int target = sum >> 1;
        int[] dp = new int[target + 1];
        for (int i = 0; i < stones.length; i++){
            for (int j = target; j >= stones[i]; j--){
                dp[j] = Math.max(dp[j],dp[j-stones[i]] + stones[i]);
            }
        }
        return sum - 2 * dp[target];
    }
}

 

 

494、目标和

基本思想:

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

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

x = (target + sum) / 2

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

因为每个物品(题目中的1)只用一次,所以是01背包

具体实现:

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

dp[ i ][ j ]表示:从数组nums中 0 - i 的元素进行加减可以得到 j 的方法数量。

从下标为[0-i]的数中任意取,填满j(包括j)这么大容积的包,有dp[ i ][ j ]种方法

2.确定递推公式

dp[ i ][ j ] 的来源

两个方向推导

  • j<nums[i]  因为容量不够,不放nums[i]:dp[i][j]=dp[i-1][j]
  • j>=nums[i]   容量够dp[i][j] =
    •   dp[i-1][j](不放nums[i]时的方法数)
    •   dp[i-1][j - nums[i]](背包容量空出一个nums[i]的位置,剩余容量的方法数,因为再加上nums[i]不需要另算一种方法)
      •   意思就是我有两条路到终点,到终点之前有一条必经之路,这条必经之路不影响我还是两条路到终点

 

 

dp[j]理解:j为5时

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

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

 

3.初始化

(1)背包容量为0时的唯一方法就是什么也不放

for (int i = 0; i < nums.length; i++){
            dp[i][0] = 1;
        }

(2)如果第一个物品他的重量为0,那么背包容量为0时就有两种情况,一种是放入这个重量为0的物品,一种是啥也不放

如果第一个物品的重量不是0,那么当背包容量正好等于这个物品重量时,就一定有一种方法

4.遍历顺序

从前到后,从上到下

5.举例

 

 

 

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int i = 0; i < nums.length; i++) sum += nums[i];
        if ((target + sum) % 2 != 0) return 0;
        int size = (target + sum) / 2;
        if(size < 0) size = -size;
        int[][] dp = new int[nums.length][size + 1];
        for (int i = 0; i < nums.length; i++){
            dp[i][0] = 1;
        }
        if(nums[0] == 0){
            dp[0][0] = 2;
        } else {
            for (int j = 0; j <= size; j++){
                if (j == nums[0]){
                    dp[0][j] = 1;
                }
            }
        }
        for (int i = 1; i < nums.length; i++) {
            for (int j = 0; j <= size; j++) {
                if (j < nums[i]){
                    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][size];
    }
}

 

 

优化:

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int i = 0; i < nums.length; i++) sum += nums[i];
        if ((target + sum) % 2 != 0) return 0;
        int size = (target + sum) / 2;
        if(size < 0) size = -size;
        int[] dp = new int[size + 1];
        dp[0] = 1;
        for (int i = 0; i < nums.length; i++) {
            for (int j = size; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[size];
    }
}

 

474、0和1

基本思想:

动态规划

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

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

本题是01背包问题

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

问背包最多能放几个物品

具体实现:

1、确定状态:

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

2、状态转移方程:

多琢磨01背包基础

没优化的

dp[i][j][k]=max(dp[i−1][j−当前字符串使用0的个数][k−当前字符串使用1的个数]+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);

字符串的zeroNum和oneNum相当于物品的重量(weight[i])。

3、初始化:

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

4、遍历顺序

优化的需要从后往前

5、举例

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

 

 

代码:

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        //dp[i][j]表示i个0和j个1时的最大子集
        int[][] dp = new int[m + 1][n + 1];
        int oneNum, zeroNum;
        for (String str : strs) {
            oneNum = 0;
            zeroNum = 0;
            for (char ch : str.toCharArray()) {
                if (ch == '0') {
                    zeroNum++;
                } else {
                    oneNum++;
                }
            }
//倒序遍历,双层循环遍历的都是背包容量
for (int i = m; i >= zeroNum; i--) { for (int j = n; j >= oneNum; j--) { dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1); } } } return dp[m][n]; } }

 

posted @ 2021-03-21 10:42  最近饭吃的很多  阅读(100)  评论(0编辑  收藏  举报