回溯第四天,491. 非递减子序列,46. 全排列,47. 全排列 II,51. N 皇后。感觉就是模板人,有原先做过的模板的题目做起来就很顺利。刚进入全排列的题目,简单题做起来也很头疼,还是感觉思维能力差了很多,思维转变起来很慢。后来有时间又补了一道N皇后。
491. 非递减子序列
子集类型题目,但稍微有些变化,整体思路不变。
参数:经典的index
停止条件:类似子集Ⅱ,无需return,因为每个节点都是需要添加的,不像之前组合题目达到一定条件这个叶子节点就不再递归下去了。然后是这个题的特殊判断,就是len>1
单层循环:设置used这个set,来判断每个树层内,元素是否有重复的,即nums[i] in used, 注意这个要在for循环前,是每层一个used,为了解释生成一下结构。然后另一个判断就是path and nums[i] < path[-1],即需要添加的元素和path内最后一个元素比较。后续就是常规的增加,递归,回溯,注意这里多一个在set中添加的操作。
Level 0: back(0), path=[], used={}
├─ i=0, nums[i]=4, used={}, 选 → path=[4]
│
│ Level 1: back(1), path=[4], used={}
│ ├─ i=1, nums[i]=6, used={}, 选 → path=[4,6]
│ │
│ │ Level 2: back(2), path=[4,6], used={}
│ │ ├─ i=2, nums[i]=7, used={}, 选 → path=[4,6,7]
│ │ │
│ │ │ Level 3: back(3), path=[4,6,7], used={}
│ │ │ ├─ i=3, nums[i]=7, used={}, 选 → path=[4,6,7,7] ✅
│ │ │ └─ 回溯 path=[4,6,7]
│ │ └─ i=3, nums[i]=7, used={7},🚫 跳过
│ ├─ i=2, nums[i]=7, used={6}, 选 → path=[4,7]
│ │
│ │ Level 2: back(3), path=[4,7], used={}
│ │ ├─ i=3, nums[i]=7, used={}, 选 → path=[4,7,7] ✅
│ │ └─ 回溯 path=[4,7]
│ └─ i=3, nums[i]=7, used={6,7},🚫 跳过
├─ i=1, nums[i]=6, used={4}, 选 → path=[6]
│
│ Level 1: back(2), path=[6], used={}
│ ├─ i=2, nums[i]=7, used={}, 选 → path=[6,7]
│ │
│ │ Level 2: back(3), path=[6,7], used={}
│ │ ├─ i=3, nums[i]=7, used={}, 选 → path=[6,7,7] ✅
│ │ └─ 回溯 path=[6,7]
│ └─ i=3, nums[i]=7, used={7},🚫 跳过
├─ i=2, nums[i]=7, used={4,6}, 选 → path=[7]
│
│ Level 1: back(3), path=[7], used={}
│ ├─ i=3, nums[i]=7, used={}, 选 → path=[7,7] ✅
│ └─ 回溯 path=[7]
└─ i=3, nums[i]=7, used={4,6,7},🚫 跳过
class Solution:
def findSubsequences(self, nums: List[int]) -> List[List[int]]:
def back(index):
if len(path) > 1:
res.append(path.copy())
used = set()
for i in range(index, len(nums)):
if (path and nums[i] < path[-1]) or nums[i] in used:
continue
used.add(nums[i])
path.append(nums[i])
back(i+1)
path.pop()
res = []
path = []
back(0)
return res
47. 全排列 II
回溯新的类型,全排列。和46. 全排列相比,依旧是多了个去重的操作,但是这里的去重比之前的要麻烦也要难理解一下。
参数:因为是全排列,不像切割那样index往前移动,所以参数不用index了。但是需要一个新的used的数组,这个数组主要功能是判断同一树层的元素是否使用过。
停止条件:因为是全排列,所以就是当path的长度等于原数组长度就停止
单层递归:两个去重判断,第一个判断是路径内是否重复使用,及纵向是否重复使用,原因是我们这里没有递归i+1而是used,所以这里要去重,不然就会出现重复使用某个单个元素的情况。第二个是判断树层内是否重复使用,及横向是否重复。如看到下面代码中level = 0, i 为1.2.3的情况,其中i = 1的时候,used[i-1]是false,这就证明了是树层上的去重,如果是true,就是树枝去重,因为证明了前一个1选了,在判断第二个1选不选。如果表明前一个1没选,就开始判断第二个1选不选,就可以知道是开始在层中判断,也就是level0, i = 1的时候。判断完之后,进入常规增加,递归,回溯。然后注意即时维护used这个set里面的T/F以保证判断正确,也就是说,你在path中append了,就把当前i在used里改为True,你在path中pop了,就把i在used里改为False。
Level 0: path=[], used=[F, F, F]
├─ i=0, 选 1 → path=[1], used=[T, F, F]
│
│ Level 1: path=[1], used=[T, F, F]
│ ├─ i=0, used[0]=T → 跳过
│ ├─ i=1, nums[1]==nums[0] && used[0]=T ✅允许 → 选 1 → path=[1,1], used=[T, T, F]
│ │
│ │ Level 2: path=[1,1], used=[T, T, F]
│ │ ├─ i=2, 选 2 → path=[1,1,2] ✅
│ │ └─ 回溯 path=[1,1]
│ ├─ i=2, 选 2 → path=[1,2], used=[T, F, T]
│ │
│ │ Level 2: path=[1,2], used=[T, F, T]
│ │ ├─ i=1, 选 1 → path=[1,2,1] ✅
│ │ └─ 回溯 path=[1,2]
│ └─ 回溯 path=[1]
├─ i=1, nums[1]==nums[0] && used[0]=F ❌剪枝 → 跳过
├─ i=2, 选 2 → path=[2], used=[F, F, T]
│
│ Level 1: path=[2], used=[F, F, T]
│ ├─ i=0, 选 1 → path=[2,1], used=[T, F, T]
│ │
│ │ Level 2: path=[2,1], used=[T, F, T]
│ │ ├─ i=1, nums[1]==nums[0] && used[0]=T ✅允许 → 选 1 → path=[2,1,1] ✅
│ │ └─ 回溯 path=[2,1]
│ └─ i=1, nums[1]==nums[0] && used[0]=F ❌剪枝 → 跳过
class Solution:
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
def subtrack(used):
if len(path) == len(nums):
res.append(path.copy())
return
for i in range(len(nums)):
if used[i]:
continue
if (i > 0 and nums[i] == nums[i - 1] and not used[i - 1]):
continue
used[i] = True
path.append(nums[i])
subtrack(used)
path.pop()
used[i] = False
nums.sort()
used = [False] * len(nums)
res = []
path = []
subtrack(used)
return res
51. N 皇后
难,但没有想象中那么难,不是那种毫无头绪一点办法没有的题,只是需要套用回溯模板,多一点判断和思考,至少一刷的时候把大框架都写完了,有一两个小问题没改出来,已经很满意了。
既然是回溯问题,就依旧按照递归三步套模板走。但是实际上做的时候,是并没有严格按照三步的。因为一时间想不全我需要什么参数,所以自己思考的步骤如下:
首先感到这道题简单的地方在于,能够把棋盘规划成一个树结构。就像原本for循环遍历层,递归遍历枝叶,那么就相当于棋盘,用row去进行行遍历,col去进行列遍历。等效过来,原本回溯模板中for循环的i,就是col,所需要的参数index,就是row。
写判断函数,从IP地址那道题学来的,判断条件过多的时候单独写判断函数,这样递归函数内就比较清晰。第一是列上不能相等,这时候写的时候不知道用什么来判断,就写了个Q,没想到确实是用Q判断。后面的判断比较复杂,因为代码左上右下这样一条线写不到一起,所以一开始分别写了左上,左下,右上,右下四个方向。后来想了一下发现考虑复杂了,因为递归是一行一行进行的,所以无需判断未来行的情况,所以只有左上,右上。然后写的时候犯错是没有用i,j所以导致写起来很复杂,而且当前chess[row][col]无需判断,直接从-1/+1判断就行。
然后开始递归函数,卡的一个点是不知道怎么填充,原计划是用path分别添加.和Q然后整合加入res,但是发现很复杂,就不太清楚怎么做了。看了题解才发现忘记先搭建个全是.的棋盘,然后到对应位置改为Q就行。
终止条件很简单,当row == n停止就行,就代表已经把0到n-1行遍历完了。然后将棋盘加到res就行。
单层遍历过程:依旧是某种意义上的增加,递归,回溯,只不过这次不是从无到有,而是先画好了棋盘进行修改就行。这里是卡了一下但还是做完了,增加就是保留原本的chess[row][:col]不变,改下一个.为Q,再保留原本chess[row][col+1:]不变就行,回溯就是原方法Q改回.就行。
其他都是常规回溯写法
class Solution:
def isvalid(self, row, col, chess, n):
for i in range(row): # 树结构是行遍历,只需检查每行中是否有col相等
if chess[i][col] == 'Q':
return False
i, j = row - 1, col - 1
while i >= 0 and j >= 0: #左上
if chess[i][j] == 'Q':
return False
i -= 1
j -= 1
i, j = row - 1, col +1
while i >= 0 and j < n: #右上
if chess[i][j] == 'Q':
return False
i -= 1
j += 1
return True
def backing(self, row, chess, res, n):
if row == n:
res.append(chess[:])
return
for col in range(n):
if self.isvalid(row, col, chess, n):
chess[row] = chess[row][:col]+'Q'+chess[row][col+1:]
self.backing(row+1,chess,res,n)
chess[row] = chess[row][:col]+'.'+chess[row][col+1:]
def solveNQueens(self, n: int) -> List[List[str]]:
chess = ['.' * n for _ in range(n)]
res = []
self.backing(0, chess, res, n)
return res