“八皇后问题”及其在方阵排列中的使用

1 问题引入

最近,项目leader给布置了一个小的练习题:求一个给定矩阵的所有不同行,不同列的数据的组合情况。刚开始,没细想,到编程的时候怎么调试就是无法获得所有符合条件的情况。纠结了一两天,在网上算法学习群里面发起了提问,有好心的网友提示像“八皇后问题”,可以使用回溯的思想去解答,于是就搜索了这些相关的问题。看了几篇文章,慢慢地也有了些眉目。通过这次经历,对算法也有了重新的认识:以前,总是感觉算法就是理论性的枯燥的东西,实践中很少能用到;现在看来,虽然现实的问题中,可能不会遇到那种纯理论假设的算法题,但是那些算法思想对于解决现实问题还是很有借鉴意义的。如果不了解那些基本算法背后的思想,工作中遇到相似的问题就会束手无策,无从查起,而了解过这些就会知道从哪些方向入手,从而大大提高工作效率。所以,有时间对基本的算法思想做一个系统的学习研究,还是很有意义的。

2 八皇后问题概述

八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。 高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。计算机发明后,有多种计算机语言可以解决此问题。
——引自百度百科

《“八皇后问题”及其在方阵排列中的使用》
图1 国际象棋棋盘

《“八皇后问题”及其在方阵排列中的使用》
图2 八皇后问题一种摆放方案

3 解决思路

3.1 问题分析

假设现在有一个N*N的二维方阵,使用数组存放各皇后的坐标位置,如果有两个皇后位置分别为Q1(x, y)和Q2(row, col):
首先,可归纳问题的条件为,8皇后之间需满足:
1.不在同一行上
2.不在同一列上
3.不在同一斜线上
4.不在同一反斜线上

3.2 处理问题的两种思路

该问题,可以从两种角度开始考虑。一种方式是从正面考虑,即直接进行可行方案的遍历;另一种思路是从反面考虑,即去除不符合条件的情况,剩下的就是满足条件的情况。

3.2.1 直接遍历可行摆放方案

从正面考虑,我们可以逐行或者逐列来进行可行摆放方案的遍历,每一行(或列)遍历出一个符合条件的位置,接着就到下一行或列遍历下一个棋子的合适位置,这种遍历思路可以保证我们遍历过程中有一个条件是绝对符合的——就是下一个棋子的摆放位置与前面的棋子不在同一行(或列)。接下来,我们只要判断当前位置是否还符合其他条件,如果符合,就遍历下一行(或列)所有位置,看看是否继续有符合条件的位置,以此类推,如果某一个行(或列)的所有位置都不合适,就返回上一行(或列)继续该行(或列)的其他位置遍历,当我们顺利遍历到最后一行(或列),且有符合条件的位置时,就是一个可行的8皇后摆放方案,累加一次八皇后可行方案的个数,然后继续遍历该行其他位置是否有合适的,如果没有,则返回上一行,遍历该行其他位置,依此下去。这样一个过程下来,我们就可以得出所有符合条件的8皇后摆放方案了。这是一个深度优先遍历的过程,同时也是经典的递归思路。

接下来,我们以逐列遍历,具体到代码,进一步说明。首先,从第一列开始找第一颗棋子的合适位置,我们知道,此时第一列的任何一个位置都是合适的,当棋子找到第一个合适的位置后,就开始到下一列考虑下一个合适的位置,此时,第二列的第一行及第二行显然就不能放第二颗棋子了,因为其与第一个棋子一个同在一行,一个同在一条斜线上。第二列第三行成为第二列第一个合适的位置,以此类推,第三列的第5行又会是一个合适位置,这个过程中,我们注意到,每一列的合适位置都是受到前面几列的位置所影响,归纳如下:

假设前面1列的棋子放在第3行,那当前列不能放的位置就一定是3行,2行,4行。因为如果放在这三行上就分别跟前一列的棋子同在一行、同在斜线、同在反斜线上,不符合我们的要求。现在我们用cols数组来表示8个列棋子所放的行数,数组下标从0开始,其中数组下标表示列数,数组的元素值表示该列棋子所在行数,当前列为N(N>=0,N<8),即cols[N-1]=3,则有:

                      cols[N] != cols[N-1](=3,表示不在同一行)
                      cols[N] != cols[N-1]-1(=3-1=2,表示不在同一斜线上)
                      cols[N]!=cols[N-1]+1(=3+1,表示不在同一反斜线上)

这里我们注意到,如果N-2列存在的话,那么我们还要考虑当前列N不与N-2列的棋子同行,同斜线,同反斜线。把当前列N的前面的某一列设为m,则m的所有取值为{m>=0,m

                      cols[N] != cols[m](与第m列的棋子不在同一行)                 
                      cols[N] != cols­­[m] -(N-m)(>=0 ,与第m列的棋子不在同一斜线上)                   
                      cols[N] != cols­­[m] + (N-m)  (<=8-1,与第m列的棋子不在同一反斜线上) 

具体到代码,很显然,取m的所有值只需要一句循环,同时我们为每一列定义一个长度为8的布尔数组row[],下标同样是从0开始,我们规定当row[i]=true时,表示该列第i行不能放棋子。这样我们就能写成下列程序段了:

 boolean[] rows = new boolean[8]; 
       for(int m=0;m<N;i++){
          rows[cols[m­]]=true;//当前列N的棋子不能放在前面列m的棋子所在行。
          int d = N-m;
         //该句用于设置当前列N的棋子不能放在前面列m的棋子的斜线上
         if(cols­­-d >= 0)rows[cols­-d]=true// 该句用于设置当前列N的棋子不能放在前面列m的棋子的反斜线上
         if(cols+d <=8-1)rows[cols­+d]=true;  
   } 

到此为止,我们程序的核心内容都具备了,一个基于深度优先的遍历流程和一个判断位置是否合适的算法。下面贴出运行后算出的所有可行方案(即92种,“+”号代表空棋位,“0”代表皇后所在位置),源码(注源码变量名定义与上述略有不同,打印效果也不是图片所显示的效果,代码有做些微改动)。

public class Queen8 {
    public static int num = 0; //累计方案总数
    public static final int MAXQUEEN = 8;//皇后个数,同时也是棋盘行列总数
    public static int[] cols = new int[MAXQUEEN]; //定义cols数组,表示8列棋子摆放情况
    public Queen8() {
       //核心函数
      getArrangement(0);
      System.out.print("\n");
      System.out.println(MAXQUEEN+"皇后问题有"+num+"种摆放方法。");
    }

    public void  getArrangement(int n){
     //遍历该列所有不合法的行,并用rows数组记录,不合法即rows[i]=true
     boolean[] rows = new boolean[MAXQUEEN];
     for(int i=0;i<n;i++){
        rows[cols[i]]=true;
        int d = n-i;
        if(cols[i]-d >= 0)rows[cols[i]-d]=true;
        if(cols[i]+d <= MAXQUEEN-1)rows[cols[i]+d]=true; 

     }
     for(int i=0;i<MAXQUEEN;i++){
       //判断该行是否合法 
       if(rows[i])continue;
       //设置当前列合法棋子所在行数
       cols[n] = i;
       //当前列不为最后一列时
       if(n<MAXQUEEN-1){
         getArrangement(n+1);
       }else{

        //累计方案个数
         num++;
         //打印棋盘信息
         printChessBoard();
       }   
     }  
    }
    public void printChessBoard(){

       System.out.print("第"+num+"种走法 \n");

       for(int i=0;i<MAXQUEEN;i++){
         for(int j=0;j<MAXQUEEN;j++){
           if(i==cols[j]){
             System.out.print("0 ");
           }else
             System.out.print("+ ");
         }
         System.out.print("\n");
       }

    }
    public static void main(String args[]){
      Queen8 queen = new Queen8();
    }

}

3.2.2 剔除不符合条件的方案

从3.1问题分析的反面考虑,我们可以先找出不符合条件的部分,然后将这些不满足条件的部分剔除掉,剩下的就是满足条件的组合。现在假设有两个皇后Q1(x, y)和Q2(row, col)不符合要求,则它们一定符合下面的四个条件之一:
1 ) 有两个皇后位于同一行,则满足行号相同: x = row。
2 ) 有两个皇后位于同一列,则满足列号一致: y = col。
3 ) 如果有两个皇后位于斜对角线上(走势如“/”),则有x + y = N – 1,row + col = N – 1(N为方阵的维数),从而得出x + y = row + col => row – x = y -col(斜向正方向)的结论。
4 ) 如果有两个皇后位于反斜对角线上(走势如“\”),则有x = y , row = col, 从而得出x – y = row – col => row – x = col – y(斜向反方向)。
从上面的3)和4)两条结论可以概括出,这两条同时满足公式abs(row – x) = abs(col – y),其中abs()表示对表达式进行求绝对值的操作。

归纳出了程序的判断条件,下面用主流的回溯法来解决N皇后问题,这里是8皇后问题。回溯法也就是带有剪枝的深度优先遍历。
用回溯的方法解决8皇后问题的步骤为:
1)从第一列开始,为皇后找到安全的一行,然后开始下一列
2)如果在第n列出现死胡同,则完全放弃后面所有列的搜索,直接后退到上一列,进行回溯
3)如果在第8列上找到了安全位置,则棋局成功。

8个皇后都找到了安全位置代表棋局的成功,用一个长度为8的整数数组colume代表成功摆放的8个皇后,数组索引代表棋盘的col向量,而数组的值为棋盘的row向量,所以(row,col)的皇后可以表示为(colume[col],col)

//C++代码
#include <iostream>
#include <cmath>

#define QUEEN_NUM 8
int ResultCounter = 0;

void printResult(int colume[]);                     //printResult方法声明
bool check(int colume[], int col);                  //check方法声明
void QueenSolution(int colume[], int col);          //QueenSolution方法声明

int main(void){
    //数组colume中存放的是行值:假设colume[0]==3,表明第1列中皇后在第4行上(数组下标从0开始,而平时说的第几是数组下标加一)
    int colume[QUEEN_NUM] = {0};   //初始时,8个皇后都假设从0行0列开始
    QueenSolution(colume, 0);
    std::cout << "Solution Total Count: " << ResultCounter << std::endl;
}

//输出数组中的一组结果
void printResult(int colume[]){
    for(int i = 0; i < QUEEN_NUM; i++)
        std::cout << "(" << colume[i] << ", " << i << ") ";
    std::cout << std::endl;
    ResultCounter++;
}

//检查当前列col,在现有情况下,能否放置皇后.如果满足以下四种情况,就返回false
//1)x=row(在纵向不能有两个皇后)
//2) y=col(横向)
//3)col + row = y+x;(斜向正方向)
//4) col - row = y-x;(斜向反方向)
//从上面的3)和4)两条结论可以概括出,这两条同时满足公式abs(row - x) = abs(col - y),其中abs()表示对表达式进行求绝对值的操作。
bool check(int colume[], int col){
    //因为提供的是列信息,我们就逐列进行检查
    for(int i = 0; i < col; i++)
    {
        if(colume[i] == colume[col] ||
           std::abs(colume[i] - colume[col]) == col - i )
            return false;
    }
    return true;
}

//尝试第col列上的所有解,即在第col列的所有行上依次检验
//调用此函数时,表明从第0列到第col-1列都已经安置好了皇后
void QueenSolution(int colume[], int col){
    if(col == QUEEN_NUM)
    {
        printResult(colume);
        return;
    }

    //新的一列中,皇后有可能在任意一行
    for(int i = 0; i < QUEEN_NUM; i++)
    {
        colume[col] = i;  //将这个皇后放在第i行,进行检查
        if( check(colume, col) )
            QueenSolution(colume, col+1);
    }
}

8皇后是个经典的问题,如果使用暴力法,每个格子都去考虑放皇后与否,一共有264 种可能。所以暴力法并不是个好办法。由于皇后们是不能放在同一行的, 所以我们可以去掉“行”这个因素,即我第1次考虑把皇后放在第1行的某个位置, 第2次放的时候就不用去放在第一行了,因为这样放皇后间是可以互相攻击的。 第2次我就考虑把皇后放在第2行的某个位置,第3次我考虑把皇后放在第3行的某个位置, 这样依次去递归。每计算1行,递归一次,每次递归里面考虑8列, 即对每一行皇后有8个可能的位置可以放。找到一个与前面行的皇后都不会互相攻击的位置, 然后再递归进入下一行。找到一组可行解即可输出,然后程序回溯去找下一组可靠解。

我们用一个一维数组来表示相应行对应的列,比如c[i]=j表示, 第i行的皇后放在第j列。如果当前行是r,皇后放在哪一列呢?c[r]列。 一共有8列,所以我们要让c[r]依次取第0列,第1列,第2列……一直到第7列, 每取一次我们就去考虑,皇后放的位置会不会和前面已经放了的皇后有冲突。 怎样是有冲突呢?同行,同列,对角线。由于已经不会同行了,所以不用考虑这一点。 同列:c[r]==c[j]; 同对角线有两种可能,即主对角线方向和副对角线方向。 主对角线方向满足,行之差等于列之差:r-j==c[r]-c[j]; 副对角线方向满足, 行之差等于列之差的相反数:r-j==c[j]-c[r]。 只有满足了当前皇后和前面所有的皇后都不会互相攻击的时候,才能进入下一级递归。

//C++代码
#include <iostream>
using namespace std;

int c[20], n=8, cnt=0;
void print(){
    for(int i=0; i<n; ++i){
        for(int j=0; j<n; ++j){
            if(j == c[i]) cout<<"1 ";
            else cout<<"0 ";
        }
        cout<<endl;
    }
    cout<<endl;
}
void search(int r){
    if(r == n){
        print();
        ++cnt;
        return;
    }
    for(int i=0; i<n; ++i){
        c[r] = i;
        int ok = 1;
        for(int j=0; j<r; ++j)
            if(c[r]==c[j] || r-j==c[r]-c[j] || r-j==c[j]-c[r]){
                ok = 0;
                break;
            }
        if(ok) search(r+1);
    }
}
int main(){
    search(0);
    cout<<cnt<<endl;
    return 0;
}

分析完上面的过程,理解了使用Python进行编程,代码量是很简短的:

def queen(A,cur=0):
    if cur == len(A): print A ;return
    for col in range(len(A)):
        A[cur],flag = col,True
        for row in range(cur):
            if A[row] == col or abs(col-A[row])==cur - row:flag=False;break
        if flag:queen(A,cur+1)
queen([None]*8)

4 借鉴“八皇后问题”的思想解决数组排列问题

上面说了那么多“八皇后问题”,感觉还是没有实际的其他用处啊。还记得刚开始引出的在工作中遇到的求一个矩阵中所有处于不同行不同列的排列组合情况吧,下面回到正题,借用“八皇后问题”背后的回溯思想的思路,尝试着去解决该问题:

def queen(A,cur=0):
    if cur == len(A):
        B = [i+1 for i in A]
        print(list(zip(range(1, len(A)+1), B)))
        return
    for col in range(len(A)):
        A[cur],flag = col,True
        for row in range(cur):
            if A[row] == col:
                flag=False
                break
        if flag:
            queen(A,cur+1)

n = int(input("please input a number string:"))
queen([None]*n)

看看上面的代码跟“八皇后问题”的Python代码是不是很相似,只是该问题只需任意两个坐标不同行不同列就行了,不需要判断处于对角线上的两种情况。是不是很惊讶,我在看过两篇“八皇后问题”的文章之后,才反应过来——其实,“八皇后问题”中的任意两个皇后不同行不同列,跟求二维方阵中所有的坐标不能同行不能同列是一样的。因为,对于“任意”、“所有”这些全称量词,我们可以从反面考虑,只要是存在(一个)处于同行同列的一个例子,就是不符合条件的,所以他们从反面是一样的,那么去掉只写反面的情况就是符合要的了。只是,“八皇后问题”要求不能存在位于对角(处于对角线和反对角线)的两种情况,所以比该问题要复杂一些啦。

参考:
八皇后问题_百度百科
https://baike.baidu.com/item/%E5%85%AB%E7%9A%87%E5%90%8E%E9%97%AE%E9%A2%98/11053477?fr=aladdin

8皇后问题(java算法实现) – CSDN博客
http://blog.csdn.net/zhong317/article/details/4586131

八皇后问题详解(最短代码) – dreamzuora的博客 – CSDN博客
http://blog.csdn.net/dreamzuora/article/details/52791401

    原文作者:八皇后问题
    原文地址: https://blog.csdn.net/love666666shen/article/details/79228050
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞