回溯算法可以看成走迷宫,不知道出口在哪,所以只能不断深入,尝试不同的路线。但一旦找到出口便可以回溯到起点,辩清路线。
- 回溯算法
- 遍历所有排序方式
- 经典问题的组合
- 查找单词问题
- 八皇后问题
- 解数独
回溯算法
简单来说,回溯采用试错的方法解决问题。一旦发现当前步骤失败,回溯方法就返回一个步骤,选择另一种方案继续试错。当没有尝试所有路线时,就找到正确路线,可见回溯算法是一个优点是搜索速度快。当然,如果恰好在最后一个分支的低端,那么该方案就没有特别的优势了。
回溯算法又称为试探法,其特点:
- 问题的答案有多个元素
- 答案需要满足的约束条件
- 寻找答案的方式在每一步骤相同
- 逐步构建答案
遍历所有排序方式
现有要读的4本书,《Gevent指南》,《PythonCookBook》,《高性能MySql》,《编程珠玑》,每次只能从图书馆借一本,一共有多少中借书的顺序呢,若学过统计学,会有 4!= 24 种不同的排序方式。但,要的到的不只是总数,还想一一输出所有的排序方式
问题求解
-
首先,有四个空位,第一个空位有4中选择,假设第一个位置为《编程珠玑》,那么第二个空位就有3个选择,假设第二个位置为《PythonCookBook》,那么第三个空位就有2个选择,假设第三个位置为《Gevent指南》,最后第四个空位就只能是《高性能Mysql》
def solve_permutataion_bad_way(array): solution = [] for i in range(len(array)): ... for j in range(len(array1)): ... for x in range(len(array2)): ... for y in range(len(array3)): ...
-
若这个构思,要提前知道数组的长度,代码十分累赘。如果,在排第一本书到 时候,有4种选择,答案集合就在这4中选择在各结尾增加剩下的三本书的排序集合,那么,只需要得出剩余的三本书的排序集合就可以了。
-
同样,排着三本书时第一本数有三种选择,所以只需要得出剩余的两本书的排序集合就可以了,那么,推到最后,排两本书时,只需要知道最后一本是什么就可以了。
-
定义一个方法来帮助概括重复代码
代码实现
class Solution(object):
def helper(self, array, solution):
# 如果没有剩余书籍
if len(array) == 0:
# 输出结果
print(solution)
# 返回上一级
return
# 遍历书籍
for i in range(len(array)):
# 新建排序列表
solution_new = solution + [array[i]]
# 新建书籍列表
array_new = array[:i] + array[i+1: ]
# 回溯
self.helper(array_new, solution_new)
def solve_permutation(self, array):
self.helper(array, [])
book_li = [
'《Gevent指南》',
'《PythonCookBook》',
'《高性能MySql》',
'《编程珠玑》',
]
print(type(book_li))
so = Solution()
so.solve_permutation(book_li)
# >>>
经典问题的组合
现要选择两种水果,有四种选择:A.香蕉,B.橙子, C.葡萄,D.荔枝。一共有多少种选择呢?用数学的方法容易得出C4^2 = 6种不同的选课组合,不过,现在要将这些组合一一输出。
问题求解
- 首先有两个空位,第一次选择时有4中选择,A,B,C,D。
- 假设选择A,那么第二次选择就有3种选择。
- 假设选择B,得出的是AB这个组合
代码实现
class Solution(object):
def helper(self, array, n, solution):
# 如果 solution中有n门课程
if len(solution) == n:
# 输出结果
print(solution)
# 返回被调用的地方
return
# 遍历每一种水果
for i in range(len(array)):
# 把水果加入新的组合列表
solution_new = solution + [array[i]]
# 创建新水果列表,更新列表
array_new = array[i+1: ]
# 调用重复方法选择剩余课程
self.helper(array_new, n, solution_new)
def solve_combination(self, array, n):
self.helper(array, n, [])
fruits = ['A.香蕉', 'B.橙子,', 'C.葡萄', 'D.荔枝']
so = Solution()
so.solve_combination(fruits, 2)
### >>>
'''
['A.香蕉', 'B.橙子,']
['A.香蕉', 'C.葡萄']
['A.香蕉', 'D.荔枝']
['B.橙子,', 'C.葡萄']
['B.橙子,', 'D.荔枝']
['C.葡萄', 'D.荔枝']
'''
查找单词问题
在一堆字母中通过上下左右找出单词,游戏的盘面如图
遵守规则找出4个单词:'world','week','rose','reef'。
给出一个单词,目标是通过回溯算法得出这个单词是否存在于盘面中
比如,'world',找出就返回True,否则返回False
问题求解
-
以'week'为例,通过模拟算法,找出写代码的逻辑
-
首先,需要看盘面中25个字母中有没有 'w'。
-
找到第一个'w' , 第二字母'e',就要查看'w' 的上下左右有没有'e',没有'e',就找第二个'w'
-
找到第二个'w',发现它的左边右边都是'e',遵守上下左右的顺序,选择左边的'e'
-
接下来,就一一找出'week',最后返回True
思路代码
def word_search_bad_way(board, word):
"""
:param board: 盘面, 二维数组
:param word: 单词
:return:
"""
for i in range(len(board)):
# 从任何一个字母开始
for j in range(len(board[0])):
# 如果board[i][j]是单词的第一个字母
if board[i][j] == word[0]:
# 将字母改成空, 以免二次利用
board[i][j] = ''
# 如果[i][j]的上面为单词的第二个字母
if board[i-1][j] == word[1]:
board[i-1][j] = ''
# 上
if board[i-2][j] == word[1]:
...
return True
# 下
if board[i][j] == word[1]:
...
return True
# 左
if board[i-1][j-1] == word[1]:
...
return True
# 右
if board[i-1][j+1] == word[1]:
...
return True
# 上下左右都不是, 改回盘面原来的字母
board[i-1][j] = word[1]
# 如果[i][j]下面是单词的第二个字母
if board[i+1][j] == word[1]:
# 寻找第三个字母
...
# 如果[i][j]左面是单词的第二个字母
if board[i][j-1] == word[1]:
# 寻找第三个字母
...
# 如果[i][j]上面是单词的第二个字母
if board[i][j+1] == word[1]:
# 寻找第三个子没有
...
# 上下左右一圈下来没找到单词, 改回原来字母, 从下一个字母开始
board[i][j] = word[0]
# 没有找到单词, 返回False
return False
基本逻辑就是:
- 从盘面上任何字母开始, 如果是单词的首字母, 看它的上下左右有没有单词的第二个字母
- 如果有,再看第二个字母的上下左右有没有单词的第三个字母
- 一直检查,直到找到单词返回True,或者没有,就改变第一个字母
- (需要注意找到一个对应的字母,要从盘面删除,以免复用,造成死循环)
代码实现
- 检查当前盘面坐标对应的子没有是不是剩余单词的首字母
- 如果对应,检查左边的上下左右是否对用剩余单词的首字母
- 检查是否已经找到单词
class Solution(object):
def helper(self, board, current, row, col):
"""
:param board: 游戏的盘面, 由字母组成的二维列表, M*N
:param current: 剩余的单词字母
:param row: 当前字母在盘面的横坐标
:param col: 当前字母在盘面的纵坐标
:return:
"""
# 如果找到单词, 返回True
if len(current) == 0:
return True
if row >= 0 and row < len(board) and col >= 0 and col < len(board[0]):
# 如果有字母对应剩余单词字母
if board[row][col] == current[0]:
# 把该字母从盘面去除
board[row][col] = ''
# 检查该字母的下一个字母, 上
if self.helper(board, current[1:], row-1, col):
return True
#下
if self.helper(board, current[1:], row+1, col):
return True
#左
if self.helper(board, current[1:], row, col-1):
return True
# 右
if self.helper(board, current[1:], row, col+1):
return True
# 未找到该单词的下一个字母, 填充原来的字母
board[row][col] = current[0]
# 上下左右都没找到剩余字母, 返回False
return False
def word_search(self, board, word):
"""
:param board:
:param word: 要找的单词
:return:
"""
# 遍历盘面
for i in range(len(board)):
for j in range(len(board[0])):
if self.helper(board, word, i, j):
return True
return False
board = [
['a', 'c', 'r', 'y', 'l'],
['l', 'w', 'o', 'r', 'i'],
['a', 'f', 'd', 'l', 'c'],
['k', 'e', 'e', 'w', 'e'],
['o', 'd', 'r', 'o', 's'],
]
so = Solution()
sign = so.word_search(board, 'week')
print(sign)
八个皇后问题
在国际象棋中,皇后能够上下左右斜无束缚地进行攻击。
八个皇后问题的原题是如何在8*8的棋盘中放置8个皇后,并令她们互相攻击不到对方。
八个皇后问题是回溯算法的代表性问题之一。
问题求解
- 首先,问题中说到的每一行,每一列和每一条斜线上都必须有且只能有一个皇后,它告诉了我们第一行的8个格子中肯定有一名保安,可以从这里入手。
- 利用每一行必须有一个皇后的前提条件,假设第一行有一个皇后,会首先假设在(1, 1)格,然后再判断这个假设时候成立
- 但不能直接判断皇后A是否能够留在(1, 1)格,只能继续假设,一直假设下去。
- 假设的过程中有两种可能的结果
- 成功假设到第8行,也就是说,找到正确答案之一
- 在没到第8行的时候走不下去,陷入死局
- 若是第一种假设,则直接输出结果,若是第二种,则退一步,重新假设前一行的皇后位置,或许退到第一行,重新假设皇后A的位置
代码实现
- 假设当前皇后可以处的不同位置
- 判断假设的保安位置是否合理
- 如果合理,继续假设下面几行的皇后位置
- 判断8名皇后是否都已经安置成功
模拟4*4
def solve_NQueen_bad_way(self):
# 用于存储4名皇后位置的数组
col_positions = [-1] *4
for i in range(4):
# 假设第一行的皇后的位置
col_positions[0] = i
# 假设成立
if self.is_valid(col_positions, 0):
for i in range(4):
# 假设第二行的皇后的位置
col_positions[1] = i
# 假设成立
if self.is_valid(col_positions, 1):
for i in range(4):
# 假设第三行的皇后的位置
col_positions[2] = i
# 假设成立
if self.is_valid(col_positions, 2):
for i in range(4):
# 假设第四行的皇后的位置
col_positions[3] = i
# 假设成立
if self.is_valid(col_positions, 3):
# 输出结果
self.print_solution(col_positions, 4)
回溯算法实现
class Solution(object):
def print_solution(self, col_positions, n):
print('=====华丽分割线=====')
for row in range(n):
line = ''
for col in range(n):
if col_positions[row] == col:
line += 'Q '
else:
line += '. '
print(line)
def is_valid(self, col_positions, row_index):
"""
检查位置是否合理
:param col_positions: 用于存储皇后位置的数组
:param row_index: 记录当前行, 从0 到8
:return:
"""
for i in range(row_index):
# 检查同列是否有保安
if col_positions[i] == col_positions[row_index]:
return False
# 检查两条斜线上是否有保安
elif abs(col_positions[i] - col_positions[row_index]) == row_index - i:
return False
return True
def helper(self, col_positions, row_index, n):
# 如果走完所有行, 输出结果, 返回到上一行的假设
if row_index == n:
self.print_solution(col_positions, n)
return
for col in range(n):
# 假设 第row_index行的皇后位置
col_positions[row_index] = col
# 如果可行
if self.is_valid(col_positions, row_index):
# 继续假设剩余行的皇后位置
self.helper(col_positions, row_index+1, n)
def solve_NQueens(self, n):
self.helper([-1]*n, 0, n)
Solution().solve_NQueens(8)