迷宫问题是一个很经典的问题,本文记叙迷宫的生成和求解(深度优先),完整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迷宫生成与迷宫求解算法相关推荐

  1. flutter生成源代码_Flutter随机迷宫生成和解迷宫小游戏功能的源码

    此博客旨在帮助大家更好的了解图的遍历算法,通过Flutter移动端平台将图的遍历算法运用在迷宫生成和解迷宫上,让算法变成可视化且可以进行交互,最终做成一个可进行随机迷宫生成和解迷宫的APP小游戏.本人 ...

  2. Flutter-随机迷宫生成和解迷宫小游戏

    此博客旨在帮助大家更好的了解图的遍历算法,通过Flutter移动端平台将图的遍历算法运用在迷宫生成和解迷宫上,让算法变成可视化且可以进行交互,最终做成一个可进行随机迷宫生成和解迷宫的APP小游戏.本人 ...

  3. 迷宫生成与路径规划算法-Python3.8-附Github代码

    MazeProblem 简单介绍一下 该项目不过是一个平平无奇的小作业,基于python3.8开发,目前提供两种迷宫生成算法与三种迷宫求解算法,希望对大家的学习有所帮助. 项目如果有后续的跟进将会声明 ...

  4. Python——迷宫生成和迷宫破解算法

    迷宫绘制函数 def draw(num_rows, num_cols, m):image = np.zeros((num_rows * 10, num_cols * 10), dtype=np.uin ...

  5. java迷宫队列实现_Creator 迷宫生成: DFS 与 BFS 算法实现

    前言: 我的迷宫代码的实现受到 [liuyubobobo] 的影响. liuyubobobo 迷宫的实现: GUI 部分使用 java Swing,编程语言是 Java. **我的迷宫代码实现: ** ...

  6. HTML+CSS+JavaScript 迷宫生成算法 【建议收藏】

    最近发现很多人都在写算法类的博客,今天就献丑了使用HTML,CSS和JavaScript制作一个简单的迷宫生成小代码.证明了自己对一些简单的小算法还是可以驾驭的,基本功没有荒废. 迷宫生成有很多种算法 ...

  7. c语言 迷宫深度遍历 算法,图的遍历迷宫生成算法浅析

    1. 引言 在平常的游戏中,我们常常会碰到随机生成的地图.这里我们就来看看一个简单的随机迷宫是如何生成. 2. 迷宫描述随机生成一个m * n的迷宫,可用一个矩阵maze[m][n]来表示,如图:   ...

  8. [迷宫中的算法实践]迷宫生成算法——Prim算法

    普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树.意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点(英语:Vertex (graph theory)), ...

  9. 迷宫求解 java_迷宫求解算法(java版)

    迷宫求解算法一直是算法学习的经典,实现自然也是多种多样,包括动态规划,递归等实现,这里我们使用穷举求解,加深对栈的理解和应用 定义Position类用于存储坐标点 起点坐标为(1,1),终点坐标为(8 ...

最新文章

  1. 双目视觉惯性里程计的在线初始化与自标定算法
  2. 【原创】DevExpress控件GridControl中的布局详解
  3. Java 基础入门随笔(1) JavaSE版——java语言三种技术架构
  4. “烘焙”ImageNet:自蒸馏下的知识整合
  5. ABP vNext微服务架构详细教程——架构介绍
  6. 使用 Jackson 树连接线形状
  7. Vue计算属性、方法、侦听器
  8. windows 和linux 同步api对比
  9. 联想成为中国女排主赞助商,却被自媒体攻击?网友:还好没赞助国足
  10. 洛谷p3803 FFT入门
  11. html++留言板增加删除,实现留言板删除留言的具体思路跟操作
  12. L2行情接口怎么用最高效?
  13. python从键盘输入一个字符串将小_python如何从键盘获取输入实例
  14. Dynamic Knowledge Graph Completionwith Jointly Structural and Textual Dependency
  15. dns udp tcp
  16. css 设置背景图片模糊效果
  17. 一行代码卖出570美元, 天价代码的内幕
  18. CommonJs和Es Module的区别
  19. 【传感器大赏】3轴模拟加速度传感器
  20. Win10离线安装.NET Framework 3.5的方法技巧(附离线安装包下载)

热门文章

  1. html中无语义的标签,HTML语义类标签都有哪些?
  2. 找不到 blog.csdn.net 的服务器 DNS 地址
  3. C语言-单词长度统计
  4. 5G 网络的会话性管理上下文对比介绍
  5. 瑞吉外卖项目剩余功能补充
  6. 数据挖掘课程笔记--关联分析
  7. MATLAB时频工具箱函数说明(包含工具箱的下载,安装,使用)
  8. HIVE获取时间函数, regexp_extract正则提取用法
  9. ISP算法:gamma矫正
  10. 金融信创虽风正时济,应对挑战该如何乘风破浪(一)