Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
244 lines
9.1 KiB
Java
244 lines
9.1 KiB
Java
package support;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* Self-contained Tarjan SCC implementations — defective and fixed.
|
|
*
|
|
* These are verbatim ports of GraphUtils.Tarjan (the private inner class)
|
|
* with one difference: the defective version uses list.contains(n) and the
|
|
* fixed version uses n.onStack. An operation counter is exposed so tests can
|
|
* assert exact comparison counts without relying on timing.
|
|
*
|
|
* This class has no dependency on jdk.compiler internals.
|
|
*
|
|
* KEY TOPOLOGY — starWithBackEdges(V):
|
|
* N0 → N1, N0 → N2, ..., N0 → N(v-1) (tree edges from N0)
|
|
* N_k → N0 for k=1..v-1 (back edges to already-visited N0)
|
|
*
|
|
* DFS visits N0 first, then each N_k in order. N0 is never popped until
|
|
* all children finish (lowlink=0 != index=k for each child). So when N_k
|
|
* fires its back edge to N0, the stack is [N_k, N_(k-1), ..., N1, N0]:
|
|
* - defective: listContains scans k+1 elements to find N0 at the bottom
|
|
* - fixed: n.onStack read in 1 operation
|
|
*
|
|
* Exact comparison counts:
|
|
* defective: 2+3+...+V = V*(V+1)/2 - 1
|
|
* fixed: 1*(V-1) = V-1
|
|
*
|
|
* Growth when V doubles: defective ≈4x (quadratic), fixed ≈2x (linear).
|
|
*/
|
|
public class TarjanAlgorithm {
|
|
|
|
// ─── Shared node base ───────────────────────────────────────────────────
|
|
|
|
public static class Node {
|
|
public final String label;
|
|
public final List<Node> deps = new ArrayList<>();
|
|
|
|
// Tarjan bookkeeping
|
|
int index = -1;
|
|
int lowlink = -1;
|
|
boolean onStack = false; // used by FIXED version
|
|
|
|
public Node(String label) { this.label = label; }
|
|
public void addDep(Node dep) { deps.add(dep); }
|
|
@Override public String toString() { return label; }
|
|
}
|
|
|
|
// ─── Result ─────────────────────────────────────────────────────────────
|
|
|
|
public static class Result {
|
|
public final List<List<Node>> sccs;
|
|
/**
|
|
* Number of element-level comparisons made during stack membership checks.
|
|
* Defective: each call to listContains() walks the list until it finds the target.
|
|
* Fixed: each call to n.onStack counts as exactly 1 comparison.
|
|
*/
|
|
public final long comparisons;
|
|
|
|
Result(List<List<Node>> sccs, long comparisons) {
|
|
this.sccs = sccs;
|
|
this.comparisons = comparisons;
|
|
}
|
|
}
|
|
|
|
// ─── DEFECTIVE implementation ───────────────────────────────────────────
|
|
// Mirrors GraphUtils.java:186 — uses list.contains(n) for on-stack check.
|
|
// We count each element-level comparison inside the linear scan.
|
|
|
|
public static Result tarjanDefective(List<Node> nodes) {
|
|
return new DefectiveTarjan().run(nodes);
|
|
}
|
|
|
|
private static class DefectiveTarjan {
|
|
int index = 0;
|
|
long comparisons = 0;
|
|
final List<Node> stack = new ArrayList<>();
|
|
final List<List<Node>> sccs = new ArrayList<>();
|
|
|
|
Result run(List<Node> nodes) {
|
|
for (Node n : nodes) {
|
|
if (n.index == -1) visit(n);
|
|
}
|
|
return new Result(sccs, comparisons);
|
|
}
|
|
|
|
void visit(Node v) {
|
|
v.index = index;
|
|
v.lowlink = index;
|
|
index++;
|
|
stack.add(0, v); // prepend — matches ListBuffer.prepend()
|
|
|
|
for (Node n : v.deps) {
|
|
if (n.index == -1) {
|
|
visit(n);
|
|
v.lowlink = Math.min(v.lowlink, n.lowlink);
|
|
} else {
|
|
// DEFECT (GraphUtils.java:186): linear scan through the stack
|
|
if (listContains(stack, n)) {
|
|
v.lowlink = Math.min(v.lowlink, n.index);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (v.lowlink == v.index) {
|
|
List<Node> scc = new ArrayList<>();
|
|
Node n;
|
|
do {
|
|
n = stack.remove(0);
|
|
n.onStack = false;
|
|
scc.add(n);
|
|
} while (n != v);
|
|
sccs.add(scc);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Counts every element comparison made during the linear scan.
|
|
* This is what ListBuffer.contains() does internally.
|
|
*/
|
|
private boolean listContains(List<Node> list, Node target) {
|
|
for (Node n : list) {
|
|
comparisons++; // each element access = one comparison
|
|
if (n == target) return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ─── FIXED implementation ────────────────────────────────────────────────
|
|
// Uses n.onStack for O(1) membership check — the correct approach.
|
|
// Exactly one comparison per non-tree edge: read n.onStack.
|
|
|
|
public static Result tarjanFixed(List<Node> nodes) {
|
|
return new FixedTarjan().run(nodes);
|
|
}
|
|
|
|
private static class FixedTarjan {
|
|
int index = 0;
|
|
long comparisons = 0;
|
|
final List<Node> stack = new ArrayList<>();
|
|
final List<List<Node>> sccs = new ArrayList<>();
|
|
|
|
Result run(List<Node> nodes) {
|
|
for (Node n : nodes) {
|
|
if (n.index == -1) visit(n);
|
|
}
|
|
return new Result(sccs, comparisons);
|
|
}
|
|
|
|
void visit(Node v) {
|
|
v.index = index;
|
|
v.lowlink = index;
|
|
index++;
|
|
stack.add(0, v);
|
|
v.onStack = true;
|
|
|
|
for (Node n : v.deps) {
|
|
if (n.index == -1) {
|
|
visit(n);
|
|
v.lowlink = Math.min(v.lowlink, n.lowlink);
|
|
} else {
|
|
comparisons++; // exactly one operation: read n.onStack
|
|
if (n.onStack) {
|
|
v.lowlink = Math.min(v.lowlink, n.index);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (v.lowlink == v.index) {
|
|
List<Node> scc = new ArrayList<>();
|
|
Node n;
|
|
do {
|
|
n = stack.remove(0);
|
|
n.onStack = false;
|
|
scc.add(n);
|
|
} while (n != v);
|
|
sccs.add(scc);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── Graph factory ───────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Star-with-back-edges graph on V nodes.
|
|
*
|
|
* Edges:
|
|
* Tree: N0→N1, N0→N2, ..., N0→N(v-1)
|
|
* Back: N_k→N0 for k=1..v-1
|
|
*
|
|
* This is ONE large SCC (all nodes reachable from N0 and back).
|
|
*
|
|
* Exact defective comparisons: V*(V+1)/2 - 1 (quadratic in V)
|
|
* Exact fixed comparisons: V-1 (linear in V)
|
|
*
|
|
* Why defective is quadratic: N0 is pushed first and stays on the stack
|
|
* until all children finish. When N_k fires its back edge N_k→N0, the
|
|
* stack is [N_k, N_(k-1), ..., N1, N0]. listContains must scan all k+1
|
|
* elements to find N0 at the bottom. Total: sum(k+1, k=1..V-1) = V*(V+1)/2 - 1.
|
|
*/
|
|
public static List<Node> starWithBackEdges(int v) {
|
|
List<Node> nodes = new ArrayList<>();
|
|
for (int i = 0; i < v; i++) nodes.add(new Node("N" + i));
|
|
// tree edges N0 → N1..N(v-1)
|
|
for (int k = 1; k < v; k++) nodes.get(0).addDep(nodes.get(k));
|
|
// back edges N_k → N0
|
|
for (int k = 1; k < v; k++) nodes.get(k).addDep(nodes.get(0));
|
|
return nodes;
|
|
}
|
|
|
|
/**
|
|
* Simple cycle (path with one back edge): N0→N1→…→N(v-1)→N0
|
|
*
|
|
* ONE SCC of size V. Only ONE non-tree edge fires (the back edge N(v-1)→N0).
|
|
*
|
|
* Defective comparisons: V (scan entire stack to find N0 at bottom)
|
|
* Fixed comparisons: 1 (read N0.onStack)
|
|
*
|
|
* Demonstrates O(V) vs O(1) per back-edge, not the full O(V²) pattern.
|
|
* Use starWithBackEdges() for the O(V²) proof.
|
|
*/
|
|
public static List<Node> pathWithBackEdge(int v) {
|
|
List<Node> nodes = new ArrayList<>();
|
|
for (int i = 0; i < v; i++) nodes.add(new Node("N" + i));
|
|
for (int i = 0; i < v - 1; i++) nodes.get(i).addDep(nodes.get(i + 1));
|
|
nodes.get(v - 1).addDep(nodes.get(0));
|
|
return nodes;
|
|
}
|
|
|
|
/**
|
|
* Linear path with no cycles: N0→N1→…→N(v-1)
|
|
*
|
|
* V singleton SCCs. No back edges → the defective `else` branch never fires.
|
|
* comparisons = 0 for both versions. Used for correctness checks only.
|
|
*/
|
|
public static List<Node> linearPath(int v) {
|
|
List<Node> nodes = new ArrayList<>();
|
|
for (int i = 0; i < v; i++) nodes.add(new Node("N" + i));
|
|
for (int i = 0; i < v - 1; i++) nodes.get(i).addDep(nodes.get(i + 1));
|
|
return nodes;
|
|
}
|
|
}
|