韩老师的最后一节课讲述了关于骑士周游问题的算法优化问题,感觉内容相对比较难以理解,因此我这里尝试使用我的理解和通俗理解对该问题进行解释:
步骤和思路分析:
1,创建棋盘chessBoard,是一个二维数组
2,将当前位置设置为已经访问,然后根据当前位置,计算马儿还能走哪些位置,并放入到一个集合中(ArrayList),最多有8个,每走一步就step+1,即记录下步数
3,遍历ArratList中存放的所有位置,看看哪个可以走,如果可以走通就继续,走不通就回溯(使用贪心算法进行优化的部分就是在这里,通过减少回溯次数来减少运行时间)
4,判断马儿是否完成了任务(即有没有走满整棋盘),使用step和应该走的步数比较,如果没有达到数量,则表示没有完成任务,将整个棋盘设置为0
注:马儿走的策略不同,则得到的结果也不一样,效率也不一样
代码分析:
import java.awt.*;
import java.util.ArrayList;
public class HorseChessBoard {
//定义属性
private static int X = 6; //表示col
private static int Y = 6; //表示row
private static int[][] chessBoard = new int[Y][X]; //棋盘
private static boolean[] visited = new boolean[X * Y]; //记录某个位置是否走过
private static boolean finished = false; //记录马是否遍历完棋盘
public static void main(String[] args) {
int row = 5;
int col = 5;
long start = System.currentTimeMillis();
traversalChessBoard(chessBoard, row - 1, col - 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();
}
}
//编写最核心算法,遍历棋盘,如果遍历成功,就把finished设置为true,并且将马走的每步step记录到chessBoard
/**
* 理解:每次选择一条路走到死胡同为止,再一步步回溯,每一次回溯就就会再次增多选择,直到这些选择全部走完,就回到最开始的位置再次开始遍历
**/
public static void traversalChessBoard(int[][] chessBoard, int row, int col, int step) {
//先把step记录到chessBoard
chessBoard[row][col] = step;
//把这个位置设置为已经访问
visited[row * X + col] = true;
//获取当前这个位置可以走的下一个位置有哪些
ArrayList<Point> ps = next(new Point(col, row)); //col - X, row - Y
//遍历
while (!ps.isEmpty()) {
//取出一个位置(点)
Point p = ps.remove(0);
//判断该位置是否走过,如果没有走过,就递归遍历
if (!visited[p.y * X + p.x]) { //和一维数组做对比
//递归遍历
traversalChessBoard(chessBoard, p.y, p.x, step + 1);
}
}
//回溯部分的代码
//当退出while,看看是否遍历成功,如果没有成功,就重置相应的值,进行回溯
if (step < X * Y && !finished) {
//重置该点重新归零
chessBoard[row][col] = 0;
visited[row * X + col] = false; //标记该条路是无法走通的,下次判断时自动避免走该条路
} else {
finished = true;
}
}
//编写方法,可以获取当前位置,可以走的下一步的所有位置(point 表示x, y)
public static ArrayList<Point> next(Point curPoint) {
//创建一个ArrayList
ArrayList<Point> ps = new ArrayList<>();
//创建一个Point对象(点/位置),准备放入到 ps
Point p1 = new Point();
//判断在curPoint是否可以走如下位置,如果可以走,就将该点(Point)放入到ps
//判断是否可以走5位置
if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y - 1) >= 0) {
ps.add(new Point(p1)); //这里一定要放入一个新的p1点,不然会造成p1点的位置一直在变换
}
//判断是否可以走6位置
if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y - 2) >= 0) {
ps.add(new Point(p1)); //这里一定要放入一个新的p1点,不然会造成p1点的位置一直在变换
}
//判断是否可以走7位置
if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y - 2) >= 0) {
ps.add(new Point(p1)); //这里一定要放入一个新的p1点,不然会造成p1点的位置一直在变换
}
//判断是否可以走5位置
if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y - 1) >= 0) {
ps.add(new Point(p1)); //这里一定要放入一个新的p1点,不然会造成p1点的位置一直在变换
}
//判断是否可以走1位置
if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y + 1) < Y) {
ps.add(new Point(p1)); //这里一定要放入一个新的p1点,不然会造成p1点的位置一直在变换
}
//判断是否可以走2位置
if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y + 2) < Y) {
ps.add(new Point(p1)); //这里一定要放入一个新的p1点,不然会造成p1点的位置一直在变换
}
//判断是否可以走3位置
if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y + 2) < Y) {
ps.add(new Point(p1)); //这里一定要放入一个新的p1点,不然会造成p1点的位置一直在变换
}
//判断是否可以走4位置
if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y + 1) < Y) {
ps.add(new Point(p1)); //这里一定要放入一个新的p1点,不然会造成p1点的位置一直在变换
}
return ps;
}
}
traversalChessBoard方法
理解:每次选择一条路走到死胡同为止,再一步步回溯,每一次回溯就就会再次增多选择,直到这些选择全部走完,就回到最开始的位置再次开始遍历。
具体步骤就是获取当前这个位置可以走的下一个位置有哪些,每次从ps数组中取出一个元素然后判断有没有走过,如果没有走过就利用递归进行遍历:
while (!ps.isEmpty()) { //取出一个位置(点) Point p = ps.remove(0); //判断该位置是否走过,如果没有走过,就递归遍历 if (!visited[p.y * X + p.x]) { //和一维数组做对比 //递归遍历 traversalChessBoard(chessBoard, p.y, p.x, step + 1); } }
然后写回溯部分代码:(本质就是将走到死胡同的选择(路)重新设置为0,然后进行新的判断)
//回溯部分的代码 //当退出while,看看是否遍历成功,如果没有成功,就重置相应的值,进行回溯 if (step < X * Y && !finished) { //重置该点重新归零 chessBoard[row][col] = 0; visited[row * X + col] = false; //标记该条路是无法走通的,下次判断时自动避免走该条路 } else { finished = true; }
next方法
该方法就是判断马的下一个位置能够去到哪8个地方(按日子格走)
//编写方法,可以获取当前位置,可以走的下一步的所有位置(point 表示x, y) public static ArrayList<Point> next(Point curPoint) { //创建一个ArrayList ArrayList<Point> ps = new ArrayList<>(); //创建一个Point对象(点/位置),准备放入到 ps Point p1 = new Point(); //判断在curPoint是否可以走如下位置,如果可以走,就将该点(Point)放入到ps //判断是否可以走5位置 if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y - 1) >= 0) { ps.add(new Point(p1)); //这里一定要放入一个新的p1点,不然会造成p1点的位置一直在变换 } //判断是否可以走6位置 if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y - 2) >= 0) { ps.add(new Point(p1)); //这里一定要放入一个新的p1点,不然会造成p1点的位置一直在变换 } //判断是否可以走7位置 if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y - 2) >= 0) { ps.add(new Point(p1)); //这里一定要放入一个新的p1点,不然会造成p1点的位置一直在变换 } //判断是否可以走5位置 if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y - 1) >= 0) { ps.add(new Point(p1)); //这里一定要放入一个新的p1点,不然会造成p1点的位置一直在变换 } //判断是否可以走1位置 if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y + 1) < Y) { ps.add(new Point(p1)); //这里一定要放入一个新的p1点,不然会造成p1点的位置一直在变换 } //判断是否可以走2位置 if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y + 2) < Y) { ps.add(new Point(p1)); //这里一定要放入一个新的p1点,不然会造成p1点的位置一直在变换 } //判断是否可以走3位置 if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y + 2) < Y) { ps.add(new Point(p1)); //这里一定要放入一个新的p1点,不然会造成p1点的位置一直在变换 } //判断是否可以走4位置 if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y + 1) < Y) { ps.add(new Point(p1)); //这里一定要放入一个新的p1点,不然会造成p1点的位置一直在变换 } return ps; }
贪心算法进行优化
优化思路是:应该选择下一个的下一个位置较少的点开始走,这样可以减少回溯的次数。
比如:下一次走的1号位置和2号位置,1号位置的下一步有两个选择,2号位置下一步有四个选择,那马下一步就走1号位置以减少回溯次数
sort 方法
该方法对ps一维数组中的所有点的下一个位置可能走的路线的数量进行升序排列,以便后续能够调用方法取出最少的一条路线形成贪心优化
//编写一个方法,对ps的各个位置,可以走的下一个位置的次数进行排序,把可能走的下一个位置从小到大排序 public static void sort(ArrayList<Point> ps) { ps.sort(new Comparator<Point>() { @Override public int compare(Point o1, Point o2) { return next(o1).size() - next(o2).size(); } }); }
然后就是考虑调用的位置 :应该在获取ps数组下一个可以走的位置有哪些之后就对这些路线的数量进行排序,而其位置就在traversalChessBoard方法下,在对ps数组进行遍历之前进行排序
直接加上:sort(ps); //这样就把ps数组里的每一个点的下一个位置的多少进行了排序,优先走下一步的下一步的次数少的格子
优化成功后的效果演示:
由此可见算法对于时间复杂度的减少是功不可没的。
相关洛谷的一道算法题
由于题目形式非常相似,让我联想到了以前在洛谷上刷题时遇到的一道算法题,名叫过河卒
代码解答
import java.util.Scanner;
public class Main {
private static final int[] fx = { 0, -2, -1, 1, 2, 2, 1, -1, -2 };
private static final int[] fy = { 0, 1, 2, 2, 1, -1, -2, -2, -1 };
private static long[][] f = new long[40][40];
private static int[][] s = new int[40][40];
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int bx = in.nextInt() + 2;
int by = in.nextInt() + 2;
int mx = in.nextInt() + 2;
int my = in.nextInt() + 2;
f[2][1] = 1;
for (int i = 0; i <= 8; i++) {
s[mx + fx[i]][my + fy[i]] = 1;
}
for (int i = 2; i <= bx; i++) {
for (int j = 2; j <= by; j++) {
if (s[i][j]==1) {
continue;
}
f[i][j] = f[i - 1][j] + f[i][j - 1]; //计算每个节点能通过的路线有几条并记录下来
}
}
System.out.println(f[bx][by]);
}
}
这其中涉及的相关知识点虽然不是贪心算法,涉及到的是动态规划,但也和算法相关。
变量解析:
设置的fx与fy都是马的所有移动可能,在马所在的位置的基础上经过加减fx,fy之后马所到达的位置
定义两个二维数组f
和s
。f
用于存储从起点到每个节点的路径数,s
用于标记马及其控制范围的节点。
读取目标坐标(bx, by)
和马的坐标(mx, my)
,并将其各自加2。这是为了方便在数组中处理,因为数组索引从0开始且预留一定边界。
初始化起点的路径数f[2][1]
为1,表示从起点到自身有一条路径。
循环解析:
首先遍历所有可能的马的移动位置,将这些位置和马所在的位置标记为1,表示这些位置是马控制的区域,不可通过。
for (int i = 0; i <= 8; i++) {
s[mx + fx[i]][my + fy[i]] = 1;
}
双重循环遍历从起点到目标点的每个节点。
如果当前节点被马控制(s[i][j] == 1
),则跳过该节点。
否则,计算当前节点的路径数f[i][j]
,等于从上方和左方来的路径数之和(f[i-1][j] + f[i][j-1]
)。
for (int i = 2; i <= bx; i++) {
for (int j = 2; j <= by; j++) {
if (s[i][j] == 1) {
continue;
}
f[i][j] = f[i - 1][j] + f[i][j - 1]; //计算每个节点能通过的路线有几条并记录下来
}
}