使用Prim算法实现随机生成的迷宫

时间:2015-04-20 05:13:00

标签: algorithm graph-theory maze minimum-spanning-tree

我正在尝试使用Prim算法实现随机生成的迷宫。

我希望我的迷宫看起来像这样: enter image description here

然而,我从我的程序生成的迷宫看起来像这样:

enter image description here

我目前正在坚持正确实施以粗体突出显示的步骤:

  
      
  1. 从一个满墙的网格开始。
  2.   
  3. 选择一个单元格,将其标记为迷宫的一部分。将单元格的墙添加到墙列表中。
  4.   
  5. 虽然列表中有墙:      
        
    • ** 1。从列表中选择一个随机墙。如果对面的细胞还没在迷宫中:      
          
        1.   
        2. 将墙壁作为通道,并将对面的单元格标记为迷宫的一部分。**
        3.   
      •   
        1.   
        2. 将单元格的相邻墙添加到墙列表中。
        3.   
      •   
    •   
      1.   
      2. 从列表中删除墙。
      3.   
    •   
  6.   

this article on maze generation.

如何确定单元格是否是墙列表的有效候选者?我想改变我的算法,以便产生正确的迷宫。任何有助于我解决问题的想法都将不胜感激。

7 个答案:

答案 0 :(得分:8)

维基百科文章中的描述确实值得改进。

本文的第一个令人困惑的部分是,随机化Prim算法的描述没有详细说明算法使用的假定数据结构。因此,像“对立细胞”这样的短语变得令人困惑。

基本上有两种主要方法“迷宫发生器程序员”可以选择:

  1. 细胞有4个邻居的墙壁或通道。有关墙/通道的信息将被存储和处理。
  2. 单元格可以是阻止(墙壁)或通道,而不存储任何额外的连接信息。
  3. 根据读者在阅读算法描述时所考虑的模型(1)或(2),他们要么理解要么不理解。

    我,我个人更喜欢将细胞用作墙壁或通道,而不是摆弄专用的通道/墙壁信息。

    然后,“边境”斑块距离通道的距离为2(而不是1)。选择边界斑块列表中的随机边界斑块,并通过使边界斑块和相邻通道之间的细胞成为通道,连接到随机相邻通道(距离2)。

    这里是我的F#实现方式:

    let rng = new System.Random()
    type Cell = | Blocked | Passage
    type Maze = 
        { 
            Grid : Cell[,]
            Width : int
            Height : int
        }
    
    let initMaze dx dy = 
        let six,siy = (1,1)
        let eix,eiy = (dx-2,dy-2)
        { 
            Grid = Array2D.init dx dy 
                (fun _ _ -> Blocked
                ) 
            Width = dx
            Height = dy
        }
    
    let generate (maze : Maze) : Maze =
        let isLegal (x,y) =
            x>0 && x < maze.Width-1 && y>0 && y<maze.Height-1
        let frontier (x,y) =
            [x-2,y;x+2,y; x,y-2; x, y+2]
            |> List.filter (fun (x,y) -> isLegal (x,y) && maze.Grid.[x,y] = Blocked)
        let neighbor (x,y) =
            [x-2,y;x+2,y; x,y-2; x, y+2]
            |> List.filter (fun (x,y) -> isLegal (x,y) && maze.Grid.[x,y] = Passage)
        let randomCell () = rng.Next(maze.Width),rng.Next(maze.Height)
        let removeAt index (lst : (int * int) list) : (int * int) list =
            let x,y = lst.[index]
            lst |> List.filter (fun (a,b) -> not (a = x && b = y) )
        let between p1 p2 =
            let x = 
                match (fst p2 - fst p1) with
                | 0 -> fst p1
                | 2 -> 1 + fst p1
                | -2 -> -1 + fst p1
                | _ -> failwith "Invalid arguments for between()"
            let y = 
                match (snd p2 - snd p1) with
                | 0 -> snd p1
                | 2 -> 1 + snd p1
                | -2 -> -1 + snd p1
                | _ -> failwith "Invalid arguments for between()"
            (x,y)
        let connectRandomNeighbor (x,y) =
            let neighbors = neighbor (x,y)
            let pickedIndex = rng.Next(neighbors.Length)
            let xn,yn = neighbors.[pickedIndex]
            let xb,yb = between (x,y) (xn,yn)
            maze.Grid.[xb,yb] <- Passage
            ()
        let rec extend front =
            match front with
            | [] -> ()
            | _ ->
                let pickedIndex = rng.Next(front.Length)
                let xf,yf = front.[pickedIndex]
                maze.Grid.[xf,yf] <- Passage
                connectRandomNeighbor (xf,yf)
                extend ((front |> removeAt pickedIndex) @ frontier (xf,yf))
    
        let x,y = randomCell()
        maze.Grid.[x,y] <- Passage
        extend (frontier (x,y))
    
        maze
    
    
    let show maze =
        printfn "%A" maze
        maze.Grid |> Array2D.iteri 
            (fun y x cell ->
                if x = 0 && y > 0 then 
                    printfn "|"
                let c = 
                    match cell with
                    | Blocked -> "X"
                    | Passage -> " "
                printf "%s" c
            )
        maze
    
    let render maze =
        let cellWidth = 10;
        let cellHeight = 10;
        let pw = maze.Width * cellWidth
        let ph = maze.Height * cellHeight
        let passageBrush = System.Drawing.Brushes.White
        let wallBrush = System.Drawing.Brushes.Black
        let bmp = new System.Drawing.Bitmap(pw,ph)
        let g = System.Drawing.Graphics.FromImage(bmp);
        maze.Grid
        |> Array2D.iteri 
            (fun y x cell ->
                let brush = 
                    match cell with
                    | Passage -> passageBrush
                    | Blocked -> wallBrush
                g.FillRectangle(brush,x*cellWidth,y*cellHeight,cellWidth,cellHeight)
            )
        g.Flush()
        bmp.Save("""E:\temp\maze.bmp""")
    
    initMaze 50 50 |> generate |> show |> render
    

    结果迷宫然后可以看起来像这样:

    enter image description here

    这里尝试用维基百科“算法”风格来描述我的解决方案:

      
        
    1. 网格由2维单元格组成。
    2.   
    3. Cell有2种状态:Blocked or Passage。
    4.   
    5. 从状态为Blocked的单元格中填充单元格。
    6.   
    7. 选择一个随机单元格,将其设置为状态Passage并计算其边界单元格。   Cell的边界单元是一个单元,其状态为Blocked且位于网格内。
    8.   
    9. 虽然前沿单元格列表不为空:      
          
      1. 从边界单元列表中选择一个随机前沿单元格。
      2.   
      3. 让邻居(frontierCell)=状态段落中距离2的所有单元格。   选择一个随机邻居并通过将其间的单元格设置为状态Passage来将边界单元与邻居连接。   计算所选前沿单元格的边界单元格并将它们添加到前沿列表中。   从边界单元列表中删除选定的前沿单元格。
      4.   
    10.   

答案 1 :(得分:4)

Prim算法的简单Java实现:

import java.util.LinkedList;
import java.util.Random;

public class Maze {
    public static final char PASSAGE_CHAR = ' ';
    public static final char WALL_CHAR = '▓';
    public static final boolean WALL    = false;
    public static final boolean PASSAGE = !WALL;

    private final boolean map[][];
    private final int width;
    private final int height;

    public Maze( final int width, final int height ){
        this.width = width;
        this.height = height;
        this.map = new boolean[width][height];

        final LinkedList<int[]> frontiers = new LinkedList<>();
        final Random random = new Random();
        int x = random.nextInt(width);
        int y = random.nextInt(height);
        frontiers.add(new int[]{x,y,x,y});

        while ( !frontiers.isEmpty() ){
            final int[] f = frontiers.remove( random.nextInt( frontiers.size() ) );
            x = f[2];
            y = f[3];
            if ( map[x][y] == WALL )
            {
                map[f[0]][f[1]] = map[x][y] = PASSAGE;
                if ( x >= 2 && map[x-2][y] == WALL )
                    frontiers.add( new int[]{x-1,y,x-2,y} );
                if ( y >= 2 && map[x][y-2] == WALL )
                    frontiers.add( new int[]{x,y-1,x,y-2} );
                if ( x < width-2 && map[x+2][y] == WALL )
                    frontiers.add( new int[]{x+1,y,x+2,y} );
                if ( y < height-2 && map[x][y+2] == WALL )
                    frontiers.add( new int[]{x,y+1,x,y+2} );
            }
        }
    }

    @Override
    public String toString(){
        final StringBuffer b = new StringBuffer();
        for ( int x = 0; x < width + 2; x++ )
            b.append( WALL_CHAR );
        b.append( '\n' );
        for ( int y = 0; y < height; y++ ){
            b.append( WALL_CHAR );
            for ( int x = 0; x < width; x++ )
                b.append( map[x][y] == WALL ? WALL_CHAR : PASSAGE_CHAR );
            b.append( WALL_CHAR );
            b.append( '\n' );
        }
        for ( int x = 0; x < width + 2; x++ )
            b.append( WALL_CHAR );
        b.append( '\n' );
        return b.toString();
    }
}

new Maze(20,20).toString()的示例输出是:

▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓   ▓     ▓       ▓ ▓▓
▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓ ▓▓▓ ▓▓
▓     ▓ ▓ ▓ ▓   ▓ ▓ ▓▓
▓ ▓▓▓▓▓ ▓ ▓ ▓▓▓ ▓ ▓ ▓▓
▓   ▓ ▓ ▓   ▓       ▓▓
▓ ▓ ▓ ▓ ▓ ▓▓▓▓▓▓▓ ▓ ▓▓
▓ ▓ ▓ ▓ ▓   ▓ ▓   ▓ ▓▓
▓ ▓▓▓ ▓ ▓▓▓ ▓ ▓ ▓▓▓▓▓▓
▓   ▓     ▓ ▓ ▓   ▓ ▓▓
▓ ▓▓▓▓▓ ▓▓▓ ▓ ▓ ▓▓▓ ▓▓
▓   ▓   ▓           ▓▓
▓ ▓ ▓ ▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓▓
▓ ▓   ▓   ▓       ▓ ▓▓
▓ ▓▓▓▓▓▓▓ ▓ ▓▓▓▓▓ ▓ ▓▓
▓ ▓     ▓   ▓   ▓ ▓ ▓▓
▓▓▓ ▓▓▓ ▓▓▓ ▓ ▓▓▓▓▓ ▓▓
▓   ▓               ▓▓
▓▓▓ ▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓ ▓▓
▓   ▓ ▓   ▓     ▓ ▓ ▓▓
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓

答案 2 :(得分:2)

尝试在程序开始时使用独特的随机权重对墙壁进行加权。权重列表永远不会改变。当您从可用墙壁列表中选择下一面墙时,请选择重量最轻的墙壁。

答案 3 :(得分:2)

您的解决方案看起来不是很错误。特别是它是一个迷宫,并且(如果你不能沿对角线行走),从每个(开放)位置到彼此(开放)位置都有一条独特的路径。唯一的问题似乎是风格。

如果你认为&#34;正确&#34;在没有外边框的情况下发布的迷宫,并将topleft单元格设为(0,0),您可以观察到某些意义上的通道和墙壁是交替的。两个坐标均匀的每个单元必须是一个通道,并且两个坐标都是奇数的每个单元必须是一个墙。因此,您可以选择的唯一单元格是一个坐标是偶数而另一个是奇数的单元格。

让一个单元格(x,y)位于字段中间,两个坐标都是偶数。这个细胞必须是一个通道。单元格(x-1,y)(x+1,y)(x,y-1)(x,y+1)是围绕它的潜在墙,单元格(x-2,y)(x+2,y),{{ 1}}和(x,y-2)这些墙的对立面上的正方形。

通过这些信息,您可以实现算法,另外要求在步骤2中您必须选择两个坐标均匀的单元格。

答案 4 :(得分:2)

以下是基于accepted answer的带注释的Java实现:

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

/**
 * Generate a maze using Prime's algorithm
 * Based on: https://stackoverflow.com/a/29758926/3992939
 *
 * @author c0der
 * 25 Jun 2020
 *
 */
public class PrimeMazeGenerator implements Runnable {

    private static final int[][] DIRECTIONS = { //distance of 2 to each side
            { 0 ,-2}, // north
            { 0 , 2}, // south
            { 2 , 0}, // east
            {-2 , 0}, // west
    };

    private long delay = 0;
    private final CellModel[][] cells;
    private final Random random;

    public PrimeMazeGenerator(CellModel[][] cells) {
        this.cells = cells;
        random = new Random();
    }

    @Override
    public void run() {
        primMazeGeneration();
    }

    public void execute() {
        new Thread(this).start();
    }

    void primMazeGeneration() {

        //Start with a grid full of cellModelViews in state wall (not a path).
        for(int i = 0; i < cells.length; i++){
            for(int j = 0; j < cells[0].length ; j++){
                cells[i][j].setWall(true);
            }
        }

        //Pick a random cell
        int x = random.nextInt(cells.length);
        int y = random.nextInt(cells[0].length);

        cells[x][y].setWall(false); //set cell to path
        //Compute cell frontier and add it to a frontier collection
        Set<CellModel> frontierCells = new HashSet<>(frontierCellsOf(cells[x][y]));

        while (!frontierCells.isEmpty()){

            //Pick a random cell from the frontier collection
            CellModel frontierCell = frontierCells.stream().skip(random.nextInt(frontierCells.size())).findFirst().orElse(null);

            //Get its neighbors: cells in distance 2 in state path (no wall)
            List<CellModel> frontierNeighbors =  passageCellsOf(frontierCell);

            if(!frontierNeighbors.isEmpty()) {
                //Pick a random neighbor
                CellModel neighbor = frontierNeighbors.get(random.nextInt(frontierNeighbors.size()));
                //Connect the frontier cell with the neighbor
                connect(frontierCell, neighbor);
            }

            //Compute the frontier cells of the chosen frontier cell and add them to the frontier collection
            frontierCells.addAll(frontierCellsOf(frontierCell));
            //Remove frontier cell from the frontier collection
            frontierCells.remove( frontierCell);
            try {
                Thread.sleep(delay);
            } catch (InterruptedException ex) { ex.printStackTrace();}
        }
    }

    //Frontier cells: wall cells in a distance of 2
    private List<CellModel> frontierCellsOf(CellModel cell) {

        return cellsAround(cell, true);
    }

    //Frontier cells: passage (no wall) cells in a distance of 2
    private List<CellModel> passageCellsOf(CellModel cell) {

        return cellsAround(cell, false);
    }

    private List<CellModel> cellsAround(CellModel cell, boolean isWall) {

        List<CellModel> frontier = new ArrayList<>();
        for(int[] direction : DIRECTIONS){
            int newRow = cell.getRow() + direction[0];
            int newCol = cell.getColumn() + direction[1];
            if(isValidPosition(newRow, newCol) && cells[newRow][newCol].isWall() == isWall){
                frontier.add(cells[newRow][newCol]);
            }
        }

        return frontier;
    }

    //connects cells which are distance 2 apart
    private void connect( CellModel frontierCellModelView, CellModel neighbour) {

        int inBetweenRow = (neighbour.getRow() + frontierCellModelView.getRow())/2;
        int inBetweenCol = (neighbour.getColumn() + frontierCellModelView.getColumn())/2;
        frontierCellModelView.setWall(false);
        cells[inBetweenRow][inBetweenCol].setWall(false);
        neighbour.setWall(false);
    }

    private boolean isValidPosition(int row, int col) {
        return row >= 0 && row < cells.length
                    && col >= 0 && col < cells[0].length;
    }

    public PrimeMazeGenerator setDelay(long delay) {
        this.delay = delay;
        return this;
    }
}

CellModel.java

/**
 * Maze cell representation
 *
 * @author c0der
 * 25 Jun 2020
 *
 */
public class CellModel{

    private final int row, column;
    private boolean isWall;
    //support to fire property change events
    private PropertyChangeSupport pcs;

    public CellModel(int row, int column)  {
       this(row, column, false);
    }

    public CellModel(int row, int column, boolean isWall) {
        this.row = row;
        this.column = column;
        this.isWall = isWall;
    }

    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof CellModel)) return false;
        CellModel other = (CellModel)obj;
        return row == other.getRow() && column == other.getColumn();
    }

    public void setPropertChangeSupport(PropertyChangeSupport pcs) {
        this.pcs = pcs;
    }

    private void firePropertyChange(String name, Object oldValue, Object newValue) {
        if(pcs != null) {
            pcs.firePropertyChange(name, oldValue, newValue);
        }
    }

    /**
    * Get {@link #isWall}
    */
    public boolean isWall() {
        return isWall;
    }

    /**
    * Set {@link #isWall}
    */
    public void setWall(boolean isWall) {
        Object old = this.isWall;
        this.isWall = isWall;
        firePropertyChange("Wall", old, isWall);
    }

    /**
    * Get {@link #row}
    */
    public int getRow() {
        return row;
    }

    /**
    * Get {@link #column}
    */
    public int getColumn() {
        return column;
    }

    @Override
    public String toString() {
        return  "["+ (isWall ? "Wall " : "Path " ) +  row + "-" + column + "]";
    }

    /* (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        return 17*row + 31*column;
    }
}
可以从CellModel[][] cells获取

MazeModel

/**
 * Maze representation
 *
 * @author c0der
 * 25 Jun 2020
 *
 */
public class MazeModel {

    /**
     * Collection to represent an entire maze
     */
    private final CellModel[][] cellModels;

    public MazeModel(int rows, int columns) {

        cellModels = new CellModel[rows][columns];
        for(int row=0; row <cellModels.length; row++) {
            for(int col=0; col<cellModels[row].length; col++) {
                CellModel cellModel = new CellModel(row, col);
                cellModels[row][col] = cellModel;
            }
        }
    }

    /**
    * Get {@link #cellModels}
    */
    public CellModel[][] getCellModels() {
        return cellModels;
    }
}

this存储库中提供了包含SwingJavaFx gui的完整的可运行代码。


enter image description here

答案 5 :(得分:1)

您问题的简单答案是,在添加边缘时,您需要检查边缘是否意味着移除作为其任何相邻墙块的最后一个邻居的墙。

这样可以防止任何墙壁仅在拐角处连接。

答案 6 :(得分:1)

在我研究这个问题之前,我自己想出了一些完全不同的东西。看看你认为这是一种有用的方法。

很久以前,当我看到 IBM PC角色图形(代码页中的字形)时,我想过以这种方式创建迷宫。我的方法分为两个阶段:

  1. 使用位编码值1-15在整数数组中生成迷宫,以指示在迷宫的每个单元格处打开方向
  2. 将其渲染为可见的形式。所以在我展示迷宫之前,墙壁不是考虑因素。
  3. 每个单元格从0开始(未选中),然后可以打开4位中的任何一位(1 =向右,2 =向下,4 =向左,8 =向上)。天真地,你可以在每个单元格中选择1-15的随机数,除了五件事:

    1. 首先画一个&#34;墙&#34;整个阵列周围的走廊和角落,并在两个点留下通道。这是处理边界条件的最简单方法。
    2. 对选择进行加权以使死角很少,直线或角落走廊很常见,完全交叉不常见。
    3. 将每个单元格与其周围已设置的单元格进行匹配:如果相邻单元格具有相应的开启位(左侧单元格中的1位,依此类推),则强制该单元格中的该位开启,如果是关闭,在此单元格中强行关闭它。
    4. 找到确保开始和结束连接的方法(此处需要进一步研究)。
    5. 管理以填充所有细胞而不会产生空隙(需要更多研究)。
    6. 这是&#34; raw&#34;的显示。字符图形方面的数组:

          ┌│───────┐  
          │└─┐┌┐│  │  
          ││┌┴┤│├─┐│  
          │├┴─┘│└┐││  
          │└─┐──┐│││  
          │┌┬┴─┌┘│││  
          ││├┐│└┬┘││  
          │└┤│└┬┴─┤│  
          │─┴┴─┘──┤│  
          └───────│┘  
      

      渲染结果时,我使用3x4网格字符图形显示每个单元格。这是一个例子:

      ╔═══╡  ╞═══════════════════════════════╗
      ║░░░│  │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░║
      ║░░╔╡  ╞════════════════════════════╗░░║
      ║░░║│  └───────┐┌──────┐┌──┐        ║░░║
      ║░░║│          ││      ││  │        ║░░║
      ║░░║└───────┐  ││  ┌┐  ││  │        ║░░║
      ║░░║┌──┐┌───┘  └┘  ││  ││  └───────┐║░░║
      ║░░║│  ││          ││  ││          │║░░║
      ║░░║│  ││  ┌────┐  ││  ││  ┌────┐  │║░░║
      ║░░║│  └┘  └────┘  ││  ││  └───┐│  │║░░║
      ║░░║│              ││  ││      ││  │║░░║
      ║░░║│  ┌───────────┘└──┘└───┐  ││  │║░░║
      ║░░║│  └───────┐┌──────────┐│  ││  │║░░║
      ║░░║│          ││          ││  ││  │║░░║
      ║░░║└───────┐  │└───────┐  │└──┘│  │║░░║
      ║░░║┌───────┘  └───┐┌───┘  │┌──┐│  │║░░║
      ║░░║│              ││      ││  ││  │║░░║
      ║░░║│  ┌┐  ┌───────┘│  ┌───┘│  ││  │║░░║
      ║░░║│  ││  └───┐┌──┐│  └────┘  ││  │║░░║
      ║░░║│  ││      ││  ││          ││  │║░░║
      ║░░║│  ││  ┌┐  ││  │└───┐  ┌───┘│  │║░░║
      ║░░║│  └┘  ││  ││  └────┘  └────┘  │║░░║
      ║░░║│      ││  ││                  │║░░║
      ║░░║└───┐  ││  │└───┐  ┌────────┐  │║░░║
      ║░░║┌───┘  └┘  └────┘  │┌───────┘  │║░░║
      ║░░║│                  ││          │║░░║
      ║░░║└──────────────────┘└───────┐  │║░░║
      ║░░╚════════════════════════════╡  ╞╝░░║
      ║░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│  │░░░║
      ╚═══════════════════════════════╡  ╞═══╝
      

      了解使用此方法可以执行的操作。 (不同的字体选择使它看起来比这里好,线条都无缝连接 - 当然必须是等宽的。)