diff --git a/.claude/SKILL.md b/.claude/SKILL.md index a4d7c1388..2c7e3f984 100644 --- a/.claude/SKILL.md +++ b/.claude/SKILL.md @@ -282,6 +282,28 @@ Common short names (use consistently across the repo): - Max line length: 100 characters (soft limit) - Imports: group by package, alphabetize within groups, no wildcard imports +### Big-O Notation Convention + +Always use explicit multiplication and parentheses in Big-O expressions for clarity: + +```java +// ✓ GOOD — explicit and unambiguous +// Time: O(n*log(n)) +// Time: O(n*log^2(n)) +// Time: O(n^2*log(n)) + +// ✗ BAD — missing multiplication and parentheses +// Time: O(n log n) +// Time: O(n log^2 n) +// Time: O(n^2 log n) + +// Simple expressions without multiplication are fine as-is +// Time: O(n) +// Time: O(n^2) +// Time: O(log(n)) +// Space: O(n) +``` + ### Avoid Java Streams Streams hurt readability for learners. Use plain loops instead: diff --git a/README.md b/README.md index 68f88a819..6d09a7cd9 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,6 @@ $ java -cp classes com.williamfiset.algorithms.search.BinarySearch # Geometry - [Angle between 2D vectors](src/main/java/com/williamfiset/algorithms/geometry/AngleBetweenVectors2D.java) **- O(1)** -- [Angle between 3D vectors](src/main/java/com/williamfiset/algorithms/geometry/AngleBetweenVectors3D.java) **- O(1)** - [Circle-circle intersection point(s)](src/main/java/com/williamfiset/algorithms/geometry/CircleCircleIntersectionPoints.js) **- O(1)** - [Circle-line intersection point(s)](src/main/java/com/williamfiset/algorithms/geometry/LineCircleIntersection.js) **- O(1)** - [Circle-line segment intersection point(s)](src/main/java/com/williamfiset/algorithms/geometry/LineSegmentCircleIntersection.js) **- O(1)** @@ -154,21 +153,28 @@ $ java -cp classes com.williamfiset.algorithms.search.BinarySearch - [Convex hull (Graham Scan algorithm)](src/main/java/com/williamfiset/algorithms/geometry/ConvexHullGrahamScan.java) **- O(nlog(n))** - [Convex hull (Monotone chain algorithm)](src/main/java/com/williamfiset/algorithms/geometry/ConvexHullMonotoneChainsAlgorithm.java) **- O(nlog(n))** - [Convex polygon area](src/main/java/com/williamfiset/algorithms/geometry/ConvexPolygonArea.java) **- O(n)** -- [Convex polygon cut](src/main/java/com/williamfiset/algorithms/geometry/ConvexPolygonCutWithLineSegment.java) **- O(n)** - [Convex polygon contains points](src/main/java/com/williamfiset/algorithms/geometry/ConvexPolygonContainsPoint.java) **- O(log(n))** +- [Triangle area algorithms](src/main/java/com/williamfiset/algorithms/geometry/TriangleArea.java) **- O(1)** +- [Line segment-circle intersection point(s)](src/main/java/com/williamfiset/algorithms/geometry/LineSegmentCircleIntersection.js) **- O(1)** +- [Line segment-line segment intersection](src/main/java/com/williamfiset/algorithms/geometry/LineSegmentLineSegmentIntersection.java) **- O(1)** + +
+More geometry algorithms + +- [Angle between 3D vectors](src/main/java/com/williamfiset/algorithms/geometry/AngleBetweenVectors3D.java) **- O(1)** +- [Convex polygon cut](src/main/java/com/williamfiset/algorithms/geometry/ConvexPolygonCutWithLineSegment.java) **- O(n)** - [Coplanar points test (are four 3D points on the same plane)](src/main/java/com/williamfiset/algorithms/geometry/CoplanarPoints.java) **- O(1)** - [Line class (handy infinite line class)](src/main/java/com/williamfiset/algorithms/geometry/Line.java) **- O(1)** - [Line-circle intersection point(s)](src/main/java/com/williamfiset/algorithms/geometry/LineCircleIntersection.js) **- O(1)** -- [Line segment-circle intersection point(s)](src/main/java/com/williamfiset/algorithms/geometry/LineSegmentCircleIntersection.js) **- O(1)** - [Line segment to general form (ax + by = c)](src/main/java/com/williamfiset/algorithms/geometry/LineSegmentToGeneralForm.java) **- O(1)** -- [Line segment-line segment intersection](src/main/java/com/williamfiset/algorithms/geometry/LineSegmentLineSegmentIntersection.java) **- O(1)** - [Longitude-Latitude geographic distance](src/main/java/com/williamfiset/algorithms/geometry/LongitudeLatitudeGeographicDistance.java) **- O(1)** - [Point is inside triangle check](src/main/java/com/williamfiset/algorithms/geometry/PointInsideTriangle.java) **- O(1)** - [Point rotation about point](src/main/java/com/williamfiset/algorithms/geometry/PointRotation.java) **- O(1)** -- [Triangle area algorithms](src/main/java/com/williamfiset/algorithms/geometry/TriangleArea.java) **- O(1)** - [[UNTESTED] Circle-circle intersection area](src/main/java/com/williamfiset/algorithms/geometry/CircleCircleIntersectionArea.java) **- O(1)** - [[UNTESTED] Circular segment area](src/main/java/com/williamfiset/algorithms/geometry/CircularSegmentArea.java) **- O(1)** +
+ # Graph theory ### Tree algorithms diff --git a/src/main/java/com/williamfiset/algorithms/datastructures/segmenttree/CompactSegmentTree.java b/src/main/java/com/williamfiset/algorithms/datastructures/segmenttree/CompactSegmentTree.java index eac9e5960..0c9bf9e8f 100644 --- a/src/main/java/com/williamfiset/algorithms/datastructures/segmenttree/CompactSegmentTree.java +++ b/src/main/java/com/williamfiset/algorithms/datastructures/segmenttree/CompactSegmentTree.java @@ -1,85 +1,123 @@ +package com.williamfiset.algorithms.datastructures.segmenttree; + +import java.util.Arrays; + /** - * A compact array based segment tree implementation. This segment tree supports point updates and - * range queries. + * Compact Array-Based Segment Tree + * + * A space-efficient segment tree stored in a flat array of size 2*n (no + * recursion, no pointers). Supports point updates and range queries using + * any associative combine function (sum, min, max, product, GCD, etc.). + * + * The tree is stored bottom-up: leaves occupy indices [n, 2n) and internal + * nodes occupy [1, n). Index 0 is unused. Each internal node i is the + * combination of its children at 2i and 2i+1. + * + * Use cases: + * - Range sum / min / max queries with point updates + * - Competitive programming (very short, cache-friendly implementation) + * + * Time: O(n) construction, O(log(n)) per query and update + * Space: O(n) * * @author Al.Cash & William Fiset, william.alexandre.fiset@gmail.com */ -package com.williamfiset.algorithms.datastructures.segmenttree; - public class CompactSegmentTree { private int N; - // Let UNIQUE be a value which does NOT - // and will not appear in the segment tree - private long UNIQUE = 8123572096793136074L; - - // Segment tree values - private long[] tree; + // Flat array storing the segment tree. Leaves are at indices [N, 2N), + // internal nodes at [1, N). Index 0 is unused. Uninitialized slots + // are null, which acts as the identity element for the combine function. + private Long[] tree; + /** + * Creates an empty segment tree of the given size, with all slots + * initialized to null. + * + * @param size the number of elements (leaves) in the segment tree + */ public CompactSegmentTree(int size) { - tree = new long[2 * (N = size)]; - java.util.Arrays.fill(tree, UNIQUE); + tree = new Long[2 * (N = size)]; } + /** + * Creates a segment tree from an array of values. + * + * @param values the initial leaf values + */ public CompactSegmentTree(long[] values) { this(values.length); - // TODO(william): Implement smarter construction. for (int i = 0; i < N; i++) modify(i, values[i]); } - // This is the segment tree function we are using for queries. - // The function must be an associative function, meaning - // the following property must hold: f(f(a,b),c) = f(a,f(b,c)). - // Common associative functions used with segment trees - // include: min, max, sum, product, GCD, and etc... - private long function(long a, long b) { - if (a == UNIQUE) return b; - else if (b == UNIQUE) return a; - - return a + b; // sum over a range - // return (a > b) ? a : b; // maximum value over a range - // return (a < b) ? a : b; // minimum value over a range - // return a * b; // product over a range (watch out for overflow!) + /** + * The associative combine function used for queries. This function must + * satisfy f(f(a,b), c) = f(a, f(b,c)) for correct segment tree behavior. + * Null acts as the identity element: f(null, x) = f(x, null) = x. + * + * Change this to customize the query type: + * return a + b; // sum over a range + * return (a > b) ? a : b; // maximum over a range + * return (a < b) ? a : b; // minimum over a range + * return a * b; // product over a range (watch for overflow!) + */ + private Long function(Long a, Long b) { + if (a == null) return b; + if (b == null) return a; + return a + b; } - // Adjust point i by a value, O(log(n)) + /** + * Updates the value at index i by combining it with the given value + * using the combine function, then propagates changes up to the root. + * + * @param i the leaf index to update (0-based) + * @param value the value to combine at position i + * + * Time: O(log(n)) + */ public void modify(int i, long value) { + // Update the leaf node tree[i + N] = function(tree[i + N], value); + // Propagate up: recompute each ancestor from its two children for (i += N; i > 1; i >>= 1) { tree[i >> 1] = function(tree[i], tree[i ^ 1]); } } - // Query interval [l, r), O(log(n)) + /** + * Queries the aggregate value over the half-open interval [l, r). + * + * Works by starting at the leaves and moving up. At each level, if the + * left boundary is a right child, include it and move right. If the right + * boundary is a right child, move left and include it. + * + * @param l left endpoint (inclusive, 0-based) + * @param r right endpoint (exclusive, 0-based) + * @return the combined result over [l, r) + * @throws IllegalStateException if the query range is empty + * + * Time: O(log(n)) + */ public long query(int l, int r) { - long res = UNIQUE; + Long res = null; for (l += N, r += N; l < r; l >>= 1, r >>= 1) { + // If l is a right child, include it and move to next subtree if ((l & 1) != 0) res = function(res, tree[l++]); + // If r is a right child, include its left sibling if ((r & 1) != 0) res = function(res, tree[--r]); } - if (res == UNIQUE) { - throw new IllegalStateException("UNIQUE should not be the return value."); + if (res == null) { + throw new IllegalStateException("Empty query range."); } return res; } public static void main(String[] args) { - // exmaple1(); - example2(); - } - - private static void example1() { - long[] values = new long[] {3, 0, 8, 9, 8, 2, 5, 3, 7, 1}; - CompactSegmentTree st = new CompactSegmentTree(values); - System.out.println(java.util.Arrays.toString(st.tree)); - } - - private static void example2() { long[] values = new long[] {1, 1, 1, 1, 1, 1}; CompactSegmentTree st = new CompactSegmentTree(values); - System.out.println(java.util.Arrays.toString(st.tree)); - + System.out.println(Arrays.toString(st.tree)); System.out.println(st.query(0, 6)); // 6 System.out.println(st.query(1, 5)); // 4 System.out.println(st.query(0, 2)); // 2 diff --git a/src/main/java/com/williamfiset/algorithms/datastructures/suffixarray/SuffixArrayFast.java b/src/main/java/com/williamfiset/algorithms/datastructures/suffixarray/SuffixArrayFast.java index df176d81b..cfba2c852 100644 --- a/src/main/java/com/williamfiset/algorithms/datastructures/suffixarray/SuffixArrayFast.java +++ b/src/main/java/com/williamfiset/algorithms/datastructures/suffixarray/SuffixArrayFast.java @@ -1,18 +1,29 @@ +package com.williamfiset.algorithms.datastructures.suffixarray; + +import java.util.Arrays; + /** - * Suffix array construction implementation. + * Fast Suffix Array Construction (Prefix Doubling with Radix Sort) + * + * Builds a suffix array using prefix doubling with counting sort (radix sort) + * instead of comparison-based sorting. Each doubling round uses two passes of + * counting sort to sort suffix pairs by their rank, achieving O(n) per round + * instead of O(n*log(n)) with comparison sort. * - *

Time Complexity: O(nlog(n)) + * Compare with SuffixArraySlow (O(n^2*log(n))) for a naive approach, and + * SuffixArrayMed (O(n*log^2(n))) for prefix doubling with comparison sort. + * + * Time: O(n*log(n)) -- O(log(n)) doubling rounds, each O(n) with radix sort + * Space: O(n + alphabetSize) * * @author William Fiset, william.alexandre.fiset@gmail.com */ -package com.williamfiset.algorithms.datastructures.suffixarray; - public class SuffixArrayFast extends SuffixArray { private static final int DEFAULT_ALPHABET_SIZE = 256; - int alphabetSize; - int[] sa2, rank, tmp, c; + private int alphabetSize; + private int[] sa2, rank, tmp, c; public SuffixArrayFast(String text) { this(toIntArray(text), DEFAULT_ALPHABET_SIZE); @@ -22,12 +33,22 @@ public SuffixArrayFast(int[] text) { this(text, DEFAULT_ALPHABET_SIZE); } - // Designated constructor + /** + * Creates a suffix array with a custom alphabet size. + * + * @param text the input text as an integer array + * @param alphabetSize the number of distinct symbols (e.g., 256 for ASCII) + */ public SuffixArrayFast(int[] text, int alphabetSize) { super(text); this.alphabetSize = alphabetSize; } + /** + * Constructs the suffix array using prefix doubling with radix sort. + * Each round doubles the comparison window and re-ranks suffixes using + * counting sort for O(n) per round, giving O(n*log(n)) total. + */ @Override protected void construct() { sa = new int[N]; @@ -36,16 +57,34 @@ protected void construct() { c = new int[Math.max(alphabetSize, N)]; int i, p, r; + + // --- Initial sort: rank suffixes by their first character using counting sort --- + + // Count occurrences of each character for (i = 0; i < N; ++i) c[rank[i] = T[i]]++; + // Convert counts to cumulative positions for (i = 1; i < alphabetSize; ++i) c[i] += c[i - 1]; + // Place suffixes into sa in sorted order (stable, right-to-left) for (i = N - 1; i >= 0; --i) sa[--c[T[i]]] = i; + + // --- Prefix doubling: sort by first 2^k characters each round --- for (p = 1; p < N; p <<= 1) { + + // Build sa2: suffixes sorted by their *second half* (positions i+p). + // Suffixes near the end (i >= N-p) have no second half, so they sort first. for (r = 0, i = N - p; i < N; ++i) sa2[r++] = i; + // Remaining suffixes inherit order from sa (already sorted by first half) for (i = 0; i < N; ++i) if (sa[i] >= p) sa2[r++] = sa[i] - p; - java.util.Arrays.fill(c, 0, alphabetSize, 0); + + // Counting sort sa2 by first-half rank to get the final sorted order. + // This is a radix sort: sa2 provides second-key order, we sort by first key. + Arrays.fill(c, 0, alphabetSize, 0); for (i = 0; i < N; ++i) c[rank[i]]++; for (i = 1; i < alphabetSize; ++i) c[i] += c[i - 1]; for (i = N - 1; i >= 0; --i) sa[--c[rank[sa2[i]]]] = sa2[i]; + + // Compute new ranks from the sorted order. Two suffixes get the same + // rank only if both their first-half and second-half ranks match. for (sa2[sa[0]] = r = 0, i = 1; i < N; ++i) { if (!(rank[sa[i - 1]] == rank[sa[i]] && sa[i - 1] + p < N @@ -53,9 +92,13 @@ protected void construct() { && rank[sa[i - 1] + p] == rank[sa[i] + p])) r++; sa2[sa[i]] = r; } + + // Swap rank and sa2 arrays to avoid allocation tmp = rank; rank = sa2; sa2 = tmp; + + // All ranks unique means sorting is complete if (r == N - 1) break; alphabetSize = r + 1; } diff --git a/src/main/java/com/williamfiset/algorithms/datastructures/suffixarray/SuffixArrayMed.java b/src/main/java/com/williamfiset/algorithms/datastructures/suffixarray/SuffixArrayMed.java index 23e4e9002..cb3965bb5 100644 --- a/src/main/java/com/williamfiset/algorithms/datastructures/suffixarray/SuffixArrayMed.java +++ b/src/main/java/com/williamfiset/algorithms/datastructures/suffixarray/SuffixArrayMed.java @@ -1,18 +1,29 @@ +package com.williamfiset.algorithms.datastructures.suffixarray; + +import java.util.Arrays; + /** - * Medium speed suffix array implementation. Time Complexity: O(nlog^2(n)) + * Medium-speed Suffix Array Construction (Prefix Doubling) + * + * Builds a suffix array by repeatedly doubling the prefix length used for + * ranking. In each round, suffixes are sorted by their first 2^k characters + * using the ranks from the previous round as a two-key comparison. + * + * Compare with SuffixArraySlow (O(n^2 log n)) for a simpler but slower approach, + * and SuffixArrayFast (O(n*log(n))) for an optimized version using radix sort. + * + * Time: O(n*log^2(n)) — O(log(n)) doubling rounds, each with O(n*log(n)) sort + * Space: O(n) * * @author William Fiset, william.alexandre.fiset@gmail.com */ -package com.williamfiset.algorithms.datastructures.suffixarray; - public class SuffixArrayMed extends SuffixArray { - // Wrapper class to help sort suffix ranks - static class SuffixRankTuple implements Comparable { - + // Holds the two-key rank (first half, second half) and original index + // for sorting suffixes by their first 2^k characters. + private static class SuffixRankTuple implements Comparable { int firstHalf, secondHalf, originalIndex; - // Sort Suffix ranks first on the first half then the second half @Override public int compareTo(SuffixRankTuple other) { int cmp = Integer.compare(firstHalf, other.firstHalf); @@ -34,25 +45,28 @@ public SuffixArrayMed(int[] text) { super(text); } - // Construct a suffix array in O(nlog^2(n)) + /** + * Constructs the suffix array using prefix doubling. Each iteration doubles + * the window size and re-ranks suffixes until all ranks are unique. + */ @Override protected void construct() { sa = new int[N]; - // Maintain suffix ranks in both a matrix with two rows containing the - // current and last rank information as well as some sortable rank objects + // Two-row matrix: row 0 = current ranks, row 1 = new ranks int[][] suffixRanks = new int[2][N]; SuffixRankTuple[] ranks = new SuffixRankTuple[N]; - // Assign a numerical value to each character in the text + // Initial ranks are the character values themselves for (int i = 0; i < N; i++) { suffixRanks[0][i] = T[i]; ranks[i] = new SuffixRankTuple(); } - // O(log(n)) + // Double the prefix length each round: 1, 2, 4, 8, ... → O(log(n)) rounds for (int pos = 1; pos < N; pos *= 2) { + // Build two-key tuples: (rank of first half, rank of second half) for (int i = 0; i < N; i++) { SuffixRankTuple suffixRank = ranks[i]; suffixRank.firstHalf = suffixRanks[0][i]; @@ -60,61 +74,36 @@ protected void construct() { suffixRank.originalIndex = i; } - // O(nlog(n)) - java.util.Arrays.sort(ranks); + Arrays.sort(ranks); + // Assign new ranks based on sorted order int newRank = 0; suffixRanks[1][ranks[0].originalIndex] = 0; for (int i = 1; i < N; i++) { + SuffixRankTuple prev = ranks[i - 1]; + SuffixRankTuple cur = ranks[i]; - SuffixRankTuple lastSuffixRank = ranks[i - 1]; - SuffixRankTuple currSuffixRank = ranks[i]; + // Increment rank only when the tuple differs from the previous + if (cur.firstHalf != prev.firstHalf || cur.secondHalf != prev.secondHalf) + newRank++; - // If the first half differs from the second half - if (currSuffixRank.firstHalf != lastSuffixRank.firstHalf - || currSuffixRank.secondHalf != lastSuffixRank.secondHalf) newRank++; - - suffixRanks[1][currSuffixRank.originalIndex] = newRank; + suffixRanks[1][cur.originalIndex] = newRank; } - // Place top row (current row) to be the last row suffixRanks[0] = suffixRanks[1]; - // Optimization to stop early + // All ranks unique means sorting is complete if (newRank == N - 1) break; } - // Fill suffix array for (int i = 0; i < N; i++) { sa[i] = ranks[i].originalIndex; - ranks[i] = null; } - - // Cleanup - suffixRanks[0] = suffixRanks[1] = null; - suffixRanks = null; - ranks = null; } public static void main(String[] args) { - - // String[] strs = { "AAGAAGC", "AGAAGT", "CGAAGC" }; - // String[] strs = { "abca", "bcad", "daca" }; - // String[] strs = { "abca", "bcad", "daca" }; - // String[] strs = { "AABC", "BCDC", "BCDE", "CDED" }; - // String[] strs = { "abcdefg", "bcdefgh", "cdefghi" }; - // String[] strs = { "xxx", "yyy", "zzz" }; - // TreeSet lcss = SuffixArrayMed.lcs(strs, 2); - // System.out.println(lcss); - - // SuffixArrayMed sa = new SuffixArrayMed("abracadabra"); - // System.out.println(sa); - // System.out.println(java.util.Arrays.toString(sa.sa)); - // System.out.println(java.util.Arrays.toString(sa.lcp)); - SuffixArrayMed sa = new SuffixArrayMed("ABBABAABAA"); - // SuffixArrayMed sa = new SuffixArrayMed("GAGAGAGAGAGAG"); System.out.println(sa); } } diff --git a/src/main/java/com/williamfiset/algorithms/datastructures/suffixarray/SuffixArraySlow.java b/src/main/java/com/williamfiset/algorithms/datastructures/suffixarray/SuffixArraySlow.java index 975e20e6f..d3d296abf 100644 --- a/src/main/java/com/williamfiset/algorithms/datastructures/suffixarray/SuffixArraySlow.java +++ b/src/main/java/com/williamfiset/algorithms/datastructures/suffixarray/SuffixArraySlow.java @@ -1,31 +1,41 @@ +package com.williamfiset.algorithms.datastructures.suffixarray; + +import java.util.Arrays; + /** - * Naive suffix array implementation. + * Naive Suffix Array Construction + * + * Builds a suffix array by generating all suffixes, sorting them with + * a standard comparison sort, and extracting the sorted indices. + * Simple to understand but slow for large inputs. * - *

Time Complexity: O(n^2log(n)) + * Compare with SuffixArrayMed (O(n*log^2(n))) and SuffixArrayFast (O(n*log(n))) + * to see progressively more efficient construction algorithms. + * + * Time: O(n^2*log(n)) — sorting is O(n*log(n)) comparisons, each O(n) + * Space: O(n) * * @author William Fiset, william.alexandre.fiset@gmail.com */ -package com.williamfiset.algorithms.datastructures.suffixarray; - public class SuffixArraySlow extends SuffixArray { private static class Suffix implements Comparable { - // Starting position of suffix in text final int index, len; final int[] text; - public Suffix(int[] text, int index) { + Suffix(int[] text, int index) { this.len = text.length - index; this.index = index; this.text = text; } - // Compare the two suffixes inspired by Robert Sedgewick and Kevin Wayne + // Lexicographic comparison of two suffixes, character by character. + // If one suffix is a prefix of the other, the shorter one comes first. @Override public int compareTo(Suffix other) { if (this == other) return 0; - int min_len = Math.min(len, other.len); - for (int i = 0; i < min_len; i++) { + int minLen = Math.min(len, other.len); + for (int i = 0; i < minLen; i++) { if (text[index + i] < other.text[other.index + i]) return -1; if (text[index + i] > other.text[other.index + i]) return +1; } @@ -38,8 +48,7 @@ public String toString() { } } - // Contains all the suffixes of the SuffixArray - Suffix[] suffixes; + private Suffix[] suffixes; public SuffixArraySlow(String text) { super(toIntArray(text)); @@ -49,8 +58,10 @@ public SuffixArraySlow(int[] text) { super(text); } - // Suffix array construction. This actually takes O(n^2log(n)) time since sorting takes on - // average O(nlog(n)) and each String comparison takes O(n). + /** + * Constructs the suffix array by creating all n suffixes, sorting + * them lexicographically, then storing the sorted starting indices. + */ @Override protected void construct() { sa = new int[N]; @@ -58,12 +69,10 @@ protected void construct() { for (int i = 0; i < N; i++) suffixes[i] = new Suffix(T, i); - java.util.Arrays.sort(suffixes); + Arrays.sort(suffixes); for (int i = 0; i < N; i++) { - Suffix suffix = suffixes[i]; - sa[i] = suffix.index; - suffixes[i] = null; + sa[i] = suffixes[i].index; } suffixes = null; diff --git a/src/test/java/com/williamfiset/algorithms/datastructures/segmenttree/BUILD b/src/test/java/com/williamfiset/algorithms/datastructures/segmenttree/BUILD index 7bafacfb9..f896cfcf9 100644 --- a/src/test/java/com/williamfiset/algorithms/datastructures/segmenttree/BUILD +++ b/src/test/java/com/williamfiset/algorithms/datastructures/segmenttree/BUILD @@ -108,5 +108,16 @@ java_test( deps = TEST_DEPS, ) +# bazel test //src/test/java/com/williamfiset/algorithms/datastructures/segmenttree:CompactSegmentTreeTest +java_test( + name = "CompactSegmentTreeTest", + srcs = ["CompactSegmentTreeTest.java"], + main_class = "org.junit.platform.console.ConsoleLauncher", + use_testrunner = False, + args = ["--select-class=com.williamfiset.algorithms.datastructures.segmenttree.CompactSegmentTreeTest"], + runtime_deps = JUNIT5_RUNTIME_DEPS, + deps = TEST_DEPS, +) + # Run all tests # bazel test //src/test/java/com/williamfiset/algorithms/datastructures/segmenttree:all diff --git a/src/test/java/com/williamfiset/algorithms/datastructures/segmenttree/CompactSegmentTreeTest.java b/src/test/java/com/williamfiset/algorithms/datastructures/segmenttree/CompactSegmentTreeTest.java new file mode 100644 index 000000000..c1ad161d1 --- /dev/null +++ b/src/test/java/com/williamfiset/algorithms/datastructures/segmenttree/CompactSegmentTreeTest.java @@ -0,0 +1,124 @@ +package com.williamfiset.algorithms.datastructures.segmenttree; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class CompactSegmentTreeTest { + + @Test + public void testSumQueryBasic() { + long[] values = {1, 2, 3, 4, 5}; + CompactSegmentTree st = new CompactSegmentTree(values); + assertThat(st.query(0, 5)).isEqualTo(15); + assertThat(st.query(0, 3)).isEqualTo(6); + assertThat(st.query(2, 5)).isEqualTo(12); + } + + @Test + public void testSingleElement() { + long[] values = {42}; + CompactSegmentTree st = new CompactSegmentTree(values); + assertThat(st.query(0, 1)).isEqualTo(42); + } + + @Test + public void testTwoElements() { + long[] values = {3, 7}; + CompactSegmentTree st = new CompactSegmentTree(values); + assertThat(st.query(0, 2)).isEqualTo(10); + assertThat(st.query(0, 1)).isEqualTo(3); + assertThat(st.query(1, 2)).isEqualTo(7); + } + + @Test + public void testPointUpdate() { + long[] values = {1, 1, 1, 1, 1, 1}; + CompactSegmentTree st = new CompactSegmentTree(values); + assertThat(st.query(0, 6)).isEqualTo(6); + + // modify combines with existing value (sum), so adding 5 to index 2 + // changes value from 1 to 6 + st.modify(2, 5); + assertThat(st.query(0, 6)).isEqualTo(11); + assertThat(st.query(2, 3)).isEqualTo(6); + } + + @Test + public void testQuerySingleElementInRange() { + long[] values = {10, 20, 30, 40, 50}; + CompactSegmentTree st = new CompactSegmentTree(values); + for (int i = 0; i < values.length; i++) { + assertThat(st.query(i, i + 1)).isEqualTo(values[i]); + } + } + + @Test + public void testAllZeros() { + long[] values = {0, 0, 0, 0}; + CompactSegmentTree st = new CompactSegmentTree(values); + assertThat(st.query(0, 4)).isEqualTo(0); + assertThat(st.query(1, 3)).isEqualTo(0); + } + + @Test + public void testNegativeValues() { + long[] values = {-5, 3, -2, 7, -1}; + CompactSegmentTree st = new CompactSegmentTree(values); + assertThat(st.query(0, 5)).isEqualTo(2); + assertThat(st.query(0, 2)).isEqualTo(-2); + assertThat(st.query(3, 5)).isEqualTo(6); + } + + @Test + public void testMultipleUpdates() { + long[] values = {1, 2, 3}; + CompactSegmentTree st = new CompactSegmentTree(values); + assertThat(st.query(0, 3)).isEqualTo(6); + + st.modify(0, 10); // 1 + 10 = 11 + st.modify(1, 20); // 2 + 20 = 22 + st.modify(2, 30); // 3 + 30 = 33 + assertThat(st.query(0, 3)).isEqualTo(66); + } + + @Test + public void testSizeConstructor() { + // Empty tree created with size constructor, then populated with modify + CompactSegmentTree st = new CompactSegmentTree(4); + st.modify(0, 5); + st.modify(1, 10); + st.modify(2, 15); + st.modify(3, 20); + assertThat(st.query(0, 4)).isEqualTo(50); + assertThat(st.query(1, 3)).isEqualTo(25); + } + + // Query with equal l and r is an empty range — should throw since + // the result would be the UNIQUE sentinel value. + @Test + public void testEmptyRangeQueryThrows() { + long[] values = {1, 2, 3}; + CompactSegmentTree st = new CompactSegmentTree(values); + assertThrows(IllegalStateException.class, () -> st.query(1, 1)); + } + + @Test + public void testLargerArray() { + int n = 100; + long[] values = new long[n]; + long total = 0; + for (int i = 0; i < n; i++) { + values[i] = i + 1; + total += values[i]; + } + CompactSegmentTree st = new CompactSegmentTree(values); + assertThat(st.query(0, n)).isEqualTo(total); + + // Query first half: sum of 1..50 = 1275 + assertThat(st.query(0, 50)).isEqualTo(1275); + // Query second half: sum of 51..100 = 3775 + assertThat(st.query(50, 100)).isEqualTo(3775); + } +} diff --git a/src/test/java/com/williamfiset/algorithms/datastructures/suffixarray/SuffixArrayTest.java b/src/test/java/com/williamfiset/algorithms/datastructures/suffixarray/SuffixArrayTest.java index b8f7c0dc2..463cf2f73 100644 --- a/src/test/java/com/williamfiset/algorithms/datastructures/suffixarray/SuffixArrayTest.java +++ b/src/test/java/com/williamfiset/algorithms/datastructures/suffixarray/SuffixArrayTest.java @@ -1,49 +1,69 @@ package com.williamfiset.algorithms.datastructures.suffixarray; import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; -import java.security.SecureRandom; import java.util.Random; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.Test; public class SuffixArrayTest { - static final SecureRandom random = new SecureRandom(); - static final Random rand = new Random(); + static final String ASCII_LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - static final int LOOPS = 1000; - static final int TEST_SZ = 40; - static final int NUM_NULLS = TEST_SZ / 5; - static final int MAX_RAND_NUM = 250; - - String ASCII_LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - - @BeforeEach - public void setup() {} + // Helper: create all 3 implementations for the same text + private static SuffixArray[] allImplementations(String text) { + return new SuffixArray[] { + new SuffixArraySlow(text), + new SuffixArrayMed(text), + new SuffixArrayFast(text) + }; + } @Test - public void suffixArrayLength() { - String str = "ABCDE"; - - SuffixArray sa1 = new SuffixArraySlow(str); - SuffixArray sa2 = new SuffixArrayMed(str); - SuffixArray sa3 = new SuffixArrayFast(str); + public void testNullTextThrows() { + assertThrows(IllegalArgumentException.class, () -> new SuffixArraySlow((int[]) null)); + assertThrows(IllegalArgumentException.class, () -> new SuffixArrayMed((int[]) null)); + assertThrows(IllegalArgumentException.class, () -> new SuffixArrayFast((int[]) null)); + } - assertThat(sa1.getSa().length).isEqualTo(str.length()); - assertThat(sa2.getSa().length).isEqualTo(str.length()); - assertThat(sa3.getSa().length).isEqualTo(str.length()); + @Test + public void testSingleCharacter() { + for (SuffixArray sa : allImplementations("A")) { + assertThat(sa.getSa()).isEqualTo(new int[] {0}); + assertThat(sa.getLcpArray()).isEqualTo(new int[] {0}); + } } @Test - public void lcsUniqueCharacters() { + public void testTwoCharactersSorted() { + // "AB" -> suffixes: "AB"(0), "B"(1) -> sorted: "AB","B" -> sa=[0,1] + for (SuffixArray sa : allImplementations("AB")) { + assertThat(sa.getSa()).isEqualTo(new int[] {0, 1}); + assertThat(sa.getLcpArray()).isEqualTo(new int[] {0, 0}); + } + } - SuffixArray sa1 = new SuffixArraySlow(ASCII_LETTERS); - SuffixArray sa2 = new SuffixArrayMed(ASCII_LETTERS); - SuffixArray sa3 = new SuffixArrayFast(ASCII_LETTERS); + @Test + public void testTwoCharactersReversed() { + // "BA" -> suffixes: "BA"(0), "A"(1) -> sorted: "A","BA" -> sa=[1,0] + for (SuffixArray sa : allImplementations("BA")) { + assertThat(sa.getSa()).isEqualTo(new int[] {1, 0}); + assertThat(sa.getLcpArray()).isEqualTo(new int[] {0, 0}); + } + } - SuffixArray[] suffixArrays = {sa1, sa2, sa3}; + @Test + public void testSuffixArrayLength() { + String str = "ABCDE"; + for (SuffixArray sa : allImplementations(str)) { + assertThat(sa.getSa().length).isEqualTo(str.length()); + assertThat(sa.getTextLength()).isEqualTo(str.length()); + } + } - for (SuffixArray sa : suffixArrays) { + @Test + public void testLcpAllZerosForUniqueCharacters() { + for (SuffixArray sa : allImplementations(ASCII_LETTERS)) { for (int i = 0; i < sa.getSa().length; i++) { assertThat(sa.getLcpArray()[i]).isEqualTo(0); } @@ -51,17 +71,10 @@ public void lcsUniqueCharacters() { } @Test - public void increasingLCPTest() { - - String UNIQUE_CHARS = "KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK"; - - SuffixArray sa1 = new SuffixArraySlow(UNIQUE_CHARS); - SuffixArray sa2 = new SuffixArrayMed(UNIQUE_CHARS); - SuffixArray sa3 = new SuffixArrayFast(UNIQUE_CHARS); - - SuffixArray[] suffixArrays = {sa1, sa2, sa3}; - - for (SuffixArray sa : suffixArrays) { + public void testLcpIncreasingForRepeatedCharacter() { + // All same character: LCP[i] = i since suffixes are "KKK...", "KK...", "K..." + String repeated = "KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK"; + for (SuffixArray sa : allImplementations(repeated)) { for (int i = 0; i < sa.getSa().length; i++) { assertThat(sa.getLcpArray()[i]).isEqualTo(i); } @@ -69,60 +82,90 @@ public void increasingLCPTest() { } @Test - public void lcpTest1() { - + public void testLcpKnownValues1() { String text = "ABBABAABAA"; - int[] lcpValues = {0, 1, 2, 1, 4, 2, 0, 3, 2, 1}; - - SuffixArray sa1 = new SuffixArraySlow(text); - SuffixArray sa2 = new SuffixArrayMed(text); - SuffixArray sa3 = new SuffixArrayFast(text); - - SuffixArray[] suffixArrays = {sa1, sa2, sa3}; - - for (SuffixArray sa : suffixArrays) { - for (int i = 0; i < sa.getSa().length; i++) { - assertThat(lcpValues[i]).isEqualTo(sa.getLcpArray()[i]); - } + int[] expected = {0, 1, 2, 1, 4, 2, 0, 3, 2, 1}; + for (SuffixArray sa : allImplementations(text)) { + assertThat(sa.getLcpArray()).isEqualTo(expected); } } @Test - public void lcpTest2() { + public void testLcpKnownValues2() { String text = "ABABABAABB"; - int[] lcpValues = {0, 1, 3, 5, 2, 0, 1, 2, 4, 1}; - - SuffixArray sa1 = new SuffixArraySlow(text); - SuffixArray sa2 = new SuffixArrayMed(text); - SuffixArray sa3 = new SuffixArrayFast(text); - - SuffixArray[] suffixArrays = {sa1, sa2, sa3}; + int[] expected = {0, 1, 3, 5, 2, 0, 1, 2, 4, 1}; + for (SuffixArray sa : allImplementations(text)) { + assertThat(sa.getLcpArray()).isEqualTo(expected); + } + } - for (SuffixArray sa : suffixArrays) { - for (int i = 0; i < sa.getSa().length; i++) { - assertThat(lcpValues[i]).isEqualTo(sa.getLcpArray()[i]); + // Verify the suffix array actually produces lexicographically sorted suffixes + @Test + public void testSuffixesAreSorted() { + String text = "ABBABAABAA"; + for (SuffixArray sa : allImplementations(text)) { + int[] arr = sa.getSa(); + for (int i = 0; i < arr.length - 1; i++) { + String s1 = text.substring(arr[i]); + String s2 = text.substring(arr[i + 1]); + assertThat(s1.compareTo(s2)).isLessThan(0); } } } @Test - public void saConstruction() { - // Test inspired by LCS. Make sure constructed SAs are equal. - // Use digits 0-9 to fake unique tokens + public void testConstructionConsistency() { + // All 3 implementations must produce the same SA String text = "BAAAAB0ABAAAAB1BABA2ABA3AAB4BBBB5BB"; + SuffixArray[] impls = allImplementations(text); + for (int i = 0; i < impls.length; i++) { + for (int j = i + 1; j < impls.length; j++) { + assertThat(impls[i].getSa()).isEqualTo(impls[j].getSa()); + } + } + } + @Test + public void testIntArrayConstructor() { + // "CAB" as int array + int[] text = {67, 65, 66}; SuffixArray sa1 = new SuffixArraySlow(text); SuffixArray sa2 = new SuffixArrayMed(text); SuffixArray sa3 = new SuffixArrayFast(text); - SuffixArray[] suffixArrays = {sa1, sa2, sa3}; - - for (int i = 0; i < suffixArrays.length; i++) { - for (int j = i + 1; j < suffixArrays.length; j++) { - SuffixArray s1 = suffixArrays[i]; - SuffixArray s2 = suffixArrays[j]; - for (int k = 0; k < s1.getSa().length; k++) { - assertThat(s1.getSa()[k]).isEqualTo(s2.getSa()[k]); - } + + // Suffixes: "CAB"(0), "AB"(1), "B"(2) -> sorted: "AB","B","CAB" -> sa=[1,2,0] + int[] expected = {1, 2, 0}; + assertThat(sa1.getSa()).isEqualTo(expected); + assertThat(sa2.getSa()).isEqualTo(expected); + assertThat(sa3.getSa()).isEqualTo(expected); + } + + // Randomized cross-validation: all implementations must agree on random inputs + @Test + public void testRandomStringsAllImplementationsAgree() { + Random rand = new Random(42); + for (int loop = 0; loop < 200; loop++) { + int len = 2 + rand.nextInt(20); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < len; i++) { + sb.append((char) ('A' + rand.nextInt(5))); + } + String text = sb.toString(); + + SuffixArray[] impls = allImplementations(text); + + // All SAs must match + for (int i = 1; i < impls.length; i++) { + assertThat(impls[i].getSa()).isEqualTo(impls[0].getSa()); + assertThat(impls[i].getLcpArray()).isEqualTo(impls[0].getLcpArray()); + } + + // Verify sorted order + int[] sa = impls[0].getSa(); + for (int i = 0; i < sa.length - 1; i++) { + String s1 = text.substring(sa[i]); + String s2 = text.substring(sa[i + 1]); + assertThat(s1.compareTo(s2)).isLessThan(0); } } }