【基礎算法】回溯法與八皇后問題


 
 

  在國際象棋中,皇后是最強大的一枚棋子,可以吃掉與其在同一行、列和斜線的敵方棋子。比中國象棋里的車強幾百倍,比她那沒用的老公更是強的飛起(國王只能前后左右斜線走一格)。上圖右邊高大的棋子即為皇后。

   八皇后問題是這樣一個問題:將八個皇后擺在一張8*8的國際象棋棋盤上,使每個皇后都無法吃掉別的皇后,一共有多少種擺法?此問題在1848年由棋手馬克斯·貝瑟爾提出,豈止是有年頭,簡直就是有年頭,82年的拉菲分分鍾被秒的渣都不剩。

  八皇后問題是典型的回溯法解決的問題,我們以這個問題為例介紹回溯法。

  所謂回溯法,名字高大上,思想很朴素。設想把你放在一個迷宮里,想要走出迷宮,最直接的辦法是什么呢?沒錯,試。先選一條路走起,走不通就往回退嘗試別的路,走不通繼續往回退,直到找到出口或所有路都試過走不出去為止。

是的你沒說錯,這就是暴力破解。對於BigMoyan這種簡單粗暴不喜歡動腦子的人來說實在是太合適了,盲僧李青說得好:如果暴力不是為了解題,那就毫無意義了。

 

  盡管回溯法也算是暴力方法,但也不是特別暴力,特別暴力的相關部門都不讓播,能播的都是可以接受的暴力。怎么說?考慮八皇后問題,解決這個問題最暴力的辦法是這樣的:

  有關部門不讓播的方法:

  從8*8=64個格子里選8個格子,放皇后,測試是否滿足條件,若滿足則計數加1,否則換8個格子繼續試。

  很顯然, 64中選8,並不是個小數字,十億級別的嘗試次數,夠暴力。

  這還是8*8的格子,要是換圍棋棋盤……這畫面太美我都不敢算。

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

  看起來這個結果已經不錯了,但嘗試的次數是隨問題規模按階乘水平提高的,BigMoyan仍然不滿意——咋可能滿意嘛!回溯法還沒出,我要是滿意了剩下的篇幅講啥。

  8皇后太多,后宮太過豐富BigMoyan可hold不住,不如我們先裁一半分析試試,於是BigMoyan與4位皇后辦了離婚手續,同時把家縮小了一半,那么現在問題變成了4皇后問題,4個皇后在4*4的格子里各自安排不打架,一共有多少種安排方法?

 

  試着來窮舉一下,真的需要4!=24次嘗試嗎?

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

 

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

  糟啦擼,前兩位皇后沆瀣一氣,已經把第三行全部鎖死了,第三位皇后無論放哪里都難逃被吃掉的厄運。於是在第一個皇后位於1號,第二個皇后位於3號的情況下問題無解。我們只能返回上一步來,給2號皇后換個位置。

  顯然,第三個皇后只有一個位置可選。當第三個皇后占據第三行藍色空位時,第四行皇后無路可走,於是發生錯誤,返回上層調用(3號皇后),而3號也別無可去,繼續返回上層調用(2號),2號已然無路可去,繼續返回上層(1號),於是1號皇后改變位置如下,繼續搜索。

  話說道這里,想必讀者對“回溯法”已經有了基本概念。然而所謂知易行難,理解算法和將算法寫出來完全是兩回事。按照BigMoyan的風格,下面該進行算法分析了,下面的代碼改寫自劉汝佳《算法競賽入門經典》,幾乎是BigMoyan看到的實現8皇后問題最簡潔的代碼,改寫后整個函數只有10行。

void queen(int row){
    if(row==n)
        total++;
    else
        for(int col=0;col!=n;col++){
            c[row]=col;
            if(is_ok(row))
                queen(row+1);
        }        
}

 

  算法是逐行安排皇后的,其參數row為現在正執行到第幾行。n是皇后數,在八皇后問題里當然就是8啦。

  第2行好理解,如果程序當前能正常執行到第8行,那自然是找到了一種解法,於是八皇后問題解法數加1。

  如果當前還沒排到第八行,則進入else語句。遍歷所有列col,將當前col存儲在數組c里,然后使用is_ok()檢查row行col列能不能擺皇后,若能擺皇后,則遞歸調用queen去安排下一列擺皇后的問題。

  還不太清楚?再慢點來,剛開始的時候row=0,意思是要對第0行擺皇后了。

  If判斷失敗,進入else,進入for循環,col初始化為0

  顯然,0行0列的位置一定可以擺皇后的,因為這是第一個皇后啊,后宮空盪她想怎么折騰就怎么折騰,於是is_ok(0)測試成功,遞歸調用queen(1)安排第1行的皇后問題。

  第1行時row=1,進來if依然測試失敗,進入for循環,col初始化為0。1行0列顯然是不能擺皇后的,因為0行0列已經有一個聖母皇太后在那擱着了,於是is_ok()測試失敗,循環什么也不做空轉一圈,col變為1。1行1列依然is_ok()測試失敗,一直到1行2列,發現可以擺皇后,於是繼續遞歸queen(2)去安排第二個皇后位置。

  如果在某種情況下問題無解呢?例如前面在4皇后問題中,0行0列擺皇后是無解的。假設前面遞歸到queen(2)時候,發現第2行沒有地方可以擺皇后,那怎么辦呢?要注意queen(2)的調用是在queen(1)的for循環框架內的,queen(2)若無解,則自然而然queen(1)的for循環col自加1,即將第1行的皇后從1行2列改為1行3列的位置,檢查可否放皇后后繼續安排下一行的皇后。如此遞歸,當queen(0)的col自加到7,說明第一列的皇后已經遍歷了從0行1列到0行7列,此時for循環結束,程序退出。

  在主函數中調用queen(0),得到正確結果,8皇后問題一共有92種解法。

  全部程序如下:

 

 1 #include<iostream>
 2 #include<math.h>
 3 using namespace std;
 4 
 5 int n=8;
 6 int total=0;
 7 int *c=new int(n);
 8 
 9 bool is_ok(int row){
10     for(int j=0;j!=row;j++){
11         if(c[row]==c[j] || row-c[row]==j-c[j] || row+c[row]==j+c[j])
12             return false;
13     }
14     return true;
15 }
16 
17 void queen(int row){
18     if(row==n)
19         total++;
20     else
21         for(int col=0;col!=n;col++){
22             c[row]=col;
23             if(is_ok(row))
24                 queen(row+1);
25         }       
26 }
27 
28 int main(){
29     queen(0);
30     cout<<total;
31     return 1;
32 }
33  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM