题目解析与图论角度剖析
题目描述
在一个 N × M 的网格中找出所有"危险位置"的数量。"危险位置"定义为:如果站在该位置上,无论采取什么移动策略,都无法到达出口。 地图包括以下元素:
.:表示普通地板,可以自由移动到上下左右相邻的格子(不可以走斜线)O:表示出口U:表示向上的传送器,踩上去会被强制传送到上方的格子D:表示向下的传送器,踩上去会被强制传送到下方的格子L:表示向左的传送器,踩上去会被强制传送到左方的格子R:表示向右的传送器,踩上去会被强制传送到右方的格子
注意,如果被传送出了竞技场之外,则算作死亡。
图论角度剖析
由于传送带是单向的,所以图为有向图。
-
图的构建:
- 将竞技场中的每个位置视为图中的一个节点。
- 如果两个位置之间可以通过普通地板或传送器直接到达,则在它们之间建立一条有向边。
-
危险位置的定义:
- 危险位置是指那些无法通过任何路径到达出口的节点。
- 这些节点可能形成一个或多个强连通分量(SCC),其中每个SCC中的节点都无法到达出口。但是不是所有危险节点都是属于一个强连通分量。例如一个九宫格外围循环传送带构成的强连通分量与中央的一个普通地板。普通地板与传送带之间只存在单向边。
-
算法选择: 广度优先搜索(BFS) :类似问题无向图问题往往从出口开始进行反向BFS,标记所有可以到达的节点。这种算法时间复杂性较低但是本题由于传送带导致图为有向图,必须从每个其它节点进行遍历来确定是否可以到达出口。这种算法的时间复杂性较高。一个解决方案就是记忆化搜索。将一次广度优先遍历过程中确定下的危险位置与安全位置记忆化在一个字典中。
数据结构与算法思路
-
数据结构选择:
- 使用
set来存储available和dangerous位置,这样可以快速判断某个位置是否已经被访问过。 - 使用
queue来进行广度优先搜索(BFS)。
- 使用
-
算法步骤:
- 初始化
available和dangerous集合。 - 对于每个位置,使用 BFS 进行搜索,判断是否可以到达出口。
- 如果遇到传送器,检查是否会导致无限循环或传送出竞技场。
- 最终计算出所有无法到达出口的位置数量。
- 初始化
部分关键代码
def isDangerous(posi, visited):
"""
判断当前位置是否是危险位置
"""
path = set()
cur = data[posi[0]][posi[1]]
while cur in dires:
if posi in path:
dangerous.update(path)
visited.update(path)
return True
path.add(posi)
idx_dire = dires.index(cur)
next_posi_row = posi[0] + row_dires[idx_dire]
next_posi_col = posi[1] + col_dires[idx_dire]
next_posi = (next_posi_row, next_posi_col)
posi = next_posi
if posi[0] < 0 or N <= posi[0] or posi[1] < 0 or M <= posi[1]:
dangerous.update(path)
visited.update(path)
return True
cur = data[next_posi_row][next_posi_col]
visited.update(path)
return False
def isValid(posi):
"""
判断当前位置是否有效
"""
if posi in dangerous:
return False
return 0 <= posi[0] < N and 0 <= posi[1] < M
def bfs(posi, available, dangerous):
"""
广度优先搜索
"""
def getPath(cur, parents):
"""
获取从起点到当前位置的路径
"""
path = set()
path.add(cur)
idx_dires = parents[cur[0]][cur[1]]
while idx_dires != -1:
cur = (cur[0] - row_dires[idx_dires], cur[1] - col_dires[idx_dires])
path.add(cur)
idx_dires = parents[cur[0]][cur[1]]
return path
visited = set()
q = queue.Queue()
q.put(posi)
parents = [[-1] * M for _ in range(N)]
while not q.empty():
posi = q.get()
visited.add(posi)
cur = data[posi[0]][posi[1]]
if data[posi[0]][posi[1]] == 'O' or posi in available:
path = getPath(posi, parents)
available.update(path)
return
if cur == '.' or cur == 'O':
for i in range(4):
next_posi_row = posi[0] + row_dires[i]
next_posi_col = posi[1] + col_dires[i]
next_posi = (next_posi_row, next_posi_col)
if next_posi not in visited and isValid(next_posi):
parents[next_posi_row][next_posi_col] = i
q.put(next_posi)
elif cur in dires:
if not isDangerous(posi, visited):
idx_dire = dires.index(cur)
next_posi_row = posi[0] + row_dires[idx_dire]
next_posi_col = posi[1] + col_dires[idx_dire]
next_posi = (next_posi_row, next_posi_col)
if next_posi not in visited and isValid(next_posi):
parents[next_posi_row][next_posi_col] = idx_dire
q.put(next_posi)
代码实现思路
-
初始化:
dires和row_dires,col_dires分别存储传送器的方向和对应的行列变化。available和dangerous集合用于存储可以到达出口的位置和危险位置。
-
isDangerous 函数:
- 用于判断当前位置是否是危险位置。如果遇到无限循环或传送出竞技场,则标记为危险位置。
-
isValid 函数:
- 用于判断当前位置是否有效,即是否在竞技场内且不在
dangerous集合中。
- 用于判断当前位置是否有效,即是否在竞技场内且不在
-
bfs 函数:
- 使用广度优先搜索(BFS)来探索竞技场中的每个位置。
- 如果遇到出口或已经标记为
available的位置,则更新available集合。 - 如果遇到传送器,则调用
isDangerous函数判断是否是危险位置。
-
主循环:
- 遍历竞技场中的每个位置,如果该位置不在
dangerous和available集合中,则进行 BFS 搜索。
- 遍历竞技场中的每个位置,如果该位置不在
-
结果计算:
- 最终计算出所有无法到达出口