LeetCodeCampsDay25回溯part04
LeetCodeCampsDay25回溯part04
491是去重复的另一种情况,此时是不能将数组先排序再判断
491. 非递减子序列
https://leetcode.cn/problems/non-decreasing-subsequences/
给你一个整数数组 nums
,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
示例 1:
1 | 输入:nums = [4,6,7,7] |
示例 2:
1 | 输入:nums = [4,4,3,2,1] |
提示:
1 <= nums.length <= 15
-100 <= nums[i] <= 100
回溯思路
本题目和90. 子集II
有点像,但不同点是,本题不让排序,就没法使用nums[i] == nums[i - 1]来判断某个数字是否在本层被用过了,但记住,去重的核心思路是:当前数字num[i]是否在num[start: i](不包含i)出现过,如果没出现过就可以用,否则continue,比如[7, 6, 7],需要判断第二个7(即nums[i],是否在nums[start: i]里);
此外,还有个剪枝的条件,如果path的最后一个值比num[i]大,则可以跳过;
- 输入输出:输入列表,开始下标;无返回值
- 终止条件:如果path长度大于2个就添加,无其它条件(单层逻辑必须保证此时是满足题目要求的)
- 单层逻辑:先去重,再剪枝掉path的最后一个值比num[i]大情况(可以不做,因为isLegal函数也会处理),随后,将当前数字添加到path并使用isLegal函数判断path是否合规,如果全规则再递归其子串;
回溯代码
- 时间复杂度O(N * 2^N)
- 空间复杂度O(N)
1 | class Solution: |
46. 全排列
https://leetcode.cn/problems/permutations/
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
1 | 输入:nums = [1,2,3] |
示例 2:
1 | 输入:nums = [0,1] |
示例 3:
1 | 输入:nums = [1] |
提示:
1 <= nums.length <= 6
-10 <= nums[i] <= 10
nums
中的所有整数 互不相同
回溯思路
画个二叉树的图后,可以发现,随着递归深度增加,for循环遍历的对象是未在path里的元素,所以需要有个used表来记录哪些元素被访问过;可以使用dict或者list实现
- 输入输出:输入列表,无返回值
- 终止条件:当path长度等于nums长度则添加到res,如果path长度大于nums长度直接退出
- 单层逻辑:for循环遍历的对象是未在path里的元素,所以是for i in range(len(nums));并且需要判断是否被访问过,只递归未访问过的元素
回溯代码
1 | class Solution: |
47. 全排列 II
https://leetcode.cn/problems/permutations-ii/
给定一个可包含重复数字的序列 nums
,按任意顺序 返回所有不重复的全排列。
示例 1:
1 | 输入:nums = [1,1,2] |
示例 2:
1 | 输入:nums = [1,2,3] |
提示:
1 <= nums.length <= 8
-10 <= nums[i] <= 10
回溯思路
本题目和47.全排列II
的不同点是“本题有可重复数字”,面对可重复数字,就需要去重复(比如先排序,再用nums[i]==nums[i-1]判断);另一方面,本题也需要和一样,需要使用used数组来保证递归时只遍历“剩下”的子串(纵向条件)
- 输入输出:输入列表,无直接返回值
- 终止条件:当path长度与nums长度相等即添加到res
- 单层逻辑:纵向上,只遍历剩余字符串;横向上,需要先去重复,再进行(标记当前数字被use->添加到path-> 递归下一层 -> 从path弹出回溯-> 标记当前数字未被use)
- 这是去重复使用
if i > 0 and nums[i] == nums[i - 1] and self.used[i - 1] == 0: continue
,注意这里的self.used[i - 1] == 0
,表示只有nums[i - 1]被用过了,而且nums[i]==nums[i-1]时,才能执行for单层逻辑,否则就跳过当前数字,因为当nums[i - 1]未被用过时,则nums[i - 1]就已经包含了nums[i]的情况(可以看下面的图)
还要强调的是去重一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了。
我以示例中的 [1,1,2]为例 (为了方便举例,已经排序)抽象为一棵树,去重过程如图:
一般来说:组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果。
在46.全排列 (opens new window)中已经详细讲解了排列问题的写法,在40.组合总和II (opens new window)、90.子集II (opens new window)中详细讲解了去重的写法
回溯代码
- 时间复杂度: O(n! * n)
- 空间复杂度: O(n)
1 | class Solution: |
反思
大家发现,去重最为关键的代码为:
1 | if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { |
如果改成 used[i - 1] == true
, 也是正确的!,去重代码如下:
1 | if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) { |
这是为什么呢,就是上面我刚说的,如果要对**树层中前一位(横向)去重,就用used[i - 1] == false
,如果要对树枝前一位(纵向)**去重用used[i - 1] == true
。
对于排列问题,树层上去重(横向)和树枝上去重(纵向),都是可以的,但是树层上去重效率更高!
这么说是不是有点抽象?
来来来,我就用输入: [1,1,1] 来举一个例子。
树层上去重(used[i - 1] == false)的树形结构如下:
树枝上去重(used[i - 1] == true)的树型结构如下:
大家应该很清晰的看到,树层上对前一位(横向)去重非常彻底,效率很高,树枝上对前一位去(纵向)重虽然最后可以得到答案,但是做了很多无用搜索。
51. N 皇后
https://leetcode.cn/problems/n-queens/
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n
个皇后放置在 n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n
,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q'
和 '.'
分别代表了皇后和空位。
示例 1:
1 | 输入:n = 4 |
示例 2:
1 | 输入:n = 1 |
提示:
1 <= n <= 9
回溯思路
本方法特点:不创建初始棋盘,并且isValid函数也无需对整个棋盘进行遍历
在做了前面的全排列与各种子集问题后,这题目反而不难,完全可以按下面的图,把代码写出来;横向遍历+纵向遍历,仍然使用path记录路径,此外,需要一个判别函数islegal()判断当前位置是否合规(在491.非递减子序列
和93. 复原 IP 地址
也使用了islegal函数)
- 输入输出:输入棋盘长度,无直接返回值
- 终止条件:当path长度与棋盘长度一致,返回结果
- 单层逻辑:横向遍历(for循环里),先判断当前位置(depth, x)是否合规,其中depth指纵向深度下标,x指横向下标;如果合规,再将(depth, x)添加到path里,递归下一个深度,从path弹出回溯。(这里可以仅将x添加到path里,因为depth和x的下标是一样的)
- islegal判断条件:
- 函数功能:当前位置的上边列、左上角、右上角没有数字;
- 输入输出:输入位置(depth, x) ,输出bool
- 不像以往那种,先创建个棋盘,再对棋盘进行遍历;我们仅需要对path和当前位置进行判断即可,对path里的每个元素i遍历
- 假设path记录的是坐标([0,0], [1,2]),对于一个当前位置[2,0]
- if i[1] == x, 说明当前列(x) 上边有元素,返回False
- if depth - i[0] == x - i[1],说明当前位置左上角有元素,返回False
- if depth - i[0] == i[1] - x,说明当前位置右上角有元素,返回False
判断函数甚至可以精简成这样(但这样就看不出来动机了)
1 | def isLegal(self, depth, col) -> bool: |
注意,本方法不需要对当前行的左边进行判断,因为进入当前行时,左边一定是不会有元素的 我们总是从上到下处理每一行(例如,从行 0 到行 N-1)。 当我们到达第 depth 行时,self.path 中只包含之前行(0 到 depth-1)的皇后位置
回溯代码
1 | class Solution: |
回溯改进思路
为了判断一个位置所在的列和两条斜线上是否已经有皇后,使用三个集合 columns、diagonals1 和 diagonals2 分别记录每一列以及两个方向的每条斜线上是否有皇后。
列的表示法很直观,一共有 N 列,每一列的下标范围从 0 到 N−1,使用列的下标即可明确表示每一列。
如何表示两个方向的斜线呢?对于每个方向的斜线,需要找到斜线上的每个位置的行下标与列下标之间的关系。
方向一的斜线为从左上到右下方向,同一条斜线上的每个位置满足行下标与列下标之差相等,例如 (0,0) 和 (3,3) 在同一条方向一的斜线上。因此使用行下标与列下标之差即可明确表示每一条方向一的斜线。
回溯代码2
1 | class Solution: |
52. N 皇后 II
https://leetcode.cn/problems/n-queens-ii/
n 皇后问题 研究的是如何将 n
个皇后放置在 n × n
的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n
,返回 n 皇后问题 不同的解决方案的数量。
示例 1:
1 | 输入:n = 4 |
示例 2:
1 | 输入:n = 1 |
提示:
1 <= n <= 9
回溯代码
1 | class Solution: |
332. 重新安排行程
https://leetcode.cn/problems/reconstruct-itinerary/
给你一份航线列表 tickets
,其中 tickets[i] = [fromi, toi]
表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。
所有这些机票都属于一个从 JFK
(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK
开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。
- 例如,行程
["JFK", "LGA"]
与["JFK", "LGB"]
相比就更小,排序更靠前。
假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。
示例 1:
1 | 输入:tickets = [["MUC","LHR"],["JFK","MUC"],["SFO","SJC"],["LHR","SFO"]] |
示例 2:
1 | 输入:tickets = [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]] |
提示:
1 <= tickets.length <= 300
tickets[i].length == 2
fromi.length == 3
toi.length == 3
fromi
和toi
由大写英文字母组成fromi != toi
回溯思路
这个方法有点儿怪,题目要求从JFK出发并且绕完所有的机场,题目保证一定有解;所以只要从JFK出发,遍历它能到达的落地节点,再从落地节点出发,遍历落地节点的落地节点……比如,JFK->KUL,到达KUL则终点了,此时将KUL添加到res里;随后遍历JFK->NRT,NRT->JFK,又到了JFK,但此时JFK已经飞过了KUL和NRT(相当于没有从JFK的票了),于是此时JFK(作为落地节点)被添加到res里;回到NRT,它也没有其它落地节点了,它被添加到res;最后回到JFK(作为初始的出发节点)也被添加到res里。
- 回溯的输入输出:输入出发机场和现有的机票,无直接输出
- 终止条件:当前出发的机场没有机票时停止,此时将当前机场添加到res里
- 单层逻辑:遍历当前出发的机场所有机票,如果有票,先消耗这张票,递归(纵向遍历)这个票的终点机场;注意,这里没有回溯,即,不会把消耗的机票还原回去,试想下,如果还原回去,此时就会进入无尽的循环。

回溯代码
- 时间复杂度O(nLogN)
- 空间复杂度O(N)
1 | class Solution: |
37. 解数独
https://leetcode.cn/problems/sudoku-solver/
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
- 数字
1-9
在每一行只能出现一次。 - 数字
1-9
在每一列只能出现一次。 - 数字
1-9
在每一个以粗实线分隔的3x3
宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 '.'
表示。
示例 1:
1 | 输入:board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]] |
提示:
board.length == 9
board[i].length == 9
board[i][j]
是一位数字或者'.'
- 题目数据 保证 输入数独仅有一个解
之前刷到的题目:77.组合(组合问题) (opens new window),131.分割回文串(分割问题) (opens new window),78.子集(子集问题) (opens new window),46.全排列(排列问题) (opens new window),以及51.N皇后(N皇后问题) (opens new window),其实这些题目都是一维递归。
N皇后问题 (opens new window)是因为每一行每一列只放一个皇后,只需要一层for循环遍历一行,递归来遍历列,然后一行一列确定皇后的唯一位置。
本题就不一样了,本题中棋盘的每一个位置都要放一个数字(而N皇后是一行只放一个皇后),并检查数字是否合法,解数独的树形结构要比N皇后更宽更深。
- 输入输出:输入棋盘,输出bool,表示如果找到解就直接一路return,不用继续搜索了
- 终止条件:本题递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。
- 单层逻辑:横向遍历是对棋盘的高、宽进行二维遍历,此外还需要对数字(1~9)进行逐个横向遍历,仅只有当前位置上(row, col)填写数字i是合规的情况下,才能纵向遍历
- 先判断当前位置是否数字,如果是则跳过
- 判断当前位置的数字i是否合规,如果合规,则将board的当前位置更新为这个数字i,再纵向遍历(递归),最后回溯将board当前位置更新为’.’
- 判断当前位置的数字是否合规
- 判断是否同行、同列、是否同一个小九宫格
- 注意小九宫格的判断方法,先求这个小九宫格里的行、列开始坐标,再此坐标上加三即可
回溯代码
下面代码会超时,但思路是正确的,超时的原因是九宫格判断,如果要遍历整个棋盘,会浪费大量时间
1 | class Solution: |
不超时版本,用空间换时间,给每行每列每个小九宫格添加个used数组,并且!不要使用函数调用!否则一定会超时
The inline condition is directly part of the loop in your backtracking method. The Python interpreter can execute it without any extra indirection. In contrast, a method call introduces a “jump” to another part of the code, which can slow things down.
1 | class Solution: |