Skip to content

Commit 891a227

Browse files
williamfisetclaude
andauthored
Refactor CompactSegmentTree and add dedicated tests (williamfiset#1274)
Apply SKILL.md conventions: fix package/header order, add detailed file-level docs explaining bottom-up array layout, replace FQN with import, make UNIQUE static final, add Javadoc on all public methods, add educational inline comments, clean up main(). Add 11 new tests covering sum queries, point updates, negative values, edge cases. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2b9b292 commit 891a227

3 files changed

Lines changed: 217 additions & 44 deletions

File tree

src/main/java/com/williamfiset/algorithms/datastructures/segmenttree/CompactSegmentTree.java

Lines changed: 82 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,123 @@
1+
package com.williamfiset.algorithms.datastructures.segmenttree;
2+
3+
import java.util.Arrays;
4+
15
/**
2-
* A compact array based segment tree implementation. This segment tree supports point updates and
3-
* range queries.
6+
* Compact Array-Based Segment Tree
7+
*
8+
* A space-efficient segment tree stored in a flat array of size 2*n (no
9+
* recursion, no pointers). Supports point updates and range queries using
10+
* any associative combine function (sum, min, max, product, GCD, etc.).
11+
*
12+
* The tree is stored bottom-up: leaves occupy indices [n, 2n) and internal
13+
* nodes occupy [1, n). Index 0 is unused. Each internal node i is the
14+
* combination of its children at 2i and 2i+1.
15+
*
16+
* Use cases:
17+
* - Range sum / min / max queries with point updates
18+
* - Competitive programming (very short, cache-friendly implementation)
19+
*
20+
* Time: O(n) construction, O(log(n)) per query and update
21+
* Space: O(n)
422
*
523
* @author Al.Cash & William Fiset, william.alexandre.fiset@gmail.com
624
*/
7-
package com.williamfiset.algorithms.datastructures.segmenttree;
8-
925
public class CompactSegmentTree {
1026

1127
private int N;
1228

13-
// Let UNIQUE be a value which does NOT
14-
// and will not appear in the segment tree
15-
private long UNIQUE = 8123572096793136074L;
16-
17-
// Segment tree values
18-
private long[] tree;
29+
// Flat array storing the segment tree. Leaves are at indices [N, 2N),
30+
// internal nodes at [1, N). Index 0 is unused. Uninitialized slots
31+
// are null, which acts as the identity element for the combine function.
32+
private Long[] tree;
1933

34+
/**
35+
* Creates an empty segment tree of the given size, with all slots
36+
* initialized to null.
37+
*
38+
* @param size the number of elements (leaves) in the segment tree
39+
*/
2040
public CompactSegmentTree(int size) {
21-
tree = new long[2 * (N = size)];
22-
java.util.Arrays.fill(tree, UNIQUE);
41+
tree = new Long[2 * (N = size)];
2342
}
2443

44+
/**
45+
* Creates a segment tree from an array of values.
46+
*
47+
* @param values the initial leaf values
48+
*/
2549
public CompactSegmentTree(long[] values) {
2650
this(values.length);
27-
// TODO(william): Implement smarter construction.
2851
for (int i = 0; i < N; i++) modify(i, values[i]);
2952
}
3053

31-
// This is the segment tree function we are using for queries.
32-
// The function must be an associative function, meaning
33-
// the following property must hold: f(f(a,b),c) = f(a,f(b,c)).
34-
// Common associative functions used with segment trees
35-
// include: min, max, sum, product, GCD, and etc...
36-
private long function(long a, long b) {
37-
if (a == UNIQUE) return b;
38-
else if (b == UNIQUE) return a;
39-
40-
return a + b; // sum over a range
41-
// return (a > b) ? a : b; // maximum value over a range
42-
// return (a < b) ? a : b; // minimum value over a range
43-
// return a * b; // product over a range (watch out for overflow!)
54+
/**
55+
* The associative combine function used for queries. This function must
56+
* satisfy f(f(a,b), c) = f(a, f(b,c)) for correct segment tree behavior.
57+
* Null acts as the identity element: f(null, x) = f(x, null) = x.
58+
*
59+
* Change this to customize the query type:
60+
* return a + b; // sum over a range
61+
* return (a > b) ? a : b; // maximum over a range
62+
* return (a < b) ? a : b; // minimum over a range
63+
* return a * b; // product over a range (watch for overflow!)
64+
*/
65+
private Long function(Long a, Long b) {
66+
if (a == null) return b;
67+
if (b == null) return a;
68+
return a + b;
4469
}
4570

46-
// Adjust point i by a value, O(log(n))
71+
/**
72+
* Updates the value at index i by combining it with the given value
73+
* using the combine function, then propagates changes up to the root.
74+
*
75+
* @param i the leaf index to update (0-based)
76+
* @param value the value to combine at position i
77+
*
78+
* Time: O(log(n))
79+
*/
4780
public void modify(int i, long value) {
81+
// Update the leaf node
4882
tree[i + N] = function(tree[i + N], value);
83+
// Propagate up: recompute each ancestor from its two children
4984
for (i += N; i > 1; i >>= 1) {
5085
tree[i >> 1] = function(tree[i], tree[i ^ 1]);
5186
}
5287
}
5388

54-
// Query interval [l, r), O(log(n))
89+
/**
90+
* Queries the aggregate value over the half-open interval [l, r).
91+
*
92+
* Works by starting at the leaves and moving up. At each level, if the
93+
* left boundary is a right child, include it and move right. If the right
94+
* boundary is a right child, move left and include it.
95+
*
96+
* @param l left endpoint (inclusive, 0-based)
97+
* @param r right endpoint (exclusive, 0-based)
98+
* @return the combined result over [l, r)
99+
* @throws IllegalStateException if the query range is empty
100+
*
101+
* Time: O(log(n))
102+
*/
55103
public long query(int l, int r) {
56-
long res = UNIQUE;
104+
Long res = null;
57105
for (l += N, r += N; l < r; l >>= 1, r >>= 1) {
106+
// If l is a right child, include it and move to next subtree
58107
if ((l & 1) != 0) res = function(res, tree[l++]);
108+
// If r is a right child, include its left sibling
59109
if ((r & 1) != 0) res = function(res, tree[--r]);
60110
}
61-
if (res == UNIQUE) {
62-
throw new IllegalStateException("UNIQUE should not be the return value.");
111+
if (res == null) {
112+
throw new IllegalStateException("Empty query range.");
63113
}
64114
return res;
65115
}
66116

67117
public static void main(String[] args) {
68-
// exmaple1();
69-
example2();
70-
}
71-
72-
private static void example1() {
73-
long[] values = new long[] {3, 0, 8, 9, 8, 2, 5, 3, 7, 1};
74-
CompactSegmentTree st = new CompactSegmentTree(values);
75-
System.out.println(java.util.Arrays.toString(st.tree));
76-
}
77-
78-
private static void example2() {
79118
long[] values = new long[] {1, 1, 1, 1, 1, 1};
80119
CompactSegmentTree st = new CompactSegmentTree(values);
81-
System.out.println(java.util.Arrays.toString(st.tree));
82-
120+
System.out.println(Arrays.toString(st.tree));
83121
System.out.println(st.query(0, 6)); // 6
84122
System.out.println(st.query(1, 5)); // 4
85123
System.out.println(st.query(0, 2)); // 2

src/test/java/com/williamfiset/algorithms/datastructures/segmenttree/BUILD

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,5 +108,16 @@ java_test(
108108
deps = TEST_DEPS,
109109
)
110110

111+
# bazel test //src/test/java/com/williamfiset/algorithms/datastructures/segmenttree:CompactSegmentTreeTest
112+
java_test(
113+
name = "CompactSegmentTreeTest",
114+
srcs = ["CompactSegmentTreeTest.java"],
115+
main_class = "org.junit.platform.console.ConsoleLauncher",
116+
use_testrunner = False,
117+
args = ["--select-class=com.williamfiset.algorithms.datastructures.segmenttree.CompactSegmentTreeTest"],
118+
runtime_deps = JUNIT5_RUNTIME_DEPS,
119+
deps = TEST_DEPS,
120+
)
121+
111122
# Run all tests
112123
# bazel test //src/test/java/com/williamfiset/algorithms/datastructures/segmenttree:all
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package com.williamfiset.algorithms.datastructures.segmenttree;
2+
3+
import static com.google.common.truth.Truth.assertThat;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
6+
import org.junit.jupiter.api.Test;
7+
8+
public class CompactSegmentTreeTest {
9+
10+
@Test
11+
public void testSumQueryBasic() {
12+
long[] values = {1, 2, 3, 4, 5};
13+
CompactSegmentTree st = new CompactSegmentTree(values);
14+
assertThat(st.query(0, 5)).isEqualTo(15);
15+
assertThat(st.query(0, 3)).isEqualTo(6);
16+
assertThat(st.query(2, 5)).isEqualTo(12);
17+
}
18+
19+
@Test
20+
public void testSingleElement() {
21+
long[] values = {42};
22+
CompactSegmentTree st = new CompactSegmentTree(values);
23+
assertThat(st.query(0, 1)).isEqualTo(42);
24+
}
25+
26+
@Test
27+
public void testTwoElements() {
28+
long[] values = {3, 7};
29+
CompactSegmentTree st = new CompactSegmentTree(values);
30+
assertThat(st.query(0, 2)).isEqualTo(10);
31+
assertThat(st.query(0, 1)).isEqualTo(3);
32+
assertThat(st.query(1, 2)).isEqualTo(7);
33+
}
34+
35+
@Test
36+
public void testPointUpdate() {
37+
long[] values = {1, 1, 1, 1, 1, 1};
38+
CompactSegmentTree st = new CompactSegmentTree(values);
39+
assertThat(st.query(0, 6)).isEqualTo(6);
40+
41+
// modify combines with existing value (sum), so adding 5 to index 2
42+
// changes value from 1 to 6
43+
st.modify(2, 5);
44+
assertThat(st.query(0, 6)).isEqualTo(11);
45+
assertThat(st.query(2, 3)).isEqualTo(6);
46+
}
47+
48+
@Test
49+
public void testQuerySingleElementInRange() {
50+
long[] values = {10, 20, 30, 40, 50};
51+
CompactSegmentTree st = new CompactSegmentTree(values);
52+
for (int i = 0; i < values.length; i++) {
53+
assertThat(st.query(i, i + 1)).isEqualTo(values[i]);
54+
}
55+
}
56+
57+
@Test
58+
public void testAllZeros() {
59+
long[] values = {0, 0, 0, 0};
60+
CompactSegmentTree st = new CompactSegmentTree(values);
61+
assertThat(st.query(0, 4)).isEqualTo(0);
62+
assertThat(st.query(1, 3)).isEqualTo(0);
63+
}
64+
65+
@Test
66+
public void testNegativeValues() {
67+
long[] values = {-5, 3, -2, 7, -1};
68+
CompactSegmentTree st = new CompactSegmentTree(values);
69+
assertThat(st.query(0, 5)).isEqualTo(2);
70+
assertThat(st.query(0, 2)).isEqualTo(-2);
71+
assertThat(st.query(3, 5)).isEqualTo(6);
72+
}
73+
74+
@Test
75+
public void testMultipleUpdates() {
76+
long[] values = {1, 2, 3};
77+
CompactSegmentTree st = new CompactSegmentTree(values);
78+
assertThat(st.query(0, 3)).isEqualTo(6);
79+
80+
st.modify(0, 10); // 1 + 10 = 11
81+
st.modify(1, 20); // 2 + 20 = 22
82+
st.modify(2, 30); // 3 + 30 = 33
83+
assertThat(st.query(0, 3)).isEqualTo(66);
84+
}
85+
86+
@Test
87+
public void testSizeConstructor() {
88+
// Empty tree created with size constructor, then populated with modify
89+
CompactSegmentTree st = new CompactSegmentTree(4);
90+
st.modify(0, 5);
91+
st.modify(1, 10);
92+
st.modify(2, 15);
93+
st.modify(3, 20);
94+
assertThat(st.query(0, 4)).isEqualTo(50);
95+
assertThat(st.query(1, 3)).isEqualTo(25);
96+
}
97+
98+
// Query with equal l and r is an empty range — should throw since
99+
// the result would be the UNIQUE sentinel value.
100+
@Test
101+
public void testEmptyRangeQueryThrows() {
102+
long[] values = {1, 2, 3};
103+
CompactSegmentTree st = new CompactSegmentTree(values);
104+
assertThrows(IllegalStateException.class, () -> st.query(1, 1));
105+
}
106+
107+
@Test
108+
public void testLargerArray() {
109+
int n = 100;
110+
long[] values = new long[n];
111+
long total = 0;
112+
for (int i = 0; i < n; i++) {
113+
values[i] = i + 1;
114+
total += values[i];
115+
}
116+
CompactSegmentTree st = new CompactSegmentTree(values);
117+
assertThat(st.query(0, n)).isEqualTo(total);
118+
119+
// Query first half: sum of 1..50 = 1275
120+
assertThat(st.query(0, 50)).isEqualTo(1275);
121+
// Query second half: sum of 51..100 = 3775
122+
assertThat(st.query(50, 100)).isEqualTo(3775);
123+
}
124+
}

0 commit comments

Comments
 (0)