动态规划4:目标和、一和零、完全背包理论、零钱兑换||、第4节总结

16. 目标和

例题494:
给你一个非负整数数组 nums 和一个整数 target 。

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

例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
在这里插入图片描述
难点:
target是固定的,那么一定会有left组合-right组合=target。
而sum也是固定的,left+right=sum
所以推出:left-(sum-left)=target——>left=(sum+target)/2
因此,后续问题就是怎么在nums中求出和为固定left的组合。

动态规划思路

  1. 确定dp数组和下标含义
    dp[j]表示容量为j的背包可装组合数。
  2. 确定递推公式
    有哪些来源可以推出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[0]=1,从递推结果反向推导
  2. 确定遍历顺序
    一维滚动数组只有一个方向:先物品后背包,并且背包是倒序遍历
  3. 举例验证递推公式
    在这里插入图片描述
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
      int left,sum=0;
        for(int i=0;i<nums.length;i++){
            sum+=nums[i];
        }
        if((target+sum)<0 || target>sum) return 0;//此时无解
        if((target+sum)%2==1) return 0;//此时无解
        left=(sum+target)/2;
        int[] dp=new int[left+1];
        dp[0]=1;
        for(int i=0;i<nums.length;i++){
            for(int j=left;j>=nums[i];j--){
                dp[j]+=dp[j-nums[i]];
            }
        }
        return dp[left];
    }
}

需要注意:
①当sum+target为奇数的时候,直接返回0。
因为left+right=sum,left-right=target。所以,sum和target应该是同奇同偶的,否则矛盾。
②dp数组的含义发生了变化,不再是以前的最大容量,而是最大可能组合数。
③对应的递推公式也发生变化,同不同的二叉搜索树题目类似,dp[j]应该由dp[j-weight[i]]的和相加。

17. 一和零

例题474:
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。

请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。

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

在这里插入图片描述

动态规划

  1. 确定dp数组和下标含义
    dp[i][j]表示有i个0,j个1时的子集长度。
  2. 确定递推公式
    dp[i][j]由当前背包0、1个数减去当前str的0、1个数得到的dp数组子集个数+1
    dp[i][j]=Math.max(dp[i][j],dp[i-zeroNum][j-oneNum]+1)
  3. 初始化
    dp[0][0]=0
  4. 遍历方向
    从物品再背包从左到右依次遍历
  5. 举例验证递推公式

代码如下:

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int[][] dp=new int[m+1][n+1];
        dp[0][0]=0;
        for(String c:strs)
        {
            int zeroNum=findZeroNum(c);
            int oneNum=findOneNum(c);
            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];
    }

    public int findZeroNum(String str){
        int count=0;
        for(int i=0;i<str.length();i++){
            if(str.charAt(i)=='0')
            count++;
        }
        return count;
    }

    public int findOneNum(String str){
        int count=0;
        for(int i=0;i<str.length();i++){
            if(str.charAt(i)=='1')
            count++;
        }
        return count;
    }
}

注意:遍历背包和物品时是倒序。防止重复装入。

18. 完全背包理论基础

有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。

同样leetcode上没有纯完全背包问题,都是需要完全背包的各种应用,需要转化成完全背包问题,所以我这里还是以纯完全背包问题进行讲解理论和原理。

在下面的讲解中,我依然举这个例子:

背包最大重量为4。

物品为:

重量价值
物品0115
物品1320
物品2430

每件商品都有无限个!

问背包能背的物品最大价值是多少?

01背包和完全背包唯一不同就是体现在遍历顺序上,所以本文就不去做动规五部曲了,我们直接针对遍历顺序经行分析!

01背包的遍历代码如下:

for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}

我们知道01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。

而完全背包的物品是可以添加多次的,所以要从小到大去遍历,即:

// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

    }
}

01背包中二维dp数组的两个for遍历的先后循序是可以颠倒了,一维dp数组的两个for循环先后循序一定是先遍历物品,再遍历背包容量。

在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!

因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。

遍历物品在外层循环,遍历背包容量在内层循环,状态如图:
在这里插入图片描述
遍历背包容量在外层循环,遍历物品在内层循环,状态如图:
在这里插入图片描述
看了这两个图,大家就会理解,完全背包中,两个for循环的先后循序,都不影响计算dp[j]所需要的值(这个值就是下标j之前所对应的dp[j])。

先遍历背包在遍历物品,代码如下:

// 先遍历背包,再遍历物品
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
    for(int i = 0; i < weight.size(); i++) { // 遍历物品
        if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
    cout << endl;
}

例题:卡码网52题
小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的空间,并且具有不同的价值。

小明的行李空间为 N,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料可以选择无数次,并且可以重复选择。

输入:

第一行包含两个整数,N,V,分别表示研究材料的种类和行李空间

接下来包含 N 行,每行两个整数 wi 和 vi,代表第 i 种研究材料的重量和价值

输出:

输出一个整数,表示最大价值。

在这里插入图片描述

import java.util.Scanner;
class Main{
    public static void main(String[] args){
        int res=comBb();
        System.out.println(res);
    }
    public static int comBb(){
int N,V;
//System.out.println("input N and V:");
Scanner scan=new Scanner(System.in);
N=scan.nextInt();
V=scan.nextInt();
//System.out.println("input weight and values:");
int[] weight=new int[N];
int[] values=new int[N];
for(int i=0;i<N;i++){
    weight[i]=scan.nextInt();
    values[i]=scan.nextInt();
}
int[] dp=new int[V+1];
for(int i=0;i<N;i++){
    for(int j=weight[i];j<=V;j++){
        dp[j]=Math.max(dp[j],dp[j-weight[i]]+values[i]);
    }
}
return dp[V];
}
}

19. 零钱兑换||

例题518:
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。

假设每一种面额的硬币有无限个。

题目数据保证结果符合 32 位带符号整数。

在这里插入图片描述
因为每一种面额的硬币有无数个,对应完全背包问题。

  1. 确定dp数组和下标定义
    dp[j]为总金额j的最多组合数。
  2. 确定递推公式
    如果有1块钱,那么dp[j]应该由dp[j-1]种组合决定;
    如果有2块钱,那么dp[j]应该由dp[j-2]种组合决定;

    如果有j块钱,那么dp[j]应该由dp[0]种组合决定。
    所以dp[j]是其前dp的和,这是一个组合问题,组合问题中dp[j]的公式如下:
    dp[j]+=dp[j-weight[i]]
  3. 初始化
    dp[0]=0,由结果反推
  4. 确定遍历方向
    完全背包的一维dp可以有两种方向:先物品后背包或者先背包后物品。只不过背包是正向遍历,保证可以取多个物品。
    但在这里不行!
    先看先物品后背包:
for (int i = 0; i < coins.size(); i++) { // 遍历物品
    for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
        dp[j] += dp[j - coins[i]];
    }
}

假设:coins[0] = 1,coins[1] = 5。

那么就是先把1加入计算,然后再把5加入计算,得到的方法数量只有{1, 5}这种情况。而不会出现{5, 1}的情况。

所以这种遍历顺序中dp[j]里计算的是组合数!

如果把两个for交换顺序,代码如下:

for (int j = 0; j <= amount; j++) { // 遍历背包容量
    for (int i = 0; i < coins.size(); i++) { // 遍历物品
        if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
    }
}

背包容量的每一个值,都是经过 1 和 5 的计算,包含了{1, 5} 和 {5, 1}两种情况。

此时dp[j]里算出来的就是排列数!
5. 举例验证递推公式
代码如下:

时间复杂度O(mn),n是coins长度,m是amount。
空间复杂度O(m)
class Solution {
    public int change(int amount, int[] coins) {
        int[] dp=new int[amount+1];
        dp[0]=1;
        for(int i=0;i<coins.length;i++){
            for(int j=coins[i];j<=amount;j++){
                dp[j]+=dp[j-coins[i]];
            }
        }
        return dp[amount];
    }
}

注意:

  1. 这个题的难点不是递推公式,因为组合问题中dp[j]+=dp[j-weight[i]]碰见过。难点是遍历顺序的确定
    如果是组合问题就是先物品后背包。
    如果是排列问题就是先背包后物品。

20. 动态规划第4节总结

  1. 目标和
    难点是想到背包的容量是left,因为sum和target固定,所以left=(sum+target)/2。并且还要想到sum和target同奇同偶可以剪枝。
    所以,题目就转换为了在容量为left的背包中,和为left的组合数。
    在这里,碰到了组合问题的背包递推公式:dp[j]+=dp[j-weight[i]]
  2. 一和零
    一个字符串数组中的每个字符串的1、0个数,找到满足m个0,n个1的最长子集数。
    dp[i][j]表示满足(小于等于)i个0和j个1的字符串个数。
    dp[i][j]应该由去掉当前字符串的01个数后的个数+1,与本身的最大值决定。
    所以,dp[i][j]=Math.max(dp[i][j],dp[i-zeroNum][j-oneNum]+1)
  3. 完全背包
    遍历方向两个都可以,需要注意的是为了保证可以重复取物品,背包是正向遍历。
    与01背包的二维数组不同就是背包的遍历方向是正序而不是倒序。
    与01背包的一维数组不同的是,一维只有一个遍历方向,那就是先物品后背包,并且背包是倒序遍历。
  4. 零钱兑换||
    完全背包中的组合问题
    该题揭示了完全背包中组合问题与排列问题的遍历顺序不同。
    组合问题:先物品后背包
    排列问题:先背包后物品
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值