Whatever-first search in Python
A generic graph traversal that keeps a bag of unvisited edges and repeatedly extracts one, with the bag's discipline giving depth-first or breadth-first search.
- File
whatever_first_search.py, 37 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/whatever-first-search/. The authors are listed in prov/4TP999.jsonl.
from collections import deque
def whatever_first_search(
adj: list[list[int]], s: int, bag: str = "stack"
) -> tuple[list[int], list[int | None]]:
"""Run whatever-first search from s and return (order, parent).
The graph has the vertices 0 .. n-1 and the adjacency lists adj. The bag holds
pairs (p, v): "v was seen from p". With bag="queue" it is a FIFO queue and the
search is breadth-first. With bag="stack" it is a LIFO stack, and each list is
pushed in reverse so that the first neighbour is taken first; the search is then
depth-first. order lists the marked vertices in the order they were marked, and
parent[v] is the p of the pair that marked v, or None. Time O(n + m).
"""
if bag not in ("stack", "queue"):
raise ValueError("bag must be 'stack' or 'queue'")
n = len(adj)
marked = [False] * n # @line L1
parent: list[int | None] = [None] * n
order: list[int] = []
items: deque[tuple[int | None, int]] = deque([(None, s)]) # @line L2
take = items.pop if bag == "stack" else items.popleft
while items: # @line L3
p, v = take() # @line L4
if not marked[v]: # @line L5
marked[v] = True # @line L6
parent[v] = p
order.append(v)
nbrs = reversed(adj[v]) if bag == "stack" else adj[v]
for w in nbrs: # @line L7
if not marked[w]: # a pair for a marked w would be skipped at L5
items.append((v, w))
return order, parent # @line L8
test_whatever_first_search.py 98 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/whatever-first-search/. The authors are listed in prov/4TP999.jsonl.
import random
from collections import deque
import pytest
from whatever_first_search import whatever_first_search
def bfs_oracle(adj: list[list[int]], s: int) -> tuple[list[int], list[int | None]]:
"""Textbook BFS: mark on enqueue, parent set at discovery."""
parent: list[int | None] = [None] * len(adj)
seen = [False] * len(adj)
seen[s] = True
order, q = [], deque([s])
while q:
u = q.popleft()
order.append(u)
for v in adj[u]:
if not seen[v]:
seen[v] = True
parent[v] = u
q.append(v)
return order, parent
def dfs_oracle(adj: list[list[int]], s: int) -> tuple[list[int], list[int | None]]:
"""Textbook recursive DFS from s: preorder and parents."""
parent: list[int | None] = [None] * len(adj)
seen = [False] * len(adj)
order: list[int] = []
def visit(u: int) -> None:
seen[u] = True
order.append(u)
for v in adj[u]:
if not seen[v]:
parent[v] = u
visit(v)
visit(s)
return order, parent
def test_example() -> None:
adj = [[1, 2], [0, 2], [0, 1, 3], [2]]
assert whatever_first_search(adj, 0, "stack") == ([0, 1, 2, 3], [None, 0, 1, 2])
assert whatever_first_search(adj, 0, "queue") == ([0, 1, 2, 3], [None, 0, 0, 2])
def test_larger_example() -> None:
adj = [[1, 2], [0, 3], [0, 3, 4], [1, 2, 5], [2, 5], [3, 4, 6], [5], []]
assert whatever_first_search(adj, 0, "stack") == (
[0, 1, 3, 2, 4, 5, 6],
[None, 0, 3, 1, 2, 4, 5, None],
)
assert whatever_first_search(adj, 0, "queue") == (
[0, 1, 2, 3, 4, 5, 6],
[None, 0, 0, 1, 2, 3, 5, None],
)
def test_edge_cases() -> None:
assert whatever_first_search([[]], 0) == ([0], [None])
assert whatever_first_search([[0]], 0, "queue") == ([0], [None])
assert whatever_first_search([[], []], 1) == ([1], [None, None])
with pytest.raises(ValueError):
whatever_first_search([[]], 0, "heap")
def test_long_path() -> None:
n = 100000
adj = [[i + 1] for i in range(n - 1)] + [[]]
order, parent = whatever_first_search(adj, 0, "stack")
assert order == list(range(n))
assert parent[n - 1] == n - 2
def test_random_against_oracles() -> None:
rng = random.Random(9137)
for trial in range(600):
n = rng.randint(1, 10)
p = rng.random()
adj: list[list[int]] = [[] for _ in range(n)]
undirected = trial % 2 == 0
for u in range(n):
for v in range(n):
if (not undirected or u < v) and rng.random() < p / 2:
adj[u].append(v)
if undirected and u != v:
adj[v].append(u)
for lst in adj:
rng.shuffle(lst)
s = rng.randrange(n)
assert whatever_first_search(adj, s, "queue") == bfs_oracle(adj, s)
assert whatever_first_search(adj, s, "stack") == dfs_oracle(adj, s)
What the next level needs
- the tests did not run in this build