1. 程式人生 > >馬踏棋盤演算法(騎士周遊問題)

馬踏棋盤演算法(騎士周遊問題)

一、馬踏棋盤演算法

1、國際象棋的棋盤為8*8的方格棋盤,將“馬”放在任意指定的方格中,按照“馬”走棋的規則將“馬”進行移動。要求每個方格只能進入一次,最終使得“馬”走遍棋盤64個方格。

2、關於國際象棋“馬”的走法:以“日”字為單位往斜對角上的那個方格走。如下圖所示馬在當前位置就有8種走法,如果不考慮邊緣的特殊情況,那麼需要8^64的時間複雜度才可以把整個棋盤的一個正確路徑找出來。


3、對於在n*n的棋盤上,當n>=5且為偶數的時候,以任意點作點都有解。

4、實現程式碼如下:

/************************************************************/
/**               馬踏棋盤演算法(騎士周遊問題)                  **/
/************************************************************/

#include <stdio.h>
#include <time.h>

#define X 8
#define Y 8

int chess[X][Y];

/**
 * 列印棋盤,棋盤中每個格子的數值就是遍歷的次序
 */
void Print()
{
    int i, j;
    for(i = 0; i < X; i++)
    {
        for(j = 0; j < Y; j++)
        {
            printf("%2d\t", chess[i][j]);
        }
        printf("\n");
    }
    printf("\n");
}

/**
 * 找到基於馬當前在棋盤中的位置的(x,y)座標的下一個可走的位置的座標,如果成功找到則返回1,且直接修改原來位置的座標;
 * 否則返回0
 * 成功找到的條件是該位置存在且還沒有被馬走過
 * @param x:當前馬所在棋盤位置的x座標
 * @param y:當前馬所在棋盤位置的y座標
 * @param count:不考慮邊緣的情況,馬在任意一位置的下一個位置都可能有八種情況,count就是對這八種情況進行判斷
                 只要找到其中一種就返回1
 */
int NextXY(int *x, int *y, int count)
{
    switch(count)
    {
    case 1:
        if(*x + 2 <= X - 1 && *y - 1 >= 0 && chess[*x + 2][*y - 1] == 0)
        {
            *x += 2;
            *y -= 1;
            return 1;
        }
        break;
    case 2:
        if(*x + 2 <= X - 1 && *y + 1 <= Y - 1 && chess[*x + 2][*y + 1] == 0)
        {
            *x += 2;
            *y += 1;
            return 1;
        }
        break;
    case 3:
        if(*x + 1 <= X - 1 && *y - 2 >= 0 && chess[*x + 1][*y - 2] == 0)
        {
            *x += 1;
            *y -= 2;
            return 1;
        }
        break;
    case 4:
        if(*x + 1 <= X - 1 && *y + 2 <= Y - 1 && chess[*x + 1][*y + 2] == 0)
        {
            *x += 1;
            *y += 2;
            return 1;
        }
        break;
    case 5:
        if(*x - 2 >= 0 && *y - 1 >= 0 && chess[*x - 2][*y - 1] == 0)
        {
            *x -= 2;
            *y -= 1;
            return 1;
        }
        break;
    case 6:
        if(*x - 2 >= 0 && *y + 1 <= Y - 1 && chess[*x - 2][*y + 1] == 0)
        {
            *x -= 2;
            *y += 1;
            return 1;
        }
        break;
    case 7:
        if(*x - 1 >= 0 && *y - 2 >= 0 && chess[*x - 1][*y - 2] == 0)
        {
            *x -= 1;
            *y -= 2;
            return 1;
        }
        break;
    case 8:
        if(*x - 1 >= 0 && *y + 2 <= Y - 1 && chess[*x - 1][*y + 2] == 0)
        {
            *x -= 1;
            *y += 2;
            return 1;
        }
        break;
    default:
        break;
    }
    return 0;
}

/**
 * 深度優先遍歷棋盤
 * @param x:當前馬即將要走的位置的x座標
 * @param y:當前馬即將要走的位置的y座標
 * @param step:當前馬即將要走的這個位置是第step步,也就是說馬已經走過了step-1個棋盤
 */
int TraversalChessBoard(int x, int y, int step)
{
    // 定義x1和y1變數儲存馬即將要走的這個位置的座標
    int x1 = x, y1 = y;
    // flag儲存是否有下一個可走的位置;count用於迴圈判斷八種可走的位置
    int flag = 0, count = 1;

    // 標記該位置已經走過了
    chess[x][y] = step;
    // 如果此時step等於棋盤格子的個數,則說明已經把棋盤的每一個格子都走過一次了
    // 則列印棋盤輸出走過的順序;這也是下面的遞迴的返回條件吧
    if(step == X * Y)
    {
        Print();
        return 1;
    }

    // 如果還沒有走完棋盤,則選擇下一個位置,依次判斷那八種可能的情況
    flag = NextXY(&x1, &y1, count);
    while(flag == 0 && count < 8)
    {
        count++;
        flag = NextXY(&x1, &y1, count);
    }

    // 在當前位置找到下一個可走的位置
    while(flag)
    {
        // 遞迴呼叫走向下一個位置,因為在NextXY函式中直接修改了當前位置的座標,所以此時的x1和y1就表示下一個可走位置的座標
        // 如果此時返回的是1,說明棋盤已經走完了,繼續向上返回1
        if(TraversalChessBoard(x1, y1, step + 1))
        {
            return 1;
        }
        // 返回的不是1,則返回上一個位置,在上一個位置重新選擇一個可走位置繼續
        x1 = x;
        y1 = y;
        // 前count種可走位置的情況都判斷過了,從count+1種情況繼續判斷
        count++;
        flag = NextXY(&x1, &y1, count);
        while(flag == 0 && count < 8)
        {
            count++;
            flag = NextXY(&x1, &y1, count);
        }
    }

    // 八種可能的情況都判斷過了,還是沒有下一個可走的位置了
    // 那麼說明該次深度遍歷失敗,此種走法不行,則應該從當前位置回退到上一個位置
    // 所以重新標記當前位置還沒有被走過
    if(0 == flag)
    {
        chess[x][y] = 0;
    }

    // 如果都沒有可走的位置了,則返回0,代表繼續向後回退
    return 0;
}

int main()
{
    int i, j;
    clock_t start, finish;
    start = clock();

    for(i = 0; i < X; i++)
    {
        for(j = 0; j < Y; j++)
        {
            chess[i][j] = 0;   // 初始化棋盤
        }
    }

    // 從座標(2,0)開始走
    if(!TraversalChessBoard(2, 0, 1))
    {
        printf("遍歷棋盤失敗......\n");
    }
    finish = clock();
    printf("本次遍歷共耗時:%f秒\n", (double)(finish - start)/CLOCKS_PER_SEC);
    return 0;
}
5、從實現程式碼上來看,該演算法運用到了遞迴法、回溯法以及圖的深度遍歷思想,不斷的遞迴判斷,失敗再回退,直到成功遍歷或者失敗為止。不過實現方式還有很大的可優化空間。執行結果如下,需要化很長一段時間來執行,不過起始點的選擇很重要,有些時候選擇了其他的起始點,則有可能很久很久都計算不出來。


6、其他相關概念

哈密爾頓路徑:圖G中的哈密爾頓路徑指的是經過圖G中每個頂點,且只經過一次的一條軌跡。如果這條軌跡是一條閉合的路徑(從起點出發不重複地遍歷所有點後仍能回到起始點),那麼這條路徑稱為哈密爾頓迴路。沒有閉合的叫通路或路徑。