【八皇后问题】算法
问题: 国际象棋棋盘是8 * 8的方格,每一个方格里放一个棋子。皇后这种棋子能够攻击同一行或者同一列或者斜线(左上左下右上右下四个方向)上的棋子。在一个棋盘上若是要放八个皇后,使得她们互相之间不能攻击(即任意两两之间都不一样行不一样列不一样斜线),求出一种(进一步的,全部)布局方式。数组
■ 描述 & 实现数据结构
以前的Python基础那本书上介绍递归和生成器的一张有解过这个问题。书本中对于此问题的解可能更偏重于对于Python语言的应用。然而果真我也是早就忘光了。下面再来从头看看这个问题。函数
首先,咱们想到递归和非递归两类算法来解决这个问题。首先说说递归地算法。布局
很天然的,咱们能够基于行来作判断标准。八个皇后都不一样行这是确定的,也就说每行有且仅有一个皇后,问题就在于皇后要放在哪一个列。固然八个列下标也都不能有相同,除此以外还要保证斜线上不能有重叠的皇后。优化
第一个须要解决的小问题就是,如何用数学的语言来表述斜线上重叠的皇后。其实咱们能够看到,对于位于(i,j)位置的皇后而言,其四个方向斜线上的格子下标分别是 (i-n,j+n), (i-n,j-n), (i+n,j-n), (i+n,j+n)。固然i和j的±n都要在[0,7]的范围内,保持不越界。暂时抛开越界限制无论,这个关系其实就是: 目标格子(a,b)和本格子(i,j)在同一条斜线上 等价于 |a - i| == |b - j| 。spa
而后,从递归的思想来看,咱们在从第一行开始给每一行的皇后肯定一个位置。每来到新的一行时,对本行的全部可能位置(皇后放在这个位置和前面全部已放置的皇后无冲突)分别进行递归地深刻;若某一行可能的位置数为0,则代表这是一条死路,返回上一层递归寻找其余办法;若来到的这一行是第九行(不存在第九行,只不过是说明前八行都已经正确配置,已经获得一个解决方案),这说明获得解决方案。设计
能够看到,寻找一行内皇后应该摆放的位置这是个递归过程,而且在进入递归时,应该要告诉这个过程的东西包括两个: 1. 以前皇后放置的状态, 2. 如今是第几行。code
因此,递归主体函数能够设计为 EightQueen(board, row),其中board表示的是当前棋盘的状态(好比一个二维数组,0表示未放置,1表示放有皇后的状态)。另外还能够有一个check(board,pos),pos能够是一个(x,y)元组,check函数用来返回以当前的board棋盘状态,若是在pos再放置一个皇后是否会有冲突。blog
基于上面的想法,初步实现以下:
def check(board,pos): # check函数暂时先不实现 pass def EightQueen(board,row): blen = len(board) if row == blen: # 来到不存在的第九行了 print board return True # 必定要return一个True,理由在下面 for possibleY in range(blen): if check(board,(row,possibleY)): board[row][possibleY] = 1 # 放置一个Queen if not EightQueen(board,row+1): # 这里实际上是本行下面全部行放置皇后的递纳入口。可是若是最终这条路没有找到一个解,那么 # 此时应该将刚才放置的皇后收回,再去寻找下一个可能的解 board[row][possibleY] = 0 else: return True return False
最开始,可能在回归返回条件那里面不会想到要return True,而只是return。对应的,下面主循环中放置完Queen以后也只是简单地递归调用EightQueen,不会作逻辑判断。可是很快能够发现这样作存在一个问题,即当某一层递归中for possibleY这个循环走完却没有找到一个合适的解(即本行无合适位置),此时返回上一行,上一行的possibleY右移一格,此时以前放在这一行的Queen的位置仍然是1。这样以后本行的全部check确定都是通不过的。因此咱们须要设计一个机制,使得第一个possibleY没有找到合理的最终解决方案(这里就加上了一个判断条件),要右移一格到下一个possibleY时将本格的Queen收回。
这个判断条件就是若是某层递归for possibleY循环整个走完未找到结果返回False(EightQueen整个函数最后的返回),上一层根据这个False反馈把前一个Queen拿掉;若是找到了某个结果那么就能够一路return True回来,结束函数的运行。
另外,若是只是获取一个解的话,能够考虑在if row == blen的时候,打印出board,而后直接sys.exit(0)。此时就只须要for possibleY循环完了以后return一个False就能够了。固然主循环中对于递归的返回的判断 if not EightQueen仍是须要的。
■ 优化
● check函数怎么搞
上面没有实现check函数。其实仔细想一下,若是按照上面的设想来实现check函数仍是有点困难的。好比令 x,y = pos,尽管此时咱们只须要去检查那些行下标小于x的board中的行,可是对于每一行中咱们仍是要一个个去遍历,找到相关行中值是1的那个格子(忽然发现这个是one-hot模式诶哈哈),而后将它再和x,y这个位置作冲突判断。因此可是这个check函数复杂度就可能会达到O(n^2),再套上外面的循环,复杂度蹭蹭往上涨。下面是check函数的一个可能的实现:
def check(board,pos): x,y = pos blen = len(board) for i in range(x): for j in range(blen): if board[i][j] == 1: if j == y or abs(j-y) == abs(i-x): return False return True
其实能够看到,咱们花了一层循环在寻找某行中的one-hot,那些大量的0值元素是咱们根本不关心的。换句话说,对于board这个二维数组,其实咱们真正关心的是每行中one-hot值的下标值。天然咱们就能够想到,能不能将board转化为一个一维数组,下标自己就表明了board中的某一行,而后值是指这一行中皇后放在第几列。
若是是这样的话,那么程序就须要改造,首先是check函数要根据新的board数据结构作一些调整:
def check(board,row,col): i = 0 while i < row: if abs(col-board[i]) in (0,abs(row-i)): return False i += 1 return True
能够看到,改变二维数组board变为一维数组以后,咱们能够在O(1)的时间就肯定row行以前每一行摆放的位置,并将其做为参考进行每一行的冲突判断。
而后是主函数的修改:
def EightQueen(board,row): blen = len(board) if row == blen: # 来到不存在的第九行了 print board return True col = 0 while col < blen: if check(board,row,col): board[row] = col if EightQueen(board,row+1): return True col += 1 return False def printBoard(board): '''为了更友好地展现结果 方便观察''' import sys for i,col in enumerate(board): sys.stdout.write('□ ' * col + '■ ' + '□ ' * (len(board) - 1 - col)) print ''
总的结构,和没修改以前是相似的,只不过在主循环中,从上面的possibleY做为游标去设置 - 去除 一个位置的放置状态,这种方式改成了简单的col += 1。改为col+=1的好处就是当某轮递归以失败了结,返回上层递归以后,就不用再去特意收回以前放置好的Queen,而是能够直接让col += 1,。
printBoard函数能够将一维数组的board状态很直观地展示出来:
■ □ □ □ □ □ □ □ □ □ □ □ ■ □ □ □ □ □ □ □ □ □ □ ■ □ □ □ □ □ ■ □ □ □ □ ■ □ □ □ □ □ □ □ □ □ □ □ ■ □ □ ■ □ □ □ □ □ □ □ □ □ ■ □ □ □ □
■ 全部结果?
上面的程序多只是生成了一个结果,而实际上八皇后能够有不少种可能的布局。如何才能求得全部结果?其实只要小小地修改一下上面的程序就能够了。
以上面修改事后一维数组维护棋盘状态为例。程序在碰到一次row == blen的状况以后就返回了True,而后递归一层层地返回True直到最上层。因此找到一个解决方案以后,程序就会退出了。
反过来,若是得到一个解决方案以后,不判断EightQueen函数的返回,此时函数会继续执行col += 1,将状态搜寻继续下去,如此收集状态的任务在row == blen的判断中,(注意这里的return可不能删,这里须要一个return来提示递归的终结条件),而对于每条递归路径老是穷尽全部可能再回头,这样就能够得到到全部可能了:
def EightQueen(board,row): blen = len(board) if row == blen: # 来到不存在的第九行了 print board return True col = 0 while col < blen: if check(board,row,col): board[row] = col if EightQueen(board,row+1): # return True 去掉这里便可,或者直接删除掉整个判断,只留下单一个EightQueen(board,row+1) pass col += 1 return False
示例结果:
[0, 4, 7, 5, 2, 6, 1, 3] [0, 5, 7, 2, 6, 3, 1, 4] [0, 6, 3, 5, 7, 1, 4, 2] [0, 6, 4, 7, 1, 3, 5, 2] [1, 3, 5, 7, 2, 0, 6, 4] [1, 4, 6, 0, 2, 7, 5, 3] [1, 4, 6, 3, 0, 7, 5, 2] [1, 5, 0, 6, 3, 7, 2, 4] [1, 5, 7, 2, 0, 3, 6, 4] …… 总共有92种布局方案
■ 非递归
非递归解这个问题,很显然是要去维护一个stack来保存一个路径了。简单来讲,这个栈中维护的应该是“还没有尝试去探索的可能”,当我开始检查一个特定的位置,若是检查经过,那么应该作的是首先将本位置右边一格加入栈,而后再把下一行的第一个格子加入栈。注意前半个操做很容易被忽视,可是若是不将本位置右边一格入栈,那么若是基于本格有皇后的状况进行的递归最终没有返回一个结果的话,接下来就不知道往哪走了。若是使用了栈,那么用于扫描棋盘的游标就不用本身在循环里+=1了,循环中游标的移动全权交给栈去维护。
代码以下:
def EightQueen(board): blen = len(board) stack = Queue.LifoQueue() stack.put((0,0)) # 为了自洽的初始化 while not stack.empty(): i,j = stack.get() if check(board,i,j): # 当检查经过 board[i] = j # 别忘了放Queen if i >= blen - 1: print board # i到达最后一行代表已经有告终果 break else: if j < blen - 1: # 虽说要把本位置右边一格入栈,可是若是本位置已是行末尾,那就不必了 stack.put((i,j+1)) stack.put((i+1,0)) # 下一行第一个位置入栈,准备开始下一行的扫描 elif j < blen - 1: stack.put((i,j+1)) # 对于未经过检验的状况,天然右移一格便可
显然,把break去掉就是求全部解了
■ 用C语言写了一版
#include <stdio.h> static int board[8] = {}; int board_size = sizeof(board)/sizeof(int); int check(int *board,int row){ int i = 0; while(i < row){ if(board[i] == board[row] || row - i == board[row] - board[i] || row - i == board[i] - board[row]){ return 0; } i++; } // printf("board[%d]: %d\n",row,board[row]); return 1; } void print_board(int *board){ int i; int size = board_size; for(i=0;i<size;i++){ printf("%d,",board[i]); } printf("\n"); i = 0; while (i < size){ int j; for (j=0;j<size;j++){ if(j == board[i]){ printf("%s ","■ "); } else{ printf("%s ","□ "); } } printf("\n"); i++; } } int eight_queen(int *board,int row){ if (row == 8){ print_board(board); return 1; } board[row] = 0; while (1){ if (check(board,row) && eight_queen(board,row+1)){ return 1; } else{ if(++board[row] >= 8){ break; } } } return 0; } int main(){ eight_queen(board,0); // print_board(board); return 0; }