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 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> 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> 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 nodes) { return new DefectiveTarjan().run(nodes); } private static class DefectiveTarjan { int index = 0; long comparisons = 0; final List stack = new ArrayList<>(); final List> sccs = new ArrayList<>(); Result run(List 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 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 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 nodes) { return new FixedTarjan().run(nodes); } private static class FixedTarjan { int index = 0; long comparisons = 0; final List stack = new ArrayList<>(); final List> sccs = new ArrayList<>(); Result run(List 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 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 starWithBackEdges(int v) { List 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 pathWithBackEdge(int v) { List 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 linearPath(int v) { List 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; } }