特殊的完美迷宫生成算法

时间:2019-06-20 14:39:54

标签: algorithm generator depth-first-search maze

我正在尝试创建一个特殊的完美迷宫生成器。

我要处理的不是由房间和墙壁组成的标准案例,而是要处理由单元格填充的网格,在那里我可以从某些单元格中删除模块:

  • 连接两个给定的单元格(例如,将左上角的单元格连接到左下角的单元格)
  • 为了移除最多的方块
  • 每个删除的块单元格必须可以通过一种方法彼此连接

我使用DFS算法来挖掘路径迷宫,但找不到确保两个给定单元已连接的方法。

正常情况从这里开始

+-+-+
| | |
+-+-+
| | |
+-+-+

到这里

+-+-+
| | |
+ + +
|   |
+-+-+

就我而言,我试图将左上角的单元格连接到右下角的单元格:

##
##

到这里

.#
..

或这里

..
#.

但不在这里(因为右下角的单元格被阻止了)

..
.#

而不是这里(两个单元未连接)

.#
#.

而不是这里(迷宫不是完美的,细胞通过多条路径连接)

..
..

还有两个8x8的示例:

好人(完美的迷宫,并且从左上角的单元格到右下角的单元格都有一条路径):

..#.....
.#.#.##.
.......#
.#.#.##.
.##...#.
..#.#...
.##.#.#.
...###..

一个错误(完美的迷宫,但是从左上角的单元格到右下角的单元格没有路径):

...#....
.##..#.#
....##..
#.#...##
#..##...
..#..#.#
#...#...
##.###.#

Some nice 1000x1000 solved generated maze

2 个答案:

答案 0 :(得分:3)

使用两步过程来生成符合您条件的迷宫看起来是相当合理的:

  1. 生成一个随机迷宫,而不考虑是否有可能从左上角到达右下角。

  2. 重复步骤(1),直到找到右下角的路径。

我已经使用两种策略对此进行了编码,一种基于随机深度优先搜索,另一种基于随机宽度优先搜索。在大小为100×100的网格上进行的随机深度优先搜索会生成迷宫,其中迷离右下角可在82%的时间内从左上角到达。通过随机的广度优先搜索,在100×100网格上的成功率约为70%。因此,该策略确实确实可行;您将平均需要使用DFS生成约1.2迷宫和使用BFS生成约1.4迷宫,然后才能找到有效的迷宫。

我用来生成无循环迷宫的机制是基于对常规BFS和DFS想法的概括。在这两种算法中,我们选择的位置是(1)我们尚未访问过但(2)与我们所拥有的位置相邻,然后将新位置添加到其中,并将先前的位置作为其父级。即,新添加的位置最终恰好与先前访问的单元之一相邻。我通过使用以下规则适应了这个想法:

  

如果一个完整的单元格与一个以上的空单元格相邻,请勿将其转换为一个空单元格。

此规则可确保我们永远不会得到任何循环(如果某个物体与两个或多个空位置相邻并且我们将其清空了,我们将通过到达第一个位置,然后移至新清空的正方形,然后再创建一个循环),移至第二个位置。

以下是使用DFS方法生成的30×30迷宫样本:

.#.........#...#.#....#.#..##.
.#.#.#.#.#.#.#.....##....#....
..#...#..#.#.##.#.#.####.#.#.#
#..#.##.##.#...#..#......#.#..
.#..#...#..####..#.#.####..##.
...#..##..#.....#..#....##..#.
.##.#.#.#...####..#.###...#.#.
..#.#.#.###.#....#..#.#.#..##.
#.#...#....#..#.###....###....
...#.###.#.#.#...#..##..#..#.#
.#....#..#.#.#.#.#.#..#..#.#..
..####..#..###.#.#...###..#.#.
.#.....#.#.....#.########...#.
#..#.##..#######.....#####.##.
..##...#........####..###..#..
.#..##..####.#.#...##..#..#..#
..#.#.#.#....#.###...#...#..#.
.#....#.#.####....#.##.#.#.#..
.#.#.#..#.#...#.#...#..#.#...#
.#..##.#..#.#..#.##..##..###..
.#.#...##....#....#.#...#...#.
...#.##...##.####..#..##..##..
#.#..#.#.#.......#..#...#..#.#
..#.#.....#.####..#...##..##..
##..###.#..#....#.#.#....#..#.
...#...#..##.#.#...#####...#..
.###.#.#.#...#.#.#..#...#.#..#
.#...#.##..##..###.##.#.#.#.##
.#.###..#.##.#....#...#.##...#
......#.......#.#...#.#....#..

这是使用BFS生成的30×30迷宫样本:

.#..#..#...#......#..##.#.....
..#.#.#.#.#..#.##...#....#.#.#
#...#.......###.####..##...#.#
.#.#..#.#.##.#.......#.#.#..#.
.....#..#......#.#.#.#..#..##.
#.#.#.###.#.##..#.#....#.#....
..##.....##..#.##...##.#...#.#
#....#.#...#..##.##...#.#.##..
.#.#..##.##..##...#.#...##...#
....#...#..#....#.#.#.##..##..
#.##..#.##.##.##...#..#..##..#
....#.##.#..#...#.####.#...#..
.#.##......#..##.#.#.....#..#.
#....#.#.#..#........#.#.#.##.
.#.###..#..#.#.##.#.#...####..
.#.#...#.#...#..#..###.#.#...#
....##.#.##.#..#.####.....#.#.
.#.#.......###.#.#.#.##.##....
#..#.#.#.##.#.#........###.#.#
.#..#..#........##.#.####..#..
...#.#.#.#.#.##.#.###..#.##..#
#.#..#.##..#.#.#...#.#.....#..
....#...##.#.....#.....##.#..#
#.#.#.##...#.#.#.#.#.##..#.##.
...#..#..##..#..#...#..#.#....
#.#.#.##...#.##..##...#....#.#
..#..#...##....##...#...#.##..
#...#..#...#.#..#.#.#.#..#...#
..#..##..##..#.#..#..#.##.##..
#.#.#...#...#...#..#........#.

有趣的是,这是我用来生成这些数字和迷宫的代码。首先,DFS代码:

#include <iostream>
#include <algorithm>
#include <set>
#include <vector>
#include <string>
#include <random>
using namespace std;

/* World Dimensions */
const size_t kNumRows = 30;
const size_t kNumCols = 30;

/* Location. */
using Location = pair<size_t, size_t>; // (row, col)

/* Adds the given point to the frontier, assuming it's legal to do so. */
void updateFrontier(const Location& loc, vector<string>& maze, vector<Location>& frontier,
                    set<Location>& usedFrontier) {
  /* Make sure we're in bounds. */
  if (loc.first >= maze.size() || loc.second >= maze[0].size()) return;

  /* Make sure this is still a wall. */
  if (maze[loc.first][loc.second] != '#') return;

  /* Make sure we haven't added this before. */
  if (usedFrontier.count(loc)) return;

  /* All good! Add it in. */
  frontier.push_back(loc);
  usedFrontier.insert(loc);
}

/* Given a location, adds that location to the maze and expands the frontier. */
void expandAt(const Location& loc, vector<string>& maze, vector<Location>& frontier,
              set<Location>& usedFrontier) {
  /* Mark the location as in use. */
  maze[loc.first][loc.second] = '.';

  /* Handle each neighbor. */
  updateFrontier(Location(loc.first, loc.second + 1), maze, frontier, usedFrontier);
  updateFrontier(Location(loc.first, loc.second - 1), maze, frontier, usedFrontier);
  updateFrontier(Location(loc.first + 1, loc.second), maze, frontier, usedFrontier);
  updateFrontier(Location(loc.first - 1, loc.second), maze, frontier, usedFrontier);
}

/* Chooses and removes a random element of the frontier. */
Location sampleFrom(vector<Location>& frontier, mt19937& generator) {
  uniform_int_distribution<size_t> dist(0, frontier.size() - 1);

  /* Pick our spot. */
  size_t index = dist(generator);

  /* Move it to the end and remove it. */
  swap(frontier[index], frontier.back());

  auto result = frontier.back();
  frontier.pop_back();
  return result;
}

/* Returns whether a location is empty. */
bool isEmpty(const Location& loc, const vector<string>& maze) {
  return loc.first < maze.size() && loc.second < maze[0].size() && maze[loc.first][loc.second] == '.';
}

/* Counts the number of empty neighbors of a given location. */
size_t neighborsOf(const Location& loc, const vector<string>& maze) {
  return !!isEmpty(Location(loc.first - 1, loc.second), maze) +
         !!isEmpty(Location(loc.first + 1, loc.second), maze) +
         !!isEmpty(Location(loc.first, loc.second - 1), maze) +
         !!isEmpty(Location(loc.first, loc.second + 1), maze);
}

/* Returns whether a location is in bounds. */
bool inBounds(const Location& loc, const vector<string>& world) {
  return loc.first < world.size() && loc.second < world[0].size();
}

/* Runs a recursive DFS to fill in the maze. */
void dfsFrom(const Location& loc, vector<string>& world, mt19937& generator) {
  /* Base cases: out of bounds? Been here before? Adjacent to too many existing cells? */
  if (!inBounds(loc, world) || world[loc.first][loc.second] == '.' ||
      neighborsOf(loc, world) > 1) return;

  /* All next places. */
  vector<Location> next = {
    { loc.first - 1, loc.second },
    { loc.first + 1, loc.second },
    { loc.first, loc.second - 1 },
    { loc.first, loc.second + 1 }
  };
  shuffle(next.begin(), next.end(), generator);

  /* Mark us as filled. */
  world[loc.first][loc.second] = '.';

  /* Explore! */
  for (const Location& nextStep: next) {
    dfsFrom(nextStep, world, generator);
  }
}

/* Generates a random maze. */
vector<string> generateMaze(size_t numRows, size_t numCols, mt19937& generator) {
  /* Create the maze. */
  vector<string> result(numRows, string(numCols, '#'));

  /* Build the maze! */
  dfsFrom(Location(0, 0), result, generator);

  return result;
}

int main() {
  random_device rd;
  mt19937 generator(rd());

  /* Run some trials. */
  size_t numTrials = 0;
  size_t numSuccesses = 0;

  for (size_t i = 0; i < 10000; i++) {
    numTrials++;

    auto world = generateMaze(kNumRows, kNumCols, generator);

    /* Can we get to the bottom? */
    if (world[kNumRows - 1][kNumCols - 1] == '.') {
      numSuccesses++;

      /* Print the first maze that works. */
      if (numSuccesses == 1) {
        for (const auto& row: world) {
          cout << row << endl;
        }
        cout << endl;
      }
    }
  }

  cout << "Trials:    " << numTrials << endl;
  cout << "Successes: " << numSuccesses << endl;
  cout << "Percent:   " << (100.0 * numSuccesses) / numTrials << "%" << endl;


  cout << endl;
  return 0;
}

接下来,BFS代码:

#include <iostream>
#include <algorithm>
#include <set>
#include <vector>
#include <string>
#include <random>
using namespace std;

/* World Dimensions */
const size_t kNumRows = 30;
const size_t kNumCols = 30;

/* Location. */
using Location = pair<size_t, size_t>; // (row, col)

/* Adds the given point to the frontier, assuming it's legal to do so. */
void updateFrontier(const Location& loc, vector<string>& maze, vector<Location>& frontier,
                    set<Location>& usedFrontier) {
  /* Make sure we're in bounds. */
  if (loc.first >= maze.size() || loc.second >= maze[0].size()) return;

  /* Make sure this is still a wall. */
  if (maze[loc.first][loc.second] != '#') return;

  /* Make sure we haven't added this before. */
  if (usedFrontier.count(loc)) return;

  /* All good! Add it in. */
  frontier.push_back(loc);
  usedFrontier.insert(loc);
}

/* Given a location, adds that location to the maze and expands the frontier. */
void expandAt(const Location& loc, vector<string>& maze, vector<Location>& frontier,
              set<Location>& usedFrontier) {
  /* Mark the location as in use. */
  maze[loc.first][loc.second] = '.';

  /* Handle each neighbor. */
  updateFrontier(Location(loc.first, loc.second + 1), maze, frontier, usedFrontier);
  updateFrontier(Location(loc.first, loc.second - 1), maze, frontier, usedFrontier);
  updateFrontier(Location(loc.first + 1, loc.second), maze, frontier, usedFrontier);
  updateFrontier(Location(loc.first - 1, loc.second), maze, frontier, usedFrontier);
}

/* Chooses and removes a random element of the frontier. */
Location sampleFrom(vector<Location>& frontier, mt19937& generator) {
  uniform_int_distribution<size_t> dist(0, frontier.size() - 1);

  /* Pick our spot. */
  size_t index = dist(generator);

  /* Move it to the end and remove it. */
  swap(frontier[index], frontier.back());

  auto result = frontier.back();
  frontier.pop_back();
  return result;
}

/* Returns whether a location is empty. */
bool isEmpty(const Location& loc, const vector<string>& maze) {
  return loc.first < maze.size() && loc.second < maze[0].size() && maze[loc.first][loc.second] == '.';
}

/* Counts the number of empty neighbors of a given location. */
size_t neighborsOf(const Location& loc, const vector<string>& maze) {
  return !!isEmpty(Location(loc.first - 1, loc.second), maze) +
         !!isEmpty(Location(loc.first + 1, loc.second), maze) +
         !!isEmpty(Location(loc.first, loc.second - 1), maze) +
         !!isEmpty(Location(loc.first, loc.second + 1), maze);
}

/* Generates a random maze. */
vector<string> generateMaze(size_t numRows, size_t numCols, mt19937& generator) {
  /* Create the maze. */
  vector<string> result(numRows, string(numCols, '#'));

  /* Worklist of free locations. */
  vector<Location> frontier;

  /* Set of used frontier sites. */
  set<Location> usedFrontier;

  /* Seed the starting location. */
  expandAt(Location(0, 0), result, frontier, usedFrontier);

  /* Loop until there's nothing left to expand. */
  while (!frontier.empty()) {
    /* Select a random frontier location to expand at. */
    Location next = sampleFrom(frontier, generator);

    /* If this spot has exactly one used neighbor, add it. */
    if (neighborsOf(next, result) == 1) {   
      expandAt(next, result, frontier, usedFrontier);
    }
  }

  return result;
}

int main() {
  random_device rd;
  mt19937 generator(rd());

  /* Run some trials. */
  size_t numTrials = 0;
  size_t numSuccesses = 0;

  for (size_t i = 0; i < 10000; i++) {
    numTrials++;

    auto world = generateMaze(kNumRows, kNumCols, generator);

    /* Can we get to the bottom? */
    if (world[kNumRows - 1][kNumCols - 1] == '.') {
      numSuccesses++;

      /* Print the first maze that works. */
      if (numSuccesses == 1) {
        for (const auto& row: world) {
          cout << row << endl;
        }
        cout << endl;
      }
    }
  }

  cout << "Trials:    " << numTrials << endl;
  cout << "Successes: " << numSuccesses << endl;
  cout << "Percent:   " << (100.0 * numSuccesses) / numTrials << "%" << endl;


  cout << endl;
  return 0;
}

希望这会有所帮助!

答案 1 :(得分:0)

下面,我描述一种构建完美迷宫的简单方法。

这个想法是,您拥有三种类型的单元:封闭单元,开放单元和边界单元。

  • 封闭的单元格是仍然被阻止的单元格:从起始单元格到该单元格没有路径。
  • 如果存在从起始单元格到单元格的路径,则该单元格是开放的。
  • 边界单元是与开放单元相邻的封闭单元。

此图显示了开放式,封闭式和边界单元。

+--+--+--+--+--+--+--+--+--+--+
|** **|FF|  |  |  |  |  |  |  |
+--+  +--+--+--+--+--+--+--+--+
|FF|**|FF|  |  |  |  |  |  |  |
+--+  +--+--+--+--+--+--+--+--+
|** **|FF|  |  |  |  |  |  |  |
+  +--+--+--+--+--+--+--+--+--+
|**|FF|FF|FF|  |  |  |  |  |  |
+  +--+--+--+--+--+--+--+--+--+
|** ** ** **|FF|  |  |  |  |  |
+--+--+--+  +--+--+--+--+--+--+
|FF|FF|FF|**|FF|  |  |  |  |  |
+--+--+--+--+--+--+--+--+--+--+
|  |  |FF|FF|  |  |  |  |  |  |
+--+--+--+--+--+--+--+--+--+--+
|  |  |  |  |  |  |  |  |  |  |
+--+--+--+--+--+--+--+--+--+--+
|  |  |  |  |  |  |  |  |  |  |
+--+--+--+--+--+--+--+--+--+--+
|  |  |  |  |  |  |  |  |  |  |
+--+--+--+--+--+--+--+--+--+--+

其中带有“ **”的单元格处于打开状态。其中带有“ FF”的单元格是边界单元格。空白单元格是封闭的单元格。

这个想法是从网格中每个封闭的单元格开始。

然后,创建一个最初为空的单元格列表。那是你的边境。

打开起始单元格和相邻单元格之一,并将与这两个单元格相邻的所有单元格添加到边界。因此,如果左上角是起始单元格,则前两行是

+--+--+--+--+--+--+--+--+--+--+
|** **|  |  |  |  |  |  |  |  |
+--+--+--+--+--+--+--+--+--+--+
|  |  |  |  |  |  |  |  |  |  |

您的边界数组包含{[0,2],[1,0],[1,1]}

现在,执行以下循环直到边界数组为空:

  1. 从边界数组中随机选择一个单元格。
  2. 将该单元格与边界数组中的最后一个单元格交换。
  3. 从边界数组中删除最后一个单元格。
  4. 将所选边界单元打开到相邻的开放单元中。
  5. 将与新打开的单元格相邻的所有闭合单元格添加到边界数组。

保证创建一个从头到尾都有一条路径的迷宫。

如果您不想打开图中的所有单元格,请修改程序以在从边界中选择并打开Finish单元格时停止。

时间复杂度为O(高度*宽度)。我记得,边界数组将达到的最大大小为(2*height*width)/3。在实践中,我从未见过它这么大。