將馬放到國際象棋的8*8棋盤上的任意指定方格中,按照“馬”的走棋規則將“馬”進行移動,要求每個方格進入且只進入一次,走遍棋盤上的64個方格,將數字1,2,3…,64依次填入一個8*8的方陣。馬在國際象棋中的走法如右圖所示。
涉及的計算思維
解決這個問題可以利用到計算機中的兩種方法,一種是深度優先搜索,也就是回溯法,體現了計算思維的遞歸思想。另一種是利用貪心法進行再優化,總是選擇最優者,體現了計算思維的“規划”思想。
解決方案
方案一 —— 深度優先搜索法
我們可以采用深度優先法求解,深度優先搜索是指對每一個可能的分支路徑深入到不能再深入為止,而且每個節點只能訪問一次。如圖1所示,當馬在當前位置時(節點1),將它下一跳的所有位置看作分支結點,然后選擇一個分支結點進行移動,如節點2,然后再走該結點的分支結點,如節點3,如果節點3不能再走下去,則退回到節點2,再選擇另一種走法,如節點4,一直走下去,直至不能派生出其他的分支結點,也就是“馬”走不通了。此時則需要返回上一層結點,順着該結點的下一條路徑進行深度優先搜索下去,直至馬把棋盤走遍。
方案二 —— 貪心法
貪心法是指,在對問題求解時總是做出在當前看來是最好的選擇。也就是說,不從整體最優上加以考慮,該方法所做出的僅是在某種意義上的局部最優解。我們在回溯法的基礎上,用貪心法進行優化,在每個結點對其子結點進行選取時,優先選擇“出口”最少的進行搜索,“出口”的意思是在這些子結點中它們的可行子結點的個數,也就是“孫子”結點越少的越優先跳,因為這樣選擇時出口少的結點會越來越少,這樣跳成功的機會就更大一些。(傳說中的“先苦后甜”??)實際體現就是,先沿着周邊跳,逐漸向中間靠攏。
值得注意的是,這種啟發式算法在馬踏棋盤這種特殊的求哈密頓路徑問題中有極好的效果,並稱作Warnsdorff法則。雖然求哈密頓路徑是一個NP問題,但是對於較小規模的棋盤,Warnsdorff法則能夠在大多數情況下與線性時間內求出一個解。
如下圖,可以先選擇3、4、5、6這幾個“出口”少的先跳,跳完一步再選擇“出口”少的往下跳,如沒有可跳出則回溯上一結點。
實現
使用貪心法,當然,基本框架是回溯。
#include<cstdio> #include<iostream> #include<cstring> #include<vector> #include<map> #include<algorithm> using namespace std; typedef pair<int, int> P; const int maxn = 100 + 10; //棋盤最大的規模 const int dx[8] = { 1,2,2,1,-1,-2,-2,-1 }; const int dy[8] = { 2,1,-1,-2,-2,-1,1,2 }; //移動方位 int vis[maxn][maxn]; int N, sx, sy; //規模,起點坐標 P ans[maxn * maxn + 10]; int cnt; vector<P>bad_points; vector<P>directs; bool judge(int x, int y) { if (x >= 0 && x < N && y >= 0 && y < N && !vis[x][y]) return true; return false; } bool cmp(P a1, P a2) { return a1.second < a2.second; //根據出口排序 } vector<P> finddirec(int x, int y) { vector<P>direc; for (int i = 0; i < 8; i++) { int xx = x + dx[i], yy = y + dy[i]; //if (xx == x && yy == y) continue; int count = 0; if (judge(xx, yy)) { for (int j = 0; j < 8; j++) { int xxx = xx + dx[j], yyy = yy + dy[j]; if (judge(xxx, yyy)) count++; } } direc.push_back(P(i, count)); } sort(direc.begin(), direc.end(), cmp); return direc; } bool dfs(int x, int y) { if (cnt >= N * N) { for (int i = 0; i < cnt; i++) printf("第%d步:%d %d\n", i, ans[i].first, ans[i].second); return true; } vector<P>directs = finddirec(x, y); int len = directs.size(); for (int i = 0; i < len; i++) { int xx = x + dx[directs[i].first], yy = y + dy[directs[i].first]; if (judge(xx, yy)) { vis[xx][yy] = cnt; ans[cnt++] = P(xx, yy); if (dfs(xx, yy)) return true; //有一個可行解就返回 cnt--; vis[xx][yy] = false; } } return false; } int main() { scanf("%d%d%d", &N, &sx, &sy); vis[sx][sy] = true; ans[cnt++] = P(sx, sy); if (!dfs(sx, sy)) printf("不存在\n"); return 0; }
注:馬踏棋盤問題是一個精確覆蓋問題,為快速實現其回溯和啟發式搜索,可以采用Dancing Links數據結構來實現X算法,具體實現可參考下面的鏈接。
參考鏈接:
1、Academia——馬踏棋盤 https://www.academia.edu/29874114/馬踏棋盤
2、計算思維百科——馬踏棋盤問題 https://wiki.jsswsq.com/index.php?title=馬踏棋盤問題
3、CSDN——馬踏棋盤問題優化 https://blog.csdn.net/steph_curry/article/details/78937296