leetcode背包问题的进一步总结

后续的背包问题的优化:其实二维转一维,和三维转二维都是因为记录了物品的维度,其实只要遍历了所有的物品即可,无需记录,重点是上一个物品得到的结果在给当前物品的使用的时候如何进行。
如果是0-1背包问题,那么是逆序for j in range(amount,-1,-1):
因为 dp[j] = max(dp[j-nums[i]], dp[j]),使用到了第i-1个物品的信息。
但是如果是完全背包问题,那么直接使用正序即可,因为使用的是当前物品的信息—当前物体可重复选择。
for j in range(amount):
dp[j] = max(dp[j-nums[i]], dp[j])
但是为了面试,最好还是先高维,然后再优化,体现进步。

Leetcode常见的背包问题分为三类。
1、组合问题。
2、True、False问题。
3、最大最小问题。

1、组合问题:
377. 组合总和 Ⅳ
494. 目标和
518. 零钱兑换 II
2、True、False问题:
139. 单词拆分
416. 分割等和子集
3、最大最小问题:
474. 一和零
322. 零钱兑换

组合问题公式
dp[i] += dp[i-num]
True、False问题公式
dp[i] = dp[i] or dp[i-num]
最大最小问题公式
dp[i] = min(dp[i], dp[i-num]+1)或者dp[i] = max(dp[i], dp[i-num]+1)
以上三组公式是解决对应问题的核心公式。
当然拿到问题后,需要做到以下几个步骤:

1.分析是否为背包问题。
2.是以上三种背包问题中的哪一种。
3.是0-1背包问题还是完全背包问题。也就是题目给的nums数组中的元素是否可以重复使用。
4.如果是组合问题,是否需要考虑元素之间的顺序。需要考虑顺序有顺序的解法,不需要考虑顺序又有对应的解法。

1、组合问题:

  1. 组合总和 Ⅳ
  2. 目标和
  3. 零钱兑换 II
    组合问题重要的是与顺序是否有关,与顺序有关的话像下方类似的爬楼梯的例子。无关的话就像零钱兑换的例子。

组合总和 Ⅳ
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
示例:nums = [1, 2, 3], target = 4
所有可能的组合为:
(1, 1, 1, 1)(1, 1, 2)(1, 2, 1)(1, 3)(2, 1, 1)(2, 2)(3, 1)
请注意,顺序不同的序列被视作不同的组合。因此输出为 7。

dp[j] += dp[j-num[i]]

# 该问题可以视为70.爬楼梯问题的升级版本。直接转化为如下问题:
# 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
# 每次你可以爬num (num in nums)级台阶。你有多少种不同的方法可以爬到楼顶呢?

#   牢记@labuladong东哥的动态规划解题魔咒:
# 1 明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义。
# 2 base case: dp[0] = 1: 想要达到第0级,只有不跳这一种方法。
# 3 明确状态: 该问题只有一格状态,就是当前台阶级数,i从0到target。
# 4 明确选择: 选择就是选择跳num级。

# 定义dp数组含义:dp[i]定义:一个人跳台阶,每次可以选择跳num阶(num in nums),他要跳到第i级台阶总共有多少种跳法。显然,跳到第i级台阶的方法数为跳到 dp[i-num] for num in nums的方法数之和,因为他只要跳到第i-num级,再一步跳num级,就可以到第i级了。
class Solution:
    def combinationSum4(self, nums: List[int], target: int) -> int:
        if not nums:
            return 0
        dp = []
        dp = [0]*(target+1)
        dp[0] = 1 # 每次可以上几阶,这里都不上,算一种方法。
        for i in range(target+1):
            for num in nums:
                if i - num >= 0:
                    dp[i] += dp[i-num] # 
        return dp[-1]

其实这一题和上一题一模一样~~
零钱兑换 II
解释一下为什么不会出现[1,2]–[2,1]的重复组合情况:
nums[i]是按顺序取的,不会出现取了2再取1的情况,肯定是取了1再继续探究是否取2.
dp[i][j] = dp[i-1][j] + dp[i][j-nums[i]]
dp[i][0] = 1
dp[0][i] = 1
dp[1][j] = dp[0][j] + dp[1][j-nums[i]]
dp[1][1] = dp[0][1] = 1
dp[1][2] = dp[0][2] + dp[1][2-2] = 1 + 1 = 2
dp[1][3] = dp[0][3] + dp[1][3-2] = 1 + 1 = 2

nums: [1, 2]
如果nums是在内循环:
dp[0][i] = 1
dp[i][0] = if i % nums[0] == 0: 1 else: 0 ------ 此时都是1
dp[1][1] = dp[1][0] + dp[1-nums[1]][1] = 1
dp[1][2] = d[0][2] + dp[1][0] = 1 + 1 = 2
dp[1][3] = dp[0][3] + dp[1][1] = 2

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 1:
输入: amount = 5, coins = [1, 2, 5] 输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
示例 2:

输入: amount = 3, coins = [2]
输出: 0
解释: 只用面额2的硬币不能凑成总金额3。

这个就是真正的完全背包问题了,dp[i][j]指的是每个j最大的组合数
dp[i][j]代表使用前i枚硬币可以拼凑出amount为j的最大组合数。

def change(self, amount: int, coins: List[int]) -> int:
        # dp[i][j]代表每个amount最大的组合数
        if not coins:
            if amount == 0:
                return 1
            else:
                return 0

        dp = []
        for i in range(len(coins)):
            dp.append([0]*(amount+1))
        
        for i in range(1, amount+1):
            if i % coins[0] == 0:
                dp[0][i] = 1
        
        for i in range(len(coins)):
            dp[i][0] = 1

        for i in range(1, len(coins)):
            for j in range(1, amount+1):
                if j - coins[i] >= 0:
                    dp[i][j] = dp[i][j-coins[i]] + dp[i-1][j] 
                else:
                    dp[i][j] = dp[i-1][j]
        
        return dp[-1][-1]

二维转一维,优化之后就是dp[i] += dp[i-nums[i]]

class Solution:
    def change(self, amount: int, coins: List[int]) -> int:

        if not coins:
            if amount == 0:
                return 1
            else:
                return 0

        dp = [0]*(amount+1)

        # 注意一下 basecase, 0的地方是1,虽然解释不同,但是列表可以发现是必须的!!
        for i in range(0, amount+1):
            if i % coins[0] == 0:
                dp[i] = 1

        for i in range(1, len(coins)):
            for j in range(1,amount+1):
                if j - coins[i] >= 0:
                    dp[j] = dp[j-coins[i]] + dp[j] 
        
        return dp[-1]

2、True、False问题:

139. 单词拆分
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:

拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
示例 1:

输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。
示例 2:

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

首先使用DP算法:

初始化 dp=[False],长度为 n+1。n 为字符串长度。dp[i] 表示 ss 的前 i 位是否可以用 wordDict 中的单词表示。
初始化 dp[0]=True 空字符可以被表示。
遍历字符串的所有子串,遍历开始索引 i,遍历区间 [0,n):
遍历结束索引 j,遍历区间 [i+1,n+1):
若 dp[i]=True 且s[i,⋯,j) 在 wordlist 中:dp[j]=True。解释:dp[i]=True 说明 s 的前 i 位可以用 wordDict 表示,则s[i,⋯,j) 出现在 wordDict中,说明 s 的前 j 位可以表示。返回 dp[n]

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool: 
        dp = [0]*(len(s)+1)
        dp[0] = 1
        for i in range(len(s)):
            for j in range(i+1, len(s)+1):
                if dp[i] and (s[i:j] in wordDict):
                    dp[j] = 1
        if dp[-1]:
            return True
        else:
            return False

注意:
import functools
@functoolls.lru_cache(None)
装饰器可以实现调用缓存的作用,这个对于重复的调用非常有用,对于一些自顶向下的DP来说非常友好。–另一个博客有进行记录。

回溯的方法:从某一个开始,便不断往后移动,如果无法继续往后,则回溯。

import functools
class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool: 
        import functools
        @functools.lru_cache(None)
        def backstrack(s):
            if not s:
                return True
            for j in range(len(s)+1):
                if s[0:j] in wordDict:
                    res = backstrack(s[j:])
                    if res:
                        return True
            return False
        return backstrack(s)

回溯使用的缓存机制有效的case:
#下面的装饰器用在重复的case,来调用缓存还不错:
#“aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab”
#[“a”,“aa”,“aaa”,“aaaa”,“aaaaa”,“aaaaaa”,“aaaaaaa”,“aaaaaaaa”,“aaaaaaaaa”,“aaaaaaaaaa”]

416. 分割等和子集

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
每个数组中的元素不会超过 100 数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true 解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
输入: [1, 2, 3, 5]
输出: false 解释: 数组不能分割成两个元素和相等的子集.

分割成两个相等的子集,也就是背包的容量是一定的,现在是要求能否装满,属于0-1背包问题,使用的是True/False来进行解决

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        total_sum = sum(nums)
        n = len(nums)
        if total_sum%2==1:
            return False
        
        half_sum = total_sum//2
        dp = [[0]*(half_sum+1) for i in range(n)]

        ### 初始化条件 ###
        if nums[0]<=half_sum:
            dp[0][nums[0]]=1 # 容量为第一个的时候,肯定能装满
        
        for i in range(n):
            dp[i][0]=1   # 容量为0的时候,肯定能装满

        for i in range(1,n):

            if dp[i][-1] == 1:   # 剪枝,只要满足了最后一列,也就是half,提前满足,可以跳出。
                return True
            
            for j in range(half_sum+1):
                dp[i][j] = dp[i-1][j]  # 如果背包装不下
                if nums[i]<=j:         # 如果背包装得下
                     dp[i][j] = dp[i-1][j] or dp[i-1][j-nums[i]]
            # dp[][:] = dp[1][:] # 注意,不可以dp[1] = dp[2]
                
        if dp[-1][-1]:
            return True
        else:
            return False

转换一维,二维转一维只要注意遍历的顺序逆序就好了。

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        total_sum = sum(nums)
        n = len(nums)
        if total_sum%2==1:
            return False
        
        half_sum = total_sum//2
        dp = [0]*(half_sum+1)

        ### 初始化条件 ###
        if nums[0]<=half_sum:
            dp[nums[0]]=1 # 容量为第一个的时候,肯定能装满
        
        dp[0]=1   # 容量为0的时候,肯定能装满

        for i in range(1,n):
            if dp[-1] == 1:   # 剪枝,只要满足了最后一列,也就是half,提前满足,可以跳出。
                return True
            for j in range(half_sum, -1, -1):
                if nums[i]<=j:         # 如果背包装得下
                     dp[j] = dp[j] or dp[j-nums[i]]
                
        if dp[-1]:
            return True
        else:
            return False

3、最大最小问题:

最大最小问题公式
dp[i] = min(dp[i], dp[i-num]+1)或者dp[i] = max(dp[i], dp[i-num]+1)

474. 一和零

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

示例 1:
输入:strs = [“10”, “0001”, “111001”, “1”, “0”], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {“10”,“0001”,“1”,“0”} ,因此答案是 4 。
其他满足题意但较小的子集包括 {“0001”,“1”} 和 {“10”,“1”,“0”} 。{“111001”} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
示例 2:
输入:strs = [“10”, “0”, “1”], m = 1, n = 1
输出:2
解释:最大的子集是 {“0”, “1”} ,所以答案是 2 。

##这道题除了使用最大最小之外,也使用了二维的数组,二维的0-1背包问题而已

class Solution:
    #  三维转二维!!!
    # 最大子集,集合里的元素越多越好。不能超过条件,就是背包问题,容量上限就是1和0的个数,每个元素都可以决定添加与否。转移
    # dp[i][j][k]代表子集数目
    def findMaxForm(self, strs, m, n):

        def sum_zo(num):
            out_one = 0
            out_zero = 0
            for i in num:
                if i == "0":
                    out_zero += 1
                elif i == "1":
                    out_one += 1
            return out_zero, out_one

        if not strs:
            return 0

        out_zero, out_one = sum_zo(strs[0])

        if len(strs) == 1:
            if out_zero <= m and out_one <= n:
                return 1
            else:
                return 0

        dp1 = []
        for j in range(m + 1):
            sub = [0]*(n+1)
            dp1.append(sub)

        if out_zero <= m and out_one <= n:
            dp1[out_zero][out_one] = 1

        for stri in range(1, len(strs)):
            out_zero, out_one = sum_zo(strs[stri])
            for j in range(m, -1, -1):
                for k in range(n, -1, -1):
                    if j - out_zero >= 0 and k - out_one >= 0:
                        dp1[j][k] = max(dp1[j - out_zero][k - out_one] + 1, dp1[j][k])

        res = 0
        for i in dp1:
            if max(i) > res:
                res = max(i)
        return res

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
先弄二维的情况:
注意basecase一定要对,以及边界条件。

# dp[i][j]代表的是前i个物品背包容量为j时,最少的硬币个数
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        if not amount:
            return 0
        if not coins:
            return -1

        dp = []
        for i in range(len(coins)):
            dp.append([amount+1] * (amount+1))

        for i in range(0, amount+1):  #注意0的地方是0,不是的话,会出现问题,后面dp[5][5] = dp[5][0] + 1,basecase一定要确认好!!!!
            if i % coins[0] == 0:
                dp[0][i] = i // coins[0]
        
        for i in range(1, len(coins)):
            for j in range(amount+1):
                dp[i][j] = dp[i-1][j]
                if j - coins[i] >= 0:
                    dp[i][j] = min(dp[i][j-coins[i]]+1, dp[i][j])
        
        
        if dp[-1][-1] == amount+1:
            return -1
        else:
            return dp[-1][-1]

一维,注意完全背包就是不需要逆序,所以遍历的顺序还是和二维是一样的。

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        if not amount:
            return 0
        if not coins:
            return -1

        dp = [amount+1] * (amount+1)

        for i in range(0, amount+1):  #注意0的地方是0,不是的话,会出现问题,后面dp[5][5] = dp[5][0] + 1,basecase一定要确认好!!!!
            if i % coins[0] == 0:
                dp[i] = i // coins[0]
        
        for i in range(1, len(coins)):
            for j in range(amount+1):
                if j - coins[i] >= 0:
                    dp[j] = min(dp[j-coins[i]]+1, dp[j])
        
        
        if dp[-1] == amount+1:
            return -1
        else:
            return dp[-1]
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值