1. 程式人生 > >八皇后問題詳解(四種解法)

八皇后問題詳解(四種解法)

這裡寫圖片描述
如果你去百度百科八皇后這個問題,你會發現人家也是歷史上有頭有臉的一個問題,最後一句“計算機發明後就有一萬種方式解決這個問題”讀起來也讓程式猿們很快活。閒話少說,開始闡述我的思路:

最無腦的解法一定是八個for遍歷,浪費了太多的計算資源在各種無用功上面,我們稍微構思一下:
首先如何決定下一個皇后能不能放這裡可以有兩種思路,第一種是嘗試維護一個8*8的二維矩陣,每次找到一個空位放下一個皇后就把對應行列對角線上的棋格做個標記,如果某行找不到可放皇后的格子就把上一個皇后拿走並把對應行列對角線的標記取消掉;第二種方法直接放棄構造矩陣來假裝棋盤,我們把問題更加抽象化,八個皇后能放下一定是一行放一個,我們只需一個數組記錄每個皇后的列數(預設第N個放第N行),那麼問題就被抽象成了陣列的第N個數和前N-1個數不存在幾個和差關係即可(比如差不為零代表不在同一列)。

接著想想問題中存在著大量的迴圈怎麼解決比較高效,我們知道遞迴和迭代一定程度上是可以很容易做到互相轉化實現同樣的思路的。遞迴是重複呼叫函式自身實現迴圈,迭代是函式內某段程式碼實現迴圈,使用遞迴的話我們應該要有一個能在第N行找到某一列的格子可以放皇后的函式,能找到把引數+1去呼叫自己去找下一行皇后能放的格子,找不到就算了。如果想用迭代,前面我們說過遞迴迭代是可以轉化的,這種在函式最後呼叫自己的遞迴更是極易轉化,我們按著迭代的套路在for迴圈的裡按照剛剛遞迴的思路加幾個判斷判別迴圈是continue、break還是返回前一層迴圈即可。最後還有一種思路,準確來說還是和遞迴脫離不了關係,學習遞迴的時候我們我們知道,遞迴可以看做底層幫你維護的一個堆疊不斷地push、pop,知道它的本質我們也可以通過手動維護一個堆疊來模擬這個遞迴呼叫的過程,只要構造兩個函式backward(往後回溯)、refresh(向前重新整理)來模擬堆疊進出即可。

最後我們來分析四個方法(矩陣維護法、遞迴法、迭代法、手動堆疊法)表現和改進,很明顯在程式碼量上遞迴會是最短的,而需要執行的空間來看手動堆疊也會比較必要更大的執行記憶體(如果用VS執行手動堆疊的程式碼,很有可能會提示你stack溢位,那麼你需要修改一下VS的配置給你的程式分配更大的記憶體)。八皇后問題有很多小細節可以改進(具體實現大家自己來,為了方便我就說一些我想到的點):很明顯棋盤是對稱的,如果你得出了一個解法那麼一定有行對稱列對稱對角線對稱的另外三種對稱的擺法,這樣就可以減少一些計算量。

頭腦風暴過後,結合程式碼和註釋講解具體實現過程:
1.矩陣維護法
這是第一個出現在我頭腦中的方法,很桑心居然不是遞迴,看來腦子還不夠抽象。上程式碼:

//八皇后維護矩陣法
#include<iostream>
using namespace std;
int cheese_table[8][8];
int queen[8];//記錄五個皇后的列數
int lastqueen=-1;
int solution=0;
int search_line(int i,int j){//搜尋這一行有沒可放的位置
    for(;j<8;j++)
        if(cheese_table[i][j]==0)
            return j;
    return -1;
}
void set_queen(int i,int j){//在可放的位置上放上皇后記錄下來並對棋盤進行操作
    cheese_table[i][j]=-1;
    queen[i]=j;
    for(int temp=0;temp<8;temp++)//列操作
        if(cheese_table[temp][j]!=-1)
            cheese_table[temp][j]++;
    for(int temp=0;temp<8;temp++)//行操作
        if(cheese_table[i][temp]!=-1)
            cheese_table[i][temp]++;
    int tempj=j+1;
    for(int tempi=i+1;tempi<8&&tempj<8;tempi++)//東南對角線操作
        cheese_table[tempi][tempj++]++;
    tempj=j-1;
    for(int tempi=i+1;tempi<8&&tempj>=0;tempi++)//東北對角線操作
        cheese_table[tempi][tempj--]++;
    tempj=j+1;
    for(int tempi=i-1;tempi>=0&&tempj<8;tempi--)//西南對角線操作
        cheese_table[tempi][tempj++]++;
    tempj=j-1;
    for(int tempi=i-1;tempi>=0&&tempj>=0;tempi--)//西北對角線操作
        cheese_table[tempi][tempj--]++;
    return;
}
void uptake_queen(int i){
    int j=queen[i];
    for(int temp=0;temp<8;temp++)//列操作
        if(cheese_table[temp][j]!=-1)
            cheese_table[temp][j]--;
    for(int temp=0;temp<8;temp++)//行操作
        if(cheese_table[i][temp]!=-1)
            cheese_table[i][temp]--;
    int tempj=j+1;
    for(int tempi=i+1;tempi<8&&tempj<8;tempi++)//東南對角線操作
        cheese_table[tempi][tempj++]--;
    tempj=j-1;
    for(int tempi=i+1;tempi<8&&tempj>=0;tempi++)//東北對角線操作
        cheese_table[tempi][tempj--]--;
    tempj=j+1;
    for(int tempi=i-1;tempi>=0&&tempj<8;tempi--)//西南對角線操作
        cheese_table[tempi][tempj++]--;
    tempj=j-1;
    for(int tempi=i-1;tempi>=0&&tempj>=0;tempi--)//西北對角線操作
        cheese_table[tempi][tempj--]--;
    cheese_table[i][j]=0;
    return;
}
int main(){
    for(int i=0;i<8;i++)
        for(int j=0;j<8;j++)
            cheese_table[i][j]=0;
    //初始化棋盤
    for(int i=0;;i++){//一行一行操作
        int j=search_line(i,lastqueen+1);
        if(j==-1){//沒有放皇后的位置了,回頭
            if(i==0)break;//真正結束位置
            uptake_queen(i-1);
            lastqueen=queen[i-1];
//把上一行的queen的位置記錄下來,便於回頭的時候從這個位置之後尋找可放位置
            i-=2;
        }
        else{
//把棋盤對應位置放上皇后,對這個皇后會影響的棋格進行操作
            lastqueen=-1;
            set_queen(i,j);
            if(i==7){
                solution++;
                uptake_queen(7);
                lastqueen=j;
                i--;
            }
        }
    }
    cout<<solution<<endl;

    return 0;
}

稍微講解一下,cheese_table為8*8的棋盤,queen陣列記錄八個皇后各自的列數(前面說過,第N個皇后預設放在第N行,所以行數是隱式記錄的),lastqueen記錄著最後放置的那個皇后的列數(回溯時候很重要,保證回溯到上一行操作時候不會踏進同一個坑即不會再把皇后放到剛剛放過的地方),solution記錄八皇后有幾種放的方法。Search_line(i,j)函式將會搜尋第i行從j列開始還有沒可以放的格子,set_queen(i,j)就是在可放皇后的(I,j)格子放下皇后,並且在棋盤上對放下的這個皇后的行列和主副對角線的格子進行標記,標記的方法是代表這些格子的數+1(這是本解法很關鍵的一點,並不是簡簡單單的對這些不可放置點從一個狀態比如0置為1代表不可放置了,而是每次把某個皇后對應影響的這些格子的數都增加1,這麼做極大的好處就是你回溯的時候只要逆著過去對拿走的皇后本會影響的格子減1即可,而不需要判斷這些格子是否還會被其他在棋盤上的的皇后影響從而決定維持不可放的狀態還是變為可放的狀態,極大的減少了維護棋盤時候大量呼叫判斷函式的時間,而只要簡單的加減即可)。Uptate_queen(i)函式就是拿起第i行的皇后,即本解法的回溯部分,對應set的過程你這做即可。最後看看主函式,初始化不說了,for迴圈中大致過程就是對每一行search出皇后可放位置,找到可放格子就放下皇后,如果八個皇后都放完了記一次數,並且在最後一行尋找是否有其他放皇后的位置,沒有的話往前一行回溯;剛剛在某一行search不到放皇后的格子就只能回溯上一行。如果發現這一行就是第0行沒有上一行了還要回溯,證明我們演算法結束了,退出迴圈。這個for迴圈大概是假的for迴圈,沒有限定i的大小,依靠的其實是想要回溯之前看看還能不能回溯來跳出。

2.遞迴法

//八皇后遞迴解法
#include<iostream>
using namespace std;
int queen[9]={-1,-1,-1,-1,-1,-1,-1,-1,-1};
int count=0;
bool available(int pointi,int pointj){//判斷某個皇后是否與已有皇后衝突
    for(int i=1;i<pointi;i++){
        if(pointj==queen[i])return false;//同一列拒絕
        if((pointi-i)==(pointj-queen[i]))return false;//同一主對角線拒絕
        if((pointi-i)+(pointj-queen[i])==0)return false;//同一副對角線拒絕
    }
    return true;
}
void findSpace(int queenNumber){//在第queenNumber行找能放皇后的位置
    for(int i=1;i<9;i++){//從1~8遍歷這一行的八個空位
        if(available(queenNumber,i)){
//如果可以放這個位置就記錄下第queenNumber個皇后的位置
            queen[queenNumber]=i;
            if(queenNumber==8){//如果八個皇后都放滿了統計一下
                count++;
                return;
            }
            int nextNumber=queenNumber+1;//還有皇后沒放遞迴放下一個皇后
            findSpace(nextNumber);
        }
    }
    queen[--queenNumber]=-1;//如果這一行沒有可放的位置說明上一行皇后放的位置不行,要為上一個皇后尋找新的可放位置
    return;
}
int main(){
    findSpace(1);//從(1,1)開始遞迴好理解
    cout<<count<<endl;
    return 0;
}

遞迴法不多說了,八皇后的最標準解法,我的註釋也很詳細,唯一我自己加的一個小技巧是把一開始設為1,1而不是0,0,畢竟人類都是習慣從1開始,當然我現在有點後悔了,寫文章的時候再看程式碼感覺很腦殘,畢竟本文物件程式猿好像已經習慣從下標0開始計數了哈。所以一開始陣列設了9個元素,main函式呼叫遞迴函式從1,1開始都當是我的自作多情,大家開心就好。

3.迭代法

//八皇后迭代解法
#include<iostream>
using namespace std;
int count=0;
int queen[8]={-1,-1,-1,-1,-1,-1,-1,-1};
bool available(int pointi,int pointj){//判斷某個皇后是否與已有皇后衝突
    for(int i=0;i<pointi;i++){
        if(pointi==i)return false;//同一行拒絕
        if(pointj==queen[i])return false;//同一列拒絕
        if((pointi-i)==(pointj-queen[i]))return false;//同一主對角線拒絕
        if((pointi-i)+(pointj-queen[i])==0)return false;//同一副對角線拒絕
    }
    return true;
}
int main(){
    int j=0;
    for(int i=0;i<8;i++){//對於每一行
        if(i==-1)break;//這才是真正退出迴圈的出口
        for(;j<8;j++){
            if(available(i,j)){
                queen[i]=j;
                if(i==7){
                    count++;
                    if(j==7){//如果最後一行最後一格試完就往前回溯
                        j=queen[--i];
                        j++;
                        queen[i]=-1;
                        i--;
                        break;
                    }
                    else
                        continue;
                }
                j=0;
                break;
            }
            else
                if(i==7&&j==7){
                    j=queen[--i];
                    j++;
                    queen[i]=-1;
                    i--;
                    break;
                }
        }
        if(j==8){
                j=queen[--i];
                j++;
                queen[i]=-1;
                i--;
        }
    }
    cout<<count<<endl;
    return 0;
}

由於迭代法是我用迭代的套路來完成遞迴的思路的一個解法,所以直接看有點抽象,但是理解上邊的遞迴以後再來看你就會發現:嗯,這裡我好像見過。

4.手動維護堆疊法

//八皇后手動維護堆疊解法
#include<iostream>
using namespace std;
int QueenNumber=0;
int solutionCount=0;
int stopflag=0;
struct point{
    int pointi;
    int pointj;
}queenPoint[8];
bool available(int pointi,int pointj);
void backward();
void refresh(int pointi,int pointj);
int main(){
    for(int i=0;i<8;i++)
        queenPoint[i].pointi=queenPoint[i].pointj=-1;
    //從(0,0)格子開始遞迴
    refresh(0,0);
    cout<<solutionCount<<endl;
    return 0;
}
bool available(int pointi,int pointj){
    for(int i=0;i<QueenNumber;i++){
        if(pointi==queenPoint[i].pointi)return false;//同一行拒絕
        if(pointj==queenPoint[i].pointj)return false;//同一列拒絕
        if((pointi-queenPoint[i].pointi)==(pointj-queenPoint[i].pointj))return false;
        //同一主對角線拒絕
        if((pointi-queenPoint[i].pointi)+(pointj-queenPoint[i].pointj)==0)return false;
        //同一副對角線拒絕
    }
    //都沒問題返回可以
    return true;
}
void backward(){
    QueenNumber--;
    int tempi=queenPoint[QueenNumber].pointi;
    int tempj=queenPoint[QueenNumber].pointj;
    queenPoint[QueenNumber].pointi=queenPoint[QueenNumber].pointj=-1;
    if(QueenNumber<0)stopflag=1;;
    refresh(tempi,++tempj);
}
void refresh(int pointi,int pointj){
    //先是兩種特殊情況的判斷?
    if(stopflag==1)return;
    if(pointj==8)
        backward();
    //某一格可放就更新資訊往下行遞迴
    if(available(pointi,pointj)){
        queenPoint[QueenNumber].pointi=pointi;
        queenPoint[QueenNumber].pointj=pointj;
        QueenNumber++;
        //如果八個皇后都放完就計數回溯
        if(QueenNumber==8){
            solutionCount++;
            backward();
        }
        //否則往下遞迴
        else
            refresh(++pointi,0);
    }
    //某一格不可放就往下一個格子遞迴
    else{
        //如果某一行都不行就回溯
        if(pointj==7)
            backward();
        else
            refresh(pointi,++pointj);
    }
}

手動維護堆疊對於我們理解遞迴本質是很有好處的,當然這段程式碼飛出了一個大bug——stack overflow,大體思路上沒啥問題啊bug老飛啊飛就很煩,時間緊迫最後我就簡單粗暴把VS的執行記憶體調大了許多解決的,希望有志之士幫忙看看本質的解決方案是什麼。
具體說說程式碼(這幾個程式碼不是連貫寫下來的,算是好幾個晚上有空的時候碼一碼,所以我總感覺自己看的有點不連貫不知為啥),queennumber記錄已經在棋盤上放的皇后數量,solutionnumber不說了結果數量,stopflag就是上面幾個方法用來跳出迴圈的這裡直接弄了一個flag,定義了一個結構point記錄八個皇后的行列資訊(大概就是這裡和前面很反差吧),三個函式available用來判斷某個點能不能放皇后,refresh用來往前推進的,函式中前三行後面說,第四行開始是主要工作,在這一行內呼叫available判斷某個格子能不能放皇后,可以的話記錄資訊,並且判斷是否把八個皇后都放完了,是的話回溯,否則從下一行的第一個格子開始遞迴,如果available判斷某格子不能的話跳到本行下一個格子遞迴,如果某一行發現都無法放皇后,呼叫backward回溯。Backward函式主要做的就是,取消最近放的那個皇后的一切資訊,在回溯過程中如果發現再回溯得回溯第-1行了(即第一歌皇后放在第一行最後一個格子的所有情況都嘗試過了),把stopflag變為1,backward最後呼叫refresh對上一個皇后的下一個可能位子遞迴判斷。所有回過頭看refresh前三行,首先如果發現stopflag出現了,那麼一層一層退回去結束這個邏輯上的迴圈,第二個if用來判斷某一行是否找不到皇后的放置點了,是的話這個解法不行,上一個皇后得換地方。

至此,四種方法都已經實現,因為寫的時間不同,有些較早寫完的沒有用上後邊發現比較快速的技巧(比如不需要記錄行的資訊),還望海涵,另外對於頭腦風暴說的對稱情況考慮可以縮小規模,大家可以自己實現。
that’s all thank you