leetcode hot100:十一、解题思路大全:回溯(全排列、子集、电话号码的字母组合、组合总和、括号生成、单词搜索、分割回文串、N皇后)
我太爱这种回溯了,多做几次就熟了的感觉,别管,已膨胀(
全排列
给定一个不含重复数字的数组 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]]
思路
回溯终止条件是path长度==nums长度,因为是全排列,所以i遍历数组时每次都会从头遍历(因为全排列不在乎数字的先后顺序),所以需要用used数组记录这个数字是否已经在path中使用过。
子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
思路
回溯终止条件依旧是path长度==nums长度。但是子集要求是有顺序的,所以i遍历数组时不会每次都会从头遍历,相反的是每次回溯我们都需要用cur来记录i之前已经遍历到的位置,这次回溯再从这个cur(cur为i+1,保证了不会选到重复的数字)开始,所以也就不再需要used数组记录。
电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
示例 2:
输入:digits = “”
输出:[]
示例 3:
输入:digits = “2”
输出:[“a”,“b”,“c”]
思路
相当于子集那道题多了一个字母映射。
组合总和
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
思路
相当于给你一个整数数组,数组中的元素互不相同,返回该数组所有可能的子集,**要求子集的和为target。解集不可以包含重复的子集,但可以包含重复的数字。**你可以按 任意顺序 返回解集。
和子集那道题对比,子集那道题如下:
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
相当于子集那道题的基础上多了两个加粗的条件。为此,我们的回溯条件改为
子集的和为target就加入答案并返回,如果子集的和已经>target,就直接返回(说明要回溯)。
除此之外,虽然解集可以包含重复的数字,但依旧是不包含重复的子集的,所以我们依旧要维护cur。 通过 cur,每次递归只能从当前位置或之后的元素开始选择,避免生成重复的组合。例如,当你已经生成组合 [2,2,3] 后,不会再生成 [2,3,2] 或 [3,2,2]。
而回溯时,我们的cur传入 i 而非 i+1,保障了可以选取到重复的数字。
在这再次贴一下子集那道题的思路:
回溯终止条件依旧是path长度==nums长度。但是子集要求是有顺序的,所以i遍历数组时不会每次都会从头遍历,相反的是每次回溯我们都需要用cur来记录i之前已经遍历到的位置,这次回溯再从这个cur(cur为i+1,保证了不会选到重复的数字)开始,所以也就不再需要used数组记录。
括号生成
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:[“((()))”,“(()())”,“(())()”,“()(())”,“()()()”]
示例 2:
输入:n = 1
输出:[“()”]
提示:
1 <= n <= 8
思路
这道题有点意思啊hhh
有效括号的条件:
- 平衡性:在生成过程中,左括号 ( 的数量必须始终大于等于右括号 ) 的数量。
- 完整性:最终生成的字符串中,左括号和右括号的数量均为 n。
回溯法的实现:
递归路径:在每一步递归中,可以选择添加左括号或右括号,但需要满足上述条件。
剪枝条件:
- 当左括号数量小于 n 时,可以添加左括号。
- 当右括号数量小于左括号数量时,可以添加右括号。
算法步骤
递归函数参数:当前字符串 path、左括号数量 left、右括号数量 right、目标对数 n、
结果列表 result。
终止条件:当 left = n 且 right = n 时,将当前字符串加入结果列表。
递归选择:
- 添加左括号:若 left < n,添加 ( 并递归 left + 1。
- 添加右括号:若 right < left,添加 ) 并递归 right + 1。
单词搜索
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
思路
标准DFS回溯。找到word[0] 的单元格 (i, j) 作为起点。
从每个起点出发,递归检查四个方向:
终止条件:
- 若当前字符索引 k 等于单词长度,说明单词已完全匹配,返回 True。
- 若越界、字符不匹配或单元格已被访问,返回 False。
递归过程:
- 标记当前单元格为已访问(例如,将 board[i][j] 改为特殊字符 #)。
- 递归搜索四个方向,只要有一个方向返回 True,则整体返回 True。
- 回溯:恢复当前单元格为原始值,继续尝试其他方向。
分割回文串
给你一个字符串 s,请你将 s 分割成一些 子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
示例 1:
输入:s = “aab”
输出:[[“a”,“a”,“b”],[“aa”,“b”]]
示例 2:
输入:s = “a”
输出:[[“a”]]
提示:
1 <= s.length <= 16
s 仅由小写英文字母组成
思路
其实这道题一眼看过去,会想到动态规划来做的。毕竟dp擅长处理这种子串分割的问题。
用二维数组 dp[i][j] 表示子串 s[i:j+1] 是否为回文串。
状态转移方程:
dp[i][j] = (s[i] == s[j]) and (j - i <= 2 or dp[i+1][j-1])
即当前字符相等,且子串长度小于等于 2 或去掉首尾后仍为回文。
但是纯 DP 通常更适合计算 “方案数量”,而本题要求生成所有具体的分割方案,所以即使用了动态规划,我们还是需要用回溯来生成具体的分割方案的。
譬如分割等和子集那道题:给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
就是可以用纯dp来做。
如果这道题我们真的要用纯dp来做的话。思路如下:
状态转移:
对于每个位置 i,遍历所有可能的分割点 j(0 ≤ j < i),如果子串 s[j:i] 是回文,则将 dp[j] 中的每个方案末尾添加 s[j:i],并加入 dp[i]。
路径记录:
在 DP 数组中,每个 dp[i] 存储一个列表,列表中的每个元素是一个分割方案(即字符串列表)。
时间复杂度超高!O(n^2 * 2^n)
所以我们用回溯法直接来做。
回溯函数的参数有两个,一个是cur,我们用cur维护分割点,一个是答案集合path。
cur:表示当前递归层的起始分割点,即从哪个位置开始尝试分割新的回文子串。
i:表示当前递归层的结束分割点,用于生成子串 s[cur:i+1] 并检查其是否为回文。
如果生成的子串是回文的,那么我们加入答案集和path,并且继续尝试新的起始分割点(cur=i+1)。(注意这里没有显式地去判断尝试新的结束分割点,但是实际上通过外层循环,我们是会去判断的)。如果生成的子串不是回文的,我们尝试新的结束分割点(i=i+1)。
直到达到回溯边界条件:起始分割点达到字符串终点,说明整个字符串已被成功分割为回文子串,记录结果并终止递归。
在这一个注意点是,我们为什么不需要判断 s[0:cur] 是否回文?
这是因为每次递归调用时,cur 参数表示当前待处理的子串起始位置。在递归进入下一层前,我们已经确保了:
- 当前路径 path 中的所有子串都是回文(因为加入 path 前已检查)。
- path 中的子串恰好覆盖 s[0:cur](因为每次递归时 cur 递增,且子串无重叠)。
因此,s[0:cur] 的合法性已经由历史递归步骤保证,无需重复检查。
示例说明:
假设当前递归层 cur=3,路径 path = [“aa”, “b”],说明:
s[0:2] = “aa” 是回文(已在 cur=0 时检查)。
s[2:3] = “b” 是回文(已在 cur=2 时检查)。
因此,s[0:3] 已被合法分割为回文子串,无需再次验证。
也就是说,两层循环,外层循环i(结束分割点),内层通过递归循环cur(起始分割点)。
外层循环负责在当前起始位置 cur 下,尝试所有可能的子串(通过 i 扩展结束位置)。
递归调用负责在找到一个合法子串后,固定该子串,并从下一个位置 i+1 开始处理剩余字符串。
N皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
思路
这个需要说明吗?需要题解吗?
黎吧皇,你很有名, 我就不给思路了。hhh