代码随想录算法训练营Day33 | Leetcode322. 零钱兑换、279.完全平方数、139.单词拆分 、多重背包(卡码网56. 携带矿石资源)

代码随想录算法训练营Day33 | Leetcode322. 零钱兑换、279.完全平方数、139.单词拆分 、多重背包(卡码网56. 携带矿石资源)

一、零钱兑换

相关题目:Leetcode322
文档讲解:Leetcode322
视频讲解:Leetcode322

1. Leetcode322. 零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。你可以认为每种硬币的数量是无限的。
提示:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 2^31 - 1
  • 0 <= amount <= 10^4
  • 思路:
    • 题目中提到每种硬币的数量是无限的,可以看出是典型的完全背包问题。
    • 动规五部曲
      • 确定 dp 数组以及下标的含义:dp[j] 表示凑足总额为 j 所需钱币的最少个数为 dp[j]。
      • 确定递推公式:凑足总额为 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])
      • dp 数组如何初始化:首先,凑足总金额为 0 所需钱币的个数一定是 0,则 dp[0] = 0;其次,由递推公式可知非 0 下标对应的数值 dp[j] 必须初始化为一个最大的数,否则就会在 min(dp[j - coins[i]] + 1, dp[j]) 比较的过程中被初始值覆盖。所以下标非 0 的元素都是应该是最大值。
      • 确定遍历顺序:本题求钱币最小个数,那么钱币有顺序和没有顺序都可以,都不影响钱币的最小个数。所以本题采用外层 for 循环遍历物品,内层 for 遍历背包或者外层 for 遍历背包,内层 for 循环遍历物品都可以的,内循环正序。
      • 举例推导 dp 数组:以输入 coins = [1, 2, 5], amount = 5 为例,如图:
        请添加图片描述
  • 先遍历物品 后遍历背包
###先遍历物品 后遍历背包
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        dp = [float('inf')] * (amount + 1)  # 创建动态规划数组,初始值为正无穷大
        dp[0] = 0  # 初始化背包容量为0时的最小硬币数量为0

        for coin in coins:  # 遍历硬币列表,相当于遍历物品
            for i in range(coin, amount + 1):  # 遍历背包容量
                if dp[i - coin] != float('inf'):  # 如果dp[i - coin]不是初始值,则进行状态转移
                    dp[i] = min(dp[i - coin] + 1, dp[i])  # 更新最小硬币数量

        if dp[amount] == float('inf'):  # 如果最终背包容量的最小硬币数量仍为正无穷大,表示无解
            return -1
        return dp[amount]  # 返回背包容量为amount时的最小硬币数量

###先遍历物品 后遍历背包(优化版)
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        dp = [float('inf')] * (amount + 1)
        dp[0] = 0

        for coin in coins:
            for i in range(coin, amount + 1): # 进行优化,从能装得下的背包开始计算,则不需要进行比较
                # 更新凑成金额 i 所需的最少硬币数量
                dp[i] = min(dp[i], dp[i - coin] + 1)

        return dp[amount] if dp[amount] != float('inf') else -1

  • 先遍历背包 后遍历物品
###先遍历背包 后遍历物品
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        dp = [float('inf')] * (amount + 1)  # 创建动态规划数组,初始值为正无穷大
        dp[0] = 0  # 初始化背包容量为0时的最小硬币数量为0

        for i in range(1, amount + 1):  # 遍历背包容量
            for j in range(len(coins)):  # 遍历硬币列表,相当于遍历物品
                if i - coins[j] >= 0 and dp[i - coins[j]] != float('inf'):  # 如果dp[i - coins[j]]不是初始值,则进行状态转移
                    dp[i] = min(dp[i - coins[j]] + 1, dp[i])  # 更新最小硬币数量

        if dp[amount] == float('inf'):  # 如果最终背包容量的最小硬币数量仍为正无穷大,表示无解
            return -1
        return dp[amount]  # 返回背包容量为amount时的最小硬币数量

###先遍历背包 后遍历物品(优化版)
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        dp = [float('inf')] * (amount + 1)
        dp[0] = 0

        for i in range(1, amount + 1):  # 遍历背包容量
            for coin in coins:  # 遍历物品
                if i - coin >= 0:
                    # 更新凑成金额 i 所需的最少硬币数量
                    dp[i] = min(dp[i], dp[i - coin] + 1)

        return dp[amount] if dp[amount] != float('inf') else -1
        

二、完全平方数

相关题目:Leetcode279
文档讲解:Leetcode279
视频讲解:Leetcode279

1. Leetcode279.完全平方数

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
提示:

  • 1 <= n <= 10^4
  • 思路:
    • 本题可以转化为完全背包问题:完全平方数就是物品(可以无限件使用),凑个正整数 n 就是背包,问凑满这个背包最少有多少物品?
    • 动规五部曲
      • 确定 dp 数组以及下标的含义:dp[j] 表示和为 j 的完全平方数的最少数量为 dp[j]。
      • 确定递推公式: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])
      • dp 数组如何初始化:dp[0] 表和为 0 的完全平方数的最小数量,那么 dp[0] 一定是 0。注意题目描述中没提从 0 开始,dp[0] = 0 完全是为了递推公式而初始化。从递归公式中可以看出每次 dp[j] 都要选最小的,所以非 0 下标的 dp[j] 一定要初始为最大值,这样 dp[j] 在递推的时候才不会被初始值覆盖。
      • 确定遍历顺序:与 Leetcode322. 零钱兑换 一样,本题是求最小数,两个 for 循环遍历顺序不影响结果。
      • 举例推导 dp 数组:以输入 n = 5 为例,dp 状态图如下:
        请添加图片描述
  • 先遍历背包, 再遍历物品
class Solution:
    def numSquares(self, n: int) -> int:
        dp = [float('inf')] * (n + 1)
        dp[0] = 0

        for i in range(1, n + 1):  # 遍历背包
            for j in range(1, int(i ** 0.5) + 1):  # 遍历物品
                # 更新凑成数字 i 所需的最少完全平方数数量
                dp[i] = min(dp[i], dp[i - j * j] + 1)

        return dp[n]

  • 先遍历物品, 再遍历背包
class Solution:
    def numSquares(self, n: int) -> int:
        dp = [float('inf')] * (n + 1)
        dp[0] = 0

        for i in range(1, int(n ** 0.5) + 1):  # 遍历物品
            for j in range(i * i, n + 1):  # 遍历背包
                # 更新凑成数字 j 所需的最少完全平方数数量
                dp[j] = min(dp[j - i * i] + 1, dp[j])

        return dp[n]
        

三、单词拆分

相关题目:Leetcode139
文档讲解:Leetcode139
视频讲解:Leetcode139

1. Leetcode139.单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
提示:

  • 1 <= s.length <= 300
  • 1 <= wordDict.length <= 1000
  • 1 <= wordDict[i].length <= 20
  • s 和 wordDict[i] 仅由小写英文字母组成
  • wordDict 中的所有字符串 互不相同
  • 思路:
    • 本题可转化为完全背包问题:单词就是物品,字符串 s 就是背包,拆分时可以重复使用字典中的单词,单词能否组成字符串 s,就是问物品能不能把背包装满。
    • 动规五部曲
      • 确定 dp 数组以及下标的含义:dp[i] 表示字符串长度为 i 的话,dp[i] 为 true,表示可以拆分为一个或多个在字典中出现的单词。
      • 确定递推公式:如果确定 dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么 dp[i] 一定是 true(j < i )。所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j] 是 true) 那么 dp[i] = true。
      • dp 数组如何初始化:从递推公式可以看出 dp[i] 的状态依靠 dp[j] 是否为 true,那么 dp[0] 一定要为 true,否则递推下去后面都都是 false。dp[0] 表示如果字符串为空的话,说明出现在字典里。但题目中说了 1 <= s.length <= 300,测试数据中不会出现 i 为 0 的情况,所以 dp[0] 初始为 true 完全就是为了推导公式。下标非 0 的 dp[i] 初始化为 false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词。
      • 确定遍历顺序:本题其实求的是排列数,因为物品(即单词)之间顺序会影响能不能组成字符串。所以本题需要遍历单词的排列,顺序为先遍历背包,再遍历物品。
      • 举例推导 dp[i]:以输入 s = “leetcode”, wordDict = [“leet”, “code”] 为例,dp 状态如图:
        请添加图片描述
  • 完全背包
###DP(版本一)
class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        wordSet = set(wordDict)
        n = len(s)
        dp = [False] * (n + 1)  # dp[i] 表示字符串的前 i 个字符是否可以被拆分成单词
        dp[0] = True  # 初始状态,空字符串可以被拆分成单词

        for i in range(1, n + 1): # 遍历背包
            for j in range(i): # 遍历单词
                if dp[j] and s[j:i] in wordSet:
                    dp[i] = True  # 如果 s[0:j] 可以被拆分成单词,并且 s[j:i] 在单词集合中存在,则 s[0:i] 可以被拆分成单词
                    break

        return dp[n]

###DP(版本二)
class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        dp = [False]*(len(s) + 1)
        dp[0] = True
        # 遍历背包
        for j in range(1, len(s) + 1):
            # 遍历单词
            for word in wordDict:
                if j >= len(word):
                    dp[j] = dp[j] or (dp[j - len(word)] and word == s[j - len(word):j])
        return dp[len(s)]

###DP(剪枝)
class Solution(object):
    def wordBreak(self, s, wordDict):

        # 先对单词按长度排序
        wordDict.sort(key=lambda x: len(x))
        n = len(s)
        dp = [False] * (n + 1)
        dp[0] = True
        # 遍历背包
        for i in range(1, n + 1):
            # 遍历单词
            for word in wordDict:
                # 简单的 “剪枝”
                if len(word) > i:
                    break
                dp[i] = dp[i] or (dp[i - len(word)] and s[i - len(word): i] == word)
        return dp[-1]
        
  • 回溯
class Solution:
    def backtracking(self, s: str, wordSet: set[str], startIndex: int) -> bool:
        # 边界情况:已经遍历到字符串末尾,返回True
        if startIndex >= len(s):
            return True

        # 遍历所有可能的拆分位置
        for i in range(startIndex, len(s)):
            word = s[startIndex:i + 1]  # 截取子串
            if word in wordSet and self.backtracking(s, wordSet, i + 1):
                # 如果截取的子串在字典中,并且后续部分也可以被拆分成单词,返回True
                return True

        # 无法进行有效拆分,返回False
        return False

    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        wordSet = set(wordDict)  # 转换为哈希集合,提高查找效率
        return self.backtracking(s, wordSet, 0)
        

四、多重背包问题

相关题目:卡码网56. 携带矿石资源
文档讲解:多重背包

1. 卡码网56. 携带矿石资源

你是一名宇航员,即将前往一个遥远的行星。在这个行星上,有许多不同类型的矿石资源,每种矿石都有不同的重要性和价值。你需要选择哪些矿石带回地球,但你的宇航舱有一定的容量限制。 给定一个宇航舱,最大容量为 C。现在有 N 种不同类型的矿石,每种矿石有一个重量 w[i],一个价值 v[i],以及最多 k[i] 个可用。不同类型的矿石在地球上的市场价值不同。你需要计算如何在不超过宇航舱容量的情况下,最大化你所能获取的总价值。
输入描述:
输入共包括四行,第一行包含两个整数 C 和 N,分别表示宇航舱的容量和矿石的种类数量。
接下来的三行,每行包含 N 个正整数。具体如下:
第二行包含 N 个整数,表示 N 种矿石的重量。
第三行包含 N 个整数,表示 N 种矿石的价格。
第四行包含 N 个整数,表示 N 种矿石的可用数量上限。
输出描述:
输出一个整数,代表获取的最大价值。
提示:

  • 1 <= C <= 2000;
  • 1 <= N <= 100;
  • 1 <= w[i], v[i], k[i] <= 1000;
  • 多重背包问题:有 N 种物品和一个容量为 V 的背包。第i种物品最多有 M i M_i Mi 件可用,每件耗费的空间是 C i C_i Ci ,价值是 W i W_i Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。
    • 多重背包和 01背包是非常像,每件物品最多有 M i M_i Mi 件可用,把 M i M_i Mi 件摊开,其实就是一个 01背包问题。例:背包最大重量为 10,物品为:
重量价值数量
物品01152
物品13203
物品24302

其可以转化为:

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

这就转成了一个 01背包问题,且每个物品只用一次,然后便可以用 01背包问题的解法进行求解。

  • 多重背包
C, N = input().split(" ")
C, N = int(C), int(N)

# value数组需要判断一下非空不然过不了
weights = [int(x) for x in input().split(" ")]
values = [int(x) for x in input().split(" ") if x]
nums = [int(x) for x in input().split(" ")]

dp = [0] * (C + 1)
# 遍历背包容量
for i in range(N):
    for j in range(C, weights[i] - 1, -1):
        for k in range(1, nums[i] + 1):
            # 遍历 k,如果已经大于背包容量直接跳出循环
            if k * weights[i] > j:
                break
            dp[j] = max(dp[j], dp[j - weights[i] * k] + values[i] * k) 
print(dp[-1])

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值