332.重新安排行程
51. N皇后
personal thinking
我的思路就是依次记录每一层的值,然后根据函数实现对应判断逻辑和递归逻辑。
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
# 问题:怎么判断下边和上边有不再下边或者上边的元素
# 树的深度为n
depth = 0
result = []
path = []
ans = []
temp = ['.'] * n
def location(i):
temp1 = temp.copy()
temp1[i] = 'Q'
return ''.join(temp1)
def isValid(row,col):
# 生成不符合要求的位置:
not_valid = []
depth = 0
while depth < row:
not_valid.append(ans[depth][1]) #获取列的坐标
not_valid.append(ans[depth][1]+(row-depth))
not_valid.append(ans[depth][1]-(row-depth))
depth += 1
if col in not_valid:
return True
return False
def backtracking(depth):
if depth == n:
result.append(path.copy())
return
for i in range(0,n):
if ans and isValid(depth,i):
continue
path.append(location(i))
ans.append((depth,i))
depth += 1
backtracking(depth)
path.pop()
ans.pop()
depth -= 1
if n == 1:
return [["Q"]]
backtracking(0)
return result
其他题解
- 利用二位数组作为结果的产出
- 回溯的时候将pop改成改变二维数组的数值即可
- 判断的时候可以分别对左上右上和同列的数值进行判断,从而得到valid的数值
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
# 思想利用二维数组进行数据的存储和修改
if not n: return []
board = [['.'] * n for _ in range(n)]
res = []
def isVaild(board,row, col):
#判断同一列是否冲突
for i in range(len(board)):
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):
if not isVaild(board, row, col):
continue
board[row][col] = 'Q'
backtracking(board, row+1, n)
board[row][col] = '.' # 回溯的话将原来的数值改回.即可,格局打开一些
backtracking(board, 0, n)
return res
37. 解数独
回溯算法总结
什么是回溯算法?
回溯算法的本质其实就是暴力搜索,并不是高效的算法,只不过通过剪枝可以减少运行的时间。
回溯法通常能够解决什么问题?
组合问题:N个数里面按一定规则找出k个数的集合
排列问题:N个数按一定规则全排列,有几种排列方式
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有多少符合条件的子集
棋盘问题:N皇后,解数独等等
回溯法解决问题的模板?
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
在该模板中在不同的问题中,需要更改的地方
- 终止条件是什么
- 是否需要递归返回startIndex值
- 递归返回的startIndex是当前结点还是下一个结点
- 结果记录的函数应该在哪里出现
- for循环中if-continue剪枝操作的条件,防止层序重复值的出现。
关键还是具体问题,具体分析,分析当前问题属于什么种类,做出树形结构是什么样的,怎么进行判断和剪枝
利用树形图像更加直观的理解回溯法:
分不同的问题,如何使用递归的方法进行求解?
组合总和问题:
- 问题1:给定k和n,求解列表中对应和的集合。
剪枝:已选元素总和如果已经大于n(题中要求的和)了,那么往后遍历就没有意义了,直接剪掉 - 问题2:只有总和限制没有数量限制
如果是一个集合来求组合的话,就需要startIndex;如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex - 问题3:集合中元素会有重复但是求解集中不能包含重复的组合
建立临时数组used
切割问题:
难点:如何模拟切割线;切割问题中如何递归终止;如何截取子串;如何判断回文
突破点:利用求解组合问题的方式来求解切割问题
本题还有细节,例如:切割过的地方不能重复切割所以递归函数需要传入i + 1
子集问题:
在树形结构中子集问题是要收集所有节点的结果,而组合问题是收集叶子节点的结果
排列问题:
排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方
在排列问题中需要注意以下两种方式:每层都是从0开始搜索而不是startIndex;需要used数组记录path里都放了哪些元素了
注意树枝去重和数层去重的区别。
去重问题:利用全局变量used进行去重;利用set集合进行去重。
回溯问题的时空复杂度情况
子集问题分析:
时间复杂度:O(2n),因为每一个元素的状态无外乎取与不取,所以时间复杂度为O(2n)
空间复杂度:O(n),递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n)
排列问题分析:
时间复杂度:O(n!),这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * … 1 = n!。
空间复杂度:O(n),和子集问题同理。
组合问题分析:
时间复杂度:O(2^n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。
空间复杂度:O(n),和子集问题同理。
N皇后问题分析:
时间复杂度:O(n!) ,其实如果看树形图的话,直觉上是O(n^n),但皇后之间不能见面所以在搜索的过程中是有剪枝的,最差也就是O(n!),n!表示n * (n-1) * … * 1。
空间复杂度:O(n),和子集问题同理。
解数独问题分析:
时间复杂度:O(9^m) , m是’.'的数目。
空间复杂度:O(n2),递归的深度是n2
其他注意点:
- 在记录结果时注意进行copy():result.append(path.copy())
- 在一些问题中需要先对原始列表进行排序:nums = sorted(nums)