一、題目
在n x n棋盤(有n x n個格點的棋盤)的某個格點上有一個中國象棋馬,馬走日字。
求一條周游棋盤的路徑,使得馬能夠從起始位置起沿着該路徑每個格點恰好走一次最后回到出發位置。
二、思路
1、初期思路:
首先想到的是用DFS來解決,不僅可以遍歷全局還可以回溯,於是着手做了起來,雖然是DFS,但是在此題中,不需要用到鄰接矩陣,也不需要數組來判斷每點是否到過,一開始的設想是利用二維數組當成棋盤,默認為全0,初始點是1,第二步就是2,這樣一直走下去,直到走滿,每次需要判斷八個方向,如果該往該方向走后仍在棋盤中,就接着判斷下一步的方向,直到八個方向都走過了或棋盤已經走滿,就返回上一步棋。
2、回溯的方法:
程序使用遞歸思想,每一步都會創建一個優先隊列,在未完成算法的前提下,只要優先隊列不為空,就會一直執行下去。
3、遇到問題:
但在實際解決過程中,每次遞歸需要傳入當前棋盤數組,但返回之前步數時數組老是會被更改,所以就嘗試了用字典來保存每步棋,奇怪的是每次操作都會改變所有字典中的值,使得實驗出錯,改用三維數組后也遇到了同樣的問題。
淺拷貝和深拷貝問題:
這兩種復制和clone函數都出現了相同的問題,在百度后了解到這涉及了淺拷貝和深拷貝,淺拷貝過程中只是引用了數組的地址(實際指向同一個空間),深拷貝才是真正開辟了新的空間
解決方法:為了避免麻煩就直接自己用兩個for循環來復制數組,運行,得到正常的結果。
還有一個是用java自帶函數時,上圖最后一條復制語句會對所有二維數組賦值而不是當前二維數組,在室友電腦上試過后也是一樣。(未解決)
三、剪枝
有了之前的思路,雖然能夠解決問題,但還是遠遠不夠的,在棋盤為8*8時幾乎跑不出答案,我們需要思考剪枝方案使代碼更快運行。
由於涉及到特定問題的剪枝需要對問題做細致研究,所以就直接百度了剪枝的方案如下:
根據剪枝方案來改造自己的代碼:
1、 為了計算每個位置的可走方向數,我們需要添加一個方法來判斷某個位置有幾種走法,只需要對當前點進行八個方向的遍歷即可,難點是優先選擇可走步數少的點,一開始我每次都走最優點,但發現這樣無法走到終點,且無法再回溯,所以我們是需要將每一步的所有可走點的可走步數保存起來的,這樣在走錯時才可能進行回溯,這里用到了優先隊列這個結構。
使用優先隊列免去了自己去寫創建數組和需要的排序算法,我們只需要給優先隊列制定排列規則即可,而無需搞懂其內部的具體實現。這帶來了很大的便利。
2、 添加一個方法來計算某個位置到中心的距離,然后在優先隊列中加入距離的比較即可實現剪枝2。
3、 最終實現的動態圖
四、復雜度: O(n+8^n)
以DFS為主要算法,時間復雜度(V次遍歷+ E次遞歸)
假設每個頂點都有八種走法,遞歸次數最多是8^N(N*N棋盤中)
有時每種情況下的時間相差很大,存在一定的特殊性(剪枝不一定是完美的)
五、實現代碼
1 public class Horse {
2 static int n;// n*n棋盤
3 static int FP[][] = { { 1, 2 }, { 1, -2 }, { -1, 2 }, { -1, -2 }, { 2, 1 }, { 2, -1 }, { -2, 1 }, { -2, -1 } };// 可能走的八個方向
4 static int x0, y0;// 起始點
5 static int[][][] group; 6 7 class node {// 當前點,坐標及可走方向數量 8 int x; 9 int y; 10 int hp;//可走方向數 11 int dc;//距離中心距離 12 13 public node(int i, int j, int k,int d) { 14 this.x = i; 15 this.y = j; 16 this.hp = k; 17 this.dc=d; 18 } 19 } 20 21 public static Comparator<node> idComparator = new Comparator<node>() {//優先隊列的比較方法(小到大 遠到近 22 @Override 23 public int compare(node n1, node n2) { 24 if(n1.hp != n2.hp) { 25 return (int) (n1.hp - n2.hp); 26 }else { 27 return n2.dc-n1.dc; 28 } 29 } 30 }; 31 32 public void init() { 33 Scanner sc = new Scanner(System.in); 34 System.out.println("int n:"); 35 n = sc.nextInt(); 36 group = new int[n * n + 1][n][n]; 37 System.out.println("請輸入起始點:"); 38 x0 = sc.nextInt(); 39 y0 = sc.nextInt(); 40 DFS(x0, y0, 1); 41 } 42 43 public void DFS(int x, int y, int now_pace) {// 進行馬的深度遍歷 44 if (check(x, y, now_pace)) { 45 return; 46 } 47 copy(group[now_pace], group[now_pace - 1]); 48 group[now_pace][x][y] = now_pace + 1; 49 now_pace++; 50 System.out.println(now_pace); 51 ps(group[now_pace - 1]); 52 System.out.println(); 53 54 if ((now_pace == n * n + 1 && ((Math.pow(x - x0, 2) + Math.pow(y - y0, 2) == 5)))) {// 判斷是否走滿且能回到原點 55 System.out.println("okkkkk"); 56 System.exit(0); 57 return; 58 } 59 60 Queue<node> nodePriorityQueue = new PriorityQueue<>(8, idComparator);// 每次來個優先隊列從小到大 61 for (int[] p : FP) {//遍歷八個方向放入優先隊列 62 //int nphs = nextPosHasSteps(x + p[0], y + p[1], now_pace); 63 nodePriorityQueue.add(new node(x + p[0], y + p[1], nextPosHasSteps(x + p[0], y + p[1], now_pace),disFromCenter(x, y))); 64 } 65 while (!nodePriorityQueue.isEmpty()) {//回溯 66 node n = nodePriorityQueue.poll(); 67 DFS(n.x, n.y, now_pace); 68 } 69 } 70 71 public boolean check(int x, int y, int now_pace) {// 判斷是否在棋盤內或已經到過 72 return x < 0 || x >= n || y < 0 || y >= n || group[now_pace - 1][x][y] != 0; 73 } 74 75 public int nextPosHasSteps(int x, int y, int now_pace) {// 計算當前位置可走的方向 76 int steps = 0; 77 for (int[] p : FP) {// 遍歷八個方向進行判斷 78 if (!check(x + p[0], y + p[1], now_pace)) { 79 steps++; 80 } 81 } 82 return steps; 83 } 84 85 public int disFromCenter(int x,int y) {//距離中心距離 86 return (int) (Math.pow(x-n/2, 2)+Math.pow(y-n/2, 2)); 87 } 88 89 public void ps(int[][] s) {//打印數組 90 for (int i = 0; i < s.length; i++) { 91 for (int j = 0; j < s.length; j++) { 92 System.out.print(s[i][j] + " "); 93 } 94 System.out.println(); 95 } 96 } 97 98 public void copy(int[][] a, int[][] b) { 99 for (int i = 0; i < a.length; i++) { 100 for (int j = 0; j < a.length; j++) { 101 a[i][j] = b[i][j]; 102 } 103 } 104 } 105 106 public static void main(String[] args) { 107 Horse h = new Horse(); 108 h.init(); 109 } 110 }
六、總結
遞歸往往面臨着很大的數據量和復雜度,在完成基本實現后進行剪枝是很重要的
馬走日實驗中在完成剪枝1后發現程序速度大大提高了
原本8*8是算不出來的完成剪枝即使是20*20也在一秒內就可以完成