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

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)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335