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) ≤ 4digits[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) ≤ 302 ≤ candidates[i] ≤ 40,元素互不相同1 ≤ target ≤ 40
教学图解
资源 Leetcode/0406_2.png:
代码(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)。

浙公网安备 33010602011771号