332. 重新安排行程
Greedy + 回溯
- 通过回溯来打破死循环
- 通过贪心算法选择最小的lexical order(如果能找到完整的itinerary,那么一定是lexical order最小的)
class Solution:
def findItinerary(self, tickets: List[List[str]]) -> List[str]:
res = ['JFK']
from_to = defaultdict(list)
for ticket in tickets:
from_to[ticket[0]].append(ticket[1])
def _backtracking(departure):
# itinerary is compelete
if len(res) == len(tickets) + 1:
return True
# sort by ascending lexical order
from_to[departure].sort()
for _ in from_to[departure]:
destination = from_to[departure].pop(0)
res.append(destination)
# if find a compelete itinerary, we do not search further
if _backtracking(destination):
return True
res.pop()
from_to[departure].append(destination)
_backtracking('JFK')
return res
51. N 皇后
- The queen (♕, ♛) is the most powerful piece in the game of chess, able to move any number of squares vertically, horizontally or diagonally, combining the power of the rook and bishop.
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
if not n:
return []
# n * n board
board = [['.'] * n for _ in range(n)]
res = []
# cannot in the same row, in the same column, and at the diagonal
# 因为是从上往下搜索,所以只需要判断左上角和右上角是否冲突
def is_valid(board, row, col):
#判断同一列是否冲突
for i in range(n):
if board[i][col] == 'Q':
return False
# 判断左上角是否冲突
i = row -1
j = col -1
while i>=0 and j>=0:
if board[i][j] == 'Q':
return False
i -= 1
j -= 1
# 判断右上角是否冲突
i = row - 1
j = col + 1
while i>=0 and j < len(board):
if board[i][j] == 'Q':
return False
i -= 1
j += 1
return True
def backtracking(board, row, n):
# 如果走到最后一行,说明已经找到一个解
if row == n:
temp_res = []
for temp in board:
temp_str = "".join(temp)
temp_res.append(temp_str)
res.append(temp_res)
for col in range(n):
# 如果在(row,col)处冲突,则剪枝
if not is_valid(board, row, col):
continue
board[row][col] = 'Q'
# 搜索下一层
backtracking(board, row+1, n)
# 回溯
board[row][col] = '.'
backtracking(board, 0, n)
return res
37. 解数独
class Solution:
def solveSudoku(self, board: List[List[str]]) -> None:
"""
Do not return anything, modify board in-place instead.
"""
def is_valid(board, num, row, col):
# row constraint
if num in board[row]:
return False
# column constraint
for i in range(9):
if num == board[i][col]:
return False
# grid constraint
start_row = 3 * (row // 3)
start_col = 3 * (col // 3)
for i in range(start_row, start_row + 3):
for j in range(start_col, start_col + 3):
if board[i][j] == num:
return False
return True
def backtracking(board):
for i in range(9): # 遍历行
for j in range(9): # 遍历列
# 若空格内已有数字,跳过
if board[i][j] != '.':
continue
# 尝试填入 1-9
for num in range(1, 10):
if not is_valid(board, str(num), i, j):
continue
board[i][j] = str(num)
if backtracking(board):
return True
board[i][j] = '.'
return False
return True # 有解
backtracking(board)
回溯总结
回溯法理论基础
-
回溯是递归的副产品,只要有递归就会有回溯,所以回溯法也经常和二叉树遍历,深度优先搜索混在一起,因为这两种方式都是用了递归。
-
回溯法就是暴力搜索,并不是什么高效的算法,最多再剪枝一下。
-
回溯算法能解决如下问题:
- 组合问题:N个数里面按一定规则找出k个数的集合
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 棋盘问题:N皇后,解数独等等
-
回溯法的模板
void backtracking(参数) { if (终止条件) { 存放结果; return; } for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { 处理节点; backtracking(路径,选择列表); // 递归 回溯,撤销处理结果 } }
组合问题
- for循环横向遍历,递归纵向遍历,回溯不断调整结果集
- 收集叶子节点的结果
- 剪枝精髓是:for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够题目要求的k个元素了,就没有必要搜索了。
切割问题
- 切割问题其实类似组合问题
- 如何模拟那些切割线
- 切割问题中递归如何终止
- 在递归循环中如何截取子串
- 收集叶子节点的结果
子集问题
-
在树形结构中子集问题是要收集所有节点的结果
result.push_back(path); // 收集子集,要放在终止添加的上面,否则会漏掉结果 if (startIndex >= nums.size()) { // 终止条件可以不加 return; }
排列问题
- 每层都是从0开始搜索而不是startIndex
- 需要used数组记录path里都放了哪些元素了
性能分析
子集问题分析:
- 时间复杂度:O(2^n),因为每一个元素的状态无外乎取与不取,所以时间复杂度为O(2^n)
- 空间复杂度:O(n),递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n)
排列问题分析:
- 时间复杂度:O(n!),这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 。
- 空间复杂度:O(n),和子集问题同理。
组合问题分析:
- 时间复杂度:O(2^n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。
- 空间复杂度:O(n),和子集问题同理。
N皇后问题分析:
- 时间复杂度:O(n!) ,其实如果看树形图的话,直觉上是O(n^n),但皇后之间不能见面所以在搜索的过程中是有剪枝的,最差也就是O(n!),n!表示。
- 空间复杂度:O(n),和子集问题同理。
解数独问题分析:
- 时间复杂度:O(9^m) , m是'.'的数目。
- 空间复杂度:O(n^2),递归的深度是n^2
一般说到回溯算法的复杂度,都说是指数级别的时间复杂度,这也算是一个概括吧!