java-topology/defects/nim/unit/NimCyclicTreeAlgorithm.java

160 lines
6.1 KiB
Java

package unit;
import java.util.*;
/**
* NimCyclicTreeAlgorithm — CWE-407 test for nim-0002
*
* Models trees.cyclicTreeAux(n, visited):
* slow: for v in visited: if v == n — O(N) scan per node, O(N²) for path tree
* fast: HashSet[pointer] visited — O(1) per node, O(N) total
*
* Test: on a path graph (depth=N), slow does O(N²) scans; fast does O(N) ops.
*/
public class NimCyclicTreeAlgorithm {
// Simple tree node (PNode proxy)
static class PNode {
final int id;
final List<PNode> sons;
PNode(int id) { this.id = id; this.sons = new ArrayList<>(); }
}
// --- SLOW: O(N²) ---
// for v in visited: if v == n: return true
// visited is a path-stack (ArrayList acting as seq with add/remove)
static class SlowCyclicCheck {
long scanOps = 0;
boolean cyclicTreeAux(PNode n, List<PNode> visited) {
if (n == null) return false;
// Linear scan of visited (the path stack)
for (PNode v : visited) {
scanOps++;
if (v == n) return true;
}
visited.add(n);
for (PNode son : n.sons) {
if (cyclicTreeAux(son, visited)) return true;
}
visited.remove(visited.size() - 1);
return false;
}
boolean cyclicTree(PNode n) {
List<PNode> visited = new ArrayList<>();
return cyclicTreeAux(n, visited);
}
}
// --- FAST: O(N) ---
// visited is a HashSet[pointer] (IdentityHashMap backed set)
static class FastCyclicCheck {
long hashOps = 0;
// Using IdentityHashMap to simulate pointer-based HashSet
boolean cyclicTreeAux(PNode n, Set<PNode> visited) {
if (n == null) return false;
hashOps++;
if (visited.contains(n)) return true; // O(1) identity hash
visited.add(n);
for (PNode son : n.sons) {
if (cyclicTreeAux(son, visited)) return true;
}
visited.remove(n);
return false;
}
boolean cyclicTree(PNode n) {
// IdentityHashMap simulates pointer-equality HashSet (Nim's HashSet[pointer])
Set<PNode> visited = Collections.newSetFromMap(new IdentityHashMap<>());
return cyclicTreeAux(n, visited);
}
}
// Build an acyclic path graph: 0 -> 1 -> 2 -> ... -> n-1
// This is worst case for the slow algorithm: depth = n, visited grows to n
static PNode buildPathGraph(int n) {
PNode[] nodes = new PNode[n];
for (int i = 0; i < n; i++) nodes[i] = new PNode(i);
for (int i = 0; i < n - 1; i++) nodes[i].sons.add(nodes[i + 1]);
return nodes[0];
}
// Build a balanced binary tree of depth d (no cycles)
static PNode buildBinaryTree(int depth) {
if (depth == 0) return new PNode(0);
PNode root = new PNode(depth);
root.sons.add(buildBinaryTree(depth - 1));
root.sons.add(buildBinaryTree(depth - 1));
return root;
}
public static void main(String[] args) {
System.out.println("NimCyclicTreeAlgorithm — nim-0002");
System.out.println(" Pattern: for v in visited: if v == n — O(N²) vs HashSet — O(N)");
System.out.println();
int passed = 0;
int total = 0;
// Test 1: Path graphs (worst case for O(N²))
int[] pathSizes = {100, 300, 500, 800};
for (int n : pathSizes) {
PNode root = buildPathGraph(n);
SlowCyclicCheck slow = new SlowCyclicCheck();
FastCyclicCheck fast = new FastCyclicCheck();
boolean slowResult = slow.cyclicTree(root);
boolean fastResult = fast.cyclicTree(root);
long slowOps = slow.scanOps;
long fastOps = fast.hashOps;
// No cycles — both should return false
boolean correctResult = !slowResult && !fastResult;
// Slow: at depth d, visited has d nodes. Sum for path of N nodes: 0+1+2+...+(N-1) = N(N-1)/2
boolean slowIsQuadratic = slowOps >= (long) n * (n - 1) / 4; // conservative
boolean fastIsLinear = fastOps <= n + 1; // at most N+1 hash checks
total += 3;
if (correctResult) { System.out.println("PASS path N=" + n + ": no cycle detected (correct)"); passed++; }
else { System.out.println("FAIL path N=" + n + ": wrong cycle detection slow=" + slowResult + " fast=" + fastResult); }
if (slowIsQuadratic) { System.out.println("PASS path N=" + n + ": slow O(N²) scans=" + slowOps + " >= N(N-1)/4=" + (n * (n - 1) / 4)); passed++; }
else { System.out.println("FAIL path N=" + n + ": slow not quadratic, scans=" + slowOps); }
if (fastIsLinear) { System.out.println("PASS path N=" + n + ": fast O(N) ops=" + fastOps + " <= N+1=" + (n + 1)); passed++; }
else { System.out.println("FAIL path N=" + n + ": fast not linear, ops=" + fastOps); }
}
// Test 2: Cycle detection correctness — introduce a back edge
{
PNode[] nodes = new PNode[5];
for (int i = 0; i < 5; i++) nodes[i] = new PNode(i);
nodes[0].sons.add(nodes[1]);
nodes[1].sons.add(nodes[2]);
nodes[2].sons.add(nodes[0]); // cycle: 2 -> 0
SlowCyclicCheck slow = new SlowCyclicCheck();
FastCyclicCheck fast = new FastCyclicCheck();
boolean slowDetects = slow.cyclicTree(nodes[0]);
boolean fastDetects = fast.cyclicTree(nodes[0]);
total += 2;
if (slowDetects) { System.out.println("PASS cycle: slow correctly detects cycle"); passed++; }
else { System.out.println("FAIL cycle: slow missed cycle"); }
if (fastDetects) { System.out.println("PASS cycle: fast correctly detects cycle"); passed++; }
else { System.out.println("FAIL cycle: fast missed cycle"); }
}
System.out.println();
System.out.println(passed + "/" + total + " PASS");
if (passed != total) {
throw new AssertionError(passed + "/" + total + " tests passed");
}
}
}