回溯法

在了解八皇后问题之前我们先了解什么是回溯法,因为八皇后问题是回溯法的一个经典算法习题,也是八皇后问题用到的主要算法。

根据百度百科解释:回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。

举个集合小例子:列举集合 {1,2,3} 中所有子集的问题

使用回溯法。从集合的开头元素开始,对每个元素都有两种操作,直到集合最后一个元素。其中的每个操作都可以看作是一次尝试,每次尝试都可以得出一个结果。将得到的结果综合起来,就是集合的所有子集。

在这里插入图片描述
回溯法和递归的联系:
递归是从问题的结果出发,例如求 n!,要想知道 n!的结果,就需要知道 n*(n-1)! 的结果,而要想知道 (n-1)! 结果,就需要提前知道 (n-1)*(n-2)!。这样不断地向自己提问,不断地调用自己的思想就是递归。

回溯和递归唯一的联系就是,回溯法可以用递归思想实现

八皇后问题

八皇后问题是以国际象棋为背景的问题:有八个皇后(可以当成八个棋子),如何在 8*8 的棋盘
中放置八个皇后,使得任意两个皇后都不在同一条横线、纵线或者斜线上。

在这里插入图片描述

算法思路:

  1. 从棋盘的第一行开始,从第一个位置开始,依次判断当前位置是否能够放置皇后。
    判断的依据为:同该行之前的所有行中的皇后所在位置进行比较,如果在同一列,或者在同一条对角线上(正方形的两条对角线),都不符合条件,继续检查后序位置;
  2. 如果i该行所有位置都不符合要求,则回溯到前一行,改变皇后的位置,继续i试探;
  3. 如果试探到最后一行,所有皇后位置摆放完毕,则直接打印出8*8棋盘。最后将棋盘恢复原样,避免影响下一次摆放。

实现代码:

代码分为三部分:

  1. conflict 函数,判断下一行皇后位置是否与之前皇后的位置冲突
  2. queens函数,采用生成器的方式来产生每一个皇后的位置,并用递归思想实现回溯法计算出每一种结果的皇后的位置
  3. prettyprint函数,友好展示棋盘,画出每一种结果的皇后的位置
def conflict(state, nextColumn):
    """
    判断是否冲突
    因为坐标是从0开始的,所以state的长度代表了下一行的行坐标
    :param state:(7,4,6,0,2) 标记每行皇后所在的位置 (0,7)一行八列 (2,4) (3,6) (4,0) (5,2)
    :param nextColumn:下一行的列坐标
    :return:
    """
    nextRow = rows = len(state) # 5
    for row in range(rows):   #  0,1,2,3,4
        # 获取当前行的列
        column = state[row]
        """
        如何判断是否冲突:
            1. 如果列的差值为0,说明两皇后在同一列
            2. 如果列的差值等于行的差值,说明两皇后在对角线上
        """
        if abs(column - nextColumn) in [0, nextRow - row]:
            return True
    return False

# 采用生成器的方式来产生每一个皇后的位置,并用递归来实现下一个皇后的位置
def queens(num, state=()):
    """
    基于递归采用回溯算法,算出每一种结果

    :param num: 皇后的数量  8
    :param state: 列坐标。初始为空。参数为元组不为列表,因为参数只能为不可变数据类型
    :return:
    """
    # 每一行的列坐标都是从0:7的
    # 0,1,2,3,4,5,6,7
    for pos in range(num):
        # 默认state为空。长度为0,但是是不冲突的
        # 判断是否冲突,state为空时不冲突
        if not conflict(state, pos): # 回溯法的体现
            # 如果state的长度为7,即到达了倒数第二行,也就是前7行皇后都已经找到了位置,最后一行又没有冲突,返回最后一行的列坐标
            if len(state) == num - 1:
                # 最后一行的(pos,)=最后一行的result,然后再递归回去求倒数第二行的result
                yield (pos,)
            else:
                for result in queens(num, state + (pos,)):
                    """
                    递归实现求state:
                        1. 向下递归
                        第一次(行): pos=0,刚开始不会进入if len(state) == num - 1,进入执行else,会执行queens(num, state + (pos, )),
                        第二次(行): 进入else,再调用queens(num, state + (pos, )),递归执行queens(num, state + (pos,) + (pos,))
                        第三次(行): 进入else,再调用queens(num, state + (pos,) + (pos,),递归执行queens(num, state + (pos,) + (pos,) + (pos,))
                        ...
                        第七次(行): 执行和上面的一样,不过此时state的长度为7
                        第八次(行): 执行f len(state) == num - 1:求出最后一行的列坐标(pos,)
                        
                        2.向上递归
                        求出第八行的列坐标,就可以求出第七行的(pos,),返回的是第七行和第八行的列坐标((pos,) + result)
                        根据下一行的结果依次求出上一行的结果;
                        ....
                        最后求出第一行的列坐标,返回整体结果
                    """
                    yield (pos,) + result

def prettyprint(solution):
    """
    进行友好展示:为了至关表现棋盘,用X表示皇后的位置
    :param solution:
    :return:
    """
    def line(pos, length=len(solution)):
        return '.' * (pos) + 'X' + '.' * (length - pos -1)

    for pos in solution:
        print(line(pos))


if __name__ == '__main__':
    solutions = queens(8)
    for index, solution in enumerate(solutions):
        print('第%d种解决方案:' %(index + 1), solution )
        prettyprint(solution)
        print('*' * 50)

结果展示:

1种解决方案: (0, 4, 7, 5, 2, 6, 1, 3)
X.......
....X...
.......X
.....X..
..X.....
......X.
.X......
...X....
**************************************************2种解决方案: (0, 5, 7, 2, 6, 3, 1, 4)
X.......
.....X..
.......X
..X.....
......X.
...X....
.X......
....X...91种解决方案: (7, 2, 0, 5, 1, 4, 6, 3)
.......X
..X.....
X.......
.....X..
.X......
....X...
......X.
...X....
**************************************************92种解决方案: (7, 3, 0, 2, 5, 1, 6, 4)
.......X
...X....
X.......
..X.....
.....X..
.X......
......X.
....X...
**************************************************

在这里插入图片描述

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐