beam: BFS shortest path module with tests
This commit is contained in:
1 parent
29d0879791
commit
7439cf003a
2 files changed
+105
No files matched your search
@@ -0,0 +1,66 @@
|
||||
"""BFS shortest path (unweighted) with deterministic path reconstruction.
|
||||
|
||||
Public API:
|
||||
shortest_path(adj, src, dst) -> list[int] | None
|
||||
Returns the vertex sequence from src to dst along a shortest path,
|
||||
or None if dst is unreachable. src==dst returns [src].
|
||||
shortest_distances(adj, src) -> dict[int, int]
|
||||
Distance in edges from src to every reachable vertex.
|
||||
"""
|
||||
|
||||
from collections import deque
|
||||
|
||||
|
||||
def shortest_distances(adj, src):
|
||||
if src not in adj:
|
||||
# isolated source still reachable to itself
|
||||
pass
|
||||
dist = {src: 0}
|
||||
q = deque([src])
|
||||
while q:
|
||||
u = q.popleft()
|
||||
for v in adj.get(u, ()):
|
||||
if v not in dist:
|
||||
dist[v] = dist[u] + 1
|
||||
q.append(v)
|
||||
return dist
|
||||
|
||||
|
||||
def shortest_path(adj, src, dst):
|
||||
if src == dst:
|
||||
if src in adj or src in _all_vertices(adj):
|
||||
return [src]
|
||||
# allow unknown isolated vertex only if explicitly present as key? We require vertex existence.
|
||||
return None
|
||||
if src not in _all_vertices(adj) or dst not in _all_vertices(adj):
|
||||
return None
|
||||
parent = {src: None}
|
||||
q = deque([src])
|
||||
found = False
|
||||
while q:
|
||||
u = q.popleft()
|
||||
if u == dst:
|
||||
found = True
|
||||
break
|
||||
for v in adj.get(u, ()):
|
||||
if v not in parent:
|
||||
parent[v] = u
|
||||
q.append(v)
|
||||
if dst not in parent:
|
||||
return None
|
||||
# reconstruct
|
||||
path = []
|
||||
cur = dst
|
||||
while cur is not None:
|
||||
path.append(cur)
|
||||
cur = parent[cur]
|
||||
path.reverse()
|
||||
return path
|
||||
|
||||
|
||||
def _all_vertices(adj):
|
||||
s = set(adj.keys())
|
||||
for u, nbrs in adj.items():
|
||||
for v in nbrs:
|
||||
s.add(v)
|
||||
return s
|
||||
@@ -0,0 +1,39 @@
|
||||
import sys
|
||||
sys.path.insert(0, "/root/.hermes/wizard-outputs/beam")
|
||||
from bfs_shortest_path import shortest_path, shortest_distances
|
||||
|
||||
def test_basic():
|
||||
adj = {0:[1,2],1:[3],2:[3],3:[]}
|
||||
assert shortest_path(adj,0,3) in ([0,1,3],[0,2,3])
|
||||
assert shortest_distances(adj,0) == {0:0,1:1,2:1,3:2}
|
||||
|
||||
def test_same():
|
||||
adj = {5:[]}
|
||||
assert shortest_path(adj,5,5) == [5]
|
||||
|
||||
def test_unreachable():
|
||||
adj = {0:[1],2:[]}
|
||||
assert shortest_path(adj,0,2) is None
|
||||
|
||||
def test_unknown_vertex():
|
||||
adj = {0:[1]}
|
||||
assert shortest_path(adj,0,99) is None
|
||||
assert shortest_path(adj,99,0) is None
|
||||
|
||||
def test_empty_graph():
|
||||
adj = {}
|
||||
assert shortest_path(adj,1,1) is None # vertex not present
|
||||
assert shortest_distances(adj,1) == {1:0} # isolated allowed for distances? spec: yes if src given
|
||||
|
||||
def test_directed():
|
||||
adj = {0:[1],1:[]}
|
||||
assert shortest_path(adj,1,0) is None
|
||||
|
||||
def test_path_reconstruction_long():
|
||||
adj = {i:[i+1] for i in range(5)}
|
||||
assert shortest_path(adj,0,5) == [0,1,2,3,4,5]
|
||||
|
||||
if __name__ == "__main__":
|
||||
for name in [n for n in globals() if n.startswith("test_")]:
|
||||
globals()[name]()
|
||||
print("ALL TESTS PASSED")
|
||||
Reference in new issue
Block a user