前言需求
今天我们学习的是马踏棋盘算法,我们还是从一个场景里引入看看
马踏棋盘算法也被称为骑士周游问题
将马随机放在国际象棋的6×6棋盘Board0~5的某个方格中
提示:马按走棋规则(马走日字)进行移动
要求:每个方格只进入一次,走遍棋盘上全部64个方格
小游戏体验网址:4399:马踏棋盘小游戏
一、马踏棋盘问题
马踏棋盘问题(骑士周游问题)实际上是:图的深度优先搜索(DFS)的应用
还记得图的深度优先搜索(DFS)吗?
有些模糊或者不记得小伙伴可以看往期文章:图(广度优先与深度优先)
那么按照我们的简单思路,是不是要一个位置一个位置去踩坑看看?
那么按照我们的深度优先搜索,就要一步步走下去,直至达成任务
当我们的所选第三步的位置,无法达成完成任务
那么我们需要回溯,将原第三步更换到下一个位置里去
在以新第三步开始,进行搜索,也要一步步走下去,直至达成任务
二、通过示例来认识算法
根据我们之前简单的思路,首先我们需要创建一个棋盘的数组
当我们做出选择下一步的时候,我们需要将当前的位置标记为已访问
,并根据当前位置计算出马儿能走那些位置,并放入到一个集合中里去
当然我们可以根据棋盘的情况来判断是否可以进行计算
注意::马儿不同的走法、会得到不同的结果,效率也会有影响(需优化)
规则判断是否可走
那么我怎么知道这些位置是否可走呢?我是怎么计算出来的呢?
首先我们先分析当前位置的x、y坐标,按照规则进行计算:(马走日字)
我们先分析一下象棋里的马走日是怎么样的吧
马走日所说的是马从提棋位置到落棋位置是一个“日”子的对角线
,在没有棋子踩住马脚时
,马是可以随意走哪个方向的日字
都是可以的
在有其他棋子在马的如图相关位置时,马就不能走该方向的日字
了,我们也熟称“踩马脚了”。注意无论踩马脚的棋子是己方的棋子还是敌方的棋子,被踩方向的日字都不能走了
如果四只马脚都被踩了,那么这只马哪里都走不了
了(如图)
在我们这个问题中,还请你看图关联看懂马儿怎么走的,即称马走日
当我们知道规则怎么玩了,就可以从图上看出来,每个点与当前点的关系
那么我们的马儿剩下的点与当前是什么关系呢?怎么走呢?
骑士周游算法思路
我们创建一个类存放棋盘行、列,并记录棋盘上的是否被访问过
- public class HorseChessboard {
- private static int x;//棋盘的列数
- private static int y;//棋盘的行数
- //创建一个数组,标记棋盘的各个位置是否被访问过
- private static boolean visited[];
- //使用一个属性,标记是否棋盘的所有位置都被访问
- private static boolean finished; // 如果为true,表示成功
- }
我们使用Point 类来表示 (x, y) 坐标空间中的位置的点
- public class Point extends Point2D implements java.io.Serializable {
-
- public int x;
-
- public int y;
-
- private static final long serialVersionUID = -5276940640259749850L;
-
- public Point() {
- this(0, 0);
- }
-
- public Point(Point p) {
- this(p.x, p.y);
- }
-
- public Point(int x, int y) {
- this.x = x;
- this.y = y;
- }
-
- //以双精度型返回点的 X 坐标。
- public double getX() {
- return x;
- }
-
- //以双精度型返回点的 Y 坐标。
- public double getY() {
- return y;
- }
-
- //返回此点的位置。
- @Transient
- public Point getLocation() {
- return new Point(x, y);
- }
- //将点的位置设为指定位置
- public void setLocation(Point p) {
- setLocation(p.x, p.y);
- }
- //将此点更改为具有指定位置
- public void setLocation(int x, int y) {
- move(x, y);
- }
-
- //将此点的位置设为指定的双精度坐标
- public void setLocation(double x, double y) {
- this.x = (int) Math.floor(x+0.5);
- this.y = (int) Math.floor(y+0.5);
- }
-
- //将此点移动到 (x,y) 坐标平面中的指定位置。
- public void move(int x, int y) {
- this.x = x;
- this.y = y;
- }
-
- //平移 (x,y) 位置的点,沿 x 轴平移 dx,沿 y 轴平移 dy,移动后得到点 (x+dx, y+dy)
- public void translate(int dx, int dy) {
- this.x += dx;
- this.y += dy;
- }
-
- //确定两个点是否相等。
- public boolean equals(Object obj) {
- if (obj instanceof Point) {
- Point pt = (Point)obj;
- return (x == pt.x) && (y == pt.y);
- }
- return super.equals(obj);
- }
- // 返回此点的字符串表示形式及其在 (x,y) 坐标空间中的位置
- public String toString() {
- return getClass().getName() + "[x=" + x + ",y=" + y + "]";
- }
- }
根据思路,需要根据当前位置判断马儿能走那些位置,并将结果放入ArrayList集合中
- public class HorseChessboard {
-
- //省略其他关键性代码....
-
- /**
- * 功能:根据当前位置(Point对象),计算马儿还能走哪些位置(Point),并放入到一个集合中(ArrayList),最多有8个位置
- * @param curPoint
- * @return
- */
- public static ArrayList<Point> next(Point curPoint){
- ArrayList<Point> ps = new ArrayList<>();
- //创建一个点
- Point p1 = new Point();
- //判断马儿是否能走5的位置
- if((p1.x = curPoint.x - 2) >=0 && (p1.y = curPoint.y+1) >=0 ){
- ps.add(new Point(p1));
- }
- return ps;
- }
- }
而其他点的位置与当前位置关系,我们之前也使用图解的方式分析,现在代码实现
- public class HorseChessboard {
-
- //省略其他关键性代码....
-
- /**
- * 功能:根据当前位置(Point对象),计算马儿还能走哪些位置(Point),并放入到一个集合中(ArrayList),最多有8个位置
- * @param curPoint
- * @return
- */
- public static ArrayList<Point> next(Point curPoint){
-
- ArrayList<Point> ps = new ArrayList<>();
-
- //创建一个点
- Point p1 = new Point();
-
- //表示马儿可以走5这个位置
- if((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y -1) >= 0) {
- ps.add(new Point(p1));
- }
- //判断马儿可以走6这个位置
- if((p1.x = curPoint.x - 1) >=0 && (p1.y=curPoint.y-2)>=0) {
- ps.add(new Point(p1));
- }
- //判断马儿可以走7这个位置
- if ((p1.x = curPoint.x + 1) < x && (p1.y = curPoint.y - 2) >= 0) {
- ps.add(new Point(p1));
- }
- //判断马儿可以走0这个位置
- if ((p1.x = curPoint.x + 2) < x && (p1.y = curPoint.y - 1) >= 0) {
- ps.add(new Point(p1));
- }
- //判断马儿可以走1这个位置
- if ((p1.x = curPoint.x + 2) < x && (p1.y = curPoint.y + 1) < y) {
- ps.add(new Point(p1));
- }
- //判断马儿可以走2这个位置
- if ((p1.x = curPoint.x + 1) < x && (p1.y = curPoint.y + 2) < y) {
- ps.add(new Point(p1));
- }
- //判断马儿可以走3这个位置
- if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y + 2) < y) {
- ps.add(new Point(p1));
- }
- //判断马儿可以走4这个位置
- if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y + 1) < y) {
- ps.add(new Point(p1));
- }
- return ps;
- }
- }
那么会不会有小伙伴有疑惑??
为什么走五那个位置就要>=0呢,走七的位置就要<x呢?<y又是什么一样?
我们先分析走五的位置的时候,为什么要>=0
同理,小于x,小于y代表我们只选择在棋盘内的点,超出的则不能走
骑士周游算法实践
往期我们使用的是二维数组代表这个点是否被访问过
但这里是36步都需要走一遍,那么我们其实可以使用一维数组进行表示
这样我们可以是用公式:马儿所在行 * 棋盘行 +马儿所在列 = 马儿下标 + 1
- public class HorseChessboard {
-
- //省略其他关键性代码....
-
-
- /**
- * 完成骑士周游问题的算法
- * @param chessboard 棋盘
- * @param row 马儿当前的位置的行 从0开始
- * @param column 马儿当前的位置的列 从0开始
- * @param step 是第几步 ,初始位置就是第1步
- */
- public static void traversalChessboard(int[][] chessboard, int row, int column, int step) {
-
- //标记当前棋盘执行的是第几步
- chessboard[row][column] = step;
-
- //row = 3 X = 6 column = 3 = 3 * 6 + 3 = 21 -1 = 20
- visited[row * x + column] = true; //标记该位置已经访问
-
- //获取当前位置可以走的下一个位置的集合
- ArrayList<Point> ps = next(new Point(column, row));
- }
- }
当我们获取到当前位置可以走的下一个位置的集合,就进行遍历递归
- public class HorseChessboard {
-
- //省略其他关键性代码....
-
-
- /**
- * 完成骑士周游问题的算法
- * @param chessboard 棋盘
- * @param row 马儿当前的位置的行 从0开始
- * @param column 马儿当前的位置的列 从0开始
- * @param step 是第几步 ,初始位置就是第1步
- */
- public static void traversalChessboard(int[][] chessboard, int row, int column, int step) {
-
- //标记当前棋盘执行的是第几步
- chessboard[row][column] = step;
-
- //row = 3 X = 6 column = 3 = 3 * 6 + 3 = 21 -1 = 20
- visited[row * x + column] = true; //标记该位置已经访问
-
- //获取当前位置可以走的下一个位置的集合
- ArrayList<Point> ps = next(new Point(column, row));
-
- //遍历 ps
- while(!ps.isEmpty()) {
- Point p = ps.remove(0);//取出下一个可以走的位置
- //判断该点是否已经访问过
- if(!visited[p.y * X + p.x]) {//说明还没有访问过
- traversalChessboard(chessboard, p.y, p.x, step + 1);
- }
- }
- }
- }
我们怎么区分当前节点的可以走的下一个位置的集合,是否一路就成功了呢?
使用step 和 应该走的步数比较:step = X * Y
假如当前节点的可以走的下一个位置的集合,没有一路就成功,怎么办?
取消该位置已访问,并将棋盘置为0,说明该节点处于回溯状态
- public class HorseChessboard {
-
- //省略其他关键性代码....
-
-
- /**
- * 完成骑士周游问题的算法
- * @param chessboard 棋盘
- * @param row 马儿当前的位置的行 从0开始
- * @param column 马儿当前的位置的列 从0开始
- * @param step 是第几步 ,初始位置就是第1步
- */
- public static void traversalChessboard(int[][] chessboard, int row, int column, int step) {
- chessboard[row][column] = step;
- //row = 4 X = 8 column = 4 = 4 * 8 + 4 = 36
- visited[row * x + column] = true; //标记该位置已经访问
- //获取当前位置可以走的下一个位置的集合
- ArrayList<Point> ps = next(new Point(column, row));
- //遍历 ps
- while(!ps.isEmpty()) {
- Point p = ps.remove(0);//取出下一个可以走的位置
- //判断该点是否已经访问过
- if(!visited[p.y * x + p.x]) {//说明还没有访问过
- traversalChessboard(chessboard, p.y, p.x, step + 1);
- }
- }
- //判断马儿是否完成了任务,使用 step 和应该走的步数比较 ,
- //如果没有达到数量,则表示没有完成任务,将整个棋盘置0
- //说明: step < X * Y 成立的情况有两种
- //1. 棋盘到目前位置,仍然没有走完
- //2. 棋盘处于一个回溯过程
- if(step < x * y && !finished ) {
- chessboard[row][column] = 0;
- visited[row * x + column] = false;
- } else {
- finished = true;
- }
- }
- }
接下来,让我们使用demo 测试一把这些思路与代码
我们采用上图的马儿作为起始位置,来测试看看
- public class HorseChessboard {
-
- //省略其他关键性代码....
-
-
- public static void main(String[] args) {
-
- System.out.println("骑士周游算法,开始运行~~");
- //测试骑士周游算法是否正确
- x = 6;
- y = 6;
- int row = 4; //马儿初始位置的行,从1开始编号
- int column = 3; //马儿初始位置的列,从1开始编号
-
- //创建棋盘
- int[][] chessboard = new int[x][y];
- visited = new boolean[x * y];//初始值都是false
-
- //测试一下耗时
- long start = System.currentTimeMillis();
- traversalChessboard(chessboard, row - 1, column - 1, 1);
- long end = System.currentTimeMillis();
- System.out.println("共耗时: " + (end - start) + " 毫秒");
- //输出棋盘的最后情况
- for(int[] rows : chessboard) {
- for(int step: rows) {
- System.out.print(step + "\t");
- }
- System.out.println();
- }
- }
- }
-
- 运行结果如下:
- 骑士周游算法,开始运行~~
- 共耗时: 40 毫秒
- 08 03 10 29 32 05
- 17 28 07 04 11 30
- 02 09 18 31 06 33
- 27 16 01 20 23 12
- 36 19 14 25 34 21
- 15 26 35 22 13 24
三、使用贪心思想进行优化
利用贪心算法的思想,对下一步的所有集合的数目, 进行非递减排序
什么是非递减?
递增的情况是:1、2、3、4、5、6、7、8、9
递减的情况是:9、8、7、6、5、4、3、2、1
非递增的情况是:9、8、7、6、5、5、4、3、2、1
非递减的情况是:1、2、2、3、3、4、4、5、6、7
目的:使马儿走的下一步是下一步集合中可选性最少的,减少回溯可能性
- public class HorseChessboard {
-
- //省略其他关键性代码....
-
- //根据当前这个一步的所有的下一步的选择位置,进行非递减排序, 减少回溯的次数
- public static void sort(ArrayList<Point> ps) {
- ps.sort(new Comparator<Point>() {
-
- @Override
- public int compare(Point o1, Point o2) {
- // TODO Auto-generated method stub
- //获取到o1的下一步的所有位置个数
- int count1 = next(o1).size();
- //获取到o2的下一步的所有位置个数
- int count2 = next(o2).size();
- if(count1 < count2) {
- return -1;
- } else if (count1 == count2) {
- return 0;
- } else {
- return 1;
- }
- }
-
- });
- }
- }
那么怎么使用呢,我们在算法里进行排序优化
- public class HorseChessboard {
-
- //省略其他关键性代码....
-
-
- /**
- * 完成骑士周游问题的算法
- * @param chessboard 棋盘
- * @param row 马儿当前的位置的行 从0开始
- * @param column 马儿当前的位置的列 从0开始
- * @param step 是第几步 ,初始位置就是第1步
- */
- public static void traversalChessboard(int[][] chessboard, int row, int column, int step) {
-
- //标记当前棋盘执行的是第几步
- chessboard[row][column] = step;
-
- //row = 3 X = 6 column = 3 = 3 * 6 + 3 = 21 -1 = 20
- visited[row * x + column] = true; //标记该位置已经访问
-
- //获取当前位置可以走的下一个位置的集合
- ArrayList<Point> ps = next(new Point(column, row));
-
- //对ps进行排序,排序的规则就是对ps的所有的Point对象的下一步的位置的数目,进行非递减排序
- sort(ps);
-
- //遍历 ps
- while(!ps.isEmpty()) {
- Point p = ps.remove(0);//取出下一个可以走的位置
- //判断该点是否已经访问过
- if(!visited[p.y * X + p.x]) {//说明还没有访问过
- traversalChessboard(chessboard, p.y, p.x, step + 1);
- }
- }
- }
- }
- public class HorseChessboard {
-
- //省略其他关键性代码....
-
-
- public static void main(String[] args) {
-
- System.out.println("骑士周游算法,开始运行~~");
- //测试骑士周游算法是否正确
- x = 6;
- y = 6;
- int row = 4; //马儿初始位置的行,从1开始编号
- int column = 3; //马儿初始位置的列,从1开始编号
-
- //创建棋盘
- int[][] chessboard = new int[x][y];
- visited = new boolean[x * y];//初始值都是false
-
- //测试一下耗时
- long start = System.currentTimeMillis();
- traversalChessboard(chessboard, row - 1, column - 1, 1);
- long end = System.currentTimeMillis();
- System.out.println("共耗时: " + (end - start) + " 毫秒");
- //输出棋盘的最后情况
- for(int[] rows : chessboard) {
- for(int step: rows) {
- System.out.print(step + "t");
- }
- System.out.println();
- }
- }
- }
-
- 运行结果如下:
- 骑士周游算法,开始运行~~
- 共耗时: 9 毫秒
- 08 03 10 29 32 05
- 17 28 07 04 11 30
- 02 09 18 31 06 33
- 27 16 01 20 23 12
- 36 19 14 25 34 21
- 15 26 35 22 13 24
从40毫秒 到9毫秒 这个速度还是很客观的,相比之前的算法更优一些