馬周遊問題非遞歸算法(不要求回到起點)

1、  原題中文大意;node

對於一個8*8的棋盤,用下列的方式編號 ios

若是它走63步正好通過除起點外的其餘位置各一次,這樣一種走法則稱馬的周遊路線,設計一個算法,從給定的起點出發,找出它的一條周遊路線。馬的走法是「日」字形路線。 算法

 Input數組

輸入有若干行。每行一個整數N(1<=N<=64),表示馬的起點。最後一行用-1表示結束,不用處理。 數據結構

 Output函數

對輸入的每個起點,求一條周遊線路。對應地輸出一行,有64個整數,從起點開始按順序給出馬每次通過的棋盤方格的編號。相鄰的數字用一個空格分開。測試

  1. 2、  算法思想及解題用到的主要數據結構;優化

最本質的仍是圖的遍歷的問題。這裏使用深度優先搜索,不用廣度優先是由於一般在尋找路徑或者迷宮之類的題目中,都是探尋的問題,用深度優先比廣度優先容易比較快地找到解。spa

涉及到回溯算法,即走到一個「死衚衕」後,返回上一步,選擇其餘方向。這裏就是到達一個不能往其餘任何方向走的位置。.net

回溯和深度優先肯定了,而後就是選擇非遞歸算法,由於本身確實理解能力有限,遞歸的方法不能徹底想明白。因而就本身思考加上查閱資料,使用非遞歸算法。

其中要提升效率,須要剪枝,即每次選擇下一位置時,要經過必定篩選策略選擇比較快速高效的一步。這裏選擇下一步位置具備最少可行步數的一步。

 

主要數據結構:

1、路徑的樹節點,使用自定義結構體,

struct Node {

            int x, y, num; // x, y 爲矩陣座標(0~7 0~7 num爲對應的數值,由xy算出

         int neighbor[8]; // 下一步數組,每個爲對應的方向數組,在運算中使用座標值,能夠訪問爲0 反之爲1

};

2、每個位置對應的走「日」字的方向數組

         int dirx[8] = {2,1,-1,-2,-2,-1,1,2};  // x方向數組

int diry[8] = {-1,-2,-2,-1,1,2,2,1}; // y方向數組

   queue<node> states;

3、記錄訪問順序的數組

int seq[64];  // 記錄走的過程

   4、深度優先回溯中用到的棧

stack<Node> t_route;  // 樹節點棧

5、標記數組

int board[8][8]; // 記錄已訪問的位置,已訪問爲 1 ,未訪問爲0

3、  詳細解題思路

1、 對於每組數據,首先把board訪問數組清零,step值置0,聲明一個Node棧的t_route。

將起始位置初始化爲一個Node結點,壓棧。

2、進入一個while循環,循環判斷條件是棧不爲空。

取棧頂元素,seq數組中step下標位置爲改棧頂元素的num值,記錄路徑, step++。若是此時step的值等於64,則說明走完了整個棋盤,退出循環。

同時,board數組也將棧頂元素對應座標位設爲1(已訪問)

3、針對棧頂元素,遍歷它的8個鄰居節點,在可行的鄰居節點中選擇可行步數最少的一個。

找到後,用flag記錄該鄰居節點的下標值,未找到則flag爲-1

  4、找到一個可行節點,將棧頂元素的neighbor數組對應下標位設爲1(已訪問)將該節點的值初始化(x,y,num,neighbor[8]),壓棧。

     棧頂元素沒有一個可行節點,將step減1,board數組對應下標位置設爲0,出棧,回溯。

  5、判斷step是否等於64,如果,則找到解,一次輸出seq數組的值;反之,沒有解,輸出-1。

其中計算一個位置是否能夠到達,調用函數canmove判斷,判斷條件爲該位置座標不越界合法而且board數組中對應下標位置爲0。

  每一個Node裏面有二維數組的座標值和自己數組,用一個轉換函數xy_to_num,能夠利用座標值算出數值。

  計算下一步的可行步數時,調用函數next_neighbor計算,函數中用canmove函數計算。

4、  逐步求精算法描述(含過程及變量說明)

變量及函數說明

// 非遞歸DFS中樹節點的結構體

struct Node {

         int x, y, num; // x, y 爲矩陣座標(0~7 0~7 num爲對應的數值,由xy算出

         int neighbor[8]; // 下一步數組,每個爲對應的方向數組,在運算中使用座標值,能夠訪問爲0 反之爲1

};                                      

 

int dirx[8] = {2,1,-1,-2,-2,-1,1,2};  // x方向數組

int diry[8] = {-1,-2,-2,-1,1,2,2,1}; // y方向數組

int seq[64];  // 記錄走的過程

int step;    // 指向seq對應的下標進行賦值, 每走一步step + 1,回溯一次step - 1

int board[8][8]; // 記錄已訪問的位置,已訪問爲 1 ,未訪問爲0

int xy_to_num(int x, int y);     // 計算座標對應數值,參數爲座標

bool canmove(int x, int y);      // 計算該位置是否可行,參數爲座標

int next_neighbor(int x, int y); // 計算下一位置的可行步數,參數爲座標

 

初始化起始位置結點,board數組,step

標記board數組,初始結點壓棧;

While(棧不爲空) {

   seq[step] = 棧頂元素的num

   board中棧頂元素設爲已訪問

   step++

   min = 8;  //初始化最小步數

   if ( step  == 64)

     找到路徑,退出循環;

  遍歷棧頂元素的鄰居節點,計算鄰居節點的可行步數

  If(找到一個鄰居節點){

     棧頂元素的該鄰居下標設爲1,已訪問。

     初始化下一步節點的x, y, num, neighbor[8]

     壓棧

}

  Else {

     棧頂元素出棧;

     Board對應位置設置爲 0

     Step--;

}

}

  If (step == 64)  // 找到解

輸出seq數組

  Else

    輸出「-1

5、  程序註釋清單(重要過程的說明);

#include<iostream>
#include<stdio.h>
#include<string>
#include<string.h>
#include<cstring>
#include<stack>
using namespace std;
// 非遞歸DFS中樹節點的結構體
struct Node {
         int x, y, num; // x, y 爲矩陣座標(0~7, 0~7 ,num爲對應的數值,由x,y算出
         int neighbor[8]; // 下一步數組,每個爲對應的方向數組,在運算中使用座標值,能夠訪問爲0, 反之爲1;
};                                      
 
int dirx[8] = {2,1,-1,-2,-2,-1,1,2};  // x方向數組
int diry[8] = {-1,-2,-2,-1,1,2,2,1}; // y方向數組
int seq[64];  // 記錄走的過程
int step;    // 指向seq對應的下標進行賦值, 每走一步step + 1,回溯一次step - 1
int board[8][8]; // 記錄已訪問的位置,已訪問爲 1 ,未訪問爲0
int xy_to_num(int x, int y);     // 計算座標對應數值,參數爲座標
bool canmove(int x, int y);      // 計算該位置是否可行,參數爲座標
int next_neighbor(int x, int y); // 計算下一位置的可行步數,參數爲座標
 
int main() {
         int N, min, flag, step, i, nextloc;
        
         while(scanf("%d", &N) && N != -1) {
           if (N >= 1 && N <= 64) {                         
                   stack<Node> t_route;  // 樹節點棧
                   step = 0;
                   memset(board, 0, sizeof(int) * 64);
                   // 初始化根節點狀態,壓棧
                   Node ini;
                   ini.y = (N-1) % 8;
                   ini.x = (N-1) / 8;
                   ini.num = N;
                   for (i = 0; i < 8; i++) {
                            if(canmove(ini.x + dirx[i], ini.y + diry[i]))
                                     ini.neighbor[i] = 0;
                            else
                                     ini.neighbor[i] = 1;                                
                  }
                   t_route.push(ini);
 
                   while (!t_route.empty()) {
                            Node temp = t_route.top();
                            board[temp.x][temp.y] = 1; // 棧頂已訪問
                            seq[step++] = temp.num; // 路徑數組賦值
                            // 找到路徑,退出
                            if (step == 64)
                                     break;
                            flag = -1; // 記錄有最少可行步數的鄰居節點的下標
                            min = 8;  // 最少步數
                            // 尋找最小步數的節點
                            for (i = 0; i < 8; i++) {
                                     if (temp.neighbor[i] == 0) {
                                               int t = next_neighbor(temp.x + dirx[i], temp.y + diry[i]);
                                               if (t <= min ) {
                                                        min = t;
                                                        flag = i;
                                               }
                                     }
                       }
                            // 找到最小步數的鄰居節點
                            if (flag != -1) {
                                     temp.neighbor[flag] = 1; // 在棧頂結點中將該節點設置爲已訪問  
                                     // 初始化下一步節點的值,壓棧
                                     Node newnode;
                                     newnode.x = temp.x + dirx[flag];
                                     newnode.y = temp.y + diry[flag];
                                     newnode.num = xy_to_num(newnode.x, newnode.y);
                           for (i = 0; i < 8; i++) {
                                   if(canmove(newnode.x + dirx[i], newnode.y + diry[i]))
                                          newnode.neighbor[i] = 0;
                                   else
                                          newnode.neighbor[i] = 1;                            
                          }
                                     t_route.push(newnode);
                            }
                            // 棧頂節點沒有能夠行走的下一位置,出棧,設爲未訪問
                            else  {
                                     t_route.pop();
                                     board[temp.x][temp.y] = 0;      
                                     step --; //路徑數組下標值同步減1
                            }
                   }      
                   // output
                   if (step == 64) {
                            for( i = 0; i < 63; i++)
                                     printf("%d ", seq[i]);
                        printf("%d\n", seq[i]);
                   }      
                   else
                            printf("-1\n");     
           } // end of if (判斷起始位置是否合法)
            else
                      printf("-1\n");
    }
         return 0;
}
 
int xy_to_num(int x, int y) {
         return x * 8 + y + 1;
}
bool canmove(int x, int y) {
         if(x >= 0 && x <= 7 && y >= 0 && y <= 7 && board[x][y] == 0)
                   return true;
         return false;
}
int next_neighbor(int x, int y) {
         int i, num;
         for (i = 0, num = 0; i < 8; i++)
                   if(canmove(x + dirx[i], y + diry[i]))
                            num++;
         return num;
}


  1. 6、  測試數據(5-10組有梯度的測試數據,要考慮邊界條件)

1、考慮出發位置合法,位於角落位置時。

2、 考慮出發位置合法,位於邊界位置時。

 

3、 考慮出發位置合法,位於中間位置時。

   

 

4、出發位置不合法:

5、調試,查看過程當中的回溯信息,在回溯的代碼中增長輸出:

else  {

                                     t_route.pop();

                                     board[temp.x][temp.y] = 0;      

                                     step --; //路徑數組下標值同步減1

                                     cout << "back "<<endl;

                          }

從 1-64 逐個調試,發現只有21有回溯信息:


7、  對時間複雜度,空間複雜度方面的分析、估算及程序優化的分析和改進.

時間複雜度:

 深度優先非遞歸實現的話,同樣有最好和最壞的狀況。對於n*n的棋盤。該題其實是一顆n叉樹。最好狀況就是一次性就找到。每一步有8次循環尋找下一步節點,一共找n*n-1次。因此時間複雜度爲On2)。最壞的話,不太好分析,由於很差從數學上證實一個明確的發生回溯的個數,大概就是每層都要回溯(不過實際問題中確定沒有每層都回溯),相似於滿8叉數的深度優先遍歷,則爲O8n*n)。

 

  代碼在sicily上耗時是0 s,仍是比較理想,可是若是棋盤更大一點就不必定了,網上資料說若是隻用一次剪枝,那麼20*20的棋盤就比較慢了。

空間複雜度:

  這個空間複雜度就是樹的深度,每一個結點使用空間爲常數,則爲O(n*n)。

程序優化分析改進:

  1、一開始想用遞歸,但是一是由於本身不太明白,想不通遞歸過程,二是以爲遞歸不太好控制,因而用了非遞歸。

   2、剪枝方案只使用了最簡單的一種,就是選擇下一步可行位置最少的(Warnsdorff's rule。在網上查閱了一些資料,發現還有一些剪枝方法:

   1)使用Warnsdorff's rule剪枝後,若是能夠優先選擇的下一個位置不止一個,則優先選擇離中心位置較遠的位置做爲下一步(即靠近邊邊的位置)。

通俗點理解,第一點的剪枝就是走那些位置可能走到機會比較小的,反正走到的機會小,那麼先走完老是好的,要否則你兜一圈回來,仍是要走這一個位置的。

第二點的剪枝就是走法儘可能從邊邊走,而後是往中間靠。

2)第三點的剪枝,每次都從棋盤的中間點開始出發,而後求出一條合法路徑後再平移映射回待求路徑。

怎麼理解呢?所謂馬周遊棋盤,最後還要回到起點。也就是在棋盤中找到一條哈密頓迴路。那麼無論你是從哪裏開始的,最後都是會在這個哈密頓迴路中的,那麼選取的中點的位置也確定是在這個迴路上的。

最後,找到這個這個以中點爲起點的哈密頓迴路後,根據設定起點在這個迴路中的序號,映射回以這個位置爲起點的馬周遊路線便可。

  3、有一些變量上的細節問題。好比board數組由於只需用到兩個值(0,1),因此能夠爲bool型,更節省空間。還有就是存儲鄰居節點的時候的數組也能夠用bool型。

參考資料:

http://www.haogongju.net/art/2430132

http://huzhihang1103.blog.163.com/blog/static/19779176920135221340255/

https://class.coursera.org/ml/lecture/2

http://bbs.csdn.net/topics/370229313

相關文章
相關標籤/搜索