美文网首页
leetcode「组合」题目汇总 回溯法

leetcode「组合」题目汇总 回溯法

作者: winter_sweetie | 来源:发表于2020-04-30 21:16 被阅读0次

    2020/4/30

    39. 组合总和

    题意
    • 在无重复数组candidates中寻找和为target的组合。
    • candidates中的数字可以无限制重复被选取。
    栗子
    输入: candidates = [2,3,6,7], target = 7,
    所求解集为:
    [
      [7],
      [2,2,3]
    ]
    
    关键点
    • 无重复数组:无需去重。
    • 元素可以重复选取:递归的时候i不用加1。

    回溯要素

    • 选择:candidates[k, len(candidates) - 1]中任一元素
    • 终止条件:
      • 情况1:找到这样的组合,即target == 0
      • 情况2:此路不通,即target < 0
    代码
    class Solution:
        def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
            res = []
            subset = []
            self.backtrack(candidates, 0, subset, res, target)
            return res
    
        def backtrack(self, candidates, k, subset, res, target):
            if target == 0:
                res.append(subset[:])
                return
            if target < 0:
                return
    
            for i in range(k, len(candidates)):
                # 如果candidates有重复元素,需要加一句:
                # if i != k and candidates[i] == candidates[i - 1]:
                # continue
                # 当然,前提是candidates有序
                subset.append(candidates[i])
                self.backtrack(candidates, i, subset, res, target - candidates[i])
                # 如果元素不可以重复选取,需要改成
                # self.backtrack(candidates, i + 1, subset, res, target - candidates[i])
                del subset[-1]
    

    40. 组合总和 II

    题意
    • 在数组candidates中寻找和为target的组合。
    • candidates中的每个数字在每个组合中只能使用一次。
    栗子
    输入: candidates = [10,1,2,7,6,1,5], target = 8,
    所求解集为:
    [
      [1, 7],
      [1, 2, 5],
      [2, 6],
      [1, 1, 6]
    ]
    
    关键点:刚好和上道题相反
    • 有重复数组:需要去重。
    • 元素不能重复选取:递归的时候i要加1。

    回溯要素

    • 选择:candidates[k, len(candidates) - 1]中的元素,但该层不能有重复元素
    • 终止条件:
      • 情况1:找到这样的组合,即target == 0
      • 情况2:此路不通,即target < 0
    代码
    class Solution:
        def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
            res = []
            subset = []
            # 先排序
            candidates.sort()
            self.backtrack(candidates, 0, subset, res, target)
            return res
    
        def backtrack(self, candidates, k, subset, res, target):
            if target == 0:
                res.append(subset[:])
                return
            
            if target < 0:
                return
            for i in range(k, len(candidates)):
                # 去重/剪枝
                if i != k and candidates[i] == candidates[i - 1]:
                    continue
                subset.append(candidates[i])
                # 因为candidates的每个数字只能使用一次,所以i + 1
                self.backtrack(candidates, i + 1, subset, res, target - candidates[i])
                del subset[-1]
    

    216. 组合总和 III

    题意
    • 找出所有相加之和为nk个数的组合。
    • 组合中只允许含有 1 - 9 的正整数
    • 每种组合中不存在重复的数字。
    栗子
    输入: k = 3, n = 9
    输出: [[1,2,6], [1,3,5], [2,3,4]]
    
    关键点
    • 元素不能重复选取:递归的时候i要加1。

    回溯要素

    • 选择:[m, 9]中的元素
    • 终止条件:
      • 情况1:找到这样的组合,即k == len(combination)n == 0
      • 情况2:此路不通,即k == len(combination)n != 0,或者k != len(combination)n == 0
    代码
    class Solution:
        def combinationSum3(self, k: int, n: int) -> List[List[int]]:
            combination = []
            res = []
            self.backtrack(k, n, 1, combination, res)
            return res
    
        def backtrack(self, k, n, m, combination, res):
            if k == len(combination):
                if n == 0:
                    res.append(combination[:])
                return
            
            for i in range(m, 10):
                combination.append(i)
                self.backtrack(k, n - i, i + 1, combination, res)
                del combination[-1]
    

    377. 组合总和 Ⅳ

    题意
    • 给定一个不存在重复数字的数组nums,找出和为target的组合的个数。
    • 顺序不同的序列被视作不同的组合。
    栗子
    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。
    
    关键点
    • nums不存在重复数字:不用去重
    • 元素不仅可以重复选取,而且可以不按顺序:遍历i的时候从0len(nums)-1
    • 求个数,不用返回具体的数组:return 个数,不用传递combinationres了。
    回溯要素
    • 选择:nums[0, len(nums)-1]的任一元素
    • 终止条件:
      • 情况1:找到这样的组合,即target == 0
      • 情况2:此路不通,即target < 0
    算法优化

    使用哈希映射表mp来存储{target: times},避免重复计算。

    代码
    from collections import defaultdict
    class Solution:
        def combinationSum4(self, nums: List[int], target: int) -> int:
            self.res = 0
            mp = defaultdict(int)
            return self.backtrack(nums, target, mp)
    
        def backtrack(self, nums, target, mp):
            if target in mp:
                return mp[target]
            if target == 0:
                self.res += 1 
                return 1
            if target < 0:
                return 0
            
            res = 0
            for i in range(len(nums)):
                res += self.backtrack(nums, target - nums[i], mp)
            mp[target] = res
            return res
    
    dp方法
    class Solution:
        def combinationSum4(self, nums: List[int], target: int) -> int:
            dp = [0] * (target + 1)
            # dp[0]初始化为1
            dp[0] = 1
            # 循环顺序一定不能乱
            for i in range(1, target + 1):
                for j in range(len(nums)):
                    if nums[j] > i:
                        continue
                    dp[i] += dp[i - nums[j]]
            return dp[target]
    

    518. 零钱兑换 II

    • 给定一个不存在重复数字的数组coins,找出和为amount的组合的个数。
    • coins可重复使用。
    • 顺序不同的序列视为相同的组合。
    栗子
    输入: amount = 5, coins = [1, 2, 5]
    输出: 4
    解释: 有四种方式可以凑成总金额:
    5=5
    5=2+2+1
    5=2+1+1+1
    5=1+1+1+1+1
    
    tricky之处
    • 如果延续377. 组合总和 Ⅳ的思路:
      • 上题是求排列,这题是求组合。
      • 既然上题是从在arr[0, len(arr) - 1]的区间进行枚举,那么这道题在arr[i, len(arr)-1]的区间不就好了吗。左边界从0变为i,表示会选择递增的元素,把这道题变成组合问题;左边界不是i + 1,因为硬币可以拿多次。
    • 看起来算法是完全正确的。但是呢...
      • 我们这道题不是返回combination,而是返回个数,如果不使用hashmap,就会陷入超时。而如果使用了hashmap,算法就不正确了。
      • 下面给出一段错误代码。注意,这段代码和377. 组合总和 Ⅳ的结果一样,左边界是i还是0完全失去效力。如果不使用mp进行缓存,结果就正确了。
    错误代码
    from collections import defaultdict
    
    class Solution:
        def change(self, amount: int, coins: List[int]) -> int:
            mp = defaultdict(int)
            return self.dfs(amount, coins, 0, mp)
    
        def dfs(self, amount, coins, k, mp):
            if amount in mp:
                return mp[amount]
            if amount == 0:
                return 1
            if amount < 0:
                return 0
            
            res = 0
            # 写成range(len(coins))是一样的
            for i in range(k, len(coins)):
                res += self.dfs(amount - coins[i], coins, i, mp)
            
            mp[amount] = res
            return res
    
    解释
    • 错误原因
      • 上段代码的决策树如图左所示。尽管枚举时索引是非递减的,但加入mp后就不是这样了。
      • 我们看虚线框,这里mp缓存的是{2: 2},当金额为2时,有两种硬币组合方法。那看中间的虚线框处,将[1,2,1,1]也计算在内,就打破了非递减的特性。
      • 所以,这种思路是不正确的。


        示意图
    正确做法
    • 如图右的决策树所示,共coins.length层。
    • 选择:coins[k]拿多少个
    • 终止条件:
      • 情况1:找到这样的组合,即amount == 0
      • 情况2:此路不通,即amount < 0
    • mp{(k, amount): times}的映射
    正确代码
    from collections import defaultdict
    
    class Solution:
        def change(self, amount: int, coins: List[int]) -> int:
            mp = defaultdict(int)
            return self.dfs(amount, coins, 0, mp)
    
        def dfs(self, amount, coins, k, mp):
            if k == len(coins):
                if amount == 0:
                    return 1
                return 0
            if amount < 0:
                return 0
            if (k, amount) in mp:
                return mp[(k, amount)]
            
            res = 0
            for i in range(amount // coins[k] + 1): # 注意加1
                res += self.dfs(amount - i * coins[k], coins, k + 1, mp)
            
            mp[(k, amount)] = res
            return res
    
    dp方法
    class Solution:
        def coinChange(self, coins: List[int], amount: int) -> int:
            dp = [float('inf')] * (amount + 1)
            dp[0] = 0
            for i in range(amount + 1):
                for c in coins:
                    if i - c < 0:
                        continue # 不能break,因为硬币不一定升序
                    dp[i] = min(dp[i], dp[i-c] + 1)
            return dp[-1] if dp[-1] != float('inf') else -1
    

    77. 组合

    题意
    • 找出1 ... n中所有可能的 k 个数的组合。
    • 每种组合中不存在重复的数字。
    栗子
    输入: n = 4, k = 2
    输出:
    [
      [2,4],
      [3,4],
      [2,3],
      [1,2],
      [1,3],
      [1,4],
    ]
    
    关键点
    • 元素不能重复选取:递归的时候i要加1。
    回溯要素
    • 选择:nums[1, n]中的元素,倒着找
    • 终止条件:
      • 情况1:找到这样的组合,即k == 0
      • 情况2:此路不通,即n < k
    代码
    class Solution:
        def combine(self, n: int, k: int) -> List[List[int]]:
            combination = []
            res = []
            self.backtrack(n, k, combination, res)
            return res
            
        def backtrack(self, n, k, combination, res):
            if k == 0:
                res.append(combination[:])
                return
            
            if n < k:
                return 
    
            for i in range(n, 0, -1):
                combination.append(i)
                self.backtrack(i - 1, k - 1, combination, res)
                del combination[-1]
    

    17. 电话号码的字母组合

    题意
    • 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
    • 给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。


    栗子
    输入:"23"
    输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
    
    回溯要素
    • 终止条件:遍历完digits字符串,即k == len(digits)
    • 选择:该数字对应的字母
    代码
    class Solution:
        def letterCombinations(self, digits: str) -> List[str]:
            if len(digits) == 0:
                return []
            mp = {'2': ['a', 'b', 'c'], '3': ['d', 'e', 'f'], '4': ['g', 'h', 'i'], '5': ['j', 'k', 'l'], '6': ['m', 'n', 'o'], '7': ['p', 'q', 'r', 's'], '8': ['t', 'u', 'v'], '9': ['w', 'x', 'y', 'z']}
            res = []
            self.backtrack(digits, 0, mp, '', res)
            return res
    
        
        def backtrack(self, digits, k, mp, combination, res):
            if k == len(digits):
                res.append(combination) #string是值传递,不是引用传递
                return
            for ch in mp[digits[k]]:
                self.backtrack(digits, k + 1, mp, combination + ch, res)
    

    相关文章

      网友评论

          本文标题:leetcode「组合」题目汇总 回溯法

          本文链接:https://www.haomeiwen.com/subject/tqptghtx.html