馬踏棋盤之貪心演算法優化

九茶發表於2014-11-28

問題描述:

曾經用簡單的深度優先搜尋方法、遞迴的形式對馬踏棋盤進行搜尋,執行效率不甚理想。(部落格見馬踏棋盤之遞迴實現)。

所以現在用貪心演算法將其優化了一下。


問題解析:

主要的思想沒有變,還是用深度優先搜尋,只是在選下一個結點的時候做了貪心演算法優化,其思路如下:

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

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

在上一篇部落格中,我們是任意取一個的,只要它在棋盤內,且未遍歷就可以了。

但其實我們怎麼選下一步,對搜尋的效率影響是非常大的!


先介紹一下“貪心演算法”。百科裡面的定義是:貪心演算法(又稱貪婪演算法),是指在對問題求解時,總是做出在當前看來是最好的選擇。也就是說,不從整體最優上加以考慮,他所做出的僅是在某種意義上的區域性最優解。它只考慮區域性的最優,從而讓總體也最優。就我們這個馬踏棋盤來說,我們每走一步都取最優的那個選擇,從而讓整體的演算法也最優。


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

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

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


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

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

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



下面放出所有程式碼,詳細的解釋後面再補上:

<pre name="code" class="cpp">#include<stdio.h>
#include<time.h>

#define H 3     // 代表對下一步的排序只取出最小的2個,而不是對8個都排序,這樣可以節省很多時間
  
int fx[8] = {-2,-1,1,2,2,1,-1,-2}, fy[8] = {1,2,2,1,-1,-2,-2,-1}, f[8] = {-15,-6,10,17,15,6,-10,-17};
						// fx[] 和 fy[] 表示馬在二維的八個方向,給二維座標x和y用;f[]表示一維的八個方向,給陣列a用。  
int dep = 1;            // dep 為遞迴的深度,代表在當前位置馬已經走了多少步  
int count, z = 0, zz  = 0;            // count 表示目標要多少種解法,而 z 記錄當前算出了多少種解法,zz 記錄在運算中回溯的次數  
int out[100001][8][8], F[8], a[64];            // out[][][] 記錄所有的遍歷路徑,a[] 用一維陣列記錄 8*8 棋盤中馬的遍歷路徑  


// 輸入起始座標,對存放遍歷路徑的陣列a進行初始化  
int Prepare()  
{  
    int i, j, n;  
    printf("請輸入起始點的座標:\n");  
    printf("x=");  
    scanf("%d", &i);  
    printf("\by=");  
    scanf("%d", &j);  
    printf("你要的解的數目count=");  
    scanf("%d", &count);  
    n = i * 8 + j - 9;                // 將起始點的二維座標 x、y 轉化成一維座標 n ,從而方便陣列 a[64] 的路徑記錄  
    for(i = 0; i<64; i++)        // a[64] 存放在 8*8 方格中馬的遍歷路徑,搜尋之前先進行清零初始化  
        a[i] = 0;  
    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 + fx[i];  
		y = n % 8 + fy[i];  //這是下一步的座標
        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 + fx[j];  
                yy = y + fy[j];  //這是再下一步的座標
                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++)  
                        out[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 Output()  
{  
    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", out[i][j][k]);  
                printf("\n");  
            }  
        }  
    }  
}  

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



執行結果如下(VC6.0、32位win8系統 + i7-3612QM處理器,如果使用VS時間會長一點):




測試了一下:十萬條路徑,耗時1秒多!!!效率比普通的深度優先搜尋不知道高了多少倍!




備註:

以上程式碼是四個月前寫的,現在才整理出來。打擊很大,當初不注意程式碼的格式和備註,現在看自己寫的程式碼看到頭暈腦炸,自食其果!以後必須注重寫程式碼的風格,注意寫上必須的備註,連自己寫的程式碼的看不懂,真真可笑。一直看了好久終於看明白各個函式 各個變數的作用了,也改了一些格式,寫了一些備註,到現在正的看不下去了。先把文章放出來,緩一陣再回來完善。



轉載請註明出處,謝謝!(原文連結:http://blog.csdn.net/bone_ace/article/details/41579957

相關文章