1. 程式人生 > >資料結構應用案例——棧結構用於8皇后問題的回溯求解

資料結構應用案例——棧結構用於8皇后問題的回溯求解

                     

【說明】本文來自由周世平老師主編的《C語言程式設計》教材。我作為參編人員執筆了第7、8章。“第8章 問題求解與演算法”中“8.6.1 回溯法”以8皇后問題的求解為例,介紹了回溯法的解題過程。這個解決方案中用到了“棧”,引用至此,作為棧應用的例子。需要說明的是,教材面向程式設計初學者,並全文中並未提出過任何關於“棧”的描述。這樣做,隱藏了術語,減少初學者的認知難度。對於資料結構的學習者而言,由於知識面的擴大,卻用不著迴避這樣的術語了。於是,在閱讀本文時,作為體會棧的應用,需要自行從中提取出應用棧式儲存及處理的部分來。

【全文】   回溯法是一種通用的搜尋演算法,幾乎可以用於求解任何可計算的問題。演算法的執行過程就像是在迷宮中搜索一條通往出口的路線,總是沿著某一方向向前試探,若能走通,則繼續向前進;如果走不通,則要做上標記,換一個方向再繼續試探,直到得出問題的解,或者所有的可能都試探過為止。   下面,用經典的8皇后問題為例來講解如何使用回溯的思想解決問題。

 

  8皇后問題是:在8×8的棋盤上擺放8個皇后,使其不能互相攻擊,即任意的兩個皇后不能處在同一行,同一列,或同一斜線上。可以把八皇后問題拓展為n皇后問題,即在n×n的棋盤上擺放n個皇后,使其任意兩個皇后都不能處於同一行、同一列或同一斜線上。

  首先需要對棋盤進行描述。直觀地,棋盤可以用二維陣列表示,有皇后的棋格對應陣列元素值為1,無皇后的棋格對應陣列元素值為0。但這種儲存結構並不是最簡單有效的選擇。   圖8.21中左邊部分給棋盤的行、列編了號,提供的擺放方法,就是問題的一個解。右邊的部分,將各行上皇后所在的列數記錄下來,用這8個數字(4, 6, 8, 2, 7, 1, 3, 5),也構成了對問題解的一種描述。 這裡寫圖片描述

圖8.21 8皇后問題的一個解

  由此可以看出,可以定義一個一維陣列int x[N];,用x[i]的值表示第i行上皇后所在的列數,n皇后問題的解可以用(x[1], x[2], ….. x[n])的形式描述。   解決了資料表示的問題,設計資料處理的方法。這裡要用回溯的策略,設計計算機對n皇后問題的求解方法。以4皇后為例,如圖8.22所示,在圖8.22(a)中,第1行第1列上放置一個皇后,圖8.22(b)中確定第2行的可能放法,在嘗試第1列、第2列由於相互攻擊而放棄之後,確定在第3列放置可以繼續,在圖8.22(c)中繼續對第3行進行考察,發現將所有4列都嘗試過了,也沒有辦法將皇后安排一個合適的位置,對第4行做任何的嘗試都沒有意義,這時產生回溯,結果是在圖8.22(d)中將第2行的皇后安排到第4列,然後第3行的暫時可以放在第2列,在圖8.22(e)中試著確定第4行的皇后,卻發現無解再次回溯,只能夠如圖8.22(f)所示將第1行的皇后放到第2列,再經圖8.22(g)、(f)之後找到4皇后問題的一個解,那就是圖8.22(g)的(2, 4, 1, 3)。 這裡寫圖片描述

圖8.22  用回溯找出4皇后問題一個解的過程

  在圖8.23中,給出了求出4皇后問題所有解的完整過程的描述。圖中(1 * * *)對應圖8.22(a)中第1行皇后安排在第1列,其他行待定的狀態,接下來的(1 3 * *)對應了圖8.22(b)中第2行皇后安排在第3列的狀態。可以判斷出在這個狀態下,繼續嘗試並不能夠完成求解,於是發生回溯(其下方的B代表回溯),於是下一個嘗試的狀態將是(1 4 * *),……。將這樣的過程繼續下去,能夠找出4皇后問題的所有解(2 4 1 3)和(3 1 4 2),如圖8.23中兩個加網格背景的結點。 這裡寫圖片描述 圖8.23 求出4皇后問題所有解的完整過程

  搞清楚用回溯法求解的過程後,將關注如何基於(x[1], x[2], ….. x[n])形式的解結構,寫出讓計算機完成求解過程的程式碼。4皇后問題尚且可以在紙上畫出解,8皇后問題的可能解有8!=40320種,最終解有92種,必須要依靠計算機求解了。   什麼樣的解才是可行的?需要描述出任何兩個皇后可以“互相攻擊”這樣的條件:   (1)有兩個皇后處在同一行:解的結構(x[1], x[2], ….. x[n])已經保證同一行不會出現兩個皇后。   (2)有兩個皇后處在同一列:表示為x[i]=x[k],假如在圖8.23中出現表示為(1 1 * *)、(4 2 3 2)之類的結點,則說明有兩個皇后在同一列了。   (3)有兩個皇后處在同一斜線:若兩個皇后的擺放位置分別是第i行第x[i]列、第k行第x[k]列,若他們在棋盤上斜率為-1的斜線上,滿足條件i-x[i]=k-x[k],例如(1 4 3 *)、(4 1 2 *);若他們在棋盤上斜率為1的斜線上,滿足條件i+x[i]=k+x[k]。將這兩個式子分別變換成i-k=x[i]-x[k]和i-k=x[k]-x[i],例如(3 4 1 *)。綜合兩種情況,兩個皇后位於同一斜線上表示為|i-k|=|x[i]-x[k]|。   在下面的程式實現中,place(x, k)函式用於判斷在第k行第x[k]列放置皇后,是否會與前面擺放好的皇后產生相互攻擊。只要有某行(第i行)的皇后與這個第k行的皇后處在同一列(x[i]=x[k])或者處在同一斜線(|i-k|=|x[i]-x[k]|),則立即返回假(0),表示不可能構成解。   再接下來,就是在實現問題求解的nQueens(x, n)函式中,從第1行開始,逐行逐列地考察皇后的擺放,當遇到某一行所有可能情況試過不必再深入到下一行考察時,及時回溯到上一行,接著考察。   程式實現中,將儲存解的陣列定義成了動態陣列。多分配一個單元,因為陣列的首元素x[0]一直空閒未用,有用的單元是x[1]到x[n]。    【例8.12】 求解8皇后問題的程式

#include <stdio.h>#include <math.h>#include <malloc.h>void nQueens(int *x, int n);     /*求解n皇后問題*/int place(int *x, int k);         /*判斷是否可以在第k行第x[k]列擺放皇后*/void printSolution(int *x, int n);  /*輸出求解結果*/int main(){    int n;    int *x;                        /*存放求解結果的陣列首地址*/    scanf("%d", &n);    x=(int*)malloc(sizeof(int)*(n+1));  /*動態分配陣列空間, x[0]空閒*/    nQueens(x, n);    return 0;}/*如果一個皇后能放在第k行第x[k]列,則返回真(1),否則返回假(0)*/int place(int *x, int k){    int i;    /*對前k-1行,逐行考察*/    for(i=1; i<k; i++)    {        /*如果前k-1行中有某行的皇后與第k行的在同一列或同一斜線,返回0*/        if((x[i]==x[k])||(fabs(x[i]-x[k])==fabs(i-k)))            return 0;    }    /*能執行下一句,說明在第k行第x[k]列擺放皇后,不會互相攻擊*/    return 1;}/*求解在n×n的棋盤上,放置n個皇后,使其不能互相攻擊*/void nQueens(int *x, int n){    int k;    k = 1;    /*k是當前行*/    x[k] = 0/*x[k]是當前列,進到迴圈中,立刻就會執行x[k]++,而選擇了第1列*/    while(k>0)/*當將所有可能的解嘗試完後,k將變為0,結束求解過程*/    {        x[k]++;                      /*移到下一列*/        while(x[k]<=n && !place(x, k))   /*逐列考察,找出能擺放皇后的列x[k]*/            x[k]++;        if(x[k]<=n)                   /*找到一個位置可以擺放皇后*/        {            if(k==n)                  /*是一個完整的解,輸出解*/                printSolution(x, n);            else  /*沒有完成最後一行的選擇,是部分解,轉向下一行*/            {                k++;    /*接著考察下一行*/                x[k]=0/*到迴圈開始執行x[k]++後,下一行將從第1列開始考察*/            }        }        else  /*對應x[k]>n的情形,這一行已經沒有再試的必要,回溯到上一行*/            k--;  /*上一行在原第x[k]列的下1列開始考察*/    }}/*輸出求解結果*/void printSolution(int *x, int n){    int i, j;    for (i = 1; i <= n; i++)    /*輸出第i行*/    {        for (j=1; j<=n; j++)        {              if (j == x[i])    /*第x[i]列輸出Q,其他列輸出*號 */                printf("Q");            else                printf("*");        }        printf("\n");    }    printf("\n");}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76

【思考題】請從解題策略和程式中,找出何處使用了棧,是如何將棧應用於回溯過程的?