Buckblog - Maze Generation - Prim's Algorithm
Buckblog - Maze Generation - Prim's Algorithm
Prim’s approaches the problem from a different angle. Rather than working
edgewise across the entire graph, it starts at one point, and grows outward
from that point. The standard version of the algorithm works something like
this:
With one little change, it becomes a suitable method for generating mazes:
just change step 2 so that instead of selecting the edge with the smallest
weight, you select an edge at random, as long as it bridges the so-called
“frontier” of the maze (the set of edges that move from within the maze, to a
cell that is outside the maze).
As before, let’s walk through an example and see how it works in practice.
An example
Let’s start with a simple 3×3 grid:
For efficiency, let’s call the set of all cells that are not yet in the maze, but
are adjacent to a cell that is in the maze, the “frontier”. We’ll color the
frontier cells red:
Now, we choose one of these frontier cells at random and carve a passage
from that frontier cell to whichever adjacent cell is already part of the maze.
Then we’ll mark the neighbors of the formerly frontier cell, as “frontier” cells
themselves.
Let’s just keep it going to the end, now, chug chug chug:
The algorithm terminates when there are no more frontier cells to choose
from, giving us the anticipated perfect maze.
Implementation
The largest bit of implementing Prim’s algorithm (for me) seems to go
toward managing the interactions with that frontier set. Maybe your
experience will be different. You basically need two operations: mark a cell
as “in” (which then marks the “out” neighbors as frontier cells), and one that
returns all the “in” neighbors of a given frontier cell. Something like this:
(and please excuse the apparent hand-waving around the add_frontier
method; it’s not complicated, just not entirely relevant. The full
implementation is given at the end of the article.)
Once you’ve got that implemented (along with the necessary supporting
methods and data structures), the algorithm itself is remarkably
straightforward.
1 until frontier.empty?
2 # ...
3 end
1 x, y = frontier.delete_at(rand(frontier.length))
1 n = neighbors(x, y, grid)
2 nx, ny = n[rand(n.length)]
Then, we record a passage from the neighbor cell to the frontier cell:
And finally, we mark the frontier cell as being “in” the maze (and add any of
its outside neighbors to the frontier):
And you’re done! For those of you not using IE (which will make a total
mess of this), here are two demos you can play with to see Prim’s in action:
Reset Step Run Reset Step Run
1 # -------------------------------------------------------------------
2 # An implementation of Prim's algorithm for generating mazes.
3 # This is a pretty fast algorithm, when implemented well, since it
4 # only needs random access to the list of frontier cells. It does
5 # require space proportional to the size of the maze, but even worse-
6 # case, it won't be but a fraction of the size of the maze itself.
7 # As with Kruskal's, this algorithm tends to generate mazes with many
8 # short cul-de-sacs.
9 # -------------------------------------------------------------------
10 # NOTE: the display routine used in this script requires a terminal
11 # that supports ANSI escape sequences. Windows users, sorry. :(
12 # -------------------------------------------------------------------
13
14 # -------------------------------------------------------------------
15 # 1. Allow the maze to be customized via command-line parameters
16 # -------------------------------------------------------------------
17
18 width = (ARGV[0] || 10).to_i
19 height = (ARGV[1] || width).to_i
20 seed = (ARGV[2] || rand(0xFFFF_FFFF)).to_i
21
22 srand(seed)
23
24 # -------------------------------------------------------------------
25 # 2. Set up constants to aid with describing the passage directions
26 # -------------------------------------------------------------------
27
28 N, S, E, W = 1, 2, 4, 8
29 IN = 0x10
30 FRONTIER = 0x20
31 OPPOSITE = { E => W, W => E, N => S, S => N }
32
33 # -------------------------------------------------------------------
34 # 3. Data structures and methods to assist the algorithm
35 # -------------------------------------------------------------------
36
37 grid = Array.new(height) { Array.new(width, 0) }
38 frontier = []
39
40 def add_frontier(x, y, grid, frontier)
41 if x >= 0 && y >= 0 && y < grid.length && x < grid[y].length && grid
42 grid[y][x] |= FRONTIER
43 frontier << [x,y]
44 end
45 end
46
47 def mark(x, y, grid, frontier)
48 grid[y][x] |= IN
49 add_frontier(x-1, y, grid, frontier)
50 add_frontier(x+1, y, grid, frontier)
51 add_frontier(x, y-1, grid, frontier)
52 add_frontier(x, y+1, grid, frontier)
53 end
54
55 def neighbors(x, y, grid)
56 n = []
57
58 n << [x-1, y] if x > 0 && grid[y][x-1] & IN != 0
59 n << [x+1, y] if x+1 < grid[y].length && grid[y][x+1] & IN != 0
60 n << [x, y-1] if y > 0 && grid[y-1][x] & IN != 0
61 n << [x, y+1] if y+1 < grid.length && grid[y+1][x] & IN != 0
62
63 n
64 end
65
66 def direction(fx, fy, tx, ty)
67 return E if fx < tx
68 return W if fx > tx
69 return S if fy < ty
70 return N if fy > ty
71 end
72
73 # -------------------------------------------------------------------
74 # 4. Routines for displaying the maze
75 # -------------------------------------------------------------------
76
77 def empty?(cell)
78 cell == 0 || cell == FRONTIER
79 end
80
81 def display_maze(grid)
82 print "\e[H" # move to upper-left
83 puts " " + "_" * (grid[0].length * 2 - 1)
84 grid.each_with_index do |row, y|
85 print "|"
86 row.each_with_index do |cell, x|
87 print "\e[41m" if cell == FRONTIER
88 if empty?(cell) && y+1 < grid.length && empty?(grid[y+1][x])
89 print " "
90 else
91 print((cell & S != 0) ? " " : "_")
92 end
93 print "\e[m" if cell == FRONTIER
94
95 if empty?(cell) && x+1 < row.length && empty?(row[x+1])
96 print((y+1 < grid.length && (empty?(grid[y+1][x]) || empty?
97 elsif cell & E != 0
98 print(((cell | row[x+1]) & S != 0) ? " " : "_")
99 else
100 print "|"
101 end
102 end
103 puts
104 end
105 end
106
107 # -------------------------------------------------------------------
108 # 5. Prim's algorithm
109 # -------------------------------------------------------------------
110
111 print "\e[2J" # clear the screen
112
113 mark(rand(width), rand(height), grid, frontier)
114 until frontier.empty?
115 x, y = frontier.delete_at(rand(frontier.length))
116 n = neighbors(x, y, grid)
117 nx, ny = n[rand(n.length)]
118
119 dir = direction(x, y, nx, ny)
120 grid[y][x] |= dir
121 grid[ny][nx] |= OPPOSITE[dir]
122
123 mark(x, y, grid, frontier)
124
125 display_maze(grid)
126 sleep 0.01
127 end
128
129 display_maze(grid)
130
131 # -------------------------------------------------------------------
132 # 6. Show the parameters used to build this maze, for repeatability
133 # -------------------------------------------------------------------
134
135 puts "#{$0} #{width} #{height} #{seed}"
prims.rb hosted with by GitHub view raw
Conclusion
Mazes generated by Prim’s algorithm share many of the characteristics of
those created via Kruskal’s algorithm, such as having an abundance of
short cul-de-sacs. Such an aesthetic appeals to some, and not to others,
but it definitely has this to say for it: for large mazes, the short cul-de-sacs
make the maze harder to puzzle out at a glance!
Whether you enjoy the look of these mazes or not, I encourage you to try
your hand at Prim’s algorithm. It’s a fun algorithm to code, not least of all
because it comes together so easily. Personally, I enjoy watching the
animation: it puts me in mind of a flame consuming a sheet of paper.
Reader Comments
Lucas Prim
12 Jan 2011
Jamis
12 Jan 2011
This actually runs just fine on IE8. (Our network blocks connections
from other programs.)
Paul
27 Jan 2011
These articles lovingly written and prepared for your reading pleasure by Jamis Buck
<[email protected]>. Follow me on Twitter and stalk my code at GitHub.