回溯一般运用于排列组合或者是“选与不选”的情况。遇到组合或者子集的,无脑回溯基本都能解出来。
排列组合
17. 电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
示例 2:
输入:digits = “”
输出:[]
示例 3:
输入:digits = “2”
输出:[“a”,“b”,“c”]
遇到这种组合的题目,首先可以尝试列举一下它的所有可能性,来看看有什么规律。
可以发现,这个组合的路径其实是一颗树,每一条路径就是一个答案。而每条路径可以用DFS来得到答案。这一类回溯题其实本质上就是写一个DFS,只是不是运用在标准的树或者图上。
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
if not digits:
return list()
phoneMap = {
"2": "abc",
"3": "def",
"4": "ghi",
"5": "jkl",
"6": "mno",
"7": "pqrs",
"8": "tuv",
"9": "wxyz",
}
def backtrace(i, cur):
if i == len(digits) -1:
for ch in phoneMap[digits[i]]:
ans.append(cur + ch)
return
#想好怎么结束回溯很重要
else:
for ch in phoneMap[digits[i]]:
backtrace(i+1, cur + ch)
ans = []
backtrace(0, "")
return ans
巩固
拓展
51. N 皇后
这题其实没有看起来那么难,只要洞察了落子后的结果在棋盘上是以何种数学形式表示的,剩下的就是简单的回溯枚举。
140. 单词拆分 II
全排列
给定一个不含重复数字的数组 nums ,返回其所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1]
输出:[[1]]
全排列,只需要每次选则数组中未选过的数加入当前结果中就可以,可以用回溯,即深度优先搜索。
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
ans = []
def backtrack(num, res):
if num == len(nums):
ans.append(res[:])
#如果是ans.append(res),由于传递的是地址,最终结果会为空
return
for i in range(len(nums)):
if nums[i] not in res:
res.append(nums[i])
backtrack(num+1, res)
res.pop()
backtrack(0, [])
return ans
拓展
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
ans = []
def backtrack(idx, num, res):
if num == target:
ans.append(res[:])
return
elif num > target:
return
for i in range(idx, len(candidates)):
backtrack(i, num+candidates[i], res + [candidates[i]])
#加入idx是为了防止出现重复的,即搜索时不会往前,因为包含前面的可能结果在之前的搜索中一定已经找到了
backtrack(0, 0, [])
return ans
子集问题
这一类的回溯则与DFS的写法有所不同,因为子集意味着有一些元素是取,而另一些元素是不取的。
2044. 统计按位或能得到最大值的子集数目
给你一个整数数组 nums ,请你找出 nums 子集 按位或 可能得到的 最大值 ,并返回按位或能得到最大值的 不同非空子集的数目 。
如果数组 a 可以由数组 b 删除一些元素(或不删除)得到,则认为数组 a 是数组 b 的一个 子集 。如果选中的元素下标位置不一样,则认为两个子集 不同 。
对数组 a 执行 按位或 ,结果等于 a[0] OR a[1] OR … OR a[a.length - 1](下标从 0 开始)。
示例 1:
输入:nums = [3,1]
输出:2
解释:子集按位或能得到的最大值是 3 。有 2 个子集按位或可以得到 3 :
-[3]
-[3,1]
示例 2:
输入:nums = [2,2,2]
输出:7
解释:[2,2,2] 的所有非空子集的按位或都可以得到 2 。总共有 23 - 1 = 7 个子集。
示例 3:
输入:nums = [3,2,1,5]
输出:6
解释:子集按位或可能的最大值是 7 。有 6 个子集按位或可以得到 7 :
-[3,5]
-[3,1,5]
-[3,2,5]
-[3,2,1,5]
-[2,5]
-[2,1,5]
首先,该题显然是一个子集问题,也就是涉及到一个“选与不选”的问题,即当前元素是否要加入到当前的子集中。首先,假设有一个回溯操作 backtrack(i)
表明对原数组中第i
个元素进行操作,那么如果不执行该操作,只需要在该函数里直接调用backtrack(i+1)
,就相当于忽略了第i
个元素。而回溯的本质还是枚举每一种情况,只是加上了上述操作后,能够将“不使用某个元素”加入了要枚举的情况中。
此外,由于本题需要求“按位或”的值,而当前的集合“按位或”的值并不会随着元素的增加而变小,因此只需要记录当前值即可。
class Solution:
def countMaxOrSubsets(self, nums: List[int]) -> int:
maxOr, cnt = 0, 0
def backtrace(index, orVal):
if index == len(nums):
nonlocal maxOr, cnt
if orVal > maxOr:
maxOr, cnt = orVal, 1
elif orVal == maxOr:
cnt += 1
return
backtrace(index + 1, orVal | nums[index])
#这是使用了当前的元素,因此“按位或”的值发生了变化
backtrace(index + 1, orVal)
#这是跳过了当前元素
backtrace(0, 0)
return cnt
拓展
473. 火柴拼正方形(推荐)
474. 一和零(推荐)
选与不选的题目往往可以用到回溯,但不一定是最优解,因为回溯是偏暴力的一种解法。还有很多类似这样的题中,可以用到动态规划来解。这一题就是两种解法都有涉及。