1. 程式人生 > >8皇后以及N皇后演算法探究,回溯演算法的JAVA實現,遞迴方案(一)

8皇后以及N皇后演算法探究,回溯演算法的JAVA實現,遞迴方案(一)

八皇后問題,是一個古老而著名的問題,是回溯演算法的典型案例。該問題是國際西洋棋棋手馬克斯·貝瑟爾於1848年提出:在8×8格的國際象棋上擺放八個皇后,使其不能互相攻擊,即任意兩個皇后都不能處於同一行、同一列或同一斜線上,問有多少種擺法。 高斯認為有76種方案。1854年在柏林的象棋雜誌上不同的作者發表了40種不同的解,後來有人用圖論的方法解出92種結果。計算機發明後,有多種計算機語言可以解決此問題。---------以上節選自百度百科

 

演算法思考,初步思路:

構建二維int或者short型陣列,記憶體中模擬棋盤

chess[r][c]=0表示:r行c列沒有皇后,chess[r][c]=1表示:r行c列位置有一個皇后

從第一行第一列開始逐行擺放皇后

依題意每行只能有一個皇后,遂逐行擺放,每行一個皇后即可

擺放後立即呼叫一個驗證函式(傳遞整個棋盤的資料),驗證合理性,安全則擺放下一個,不安全則嘗試擺放這一行的下一個位置,直至擺到棋盤邊界

當這一行所有位置都無法保證皇后安全時,需要回退到上一行,清除上一行的擺放記錄,並且在上一行嘗試擺放下一位置的皇后(回溯演算法的核心)

當擺放到最後一行,並且呼叫驗證函式確定安全後,累積數自增1,表示有一個解成功算出

驗證函式中,需要掃描當前擺放皇后的左上,中上,右上方向是否有其他皇后,有的話存在危險,沒有則表示安全,並不需要考慮當前位置棋盤下方的安全性,因為下面的皇后還沒有擺放

 

回溯演算法的天然實現是使用編譯器的遞迴函式,但是其效能令人遺憾

下面我們使用上面的思路初步實現8皇后的問題解法,並且將所有解法打印出來,供我們驗證正確性

複製程式碼

import java.util.Date;


/**
 * 在8×8格的國際象棋上擺放八個皇后,使其不能互相攻擊,
 * 即任意兩個皇后都不能處於同一行、同一列或同一斜線上,問有多少種擺法。
 * 下面使用遞迴方法解決
 * @author [email protected]
 *
 */
public class EightQueen {
    private static final short N=8;        //使用常量來定義,方便之後解N皇后問題
    private static int count=0;            //結果計數器
    
    public static void main(String[] args) {
        Date begin =new Date();
        //初始化棋盤,全部置0
        short chess[][]=new short[N][N];
        for(int i=0;i<N;i++){
            for(int j=0;j<N;j++){
                chess[i][j]=0;
            }
        }
        
        putQueenAtRow(chess,0);
        Date end =new Date();
        System.out.println("解決 " +N+ " 皇后問題,用時:" +String.valueOf(end.getTime()-begin.getTime())+ "毫秒,計算結果:"+count);
    }

    private static void putQueenAtRow(short[][] chess, int row) {        
        /**
         * 遞迴終止判斷:如果row==N,則說明已經成功擺放了8個皇后
         * 輸出結果,終止遞迴
         */
        if(row==N){
            count++;
            System.out.println("第 "+ count +" 種解:");
            for(int i=0;i<N;i++){
                for(int j=0;j<N;j++){
                    System.out.print(chess[i][j]+" ");
                }
                System.out.println();
            }
            return;
        }
        
        short[][] chessTemp=chess.clone();
        
        /**
         * 向這一行的每一個位置嘗試排放皇后
         * 然後檢測狀態,如果安全則繼續執行遞迴函式擺放下一行皇后
         */
        for(int i=0;i<N;i++){
            //擺放這一行的皇后,之前要清掉所有這一行擺放的記錄,防止汙染棋盤
            for(int j=0;j<N;j++)
                chessTemp[row][j]=0;
            chessTemp[row][i]=1;
            
            if( isSafety( chessTemp,row,i ) ){
                putQueenAtRow(chessTemp,row+1);
            }
        }
    }

    private static boolean isSafety(short[][] chess,int row,int col) {
        //判斷中上、左上、右上是否安全
        int step=1;
        while(row-step>=0){
            if(chess[row-step][col]==1)                //中上
                return false;
            if(col-step>=0 && chess[row-step][col-step]==1)        //左上
                return false;
            if(col+step<N && chess[row-step][col+step]==1)        //右上
                return false;
            
            step++;
        }
        return true;
    }
}

複製程式碼

輸出結果:

需要列印棋盤時,耗時34毫秒,再看一看不需要列印棋盤時的效能:

 

耗時2毫秒,效能感覺還可以。

 

你以為到這兒就結束了嗎?高潮還沒開始,下面我們來看看這種演算法解決9、10、11...15皇后問題的效能

稍微變動一下程式碼,迴圈打印出各個解的結果,如下圖所示:

當我開始嘗試解決16皇后問題時,發現時間複雜度已經超出我的心裡預期,最終沒讓它繼續執行下去。

上網一查N皇后的國際記錄,已經有科研單位給出了25皇后的計算結果,耗時暫未可知

我們的程式跑16皇后已經無能為力,跑15皇后已經捉襟見肘(87秒)

中國的一些演算法高手能在100秒內跑16皇后,可見上面的演算法效率只能說一般,辣麼,該如何改進呢?

 

我們發現二維棋盤資料在記憶體中浪費嚴重,全是0和1的組成,同時每次遞迴時使用JAVA的clone函式克隆一個新的棋盤,消耗進一步擴大,這裡面一定有改進的方案。

我們考慮將二維陣列使用一維陣列代替,將一維陣列的下標資料利用起來,模仿棋盤結構,如chess[R]=C時,表示棋盤上R行C列有一個皇后

這樣程式的空間效率會得到迅速提高,同時二維資料改變成一維資料後的遍歷過程也會大為縮減,時間效率有所提高,下面貼出程式碼:

複製程式碼

import java.util.Date;


public class EightQueen2 {
    private static final short K=15;        //使用常量來定義,方便之後解N皇后問題
    private static int count=0;            //結果計數器
    private static short N=0;
    
    public static void main(String[] args) {
        for(N=9;N<=K;N++){
            Date begin =new Date();
            /**
             * 初始化棋盤,使用一維陣列存放棋盤資訊
             * chess[n]=X:表示第n行X列有一個皇后
             */
            short chess[]=new short[N];
            for(int i=0;i<N;i++){
                chess[i]=0;
            }
            
            putQueenAtRow(chess,(short)0);
            Date end =new Date();
            System.out.println("解決 " +N+ "皇后問題,用時:" +String.valueOf(end.getTime()-begin.getTime())+ "毫秒,計算結果:"+count);
        }
    }

    private static void putQueenAtRow(short[] chess, short row) {        
        /**
         * 遞迴終止判斷:如果row==N,則說明已經成功擺放了8個皇后
         * 輸出結果,終止遞迴
         */
        if(row==N){
            count++;
//            System.out.println("第 "+ count +" 種解:");
//            for(int i=0;i<N;i++){
//                for(int j=0;j<N;j++){
//                    System.out.print(chess[i][j]+" ");
//                }
//                System.out.println();
//            }
            return;
        }
        
        short[] chessTemp=chess.clone();
        
        /**
         * 向這一行的每一個位置嘗試排放皇后
         * 然後檢測狀態,如果安全則繼續執行遞迴函式擺放下一行皇后
         */
        for(short i=0;i<N;i++){
            //擺放這一行的皇后
            chessTemp[row]=i;
            
            if( isSafety( chessTemp,row,i ) ){
                putQueenAtRow(chessTemp,(short) (row+1));
            }
        }
    }

    private static boolean isSafety(short[] chess,short row,short col) {
        //判斷中上、左上、右上是否安全
        short step=1;
        for(short i=(short) (row-1);i>=0;i--){
            if(chess[i]==col)    //中上
                return false;
            if(chess[i]==col-step)    //左上
                return false;
            if(chess[i]==col+step)    //右上
                return false;
            
            step++;
        }
        
        return true;
    }
}

複製程式碼

運算結果:

可以看到所有結果的耗時縮短了一倍有餘,這無疑是一次演算法的進步

辣麼,還有改進空間嗎?

答案必然是肯定的,對於演算法,我們越是精益求精,我們的能力就越強大,我們越是淺嘗輒止,我們的進步就越慢。

下一篇部落格我們來繼續改進這個問題的演算法,摒棄編譯器自帶的遞歸回溯,自己手寫回溯過程,相信效率會進一步提高,最終在可控範圍內將16皇后問題解出來。