Python----递归------Eight Queens 八皇后问题

递归思想是算法编程中的重要思想。

作为初学者,对递归编程表示很蒙逼,每次遇到需要递归的问题,心里就有一万头草泥马飞过~~~~~~(此处略去一万头草泥马)

 

在B站看数据结构与算法的视频时,视频中给了两个非常典型的例子——《汉诺塔》和《八皇后问题》,就希望自己用Python实现一下这两个递归程序,其中汉诺塔问题比较简单,还是能够理解,这里就不讲了。

《八皇后问题》:说要在一个棋盘上放置8个皇后,但是不能发生战争,皇后们都小心眼,都爱争风吃醋,如果有人和自己在一条线上(水平、垂直、对角线)就会引发撕13大战,所以我们就是要妥当的安排8位娘娘,以保后宫太平。[1]

 

 

与网上普遍搜到的利用yield函数完成递归方法不同,注意时因为对于初学Python的我来说,完全搞不懂yield怎么用,更不要说将其放在递归程序里了, [捂脸]

下面我会对我的代码进行逐一解释,希望更多的像我这样的初学者能够看懂。!!!敲黑板!!!------------不仅要看懂,还要自己会写代码-----------------


 首先声明:

代码中位置状况采用list列表的形式, 如,chess[0] = 4代表在棋盘第1行的第5个位置放置皇后。 

count = 0                   # 定义一个全局变量count, 用于八皇后方案的统计

# conflict 函数为冲突函数,主要用于判断新添加的位置是否与前面的位置冲突
# pos 为新添加的位置, chess为添加pos位置前的皇后位置分布
def conflict(pos, chess):
    len_chess = len(chess)         # 现在已经分配了几行,几有几个皇后在棋盘上,因为初始chess为空,每次判断一个符合规则的位置后再加入到棋盘中
    for i in range(len_chess):if abs(chess[i] - pos) in (0, len_chess - i): # 这一行为关键,太多了就放后面了
            return True
    return False

# Queen函数为递归函数
def Queens(num,chess):
    global count                # python 中要再函数中使用之前定义的全局变量,必须再函数中用 global 声明一下

    if len(chess) == 8:            # 判断chess的长度,当chess长度为8的时候,说明8个皇后都已经全部放好位置了,就可以直接输出相应的方案了
        count += 1
        print(chess)
    else:                      # chess长度小于8,说明还有没安排好的皇后,所以继续安排剩下的位置
        for pos in range(num):        # 对接下来的一行的8个位置进行遍历 
            if not conflict(pos, chess): # 对每个位置进行冲突判断
                chess1 = []            #关键是这三行,用于构造棋盘的副本,这三句非常重要,具体原因放在后面解释
                for i in chess:
                    chess1.append(i)
                chess1.append(pos)      # 符合规则,则将其附在chess副本的后面
                Queens(num,chess1)      # 递归调用Queens函数

board = []  # 初始化空棋盘
Queens(8,board)  # 调用函数
print(count)  # 打印总共有多少中方案

  

冲突判断

 

def conflict(pos, chess):
    len_chess = len(chess)   # 现在已经分配了几行,几有几个皇后在棋盘上,因为初始chess为空,每次判断一个符合规则的位置后再加入到棋盘中
    for i in range(len_chess):if abs(chess[i] - pos) in (0, len_chess - i): # 这一行为关键,太多了就放后面了
            return True
    return False

 

根据规则:有三种情况存在冲突,我i们一一来列举分析一下:

  1. 在同一列的情况,若两个皇后在同一列,那么这两行的纵坐标相同,反映到我们的棋盘定义上则未:chess[i] = chess[j] 即第i行与第j行的两个皇后在同一列了
  2. 第二种情况则为在“\” 对角线上,那么各位置的(横坐标 - 纵坐标)的值时一样的,假设chess[i] = a1,chess[j] = a2在对角线"\"上,i - a1 = j - a2。 如(0,1),(1,2),(2,3)横纵坐标之差都为1
  3. 第三种情况则为在" / " 对角线上,那么各位置的(横坐标 + 纵坐标)的值时一样的,假设chess[i] = a1,chess[j] = a2在对角线"\"上,i + a1 = j + a2。 如(0,3),(1,2),(2,1)横纵坐标之和都为1

我们再观察,就会发现(这个我自己想时想不出来的,也是对照着代码思考出来的):

对于被判断是否符合要求的元素,其索引恰好为现在chess的长度,因为最后一个元素的索引为len(chess)-1,那么新的元素的索引恰为len(chess)

我们另 len_chess 为当前chess的长度,则三种情况对应的表达式为:

 

 

  1. chess[i] - pos = 0
  2. chess[i] - i = pos - len_chess   --------->   chess[i] - pos = i-len_chess
  3. chess[i] + i = pos + len_chess  ---------> chess[i] - pos = len_chess - i

由于 len_chess > i, 所以可以得到当abs(chess[i] - pos)==0 或len_chess - i时则不符合规则,返回True :

 

棋盘副本建立

 

        for pos in range(num):   #对接下来的一行的8个位置进行遍历 
            if not conflict(pos, chess): #对每个位置进行冲突判断
        chess1 = []          #关键是这三行,用于构造棋盘的副本,这三句非常重要,具体原因放在后面解释
                for i in chess:
                    chess1.append(i)
                chess1.append(pos)    # 符合规则,则将其附在chess副本的后面
                Queens(num,chess1)    # 递归调用Queens函数

  

这里首先提一点的是,python里面所有如果要将一个list拷贝一份,不能直接像C语言里面一样用幅值语句,需要重新初化,

我个人的理解是:因为list的名称为一个地址,当我们将一个地址幅值给一个新的变量时,新的变量指向的还是原来那个数组,所以并没有起到拷贝的作用。 比较典型的例子就是规定大小的二维数组的建立

 言归正传,我们为什么要新建立一个副本呢?

 

  • 因为我们需要使用副本来进行深层次的递归,如果不使用副本chess1而直接使用原始数据chess,那么随着递归的深入,chess所对应的地址内的值在不断变化,当返回到递归点的时候,原始数据已经不存在了,

只会从当前chess对应的数组继续进行递归。

  • 比如,当 chess = [0,2,4,1,3]时,此时我们需要填充第6行皇后的位置,但是通过判断,发现第6行所有位置都不符合要求,那么此时程序应该退回chess = [0,2,4,1,4]再继续判断第6行是否有符合的位置,但是若不用副本

的话,程序会退回,但是chess所对应的值还是[0,2,4,1,3],这样是错误的。

 

 

 QAQ: 这一段我也没有讲的特别清楚,主要关键就是这个副本的建立使原始数据不受破坏,可以在递归从深层跳出的时候保持为原来的状态继续进行,可以在编译器中单步运行看一看再理解会更好

 

 

[1]. 摘自http://www.cnblogs.com/littleseven/p/5362791.html, 斯认为该博主写的还可以,

 

posted @ 2017-08-30 20:29  TomHawk  阅读(1635)  评论(0编辑  收藏  举报