package unit; import support.TarjanAlgorithm; import support.TarjanAlgorithm.Node; import support.TarjanAlgorithm.Result; import java.util.List; /** * Unit tests for the Tarjan SCC defect in GraphUtils.java:186. * * Proves: * 1. Both implementations produce identical (correct) SCCs. * 2. The defective version performs exactly V*(V+1)/2 - 1 comparisons * on starWithBackEdges(V) — quadratic in V. * 3. The fixed version performs exactly V-1 comparisons — linear in V. * 4. Doubling V quadruples defective work; doubling V doubles fixed work. * * The key topology is starWithBackEdges(V): * N0 → N1..N(v-1) (tree edges) * N_k → N0 (back edges; k=1..v-1) * * N0 is never popped until all children finish. When N_k fires its back * edge, the stack is [N_k, N_(k-1), ..., N1, N0]. The linear scan must * walk k+1 elements to reach N0 at the bottom. * Total comparisons: sum(k+1, k=1..v-1) = v*(v+1)/2 - 1. * * No build tool required. Compile and run: * * cd tests * java -m jdk.compiler/com.sun.tools.javac.Main -cp . \ * support/TarjanAlgorithm.java unit/TarjanComplexityTest.java * java -cp . unit.TarjanComplexityTest */ public class TarjanComplexityTest { private static int passed = 0; private static int failed = 0; public static void main(String[] args) { System.out.println("=== TarjanComplexityTest ===\n"); System.out.println("-- Correctness --"); testCorrectnessStarBackEdges(); testCorrectnessPathBackEdge(); testCorrectnessLinearPath(); testCorrectnessIndependentCycles(); testCorrectnessDiamondNoBack(); testCorrectnessDiamondWithBack(); System.out.println("\n-- Complexity: defective vs fixed comparison counts --"); testDefectiveExactCounts(); testFixedExactCounts(); System.out.println("\n-- Complexity: path+back-edge (O(V) vs O(1) per back-edge) --"); testPathBackEdgeComparisons(); System.out.println("\n-- Complexity: growth ratio proves quadratic vs linear --"); testGrowthRatioQuadratic(); System.out.printf("\n%d passed, %d failed%n", passed, failed); if (failed > 0) System.exit(1); } // ─── Correctness ───────────────────────────────────────────────────────── static void testCorrectnessStarBackEdges() { // starWithBackEdges(5): N0→{N1..N4}, N1..N4→N0 → one SCC of size 5 Result def = TarjanAlgorithm.tarjanDefective(TarjanAlgorithm.starWithBackEdges(5)); Result fix = TarjanAlgorithm.tarjanFixed(TarjanAlgorithm.starWithBackEdges(5)); assertEqual("star-back-5: defective SCC count", 1, def.sccs.size()); assertEqual("star-back-5: fixed SCC count", 1, fix.sccs.size()); assertEqual("star-back-5: defective SCC size", 5, def.sccs.get(0).size()); assertEqual("star-back-5: fixed SCC size", 5, fix.sccs.get(0).size()); } static void testCorrectnessPathBackEdge() { // pathWithBackEdge(4): N0→N1→N2→N3→N0 → one SCC of size 4 Result def = TarjanAlgorithm.tarjanDefective(TarjanAlgorithm.pathWithBackEdge(4)); Result fix = TarjanAlgorithm.tarjanFixed(TarjanAlgorithm.pathWithBackEdge(4)); assertEqual("path-back-4: defective SCC count", 1, def.sccs.size()); assertEqual("path-back-4: fixed SCC count", 1, fix.sccs.size()); assertEqual("path-back-4: defective SCC size", 4, def.sccs.get(0).size()); assertEqual("path-back-4: fixed SCC size", 4, fix.sccs.get(0).size()); } static void testCorrectnessLinearPath() { // linearPath(4): N0→N1→N2→N3 → 4 singleton SCCs Result def = TarjanAlgorithm.tarjanDefective(TarjanAlgorithm.linearPath(4)); Result fix = TarjanAlgorithm.tarjanFixed(TarjanAlgorithm.linearPath(4)); assertEqual("linear-4: defective SCC count", 4, def.sccs.size()); assertEqual("linear-4: fixed SCC count", 4, fix.sccs.size()); for (int i = 0; i < 4; i++) { assertEqual("linear-4: defective SCC[" + i + "] size", 1, def.sccs.get(i).size()); } } static void testCorrectnessIndependentCycles() { // 3 independent cycles of length 3 → 3 SCCs of size 3 Result def = TarjanAlgorithm.tarjanDefective(buildCycles(3, 3)); Result fix = TarjanAlgorithm.tarjanFixed(buildCycles(3, 3)); assertEqual("3x3-cycles: defective SCC count", 3, def.sccs.size()); assertEqual("3x3-cycles: fixed SCC count", 3, fix.sccs.size()); for (int i = 0; i < 3; i++) { assertEqual("3x3-cycles: defective SCC[" + i + "] size", 3, def.sccs.get(i).size()); assertEqual("3x3-cycles: fixed SCC[" + i + "] size", 3, fix.sccs.get(i).size()); } } static void testCorrectnessDiamondNoBack() { // N0→{N1,N2}→N3, no back edge → 4 singleton SCCs Result def = TarjanAlgorithm.tarjanDefective(buildDiamond(false)); Result fix = TarjanAlgorithm.tarjanFixed(buildDiamond(false)); assertEqual("diamond-no-back: defective SCC count", 4, def.sccs.size()); assertEqual("diamond-no-back: fixed SCC count", 4, fix.sccs.size()); } static void testCorrectnessDiamondWithBack() { // N0→{N1,N2}→N3→N0 → 1 SCC of size 4 Result def = TarjanAlgorithm.tarjanDefective(buildDiamond(true)); Result fix = TarjanAlgorithm.tarjanFixed(buildDiamond(true)); assertEqual("diamond-back: defective SCC count", 1, def.sccs.size()); assertEqual("diamond-back: fixed SCC count", 1, fix.sccs.size()); assertEqual("diamond-back: defective SCC size", 4, def.sccs.get(0).size()); assertEqual("diamond-back: fixed SCC size", 4, fix.sccs.get(0).size()); } // ─── Exact comparison counts ────────────────────────────────────────────── /** * PROVES DEFECT: starWithBackEdges(V) forces exactly V*(V+1)/2 - 1 comparisons. * * Derivation: * When N_k fires its back edge to N0, stack = [N_k, N_(k-1), ..., N0]. * listContains scans k+1 elements before finding N0 at index k (0-based). * Total: sum(k+1, k=1..V-1) = sum(2..V) = V*(V+1)/2 - 1. */ static void testDefectiveExactCounts() { System.out.println("[defective] starWithBackEdges(V) — expected V*(V+1)/2 - 1:"); int[] sizes = {5, 10, 20, 50, 100}; for (int v : sizes) { Result r = TarjanAlgorithm.tarjanDefective(TarjanAlgorithm.starWithBackEdges(v)); long expected = (long) v * (v + 1) / 2 - 1; System.out.printf(" V=%-4d actual=%-8d expected=%-8d %s%n", v, r.comparisons, expected, r.comparisons == expected ? "PASS" : "FAIL expected=" + expected); assertEqual("defective V=" + v, expected, r.comparisons); } } /** * PROVES FIX: starWithBackEdges(V) requires exactly V-1 comparisons with the fix. * One comparison per back edge (one per non-tree edge that triggers the else branch). */ static void testFixedExactCounts() { System.out.println("[fixed] starWithBackEdges(V) — expected V-1:"); int[] sizes = {5, 10, 20, 50, 100}; for (int v : sizes) { Result r = TarjanAlgorithm.tarjanFixed(TarjanAlgorithm.starWithBackEdges(v)); long expected = v - 1; System.out.printf(" V=%-4d actual=%-8d expected=%-8d %s%n", v, r.comparisons, expected, r.comparisons == expected ? "PASS" : "FAIL"); assertEqual("fixed V=" + v, expected, r.comparisons); } } /** * Path+back-edge: one back edge N(v-1)→N0. * Stack when back edge fires: [N(v-1),...,N0], size=V. * N0 is at the bottom → defective scans V elements, fixed scans 1. */ static void testPathBackEdgeComparisons() { int[] sizes = {5, 10, 20}; for (int v : sizes) { Result def = TarjanAlgorithm.tarjanDefective(TarjanAlgorithm.pathWithBackEdge(v)); Result fix = TarjanAlgorithm.tarjanFixed(TarjanAlgorithm.pathWithBackEdge(v)); System.out.printf(" V=%-4d defective=%-6d fixed=%-6d%n", v, def.comparisons, fix.comparisons); assertEqual("path-back V=" + v + " defective", (long) v, def.comparisons); assertEqual("path-back V=" + v + " fixed", 1L, fix.comparisons); } } // ─── Growth ratio ───────────────────────────────────────────────────────── /** * PROVES QUADRATIC GROWTH: * When V doubles, defective comparisons quadruple (≈4x), fixed double (≈2x). * * Math: * defective(V) = V*(V+1)/2 - 1 ≈ V²/2 * defective(2V) = 2V*(2V+1)/2-1 ≈ 2V² = 4 * V²/2 * ratio ≈ 4 * * fixed(V) = V-1 * fixed(2V) = 2V-1 * ratio ≈ 2 */ static void testGrowthRatioQuadratic() { int[][] pairs = {{10, 20}, {20, 40}, {50, 100}, {100, 200}}; for (int[] pair : pairs) { int v1 = pair[0], v2 = pair[1]; long def1 = TarjanAlgorithm.tarjanDefective(TarjanAlgorithm.starWithBackEdges(v1)).comparisons; long def2 = TarjanAlgorithm.tarjanDefective(TarjanAlgorithm.starWithBackEdges(v2)).comparisons; long fix1 = TarjanAlgorithm.tarjanFixed(TarjanAlgorithm.starWithBackEdges(v1)).comparisons; long fix2 = TarjanAlgorithm.tarjanFixed(TarjanAlgorithm.starWithBackEdges(v2)).comparisons; double defRatio = (double) def2 / def1; double fixRatio = (double) fix2 / fix1; System.out.printf(" V %d→%d: defective ratio=%.2f (expect ~4.0) fixed ratio=%.2f (expect ~2.0)%n", v1, v2, defRatio, fixRatio); assertTrue("defective V=" + v1 + "→" + v2 + " ratio ≥ 3.8", defRatio >= 3.8); assertTrue("defective V=" + v1 + "→" + v2 + " ratio ≤ 4.2", defRatio <= 4.2); assertTrue("fixed V=" + v1 + "→" + v2 + " ratio ≥ 1.9", fixRatio >= 1.9); // exact ratio = (2V-1)/(V-1) → approaches 2.0 as V→∞; ≤2.12 for V≥10 assertTrue("fixed V=" + v1 + "→" + v2 + " ratio ≤ 2.15", fixRatio <= 2.15); } } // ─── Graph builders ─────────────────────────────────────────────────────── static List buildCycles(int k, int len) { List all = new java.util.ArrayList<>(); for (int c = 0; c < k; c++) { List cycle = new java.util.ArrayList<>(); for (int i = 0; i < len; i++) cycle.add(new Node("C" + c + "_N" + i)); for (int i = 0; i < len - 1; i++) cycle.get(i).addDep(cycle.get(i + 1)); cycle.get(len - 1).addDep(cycle.get(0)); all.addAll(cycle); } return all; } static List buildDiamond(boolean backEdge) { Node n0 = new Node("N0"), n1 = new Node("N1"), n2 = new Node("N2"), n3 = new Node("N3"); n0.addDep(n1); n0.addDep(n2); n1.addDep(n3); n2.addDep(n3); if (backEdge) n3.addDep(n0); return java.util.List.of(n0, n1, n2, n3); } // ─── Helpers ───────────────────────────────────────────────────────────── static void assertEqual(String name, long expected, long actual) { if (expected == actual) { System.out.printf(" PASS %s%n", name); passed++; } else { System.out.printf(" FAIL %s expected=%d actual=%d%n", name, expected, actual); failed++; } } static void assertTrue(String name, boolean condition) { if (condition) { System.out.printf(" PASS %s%n", name); passed++; } else { System.out.printf(" FAIL %s%n", name); failed++; } } }