【问题标题】:Special Perfect Maze Generation Algorithm特殊完美迷宫生成算法
【发布时间】:2019-11-03 10:01:49
【问题描述】:

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

我处理的是一个充满方块的单元格网格,而不是具有房间和墙壁的标准案例,我可以在其中从某些单元格中删除块:

  • 连接两个给定的单元格(例如,将左上角的单元格连接到左下角的单元格)
  • 为了移除最大块数
  • 每个已移除的块单元都必须使用一种方式相互连接

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

正常情况从这里开始

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

到这里

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

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

##
##

到这里

.#
..

或这里

..
#.

但不是这里(因为右下角的单元格被挡住了)

..
.#

而不是这里(两个单元格没有连接)

.#
#.

而不是这里(迷宫并不完美,细胞由不止一条路径连接)

..
..

这里还有两个 8x8 示例:

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

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

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

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

【问题讨论】:

    标签: algorithm generator depth-first-search maze


    【解决方案1】:

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

    这个想法是您拥有三种类型的细胞:封闭细胞、开放细胞和前沿细胞。

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

    此图显示了开放、封闭和边界细胞。

    +--+--+--+--+--+--+--+--+--+--+
    |** **|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. 将与新打开的单元格相邻的所有已关闭单元格添加到边界数组。

    这样可以保证创建一个从开始到结束只有一条路径的迷宫。

    如果你不想打开图表中的所有单元格,那么修改程序以在从边界中选择完成单元格并打开时停止。

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

    【讨论】:

    • 我认为您误解了 OP 的问题。他明确地不是建造一个迷宫,其中的细胞之间可能有也可能没有墙壁,而是一个迷宫,其中的细胞可能会或可能不会被方块覆盖。因此,您的第 4 步“将选定的边界单元格打开到相邻的开放单元格”是不可能的;如果您“打开”一个单元格,那么您会将其连接到 所有 个相邻的打开单元格,而不仅仅是一个。
    • 我完全有可能误解了 OP 的问题,但我的感觉是,他们试图以一种与您提出的方式有点不同的方式通过网格布置路径这里。具体来说,它们没有单元格之间的墙的概念,因为每个单元格要么是完全空的,要么是完全充满的。
    • @templatetypedef 有趣。我认为我提出的算法仍然有效,只需进行一次修改。如果边界单元与多个开放单元相邻,则不能打开它。哦...除此之外,您可能无法打开完成单元格。嗯。 . .
    • 我相信它可以工作(见我的回答,其中谈到了如何做到这一点),但需要一些额外的处理,因为这样的迷宫不能保证包括右下角和左上角。
    • @JimMischel “哦......除非那样你可能无法打开完成单元格。嗯......”。这正是重点:p
    【解决方案2】:

    看起来使用两步过程生成满足您的标准的迷宫实际上是相当合理的:

    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;
    }
    

    希望这会有所帮助!

    【讨论】:

    • 能否通过从两端进行搜索来提高成功率?首先打开起始单元格和一个相邻的单元格。然后打开完成单元格和一个相邻的单元格。继续交替,直到两者在某个地方相遇。
    • @JimMischel 我敢打赌你可能会做这样的事情。虽然成功率如此之高,但我怀疑在实践中使用这种方法是没有必要的。
    • @templatetypedef 没有必要,当然。但是它会使成功率接近 100%。
    • 我已经在不计算统计数据的情况下设想了您的解决方案。但是,不可能计算 BigO 表示法:最坏的情况 => 它永远不会结束。但是,这是从现在开始的最佳解决方案 ;-)
    • 你说得对,它可以运行无限长的时间,但它仍然可以派生出,比如说,expected 运行时。例如,如果(根据经验似乎就是这种情况)成功的概率是一个常数(例如,大约 70% 或 80%),那么您需要运行此过程超过 k 次才能达到对于某个常数 c,有效的迷宫将是 1 / c^k 形式的东西,对于任何相当大的 k 来说都是极不可能的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-10-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多