代码随想录-动态规划-完全背包问题(Java)

完全背包问题

(内容参考卡哥代码随想录)

完全背包问题是什么:

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

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

一维滚动数组

01背包一维滚动数组倒序的原因:物品只能添加一次
在完全背包问题中,物品可以被添加多次,所以不需要倒序
在纯完全背包问题中,物品遍历和重量遍历 的 前后遍历顺序都可以
一些非纯完全背包问题,物品遍历在前遍历的是组合数;重量遍历在前遍历的是排列数。
在这里只讨论纯完全背包问题的遍历:

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

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

零钱兑换II

链接: 518.零钱兑换II

思路

自己的思路:

解本题是我有点晕晕乎乎的,包括现在写笔记也有点晕晕乎乎的

抓住完全背包问题

遍历顺序之前解析中提到过:

 for (int i = 0;i < coins.length;i++){
            for (int j = coins[i];j <= amount;j++){
                dp[j] = Math.max(dp[j],dp[j - coins[i]] + coins[i]);
                nums[j] += nums[j - coins[i]];
            }
        }

但是之前提到的是求背包中的最大容量问题

而本题是让求能否装满 以及装满后的方法

自己设置了两个滚动数组,dp[j]用来求容量为j的背包能装的最大金币,然后在最后用这个最大金币与amount进行比较,如果不等则返回0,相等的话返回我们的方法nums[amount]

而我们的方法用另一个数组进行遍历

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

注意初始值nums[0] = 1

其实按实际情况推断nums[0] 0 也行,1也行,为了便于解题,设成1

PS;方法类的题目初始值都是设成的1

官方方法没有那么绕,省略了自己之前审题错误的问题,如果无法凑出则直接返回了0,不需要专门设置一个用来记录最多容纳金币价值的数组来记录

注意:排列数和组合数

纯完全背包问题,两层for循环可以颠倒,但是本题是求方法类的
在这里插入图片描述

物品在前背包在后,可以理解成一行一行去遍历,集合中始终是nums[0]始终在nums[1]后面

而背包在前物品在后,可以理解为纵向遍历,一列一列地遍历,则顺序不一定

【用JS做的时候,打印一下】

代码

class Solution {
    public int change(int amount, int[] coins) {
        // dp[j]表示容量为 j 的背包中,最多能容纳的金币价值
        // nums[] 表示容量为j 的背包中,凑成总金额的方法数
        int[] dp = new int[amount + 1];
        int[] nums = new int[amount + 1];
        dp[0] = 0;
        nums[0] = 1;
        for (int i = 0;i < coins.length;i++){
            for (int j = coins[i];j <= amount;j++){
                dp[j] = Math.max(dp[j],dp[j - coins[i]] + coins[i]);
                nums[j] += nums[j - coins[i]];
            }
        }
        return dp[amount] == amount ? nums[amount] : 0;
    }
}

官方代码

class Solution {
    public int change(int amount, int[] coins) {
        //递推表达式
        int[] dp = new int[amount + 1];
        //初始化dp数组,表示金额为0时只有一种情况,也就是什么都不装
        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];
    }
}

组合总和IV

链接: 377. 组合总和 Ⅳ

思路

组合数和排列数

本题看起来就是普通的完全背包问题

但是需要注意求的是排列数

排列数需要背包在前物品在后

组合数需要物品在前背包在后

【具体见零钱兑换II】

组合总和 总结 (结合回溯算法)

本题题目描述

给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。

题目数据保证答案符合 32 位整数范围。

在这里插入图片描述

说是组合的个数,其实是求排列数

39.组合总和

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的数字可以无限制重复被选取。

此题求的是组合,而非组合数

求组合数用背包可以求出来,求具体的组合则需要回溯爆搜了

代码

class Solution {
    public int combinationSum4(int[] nums, int target) {
        // dp[j]指 总和为j 的元素组合个数
        int[] dp = new int[target + 1];
        dp[0] = 1;
        // 这种方法不强调顺序 组合数
        // for (int i = 0; i < nums.length; i++){ // 遍历物品
        //     for (int j = nums[i]; j <= target; j++){ // 遍历背包容量
        //         dp[j] += dp[j - nums[i]];
        //     }
        // }
        // 强调顺序  排列数
        for (int i = 0;i <= target;i++){
            for (int j = 0;j < nums.length;j++){
                if (i >= nums[j])   dp[i] += dp[i - nums[j]];
            }
        }
        return dp[target];
    }
}

爬楼梯(进阶版)

链接: 70. 爬楼梯

思路

用完全背包的视角看待本问题

1,2两级台阶,可以重复使用

代码

排列数

class Solution {
    public int climbStairs(int n) {
        int[] dp = new int[n + 1];
        dp[0] = 1;
        int[] weight = {1,2};
        for (int i = 1;i <= n;i++){
            for (int j = 0;j < weight.length;j++){
                if (i >= weight[j])
                    dp[i] += dp[i - weight[j]];
            }
        }
        return dp[n];
    }
}

零钱兑换

链接: 322. 零钱兑换

思路

本题的思路依然很简单

但自己存在一个比较不踏实的状态是

简单的自己不按完全背包的思路和步骤走容易漏掉条件

而难的问题自己只顾着害怕,不敢按着完全背包的思路走

take easy

本题简单,但自己的思维很跳脱,导致漏了很多条件

1.dp[j] 总金额为amount时 最少的硬币个数

2.dp[j] = Math.min(dp[j],dp[j - coins[i]] + 1);

尤其要注意本递推公式一定要在dp[j - coins[i]]有意义的情况下才成立;所以需要j 从coins[i]开始遍历,同时dp[j - coins[i]]不是初始值

3.遍历顺序,组合数,物品遍历嵌套总金额遍历

4.初始值,因为是求最小值,所以我们将数组中的每个元素初始化为最大

dp[0] = 0 (不然本题没有意义)

5.举例推导

PS:稍微巧妙的一步是,最后的返回值,如果dp[amount]还是初始值的话,那么说明凑不成amount,所以返回-1

代码

class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] dp = new int[amount + 1];
        int max = Integer.MAX_VALUE;
        for (int i = 0;i <= amount;i++){
            dp[i] = max;
        }
        dp[0] = 0;
        for (int i = 0;i < coins.length;i++){
            for (int j = coins[i];j <= amount;j++){
                // 这一步自己漏掉了,如果前面一个数没有遍历过,你+1也没有意义
                if (dp[j - coins[i]] != max)
                    dp[j] = Math.min(dp[j],dp[j - coins[i]] + 1);
            }
        }
        return dp[amount] == max ? -1 : dp[amount];
    }
}

完全平方数

链接: 279.完全平方数

思路

本道题目和零钱兑换是同一类题目,都是求最小值

需要将数组中的元素初始化为最大值

同时需要单独处理一下dp[0]

本题在处理完全平方数 i * i时需要单独注意一下

PS:初始化也要特别注意一下

代码

class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n + 1];
        int max = Integer.MAX_VALUE;
        for (int i = 0;i <= n;i++){
            dp[i] = max;
        }
        dp[0] = 0;
        for (int i = 1;i * i <= n;i++){
            for (int j = i * i;j <= n;j++){
                if (dp[j - i * i] != max)
                    dp[j] = Math.min(dp[j],dp[j - i * i] + 1);
            }
        }
         return dp[n];
    }
}

单词拆分

链接: 139.单词拆分

思路

与字符串结合的题目千变万化

本题按完全背包的思路正常理一下

纵观本题:

因为字符串有顺序,是求排列数

字符串s在前,字符串列表在后

本题 的返回值是布尔值,也需要依赖前面值,但处理方法与其他完全背包有所不同

题目千变万化,大脑灵活,不要妄想可以碰到原题。

动态规划五部曲:

1.dp[i] 为true表示该字符是字符单词的末尾而且,该单词在字符列表中出现过

2.递推公式 (非传统意义)

因为只是true 或者false,所以不需要一个递推公式

而是在if条件中将需要满足的条件列出来

3.遍历顺序

背包容量(字符串长度)嵌套物品数量(字符列表)

因为本题的字符列表采用的是List形式

所以for (String word : wordDict)更方便一些

关于字符串数组下标,最好还是在草稿纸上面认真看一下

s.substring(a,b)左闭右开

代码

方法一:

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        boolean[] dp = new boolean[s.length() + 1];
        dp[0] = true;
        for (int i = 1;i <= s.length();i++){ //背包容量
            for (String word : wordDict){  //物品
                int len = word.length();
                // 一些条件  i >= len 基本条件 dp[i - len] 表示前一部分字符在字符列表中
                //word.equals(s.substring(i - len,i)) 表示本字段字符符合条件
                if (i >= len && dp[i - len] && word.equals(s.substring(i - len,i))){
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[s.length()];
    }
}

方法二:

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        HashSet<String> set = new HashSet<>(wordDict);
        boolean[] dp = new boolean[s.length() + 1];
        dp[0] = true;

        for (int i = 1;i <= s.length();i++){
            // 遍历的是字符串下标
            // 上一种方法遍历的是字符列表
            // 所以处理方法有很多不一样的地方
            for (int j = 0;j < i && !dp[i] ;j++){
                if (set.contains(s.substring(j,i)) && dp[j]){
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[s.length()];
    }
}

多重背包问题

(之后再整理)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值