Graph traversal — DedSec Project
Graph traversal — cycle-safe breadth-first search Contract A small offline dependency graph is represented as a mapping from every known node to its neighbors. Return a shortest path in…
Graph traversal — cycle-safe breadth-first search
Contract
A small offline dependency graph is represented as a mapping from every known node to its neighbors. Return a shortest path in number of edges between two existing nodes; return None when no route exists. Reject unknown start or goal nodes and dangling neighbor references. With unweighted edges, breadth-first search visits nodes in layers and can guarantee the minimum edge count. Dijkstra is needed when edge weights differ.
Executable Python 3 lab
Save as bfs_lab.py and run python3 bfs_lab.py. The graph contains a cycle, two equally short choices, and a disconnected island. Neighbors are sorted to make tie-breaking deterministic for the fixture.
from collections import deque
def shortest(graph, start, goal):
if start not in graph or goal not in graph:
raise ValueError("unknown node")
pending = deque([(start, [start])])
visited = {start}
while pending:
node, path = pending.popleft()
if node == goal:
return path
for neighbor in sorted(graph[node]):
if neighbor not in graph:
raise ValueError("dangling neighbor")
if neighbor not in visited:
visited.add(neighbor)
pending.append((neighbor, path + [neighbor]))
return None
g = {"a":["c","b"],"b":["a","d"],"c":["a","d"],
"d":["b","c"],"island":[]}
assert shortest(g,"a","d") == ["a","b","d"]
assert shortest(g,"a","island") is None
assert shortest(g,"a","a") == ["a"]
try:
shortest(g,"ghost","a")
except ValueError:
pass
else:
raise AssertionError("unknown start accepted")
print("PASS: BFS shortest edges, cycles, no path")
Invariants and complexity
Add a node to visited when it is enqueued, not when it is dequeued. That prevents two parents from repeatedly adding it and keeps the queue bounded. A FIFO deque ensures all paths with k edges are processed before paths with k+1 edges. Traversal visits each vertex and edge at most a constant number of times, O(V+E), assuming adjacency iteration is already available. This example sorts each adjacency list, adding sorting cost; the path-list copying also introduces extra work, so a production implementation for large graphs should track predecessor nodes and reconstruct the path once.
Controlled failure cases
- Add a self-loop and duplicate neighbors. Ensure the search terminates and returns the same shortest edge count.
- Remove island's connections or the goal itself: distinguish no-path (None) from unknown-node (ValueError).
- Replace the deque with a LIFO stack and find a graph where the first route is longer than the BFS answer.
- For an undirected edge, verify the reverse edge is present. For directed graphs, explain why it need not be.
Evidence: PASS, queue/visited trace, one disconnected case and a written note on weighted edges and tie-breaking.