JavaScript完成迷宫的自动生成与自动求解

更多算法(语言为JavaScript) 持续更新...

戳我去GitHub看更多算法问题>>>>目录

迷宫生成

戳我去GitHub看详细代码以及生成动画>>>>迷宫生成

下文所涉及的迷宫代码为:

  • 只有一个出口,一个入口
  • 解有且只有一个
  • 方形画布,行与列均为奇数
  • 墙与路径均占一个格子
  • 路径连续

思路

  • 初始化出如下图所示的图形,然后进行图的遍历


    初始化

    通过遍历目前的白色方块,然后按照某种方法连通两个方块(即把两者之间的蓝色方块变成白色)

    // row, col, 迷宫的行数 列数
    // paintProgressTime, 开启可视化展示时的间隔
    // width = 500, height = 500 迷宫的宽高 默认500px

    // 此demo所实现的迷宫为固定出口,固定入口, 解有且只有一个,行与列均为奇数
    // demo中所有坐标相关的变量均代表的是第几行 第几列 从0开始
    constructor(row, col, paintProgressTime, width = 500, height = 500) {
      // 类名
      this.class = Math.random().toFixed(2)
      // Maze行列
      this.row = row
      this.col = col
      // 迷宫的长宽
      this.width = width
      this.height = height
      // 设置路与墙
      this.road = ' '
      this.wall = '#'
      // 入口坐标(1, 0)
      this.entryX = 1
      this.entryY = 0
      // 出口坐标(倒数第二行, 最后一列)
      this.outX = row - 2
      this.outY = col - 1
      // 迷宫数据
      this.maze = []
      // 各节点的遍历情况
      this.visited = []
      // 设置上下左右的偏移坐标值(上右下左)
      this.offset = [[-1, 0], [0, 1], [1, 0], [0, -1]]
      // 可视化展示间隔
      this.paintProgressTime = paintProgressTime
      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) // 初始化访问状态为false
        for (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.road
      maze[this.outX][this.outY] = this.road

      return maze
    }
// 初始化迷宫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.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.class
          oSpan.style.width = (this.width / this.col).toFixed(2) + 'px'
          oSpan.style.height = (this.height / this.row).toFixed(2) + 'px'
          // 加边框
          // oSpan.style.border = '1px solid #ccc'
          // oSpan.style.boxSizing = 'border-box'
          oSpan.style.background = maze[i][j] === this.wall ? '#4facfe' : '#fff'
          oDiv.appendChild(oSpan)
        }
      }
      document.body.appendChild(oDiv)
    }
// 重新渲染迷宫 改变的格子坐标为(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 ? '#4facfe' : '#fff'
      }
    }
// 判断坐标是否越界
    isArea(x, y) {
      return x > 0 && x < this.row - 1 && y > 0 && y < this.col - 1
    }
深度优先遍历_递归
    // 渲染迷宫
    paintMaze() {
      this.initMaze()
      this.go(this.entryX, this.entryY + 1) // 执行遍历 起点是入口右侧的点
    }

    // 遍历
    go(x, y) {
      this.visited[x][y] = true
      for (let i = 0; i < 4; i++) {
        let newX = x + this.offset[i][0] * 2  // 两步是 *2
        let newY = y + this.offset[i][1] * 2
        // console.log(newX, newY);
        // 坐标没有越界 而且 没有被访问过
        if (this.isArea(newX, newY) && !this.visited[newX][newY]) {
          this.resetMazeShow((newX + x) / 2, (newY + y) / 2, this.road) // 打通两个方块之间的墙
          this.go(newX, newY)
        }
      }
    }
深度优先遍历_递归
深度优先遍历_递归生成.gif
深度优先遍历_非递归(通过栈实现)
// 模拟栈
  class Stack {
    constructor() {
      this.stack = []
    }
    push(pos) {
      this.stack.push(pos)
    }
    pop() {
      return this.stack.pop()
    }
    empty() {
      return !this.stack.length
    }
  }
    // 渲染迷宫
    paintMaze() {
      this.initMaze()

      let stack = new Stack()
      stack.push({x: this.entryX, y: this.entryY + 1})
      this.visited[this.entryX][this.entryY + 1] = true

      while (!stack.empty()) {
        let curPos = stack.pop()
        for (let i = 0; i < 4; i++) {
          let newX = curPos.x + this.offset[i][0] * 2  // 两步是 *2
          let newY = curPos.y + this.offset[i][1] * 2
          // 坐标没有越界 而且 没有被访问过
          if (this.isArea(newX, newY) && !this.visited[newX][newY]) {
            // this.resetMazeShow((newX + curPos.x) / 2, (newY + curPos.y) / 2, this.road) // 打通两个方块之间的墙
            stack.push({x: newX, y: newY})
            this.visited[newX][newY] = true
          }
        }
      }
    }
深度优先遍历_非递归

深度优先遍历_非递归生成.gif
广度优先遍历(在深度优先遍历的基础上将栈改为队列即可实现)

只需要将原来的栈的实现改为队列,主要是pop()的更改

// 模拟队列
  class Queue {
    constructor() {
      this.queue = []
    }
    push(pos) {
      this.queue.push(pos)
    }
    pop() {
      return this.queue.shift()
    }
    empty() {
      return !this.queue.length
    }
  }
广度优先遍历
广度优先遍历.gif
随机队列生成迷宫

更改队列的pop()实现即可。随机选一个元素与队列的头交换,实现随机出队一个元素。

// 模拟队列
  class Queue {
    constructor() {
      this.queue = []
    }
    push(pos) {
      this.queue.push(pos)
    }
    pop() {
      // 随机选出一个与队列的头交换
      let randomIndex = Math.floor(Math.random() * this.queue.length)
      let temp = this.queue[0]
      this.queue[0] = this.queue[randomIndex]
      this.queue[randomIndex] = temp

      return this.queue.shift()
    }
    empty() {
      return !this.queue.length
    }
  }
随机队列生成迷宫
随机队列生成.gif
更加随机的迷宫
  // 模拟队列
  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
    }
  }
 // 多解
if (this.isArea(newX, newY)) {
    if (!this.visited[newX][newY]) {
      this.resetMazeShow((newX + curPos.x) / 2, (newY + curPos.y) / 2, this.road) // 打通两个方块之间的墙
      queue.push({x: newX, y: newY})
      this.visited[newX][newY] = true
    } else if (Math.random() < 0.05) {
      this.resetMazeShow((newX + curPos.x) / 2, (newY + curPos.y) / 2, this.road) // 打通两个方块之间的墙
    }
  }
更加随机的迷宫

迷宫求解

戳我去GitHub看详细代码以及求解动画>>>>迷宫求解

以下求解的迷宫为迷宫生成中随机生成的迷宫 代码只得出一个解

思路

求解迷宫(x, y)

  • 尝试向上走,继续求解迷宫
  • 尝试向右走,继续求解迷宫
  • 尝试向下走,继续求解迷宫
  • 尝试向左走,继续求解迷宫

求解

新增解迷宫需要的属性

      // 求解迷宫时各节点的遍历情况
      this.findPathVisited = []
      // 迷宫是否生成完成
      this.hasDown = false
    // 迷宫自动求解
    findPath() {
      if (!this.hasDown) throw new Error('请等待迷宫生成后再求解!')
      if (!this.findPathGo(this.entryX, this.entryY)) throw new Error('迷宫无解!')
    }
    // 渲染迷宫指定位置
    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
      }
    }
    findPathReset(x, y, color = '#cd9cf2') {
      if (!this.paintProgressTime) {
        this.findPathSpan(x, y, color)
        return
      }
      this.i++ // 可视化展示
      setTimeout(() => { // 可视化展示
        this.findPathSpan(x, y, color)
      }, this.i * this.paintProgressTime);
    }
深度优先递归+回溯
    // 递归遍历 渲染迷宫求解路径
    findPathGo(x, y) {
      if (this.isArea(x, y)) {
        this.findPathVisited[x][y] = true // 求解时访问过
        this.findPathReset(x, y)  // 渲染当前点
        if (x == this.outX && y == this.outY) return true // 已找到出口 递归终止
        
        // 遍历该点的四个方向是否可继续遍历
        for (let i = 0; i < 4; i++) {
          let newX = x + this.offset[i][0]
          let newY = y + this.offset[i][1]
          if (this.isArea(newX, newY) && this.maze[newX][newY] === this.road && !this.findPathVisited[newX][newY]) {
            if (this.findPathGo(newX, newY)) return true
          }
        }
        // 回溯 遍历完四个方向的点均没有找到出口 则表示该点不是解的路径上的点 变回路的颜色
        this.findPathReset(x, y, '#fff')
        return false
      }
    }
深度优先递归+回溯 自动解迷宫
深度优先_递归+回溯.gif
深度优先非递归

新增需要属性,由于非递归无法像递归一样轻易回溯,所以通过记录每次遍历到的节点是从哪个节点过来的可以再找到出口时,找到出口的前一个元素,依次找到入口,即可完成路径的寻找。

      // 迷宫是否有解
      this.hasFindPath = false
      // 存储迷宫某点的上一点位置
      this.path = []
   // 迷宫自动求解
    findPath() {
      if (!this.hasDown) throw new Error('请等待迷宫生成后再求解!')
      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 // 求解时访问过
        this.findPathReset(curPos.x, curPos.y)  // 渲染当前点
        // 找到出口
        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) throw new Error('迷宫无解!')
    }
深度优先非递归
深度优先_非递归.gif
广度优先

与深度优先遍历非递归逻辑完全相同,只需要将模拟的栈替换成模拟的队列即可

  // 模拟队列  -- 自动寻解
  class QueueFindPath {
    constructor() {
      this.queue = []
    }
    push(pos) {
      this.queue.push(pos)
    }
    pop() {
      return this.queue.shift()
    }
    empty() {
      return !this.queue.length
    }
  }
// 迷宫自动求解
    findPath() {
      if (!this.hasDown) throw new Error('请等待迷宫生成后再求解!')
      let queueFindPath = new QueueFindPath()
      queueFindPath.push({x: this.entryX, y: this.entryY}) // 入栈
      while (!queueFindPath.empty()) {
        let curPos = queueFindPath.pop()
        this.findPathVisited[curPos.x][curPos.y] = true // 求解时访问过
        this.findPathReset(curPos.x, curPos.y)  // 渲染当前点
        // 找到出口
        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} // 记录新的点以及该点由谁走过来
            queueFindPath.push({x: newX, y: newY}) // 入栈
          }
        }
      }
      if(!this.hasFindPath) throw new Error('迷宫无解!')
    }
广度优先
广度优先.gif

更多算法(语言为JavaScript) 持续更新...

戳我去GitHub看更多算法问题>>>>目录

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容