五大经典算法:回溯问题(python版)

回溯问题(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. 基本步骤

  1. 先对数组进行排序
  2. 从第一个元素开始,可以有两个选择(要这个元素和不要这个元素)。如果要,则把这个元素添加进入temp然后继续进行下一层(下一层还是可以从自己这个元素开始)。如果不要,则直接往右移动,不对temp进行添加操作
  3. 什么时候停止:如果temp的和大于target,如果index的值等于n。
  4. 什么时候添加:如果temp的和等于target。
  5. 从第几个开始:第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. 基本步骤

  1. 对数组进行排序和相应大的第一轮剪枝
  2. 从第一个元素开始,同样可以选择选或者不选。如果选这个元素,则就需要把当前下标的元素添加进temp,然后index+1进入循环,如果不选,那就直接index+1进入循环。
  3. 什么时候停止:如果当前的temp和大于taget或者index等于n
  4. 什么时候添加:如果当前的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. 创建一个列表(1-9)
  2. 从第一个元素开始,任然是可以选或者不选。如果选,则index+1,然后temp加上下标的值进入下一层。而如果不选,则直接index+1然后进入下一层
  3. 什么时候结束:如果整个长度大于k或者整个temp加起来大于target再或者index大于9
  4. 什么时候添加:如果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. 基本步骤

  1. 绘制好棋盘
  2. 创建检查函数:该函数用来判断当前节点是否合法(即整行、整列、主对角线和副对角线都不存在Q,后面会仔细说下这个地方)
  3. 同样的从第一个元素(0,0)开始,先判断这个位置是否合法。如果不合法则返回,如果合法,则把这个位置设置为Q,然后从0开始到n,依次查看下一层的元素是否都合法,如果下一层每个位置都不合法,则把该位置变回“.”,然后用下一个元素进行重新判断。而只要下一层元素有一个合法,那就可以进行下下层的递归。
  4. 什么时候结束:只要x或y超出边界即可结束,返回上一轮
  5. 什么时候添加:如果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. 从第一个数开始,考虑这个数可以放1么?如果可以,那就进入他的右边一个和下边一个进行判断是否符合。如果他的下边和右边都不行,则把这个位置变成2,如果还不行就变成3。如果这三个都不行,那就跳回上一层,但是这里的(x,y)被记录为3,会影响其他的判断,因此需要复位0。
  3. 判断函数:判断这个值的上下左右是否规范
  4. 什么时候结束:如果x等于n或者y等于3。
  5. 什么时候添加:这里还是同样的问题,当你进入倒数第二层的时候,此时的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))

总结

写了这么多,回溯也就稍微搞明白了,无非你需要搞清楚几个问题:

  1. 初始工作需要做什么
  2. 从第一个元素开始,他需要怎么走
  3. 最后一层是否需要再倒数第二层进行提前判断
  4. 是否需要复位
  5. 什么时候结束
  6. 什么时候添加,添加完是否需要复位

加以,继续努力!!!

  • 9
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值