馬踏棋盤演算法(騎士周遊問題)----遞迴與貪心優化演算法

B發表於2021-04-10

問題描述

所謂“馬踏棋盤”問題,就是指在中國象棋的棋盤上,用馬的走法走遍整個棋盤,在8*8的方格中,每個格都要遍歷,且只能遍歷一次。

問題解析

從起始點開始,根據“馬”的走法,它的下一步的可選擇數是有0—8個的。

我們知道,當下一步的可選擇數為0的時候,進行回溯。當下一步的可選擇數有1個的時候,我們直接取那一步就行了。但是如果下一步的可選擇數有多個的時候呢? (思路取自九茶dalao

但是我們選擇下一步的時候(假設有a、b、c、d四個點可以選擇),怎樣選才算是最優呢?

答案是:哪一個點的下一步少,就選哪一個。

我們選擇a、b、c、d之中的某一個點作為下一步,選哪個比較好,就看哪個點的後續下一步比較少。例如:馬走到a點後的下一步有3個選擇;而b的下一步有2個;c有1個,d有2個。那麼我們的最優選擇是:c點!

為什麼要這樣選呢?網上的解釋是:“選擇最難走的路,才能走的遠”嗚。。。好像太抽象了。

我的理解是:有些選擇的後續下一步很少,例如c點,如果不先遍歷它的話以後可能會很難遍歷到它。

甚至極端一點的情況是,如果現在不遍歷它,以後都遍歷不到了。遍歷不成功的時候只能回溯,一直回溯到此刻的點,然後選了c點以後才能完成,這就浪費了大量的時間。

程式碼實現(以前的遞迴程式碼和現在的貪心演算法)

遞迴;

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

#define X 5 //定義棋盤。為測試方便,用5格棋盤。8格棋盤的時間複雜度,真的傷不起啊……期待更好的演算法
#define Y 5

void print_chess();
int next(int *x, int *y, int step);
int traverse(int x, int y, int count);
int traverse_chess(int x, int y, int tag);

int chess[X][Y]; //棋盤

int main() {
    clock_t start, end; //記錄一下程式耗時
    int i, j;
    //初始化棋盤
    for (i = 0; i < X; i++) {
        for (j = 0; j < Y; j++) {
            chess[i][j] = 0;
        }
    }
    start = clock();

    //方法一
    chess[2][0] = 1;
    int result  = traverse(2, 0, 2);

    //方法二
    //int result=traverse_chess(2,0,1); //也可以使用這個方法

    end = clock();
    if (1 == result) {
        printf("ok\n");
        print_chess();
        printf("共耗時:%f\n", (double)(end - start) / CLOCKS_PER_SEC);
    } else {
        printf("此路不通,馬兒無法踏遍所有棋格!\n");
    }
    return 0;
}

/*
判斷下一個結點位置是否可用
當前結點位置(x,y)
step:下一個結點位置編號
*/
int next(int *x, int *y, int step) {
    // printf("%d\n",step);
    switch (step) {
    case 0:
        if (*y + 2 <= Y - 1 && *x - 1 >= 0 && chess[*x - 1][*y + 2] == 0) {
            *y += 2;
            *x -= 1;
            return 1;
        }
        break;
    case 1:
        if (*y + 2 <= Y - 1 && *x + 1 <= X - 1 && chess[*x + 1][*y + 2] == 0) {
            *y += 2;
            *x += 1;
            return 1;
        }
        break;
    case 2:
        if (*y + 1 <= Y - 1 && *x + 2 <= X - 1 && chess[*x + 2][*y + 1] == 0) {
            *y += 1;
            *x += 2;
            return 1;
        }
        break;
    case 3:
        if (*y - 1 >= 0 && *x + 2 <= X - 1 && chess[*x + 2][*y - 1] == 0) {
            *y -= 1;
            *x += 2;
            return 1;
        }
        break;
    case 4:
        if (*y - 2 >= 0 && *x + 1 <= X - 1 && chess[*x + 1][*y - 2] == 0) {
            *y -= 2;
            *x += 1;
            return 1;
        }
        break;
    case 5:
        if (*y - 2 >= 0 && *x - 1 >= 0 && chess[*x - 1][*y - 2] == 0) {
            *y -= 2;
            *x -= 1;
            return 1;
        }
        break;
    case 6:
        if (*y - 1 >= 0 && *x - 2 >= 0 && chess[*x - 2][*y - 1] == 0) {
            *y -= 1;
            *x -= 2;
            return 1;
        }
        break;
    case 7:
        if (*y + 1 <= Y - 1 && *x - 2 >= 0 && chess[*x - 2][*y + 1] == 0) {
            *y += 1;
            *x -= 2;
            return 1;
        }
        break;
    default:
        break;
    }
    return 0;
}

/*
遍歷整個棋盤-方法一
(x,y)為座標位置
count為遍歷次數
*/
int traverse(int x, int y, int count) {
    int x1 = x, y1 = y; //新節點位置
    if (count > X * Y)  //已全部遍歷且可用,則返回。
        return 1;
    int flag, result, i;
    for (i = 0; i < 8; i++) {
        flag = next(&x1, &y1, i); //尋找下一個可用位置
        if (1 == flag) {
            chess[x1][y1] = count;                       //新找到的結點標識可用,
            result        = traverse(x1, y1, count + 1); //以新節點為根據,再次遞迴下一個可用結點
            if (result)                                  //當前棋盤已全部可用
            {
                return 1;
            } else //新找到的結點無下一個可用位置,進行回溯
            {
                chess[x1][y1] = 0;
                x1            = x; //結點位置也要回溯
                y1            = y;
            }
        }
    }
    return 0;
}

/*
遍歷整個棋盤-方法二
(x,y)為座標位置
tag為遍歷次數
*/
int traverse_chess(int x, int y, int tag) {
    int x1 = x, y1 = y, flag = 0, count = 0;
    chess[x][y] = tag;
    if (X * Y == tag) {
        return 1;
    }
    flag = next(&x1, &y1, count);
    while (0 == flag && count <= 7) {
        count++;
        flag = next(&x1, &y1, count);
    }
    while (flag) {
        if (traverse_chess(x1, y1, tag + 1)) //如果全部遍歷完畢,則返回。
        {
            return 1;
        }
        //沒有找到下一個可用結點,則回溯
        x1 = x;
        y1 = y;
        count++;
        flag = next(&x1, &y1, count);
        while (0 == flag && count <= 7) {
            count++;
            flag = next(&x1, &y1, count);
        }
    }
    if (flag == 0) {
        chess[x][y] = 0;
    }
    return 0;
}

/*
列印棋盤
*/
void print_chess() {
    int i, j;
    for (i = 0; i < X; i++) {
        for (j = 0; j < Y; j++) {
            printf("%d\t", chess[i][j]);
        }
        printf("\n");
    }
}

貪心優化演算法:

#include <bits/stdc++.h>
using namespace std;

#define H 3
int next_[8][2] = {{-2, 1}, {-1, 2}, {1, 2}, {2, 1}, {2, -1}, {1, -2}, {-1, -2}, {-2, -1}};
int f[8]        = {-15, -6, 10, 17, 15, 6, -10, -17}; //把8×8的棋盤轉成一維陣列,馬走法的八個方向分別是下標-15,-6,10,17,15,6,-10,-17
int dep         = 1;                                  //深度
int count_, z, zz;                                    // count_ 表示目標要多少種解法,而 z 記錄當前算出了多少種解法,zz 記錄在運算中回溯的次數
int print[10001][8][8], F[8], a[64];                  //print[][][] 記錄所有的遍歷路徑,a[] 用一維陣列記錄 8*8 棋盤中馬的遍歷路徑

int Prepare() {
    int i, j, n;
    printf("請輸入起始點的座標:\n");
    cin >> i >> j >> count_;
    n    = i * 8 + j - 9;
    a[n] = 1;
    return n;
}

// Sortint() 函式對點 n 的下一步進行“後續下一步可選擇數”的排序,結果儲存在 b[][] 裡面
// c 表示前驅結點在結點 n 的哪個位置。
void Sorting(int b[64][H], int n, int c) {
    int i, j, x, y, m1, m2, k, k1, l = 1, xx, yy;
    if (c != -1)
        c = (c + 8 - 4) % 8;
    for (i = 0; i < 8; i++) //對於當前節點的八個方向
    {
        F[i] = -1; //F記錄八個方向的下一步的再下一步有多少個
        m1   = n + f[i];
        x    = n / 8 + next_[i][0];
        y    = n % 8 + next_[i][1];                                     //這是下一步的座標
        if (c != i && x >= 0 && x < 8 && y >= 0 && y < 8 && a[m1] == 0) //如果下一步存在
        {
            F[i]++;
            for (j = 0; j < 8; j++) //對於下一步的八個方向
            {
                m2 = m1 + f[j];
                xx = x + next_[j][0];
                yy = y + next_[j][1];                                     //這是再下一步的座標
                if (xx >= 0 && xx < 8 && yy >= 0 && yy < 8 && a[m2] == 0) //如果再下一步存在
                    F[i]++;
            }
        }
    }
    b[n][0] = -1;
    for (i = 1; i < H; i++) {
        k = 9;
        for (j = 0; j < 8; j++) {
            if (F[j] > -1 && F[j] < k) {
                k  = F[j];
                k1 = j;
            }
        }
        if (k < 9) {
            b[n][l++] = k1;
            F[k1]     = -1;
            b[n][0]   = 1;
        } else {
            b[n][l++] = -1;
            break;
        }
    }
}

// 搜尋遍歷路徑
void Running(int n) {
    int i, j, k;
    int b[64][H], s[64]; // b[][] 用來存放下一步的所有後續結點排序
    s[0] = n;
    Sorting(b, n, -1);
    while (dep >= 0) {
        if (b[n][0] != -1 && b[n][0] < H && b[n][b[n][0]] != -1) {
            k = b[n][b[n][0]];
            b[n][0]++;
            n += f[k];
            Sorting(b, n, k);
            a[n]       = ++dep;
            s[dep - 1] = n;
            if (dep == 64) {
                for (i = 0; i < 8; i++)
                    for (j = 0; j < 8; j++)
                        print[z][i][j] = a[i * 8 + j];
                z++;
                if (z == count_) {
                    printf("\n完成!!\n");
                    printf("回溯的次數:%d\n", zz);
                    break;
                }
            }
        } else {
            dep--;
            zz++;
            a[n] = 0;
            n    = s[dep - 1];
        }
    }
}

// 輸出所有的遍歷路徑
void Print_() {
    int i, j, k;
    printf("\n\n輸入'1'展示詳細遍歷,輸入'0'退出程式:");
    scanf("%d", &count_);
    if (count_) {
        for (i = 0; i < z; i++) {
            printf("第%d個解:\n", i + 1);
            for (j = 0; j < 8; j++) {
                for (k = 0; k < 8; k++)
                    printf("%3d", print[i][j][k]);
                printf("\n");
            }
        }
    }
}

int main() {
    int n;
    double start, finish;
    n     = Prepare();
    start = clock();
    Running(n);
    finish = clock();
    printf("執行時間:%.3f秒\n", (finish - start) / CLOCKS_PER_SEC);
    Print_();
    return 0;
}

書寫程式碼以後的一些感想

利用貪心以後的DFS以後程式碼效率明顯提高了很多,但在深度搜尋時print陣列裡的各種路徑解法卻是顯示一樣的,可能是在貪心演算法中仍可能會捨棄一些解法路徑導致在陣列中總是儲存同一種

相關文章