160 lines
6.1 KiB
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");
|
|
}
|
|
}
|
|
}
|