八皇后問題——DFS

國際象棋中的皇后比中國象棋裏的大車還厲害,皇后能橫向,縱向和斜向移動,在這三條線上的其他棋子都可以被吃掉。所謂八皇后問題就是:將八位皇后放在一張8x8的棋盤上,使得每位皇后都無法吃掉別的皇后,(即任意兩個皇后都不在同一條橫線,豎線和斜線上),問一共有多少種擺法。此問題是在1848年由棋手馬克思·貝瑟爾提出的,後面陸續有包括高斯等大數學家們給出自己的思考和解法,所以此問題不只是有年頭了,簡直比82年的拉菲還有年頭,我們今天不妨嚐嚐這老酒。

我們先舉例來理解一下這個問題的場景到底是什麼樣子的,下面的綠色格子是一個皇后在棋盤上的“封鎖範圍”,其他的皇后不能放置在這些綠格子中:

一個皇后的封鎖範圍

我們再放入一個皇后,看一下兩個皇后的“封鎖範圍”(綠格子不能放):

兩個皇后的封鎖範圍

如此繼續下去,能安放下一位皇后的位置越來越少,那麼我們最終如何能安放完這8位皇后呢?

首先我們看一下特別暴力的方法:從8x8的格子裏選8個格子,放皇后,然後測試是否滿足條件,若滿足則結果加1,否則換8個格子繼續試。很顯然,64選8,並不是個小數字,十億級別的次數,夠暴力。如果換成圍棋的棋盤,畫面就會太美而不敢算。

稍加分析,我們可以得到另一個不那麼暴力的方法:顯然,每行每列最多只能有一位皇后,如果基於這個事實再進行暴力破解,那結果會好很多。安排皇后時,第一行有8種選法,一旦第一行選定,假設選爲(1,i),那麼第二行只能選(2,j),其中,j不等於i,所以有7種選法。以此類推,需要窮舉的情況有8!=40320種,比十億級別的小很多了。

這看起來已經不錯了,但嘗試的次數還是隨着問題規模按階乘水平提高的,我們仍然不滿意,所以,“遞歸回溯”的思想就被提出了,專治這種問題。

爲了理解“遞歸回溯”的思想,我們不妨先將4位皇后打入冷宮,留下剩下的4位安排進4x4的格子中且不能互相打架,有多少種安排方法呢?如果按照上面方式窮舉,需要4!=24次嘗試嗎?

現在我們把第一個皇后放在第一個格子,被塗黑的地方是不能放皇后的:

放第1個皇后

第二行的皇后只能放在第三格或第四格,比如我們放在第三格:

放第2個皇后

這樣一來前面兩位皇后已經把第三行全部鎖死了,第三位皇后無論放在第三行的哪裏都難逃被吃掉的厄運。於是在第一個皇后位於第一格,第二個皇后位於第三格的情況下此問題無解。所以我們只能返回上一步,來給2號皇后換個位置:

給2號皇后換個位置

此時,第三個皇后只有一個位置可選。當第三個皇后佔據第三行藍色空位時,第四行皇后無路可走,於是發生錯誤,則返回上層調整3號皇后,而3號皇后也別無可去,繼續返回上層調整2號皇后,而2號皇后已然無路可去,則再繼續返回上層調整1號皇后,於是1號皇后往後移一格位置如下,再繼續往下安排:

回溯重新安排1號皇后

上面的圖例正是回溯遞歸思想的展現,然而知易行難,在代碼中我們怎樣來實現這種算法呢?實現的代碼有很多種,我們找一個最簡單的來舉例吧:

遞歸回溯的核心代碼

我們來重點看一下這段代碼(這段代碼雖短,但真的非常非常重要,是整個算法的核心和靈魂):

第一次進來,row=0,意思是要在第一行擺皇后,只要傳進來的row參數不是8,表明還沒出結果,就都不會走if裏面的return,那麼就進入到for循環裏面,column從0開始,即第一列。此時第一行第一列肯定合乎要求(即check方法肯定通過),能放下皇后,因爲還沒有任何其他皇后來干擾。

關鍵是check方法通過了之後,在if裏面又會調用一下自己(即遞歸),row加了1,表示擺第二行的皇后了。第二行的皇后在走for循環的時候,分兩種情況,第一種情況:for循環沒走到頭時就有通過check方法的了,那麼這樣就順理成章地往下走再調用一下自己(即再往下遞歸),row再加1(即擺第三行的皇后了,以此類推)。第二種情況:for循環走到頭了都沒有通過check方法的,說明第二行根本一個皇后都擺不了,也觸發不了遞歸,下面的第三行等等後面的就更不用提了,此時控制第一行皇后位置的for循環column加1,即第一行的皇后往後移一格,即擺在第一行第二列的位置上,然後再往下走,重複上述邏輯。

注意,一定要添加清零的代碼,它只有在皇后擺不下去的時候會執行清0的動作(避免髒數據干擾),如果皇后擺放很順利的話從頭到尾是不會走這個請0的動作的,因爲已經提前走if裏面的return方法結束了。

總之,這段核心代碼很繞,原理一定要想通,想個十幾二十遍差不多就能理解其中的原理了,遞歸回溯的思想也就不言而喻了。八皇后問題一共有92種情況,下面是用Java實現的完整代碼:

public static int[][] arry=new int[8][8];//棋盤,放皇后
public static int map=0;//存儲方案結果數量

public static void main(String[] args) {
    // TODO Auto-generated method stub

    System.out.println("八皇后問題");
    findQueen(0);
    System.out.println("八皇后問題共有:"+map+"種可能");
}

public static void findQueen(int i){//尋找皇后節點
    if(i>7){//八皇后的解  
        map++;
        print();//打印八皇后的解
        return;
    }
    
    for(int m=0;m<8;m++){//深度回溯,遞歸算法
        if(check(i,m)){//檢查皇后擺放是否合適
            arry[i][m]=1;
            findQueen(i+1);
            arry[i][m]=0;//清零,以免回溯的時候出現髒數據
            }
    }   
}

public static boolean check(int k,int j){//判斷節點是否合適
    for(int i=0;i<8;i++){//檢查行列衝突
         if(arry[i][j]==1){
                return false;
         }
    }
    for(int i=k-1,m=j-1; i>=0 && m>=0; i--,m--){//檢查左對角線
        if(arry[i][m]==1){
                return false;
        }
    }
    for(int i=k-1,m=j+1; i>=0 && m<=7; i--,m++){//檢查右對角線
        if(arry[i][m]==1){
                return false;
        }
    }
    return true;
}

public static void print(){//打印結果
    System.out.print("方案"+map+":"+"\n");
    for(int i=0;i<8;i++){
        for(int m=0;m<8;m++){
            if(arry[i][m]==1){  
                //System.out.print("皇后"+(i+1)+"在第"+i+"行,第"+m+"列\t");
                System.out.print("o ");
            }
            else{
                    System.out.print("+ ");
            }
        }
        System.out.println();
    }
    System.out.println();
}

小禮物走一走,來簡書關注我



作者:Stephen_Xie
鏈接:https://www.jianshu.com/p/65c8c60b83b8
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯繫作者獲得授權並註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章