From ccb76d1a2a6331d4603a88784d7659640c836b1f Mon Sep 17 00:00:00 2001 From: William Fiset Date: Tue, 31 Mar 2026 18:36:16 -0700 Subject: [PATCH 1/3] Refactor Heapsort: add docs, simplify sink with for loop (#1310) * Refactor Heapsort: add docs, simplify sink with for loop Co-Authored-By: Claude Opus 4.6 * Revert sink to while loop, improve comments Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .../algorithms/sorting/Heapsort.java | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/williamfiset/algorithms/sorting/Heapsort.java b/src/main/java/com/williamfiset/algorithms/sorting/Heapsort.java index 16265099d..35d3929a0 100644 --- a/src/main/java/com/williamfiset/algorithms/sorting/Heapsort.java +++ b/src/main/java/com/williamfiset/algorithms/sorting/Heapsort.java @@ -1,11 +1,15 @@ /** - * Implementation of heapsort + * In-place heapsort. Builds a max-heap in O(n), then repeatedly extracts the maximum. + * + *

Time: O(n log(n)) + * + *

Space: O(1) * * @author William Fiset, william.alexandre.fiset@gmail.com */ package com.williamfiset.algorithms.sorting; -import java.util.*; +import java.util.Arrays; public class Heapsort implements InplaceSort { @@ -15,39 +19,38 @@ public void sort(int[] values) { } private static void heapsort(int[] ar) { - if (ar == null) return; + if (ar == null) + return; int n = ar.length; - // Heapify, converts array into binary heap O(n), see: - // http://www.cs.umd.edu/~meesh/351/mount/lectures/lect14-heapsort-analysis-part.pdf - for (int i = Math.max(0, (n / 2) - 1); i >= 0; i--) { + // Build max-heap from the array bottom-up in O(n). + for (int i = n / 2 - 1; i >= 0; i--) sink(ar, n, i); - } - // Sorting bit + // Extract elements one by one: move the root (max element) to the end of the unsorted + // region, shrink the heap by one, and sink the new root to restore the heap property. for (int i = n - 1; i >= 0; i--) { swap(ar, 0, i); sink(ar, i, 0); } } + // Sinks element at index i down to its correct position in a max-heap of size n. private static void sink(int[] ar, int n, int i) { while (true) { - int left = 2 * i + 1; // Left node - int right = 2 * i + 2; // Right node + int left = 2 * i + 1; + int right = 2 * i + 2; int largest = i; - // Right child is larger than parent - if (right < n && ar[right] > ar[largest]) largest = right; + if (left < n && ar[left] > ar[largest]) + largest = left; + if (right < n && ar[right] > ar[largest]) + largest = right; + if (largest == i) + return; - // Left child is larger than parent - if (left < n && ar[left] > ar[largest]) largest = left; - - // Move down the tree following the largest node - if (largest != i) { - swap(ar, largest, i); - i = largest; - } else break; + swap(ar, largest, i); + i = largest; } } @@ -57,14 +60,10 @@ private static void swap(int[] ar, int i, int j) { ar[j] = tmp; } - /* TESTING */ - public static void main(String[] args) { Heapsort sorter = new Heapsort(); int[] array = {10, 4, 6, 4, 8, -13, 2, 3}; sorter.sort(array); - // Prints: - // [-13, 2, 3, 4, 4, 6, 8, 10] - System.out.println(java.util.Arrays.toString(array)); + System.out.println(Arrays.toString(array)); // [-13, 2, 3, 4, 4, 6, 8, 10] } } From a0857a6fa012a1c3366087f8dbeee6badf48edb2 Mon Sep 17 00:00:00 2001 From: William Fiset Date: Tue, 31 Mar 2026 18:36:26 -0700 Subject: [PATCH 2/3] Refactor Lazy and Eager Prim's MST implementations (#1311) * Refactor Lazy and Eager Prim's MST implementations: clean up, add docs, trim IPQ Co-Authored-By: Claude Opus 4.6 * Add tests for Lazy and Eager Prim's MST implementations Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .../graphtheory/EagerPrimsAdjacencyList.java | 414 ++++-------------- .../graphtheory/LazyPrimsAdjacencyList.java | 212 +++------ .../williamfiset/algorithms/graphtheory/BUILD | 22 + .../EagerPrimsAdjacencyListTest.java | 219 +++++++++ .../LazyPrimsAdjacencyListTest.java | 188 ++++++++ 5 files changed, 570 insertions(+), 485 deletions(-) create mode 100644 src/test/java/com/williamfiset/algorithms/graphtheory/EagerPrimsAdjacencyListTest.java create mode 100644 src/test/java/com/williamfiset/algorithms/graphtheory/LazyPrimsAdjacencyListTest.java diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/EagerPrimsAdjacencyList.java b/src/main/java/com/williamfiset/algorithms/graphtheory/EagerPrimsAdjacencyList.java index 64ba0fce2..8a38c1590 100644 --- a/src/main/java/com/williamfiset/algorithms/graphtheory/EagerPrimsAdjacencyList.java +++ b/src/main/java/com/williamfiset/algorithms/graphtheory/EagerPrimsAdjacencyList.java @@ -1,15 +1,19 @@ /** - * An implementation of the eager version of Prim's algorithm which relies on using an indexed - * priority queue data structure to query the next best edge. + * Eager implementation of Prim's minimum spanning tree algorithm using an indexed priority queue + * (IPQ). * - *

Time Complexity: O(ElogV) + *

"Eager" because when a better edge to a frontier node is found, the IPQ entry is updated + * in-place (via {@code decrease}), so stale edges never accumulate — unlike the lazy variant which + * leaves them in the queue. + * + *

Time: O(E log(V)) + * + *

Space: O(V + E) * * @author William Fiset, william.alexandre.fiset@gmail.com */ package com.williamfiset.algorithms.graphtheory; -import static java.lang.Math.*; - import java.util.*; public class EagerPrimsAdjacencyList { @@ -25,102 +29,93 @@ public Edge(int from, int to, int cost) { @Override public int compareTo(Edge other) { - return cost - other.cost; + return Integer.compare(cost, other.cost); } } - // Inputs private final int n; private final List> graph; - // Internal private boolean solved; private boolean mstExists; private boolean[] visited; private MinIndexedDHeap ipq; - // Outputs private long minCostSum; private Edge[] mstEdges; + /** + * Creates an Eager Prim's MST solver for the given graph. + * + * @param graph adjacency list where each node maps to a list of weighted edges. + * @throws IllegalArgumentException if the graph is null or empty. + */ public EagerPrimsAdjacencyList(List> graph) { - if (graph == null || graph.isEmpty()) throw new IllegalArgumentException(); + if (graph == null || graph.isEmpty()) + throw new IllegalArgumentException(); this.n = graph.size(); this.graph = graph; } - // Returns the edges used in finding the minimum spanning tree, - // or returns null if no MST exists. + /** Returns the MST edges, or null if no MST exists. */ public Edge[] getMst() { solve(); return mstExists ? mstEdges : null; } + /** Returns the MST total cost, or null if no MST exists. */ public Long getMstCost() { solve(); return mstExists ? minCostSum : null; } - private void relaxEdgesAtNode(int currentNodeIndex) { - visited[currentNodeIndex] = true; - - // edges will never be null if the createEmptyGraph method was used to build the graph. - List edges = graph.get(currentNodeIndex); + private void relaxEdgesAtNode(int node) { + visited[node] = true; + for (Edge edge : graph.get(node)) { + if (visited[edge.to]) + continue; - for (Edge edge : edges) { - int destNodeIndex = edge.to; - - // Skip edges pointing to already visited nodes. - if (visited[destNodeIndex]) continue; - - if (ipq.contains(destNodeIndex)) { - // Try and improve the cheapest edge at destNodeIndex with the current edge in the IPQ. - ipq.decrease(destNodeIndex, edge); - } else { - // Insert edge for the first time. - ipq.insert(destNodeIndex, edge); - } + if (ipq.contains(edge.to)) + ipq.decrease(edge.to, edge); + else + ipq.insert(edge.to, edge); } } - // Computes the minimum spanning tree and minimum spanning tree cost. private void solve() { - if (solved) return; + if (solved) + return; solved = true; - int m = n - 1, edgeCount = 0; + int m = n - 1; + int edgeCount = 0; visited = new boolean[n]; mstEdges = new Edge[m]; - // The degree of the d-ary heap supporting the IPQ can greatly impact performance, especially - // on dense graphs. The base 2 logarithm of n is a decent value based on my quick experiments - // (even better than E/V in many cases). - int degree = (int) Math.ceil(Math.log(n) / Math.log(2)); - ipq = new MinIndexedDHeap<>(max(2, degree), n); + // The degree of the d-ary heap can greatly impact performance, especially on dense graphs. + // The base-2 logarithm of n is a good heuristic. + int degree = Math.max(2, (int) Math.ceil(Math.log(n) / Math.log(2))); + ipq = new MinIndexedDHeap<>(degree, n); - // Add initial set of edges to the priority queue starting at node 0. relaxEdgesAtNode(0); while (!ipq.isEmpty() && edgeCount != m) { - int destNodeIndex = ipq.peekMinKeyIndex(); // equivalently: edge.to + int destNode = ipq.peekMinKeyIndex(); Edge edge = ipq.pollMinValue(); mstEdges[edgeCount++] = edge; minCostSum += edge.cost; - - relaxEdgesAtNode(destNodeIndex); + relaxEdgesAtNode(destNode); } - // Verify MST spans entire graph. mstExists = (edgeCount == m); } - /* Graph construction helpers. */ - - // Creates an empty adjacency list graph with n nodes. + /** Creates an adjacency list with n nodes. */ static List> createEmptyGraph(int n) { List> g = new ArrayList<>(); - for (int i = 0; i < n; i++) g.add(new ArrayList<>()); + for (int i = 0; i < n; i++) + g.add(new ArrayList<>()); return g; } @@ -133,19 +128,20 @@ static void addUndirectedEdge(List> g, int from, int to, int cost) { addDirectedEdge(g, to, from, cost); } - /* Example usage. */ - public static void main(String[] args) { - // example1(); - // firstGraphFromSlides(); - // squareGraphFromSlides(); - // disjointOnFirstNode(); - // disjointGraph(); - eagerPrimsExampleFromSlides(); - // lazyVsEagerAnalysis(); + exampleConnectedGraph(); + System.out.println(); + exampleGraphWithNegativeEdges(); + System.out.println(); + exampleSquareGraph(); + System.out.println(); + exampleDisjointFromStart(); + System.out.println(); + exampleDisconnectedGraph(); } - private static void example1() { + // Example 1: Connected graph with 10 nodes. MST cost = 14. + private static void exampleConnectedGraph() { int n = 10; List> g = createEmptyGraph(n); @@ -169,31 +165,11 @@ private static void example1() { addUndirectedEdge(g, 7, 8, 6); EagerPrimsAdjacencyList solver = new EagerPrimsAdjacencyList(g); - Long cost = solver.getMstCost(); - - if (cost == null) { - System.out.println("No MST does not exists"); - } else { - System.out.println("MST cost: " + cost); - for (Edge e : solver.getMst()) { - System.out.println(String.format("from: %d, to: %d, cost: %d", e.from, e.to, e.cost)); - } - } - - // Output: - // MST cost: 14 - // from: 0, to: 4, cost: 1 - // from: 4, to: 5, cost: 1 - // from: 4, to: 3, cost: 2 - // from: 3, to: 1, cost: 2 - // from: 3, to: 7, cost: 2 - // from: 7, to: 6, cost: 1 - // from: 6, to: 8, cost: 4 - // from: 8, to: 9, cost: 0 - // from: 8, to: 2, cost: 1 + printMst(solver); } - private static void firstGraphFromSlides() { + // Example 2: Graph with 7 nodes and a negative edge weight. MST cost = 9. + private static void exampleGraphWithNegativeEdges() { int n = 7; List> g = createEmptyGraph(n); @@ -211,19 +187,11 @@ private static void firstGraphFromSlides() { addUndirectedEdge(g, 5, 6, 1); EagerPrimsAdjacencyList solver = new EagerPrimsAdjacencyList(g); - Long cost = solver.getMstCost(); - - if (cost == null) { - System.out.println("No MST does not exists"); - } else { - System.out.println("MST cost: " + cost); - for (Edge e : solver.getMst()) { - System.out.println(String.format("from: %d, to: %d, cost: %d", e.from, e.to, e.cost)); - } - } + printMst(solver); } - private static void squareGraphFromSlides() { + // Example 3: Square-shaped graph with 9 nodes. MST cost = 39. + private static void exampleSquareGraph() { int n = 9; List> g = createEmptyGraph(n); @@ -241,149 +209,51 @@ private static void squareGraphFromSlides() { addUndirectedEdge(g, 7, 8, 5); EagerPrimsAdjacencyList solver = new EagerPrimsAdjacencyList(g); - Long cost = solver.getMstCost(); - - if (cost == null) { - System.out.println("No MST does not exists"); - } else { - System.out.println("MST cost: " + cost); - for (Edge e : solver.getMst()) { - System.out.println(String.format("from: %d, to: %d, cost: %d", e.from, e.to, e.cost)); - } - } + printMst(solver); } - private static void disjointOnFirstNode() { + // Example 4: Node 0 is disconnected from the rest — no MST exists. + private static void exampleDisjointFromStart() { int n = 4; List> g = createEmptyGraph(n); - // Node edges connected to zero addUndirectedEdge(g, 1, 2, 1); addUndirectedEdge(g, 2, 3, 1); addUndirectedEdge(g, 3, 1, 1); EagerPrimsAdjacencyList solver = new EagerPrimsAdjacencyList(g); - Long cost = solver.getMstCost(); - - if (cost == null) { - System.out.println("No MST does not exists"); - } else { - System.out.println("MST cost: " + cost); - for (Edge e : solver.getMst()) { - System.out.println(String.format("from: %d, to: %d, cost: %d", e.from, e.to, e.cost)); - } - } + printMst(solver); } - private static void disjointGraph() { + // Example 5: Two disconnected components — no MST exists. + private static void exampleDisconnectedGraph() { int n = 6; List> g = createEmptyGraph(n); - // Component 1 addUndirectedEdge(g, 0, 1, 1); addUndirectedEdge(g, 1, 2, 1); addUndirectedEdge(g, 2, 0, 1); - // Component 2 addUndirectedEdge(g, 3, 4, 1); addUndirectedEdge(g, 4, 5, 1); addUndirectedEdge(g, 5, 3, 1); EagerPrimsAdjacencyList solver = new EagerPrimsAdjacencyList(g); - Long cost = solver.getMstCost(); - - if (cost == null) { - System.out.println("No MST does not exists"); - } else { - System.out.println("MST cost: " + cost); - for (Edge e : solver.getMst()) { - System.out.println(String.format("from: %d, to: %d, cost: %d", e.from, e.to, e.cost)); - } - } + printMst(solver); } - private static void eagerPrimsExampleFromSlides() { - int n = 7; - List> g = createEmptyGraph(n); - - addDirectedEdge(g, 0, 2, 0); - addDirectedEdge(g, 0, 5, 7); - addDirectedEdge(g, 0, 3, 5); - addDirectedEdge(g, 0, 1, 9); - - addDirectedEdge(g, 2, 0, 0); - addDirectedEdge(g, 2, 5, 6); - - addDirectedEdge(g, 3, 0, 5); - addDirectedEdge(g, 3, 1, -2); - addDirectedEdge(g, 3, 6, 3); - addDirectedEdge(g, 3, 5, 2); - - addDirectedEdge(g, 1, 0, 9); - addDirectedEdge(g, 1, 3, -2); - addDirectedEdge(g, 1, 6, 4); - addDirectedEdge(g, 1, 4, 3); - - addDirectedEdge(g, 5, 2, 6); - addDirectedEdge(g, 5, 0, 7); - addDirectedEdge(g, 5, 3, 2); - addDirectedEdge(g, 5, 6, 1); - - addDirectedEdge(g, 6, 5, 1); - addDirectedEdge(g, 6, 3, 3); - addDirectedEdge(g, 6, 1, 4); - addDirectedEdge(g, 6, 4, 6); - - addDirectedEdge(g, 4, 1, 3); - addDirectedEdge(g, 4, 6, 6); - - EagerPrimsAdjacencyList solver = new EagerPrimsAdjacencyList(g); + private static void printMst(EagerPrimsAdjacencyList solver) { Long cost = solver.getMstCost(); - if (cost == null) { - System.out.println("No MST does not exists"); + System.out.println("No MST exists"); } else { System.out.println("MST cost: " + cost); - for (Edge e : solver.getMst()) { - System.out.println(String.format("from: %d, to: %d, cost: %d", e.from, e.to, e.cost)); - } - } - } - - static Random random = new Random(); - - private static void lazyVsEagerAnalysis() { - int n = 5000; - List> g1 = EagerPrimsAdjacencyList.createEmptyGraph(n); - List> g2 = LazyPrimsAdjacencyList.createEmptyGraph(n); - - for (int i = 0; i < n; i++) { - for (int j = i + 1; j < n; j++) { - int r = random.nextInt(10); - EagerPrimsAdjacencyList.addUndirectedEdge(g1, i, j, r); - LazyPrimsAdjacencyList.addUndirectedEdge(g2, i, j, r); - } - } - - EagerPrimsAdjacencyList eagerSolver = new EagerPrimsAdjacencyList(g1); - LazyPrimsAdjacencyList lazySolver = new LazyPrimsAdjacencyList(g2); - - long startTime = System.nanoTime(); - Long eagerCost = eagerSolver.getMstCost(); - long endTime = System.nanoTime(); - System.out.println("Eager: " + (endTime - startTime)); - - startTime = System.nanoTime(); - Long lazyCost = lazySolver.getMstCost(); - endTime = System.nanoTime(); - System.out.println("Lazy: " + (endTime - startTime)); - - if (eagerCost.longValue() != lazyCost.longValue()) { - System.out.println("Oh dear. " + eagerCost + " != " + lazyCost); + for (Edge e : solver.getMst()) + System.out.printf(" %d -> %d (cost %d)\n", e.from, e.to, e.cost); } } - /* Supporting indexed priority queue implementation. */ + /* Minimal indexed d-ary min-heap — only the operations needed by Prim's are kept. */ private static class MinIndexedDHeap> { @@ -408,16 +278,13 @@ private static class MinIndexedDHeap> { // 'im' and 'pm' are inverses of each other, so: pm[im[i]] = im[pm[i]] = i public final int[] im; - // The values associated with the keys. It is very important to note + // The values associated with the keys. It is very important to note // that this array is indexed by the key indexes (aka 'ki'). public final Object[] values; - // Initializes a D-ary heap with a maximum capacity of maxSize. public MinIndexedDHeap(int degree, int maxSize) { - if (maxSize <= 0) throw new IllegalArgumentException("maxSize <= 0"); - - D = max(2, degree); - N = max(D + 1, maxSize); + D = Math.max(2, degree); + N = Math.max(D + 1, maxSize); im = new int[N]; pm = new int[N]; @@ -432,45 +299,26 @@ public MinIndexedDHeap(int degree, int maxSize) { } } - public int size() { - return sz; - } - public boolean isEmpty() { return sz == 0; } public boolean contains(int ki) { - keyInBoundsOrThrow(ki); return pm[ki] != -1; } public int peekMinKeyIndex() { - isNotEmptyOrThrow(); return im[0]; } - public int pollMinKeyIndex() { - int minki = peekMinKeyIndex(); - delete(minki); - return minki; - } - @SuppressWarnings("unchecked") - public T peekMinValue() { - isNotEmptyOrThrow(); - return (T) values[im[0]]; - } - public T pollMinValue() { - T minValue = peekMinValue(); - delete(peekMinKeyIndex()); - return minValue; + T minVal = (T) values[im[0]]; + delete(im[0]); + return minVal; } public void insert(int ki, T value) { - if (contains(ki)) throw new IllegalArgumentException("index already exists; received: " + ki); - valueNotNullOrThrow(value); pm[ki] = sz; im[sz] = ki; values[ki] = value; @@ -478,15 +326,16 @@ public void insert(int ki, T value) { } @SuppressWarnings("unchecked") - public T valueOf(int ki) { - keyExistsOrThrow(ki); - return (T) values[ki]; + public void decrease(int ki, T value) { + if (((Comparable) value).compareTo((T) values[ki]) < 0) { + values[ki] = value; + swim(pm[ki]); + } } @SuppressWarnings("unchecked") public T delete(int ki) { - keyExistsOrThrow(ki); - final int i = pm[ki]; + int i = pm[ki]; swap(i, --sz); sink(i); swim(i); @@ -497,37 +346,6 @@ public T delete(int ki) { return value; } - @SuppressWarnings("unchecked") - public T update(int ki, T value) { - keyExistsAndValueNotNullOrThrow(ki, value); - final int i = pm[ki]; - T oldValue = (T) values[ki]; - values[ki] = value; - sink(i); - swim(i); - return oldValue; - } - - // Strictly decreases the value associated with 'ki' to 'value' - public void decrease(int ki, T value) { - keyExistsAndValueNotNullOrThrow(ki, value); - if (less(value, values[ki])) { - values[ki] = value; - swim(pm[ki]); - } - } - - // Strictly increases the value associated with 'ki' to 'value' - public void increase(int ki, T value) { - keyExistsAndValueNotNullOrThrow(ki, value); - if (less(values[ki], value)) { - values[ki] = value; - sink(pm[ki]); - } - } - - /* Helper functions */ - private void sink(int i) { for (int j = minChild(i); j != -1; ) { swap(i, j); @@ -543,10 +361,16 @@ private void swim(int i) { } } - // From the parent node at index i find the minimum child below it private int minChild(int i) { - int index = -1, from = child[i], to = min(sz, from + D); - for (int j = from; j < to; j++) if (less(j, i)) index = i = j; + int index = -1; + int from = child[i]; + int to = Math.min(sz, from + D); + for (int j = from; j < to; j++) { + if (less(j, i)) { + index = j; + i = j; + } + } return index; } @@ -558,63 +382,9 @@ private void swap(int i, int j) { im[j] = tmp; } - // Tests if the value of node i < node j @SuppressWarnings("unchecked") private boolean less(int i, int j) { return ((Comparable) values[im[i]]).compareTo((T) values[im[j]]) < 0; } - - @SuppressWarnings("unchecked") - private boolean less(Object obj1, Object obj2) { - return ((Comparable) obj1).compareTo((T) obj2) < 0; - } - - @Override - public String toString() { - List lst = new ArrayList<>(sz); - for (int i = 0; i < sz; i++) lst.add(im[i]); - return lst.toString(); - } - - /* Helper functions to make the code more readable. */ - - private void isNotEmptyOrThrow() { - if (isEmpty()) throw new NoSuchElementException("Priority queue underflow"); - } - - private void keyExistsAndValueNotNullOrThrow(int ki, Object value) { - keyExistsOrThrow(ki); - valueNotNullOrThrow(value); - } - - private void keyExistsOrThrow(int ki) { - if (!contains(ki)) throw new NoSuchElementException("Index does not exist; received: " + ki); - } - - private void valueNotNullOrThrow(Object value) { - if (value == null) throw new IllegalArgumentException("value cannot be null"); - } - - private void keyInBoundsOrThrow(int ki) { - if (ki < 0 || ki >= N) - throw new IllegalArgumentException("Key index out of bounds; received: " + ki); - } - - /* Test functions */ - - // Recursively checks if this heap is a min heap. This method is used - // for testing purposes to validate the heap invariant. - public boolean isMinHeap() { - return isMinHeap(0); - } - - private boolean isMinHeap(int i) { - int from = child[i], to = min(sz, from + D); - for (int j = from; j < to; j++) { - if (!less(i, j)) return false; - if (!isMinHeap(j)) return false; - } - return true; - } } } diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/LazyPrimsAdjacencyList.java b/src/main/java/com/williamfiset/algorithms/graphtheory/LazyPrimsAdjacencyList.java index 53f4e5d5a..b95c6992d 100644 --- a/src/main/java/com/williamfiset/algorithms/graphtheory/LazyPrimsAdjacencyList.java +++ b/src/main/java/com/williamfiset/algorithms/graphtheory/LazyPrimsAdjacencyList.java @@ -1,8 +1,12 @@ /** - * An implementation of the lazy version of Prim's algorithm which relies on using a traditional - * priority queue to query the next best edge. + * Lazy implementation of Prim's minimum spanning tree algorithm using a priority queue. * - *

Time Complexity: O(ElogE) + *

"Lazy" because stale edges (to already-visited nodes) remain in the priority queue and are + * skipped when polled, rather than being eagerly removed or updated. + * + *

Time: O(E log(E)) + * + *

Space: O(V + E) * * @author William Fiset, william.alexandre.fiset@gmail.com */ @@ -23,90 +27,84 @@ public Edge(int from, int to, int cost) { @Override public int compareTo(Edge other) { - return cost - other.cost; + return Integer.compare(cost, other.cost); } } - // Inputs private final int n; private final List> graph; - // Internal private boolean solved; private boolean mstExists; private boolean[] visited; private PriorityQueue pq; - // Outputs private long minCostSum; private Edge[] mstEdges; + /** + * Creates a Lazy Prim's MST solver for the given graph. + * + * @param graph adjacency list where each node maps to a list of weighted edges. + * @throws IllegalArgumentException if the graph is null or empty. + */ public LazyPrimsAdjacencyList(List> graph) { - if (graph == null || graph.isEmpty()) throw new IllegalArgumentException(); + if (graph == null || graph.isEmpty()) + throw new IllegalArgumentException(); this.n = graph.size(); this.graph = graph; } - // Returns the edges used in finding the minimum spanning tree, - // or returns null if no MST exists. + /** Returns the MST edges, or null if no MST exists. */ public Edge[] getMst() { solve(); return mstExists ? mstEdges : null; } + /** Returns the MST total cost, or null if no MST exists. */ public Long getMstCost() { solve(); return mstExists ? minCostSum : null; } - private void addEdges(int nodeIndex) { - visited[nodeIndex] = true; - - // edges will never be null if the createEmptyGraph method was used to build the graph. - List edges = graph.get(nodeIndex); - for (Edge e : edges) - if (!visited[e.to]) { - // System.out.printf("(%d, %d, %d)\n", e.from, e.to, e.cost); + private void addEdges(int node) { + visited[node] = true; + for (Edge e : graph.get(node)) + if (!visited[e.to]) pq.offer(e); - } } - // Computes the minimum spanning tree and minimum spanning tree cost. private void solve() { - if (solved) return; + if (solved) + return; solved = true; - int m = n - 1, edgeCount = 0; + int m = n - 1; + int edgeCount = 0; pq = new PriorityQueue<>(); visited = new boolean[n]; mstEdges = new Edge[m]; - // Add initial set of edges to the priority queue starting at node 0. addEdges(0); - // Loop while the MST is not complete. while (!pq.isEmpty() && edgeCount != m) { Edge edge = pq.poll(); - int nodeIndex = edge.to; - - // Skip any edge pointing to an already visited node. - if (visited[nodeIndex]) continue; + if (visited[edge.to]) + continue; mstEdges[edgeCount++] = edge; minCostSum += edge.cost; - - addEdges(nodeIndex); + addEdges(edge.to); } - // Check if MST spans entire graph. mstExists = (edgeCount == m); } - /* Graph construction helpers. */ - + /** Creates an adjacency list with n nodes. */ static List> createEmptyGraph(int n) { List> g = new ArrayList<>(); - for (int i = 0; i < n; i++) g.add(new ArrayList<>()); + for (int i = 0; i < n; i++) + g.add(new ArrayList<>()); return g; } @@ -119,16 +117,14 @@ static void addUndirectedEdge(List> g, int from, int to, int cost) { addDirectedEdge(g, to, from, cost); } - /* Example usage. */ - public static void main(String[] args) { - // example1(); - // firstGraphFromSlides(); - // squareGraphFromSlides(); - lazyPrimsDemoFromSlides(); + exampleConnectedGraph(); + System.out.println(); + exampleDisconnectedGraph(); } - private static void example1() { + // Example 1: Connected graph with 10 nodes. MST cost = 14. + private static void exampleConnectedGraph() { int n = 10; List> g = createEmptyGraph(n); @@ -152,140 +148,30 @@ private static void example1() { addUndirectedEdge(g, 7, 8, 6); LazyPrimsAdjacencyList solver = new LazyPrimsAdjacencyList(g); - Long cost = solver.getMstCost(); - - if (cost == null) { - System.out.println("No MST does not exists"); - } else { - System.out.println("MST cost: " + cost); - for (Edge e : solver.getMst()) { - System.out.println(String.format("from: %d, to: %d, cost: %d", e.from, e.to, e.cost)); - } - } - - // Output: - // MST cost: 14 - // from: 0, to: 4, cost: 1 - // from: 4, to: 5, cost: 1 - // from: 4, to: 3, cost: 2 - // from: 3, to: 1, cost: 2 - // from: 3, to: 7, cost: 2 - // from: 7, to: 6, cost: 1 - // from: 6, to: 8, cost: 4 - // from: 8, to: 9, cost: 0 - // from: 8, to: 2, cost: 1 + printMst(solver); } - private static void firstGraphFromSlides() { - int n = 7; + // Example 2: Disconnected graph — no MST exists. + private static void exampleDisconnectedGraph() { + int n = 4; List> g = createEmptyGraph(n); - addUndirectedEdge(g, 0, 1, 9); - addUndirectedEdge(g, 0, 2, 0); - addUndirectedEdge(g, 0, 3, 5); - addUndirectedEdge(g, 0, 5, 7); - addUndirectedEdge(g, 1, 3, -2); - addUndirectedEdge(g, 1, 4, 3); - addUndirectedEdge(g, 1, 6, 4); - addUndirectedEdge(g, 2, 5, 6); - addUndirectedEdge(g, 3, 5, 2); - addUndirectedEdge(g, 3, 6, 3); - addUndirectedEdge(g, 4, 6, 6); - addUndirectedEdge(g, 5, 6, 1); + addUndirectedEdge(g, 0, 1, 3); + addUndirectedEdge(g, 2, 3, 5); + // Nodes {0,1} and {2,3} are not connected. LazyPrimsAdjacencyList solver = new LazyPrimsAdjacencyList(g); - Long cost = solver.getMstCost(); - - if (cost == null) { - System.out.println("No MST does not exists"); - } else { - System.out.println("MST cost: " + cost); - for (Edge e : solver.getMst()) { - System.out.println(String.format("from: %d, to: %d, cost: %d", e.from, e.to, e.cost)); - } - } + printMst(solver); } - private static void squareGraphFromSlides() { - int n = 9; - List> g = createEmptyGraph(n); - - addUndirectedEdge(g, 0, 1, 6); - addUndirectedEdge(g, 0, 3, 3); - addUndirectedEdge(g, 1, 2, 4); - addUndirectedEdge(g, 1, 4, 2); - addUndirectedEdge(g, 2, 5, 12); - addUndirectedEdge(g, 3, 4, 1); - addUndirectedEdge(g, 3, 6, 8); - addUndirectedEdge(g, 4, 5, 7); - addUndirectedEdge(g, 4, 7, 9); - addUndirectedEdge(g, 5, 8, 10); - addUndirectedEdge(g, 6, 7, 11); - addUndirectedEdge(g, 7, 8, 5); - - LazyPrimsAdjacencyList solver = new LazyPrimsAdjacencyList(g); - Long cost = solver.getMstCost(); - - if (cost == null) { - System.out.println("No MST does not exists"); - } else { - System.out.println("MST cost: " + cost); - for (Edge e : solver.getMst()) { - System.out.println(String.format("from: %d, to: %d, cost: %d", e.from, e.to, e.cost)); - } - } - } - - private static void lazyPrimsDemoFromSlides() { - int n = 8; - List> g = createEmptyGraph(n); - - addDirectedEdge(g, 0, 1, 10); - addDirectedEdge(g, 0, 2, 1); - addDirectedEdge(g, 0, 3, 4); - - addDirectedEdge(g, 2, 1, 3); - addDirectedEdge(g, 2, 5, 8); - addDirectedEdge(g, 2, 3, 2); - addDirectedEdge(g, 2, 0, 1); - - addDirectedEdge(g, 3, 2, 2); - addDirectedEdge(g, 3, 5, 2); - addDirectedEdge(g, 3, 6, 7); - addDirectedEdge(g, 3, 0, 4); - - addDirectedEdge(g, 5, 2, 8); - addDirectedEdge(g, 5, 4, 1); - addDirectedEdge(g, 5, 7, 9); - addDirectedEdge(g, 5, 6, 6); - addDirectedEdge(g, 5, 3, 2); - - addDirectedEdge(g, 4, 1, 0); - addDirectedEdge(g, 4, 5, 1); - addDirectedEdge(g, 4, 7, 8); - - addDirectedEdge(g, 1, 0, 10); - addDirectedEdge(g, 1, 2, 3); - addDirectedEdge(g, 1, 4, 0); - - addDirectedEdge(g, 6, 3, 7); - addDirectedEdge(g, 6, 5, 6); - addDirectedEdge(g, 6, 7, 12); - - addDirectedEdge(g, 7, 4, 8); - addDirectedEdge(g, 7, 5, 9); - addDirectedEdge(g, 7, 6, 12); - - LazyPrimsAdjacencyList solver = new LazyPrimsAdjacencyList(g); + private static void printMst(LazyPrimsAdjacencyList solver) { Long cost = solver.getMstCost(); - if (cost == null) { - System.out.println("No MST does not exists"); + System.out.println("No MST exists"); } else { System.out.println("MST cost: " + cost); - for (Edge e : solver.getMst()) { - System.out.println(String.format("from: %d, to: %d, cost: %d", e.from, e.to, e.cost)); - } + for (Edge e : solver.getMst()) + System.out.printf(" %d -> %d (cost %d)\n", e.from, e.to, e.cost); } } } diff --git a/src/test/java/com/williamfiset/algorithms/graphtheory/BUILD b/src/test/java/com/williamfiset/algorithms/graphtheory/BUILD index 965b96e39..b4b1519e5 100644 --- a/src/test/java/com/williamfiset/algorithms/graphtheory/BUILD +++ b/src/test/java/com/williamfiset/algorithms/graphtheory/BUILD @@ -200,5 +200,27 @@ java_test( deps = TEST_DEPS, ) +# bazel test //src/test/java/com/williamfiset/algorithms/graphtheory:LazyPrimsAdjacencyListTest +java_test( + name = "LazyPrimsAdjacencyListTest", + srcs = ["LazyPrimsAdjacencyListTest.java"], + main_class = "org.junit.platform.console.ConsoleLauncher", + use_testrunner = False, + args = ["--select-class=com.williamfiset.algorithms.graphtheory.LazyPrimsAdjacencyListTest"], + runtime_deps = JUNIT5_RUNTIME_DEPS, + deps = TEST_DEPS, +) + +# bazel test //src/test/java/com/williamfiset/algorithms/graphtheory:EagerPrimsAdjacencyListTest +java_test( + name = "EagerPrimsAdjacencyListTest", + srcs = ["EagerPrimsAdjacencyListTest.java"], + main_class = "org.junit.platform.console.ConsoleLauncher", + use_testrunner = False, + args = ["--select-class=com.williamfiset.algorithms.graphtheory.EagerPrimsAdjacencyListTest"], + runtime_deps = JUNIT5_RUNTIME_DEPS, + deps = TEST_DEPS, +) + # Run all tests # bazel test //src/test/java/com/williamfiset/algorithms/graphtheory:all diff --git a/src/test/java/com/williamfiset/algorithms/graphtheory/EagerPrimsAdjacencyListTest.java b/src/test/java/com/williamfiset/algorithms/graphtheory/EagerPrimsAdjacencyListTest.java new file mode 100644 index 000000000..e7fb3d48f --- /dev/null +++ b/src/test/java/com/williamfiset/algorithms/graphtheory/EagerPrimsAdjacencyListTest.java @@ -0,0 +1,219 @@ +package com.williamfiset.algorithms.graphtheory; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.williamfiset.algorithms.graphtheory.EagerPrimsAdjacencyList.Edge; +import java.util.*; +import org.junit.jupiter.api.*; + +public class EagerPrimsAdjacencyListTest { + + private static List> createGraph(int n) { + return EagerPrimsAdjacencyList.createEmptyGraph(n); + } + + private static void addEdge(List> g, int from, int to, int cost) { + EagerPrimsAdjacencyList.addUndirectedEdge(g, from, to, cost); + } + + @Test + public void testNullGraphThrowsException() { + assertThrows(IllegalArgumentException.class, () -> new EagerPrimsAdjacencyList(null)); + } + + @Test + public void testEmptyGraphThrowsException() { + assertThrows(IllegalArgumentException.class, () -> new EagerPrimsAdjacencyList(new ArrayList<>())); + } + + @Test + public void testSingleNode() { + List> g = createGraph(1); + EagerPrimsAdjacencyList solver = new EagerPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isEqualTo(0L); + assertThat(solver.getMst()).isEmpty(); + } + + @Test + public void testTwoNodesConnected() { + List> g = createGraph(2); + addEdge(g, 0, 1, 5); + EagerPrimsAdjacencyList solver = new EagerPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isEqualTo(5L); + assertThat(solver.getMst()).hasLength(1); + } + + @Test + public void testTwoNodesDisconnected() { + List> g = createGraph(2); + EagerPrimsAdjacencyList solver = new EagerPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isNull(); + assertThat(solver.getMst()).isNull(); + } + + @Test + public void testSimpleTriangle() { + List> g = createGraph(3); + addEdge(g, 0, 1, 1); + addEdge(g, 1, 2, 2); + addEdge(g, 0, 2, 3); + EagerPrimsAdjacencyList solver = new EagerPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isEqualTo(3L); + assertThat(solver.getMst()).hasLength(2); + } + + @Test + public void testDisconnectedGraph() { + List> g = createGraph(4); + addEdge(g, 0, 1, 1); + addEdge(g, 2, 3, 2); + EagerPrimsAdjacencyList solver = new EagerPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isNull(); + assertThat(solver.getMst()).isNull(); + } + + @Test + public void testConnectedGraphMstCost14() { + List> g = createGraph(10); + addEdge(g, 0, 1, 5); + addEdge(g, 1, 2, 4); + addEdge(g, 2, 9, 2); + addEdge(g, 0, 4, 1); + addEdge(g, 0, 3, 4); + addEdge(g, 1, 3, 2); + addEdge(g, 2, 7, 4); + addEdge(g, 2, 8, 1); + addEdge(g, 9, 8, 0); + addEdge(g, 4, 5, 1); + addEdge(g, 5, 6, 7); + addEdge(g, 6, 8, 4); + addEdge(g, 4, 3, 2); + addEdge(g, 5, 3, 5); + addEdge(g, 3, 6, 11); + addEdge(g, 6, 7, 1); + addEdge(g, 3, 7, 2); + addEdge(g, 7, 8, 6); + + EagerPrimsAdjacencyList solver = new EagerPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isEqualTo(14L); + assertThat(solver.getMst()).hasLength(9); + } + + @Test + public void testGraphWithNegativeWeightEdges() { + List> g = createGraph(7); + addEdge(g, 0, 1, 9); + addEdge(g, 0, 2, 0); + addEdge(g, 0, 3, 5); + addEdge(g, 0, 5, 7); + addEdge(g, 1, 3, -2); + addEdge(g, 1, 4, 3); + addEdge(g, 1, 6, 4); + addEdge(g, 2, 5, 6); + addEdge(g, 3, 5, 2); + addEdge(g, 3, 6, 3); + addEdge(g, 4, 6, 6); + addEdge(g, 5, 6, 1); + EagerPrimsAdjacencyList solver = new EagerPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isEqualTo(9L); + assertThat(solver.getMst()).hasLength(6); + } + + @Test + public void testGraphWithZeroWeightEdges() { + List> g = createGraph(4); + addEdge(g, 0, 1, 0); + addEdge(g, 1, 2, 0); + addEdge(g, 2, 3, 0); + EagerPrimsAdjacencyList solver = new EagerPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isEqualTo(0L); + assertThat(solver.getMst()).hasLength(3); + } + + @Test + public void testCompleteGraphK4() { + List> g = createGraph(4); + addEdge(g, 0, 1, 1); + addEdge(g, 0, 2, 4); + addEdge(g, 0, 3, 3); + addEdge(g, 1, 2, 2); + addEdge(g, 1, 3, 5); + addEdge(g, 2, 3, 6); + EagerPrimsAdjacencyList solver = new EagerPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isEqualTo(6L); + assertThat(solver.getMst()).hasLength(3); + } + + @Test + public void testClassicGraph9Nodes() { + List> g = createGraph(9); + addEdge(g, 0, 1, 4); + addEdge(g, 0, 7, 8); + addEdge(g, 1, 2, 8); + addEdge(g, 1, 7, 11); + addEdge(g, 2, 3, 7); + addEdge(g, 2, 5, 4); + addEdge(g, 2, 8, 2); + addEdge(g, 3, 4, 9); + addEdge(g, 3, 5, 14); + addEdge(g, 4, 5, 10); + addEdge(g, 5, 6, 2); + addEdge(g, 6, 7, 1); + addEdge(g, 6, 8, 6); + addEdge(g, 7, 8, 7); + EagerPrimsAdjacencyList solver = new EagerPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isEqualTo(37L); + assertThat(solver.getMst()).hasLength(8); + } + + @Test + public void testMstIsIdempotent() { + List> g = createGraph(3); + addEdge(g, 0, 1, 1); + addEdge(g, 1, 2, 2); + addEdge(g, 0, 2, 3); + EagerPrimsAdjacencyList solver = new EagerPrimsAdjacencyList(g); + + long cost1 = solver.getMstCost(); + long cost2 = solver.getMstCost(); + Edge[] mst1 = solver.getMst(); + Edge[] mst2 = solver.getMst(); + + assertThat(cost1).isEqualTo(cost2); + assertThat(mst1).isEqualTo(mst2); + } + + @Test + public void testStartNodeDisconnected() { + List> g = createGraph(4); + addEdge(g, 1, 2, 1); + addEdge(g, 2, 3, 1); + addEdge(g, 3, 1, 1); + EagerPrimsAdjacencyList solver = new EagerPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isNull(); + assertThat(solver.getMst()).isNull(); + } + + @Test + public void testLazyAndEagerAgree() { + List> eg = EagerPrimsAdjacencyList.createEmptyGraph(10); + List> lg = LazyPrimsAdjacencyList.createEmptyGraph(10); + + int[][] edges = { + {0, 1, 5}, {1, 2, 4}, {2, 9, 2}, {0, 4, 1}, {0, 3, 4}, + {1, 3, 2}, {2, 7, 4}, {2, 8, 1}, {9, 8, 0}, {4, 5, 1}, + {5, 6, 7}, {6, 8, 4}, {4, 3, 2}, {5, 3, 5}, {3, 6, 11}, + {6, 7, 1}, {3, 7, 2}, {7, 8, 6} + }; + for (int[] e : edges) { + EagerPrimsAdjacencyList.addUndirectedEdge(eg, e[0], e[1], e[2]); + LazyPrimsAdjacencyList.addUndirectedEdge(lg, e[0], e[1], e[2]); + } + + EagerPrimsAdjacencyList eagerSolver = new EagerPrimsAdjacencyList(eg); + LazyPrimsAdjacencyList lazySolver = new LazyPrimsAdjacencyList(lg); + + assertThat(eagerSolver.getMstCost()).isEqualTo(lazySolver.getMstCost()); + } +} diff --git a/src/test/java/com/williamfiset/algorithms/graphtheory/LazyPrimsAdjacencyListTest.java b/src/test/java/com/williamfiset/algorithms/graphtheory/LazyPrimsAdjacencyListTest.java new file mode 100644 index 000000000..ca5dc6743 --- /dev/null +++ b/src/test/java/com/williamfiset/algorithms/graphtheory/LazyPrimsAdjacencyListTest.java @@ -0,0 +1,188 @@ +package com.williamfiset.algorithms.graphtheory; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.williamfiset.algorithms.graphtheory.LazyPrimsAdjacencyList.Edge; +import java.util.*; +import org.junit.jupiter.api.*; + +public class LazyPrimsAdjacencyListTest { + + private static List> createGraph(int n) { + return LazyPrimsAdjacencyList.createEmptyGraph(n); + } + + private static void addEdge(List> g, int from, int to, int cost) { + LazyPrimsAdjacencyList.addUndirectedEdge(g, from, to, cost); + } + + @Test + public void testNullGraphThrowsException() { + assertThrows(IllegalArgumentException.class, () -> new LazyPrimsAdjacencyList(null)); + } + + @Test + public void testEmptyGraphThrowsException() { + assertThrows(IllegalArgumentException.class, () -> new LazyPrimsAdjacencyList(new ArrayList<>())); + } + + @Test + public void testSingleNode() { + List> g = createGraph(1); + LazyPrimsAdjacencyList solver = new LazyPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isEqualTo(0L); + assertThat(solver.getMst()).isEmpty(); + } + + @Test + public void testTwoNodesConnected() { + List> g = createGraph(2); + addEdge(g, 0, 1, 5); + LazyPrimsAdjacencyList solver = new LazyPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isEqualTo(5L); + assertThat(solver.getMst()).hasLength(1); + } + + @Test + public void testTwoNodesDisconnected() { + List> g = createGraph(2); + LazyPrimsAdjacencyList solver = new LazyPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isNull(); + assertThat(solver.getMst()).isNull(); + } + + @Test + public void testSimpleTriangle() { + List> g = createGraph(3); + addEdge(g, 0, 1, 1); + addEdge(g, 1, 2, 2); + addEdge(g, 0, 2, 3); + LazyPrimsAdjacencyList solver = new LazyPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isEqualTo(3L); + assertThat(solver.getMst()).hasLength(2); + } + + @Test + public void testDisconnectedGraph() { + List> g = createGraph(4); + addEdge(g, 0, 1, 1); + addEdge(g, 2, 3, 2); + LazyPrimsAdjacencyList solver = new LazyPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isNull(); + assertThat(solver.getMst()).isNull(); + } + + @Test + public void testConnectedGraphMstCost14() { + List> g = createGraph(10); + addEdge(g, 0, 1, 5); + addEdge(g, 1, 2, 4); + addEdge(g, 2, 9, 2); + addEdge(g, 0, 4, 1); + addEdge(g, 0, 3, 4); + addEdge(g, 1, 3, 2); + addEdge(g, 2, 7, 4); + addEdge(g, 2, 8, 1); + addEdge(g, 9, 8, 0); + addEdge(g, 4, 5, 1); + addEdge(g, 5, 6, 7); + addEdge(g, 6, 8, 4); + addEdge(g, 4, 3, 2); + addEdge(g, 5, 3, 5); + addEdge(g, 3, 6, 11); + addEdge(g, 6, 7, 1); + addEdge(g, 3, 7, 2); + addEdge(g, 7, 8, 6); + + LazyPrimsAdjacencyList solver = new LazyPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isEqualTo(14L); + assertThat(solver.getMst()).hasLength(9); + } + + @Test + public void testGraphWithNegativeWeightEdges() { + List> g = createGraph(3); + addEdge(g, 0, 1, -5); + addEdge(g, 1, 2, -3); + addEdge(g, 0, 2, 10); + LazyPrimsAdjacencyList solver = new LazyPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isEqualTo(-8L); + assertThat(solver.getMst()).hasLength(2); + } + + @Test + public void testGraphWithZeroWeightEdges() { + List> g = createGraph(4); + addEdge(g, 0, 1, 0); + addEdge(g, 1, 2, 0); + addEdge(g, 2, 3, 0); + LazyPrimsAdjacencyList solver = new LazyPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isEqualTo(0L); + assertThat(solver.getMst()).hasLength(3); + } + + @Test + public void testCompleteGraphK4() { + List> g = createGraph(4); + addEdge(g, 0, 1, 1); + addEdge(g, 0, 2, 4); + addEdge(g, 0, 3, 3); + addEdge(g, 1, 2, 2); + addEdge(g, 1, 3, 5); + addEdge(g, 2, 3, 6); + LazyPrimsAdjacencyList solver = new LazyPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isEqualTo(6L); + assertThat(solver.getMst()).hasLength(3); + } + + @Test + public void testClassicGraph9Nodes() { + List> g = createGraph(9); + addEdge(g, 0, 1, 4); + addEdge(g, 0, 7, 8); + addEdge(g, 1, 2, 8); + addEdge(g, 1, 7, 11); + addEdge(g, 2, 3, 7); + addEdge(g, 2, 5, 4); + addEdge(g, 2, 8, 2); + addEdge(g, 3, 4, 9); + addEdge(g, 3, 5, 14); + addEdge(g, 4, 5, 10); + addEdge(g, 5, 6, 2); + addEdge(g, 6, 7, 1); + addEdge(g, 6, 8, 6); + addEdge(g, 7, 8, 7); + LazyPrimsAdjacencyList solver = new LazyPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isEqualTo(37L); + assertThat(solver.getMst()).hasLength(8); + } + + @Test + public void testMstIsIdempotent() { + List> g = createGraph(3); + addEdge(g, 0, 1, 1); + addEdge(g, 1, 2, 2); + addEdge(g, 0, 2, 3); + LazyPrimsAdjacencyList solver = new LazyPrimsAdjacencyList(g); + + long cost1 = solver.getMstCost(); + long cost2 = solver.getMstCost(); + Edge[] mst1 = solver.getMst(); + Edge[] mst2 = solver.getMst(); + + assertThat(cost1).isEqualTo(cost2); + assertThat(mst1).isEqualTo(mst2); + } + + @Test + public void testStartNodeDisconnected() { + List> g = createGraph(4); + addEdge(g, 1, 2, 1); + addEdge(g, 2, 3, 1); + addEdge(g, 3, 1, 1); + LazyPrimsAdjacencyList solver = new LazyPrimsAdjacencyList(g); + assertThat(solver.getMstCost()).isNull(); + assertThat(solver.getMst()).isNull(); + } +} From 4cbda50d8f955c55b4e17c41922be0b03793406d Mon Sep 17 00:00:00 2001 From: William Fiset Date: Tue, 31 Mar 2026 18:46:01 -0700 Subject: [PATCH 3/3] =?UTF-8?q?Fix=20Steiner=20tree=20complexity=20formatt?= =?UTF-8?q?ing=20in=20README:=20replace=20=5F=20with=20=C2=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 30ff21991..fa11a9c3f 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ $ java -cp classes com.williamfiset.algorithms.search.BinarySearch - [Kosaraju's strongly connected components algorithm (adjacency list)](src/main/java/com/williamfiset/algorithms/graphtheory/Kosaraju.java) **- O(V+E)** - [:movie_camera:](https://www.youtube.com/watch?v=jsmMtJpPnhU) [Prim's min spanning tree algorithm (lazy version, adjacency list)](src/main/java/com/williamfiset/algorithms/graphtheory/LazyPrimsAdjacencyList.java) **- O(Elog(E))** - [:movie_camera:](https://www.youtube.com/watch?v=xq3ABa-px_g) [Prim's min spanning tree algorithm (eager version, adjacency list)](src/main/java/com/williamfiset/algorithms/graphtheory/EagerPrimsAdjacencyList.java) **- O(Elog(V))** -- [Steiner tree (minimum spanning tree generalization)](src/main/java/com/williamfiset/algorithms/graphtheory/SteinerTree.java) **- O(V3 + V2 _ 2T + V _ 3T)** +- [Steiner tree (minimum spanning tree generalization)](src/main/java/com/williamfiset/algorithms/graphtheory/SteinerTree.java) **- O(V3 + V2 · 2T + V · 3T)** - [:movie_camera:](https://www.youtube.com/watch?v=wUgWX0nc4NY) [Tarjan's strongly connected components algorithm (adjacency list)](src/main/java/com/williamfiset/algorithms/graphtheory/TarjanSccSolverAdjacencyList.java) **- O(V+E)** - [:movie_camera:](https://www.youtube.com/watch?v=eL-KzMXSXXI) [Topological sort (acyclic graph, adjacency list)](src/main/java/com/williamfiset/algorithms/graphtheory/TopologicalSortAdjacencyList.java) **- O(V+E)** - [:movie_camera:](https://www.youtube.com/watch?v=cY4HiiFHO1o) [Traveling Salesman Problem (dynamic programming, iterative)](src/main/java/com/williamfiset/algorithms/graphtheory/TspDynamicProgrammingIterative.java) **- O(n22n)**