0 前言
最近做了不少关于回溯法的算法题,算是积累了一些小小的心得,这篇文章算是对回溯法的一个小总结。
1 回溯法简介
回溯法简单来说就是按照深度优先的顺序,穷举所有可能性的算法,但是回溯算法比暴力穷举法更高明的地方就是回溯算法可以随时判断当前状态是否符合问题的条件。一旦不符合条件,那么就退回到上一个状态,省去了继续往下探索的时间。
例如上图的四皇后问题,当判断出当前状态下皇后的位置有冲突时,便不再继续往下探索这种摆放的可能性,极大的缩短了我们的解空间。
状态的返回只有当前的子结点不再满足问题的条件或者我们已经找到了问题的一个解时,才会返回,否则会以深度优先一直在解空间树内遍历下去。
当然,对于某些问题如果其解空间过大,即使用回溯法进行计算也有很高的时间复杂度,因为回溯法会尝试解空间树中所有的分支。所以根据这类问题,我们有一些优化剪枝策略以及启发式搜索策略。
所谓优化剪枝策略,就是判断当前的分支树是否符合问题的条件,如果当前分支树不符合条件,那么就不再遍历这个分支里的所有路径。
所谓启发式搜索策略指的是,给回溯法搜索子结点的顺序设定一个优先级,从该子结点往下遍历更有可能找到问题的解。
2 回溯函数的组成
1.回溯出口,判断是否找到了问题的解。
2.回溯主体,遍历当前的状态下的所有子结点,并判断该子结点是否是还满足问题的条件,如果满足问题条件,那么进入该子结点继续向下搜索,若不满足则跳过该结点。
3.状态返回,如果当前状态不满足条件,那么返回到前一个状态。
简单的回溯函数基本形式:
def backtrack(current_statement) -> bool:
if condition is satisfy:
solution = current_statement
return True
else:
for diff_posibility in current_statement:
next_statement = diff_posibility
if next_statement is satisfy condition:
if backtrack(next_statement):
return True
else:
back to current_statement
return False
3 问题1 —— 解数独
1.问题描述:
给定一个部分填充的数字表格,通过已填充的空格来解决数独问题。
37. 解数独 - 力扣(LeetCode)leetcode-cn.com2. 问题分析:
首先,根据数独的规则,如果我们在某个空格填了一个数字,那么该数字所在的行与列还有九宫格不能够与原来的数字重复。根据这个条件判断我们当前的状态是否合法,如果遇到不合法的数字组合,要回退到上一个合法状态,我们用一个三维列表记录此时的状态。
'''
三个二维列表(9*9)(代码里整合到了一个三维列表里),元素全部初始化为False。
第一个二维列表的某个位置(i,j),表示第i行是否有数j在,如果有是True,没有是False。
第二个二维列表的某个位置(i,j),表示第i列是否有数j在,如果有是True,没有是False。
第三个二维列表的某个位置(i,j),表示第i个box是否有数j在,如果有是True,没有是False。
'''
jugement = [[[False for _ in range(0,9)] for _ in range(0,9)] for _ in range(0,3)]
判断当前的状态是否合法,如果状态合法,按照深度优先的顺序继续往下探索。
def isValid(board,position,value):
num = int(value)
row = position[0]
column = position[1]
box_index = int(row/3) * 3 + int(column/3)
if jugement[0][row][num-1] or jugement[1][column][num-1] or jugement[2][box_index][num-1]:
return False
else:
jugement[0][row][num-1] = True
jugement[1][column][num-1] = True
jugement[2][box_index][num-1] = True
return True
如果当前状态不合法,要回退会上一个状态。
# 将已经填入到board内的数字重置
board[position[0]][position[1]] = '.'
# 将判断矩阵相应元素重置
jugement[0][position[0]][int(value)-1] = False
jugement[1][position[1]][int(value)-1] = False
jugement[2][box_index][int(value)-1] = False
3. 完整代码:
class Solution:
def solveSudoku(self, board):
location = []
count = 1
jugement = [[[False for _ in range(0,9)] for _ in range(0,9)] for _ in range(0,9)]
choose = [['1', '2', '3', '4', '5', '6', '7', '8', '9'],
['1', '2', '3', '4', '5', '6', '7', '8', '9'],
['1', '2', '3', '4', '5', '6', '7', '8', '9'],
['1', '2', '3', '4', '5', '6', '7', '8', '9'],
['1', '2', '3', '4', '5', '6', '7', '8', '9'],
['1', '2', '3', '4', '5', '6', '7', '8', '9'],
['1', '2', '3', '4', '5', '6', '7', '8', '9'],
['1', '2', '3', '4', '5', '6', '7', '8', '9'],
['1', '2', '3', '4', '5', '6', '7', '8', '9']]
for row in range(9):
for column in range(9):
box_index = int(row/3) * 3 + int(column/3)
if board[row][column] is '.':
location.append((count,row, column))
count += 1
else:
choose[row].remove(board[row][column])
jugement[0][row][int(board[row][column])-1] = True
jugement[1][column][int(board[row][column])-1] = True
jugement[2][box_index][int(board[row][column])-1] = True
location.append((count,-1,-1))
def isValid(board,position,value):
num = int(value)
row = position[0]
column = position[1]
box_index = int(row/3) * 3 + int(column/3)
if jugement[0][row][num-1] or jugement[1][column][num-1] or jugement[2][box_index][num-1]:
return False
else:
jugement[0][row][num-1] = True
jugement[1][column][num-1] = True
jugement[2][box_index][num-1] = True
return True
def back(board:list,current_location:tuple):
position = (current_location[1],current_location[2])
next_count = current_location[0]
if position == (-1, -1):
return True
for value in choose[current_location[1]]:
if isValid(board,position,value) is True:
board[position[0]][position[1]] = value
next_positon = location[next_count]
if back(board,next_positon) is True:
return True
else:
box_index = int(position[0]/3) * 3 + int(position[1]/3)
board[position[0]][position[1]] = '.'
jugement[0][position[0]][int(value)-1] = False
jugement[1][position[1]][int(value)-1] = False
jugement[2][box_index][int(value)-1] = False
return False
back(board, location[0])
4 问题2 —— 组合总数
1.问题描述:
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取,并且所有数字(包括 target)都是正整数,解集不能包含重复的组合。
39. 组合总和 - 力扣(LeetCode)leetcode-cn.com2.问题分析:
首先对candidates进行升序排序,那么搜索结点的选择顺序就是从小到大进行搜索。
每遇到一个数字,将该数字加入到我们的当前组合中,对组合数字进行累加,传递到下一个状态。
如上图所示,如果在某个结点下,我们找到了当前的结点代表的累加值就是我们的target,那么该层的剩余结点不再遍历,并且以该结点为根结点的子树也不再遍历。
原因是:
- candidates中只包含正数,因此以该结点为根结点的子树下的任意一个结点代表的累加值都会大于target。
- candidates的数字不重复,且我们对candidates进行过升序排序,因此该结点的右兄弟结点所代表的累加值一定也会大于target。
通过上述的分析,我们就对解空间树进行了剪枝,减少了不必要的搜索空间。
3.完整代码:
class Solution:
def combinationSum(self, candidates: list, target: int) -> list:
candidates = sorted(candidates)
answer = []
def backtrack(current_sum:int=0,current_list:list=[]):
if current_sum == target:
if sorted(current_list) not in answer:
answer.append(current_list)
else:
for number in candidates:
if current_sum + number > target:
break
else:
backtrack(current_sum+number,current_list+[number])
backtrack()
return answer
5 回溯算法3 —— 骑士巡游问题
1.问题描述:
在 8 × 8 方格的国际象棋棋盘上,骑士从任意指定的方格出发,以跳马规则(横一步竖两步或横两步竖一步),周游棋盘的每一个格子,要求每个格子只能跳过一次。
2.问题分析:
骑士巡游问题的一个启发式搜索策略是将当前的子结点排一个序,按照当前子结点的邻接格子的个数从小到大排序,这个搜索策略就是先尽量遍历棋盘的边缘,到后期骑士自然而然会遍历棋盘中间,到那时候中间可走的路径要更多,从而更有可能找到问题的解。
首先我们要判断当前的位置下,骑士能够走的下一个合法位置(不能够超过棋盘的边界,并且是一个完全没有走过的新位置),然后记录下一个位置的邻接格子的个数并按照这个进行排序。
这种搜索策略算是一种基于贪心算法的策略,其优点是能够加快我们寻找一种解的速度,但是对于寻找骑士巡游问题的全部解而言,仍然没有起到一个加速的作用。
3.完整代码:
import matplotlib.pyplot as plt
class Solution:
def knight_moves(self,start_location):
answer = []
answer.append(start_location)
board = [[0 for _ in range(8)] for _ in range(8)]
board[start_location[0]][start_location[1]] = 1
def backtrack(current_location):
if len(answer) == 64:
return True
next_node = self.choose_node(board,current_location)
for next_location in next_node:
answer.append(next_location)
board[next_location[0]][next_location[1]] = 1
if backtrack(next_location):
return True
else:
answer.pop()
board[next_location[0]][next_location[1]] = 0
return False
backtrack(start_location)
return answer
def not_touch(self,next_location):
if next_location[0] in range(0,8) and next_location[1] in range(0,8):
return True
else:
return False
def move(self,current_location,mode):
a = current_location[0]
b = current_location[1]
if mode == 1:
next_location = [a-1,b+2]
elif mode == 2:
next_location = [a+1,b+2]
elif mode == 3:
next_location = [a-2,b-1]
elif mode == 4:
next_location = [a-2,b+1]
elif mode == 5:
next_location = [a-1,b-2]
elif mode == 6:
next_location = [a+1,b-2]
elif mode == 7:
next_location = [a+2,b-1]
else:
next_location = [a+2,b+1]
return next_location
def choose_node(self,board,current_location):
nodes = []
for mode in range(1,9):
next_location = self.move(current_location,mode)
if self.not_touch(next_location) == True and board[next_location[0]][next_location[1]] == 0:
number_nor = self.number_nor(next_location)
nodes.append([next_location,number_nor])
nodes = sorted(nodes,key=lambda x:x[1])
new_nodes = []
for item in nodes:
new_nodes.append(item[0])
return new_nodes
def number_nor(self,location) -> int:
if location == [0,0] or location == [7,7] or location == [0,7] or location == [7,0]:
return 2
if location[0] == 0 or location[0] == 7 or location[1] == 0 or location[1] == 7:
return 3
else:
return 4
if __name__ == '__main__':
ans = Solution().knight_moves([0,0])
x = [item[0] for item in ans]
y = [item[1] for item in ans]
plt.plot(x,y,label='Path')
plt.xlabel('row')
plt.ylabel('column')
plt.title('Knight Moves')
plt.scatter(x[0],y[0],c='green',marker='x',label='Start location')
plt.scatter(x[1:],y[1:],c='red',label='Path location')
plt.legend(loc='best')
plt.show()
运行结果图:
6 回溯算法总结
对于回溯法,上述的例子已经讲解的差不多了,其实分析一个问题如何用回溯法解决,关键部分在于如何高效地判断当前的状态是否合法,如果状态不合法,果断剪枝,并回退到上一个状态。在搜索时,针对问题的特点,来制定一个启发式的搜索策略,也能够提高我们寻求问题的解的效率。