zoukankan      html  css  js  c++  java
  • 【核心算法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)
    
  • 相关阅读:
    linux下进程的实际用户ID(有效组)和有效用户ID(有效组ID)
    ubuntu下软件中心闪退问题解决
    LINUX(UNIX)文件I/O学习
    ubunut下桌面文件路径修改
    ubuntu下设置jdk/jre环境
    Fire net
    JavaScript 自己写一个 replaceAll() 函数
    Canvas 绘制一个像素风电子时钟
    Python3 笔记01:求两数之和
    尝试笔记 01 之 CSS 边角上的标签
  • 原文地址:https://www.cnblogs.com/JoshuaP/p/13155195.html
Copyright © 2011-2022 走看看