简说Python,号主老表,Python终身学习者,数据分析爱好者,从18年开始分享Python知识,原创文章227篇,写过Python、SQL、Excel入门文章,也写过Web开发、数据分析文章,老表还总结整理了一份2022Python学习资料和电子书资源,关注后私信回复:2022 即可领取。
走迷宫
显示迷宫
迷宫生成等等再提,先看一下迷宫的读取和显示。
第一行是行数和列数,代表有101行101列,这个迷宫后面可以使用最小生成树生成。读进一个迷宫:
public class MazeData { private char[][] maze; private int N, M; public static final char ROAD = '#'; public static final char WALL = ' '; public MazeData(String fileName) { if (fileName == null) { throw new IllegalArgumentException("filename can't be null!"); } Scanner scanner = null; try { File file = new File(fileName); if (!file.exists()) { throw new IllegalArgumentException("File is not exist!"); } FileInputStream fileInputStream = new FileInputStream(file); scanner = new Scanner(new BufferedInputStream(fileInputStream), "UTF-8"); String nm = scanner.nextLine(); String[] nmC = nm.trim().split("\\s+"); N = Integer.parseInt(nmC[0]); M = Integer.parseInt(nmC[1]); maze = new char[N][M]; for (int i = 0; i < N; i++) { String line = scanner.nextLine(); if (line.length() != M) { throw new IllegalArgumentException("Message of file is not completed!"); } for (int j = 0; j < M; j++) { maze[i][j] = line.charAt(j); } } } catch (Exception e) { e.printStackTrace(); } finally { if (scanner != null) { scanner.close(); } } } public char getMaze(int i, int j) { if (!inArea(i, j)) { throw new IllegalArgumentException("out of range!"); } return maze[i][j]; } public boolean inArea(int x, int y) { return x >= 0 && x < N && y >= 0 && y < M; } public int N() { return N; } public int M() { return M; } }
使用File来获得当前文件,如果没有就要抛出异常。scanner获得输入流之后可以通过读取下一行获得每一行的内容,列数在前面已经提到了,所以要检查每一行是不是M列,不是就没得读了。#就是墙,空格是路,可以设置两个静态变量表示。同时还需要各种辅助函数,比如是否是在整区域里面的,返回当前区域的值等等。然后就是显示函数了:
int w = canvasWidth / data.M(); int h = canvasHeight / data.N(); for (int i = 0; i < data.N(); i++) { for (int j = 0; j < data.M(); j++) { if (data.getMaze(i, j) == MazeData.ROAD){ AlgorithmHelper.setColor(graphics2D, AlgorithmHelper.LightBlue); }else { AlgorithmHelper.setColor(graphics2D, AlgorithmHelper.White); } AlgorithmHelper.fillRectangle(graphics2D, j * w, i * h, w, h); } }
墙的宽度自适应,这样整个屏幕刚刚够。#号画浅蓝色,其余的白色。之后就是再控制器里面调用repaint即可。
迷宫问题
白色方块是可以走的路径,红色的就是墙。迷宫的本质就是一个图结构。可以把整个迷宫当成是一个图,而走迷宫的过程就可以等价成是图的遍历。从起始点开始遍历,直到遍历到了某一个终止点即可。如果遍历了所有点都没有得到结果,那么就可以确认无解了。
图的遍历可以分成两种遍历。深度优先遍历和广度优先遍历,一种遍历是按照深度,先往一个方向深了走,没有路了再回头,而广度是先广度走一遍再一层一层下去。首先固定了每一个迷宫的出口和入口位置,从一开始,就需要从相邻的四个方向走迷宫,如果可以就继续,不能就回头,其实就是递归实现。
深度优先
首先还是递归实现,递归比较方便,首先要准备递归函数,和上述的一样,走四个方向,一个一个尝试,如果下一个格子是在这个图里面,是一条路,而且还没有被访问过,那么久可以继续走,否则就需要返回。
private boolean go(int x, int y) { if (!data.inArea(x, y)) { throw new IllegalArgumentException("Paramenter is illgel!"); } data.visited[x][y] = true; setData(x, y, true); if (x == data.getExitX() && y == data.getExitY()) { return true; } else { for (int i = 0; i < 4; i++) { int nexX = x + direction[i][0]; int nexY = y + direction[i][1]; if (data.inArea(nexX, nexY) && data.getMaze(nexX, nexY) == MazeData.ROAD && !data.visited[nexX][nexY]) { if (go(nexX, nexY)) { return true; } } } setData(x, y, false); return false; } }
如果四个点都尝试过了,都是走不了的,那么还需要消除画的格子。相对来说还是比较简单的。再消除格子上这个步骤对于递归来说是相对方便,因为再回溯的过程中是有保留之前的点的信息的,所以相对简单。
这就是生成的结果了。
如果是非递归,用栈就可以模拟,因为递归本身就是用栈实现的。对于删除无用路径的情况,其实有点难,因为无用的路径是直接丢弃的,先前的递归可以是因为递归的栈保留了更加多的内容,而这里只是保留了点而已。
private boolean go_iteration() { Stack<Position> stack = new Stack<>(); Position entrance = new Position(data.getEntanceX(), data.getEntanceY()); stack.push(entrance); data.visited[entrance.getX()][entrance.getY()] = true; while (!stack.isEmpty()) { Position position = stack.pop(); setData(position.getX(), position.getY(), true); for (int i = 0; i < 4; i++) { int newX = position.getX() + direction[i][0]; int newY = position.getY() + direction[i][1]; if (newX == data.getExitX() && newY == data.getExitY()) { setData(newX, newY, true); return true; } Position newPosition = new Position(newX, newY, position); if (data.inArea(newPosition.getX(), newPosition.getY()) && data.getMaze(newPosition.getX(), newPosition.getY()) == MazeData.ROAD && !data.visited[newPosition.getX()][newPosition.getY()]) { stack.push(newPosition); data.visited[newPosition.getX()][newPosition.getY()] = true; } } } return false; }
广度优先
广度和深度在搜索策略上是不同的。深度是走到死路才回头,广度是对于每一次都是齐头并进。和遍历的深度优先的区别就是在于他们的数据结构不一样,一个是队列,一个是栈,其他的基本差不多。
private boolean go_level() { Queue<Position> queue = new LinkedList<>(); Position position = new Position(data.getEntanceX(), data.getEntanceY()); queue.add(position); data.visited[position.getX()][position.getY()] = true; while (!queue.isEmpty()) { Position position1 = queue.poll(); setData(position1.getX(), position1.getY(), true); for (int i = 0; i < 4; i++) { int newX = position1.getX() + direction[i][0]; int newY = position1.getY() + direction[i][1]; if (newX == data.getExitX() && newY == data.getExitY()) { findPath(position1); setData(newX, newY, true); return true; } Position newPosition = new Position(newX, newY, position1); if (data.inArea(newPosition.getX(), newPosition.getY()) && data.getMaze(newPosition.getX(), newPosition.getY()) == MazeData.ROAD && !data.visited[newPosition.getX()][newPosition.getY()]) { queue.add(newPosition); data.visited[newPosition.getX()][newPosition.getY()] = true; } } } return false; }
如果迷宫有很多个解,深度优先遍历那么久只会搜索到第一个碰到的解,搜索到的解那么就是一个随缘排序出来,广度优先就是会查找最短的路径。广度优先可以找到无全图的最短路径。深度和广度的非递归差不多,只是使用的数据结构不同而已。
生成迷宫
刚刚是走迷宫,刚刚生成的那个用例其实就是生成的迷宫。对于一个迷宫,只有一个入口一个出口,为了简单化,入口就是第二行的第一个口,出口是倒数第二行的第一个口。而且只有一个解,并且路径是连续的,有些游戏里面的迷宫是有传送点的,改变起来也很简单。
首先迷宫其实就是一棵树,每一个点都会有分支,任何一个叶子或者是节点都可以作为是一个入口,生成一个迷宫其实就是一个生成树的过程。之前在数据结构有提到过一个最小生成树,但是由于是一个随机的迷宫,所以应该是随机生成树。无论是什么树,都是基于树的。而图的遍历结果就是一颗树,每一个节点只是访问一次,且没有环,深度优先遍历的结果是一颗深度优先树,广度优先的结果是广度优先树。可以先把一张画布分成很多很多小格子,然后每隔一个格子就挖空一个点,没有挖空点的都是墙,用一种遍历方法来遍历这些点所生成的树就是一个迷宫了。但是这样的迷宫其实带有偏见的,随机性不高,所以可以在选择点的进行遍历的时候进行随机选择。
@@@@@
@ @ @
@@@@@
@ _ @ _@
@@@@@
可以看的出无论怎么看,行和列一定要是基数,限制还是蛮多的。深度优先生成迷宫其实和之前的差不多,没有上面打的差别。首先是要得到一个格子布。然后通过深度遍历把格子全部连接起来。递归实现:
private void run() { setData(-1, -1); go(data.getEntranceX(), data.getEntranceY() + 1); setData(-1, -1); } private void go(int x, int y) { if (!data.inArea(x, y)) { throw new IllegalArgumentException("x or y is illegal!"); } data.visited[x][y] = true; for (int i = 0; i < 4; i++) { int newX = x + direction[i][0] * 2; int newY = y + direction[i][1] * 2; if (data.inArea(newX, newY) && !data.visited[newX][newY]) { setData(x + direction[i][0], y + direction[i][1]); go(newX, newY); } } }
这里要注意每两个格子之间的差距是2,因为中间都隔着一堵墙。go的参数是开始的参数,开始不能直接从入口开始,因为入口是我们新加的,不符合迷宫节点的规矩,比如每一个格子相差两个点,但是出口和第一个点差一个而已。
int newX = x + direction[i][0] * 2;
int newY = y + direction[i][1] * 2;
乘上2的原因就是因为两个格子之间相差了2。而渲染都放在了setData里面处理。渲染的点就不是我们newX和newY了,因为那两个点本来就是road,渲染的应该是两个点之间的墙,所以是加1的。和前面深度搜索对比区别就是,这里没有终止点,不是到了exit就退出,事实上是不一定有解的。因为我们这里是要全部生成而不是生成到了终点就停止,所以是无终止条件的。但是for循环里面是隐含了的。还有一个就是条件确认是不是一条路,这个决策是不必要的,因为就要生成路的。但是这样导致的迷宫很无随机性:
因为方向都是一样的,从左上右下这样。
这是递归方法,非递归方法:
private void go_iterator(){ Stack<Position> stack = new Stack<>(); Position firstPosition = new Position(data.getEntranceX(), data.getEntranceY() + 1); stack.push(firstPosition); data.visited[firstPosition.getX()][firstPosition.getY()] = true; while (!stack.isEmpty()){ Position position = stack.pop(); for (int i = 0; i < 4; i++) { int newX = position.getX() + direction[i][0] * 2; int newY = position.getY() + direction[i][1] * 2; if (data.inArea(newX, newY) && !data.visited[newX][newY]) { setData(position.getX() + direction[i][0], position.getY() + direction[i][1]); stack.push(new Position(newX, newY)); data.visited[newX][newY] = true; } } } }
不一样的原因就是因为在加入栈的时候就已经上下左右看了,看看能不能走,走就直接把墙消去了,所以会出现锯齿型。至于大体的trend的不一样的因为两个方向是相反的。
广度遍历:之前提到过了和深度遍历差不多:
private void go_level(){ LinkedList<Position> stack = new LinkedList<>(); Position firstPosition = new Position(data.getEntranceX(), data.getEntranceY() + 1); stack.addLast(firstPosition); data.visited[firstPosition.getX()][firstPosition.getY()] = true; while (stack.size() != 0){ Position position = stack.removeFirst(); for (int i = 0; i < 4; i++) { int newX = position.getX() + direction[i][0] * 2; int newY = position.getY() + direction[i][1] * 2; if (data.inArea(newX, newY) && !data.visited[newX][newY]) { setData(position.getX() + direction[i][0], position.getY() + direction[i][1]); stack.addLast(new Position(newX, newY)); data.visited[newX][newY] = true; } } } }
生成的图像都很规则。 一个很重要的原因的因为我们在数据结构的选择过程中都是栈和队列,可预期性太强了。我们只需要在数据结构中加上随机性就好了。出队或者是删除都是随机队列。
public class RandomQueue<E> { private ArrayList<E> queue; public RandomQueue() { queue = new ArrayList<>(); } public void add(E e) { queue.add(e); } public E remove() { if (queue.size() == 0) { throw new IllegalArgumentException("no elements!"); } int randomIndex = (int) (Math.random() * queue.size()); E Ele = queue.get(randomIndex); queue.set(randomIndex, queue.get(queue.size() - 1)); queue.remove(queue.size() - 1); return Ele; } public boolean isEmpty(){ return queue.isEmpty(); } }
在广度优先里面把队列改一下:
这样就有一定的随机性了。可以看到在很多空白的小格子很容易让别人猜到我们是怎么生成的。所以可以加上如果没有遍历到的格子全部变黑色。
private void go_level(){ RandomQueue<Position> stack = new RandomQueue<>(); Position firstPosition = new Position(data.getEntranceX(), data.getEntranceY() + 1); stack.add(firstPosition); data.openMinst(firstPosition.getX(), firstPosition.getY()); data.visited[firstPosition.getX()][firstPosition.getY()] = true; while (!stack.isEmpty()){ Position position = stack.remove(); for (int i = 0; i < 4; i++) { int newX = position.getX() + direction[i][0] * 2; int newY = position.getY() + direction[i][1] * 2; if (data.inArea(newX, newY) && !data.visited[newX][newY]) { data.openMinst(newX, newY); setData(position.getX() + direction[i][0], position.getY() + direction[i][1]); stack.add(new Position(newX, newY)); data.visited[newX][newY] = true; } } } }private void go_level(){ RandomQueue<Position> stack = new RandomQueue<>(); Position firstPosition = new Position(data.getEntranceX(), data.getEntranceY() + 1); stack.add(firstPosition); data.openMinst(firstPosition.getX(), firstPosition.getY()); data.visited[firstPosition.getX()][firstPosition.getY()] = true; while (!stack.isEmpty()){ Position position = stack.remove(); for (int i = 0; i < 4; i++) { int newX = position.getX() + direction[i][0] * 2; int newY = position.getY() + direction[i][1] * 2; if (data.inArea(newX, newY) && !data.visited[newX][newY]) { data.openMinst(newX, newY); setData(position.getX() + direction[i][0], position.getY() + direction[i][1]); stack.add(new Position(newX, newY)); data.visited[newX][newY] = true; } } } }
但是其实还有一个问题,很多时候这个迷宫的路径顺序是都是斜向下的趋势,所以有时候是可以猜到怎么走的。可以通过改进随机队列:
public void add(E e) { if (Math.random() < 0.5){ queue.addFirst(e); }else { queue.addLast(e); } } public E remove() { if (queue.size() == 0) { throw new IllegalArgumentException("no elements!"); } // int randomIndex = (int) (Math.random() * queue.size()); // E Ele = queue.get(randomIndex); // queue.set(randomIndex, queue.get(queue.size() - 1)); // queue.remove(queue.size() - 1); // return Ele; if (Math.random() < 0.5){ return queue.removeFirst(); }else { return queue.removeLast(); } }
这个时候随机性就更强了: