Practical decisions
How to choose between a Fenwick tree and a segment tree, what each costs in memory, how to write the iterative version, how to convert indices, and how to test range boundaries.
Learning goals
- Choose the right structure for an operation and a set of queries
- Estimate the array sizes of each structure
- Write a bottom-up segment tree that is correct for non-commutative operations
- Convert between 0-based, 1-based and half-open indices without off-by-one bugs
Prerequisites
Most bugs in range-query code are not in the algorithm but at the edges: an index that is off by one, a range that is empty, a tree that is one cell too small. This lesson collects the decisions and the tests that prevent them.
Fenwick vs segment tree
Use a Fenwick tree when the operation has an inverse (sum, XOR, counting) and you only need point updates with prefix or range queries. It is shorter, faster in practice, and uses n + 1 cells. Use a segment tree when the operation has no inverse (minimum, maximum, GCD), when you need range updates with lazy propagation, or when the node must store a structure (maximum subarray, sorted lists).
For range add with range sum, a Fenwick pair works and is simple; for range assign, or add with minimum, you need a lazy segment tree. When in doubt, ask two questions: can the answer for a range be computed from the answer for the prefixes (then Fenwick), and does an update change the whole range at once (then lazy)?
| Need | Fenwick tree | Segment tree |
|---|---|---|
| Point update + prefix/range sum | yes, simplest | yes |
| Range min / max / GCD | no (no inverse) | yes |
| Range add + range sum | yes (two trees) | yes (lazy) |
| Range assign | no | yes (lazy) |
| k-th element by descent | yes | yes |
| Node holds a structure (tuple, list) | no | yes |
| Memory | n + 1 cells | 2n to 4n cells |
| Code length | about 10 lines | about 40 lines |
Fenwick tree (binary indexed tree): build, update, prefix and range sums
Interactive · edit the inputa[i], i = 1..n
bit[i] = sum of a over the range shown under it
The Fenwick tree for a = [3, 2, -1, 6, 5, 4, -3, 3]. bit[i] holds the sum of the lowbit(i) values ending at position i (shown under each cell). Positions are 1-based: index 5 of the input is i = 6.
1int[] bit = new int[n + 1]; // 1-based: bit[i] covers the lowbit(i) values ending at i2void update(int i, int delta) { // a[i] += delta, i is 1-based3for (; i <= n; i += i & -i) bit[i] += delta; // climb: add the lowest set bit4}5int prefix(int i) { // sum of a[1..i]6int s = 0;7for (; i > 0; i -= i & -i) s += bit[i]; // descend: strip the lowest set bit8return s;9}10int range(int l, int r) { return prefix(r) - prefix(l - 1); }11void buildLinear(int[] a) { // O(n) build12for (int i = 1; i <= n; i++) {13bit[i] += a[i];14int j = i + (i & -i); // the next cell that covers i15if (j <= n) bit[j] += bit[i];16}17}
Live state
n
8
Execution trace (dry run table)Show 4 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | n=8 | The Fenwick tree for a = [3, 2, -1, 6, 5, 4, -3, 3]. bit[i] holds the sum of the lowbit(i) values ending at position i (shown under each cell). Positions are 1-based: index 5 of the input is i = 6. |
| 2 | L7 | i=6 binary=110 i & -i=2 sum=9 | prefix(6): add bit[6] = 9 (covers 5..6). Next i = 6 - 2 = 4. |
| 3 | L7 | i=4 binary=100 i & -i=4 sum=19 | prefix(6): add bit[4] = 10 (covers 1..4). Next i = 4 - 4 = 0: done. |
| 4 | L8 | return=19 cells touched=2 | prefix(6) = 19: the sum of a[1..6] from 2 cells, because the ranges of the visited cells tile 1..6 exactly. |
Space costs
A Fenwick tree needs n + 1 cells. A recursive segment tree has 2n - 1 nodes but, stored by heap index, needs an array big enough for the largest index in use. Sizing it as 4n is always safe. A tighter safe size is twice the next power of two above n, which is at most 4n - 2 and often much smaller. A bottom-up segment tree needs exactly 2n cells, because leaves go at n..2n-1.
Lazy trees need extra arrays for the tags (one per tag field), and merge-sort trees need O(n log n) cells. Persistent trees add O(log n) nodes per update and dynamic trees O(log size) nodes per touched position. When memory is tight, use the smallest layout that supports your operations: 2n iterative, then 2 × next power of two recursive, then 4n.
| Structure | Cells for n values | Notes |
|---|---|---|
| Fenwick tree | n + 1 | index 0 unused |
| Iterative segment tree | 2n | leaves at n..2n-1 |
| Recursive, tight | 2 × next power of two ≥ n | at most 4n - 2 |
| Recursive, safe | 4n | the usual choice |
| Lazy segment tree | recursive size × (1 + tag fields) | sum plus the tags |
| Merge-sort tree | n log n values | sorted list per node |
1static int nextPowerOfTwo(int n) { return n <= 1 ? 1 : Integer.highestOneBit(n - 1) << 1; }2static int recursiveTreeSize(int n) { return 2 * nextPowerOfTwo(n); } // enough for every heap index of the recursive split3static int iterativeTreeSize(int n) { return 2 * n; }4static int fenwickSize(int n) { return n + 1; }
Iterative segment tree
The **bottom-up** segment tree stores the leaves at positions n..2n-1 and every inner node i as the combination of 2i and 2i + 1. A point update sets the leaf and walks up by halving the index. A range query on the half-open interval [l, r) moves both ends up one level at a time: if l is a right child, its node is part of the answer and l moves right; if r is a right child, the node to its left is part of the answer. There is no recursion and the array has exactly 2n cells.
For a non-commutative operation keep two accumulators: the nodes taken at the left end go to the left result in increasing position order, and the nodes taken at the right end are prepended to the right result. The final answer is left combined with right. Compare with the animation's "Iterative sum" preset: the query [1, 4] on 2 5 1 4 9 3 takes 3 nodes and returns 19.
1class IterativeSegmentTree {2private final int n;3private final long[] t;4private final LongBinaryOperator op;5private final long identity;6IterativeSegmentTree(int[] a, LongBinaryOperator op, long identity) {7this.n = a.length; this.op = op; this.identity = identity;8t = new long[2 * n];9for (int i = 0; i < n; i++) t[n + i] = a[i];10for (int i = n - 1; i >= 1; i--) t[i] = op.applyAsLong(t[2 * i], t[2 * i + 1]);11}12void set(int i, long v) {13for (t[i += n] = v; i > 1; i >>= 1) t[i >> 1] = op.applyAsLong(t[i & ~1], t[i | 1]); // left child first14}15long query(int l, int r) { // half-open [l, r); correct for non-commutative operations16long left = identity, right = identity;17for (l += n, r += n; l < r; l >>= 1, r >>= 1) {18if ((l & 1) == 1) left = op.applyAsLong(left, t[l++]); // append on the left side19if ((r & 1) == 1) right = op.applyAsLong(t[--r], right); // prepend on the right side20}21return op.applyAsLong(left, right);22}23}
Segment tree: build, range query, point update, iterative version
Interactive · edit the inputsegment tree (each node shows its range under the value) · 11 nodes
[0, 5]
[0, 2]
[0, 1]
[0, 0]
[1, 1]
[2, 2]
[3, 5]
[3, 4]
[3, 3]
[4, 4]
[5, 5]
a[]
Build a sum tree over 6 values: each node stores the sum of its range; a leaf stores one element. ? marks nodes not built yet.
1void build(int node, int lo, int hi) { // recursive segment tree2if (lo == hi) { tree[node] = a[lo]; return; } // leaf3int mid = lo + (hi - lo) / 2;4build(2 * node, lo, mid);5build(2 * node + 1, mid + 1, hi);6tree[node] = tree[2 * node] + tree[2 * node + 1]; // combine7}8int query(int node, int lo, int hi, int l, int r) {9if (r < lo || hi < l) return 0; // disjoint: the neutral element10if (l <= lo && hi <= r) return tree[node]; // fully inside: use the stored value11int mid = lo + (hi - lo) / 2;12return query(2 * node, lo, mid, l, r) + query(2 * node + 1, mid + 1, hi, l, r);13}14void update(int node, int lo, int hi, int i, int v) {15if (lo == hi) { tree[node] = v; return; }16int mid = lo + (hi - lo) / 2;17if (i <= mid) update(2 * node, lo, mid, i, v); else update(2 * node + 1, mid + 1, hi, i, v);18tree[node] = tree[2 * node] + tree[2 * node + 1]; // recompute on the way back19}20int queryIterative(int l, int r) { // bottom-up, half-open [l, r)21int res = 0;22for (l += n, r += n; l < r; l >>= 1, r >>= 1) {23if ((l & 1) == 1) res += tree[l++]; // l is a right child: take it and move right24if ((r & 1) == 1) res += tree[--r]; // r is exclusive: its left neighbour is a right child25}26return res;27}
Live state
n
6
Execution trace (dry run table)Show 27 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | n=6 | Build a sum tree over 6 values: each node stores the sum of its range; a leaf stores one element. ? marks nodes not built yet. |
| 2 | L2 | node=8 range=[0, 0] value=2 | Leaf [0]: tree[8] = a[0] = 2. |
| 3 | L2 | node=9 range=[1, 1] value=5 | Leaf [1]: tree[9] = a[1] = 5. |
| 4 | L6 | node=4 range=[0, 1] value=7 | Combine the children of [0, 1]: 2 + 5 = 7. |
| 5 | L2 | node=5 range=[2, 2] value=1 | Leaf [2]: tree[5] = a[2] = 1. |
| 6 | L6 | node=2 range=[0, 2] value=8 | Combine the children of [0, 2]: 7 + 1 = 8. |
| 7 | L2 | node=12 range=[3, 3] value=4 | Leaf [3]: tree[12] = a[3] = 4. |
| 8 | L2 | node=13 range=[4, 4] value=9 | Leaf [4]: tree[13] = a[4] = 9. |
| 9 | L6 | node=6 range=[3, 4] value=13 | Combine the children of [3, 4]: 4 + 9 = 13. |
| 10 | L2 | node=7 range=[5, 5] value=3 | Leaf [5]: tree[7] = a[5] = 3. |
| 11 | L6 | node=3 range=[3, 5] value=16 | Combine the children of [3, 5]: 13 + 3 = 16. |
| 12 | L6 | node=1 range=[0, 5] value=24 | Combine the children of [0, 5]: 8 + 16 = 24. |
| 13 | L8 | l=1 r=4 | Query sum(a[1..4]). Each node is outside (return 0, the neutral element), fully inside (use its stored value) or partly inside (ask both children and combine). |
| 14 | L11 | node=1 range=[0, 5] kind=partial | [0, 5] overlaps [1, 4] only partly: recurse into both children. |
| 15 | L11 | node=2 range=[0, 2] kind=partial | [0, 2] overlaps [1, 4] only partly: recurse into both children. |
| 16 | L11 | node=4 range=[0, 1] kind=partial | [0, 1] overlaps [1, 4] only partly: recurse into both children. |
| 17 | L9 | node=8 range=[0, 0] kind=disjoint | [0, 0] lies outside [1, 4]: return 0. |
| 18 | L10 | node=9 range=[1, 1] kind=inside | [1, 1] lies inside [1, 4]: use the stored value 5; no need to look at its children. |
| 19 | L12 | node=4 result=5 | Combine the two answers below [0, 1]: 5. |
| 20 | L10 | node=5 range=[2, 2] kind=inside | [2, 2] lies inside [1, 4]: use the stored value 1; no need to look at its children. |
| 21 | L12 | node=2 result=6 | Combine the two answers below [0, 2]: 6. |
| 22 | L11 | node=3 range=[3, 5] kind=partial | [3, 5] overlaps [1, 4] only partly: recurse into both children. |
| 23 | L10 | node=6 range=[3, 4] kind=inside | [3, 4] lies inside [1, 4]: use the stored value 13; no need to look at its children. |
| 24 | L9 | node=7 range=[5, 5] kind=disjoint | [5, 5] lies outside [1, 4]: return 0. |
| 25 | L12 | node=3 result=13 | Combine the two answers below [3, 5]: 13. |
| 26 | L12 | node=1 result=19 | Combine the two answers below [0, 5]: 19. |
| 27 | L12 | return=19 nodes visited=9 | sum(a[1..4]) = 19, after visiting 9 nodes. At most four nodes per level are visited, so a query is O(log n). |
Index conversions
Three conventions meet in this chapter: Fenwick trees are 1-based, most Java arrays and segment trees are 0-based, and the iterative segment tree and String.substring use half-open ranges [l, r). Convert once, at the boundary, and keep everything inside the structure in one convention. A wrapper class that exposes 0-based inclusive ranges, as most problem statements do, removes the whole class of off-by-one bugs from the calling code.
The usual conversions: 0-based index i is Fenwick position i + 1; an inclusive [l, r] is the half-open [l, r + 1); and sum(l..r) = prefix(r + 1) - prefix(l) when prefix(e) means the sum of the first e elements. With these, l = 0 needs no special case, because prefix(0) is an empty sum.
| Convention | Range of 3 elements starting at index 2 | Fenwick position of the first |
|---|---|---|
| 0-based, inclusive | [2, 4] | 3 |
| 0-based, half-open | [2, 5) | 3 |
| 1-based, inclusive | [3, 5] | 3 |
1class ZeroBasedFenwick { // 0-based, inclusive ranges, like most problem statements2private final long[] tree;3ZeroBasedFenwick(int n) { tree = new long[n + 1]; }4void add(int index, long delta) {5for (int i = index + 1; i < tree.length; i += i & -i) tree[i] += delta; // 0-based -> 1-based6}7long prefix(int endExclusive) { // sum of [0, endExclusive)8long s = 0;9for (int i = endExclusive; i > 0; i -= i & -i) s += tree[i];10return s;11}12long sum(int l, int r) { return prefix(r + 1) - prefix(l); } // inclusive [l, r]13}
Testing range boundaries
Range structures break at the edges, so test those on purpose, against a brute-force array. The important cases: a single element (l == r), the first element, the last element, the whole array, n = 1, n a power of two and n one more or one less than a power of two, updates at the first and last index, and an update that does not change the value. Then add randomised operations: random updates and random ranges, compared with a plain loop after every step.
The harness below drives any structure that implements a tiny interface, so the same test checks a Fenwick wrapper, a segment tree or a lazy tree. Failures print the first operation that disagrees, which is usually enough to see the bug. Run it for several sizes, not only one: many bugs appear only when n is odd or a power of two.
1interface RangeSum {2void set(int i, long v); // a[i] = v, 0-based3long sum(int l, int r); // inclusive, 0-based4}5static String stress(RangeSum fast, int n, int rounds, long seed) {6Random rnd = new Random(seed);7long[] a = new long[n];8for (int round = 0; round < rounds; round++) {9int i = rnd.nextInt(n);10long v = rnd.nextInt(2001) - 1000;11a[i] = v;12fast.set(i, v);13int l = rnd.nextInt(n), r = rnd.nextInt(n);14if (l > r) { int t = l; l = r; r = t; }15int[][] ranges = {{l, r}, {0, n - 1}, {0, 0}, {n - 1, n - 1}, {i, i}}; // random, whole, first, last, changed16for (int[] q : ranges) {17long want = 0;18for (int k = q[0]; k <= q[1]; k++) want += a[k];19if (fast.sum(q[0], q[1]) != want)20return "sum(" + q[0] + ", " + q[1] + ") after set(" + i + ", " + v + "): expected " + want;21}22}23return "ok";24}
Practice ladder
Now try it yourself
Easy
- Range Sum Query - Mutable
Medium
- Range Sum Query 2D - Mutable
- Count of Smaller Numbers After Self
Advanced
- Count of Range Sum
- Longest Increasing Subsequence II
- Falling Squares
Key takeaways
- Fenwick: invertible operation, point update, prefix/range query. Segment tree: everything else.
- Sizes: Fenwick n + 1, iterative segment tree 2n, recursive segment tree 4n (or 2 × next power of two).
- Convert indices once at the boundary, and test single, first, last and whole-array ranges against a brute force.