首页 > 其他分享 >代码随想录训练第三十三天|LeetCode322. 零钱兑换、LeetCode279.完全平方数、LeetCode139.单词拆分、多重背包、背包总结

代码随想录训练第三十三天|LeetCode322. 零钱兑换、LeetCode279.完全平方数、LeetCode139.单词拆分、多重背包、背包总结

时间:2024-07-31 18:55:50浏览次数:16  
标签:LeetCode279 遍历 int 随想录 背包 物品 new dp

文章目录

322. 零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1

示例 2:

输入:coins = [2], amount = 3
输出:-1

示例 3:

输入:coins = [1], amount = 0
输出:0

提示:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 231 - 1
  • 0 <= amount <= 104

Related Topics

  • 广度优先搜索

  • 数组

  • 动态规划

思路

动态规划:518.零钱兑换II (opens new window)中我们已经兑换一次零钱了,这次又要兑换,套路不一样!

题目中说每种硬币的数量是无限的,可以看出是典型的完全背包问题。

动态规划五部曲

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

dp[j]:凑足总额为j所需钱币的最少个数为dp[j]

  1. 确定递推公式

凑足总额为j - coins[i]的最少个数为dp[j - coins[i]],那么只需要加上一个钱币coins[i]即dp[j - coins[i]] + 1就是dp[j](考虑coins[i])

所以dp[j] 要取所有 dp[j - coins[i]] + 1 中最小的。

递推公式:dp[j] = min(dp[j - coins[i]] + 1, dp[j]);

  1. dp数组如何初始化

首先凑足总金额为0所需钱币的个数一定是0,那么dp[0] = 0;

其他下标对应的数值呢?

考虑到递推公式的特性,dp[j]必须初始化为一个最大的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖。

所以下标非0的元素都是应该是最大值。

    //定义dp数组
    int[] dp = new int[amount + 1];
    //初始化dp数组
    int max = Integer.MAX_VALUE;
    for (int i = 0; i < dp.length; i++) {
        dp[i] = max;
    }
  1. 确定遍历顺序

本题求钱币最小个数,那么钱币有顺序和没有顺序都可以,都不影响钱币的最小个数

所以本题并不强调集合是组合还是排列。

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

在动态规划专题我们讲过了求组合数是动态规划:518.零钱兑换II (opens new window),求排列数是动态规划:377. 组合总和 Ⅳ (opens new window)

所以本题的两个for循环的关系是:外层for循环遍历物品,内层for遍历背包或者外层for遍历背包,内层for循环遍历物品都是可以的!

那么我采用coins放在外循环,target在内循环的方式。

本题钱币数量可以无限使用,那么是完全背包。所以遍历的内循环是正序

综上所述,遍历顺序为:coins(物品)放在外循环,target(背包)在内循环。且内循环正序。

  1. 举例推导dp数组

以输入:coins = [1, 2, 5], amount = 5为例

322.零钱兑换

dp[amount]为最终结果。

整体代码

public int coinChange(int[] coins, int amount) {
    //定义dp数组
    int[] dp = new int[amount + 1];
    //初始化dp数组
    int max = Integer.MAX_VALUE;
    for (int i = 0; i < dp.length; i++) {
        dp[i] = max;
    }
    //当金额为0时需要硬币数量为0;
    dp[0] = 0;
    //遍历物品
    for (int i = 0; i < coins.length; i++) {
        //遍历背包
        for (int j = coins[i]; j <= amount; j++) {
            //只有dp[j-coins[i]]不是初始最大值时,该位才有选择的必要
            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.完全平方数

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,14916 都是完全平方数,而 311 不是。

示例 1:

输入:n = 12
输出:3 
解释:12 = 4 + 4 + 4

示例 2:

输入:n = 13
输出:2
解释:13 = 4 + 9

提示:

  • 1 <= n <= 104

Related Topics

  • 广度优先搜索

  • 数学

  • 动态规划

思路

我来把题目翻译一下:完全平方数就是物品(可以无限件使用),凑个正整数n就是背包,问凑满这个背包最少有多少物品?

感受出来了没,这么浓厚的完全背包氛围,而且和昨天的题目动态规划:322. 零钱兑换 (opens new window)就是一样一样的!

动态规划五部曲

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

dp[j]:和为j的完全平方数的最少数量为dp[j]

  1. 确定递推公式

dp[j] 可以由dp[j - i * i]推出, dp[j - i * i] + 1 便可以凑成dp[j]。

此时我们要选择最小的dp[j],所以递推公式:dp[j] = min(dp[j - i * i] + 1, dp[j]);

  1. dp数组如何初始化

dp[0]表示 和为0的完全平方数的最小数量,那么dp[0]一定是0。

有同学问题,那0 * 0 也算是一种啊,为啥dp[0] 就是 0呢?

看题目描述,找到若干个完全平方数(比如 1, 4, 9, 16, …),题目描述中可没说要从0开始,dp[0]=0完全是为了递推公式。

非0下标的dp[j]应该是多少呢?

从递归公式dp[j] = min(dp[j - i * i] + 1, dp[j]);中可以看出每次dp[j]都要选最小的,所以非0下标的dp[j]一定要初始为最大值,这样dp[j]在递推的时候才不会被初始值覆盖

  1. 确定遍历顺序

我们知道这是完全背包,

如果求组合数就是外层for循环遍历物品,内层for遍历背包。

如果求排列数就是外层for遍历背包,内层for循环遍历物品。

动态规划:322. 零钱兑换 (opens new window)中我们就深入探讨了这个问题,本题也是一样的,是求最小数!

所以本题外层for遍历背包,内层for遍历物品,还是外层for遍历物品,内层for遍历背包,都是可以的!

    //遍历物品(数字是从1开始遍历的)
    for (int i = 1; i * i <= n; i++) {
        //遍历背包
        for (int j = i * i; j <= n; j++) {
            dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
        }
    }
  1. 举例推导dp数组

已输入n为5例,dp状态图如下:

279.完全平方数

dp[0] = 0 dp[1] = min(dp[0] + 1) = 1 dp[2] = min(dp[1] + 1) = 2 dp[3] = min(dp[2] + 1) = 3 dp[4] = min(dp[3] + 1, dp[0] + 1) = 1 dp[5] = min(dp[4] + 1, dp[1] + 1) = 2

最后的dp[n]为最终结果。

整体代码

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;
    //遍历物品(数字是从1开始遍历的)
    for (int i = 1; i * i <= n; i++) {
        //遍历背包
        for (int j = i * i; j <= n; j++) {
            dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
        }
    }
    return dp[n];
}
  • 时间复杂度: O(n * √n)
  • 空间复杂度: O(n)

139.单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true

**注意:**不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1:

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。

示例 2:

输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
     注意,你可以重复使用字典中的单词。

示例 3:

输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false

提示:

  • 1 <= s.length <= 300
  • 1 <= wordDict.length <= 1000
  • 1 <= wordDict[i].length <= 20
  • swordDict[i] 仅由小写英文字母组成
  • wordDict 中的所有字符串 互不相同

Related Topics

  • 字典树

  • 记忆化搜索

  • 数组

  • 哈希表

  • 字符串

  • 动态规划

思路

单词就是物品,字符串s就是背包,单词能否组成字符串s,就是问物品能不能把背包装满。

拆分时可以重复使用字典中的单词,说明就是一个完全背包!

动态规划五部曲

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

dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词

  1. 确定递推公式

如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。

所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。

  1. dp数组如何初始化

从递推公式中可以看出,dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递推的根基,dp[0]一定要为true,否则递推下去后面都都是false了。

那么dp[0]有没有意义呢?

dp[0]表示如果字符串为空的话,说明出现在字典里。

但题目中说了“给定一个非空字符串 s” 所以测试数据中不会出现i为0的情况,那么dp[0]初始为true完全就是为了推导公式。

下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词。

  1. 确定遍历顺序

题目中说是拆分为一个或多个在字典中出现的单词,所以这是完全背包。

还要讨论两层for循环的前后顺序。

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

我在这里做一个总结:

求组合数:动态规划:518.零钱兑换II (opens new window)求排列数:动态规划:377. 组合总和 Ⅳ (opens new window)动态规划:70. 爬楼梯进阶版(完全背包) (opens new window)求最小数:动态规划:322. 零钱兑换 (opens new window)动态规划:279.完全平方数(opens new window)

而本题其实我们求的是排列数,为什么呢。 拿 s = “applepenapple”, wordDict = [“apple”, “pen”] 举例。

“apple”, “pen” 是物品,那么我们要求 物品的组合一定是 “apple” + “pen” + “apple” 才能组成 “applepenapple”。

“apple” + “apple” + “pen” 或者 “pen” + “apple” + “apple” 是不可以的,那么我们就是强调物品之间顺序。

所以说,本题一定是 先遍历 背包,再遍历物品。

    //遍历背包
    for (int i = 1; i <= s.length(); i++) {
        //遍历物品
        for (String word : wordDict) {
            int len = word.length();
            if (i >= len && dp[i - len] == true && word.equals(s.substring(i - len, i))) {
                dp[i] = true;
                break;
            }
        }
    }
  1. 举例推导dp[i]

以输入: s = “leetcode”, wordDict = [“leet”, “code”]为例,dp状态如图:

139.单词拆分

dp[s.size()]就是最终结果。

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();
            if (i >= len && dp[i - len] == true && word.equals(s.substring(i - len, i))) {
                dp[i] = true;
                break;
            }
        }
    }
    return dp[s.length()];
}

多重背包

对于多重背包,我在力扣上还没发现对应的题目,所以这里就做一下简单介绍,大家大概了解一下。

有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。

多重背包和01背包是非常像的, 为什么和01背包像呢?

每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。

例如:

背包最大重量为10。

物品为:

重量价值数量
物品01152
物品13203
物品24302

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

和如下情况有区别么?

重量价值数量
物品01151
物品01151
物品13201
物品13201
物品13201
物品24301
物品24301

毫无区别,这就转成了一个01背包问题了,且每个物品只用一次。

import java.util.Scanner;
class multi_pack{
    public static void main(String [] args) {
        Scanner sc = new Scanner(System.in);

        /**
         * bagWeight:背包容量
         * n:物品种类
         */
        int bagWeight, n;
        
        //获取用户输入数据,中间用空格隔开,回车键换行
        bagWeight = sc.nextInt();
        n = sc.nextInt();
        int[] weight = new int[n];
        int[] value = new int[n];
        int[] nums = new int[n];
        for (int i = 0; i < n; i++) weight[i] = sc.nextInt();
        for (int i = 0; i < n; i++) value[i] = sc.nextInt();
        for (int i = 0; i < n; i++) nums[i] = sc.nextInt();

        int[] dp = new int[bagWeight + 1];

        //先遍历物品再遍历背包,作为01背包处理
        for (int i = 0; i < n; i++) {
            for (int j = bagWeight; j >= weight[i]; j--) {
                //遍历每种物品的个数
                for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++) {
                    dp[j] = Math.max(dp[j], dp[j - k * weight[i]] + k * value[i]);
                }
            }
        }
        System.out.println(dp[bagWeight]);
    }
}

背包总结

背包问题是动态规划里的非常重要的一部分,所以我把背包问题单独总结一下,等动态规划专题更新完之后,我们还会在整体总结一波动态规划。

关于这几种常见的背包,其关系如下:

416.分割等和子集1

通过这个图,可以很清晰分清这几种常见背包之间的关系。

在讲解背包问题的时候,我们都是按照如下五部来逐步分析,相信大家也体会到,把这五部都搞透了,算是对动规来理解深入了。

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

其实这五部里哪一步都很关键,但确定递推公式和确定遍历顺序都具有规律性和代表性,所以下面我从这两点来对背包问题做一做总结

问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); ,对应题目如下:

问装满背包有几种方法:dp[j] += dp[j - nums[i]] ,对应题目如下:

问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); ,对应题目如下:

问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); ,对应题目如下:

遍历顺序

01背包

动态规划:关于01背包问题,你该了解这些! (opens new window)中我们讲解二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。

动态规划:关于01背包问题,你该了解这些!(滚动数组) (opens new window)中,我们讲解一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。

一维dp数组的背包在遍历顺序上和二维dp数组实现的01背包其实是有很大差异的,大家需要注意!

完全背包

说完01背包,再看看完全背包。

动态规划:关于完全背包,你该了解这些! (opens new window)中,讲解了纯完全背包的一维dp数组实现,先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。

但是仅仅是纯完全背包的遍历顺序是这样的,题目稍有变化,两个for循环的先后顺序就不一样了。

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

相关题目如下:

如果求最小数,那么两层for循环的先后顺序就无所谓了,相关题目如下:

对于背包问题,其实递推公式算是容易的,难是难在遍历顺序上,如果把遍历顺序搞透,才算是真正理解了

总结

这篇背包问题总结篇是对背包问题的高度概括,讲最关键的两部:递推公式和遍历顺序,结合力扣上的题目全都抽象出来了

而且每一个点,我都给出了对应的力扣题目

最后如果你想了解多重背包,可以看这篇动态规划:关于多重背包,你该了解这些! (opens new window),力扣上还没有多重背包的题目,也不是面试考察的重点。

如果把我本篇总结出来的内容都掌握的话,可以说对背包问题理解的就很深刻了,用来对付面试中的背包问题绰绰有余!

背包问题总结:

img

标签:LeetCode279,遍历,int,随想录,背包,物品,new,dp
From: https://blog.csdn.net/weixin_60364343/article/details/140830217

相关文章

  • 代码随想录算法训练营第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通过倒是......
  • 一种优化 01 可行背包的方法
    source:abc221g有\(n\)个物品,体积分别为\(a_{1,2,\dots,n}\),要求从中选出若干个物品使得体积和为\(V\)。令\(A=\maxa_i\),\(V\lenA\)。一般的01背包做法是\(O(n^2A)\)的,但存在一种相对简单的做法可以做到复杂度\(O(nA)\)。下面描述这个做法。首先任意排列这个物......
  • 代码随想录二刷(链表章节)
    代码随想录二刷(链表章节)链表就是通过指针串联在一起的线性结构,每个节点都是由一个数据域和指针域(存放下一个节点的指针)。双链表就是每个节点中既有指向前一个节点的,也有指向后一个节点的。循环链表就是把头和尾连起来。性能分析如下:下面来看下链表的具体题目:Leetcod......
  • 二维费用背包问题
    宠物小精灵之收服题解:设状态\(dp[i][j][k]\)表示从前\(i\)个物品中选择,物品的费用\(1\)为\(j\),费用\(2\)为\(k\)的最大选择数量。则状态转移方程为:\[dp[i][j][k]=max(dp[i-1][j][k],dp[i-1][j-v_1[i]][k-v_2[i]]+1)\]跟普通01背包一样,第一维可以直接......
  • 代码随想录day14 || 226 翻转二叉树,101 对称二叉树, 104 二叉树的最大深度, 111 二叉树
    226翻转二叉树funcinvertTree(root*TreeNode)*TreeNode{ //思考,广度优先遍历,对于每一层,翻转其左右子节点 ifroot==nil{ returnnil } queue:=list.New() queue.PushBack(root) size:=1//存储每一层的节点个数 forqueue.Len()>0{ varcountint ......
  • 代码随想录算法训练营第28天 | 贪心进阶
    2024年7月30日题122.买卖股票的最佳时机II上涨就买,下跌就不买。classSolution{publicintmaxProfit(int[]prices){intsum=0;for(inti=1;i<prices.length;i++){sum+=prices[i]-prices[i-1]>0?prices[i]-prices[i-1]:0;......
  • 代码随想录算法训练营第27天 | 初入贪心
    2024年7月29日题455.分发饼干先排序,然后依次分发即可。classSolution{publicintfindContentChildren(int[]g,int[]s){//对于每个孩子胃口,从小到大分配,且给尽可能少的饼干Arrays.sort(g);Arrays.sort(s);intcnt=0;......
  • 代码随想录——完全平方数(Leetcode 279)
    题目链接动态规划动态规划思路:状态定义:定义一个一维数组dp,其中dp[i]表示组成整数i所需的最少完全平方数的数量。状态初始化:将dp数组中的所有元素初始化为Integer.MAX_VALUE,表示初始状态下组成每个整数的完全平方数数量是无限大(即不可能)。但dp[0]需要初始化为0,因为组成......