Leetcode 回溯 2

题号直达: 17 电话号码的字母组合 | 39 组合总和 | 22 括号生成

LeetCode 17. 电话号码的字母组合 ↑

题目

给定一个仅包含数字 2–9 的字符串 digits,返回它能表示的所有字母组合。答案可以按任意顺序返回。

数字到字母的映射与手机九宫格键盘相同(1 不对应任何字母)。映射如下:

数字 字母
2 abc
3 def
4 ghi
5 jkl
6 mno
7 pqrs
8 tuv
9 wxyz

键盘示意图(0406.png`):

电话键盘数字与字母对应示意

测试样例

  • 示例 1: digits = "23"
    输出:["ad","ae","af","bd","be","bf","cd","ce","cf"](顺序可任意)

  • 示例 2: digits = "2"
    输出:["a","b","c"]

提示

  • 1 ≤ len(digits) ≤ 4
  • digits[i] 为 ['2','9'] 中的数字

代码(Python)解法一:混合进制枚举

第 j 位数字对应 choices[j] 种字母,全体组合个数为各 choices[j] 之积。将 0 … total-1 的整数 i 按混合进制分解:从右到左依次对每一位做 temp % choices[j] 取下标,再 temp //= choices[j],即可唯一对应一种字母组合。最后 result.sort() 仅为输出有序(题目允许任意顺序,可删)。

class Solution(object):
    def letterCombinations(self, digits):
        if not digits:
            return []

        alphabet = {
            "2": "abc",
            "3": "def",
            "4": "ghi",
            "5": "jkl",
            "6": "mno",
            "7": "pqrs",
            "8": "tuv",
            "9": "wxyz",
        }

        choices = [len(alphabet[d]) for d in digits]

        result = []
        total = 1
        for c in choices:
            total *= c

        for i in range(total):
            temp = i
            combo = []
            for j in range(len(digits) - 1, -1, -1):
                combo.append(alphabet[digits[j]][temp % choices[j]])
                temp //= choices[j]
            result.append("".join(reversed(combo)))

        result.sort()
        return result

思路解析(解法一)

  • 混合进制:若每位独立选字母,总方案数为 ∏ choices[j];枚举 i ∈ [0, total) 相当于从 0 数到 total-1,在「每位基数不同」的进制下展开,与组合一一对应。
  • 分解顺序:内层从 digits 的最后一位向前取余,与手写多位数进位方向一致;combo 自右向左建好后 reversed 再拼接成字符串。
  • 空串:digits 为空时题目期望 [],需特判。
  • 复杂度:共 total 个组合,每个长度 O(n),时间 O(n · total),其中 total ≤ 4^4 量级(每位最多 4 个字母);空间 O(total · n) 存答案。

代码(Python)解法二:回溯

按 index 依次处理 digits[index] 的每个候选字母;选则 append,递归 index+1,再 pop 撤销。index == len(digits) 时把当前路径拼成字符串加入结果。

class Solution(object):
    def letterCombinations(self, digits):
        """
        :type digits: str
        :rtype: List[str]
        """
        if not digits:
            return []

        phoneMap = {
            "2": "abc",
            "3": "def",
            "4": "ghi",
            "5": "jkl",
            "6": "mno",
            "7": "pqrs",
            "8": "tuv",
            "9": "wxyz",
        }

        def backtrack(index):
            if index == len(digits):
                combinations.append("".join(combination))
            else:
                digit = digits[index]
                for letter in phoneMap[digit]:
                    combination.append(letter)
                    backtrack(index + 1)
                    combination.pop()

        combination = []
        combinations = []
        backtrack(0)
        return combinations

解法二 讲解视频(讲解视频文件 0406_1.mp4)

思路解析(解法二)

  • 深度优先:每条从根到叶的路径对应一种字母组合;index 递增一层,等价于在决策树里向下走。
  • 回溯:append 后递归,返回前 pop,保证同一 combination 列表被复用,不拷贝整段路径。
  • 终止:index == len(digits) 时当前路径已满,用 "".join(combination) 写入。
  • 复杂度:与解法一相同阶,时间 O(n · 4^n) 量级(每位最多 4 个字母);递归栈 O(n)。

LeetCode 39. 组合总和 ↑

题目

给定一个无重复元素的整数数组 candidates 和一个目标整数 target,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的同一个数字可以无限制重复选取;若至少一个数字的选取次数不同,则两种组合视为不同。答案顺序任意。

题目保证对于给定输入,和为 target 的不同组合数少于 150。

测试样例

  • 示例 1: candidates = [2,3,6,7],target = 7
    输出:[[2,2,3],[7]]

  • 示例 2: candidates = [2,3,5],target = 8
    输出:[[2,2,2,2],[2,3,3],[3,5]]

  • 示例 3: candidates = [2],target = 1
    输出:[]

提示

  • 1 ≤ len(candidates) ≤ 30
  • 2 ≤ candidates[i] ≤ 40,元素互不相同
  • 1 ≤ target ≤ 40

教学图解

资源 Leetcode/0406_2.png:

组合总和 回溯与 DFS 思路示意

代码(Python):DFS 回溯

按「当前考虑下标 idx」分支:选 candidates[idx] 则加入 combine,target 减去该数,且 idx 不变(可重复选同一数);不选 则 idx 后移。target == 0 时拷贝一份 combine 入答案;target < 0 或 idx 越界且 target ≠ 0 则剪枝。

class Solution(object):
    def combinationSum(self, candidates, target):
        """
        :type candidates: List[int]
        :type target: int
        :rtype: List[List[int]]
        """

        def dfs(target, combine, idx):
            if target <= 0 or idx > len(candidates) - 1:
                if target == 0:
                    combinations.append(combine[:])
            else:
                combine.append(candidates[idx])
                dfs(target - candidates[idx], combine, idx)
                combine.pop()
                dfs(target, combine, idx + 1)

        combinations = []
        dfs(target, [], 0)
        return combinations

思路解析

  • 去重顺序:固定从 idx 起向后选,避免同一 multiset 用不同顺序重复出现(本题写法通过「先反复选当前下标再进位」自然保证)。
  • 剪枝:target < 0 时由 target <= 0 进入分支且不记录;idx 超出下标时仅当 target == 0 才记录。
  • 复杂度:与合法组合数相关,题目保证组合数 < 150;最坏时间随搜索树指数增长,target 与数组规模较小时可接受。

LeetCode 22. 括号生成 ↑

题目

给定整数 n,表示 n 对括号。请设计函数,生成所有由 n 对括号组成的有效括号组合。答案顺序任意。

测试样例

  • 示例 1: n = 3
    输出:["((()))","(()())","(())()","()(())","()()()"](顺序可任意)

  • 示例 2: n = 1
    输出:["()"]

提示

  • 1 ≤ n ≤ 8

代码(Python)解法一:枚举长为 2n 的串 + 合法性判断

用回溯在每一位尝试 '(' 或 ')',得到长度为 2n 的串;再用 valid 做单次扫描:维护平衡值 bal,遇 '(' 加 1、遇 ')' 减 1,中途 bal < 0 或结束时 bal ≠ 0 则非法。

class Solution(object):
    def generateParenthesis(self, n):
        """
        :type n: int
        :rtype: List[str]
        """

        def generate(A):
            if len(A) == 2 * n:
                if valid(A):
                    ans.append("".join(A))
            else:
                A.append("(")
                generate(A)
                A.pop()
                A.append(")")
                generate(A)
                A.pop()

        def valid(A):
            bal = 0
            for c in A:
                if c == "(":
                    bal += 1
                else:
                    bal -= 1
                if bal < 0:
                    return False
            return bal == 0

        ans = []
        generate([])
        return ans

思路解析(解法一)

  • 枚举:共 2^(2n) 种括号串,每种 O(n) 校验,最坏时间 O(n · 2^(2n)),n ≤ 8 时尚可接受。
  • valid:等价于「任意前缀中 '(' 数量不少于 ')'」,且整串左右数量相等。
  • 改进:可在构造过程中剪枝(左括号数、右括号数与剩余位置),见下方解法二,复杂度可降至与卡特兰数同阶。

代码(Python)解法二:回溯剪枝(左 / 右括号计数)

维护已放左括号个数 left、右括号个数 right。左括号未满 n 时可追加 '(';仅当 right < left 时可追加 ')',保证任意前缀中左括号不少于右括号。长度到 2n 时加入答案。

class Solution(object):
    def generateParenthesis(self, n):
        """
        :type n: int
        :rtype: List[str]
        """
        ans = []

        def backtrack(S, left, right):
            if len(S) == 2 * n:
                ans.append("".join(S))
                return
            if left < n:
                S.append("(")
                backtrack(S, left + 1, right)
                S.pop()
            if right < left:
                S.append(")")
                backtrack(S, left, right + 1)
                S.pop()

        backtrack([], 0, 0)
        return ans

思路解析(解法二)

  • 剪枝:left < n 时尚可放左括号;right < left 时才放右括号,避免出现非法前缀(右括号多于左括号)。
  • 复杂度分析(与题解一致):
    • 合法括号串个数为第 n 个卡特兰数 C_n,满足 C_n = [1/(n+1)] × C(2n, n)(组合数),数量级约为 4^n / (n√n)(√ 表示算术平方根)。
    • 时间:回溯只生成合法串;结果个数与 C_n 同阶,每个串拷贝进答案 O(n),总时间常记为 O(4^n / √n)(与官方题解同阶)。
    • 空间:除返回数组外,递归栈深度 O(2n) = O(n),每层 O(1),故额外空间 O(n)。
posted @ 2026-10-12 00:18  Phier  阅读(3)  评论(0)    收藏  举报