I am working on a maze-solving program where I need to count the number of cul-de-sacs, also known as dead-ends. The maze is represented in a way that allows identifying different paths and intersections.
What are the possible methods I can use to count the cul-de-sacs in the maze using Python? I am looking for algorithmic suggestions or Python libraries that might simplify this process.
The maze has 8 accessible cul-de-sacs. The accesses are indicated by triangles
[[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1],
[1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1],
[1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1],
[1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1],
[1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1],
[1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1],
[0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1],
[1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1],
[1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1],
[1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1]]
Note that the code used to represent your maze does not correspond exactly to your drawing (thick walls in your code, thin walls in the drawing). Nevertheless I had some fun implementing a Maze class that extracts the "thin wall" maze from the "thick wall" data; I added a method to list dead-ends (by which I mean the actual dead-ends, not the entrances you marked on your drawing), a method to cut the maze branch leading to a dead-end, and another to cut all dead-end branches, leaving only (in simple mazes) the correct path.
This doesn't do exactly what you asked (counting entrances), but since your question was a little vague it's the best I could do. You can always build other methods on top of mine to fit your needs.
Edit: I tweaked the code a little. The .trim_maze
method now returns a set of dead-end entrances (but the "double" entrance appears only once), and these appear in .show_work_state
; the .count_dead_end_entrances
computes the number of dead-end paths (so the "double" entrance counts for 2):
from copy import deepcopy
class Maze:
def __init__(self, data):
self.data = data
self.work_data = deepcopy(data)
self.height = len(data) // 2
self.width = len(data[0]) // 2
self.dead_end_entrances = set()
self.exit_dict = {
(-1, 0): 'N',
(1, 0): 'S',
(-0, -1): 'W',
(0, 1): 'E',
}
self.offset_dict = {e: o for o, e in self.exit_dict.items()}
opposite_dict = {
'N': 'S',
'S': 'N',
'W': 'E',
'E': 'W',
}
def node_exits(self, x, y, work=False):
data = self.work_data if work else self.data
if not all((0 <= x <= self.width, 0 <= y <= self.height)):
raise Exception("Out of bounds")
exits = []
for offset, direction in self.exit_dict.items():
if not data[2 * y + 1 + offset[0]][2 * x + 1 + offset[1]]:
exits.append(direction)
return exits
def node_repr(self, x, y, work=False):
ne = self.node_exits(x, y, work)
return(
f' {" | " if "N" in ne else " "} ',
f'{"---" if "W" in ne else " "}{" x " if len(ne) == 1 else " o "}{"---" if "E" in ne else " "}',
f' {" | " if "S" in ne else " "} ')
def node_print(self, x, y, work=False):
print('\n'.join(self.node_repr(x, y, work)))
def __repr__(self, work=False):
return '\n'.join(
'\n'.join(
''.join(
self.node_repr(x, y, work)[i]
for x in range(self.width)
)
for i in range(3)
)
for y in range(self.height)
)
def show_work_state(self):
print(self.__repr__(True))
def dead_ends(self, work=False):
return [(x, y) for x in range(self.width) for y in range(self.height) if len(self.node_exits(x, y, work)) == 1]
def cut_dead_end(self, x, y):
ne = self.node_exits(x, y, work=True)
if len(ne) != 1:
return x, y
self.dead_end_entrances.discard((x, y))
offset = self.offset_dict[ne[0]]
self.work_data[2 * y + 1 + offset[0]][2 * x + 1 + offset[1]] = 1
return self.cut_dead_end(x + offset[1], y + offset[0])
def trim_maze(self):
dead_ends = self.dead_ends(True)
for de in dead_ends:
self.dead_end_entrances.add(self.cut_dead_end(*de))
return self.dead_end_entrances
def count_dead_end_entrances(self):
return sum(
len(self.node_exits(*node)) - len(self.node_exits(*node, True))
for node in self.dead_end_entrances
)
Example:
a = Maze([
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1],
[1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1],
[1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1],
[1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1],
[1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1],
[1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1],
[0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1],
[1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1],
[1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1],
[1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1]
])
print(a)
o ------ o ------ o ------ o ------ o ------ o ------ o ------ o ------ o x
| | | | |
| | | | |
o ------ o ------ o o o ------ o o ------ o o ------ o
| | | | |
| | | | |
x o ------ o x o ------ o x o ------ o ------ o
| | | | |
| | | | |
o ------ o o ------ o ------ o o ------ o o ------ o o
| | | | |
| | | | |
o ------ o ------ o ------ o ------ o ------ o o ------ o o ------ o
| | | | |
| | | | |
o ------ o o o ------ o o x ------ o ------ o ------ o
| | | | |
| | | | |
o ------ o x o o ------ o o ------ x o ------ o
| | | | |
| | | | |
--- o o ------ o o o ------ x o ------ o o ------ o
| | | | | |
| | | | | |
o o o ------ o o ------ o ------ o ------ o x o
| | | | |
| | | | |
x o ------ o ------ o ------ o x ------ o ------ o ------ o ------ o
|
print(a.dead_ends())
# [(0, 2), (0, 9), (2, 6), (3, 2), (5, 7), (5, 9), (6, 2), (6, 5), (7, 6), (8, 8), (9, 0)]
a.cut_dead_end(0, 2)
a.show_work_state()
o o o o ------ o ------ o ------ o ------ o ------ o x
| | | |
| | | |
o o o o o ------ o o ------ o o ------ o
| | | |
| | | |
o o o x o ------ o x o ------ o ------ o
| | |
| | |
o o o ------ o ------ o o ------ o o ------ o o
| | | | |
| | | | |
o ------ o ------ o ------ o ------ o ------ o o ------ o o ------ o
| | | | |
| | | | |
o ------ o o o ------ o o x ------ o ------ o ------ o
| | | | |
| | | | |
o ------ o x o o ------ o o ------ x o ------ o
| | | | |
| | | | |
--- o o ------ o o o ------ x o ------ o o ------ o
| | | | | |
| | | | | |
o o o ------ o o ------ o ------ o ------ o x o
| | | | |
| | | | |
x o ------ o ------ o ------ o x ------ o ------ o ------ o ------ o
|
print(a.trim_maze())
# {(0, 7), (2, 4), (7, 4), (8, 9), (7, 5), (6, 9), (5, 2)}
a.show_work_state()
o o o o o o o o o o
o o o o o o o o o o
o o o o o ------DEE o o o o
| |
| |
o o o ------ o ------ o o ------ o o o o
| |
| |
o ------ o ------DEE o o o o ------DEE o o
| |
| |
o ------ o o o o o o DEE------ o ------ o
| |
| |
o ------ o o o o o o o o ------ o
| |
| |
---DEE o o o o o o o o ------ o
|
|
o o o o o o o o o o
|
|
o o o o o o DEE------ o ------DEE------ o
|
a.count_dead_end_entrances()
# 8