【核心算法5】回溯算法

回溯算法可以看成走迷宫,不知道出口在哪,所以只能不断深入,尝试不同的路线。但一旦找到出口便可以回溯到起点,辩清路线。

  • 回溯算法
  • 遍历所有排序方式
  • 经典问题的组合
  • 查找单词问题
  • 八皇后问题
  • 解数独

回溯算法

简单来说,回溯采用试错的方法解决问题。一旦发现当前步骤失败,回溯方法就返回一个步骤,选择另一种方案继续试错。当没有尝试所有路线时,就找到正确路线,可见回溯算法是一个优点是搜索速度快。当然,如果恰好在最后一个分支的低端,那么该方案就没有特别的优势了。

回溯算法又称为试探法,其特点:

  • 问题的答案有多个元素
  • 答案需要满足的约束条件
  • 寻找答案的方式在每一步骤相同
  • 逐步构建答案

遍历所有排序方式

现有要读的4本书,《Gevent指南》,《PythonCookBook》,《高性能MySql》,《编程珠玑》,每次只能从图书馆借一本,一共有多少中借书的顺序呢,若学过统计学,会有 4!= 24 种不同的排序方式。但,要的到的不只是总数,还想一一输出所有的排序方式

问题求解

  1. 首先,有四个空位,第一个空位有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)):
                        ...
    
  2. 若这个构思,要提前知道数组的长度,代码十分累赘。如果,在排第一本书到 时候,有4种选择,答案集合就在这4中选择在各结尾增加剩下的三本书的排序集合,那么,只需要得出剩余的三本书的排序集合就可以了。

  3. 同样,排着三本书时第一本数有三种选择,所以只需要得出剩余的两本书的排序集合就可以了,那么,推到最后,排两本书时,只需要知道最后一本是什么就可以了。

  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种不同的选课组合,不过,现在要将这些组合一一输出。

问题求解

  1. 首先有两个空位,第一次选择时有4中选择,A,B,C,D。
  2. 假设选择A,那么第二次选择就有3种选择。
  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

问题求解

  1. 以'week'为例,通过模拟算法,找出写代码的逻辑

  2. 首先,需要看盘面中25个字母中有没有 'w'。

  3. 找到第一个'w' , 第二字母'e',就要查看'w' 的上下左右有没有'e',没有'e',就找第二个'w'

  4. 找到第二个'w',发现它的左边右边都是'e',遵守上下左右的顺序,选择左边的'e'

  5. 接下来,就一一找出'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,或者没有,就改变第一个字母
  • (需要注意找到一个对应的字母,要从盘面删除,以免复用,造成死循环)

代码实现

  1. 检查当前盘面坐标对应的子没有是不是剩余单词的首字母
  2. 如果对应,检查左边的上下左右是否对用剩余单词的首字母
  3. 检查是否已经找到单词
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个皇后,并令她们互相攻击不到对方。

八个皇后问题是回溯算法的代表性问题之一。

问题求解

  1. 首先,问题中说到的每一行,每一列和每一条斜线上都必须有且只能有一个皇后,它告诉了我们第一行的8个格子中肯定有一名保安,可以从这里入手。
  2. 利用每一行必须有一个皇后的前提条件,假设第一行有一个皇后,会首先假设在(1, 1)格,然后再判断这个假设时候成立
  3. 但不能直接判断皇后A是否能够留在(1, 1)格,只能继续假设,一直假设下去。
  4. 假设的过程中有两种可能的结果
    • 成功假设到第8行,也就是说,找到正确答案之一
    • 在没到第8行的时候走不下去,陷入死局
  5. 若是第一种假设,则直接输出结果,若是第二种,则退一步,重新假设前一行的皇后位置,或许退到第一行,重新假设皇后A的位置

代码实现

  1. 假设当前皇后可以处的不同位置
  2. 判断假设的保安位置是否合理
  3. 如果合理,继续假设下面几行的皇后位置
  4. 判断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)
posted @ 2020-06-17 22:40  小教官vv  阅读(299)  评论(0编辑  收藏  举报