Part III · Classical Algorithms Reconstructed
Chapter 10
Graph Algorithms Reconstructed
mixed Classical algorithms with cited sources; the exact work count of the right-linear path grammar is proved here.
- Implement and justify BFS, DFS, topological sorting, strongly connected components, Dijkstra, Bellman–Ford, Kruskal, Prim, union–find and Floyd–Warshall, with counted costs.
- Read each algorithm as the evaluation of a path grammar in a particular algebra.
- Prove the exact rule-application count of semi-naive evaluation of the right-linear path grammar, and compare it with the doubling grammar.
- Explain why two grammars with the same meaning can have asymptotically different costs, trading total work against parallel depth.
10.1 Traversal, order and components
Breadth-first search from visits vertices in order of hop distance and computes in ; depth-first search visits each vertex and edge once and classifies edges by discovery and finishing times. Both are textbook algorithms [Cormen et al. 2022]. Three classical consequences:
- Topological sorting. A DAG’s vertices can be ordered so that every edge goes forward; Kahn’s algorithm repeatedly removes a vertex of in-degree zero, in , and reports a cycle if it gets stuck [Kahn 1962].
- Strongly connected components. Tarjan’s algorithm [Tarjan 1972] finds them in one DFS using low-link values, and numbers them in reverse topological order of the condensation (the SDK’s
tarjan_sccreturns that numbering, so component is a sink). - Reachability and closure. The reflexive–transitive closure — the semantics of Chapter 6 — can be computed by condensing SCCs and OR-ing bitset rows in reverse topological order: each OR of -bit rows costs word operations.
from cgtsdk import Cost
from cgtsdk.algorithms import bfs, tarjan_scc, topological_sort, transitive_closure
from cgtsdk.lab import random_digraph
for name, adj in (("random DAG", random_digraph(2000, 2.0, seed=1, dag=True)),
("random digraph", random_digraph(2000, 2.0, seed=1))):
c1, c2, c3 = Cost(), Cost(), Cost()
d = bfs(adj, 0, c1)
comp, k = tarjan_scc(adj, c2)
order = topological_sort(adj)
rows = transitive_closure(adj, c3)
pairs = sum(bin(r).count("1") for r in rows)
print(f"{name:15s} m={sum(map(len, adj))} BFS edges={c1.counts['edge']} SCCs={k} "
f"topological order: {'yes' if order else 'no (cycle)'} closure pairs={pairs} closure cost={c3.total}")
random DAG m=4000 BFS edges=20 SCCs=2000 topological order: yes closure pairs=26226 closure cost=136000
random digraph m=4000 BFS edges=3240 SCCs=718 topological order: no (cycle) closure pairs=2575275 closure cost=39584
On the random digraph a giant strongly connected component absorbs 1,283 of the 2,000 vertices; condensing it shares one closure row among all its members, which is why the closure of a graph with more reachable pairs costs fewer steps. This is the sharing mechanism (M-SHARE) at work inside a classical algorithm.
10.2 Shortest paths and spanning trees
| Algorithm | Problem | Requirement | Cost | Source |
|---|---|---|---|---|
| Dijkstra | single-source shortest paths | weights | with a binary heap | [Dijkstra 1959] |
| Bellman–Ford | single-source shortest paths, negative-cycle detection | none | [Bellman 1958; Ford 1956] | |
| Floyd–Warshall | all-pairs shortest paths | no negative cycles | [Cormen et al. 2022] | |
| Kruskal | minimum spanning forest | undirected | with union–find | [Kruskal 1956; Tarjan 1975] |
| Prim | minimum spanning forest | undirected | with a binary heap | [Prim 1957] |
All are implemented in cgtsdk.algorithms and tested against one another and against brute force: Dijkstra and Bellman–Ford against Floyd–Warshall on random weighted digraphs, Kruskal against Prim and against exhaustive search over edge subsets on small graphs.
In the path-algebra view of Chapter 6 all three shortest-path algorithms evaluate the same path grammar in the tropical semiring ; they differ in evaluation strategy. Dijkstra exploits a property of the algebra on non-negative weights — once a vertex is settled its distance never improves — to evaluate each vertex once in increasing order of distance. Bellman–Ford makes no such assumption and relaxes every edge up to times. Floyd–Warshall is Kleene’s algorithm for the closure of a matrix: it allows intermediate vertices in turn. The grammar is common; the costs come from the semantics’ algebraic properties and the evaluation order. A grammatical description, by itself, does not choose the order.
10.3 Evaluating a path grammar
We now make that last sentence precise. The closure (paths of length at least one) is the least relation satisfying either of two grammars:
Both have the same meaning (Chapter 6). Evaluate each bottom-up: start from the facts , and repeatedly apply the productions to newly derived facts until nothing new appears (semi-naive evaluation, standard in Datalog).
Let have vertices and edges, and let be the largest length of a shortest positive-length path between a pair in (pairs on cycles included).
- Semi-naive evaluation of the right-linear grammar makes exactly rule applications, which is at most .
- Semi-naive evaluation of the doubling grammar makes rule applications.
- With synchronous rounds, the right-linear grammar needs productive rounds and the doubling grammar .
(1) Initialization attempts each edge once: . A fact enters the work list only when first derived, so each fact of is processed exactly once; processing it applies once per in-edge , i.e. times. Soundness and completeness of semi-naive evaluation follow by induction on path length.
(2) Each fact is processed once, and processing joins it with the current and , at most attempts.
(3) By induction, after synchronous rounds the right-linear iterate contains exactly the pairs at distance in and the doubling iterate those at distance in .
∎Part 1 says something exact and somewhat deflating: evaluating the path grammar is computing the closure, with the work of one reverse search per target. The recursive description of paths, however elegant, does no less work than the classical algorithm. Parts 2 and 3 show that the shape of a grammar is a real cost parameter: the doubling grammar does asymptotically more work on long paths but needs exponentially fewer rounds — the trade between total work and parallel depth behind computing transitive closure by repeated squaring. This is well known in Datalog practice (linear versus non-linear recursion); the exact count in part 1 is, as far as this research has found, a convenient statement rather than a new result.
The SDK checks part 1 exactly on every test graph. The experiment of the ledger measured the doubling grammar on directed paths (CGT-EXP-007): its rule applications grew by a factor of about each time doubled (cubic), while the right-linear grammar’s grew by (quadratic).
from cgtsdk import Cost
from cgtsdk.algorithms import path_grammar_closure
for n in (32, 64, 128):
path = [[i + 1] if i + 1 < n else [] for i in range(n)]
lin, dbl = Cost(), Cost()
P1 = path_grammar_closure(path, "linear", lin)
P2 = path_grammar_closure(path, "doubling", dbl)
assert P1 == P2
pairs = sum(map(len, P1))
formula = (n - 1) + sum(len(P1[w]) * (1 if w > 0 else 0) for w in range(n))
print(f"n={n:4d} |P+|={pairs:5d} right-linear rules={lin.counts['rule']:6d} (formula {formula}) doubling rules={dbl.counts['rule']:8d}")
n= 32 |P+|= 496 right-linear rules= 496 (formula 496) doubling rules= 9516
n= 64 |P+|= 2016 right-linear rules= 2016 (formula 2016) doubling rules= 81500
n= 128 |P+|= 8128 right-linear rules= 8128 (formula 8128) doubling rules= 675004
(On a directed path every vertex except the first has in-degree , so the formula reduces to .)
10.4 The mapping, algorithm by algorithm
| Algorithm | Grammar it evaluates | Algebra | Evaluation strategy | Mechanism visible |
|---|---|---|---|---|
| BFS | from one source | Boolean / hop count | frontier by frontier | — (it is the search) |
| SCC condensation + bitsets | , all sources | Boolean | reverse topological, sharing rows | M-SHARE |
| Dijkstra | , non-negative | settle in distance order | algebraic monotonicity | |
| Bellman–Ford | global rounds | — | ||
| Floyd–Warshall | restricted by intermediate vertex | Kleene elimination | — | |
| semi-naive right-linear | Boolean | worklist | — (exact work count, THM-004) | |
| semi-naive doubling | Boolean | worklist / squaring | depth for work | |
| Kruskal / union–find | — (not a path problem) | — | greedy by weight | path compression is precomputation (M-PRE, amortized) |
The verdict for graph algorithms is again equivalence: each classical algorithm is an evaluation strategy for a path grammar in some algebra, and the grammar alone does not supply the strategy. The grammatical view earns its keep by making the shared structure explicit — which is what made the product construction of Chapter 8 and the constrained random surfer of Chapter 16 straightforward to derive.
Verify part 1 of Theorem 10.1 by hand on the graph with edges , , , .
Solution to Exercise 10.1
In-degrees: , , . All pairs are in (the graph is strongly connected), so each appears as first component in facts. Rule applications: .
Show that Dijkstra’s algorithm can return wrong distances with one negative edge and no negative cycle. Which algebraic property used by its evaluation strategy fails?
Design a third grammar for — for example (left-linear) — and derive its exact rule count under semi-naive evaluation. When is it cheaper than the right-linear grammar?
Solution to Exercise 10.3
Symmetrically to part 1, processing a new fact applies once per out-edge of , so the count is . It is cheaper when vertices that are frequently reached have small out-degree relative to the in-degree of vertices that frequently reach; on the reverse graph the two grammars exchange costs.
Using the SDK, compare transitive_closure (bitsets) with path_grammar_closure on random digraphs of increasing density. Explain the result in terms of the word-RAM’s word parallelism.
- Classical graph algorithms — traversal, components, shortest paths, spanning trees, closure — are implemented with counted costs and cross-checked against each other and brute force.
- Each path algorithm evaluates the grammar in an algebra; the strategy, not the grammar, determines the cost.
- Evaluating the right-linear path grammar costs exactly rule applications: it is closure computation (CGT-THM-004).
- Two grammars with the same meaning can differ asymptotically: doubling trades work on paths for rounds.
- The verdict is equivalence; the benefit of the grammatical view is a shared vocabulary that makes new constructions easy to derive.
References cited in this chapter
- Bellman 1958 R. Bellman (1958). On a routing problem. Quarterly of Applied Mathematics 16(1):87–90.
- Cormen et al. 2022 T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein (2022). Introduction to Algorithms, 4th edition. MIT Press (ISBN 978-0-262-04630-5).
- Dijkstra 1959 E. W. Dijkstra (1959). A note on two problems in connexion with graphs. Numerische Mathematik 1:269–271. doi:10.1007/BF01386390.
- Ford 1956 L. R. Ford Jr. (1956). Network flow theory. RAND Corporation Paper P-923.
- Kahn 1962 A. B. Kahn (1962). Topological sorting of large networks. Communications of the ACM 5(11):558–562. doi:10.1145/368996.369025.
- Kruskal 1956 J. B. Kruskal (1956). On the shortest spanning subtree of a graph and the traveling salesman problem. Proceedings of the AMS 7:48–50. doi:10.1090/S0002-9939-1956-0078686-7.
- Prim 1957 R. C. Prim (1957). Shortest connection networks and some generalizations. Bell System Technical Journal 36(6):1389–1401. doi:10.1002/j.1538-7305.1957.tb01515.x.
- Tarjan 1972 R. E. Tarjan (1972). Depth-first search and linear graph algorithms. SIAM Journal on Computing 1(2):146–160. doi:10.1137/0201010.
- Tarjan 1975 R. E. Tarjan (1975). Efficiency of a good but not linear set union algorithm. Journal of the ACM 22(2):215–225.