将马放到国际象棋的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 ", 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("不存在 "); 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