回溯问题(python版)
文章目录
回溯算法:是五大常用算法之一、回溯算法实际上一个类似枚举的搜索尝试过程。
主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。再或者简单点说,用来解决一个不知道要递归多少层的循环体。
一般步骤:
1、 针对所给问题,定义问题的解空间,它至少包含问题的一个(最优)解。
2 、确定易于搜索的解空间结构,使得能用回溯法方便地搜索整个解空间 。
3 、以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。
确定了解空间的组织结构后,回溯法就从开始结点(根结点)出发,以深度优先的方式搜索整个解空间。
这个开始结点就成为一个活结点,同时也成为当前的扩展结点。在当前的扩展结点处,搜索向纵深方向移至一个新结点。这个新结点就成为一个新的活结点,并成为当前扩展结点。
如果在当前的扩展结点处不能再向纵深方向移动,则当前扩展结点就成为死结点。此时,应往回移动(回溯)至最近的一个活结点处,并使这个活结点成为当前的扩展结点。回溯法即以这种工作方式递归地在解空间中搜索,直至找到所要求的解或解空间中已没有活结点时为止。
基本思想:
从一条路往前走,能进则进,不能进则退回来,换一条路再试。算法搜索至解空间树的任一结点时,总是先判断该结点是否肯定不包含问题的解。如果肯定不包含,则跳过对以该结点为根的子树的系统搜索,逐层向其祖先结点回溯。否则,进入该子树,继续按深度优先的策略进行搜索。回溯法在用来求问题的所有解时,要回溯到根,且根结点的所有子树都已被搜索遍才结束。而回溯法在用来求问题的任一解时,只要搜索到问题的一个解就可以结束。这种以深度优先的方式系统地搜索问题的解的算法称为回溯法,它适用于解一些组合数较大的问题。
一般模型:
def backward():
if (回朔点):# 这条路走到底的条件。也是递归出口
保存该结果
return
else:
for route in all_route_set : 逐步选择当前节点下的所有可能route
if 剪枝条件:
剪枝前的操作
return #不继续往下走了,退回上层,换个路再走
else:#当前路径可能是条可行路径
保存当前数据 #向下走之前要记住已经走过这个节点了。例如push当前节点
self.backward() #递归发生,继续向下走一步了。
回朔清理 # 该节点下的所有路径都走完了,清理堆栈,准备下一个递归。例如弹出当前节点
第一题:组合总数
1. 题目
给定一个无重复元素的数组candidates和一个目标数target,找出candidates中所有可以使数字和为target的组合。
candidates中的数字可以无限制重复被选取。
链接:https://leetcode-cn.com/problems/combination-sum/
2. 基本步骤
- 先对数组进行排序
- 从第一个元素开始,可以有两个选择(要这个元素和不要这个元素)。如果要,则把这个元素添加进入temp然后继续进行下一层(下一层还是可以从自己这个元素开始)。如果不要,则直接往右移动,不对temp进行添加操作
- 什么时候停止:如果temp的和大于target,如果index的值等于n。
- 什么时候添加:如果temp的和等于target。
- 从第几个开始:第0个元素,temp为
[]
3. 代码展示
def combinationSum(candidates: list, target):
candidates.sort()
n = len(candidates)
res = []
def dfs(index: int, temp: list):
if sum(temp) > target or index == n:
return
if sum(target) == target:
res.append(temp)
return
dfs(index, temp + [candidates[index]])
dfs(index + 1, temp)
dfs(0, [])
return res
第二题:组合总数II
1. 题目
给定一个数组candidates和一个目标数target,找出candidates中所有可以使数字和为target的组合。
candidates中的每个数字在每个组合中只能使用一次。
链接:https://leetcode-cn.com/problems/combination-sum-ii/
2. 思路
首先,还是和I类似,从第一个根路由开始,判断已存在的temp加上自己下标的值的和是否大于target,如果大于则直接返回,这里需要注意的是,如果还是按照I的方式使用temp来判断,则最后一个数字就无法进行判断,因为在最后一个下标进入的适合,temp是前一个即要么是[]要么是缺少该值的,所以我们就需要在判断index的时候就提前把index所对应的值加上去,当然是在index小于n的时候,如果大于了那就直接temp放入即可
3. 基本步骤
- 对数组进行排序和相应大的第一轮剪枝
- 从第一个元素开始,同样可以选择选或者不选。如果选这个元素,则就需要把当前下标的元素添加进temp,然后index+1进入循环,如果不选,那就直接index+1进入循环。
- 什么时候停止:如果当前的temp和大于taget或者index等于n
- 什么时候添加:如果当前的temp加上当前下标值的和等于target(为什么要加上当前的下标值?因为如果不添加,再倒数第二层的时候index已经是n-1,如果想进行最后一轮判断,那你就得让index变成n,而这就导致后面的获取下标值会越界,因此只能再倒数第二行加入下标值进行判断(后续很多题都需要这样,即我们把n作为停止的标志,但是最后一行又与n相关联))。别忘了,当你添加完之和就进行return,别不舍得return(吃亏的地方太多了)
4. 代码展示
def combinationSum2(candidates: list, target: int):
candidates.sort()
n = len(candidates)
res = []
if sum(candidates) < target:
return res
def dfs(index, temp):
if index < n:
new_temp = temp + [candidates[index]]
else:
new_temp = temp
if sum(new_temp) > target or index == n:
return
if sum(new_temp) == target:
if new_temp not in res:
res.append(new_temp)
return
dfs(index + 1, new_temp)
dfs(index + 1, temp)
dfs(0, [])
return res
第三题:组合总数III
1. 题目
题目:
找出所有相加之和为n的k个数的组合。组合中只允许含有1-9的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。解集不能包含重复的组合。
链接:https://leetcode-cn.com/problems/combination-sum-iii/
2. 思路
该思路还是和I与II相同,我们可以先创建出[1-9]的列表,然后同样使用dfs,如果长度大于k或者和大于n或者索引大于9时就返回。不然就判断这些值是否符合标准,符合就添加,不符合就继续。而这里需要注意,应为是不重复,按照I的方式,会出现最后一个无法加入的情况,因此我们需要在判断之前加入我们的下标值。
3. 基本步骤
- 创建一个列表(1-9)
- 从第一个元素开始,任然是可以选或者不选。如果选,则index+1,然后temp加上下标的值进入下一层。而如果不选,则直接index+1然后进入下一层
- 什么时候结束:如果整个长度大于k或者整个temp加起来大于target再或者index大于9
- 什么时候添加:如果temp加上下标值等于n并且长度为k时(为什么加下标值上面说过了,这里就不再赘述了)只是记得,别忘了return
4. 代码展示
def combinationSum3(k: int, n: int):
candidates = [i for i in range(1, 10)]
res = []
def dfs(index, temp):
if index < 9:
new_temp = temp + [candidates[index]]
else:
new_temp = temp
if len(new_temp) > k or sum(new_temp) > n or index > 9:
return
if sum(new_temp) == n and len(new_temp) == k:
if new_temp not in res:
res.append(new_temp)
return
dfs(index + 1, new_temp)
dfs(index + 1, temp)
dfs(0, [])
return res
第四题:组合总数IV
1. 题目
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
链接:https://leetcode-cn.com/problems/combination-sum-iv/
2. 思路
回溯也行,就是太费时间。按照之前的三题,只需要注意的是现在不在是从标记点开始,而是从头开始一次递归,所以需要使用for循环。
3. 代码展示(自己比较low,代码超时了)
def combinationSum4(nums: list, target: int):
nums.sort()
res = []
def dfs(temp):
for j in range(len(nums)):
if sum(temp) > target:
return
if sum(temp) == target:
if temp not in res:
res.append(temp)
return
dfs(temp + [nums[j]])
dfs([])
return len(res)
第五题:子集
1. 题目
给你一个整数数组 nums ,数组中的元素互不相同。返回该数组所有可能的子集(幂集)。解集不能包含重复的子集。你可以按任意顺序返回解集。
链接:https://leetcode-cn.com/problems/subsets/
2. 思路
利用回溯,从第一个元素开始,依次遍历到最后一个。(还是同样的道理,这个元素选还是不选,如果选,则把该元素添加进入temp然后进入下一轮,如果不选,则直接进入下一轮。这里是不重复,因此我们需要提前将该值添加进入temp然后进行添加,不然最后一个元素始终无法见天日)
3. 代码展示
def subsets(nums):
ans = [[],]
def dfs(i, temp):
if i == len(nums):
return
if i < len(nums):
new_temp = temp + [nums[i]]
else:
new_temp = temp
ans.append(new_temp)
dfs(i + 1, new_temp)
dfs(i + 1, temp)
dfs(0, [])
return ans
第六题:子集II
1. 题目
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
链接:https://leetcode-cn.com/problems/subsets-ii/
2. 说明
只需要添加一个排序,然后在1的基础上判断是否有重复元素即可
3. 代码展示
def subsetsWithDup(nums):
nums.sort()
ans = [[],]
def dfs(i, temp):
if i == len(nums):
return
if i < len(nums):
new_temp = temp + [nums[i]]
else:
new_temp = temp
if new_temp not in ans:
ans.append(new_temp)
dfs(i + 1, new_temp)
dfs(i + 1, temp)
dfs(0, [])
return ans
第七题:8皇后、N皇后问题
1. 题目
设计一种算法,打印 N 皇后在 N × N 棋盘上的各种摆法,其中每个皇后都不同行、不同列,也不在对角线上。这里的“对角线”指的是所有的对角线,不只是平分整个棋盘的那两条对角线。
链接:https://leetcode-cn.com/problems/eight-queens-lcci/
2. 思路
首先,我们还是使用回溯法,先确定一个初始点(0,0),查看是否符合,如果符合,则把当前位置标记为Q,然后进入下一层:从0开始到n依次判断是否合法,如果合法则重复上面步骤,如果不合法,都不合法,则退出,回到上一层,让上一层的Q变回.然后递归该点的右边,再次判重复判断。
3. 基本步骤
- 绘制好棋盘
- 创建检查函数:该函数用来判断当前节点是否合法(即整行、整列、主对角线和副对角线都不存在Q,后面会仔细说下这个地方)
- 同样的从第一个元素(0,0)开始,先判断这个位置是否合法。如果不合法则返回,如果合法,则把这个位置设置为Q,然后从0开始到n,依次查看下一层的元素是否都合法,如果下一层每个位置都不合法,则把该位置变回“.”,然后用下一个元素进行重新判断。而只要下一层元素有一个合法,那就可以进行下下层的递归。
- 什么时候结束:只要x或y超出边界即可结束,返回上一轮
- 什么时候添加:如果x的值为n-1,并且当前位置合法,那就把这个位置变成“Q”。接下来就是格式化每一个列,然添加到res即可,注意添加完之后,需要把现在这个位置变为“.”。最后就是return
4. 代码展示
def solveNQueens(n: int):
res = []
ans = [["." for _ in range(n)] for _ in range(n)] # 提前放置好棋盘
# 该函数是检查函数,用来查看当前坐标是否合法
def checkxy(x, y) -> bool:
# 纵向、横向
for i in range(n):
if ans[x][i] == "Q" or ans[i][y] == "Q":
return False
# 对角线判断
for i in range(n):
a = x - i
b = x + i
c = y - i
d = y + i
if a >= 0 and c >= 0:
if ans[a][c] == "Q":
return False
if a >= 0 and d < n:
if ans[a][d] == "Q":
return False
if b < n and c >= 0:
if ans[b][c] == "Q":
return False
if b < n and d < n:
if ans[b][d] == "Q":
return False
return True
# 主要的回溯函数
def dfs(x, y):
# 退出判断,如果超过边界,则直接返回
if y == n:
return
if x == n:
return
# 当达到最后一层并且该坐标合法,则把它加入进答案
if x == n - 1 and checkxy(x, y):
ans[x][y] = "Q" # 在该点放置Q
a = [] # 使用join格式化排列
for i in range(n):
a.append("".join(ans[i]))
if a not in res: # 判断是否存在答案中
res.append(a)
ans[x][y] = "." # 复原该点
return
if not checkxy(x, y): # 判断该点是否合法
return
else:
# 如果合法,则设置该坐标为Q,然后依次递归下一层的每一个元素,如果整层都不合法,则将该坐标退回为”.“,然后往右递归一个。
ans[x][y] = "Q"
for i in range(n):
dfs(x + 1, i)
else:
ans[x][y] = "."
dfs(x, y + 1)
dfs(0, 0)
return res
5. 额外说明检查函数
这个检查函数第一次的四个方向是这样写的:
for i in range(n):
# 横向
if ans[x][y - i] == Q:
return False
# 纵向
if ans[x - i][y] == Q:
return False
# 主对角线
if ans[x - i][y - i] == Q:
return False
# 副对角线
if ans[(x + i) // n][y - i] == Q:
return False
但是漏洞很大,刚刚开始以为两个对角线我可以使用负值进行索引,但是无论咋样都达不到理想的结果。也是调试半天一直以为是dfs代码的错误,后来检查之后才发现是这里的问题。我们来看一张图片:
对于(-1,-1)这个点,它不存在副对角线,而我们上面的函数通过负值可以获取到其他地方的值。同样的除了最中间的点,其他都或多或少会出现错误的点位。
因此,我们需要判断的是当前的x与i各自加上和减去的值是否合法,如果合法再进行判断,最后给出一个bool值。
# 对角线判断
for i in range(n):
a = x - i
b = x + i
c = y - i
d = y + i
if a >= 0 and c >= 0:
if ans[a][c] == "Q":
return False
if a >= 0 and d < n:
if ans[a][d] == "Q":
return False
if b < n and c >= 0:
if ans[b][c] == "Q":
return False
if b < n and d < n:
if ans[b][d] == "Q":
return False
第八题:阿里2021春招题第一批笔试题
1. 题目
题目(只记得大概):
首先输入一个n表示有n组数据,接下来的每一行都是一个整数,然后每个整数都组成一个n*3的矩阵,在矩阵的每个位置可以放入1,2,3。3张卡片,但是一个位置的上下左右不能出现相同的卡片。由于结果可能会过大,则直接取10**9+7的模
例子:
2
1
2
输出:
12
54
leetcode链接:https://leetcode-cn.com/problems/number-of-ways-to-paint-n-3-grid
2. 思路
这题的思路与n皇后有些类似,区别两点:n皇后只有一个值(“Q”),而这里是1,2,3。n皇后的check函数与这里不相同(思路类似,超过边界的不考虑)。这里就是判断这个位置是否可以放1,如果可以放1,那就查看他的右边和下边,如果不能,那就放2,如果再不能那就放3,如果都不能,就跳转到该层的上一层,让上一层进行更换。这里还是需要注意,如果成功那就需要把值复位。
3. 基本步骤
- 依次获取每一个值,然后创建空棋盘
- 从第一个数开始,考虑这个数可以放1么?如果可以,那就进入他的右边一个和下边一个进行判断是否符合。如果他的下边和右边都不行,则把这个位置变成2,如果还不行就变成3。如果这三个都不行,那就跳回上一层,但是这里的(x,y)被记录为3,会影响其他的判断,因此需要复位0。
- 判断函数:判断这个值的上下左右是否规范
- 什么时候结束:如果x等于n或者y等于3。
- 什么时候添加:这里还是同样的问题,当你进入倒数第二层的时候,此时的x已经是n-1了,而如果你还继续想按照之前的思路,是不可能进入最后一层的,也就是你会发现最后一个始终为0,这就需要你在x为n-1的时候就进行最后的判断。因此,如果x等于n-1,并且y==2时,你就可以进行最后一轮的判断了,从1-3依次判断放入这个地方是否可行,如果行,就加1,如果不行那就复位0,然后return,退回上一层。
4. 代码展示(错误代码 )
下面是错误的一个解法,1能过剩下的就都不行了。还是自己太菜,继续DP学习了,学完dp再战。
# -*- coding: utf-8 -*-
# @Auther:Summer
n = 2
def check(x, y):
model = res[x][y]
if x - 1 >= 0 and res[x - 1][y] == model:
return False
if x + 1 < n and res[x + 1][y] == model:
return False
if y - 1 >= 0 and res[x][y - 1] == model:
return False
if y + 1 < 3 and res[x][y + 1] == model:
return False
return True
def dfs(x, y):
global ans
if x == n:
return
if y == 3:
return
if x == n - 1 and y == 2:
for a in range(1, 4):
res[x][y] = a
if check(x, y):
ans += 1
res[x][y] = 0
return
for i in range(1, 4):
res[x][y] = i
if not check(x, y):
continue
else:
dfs(x, y + 1)
dfs(x + 1, y)
res[x][y] = 0
return ans
for i in range(n):
n = 1 # 这里使用sys获取本地的值,我这里默认为1
res = [[0, 0, 0] for _ in range(n)] # 设置棋盘
ans = 0
print(dfs(0, 0) % (10 ** 9 + 7))
总结
写了这么多,回溯也就稍微搞明白了,无非你需要搞清楚几个问题:
- 初始工作需要做什么
- 从第一个元素开始,他需要怎么走
- 最后一层是否需要再倒数第二层进行提前判断
- 是否需要复位
- 什么时候结束
- 什么时候添加,添加完是否需要复位
加以,继续努力!!!