js迷宫生成与迷宫求解算法
迷宫问题是一个很经典的问题,本文记叙迷宫的生成和求解(深度优先),完整dome见文章末尾(包括动画演示)
所涉及迷宫为:
- 方形规则迷宫
- 只有一个出口和一个入口
- 路径连续
- 只有一个解
先看效果:
a.迷宫的生成
生成迷宫要比将大象放进冰箱简单,只需要两步
1. 生成数据 2.渲染数据
思路:
首先先渲染如图的图形,每个蓝色或白色都是一个方形的小格子,之后对图形中每一个小格子进行遍历
通过遍历图形中白色部分的小方块,通过某种方法将两个白色方块之间的蓝色小方块变成白色,得到迷宫
开始敲代码:
首先定义一个Maze类,初始化属性和方法
包括每一个小格子的类名,整个迷宫的行数和列数,迷宫的宽高,出入口坐标,各数据容器(数组)
class Maze {constructor(row, col, width = 500, height = 500) {// 类名this.class = Math.random().toFixed(2)// 行数,列数this.row = rowthis.col = col// 迷宫的长宽this.width = widththis.height = height// 设置路与墙this.road = ' 'this.wall = '#'// 入口坐标(1, 0)this.entryX = 1this.entryY = 0// 出口坐标this.outX = row - 2this.outY = col - 1// 迷宫数据this.maze = []}// 初始化迷宫数据initData(maze) {}// 初始化迷宫domeinitDOM(maze){}// 渲染迷宫paintMaze() {}}
首先初始化迷宫数据,得到一个初始框架
initData(maze) {for (let i = 0; i < this.row; i++) {maze[i] = new Array(this.col).fill(this.wall) // 初始化二维数组this.visited[i] = new Array(this.col).fill(false) // 初始化访问状态为false,用于求解this.findPathVisited[i] = new Array(this.col).fill(false) // 初始化访问状态为falsethis.path[i] = new Array(this.col).fill(null) // 初始化所有元素的上一个元素为nullfor (let j = 0; j < this.col; j++) {// 横纵坐标均为奇数 则是路if (i % 2 === 1 && j % 2 === 1) {maze[i][j] = this.road}}}// 入口及出口 则是路maze[this.entryX][this.entryY] = this.roadmaze[this.outX][this.outY] = this.roadreturn maze
}
其中设置了访问状态,为后续求解做准备,数据准备完成后开始初始化DOM,后续将准备好的DOM渲染出来
initDOM(maze) {let oDiv = document.createElement('div')oDiv.style.width = this.width + 'px'oDiv.style.height = this.height + 'px'oDiv.style.display = 'flex'oDiv.className = 'box'oDiv.style.flexWrap = 'wrap'oDiv.style.marginBottom = '20px'for (let i = 0; i < maze.length; i++) {for (let j = 0; j < maze[i].length; j++) {let oSpan = document.createElement('span')oSpan.dataset.index = i + '-' + j + '-' + this.classoSpan.style.width = (this.width / this.col).toFixed(2) + 'px'oSpan.style.height = (this.height / this.row).toFixed(2) + 'px'oSpan.style.background = maze[i][j] === this.wall ? '#185ed1' : '#fff'oDiv.appendChild(oSpan)}}document.querySelector('.main').appendChild(oDiv)
}
创建一个div元素,设置宽高,类型和相关样式作为父盒子,通过for循环为其添加子元素(小格子)通过父盒子的宽高和迷宫所需要的行列数计算每个小格子的宽高
oSpan.style.width = (this.width / this.col).toFixed(2) + 'px'
oSpan.style.height = (this.height / this.row).toFixed(2) + 'px'
重点在于每个小格子的一个标识,使用自定义属性data-index标识,使用位置坐标加初始化时生成的随机数
oSpan.dataset.index = i + '-' + j + '-' + this.class
box的div准备完成之后将其渲染到界面内,此时就完成了最初的图形
接下来开始制作迷宫
定义一个方法来初始化迷宫
initMaze() {// 迷宫数据let maze = this.initData(this.maze)// 初始迷宫DOMthis.initDOM(maze)
}
定义一个方法,来渲染迷宫对应节点DOM的颜色
resetMaze(x, y, type) {// 只有不越界的点才做后续处理if (this.isArea(x, y)) {// 改变this.maze中对应的数据this.maze[x][y] = type// 改变dom中对应的节点颜色let changeSpan = document.querySelector(`span[data-index='${x}-${y}-${this.class}']`)changeSpan.style.background = type === this.wall ? '#185ed1' : '#fff'}
}
在整个过程中要判断处理的小格子是否越界,即是否处理到了边框,要限制只有一个出口和入口
定义一个方法来判断是否越界
isArea(x, y) {return x > 0 && x < this.row - 1 && y > 0 && y < this.col - 1 || x == this.entryX && y == this.entryY || x == this.outX && y == this.outY
}
准备一个类,来生成随机迷宫
class Queue {constructor() {this.queue = []}push(pos) {if (Math.random() < 0.5) {this.queue.push(pos)} else {this.queue.unshift(pos)}}pop() {if (Math.random() < 0.5) {return this.queue.pop()} else {return this.queue.shift()}}empty() {return !this.queue.length}
}
其中定义push和pop的添加与删除删除方法,以及一个是否为空的方法
准备工作完成开始定义渲染迷宫的方法
paintMaze() {this.initMaze()let queue = new Queue()queue.push({ x: this.entryX, y: this.entryY + 1 })this.visited[this.entryX][this.entryY + 1] = truewhile (!queue.empty()) {let curPos = queue.pop()for (let i = 0; i < 4; i++) {let newX = curPos.x + this.offset[i][0] * 2 // 两步是 *2let newY = curPos.y + this.offset[i][1] * 2// 坐标没有越界 而且 没有被访问过if (this.isArea(newX, newY) && !this.visited[newX][newY]) {this.resetMaze((newX + curPos.x) / 2, (newY + curPos.y) / 2, this.road) // 打通两个方块之间的墙queue.push({ x: newX, y: newY })this.visited[newX][newY] = true}}}this.hasDown = truereturn this
}
此处也使用了是否被访问过属性,当被访问过则跳过处理,当此方法被调用时
首先初始化迷宫盒子,生成随机迷宫数据,将其改变到迷宫盒子上,得到一个迷宫
看效果
重点在于迷宫的随机以及理解如何重新渲染迷宫(在父盒子基础上得到迷宫),每一个小方格的处理都需要注意是否访问过以及是否越界
之后抽离相关属性,如需要渲染的行数列数,迷宫宽高等,以上代码以做抽离
b.迷宫的自动求解
整体思路:
首先整个迷宫是由有限个小格子组成,这些小格子分为两类,一种是墙不可跨越,一种是路,需要沿着路走
现实中求解迷宫最基础的方法就是尝试,确定出口对于入口的方向开始尝试,此处也是一样
对于求解,相当于一个点在尝试,经过的路点会留下痕迹,对于一个点在此迷宫中有一个坐标(x,y)他要行走只有四个方向
- 尝试向下走,优先求解
- 尝试向右走,次优先求解
- 尝试向上走,求解
- 尝试向左走,求解
整体方向需要向着出口走,首先向下方尝试,遇到路后尝试右方,如果通则走,不通则尝试上和左,直到往复找到出口
看代码
为Maze添加求解迷宫需要的属性
// 求解迷宫时各节点的遍历情况
this.findPathVisited = []
// 迷宫是否生成完成
this.hasDown = false
定义一个方法来渲染路线(渲染指定节点)
findPathReset(x, y, color = '#fff') {this.i++this.findPathSpan(x, y, color)
}
findPathSpan(x, y, color) {// 只有不越界的点才做后续处理if (this.isArea(x, y)) {// 改变dom中对应的节点颜色let changeSpan = document.querySelector(`span[data-index='${x}-${y}-${this.class}']`)changeSpan.style.background = color}
}
此处依然需要判断是否越界
定义一个类,记录自动求解过程数据,如上文Queue类
class Stack {constructor() {this.stack = []}push(pos) {this.stack.push(pos)}pop() {return this.stack.pop()}empty() {return !this.stack.length}
}
准备工作完成后开始求解
findPath() {let stack = new Stack()stack.push({ x: this.entryX, y: this.entryY }) // 开始while (!stack.empty()) {let curPos = stack.pop()this.findPathVisited[curPos.x][curPos.y] = true // 求解时访问过// 找到出口if (curPos.x === this.outX && curPos.y === this.outY) {this.hasFindPath = true// 绘制解this.findPathReset(curPos.x, curPos.y, 'red') // 绘制出口let prePos = this.path[curPos.x][curPos.y] // 获取上一个点while (prePos != null) {this.findPathReset(prePos.x, prePos.y, 'red') // 渲染上一个点prePos = this.path[prePos.x][prePos.y] // 获取上一个点的上一个点}break;}for (let i = 0; i < 4; i++) {let newX = curPos.x + this.offset[i][0]let newY = curPos.y + this.offset[i][1]if (this.isArea(newX, newY) && this.maze[newX][newY] === this.road && !this.findPathVisited[newX][newY]) {this.path[newX][newY] = { x: curPos.x, y: curPos.y } // 记录新的点以及该点由谁走过来stack.push({ x: newX, y: newY })}}}if (!this.hasFindPath) return alert('迷宫无解!')
}
如上原理所示,开始向着四个方向尝试寻找出口,在寻找过程中将访问过的节点设置为已访问,逐步向下,当找到出口时找到完整路线渲染
效果如下
求解过程重点是对整个元素的遍历,如何遍历,如何标记以及遍历到的元素如何处理,以及记忆路线
c. 过程可视化及完整dome
在路线渲染过程中定义一个参数来做时间延迟,对每个访问过的节点做颜色标识
修改求解过程中相关代码如下
findPathReset(x, y, color = '#fff', iss) {if (!iss) {this.findPathSpan(x, y, color)return}this.i++setTimeout(() => { // 可视化展示this.findPathSpan(x, y, color)}, this.i * iss);
}
求解过程修改
findPath(iss) {if (!this.hasDown) return alert('请等待迷宫生成后再求解!')let stack = new Stack()stack.push({ x: this.entryX, y: this.entryY }) // 开始while (!stack.empty()) {let curPos = stack.pop()this.findPathVisited[curPos.x][curPos.y] = true // 求解时访问过if (iss > 0) this.findPathReset(curPos.x, curPos.y, '#cd9cf2', iss) // 渲染当前点// 找到出口if (curPos.x === this.outX && curPos.y === this.outY) {this.hasFindPath = true// 绘制解this.findPathReset(curPos.x, curPos.y, 'red', iss) // 绘制出口let prePos = this.path[curPos.x][curPos.y] // 获取上一个点while (prePos != null) {this.findPathReset(prePos.x, prePos.y, 'red', iss) // 渲染上一个点prePos = this.path[prePos.x][prePos.y] // 获取上一个点的上一个点}break;}for (let i = 0; i < 4; i++) {let newX = curPos.x + this.offset[i][0]let newY = curPos.y + this.offset[i][1]if (this.isArea(newX, newY) && this.maze[newX][newY] === this.road && !this.findPathVisited[newX][newY]) {this.path[newX][newY] = { x: curPos.x, y: curPos.y } // 记录新的点以及该点由谁走过来stack.push({ x: newX, y: newY }) }}}if (!this.hasFindPath) return alert('迷宫无解!')
}
定义三个按钮,分别是生成迷宫,计算,显示计算过程
<button onclick="init()">生成迷宫</button>
<button onclick="calculation(0)">开始计算</button>
<button onclick="calculation(80)">显示计算过程</button>
定义对应函数
let mazeHasDown = new Maze(45, 45, 400, 400)
function init() {mazeHasDown.paintMaze()
}
function calculation(iss) {mazeHasDown.findPath(iss)
}
完整dome
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Document</title><style>.main {margin-top: 30px;position: relative;}</style>
</head><body><button onclick="init()">生成迷宫</button><button onclick="calculation(0)">开始计算</button><button onclick="calculation(80)">显示计算过程</button><div class="main"></div>
</body><script>// 生成随机迷宫class Queue {constructor() {this.queue = []}push(pos) {if (Math.random() < 0.5) {this.queue.push(pos)} else {this.queue.unshift(pos)}}pop() {if (Math.random() < 0.5) {return this.queue.pop()} else {return this.queue.shift()}}empty() {return !this.queue.length}}// 自动求解class Stack {constructor() {this.stack = []}push(pos) {this.stack.push(pos)}pop() {return this.stack.pop()}empty() {return !this.stack.length}}// 自动生成迷宫class Maze {constructor(row, col, width = 500, height = 500) {// 类名this.class = Math.random().toFixed(2)// Maze行列this.row = rowthis.col = col// 迷宫的长宽this.width = widththis.height = height// 设置路与墙this.road = ' 'this.wall = '#'// 入口坐标(1, 0)this.entryX = 1this.entryY = 0// 出口坐标(倒数第二行, 最后一列)this.outX = row - 2this.outY = col - 1// 迷宫数据this.maze = []// 生成迷宫时各节点的遍历情况this.visited = []// 求解迷宫时各节点的遍历情况this.findPathVisited = []// 设置上下左右的偏移坐标值(上右下左)this.offset = [[-1, 0], [0, 1], [1, 0], [0, -1]]// 迷宫是否生成完成this.hasDown = false// 迷宫是否有解this.hasFindPath = false// 存储迷宫某点的上一点位置this.path = []this.i = 0 // 可视化展示索引}// 初始化迷宫数据initData(maze) {for (let i = 0; i < this.row; i++) {maze[i] = new Array(this.col).fill(this.wall) // 初始化二维数组this.visited[i] = new Array(this.col).fill(false) // 初始化访问状态为falsethis.findPathVisited[i] = new Array(this.col).fill(false) // 初始化访问状态为falsethis.path[i] = new Array(this.col).fill(null) // 初始化所有元素的上一个元素为nullfor (let j = 0; j < this.col; j++) {// 横纵坐标均为奇数 则是路if (i % 2 === 1 && j % 2 === 1) {maze[i][j] = this.road}}}// 入口及出口 则是路maze[this.entryX][this.entryY] = this.roadmaze[this.outX][this.outY] = this.roadreturn maze}// 初始化迷宫DOMinitDOM(maze) {let oDiv = document.createElement('div')oDiv.style.width = this.width + 'px'oDiv.style.height = this.height + 'px'oDiv.style.display = 'flex'oDiv.className = 'box'oDiv.style.flexWrap = 'wrap'oDiv.style.marginBottom = '20px'for (let i = 0; i < maze.length; i++) {for (let j = 0; j < maze[i].length; j++) {let oSpan = document.createElement('span')oSpan.dataset.index = i + '-' + j + '-' + this.classoSpan.style.width = (this.width / this.col).toFixed(2) + 'px'oSpan.style.height = (this.height / this.row).toFixed(2) + 'px'oSpan.style.background = maze[i][j] === this.wall ? '#185ed1' : '#fff'oDiv.appendChild(oSpan)}}// 删除类名为box的div,演示三种切换,需要删除元素重新渲染let box = document.querySelector('.box')if (box) {// document.body.removeChild(box)document.querySelector('.main').removeChild(box)}// document.body.appendChild(oDiv)document.querySelector('.main').appendChild(oDiv)}// 初始化迷宫initMaze() {// 迷宫数据let maze = this.initData(this.maze)// 初始迷宫DOMthis.initDOM(maze)}// 重新渲染迷宫 改变的格子坐标为(i, j)resetMaze(x, y, type) {// 只有不越界的点才做后续处理if (this.isArea(x, y)) {// 改变this.maze中对应的数据this.maze[x][y] = type// 改变dom中对应的节点颜色let changeSpan = document.querySelector(`span[data-index='${x}-${y}-${this.class}']`)changeSpan.style.background = type === this.wall ? '#185ed1' : '#fff'}}// 渲染迷宫paintMaze() {this.initMaze()let queue = new Queue()queue.push({ x: this.entryX, y: this.entryY + 1 })this.visited[this.entryX][this.entryY + 1] = truewhile (!queue.empty()) {let curPos = queue.pop()for (let i = 0; i < 4; i++) {let newX = curPos.x + this.offset[i][0] * 2 // 两步是 *2let newY = curPos.y + this.offset[i][1] * 2// 坐标没有越界 而且 没有被访问过if (this.isArea(newX, newY) && !this.visited[newX][newY]) {this.resetMaze((newX + curPos.x) / 2, (newY + curPos.y) / 2, this.road) // 打通两个方块之间的墙queue.push({ x: newX, y: newY })this.visited[newX][newY] = true}}}this.hasDown = truereturn this}// 判断坐标是否越界isArea(x, y) {return x > 0 && x < this.row - 1 && y > 0 && y < this.col - 1 || x == this.entryX && y == this.entryY || x == this.outX && y == this.outY}// 迷宫自动求解findPath(iss) {if (!this.hasDown) return alert('请等待迷宫生成后再求解!')let stack = new Stack()stack.push({ x: this.entryX, y: this.entryY }) // 开始while (!stack.empty()) {let curPos = stack.pop()this.findPathVisited[curPos.x][curPos.y] = true // 求解时访问过if (iss > 0) this.findPathReset(curPos.x, curPos.y, '#cd9cf2', iss) // 渲染当前点// 找到出口if (curPos.x === this.outX && curPos.y === this.outY) {this.hasFindPath = true// 绘制解this.findPathReset(curPos.x, curPos.y, 'red', iss) // 绘制出口let prePos = this.path[curPos.x][curPos.y] // 获取上一个点while (prePos != null) {this.findPathReset(prePos.x, prePos.y, 'red', iss) // 渲染上一个点prePos = this.path[prePos.x][prePos.y] // 获取上一个点的上一个点}break;}for (let i = 0; i < 4; i++) {let newX = curPos.x + this.offset[i][0]let newY = curPos.y + this.offset[i][1]if (this.isArea(newX, newY) && this.maze[newX][newY] === this.road && !this.findPathVisited[newX][newY]) {this.path[newX][newY] = { x: curPos.x, y: curPos.y } // 记录新的点以及该点由谁走过来stack.push({ x: newX, y: newY }) }}}if (!this.hasFindPath) return alert('迷宫无解!')}// 渲染迷宫指定位置findPathReset(x, y, color = '#fff', iss) {if (!iss) {this.findPathSpan(x, y, color)return}this.i++setTimeout(() => { // 可视化展示this.findPathSpan(x, y, color)}, this.i * iss);}findPathSpan(x, y, color) {// 只有不越界的点才做后续处理if (this.isArea(x, y)) {// 改变dom中对应的节点颜色let changeSpan = document.querySelector(`span[data-index='${x}-${y}-${this.class}']`)changeSpan.style.background = color}}}let mazeHasDown = new Maze(45, 45, 400, 400)function init() {mazeHasDown.paintMaze()}function calculation(iss) {mazeHasDown.findPath(iss)}</script></html><style>
</style>
js迷宫生成与迷宫求解算法相关推荐
- flutter生成源代码_Flutter随机迷宫生成和解迷宫小游戏功能的源码
此博客旨在帮助大家更好的了解图的遍历算法,通过Flutter移动端平台将图的遍历算法运用在迷宫生成和解迷宫上,让算法变成可视化且可以进行交互,最终做成一个可进行随机迷宫生成和解迷宫的APP小游戏.本人 ...
- Flutter-随机迷宫生成和解迷宫小游戏
此博客旨在帮助大家更好的了解图的遍历算法,通过Flutter移动端平台将图的遍历算法运用在迷宫生成和解迷宫上,让算法变成可视化且可以进行交互,最终做成一个可进行随机迷宫生成和解迷宫的APP小游戏.本人 ...
- 迷宫生成与路径规划算法-Python3.8-附Github代码
MazeProblem 简单介绍一下 该项目不过是一个平平无奇的小作业,基于python3.8开发,目前提供两种迷宫生成算法与三种迷宫求解算法,希望对大家的学习有所帮助. 项目如果有后续的跟进将会声明 ...
- Python——迷宫生成和迷宫破解算法
迷宫绘制函数 def draw(num_rows, num_cols, m):image = np.zeros((num_rows * 10, num_cols * 10), dtype=np.uin ...
- java迷宫队列实现_Creator 迷宫生成: DFS 与 BFS 算法实现
前言: 我的迷宫代码的实现受到 [liuyubobobo] 的影响. liuyubobobo 迷宫的实现: GUI 部分使用 java Swing,编程语言是 Java. **我的迷宫代码实现: ** ...
- HTML+CSS+JavaScript 迷宫生成算法 【建议收藏】
最近发现很多人都在写算法类的博客,今天就献丑了使用HTML,CSS和JavaScript制作一个简单的迷宫生成小代码.证明了自己对一些简单的小算法还是可以驾驭的,基本功没有荒废. 迷宫生成有很多种算法 ...
- c语言 迷宫深度遍历 算法,图的遍历迷宫生成算法浅析
1. 引言 在平常的游戏中,我们常常会碰到随机生成的地图.这里我们就来看看一个简单的随机迷宫是如何生成. 2. 迷宫描述随机生成一个m * n的迷宫,可用一个矩阵maze[m][n]来表示,如图: ...
- [迷宫中的算法实践]迷宫生成算法——Prim算法
普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树.意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点(英语:Vertex (graph theory)), ...
- 迷宫求解 java_迷宫求解算法(java版)
迷宫求解算法一直是算法学习的经典,实现自然也是多种多样,包括动态规划,递归等实现,这里我们使用穷举求解,加深对栈的理解和应用 定义Position类用于存储坐标点 起点坐标为(1,1),终点坐标为(8 ...
最新文章
- 双目视觉惯性里程计的在线初始化与自标定算法
- 【原创】DevExpress控件GridControl中的布局详解
- Java 基础入门随笔(1) JavaSE版——java语言三种技术架构
- “烘焙”ImageNet:自蒸馏下的知识整合
- ABP vNext微服务架构详细教程——架构介绍
- 使用 Jackson 树连接线形状
- Vue计算属性、方法、侦听器
- windows 和linux 同步api对比
- 联想成为中国女排主赞助商,却被自媒体攻击?网友:还好没赞助国足
- 洛谷p3803 FFT入门
- html++留言板增加删除,实现留言板删除留言的具体思路跟操作
- L2行情接口怎么用最高效?
- python从键盘输入一个字符串将小_python如何从键盘获取输入实例
- Dynamic Knowledge Graph Completionwith Jointly Structural and Textual Dependency
- dns udp tcp
- css 设置背景图片模糊效果
- 一行代码卖出570美元, 天价代码的内幕
- CommonJs和Es Module的区别
- 【传感器大赏】3轴模拟加速度传感器
- Win10离线安装.NET Framework 3.5的方法技巧(附离线安装包下载)