代码随想录算法训练营-回溯算法|491.递增子序列、46. 全排列、47. 全排列 II、332. 重新安排行程、37. 解数独
不对原数组进行排序,利用set对同层的子集进行去重。
1 class Solution: 2 def findSubsequences(self, nums): 3 result = [] 4 path = [] 5 self.backtracking(nums, 0, path, result) 6 return result 7 8 def backtracking(self, nums, startIndex, path, result): 9 if len(path) > 1: 10 result.append(path[:]) # 注意要使用切片将当前路径的副本加入结果集 11 # 注意这里不要加return,要取树上的节点 12 13 uset = set() # 使用集合对本层元素进行去重 14 for i in range(startIndex, len(nums)): 15 if (path and nums[i] < path[-1]) or nums[i] in uset: 16 continue 17 18 uset.add(nums[i]) # 记录这个元素在本层用过了,本层后面不能再用了 19 path.append(nums[i]) 20 self.backtracking(nums, i + 1, path, result) 21 path.pop()
不用像子集和切割问题使用startIndex,但需要一个used数组记录path里面都有哪些元素被使用了。
- 时间复杂度: O(n!)
- 空间复杂度: O(n)
1 class Solution: 2 def permute(self, nums): 3 result = [] 4 #used数组=[False]*len(nums) 5 self.backtracking(nums, [], [False] * len(nums), result) 6 return result 7 8 def backtracking(self, nums, path, used, result): 9 #停止条件:到达叶子结点:全排列 10 if len(path) == len(nums): 11 result.append(path[:]) 12 return 13 for i in range(len(nums)):#for循环控制树寛 14 if used[i]: #被使用的元素不能再被选择,因为for是从0开始,所以需要一个used数组 15 continue 16 #处理节点 17 used[i] = True 18 path.append(nums[i]) 19 #递归 20 self.backtracking(nums, path, used, result) 21 #回溯 22 path.pop() 23 used[i] = False
1. 给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
2. 与46题的区别在于去重,去重一定要对元素进行排序,才方便通过相邻的节点来判断是否重复使用了。
3. 对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!
- 时间复杂度: O(n! * n)
- 空间复杂度: O(n)
1 class Solution: 2 def permuteUnique(self, nums): 3 nums.sort() # 排序 4 result = [] 5 self.backtracking(nums, [], [False] * len(nums), result) 6 return result 7 8 def backtracking(self, nums, path, used, result): 9 if len(path) == len(nums): 10 result.append(path[:]) 11 return 12 for i in range(len(nums)): 13 if (i > 0 and nums[i] == nums[i - 1] and not used[i - 1]) or used[i]: #树层去重 14 continue 15 used[i] = True 16 path.append(nums[i]) 17 self.backtracking(nums, path, used, result) 18 path.pop() 19 used[i] = False
用used数组记录机票是否被使用过,依据题意记得先对机票tickets排序。
1 class Solution: 2 def findItinerary(self, tickets: List[List[str]]) -> List[str]: 3 tickets.sort() # 先排序,这样一旦找到第一个可行路径,一定是字母排序最小的 4 used = [0] * len(tickets) 5 path = ['JFK'] 6 results = [] 7 self.backtracking(tickets, used, path, 'JFK', results) 8 return results[0] 9 10 def backtracking(self, tickets, used, path, cur, results): #cur出发点 11 if len(path) == len(tickets) + 1: # 终止条件:路径长度等于机票数量+1 12 results.append(path[:]) # 将当前路径添加到结果列表 13 return True 14 15 for i, ticket in enumerate(tickets): # 遍历机票列表 16 if ticket[0] == cur and used[i] == 0: # 找到起始机场为cur且未使用过的机票 17 used[i] = 1 # 标记该机票为已使用 18 path.append(ticket[1]) # 将到达机场添加到路径中 19 state = self.backtracking(tickets, used, path, ticket[1], results) # 递归搜索 #上一个降落点是下一个出发点 20 path.pop() # 回溯,移除最后添加的到达机场 21 used[i] = 0 # 标记该机票为未使用 22 if state: 23 return True # 只要找到一个可行路径就返回,不继续搜索
皇后们的约束条件:
- 不能同行
- 不能同列
- 不能同斜线
N皇后问题是因为每一行每一列只放一个皇后,只需要一层for循环遍历一行,递归来遍历列,然后一行一列确定皇后的唯一位置。
1 class Solution: 2 def solveNQueens(self, n: int) -> List[List[str]]: 3 result = [] # 存储最终结果的二维字符串数组 4 5 chessboard = ['.' * n for _ in range(n)] # 初始化棋盘['....', '....', '....', '....'] 6 self.backtracking(n, 0, chessboard, result) # 回溯求解 7 return [[''.join(row) for row in solution] for solution in result] # 返回结果集 8 9 def backtracking(self, n: int, row: int, chessboard: List[str], result: List[List[str]]) -> None: 10 if row == n:#遍历完了整个棋盘,是收集结果的时候 ,收集结果的时候就是满足要求的时候 11 result.append(chessboard[:]) # 棋盘填满,将当前解加入结果集 12 return 13 14 for col in range(n): #遍历一行中的所有列 15 if self.isValid(row, col, chessboard): #判断是否满足N皇后要求 16 chessboard[row] = chessboard[row][:col] + 'Q' + chessboard[row][col+1:] # 放置皇后 17 self.backtracking(n, row + 1, chessboard, result) # 递归到下一行 18 chessboard[row] = chessboard[row][:col] + '.' + chessboard[row][col+1:] # 回溯,撤销当前位置的皇后 19 20 def isValid(self, row: int, col: int, chessboard: List[str]) -> bool: 21 # 检查列 22 for i in range(row): 23 if chessboard[i][col] == 'Q': 24 return False # 当前列已经存在皇后,不合法 25 26 # 检查 45 度角是否有皇后 27 i, j = row - 1, col - 1 28 while i >= 0 and j >= 0: 29 if chessboard[i][j] == 'Q': 30 return False # 左上方向已经存在皇后,不合法 31 i -= 1 32 j -= 1 33 34 # 检查 135 度角是否有皇后 35 i, j = row - 1, col + 1 36 while i >= 0 and j < len(chessboard): 37 if chessboard[i][j] == 'Q': 38 return False # 右上方向已经存在皇后,不合法 39 i -= 1 40 j += 1 41 42 return True # 当前位置合法
1. 与以往的不同在于这道题使用二维递归。
2. 解数独的树形结构要比N皇后更宽更深。
3. 解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值。
4. 一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!
1 class Solution: 2 def solveSudoku(self, board: List[List[str]]) -> None: 3 """ 4 Do not return anything, modify board in-place instead. 5 """ 6 self.backtracking(board) 7 8 def backtracking(self, board: List[List[str]]) -> bool: 9 # 若有解,返回True;若无解,返回False 10 for i in range(len(board)): # 遍历行 11 for j in range(len(board[0])): # 遍历列 12 # 若空格内已有数字,跳过 13 if board[i][j] != '.': continue 14 for k in range(1, 10): 15 if self.is_valid(i, j, k, board): 16 board[i][j] = str(k) 17 if self.backtracking(board): return True 18 board[i][j] = '.' 19 # 若数字1-9都不能成功填入空格,返回False无解 20 return False 21 return True # 有解 22 23 def is_valid(self, row: int, col: int, val: int, board: List[List[str]]) -> bool: 24 # 判断同一行是否冲突 25 for i in range(9): 26 if board[row][i] == str(val): 27 return False 28 # 判断同一列是否冲突 29 for j in range(9): 30 if board[j][col] == str(val): 31 return False 32 # 判断同一九宫格是否有冲突 33 start_row = (row // 3) * 3 34 start_col = (col // 3) * 3 35 for i in range(start_row, start_row + 3): 36 for j in range(start_col, start_col + 3): 37 if board[i][j] == str(val): 38 return False 39 return True