一、题目
在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也在一秒内就可以完成