Segment tree extensions
Heavier segment-tree designs: merge-sort trees for counting in a range, segment tree beats for range minimum assignments, 2D trees and implicit trees over huge ranges.
Learning goals
- Count values at most x in a range with a merge-sort tree
- Explain how segment tree beats bounds the work of range chmin
- Build a 2D segment tree for point updates and rectangle sums
- Use an implicit tree for lazy updates on a huge index range
Prerequisites
Some range questions need more than a number per node: a sorted list, a second maximum, or a tree inside a tree. These designs cost more memory or time per query, and knowing the price tells you when they are worth it.
Merge-sort tree
A **merge-sort tree** stores, in every node, the sorted list of the values in its range. It is built exactly like merge sort, merging the two children's lists, so building takes O(n log n) time and O(n log n) space. A fully covered node answers 'how many values are at most x' by binary search in its list.
A query decomposes [l, r] into O(log n) nodes and does one binary search in each, so it costs O(log² n). The tree does not support cheap updates (a change would have to fix a list at each level). In the preset "Merge-sort tree count" (5 2 8 2 9 4, positions 1..4, x = 4) the nodes show their sorted lists and the answer is 2 (the two 2s).
1class MergeSortTree {2private final int n;3private final int[][] sorted;4MergeSortTree(int[] a) {5n = a.length;6sorted = new int[4 * n][];7build(a, 1, 0, n - 1);8}9private void build(int[] a, int node, int lo, int hi) {10if (lo == hi) { sorted[node] = new int[]{a[lo]}; return; }11int mid = lo + (hi - lo) / 2;12build(a, 2 * node, lo, mid);13build(a, 2 * node + 1, mid + 1, hi);14sorted[node] = merge(sorted[2 * node], sorted[2 * node + 1]);15}16private static int[] merge(int[] x, int[] y) {17int[] out = new int[x.length + y.length];18for (int i = 0, j = 0, k = 0; k < out.length; k++)19out[k] = j >= y.length || (i < x.length && x[i] <= y[j]) ? x[i++] : y[j++];20return out;21}22int countAtMost(int l, int r, int x) { return count(1, 0, n - 1, l, r, x); }23private int count(int node, int lo, int hi, int l, int r, int x) {24if (r < lo || hi < l) return 0;25if (l <= lo && hi <= r) return upperBound(sorted[node], x); // binary search in the node's list26int mid = lo + (hi - lo) / 2;27return count(2 * node, lo, mid, l, r, x) + count(2 * node + 1, mid + 1, hi, l, r, x);28}29private static int upperBound(int[] s, int x) { // number of elements <= x30int lo = 0, hi = s.length;31while (lo < hi) {32int mid = lo + (hi - lo) / 2;33if (s[mid] <= x) lo = mid + 1; else hi = mid;34}35return lo;36}37}
Segment tree nodes beyond sums: min+index, GCD, max subarray, k-th one, merge-sort tree
Interactive · edit the inputsegment tree; each node shows min@index · 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[]
Each node stores the minimum of its range and WHERE it is (value@index). Combining two nodes keeps the smaller value; on a tie the left index wins.
1record Min(int val, int idx) {} // minimum with its index2static Min combine(Min x, Min y) { return y.val < x.val ? y : x; } // ties keep the left index3static int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } // combine for GCD, neutral element 04record Seg(long sum, long pref, long suf, long best) {} // maximum subarray node5static Seg combine(Seg x, Seg y) {6return new Seg(x.sum + y.sum,7Math.max(x.pref, x.sum + y.pref), // best prefix may run into the right part8Math.max(y.suf, y.sum + x.suf),9Math.max(Math.max(x.best, y.best), x.suf + y.pref)); // best crossing the middle10}11int kthOne(int node, int lo, int hi, int k) { // k-th 1, counting from 112if (lo == hi) return lo;13int mid = lo + (hi - lo) / 2;14if (cnt[2 * node] >= k) return kthOne(2 * node, lo, mid, k); // it is in the left half15return kthOne(2 * node + 1, mid + 1, hi, k - cnt[2 * node]); // skip the left ones16}17int countAtMost(int node, int lo, int hi, int l, int r, int x) { // merge-sort tree18if (r < lo || hi < l) return 0;19if (l <= lo && hi <= r) return upperBound(sorted[node], x); // binary search in the node's sorted list20int mid = lo + (hi - lo) / 2;21return countAtMost(2 * node, lo, mid, l, r, x) + countAtMost(2 * node + 1, mid + 1, hi, l, r, x);22}
Live state
n
6
Execution trace (dry run table)Show 15 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | n=6 | Each node stores the minimum of its range and WHERE it is (value@index). Combining two nodes keeps the smaller value; on a tie the left index wins. |
| 2 | L2 | node=1 range=[0, 5] kind=partial | [0, 5] is partly inside: recurse into both children. |
| 3 | L2 | node=2 range=[0, 2] kind=partial | [0, 2] is partly inside: recurse into both children. |
| 4 | L2 | node=4 range=[0, 1] kind=partial | [0, 1] is partly inside: recurse into both children. |
| 5 | L2 | node=8 range=[0, 0] kind=disjoint | [0, 0] is outside [1, 4]: neutral element. |
| 6 | L2 | node=9 range=[1, 1] kind=inside | [1, 1] is inside: use its stored value 2@1. |
| 7 | L2 | node=4 result=2@1 | Combine the answers for [0, 1]: 2@1. |
| 8 | L2 | node=5 range=[2, 2] kind=inside | [2, 2] is inside: use its stored value 8@2. |
| 9 | L2 | node=2 result=2@1 | Combine the answers for [0, 2]: 2@1. |
| 10 | L2 | node=3 range=[3, 5] kind=partial | [3, 5] is partly inside: recurse into both children. |
| 11 | L2 | node=6 range=[3, 4] kind=inside | [3, 4] is inside: use its stored value 2@3. |
| 12 | L2 | node=7 range=[5, 5] kind=disjoint | [5, 5] is outside [1, 4]: neutral element. |
| 13 | L2 | node=3 result=2@3 | Combine the answers for [3, 5]: 2@3. |
| 14 | L2 | node=1 result=2@1 | Combine the answers for [0, 5]: 2@1. |
| 15 | L2 | return=2@1 nodes visited=9 | Minimum over a[1..4] = 2 at index 1, after 9 node visits. |
Segment tree beats
**Segment tree beats** handles updates like a[i] = min(a[i], x) over a range (range chmin) together with range sums. A node stores its maximum, the number of times the maximum occurs, the strict second maximum and the sum. If x is larger than the second maximum but smaller than the maximum, the update changes only the maximum elements, and the node's sum can be fixed in O(1) as sum -= (max - x) × count, with the new maximum left as a tag for the children.
If x is not larger than the second maximum, the node cannot be fixed directly and the update recurses. The surprising result, from the literature, is that the total number of such extra recursions stays small: range chmin with range sum runs in O(log n) amortised time per operation (O(log² n) when combined with range add). The tests below check the code against brute force, but this course does not reproduce the amortised proof.
1class ChminTree {2private final int n;3private final long[] sum, max1, max2, cnt; // max2 is the strict second maximum (Long.MIN_VALUE if none)4ChminTree(int[] a) {5n = a.length;6sum = new long[4 * n]; max1 = new long[4 * n]; max2 = new long[4 * n]; cnt = new long[4 * n];7build(a, 1, 0, n - 1);8}9private void build(int[] a, int node, int lo, int hi) {10if (lo == hi) { sum[node] = max1[node] = a[lo]; max2[node] = Long.MIN_VALUE; cnt[node] = 1; return; }11int mid = lo + (hi - lo) / 2;12build(a, 2 * node, lo, mid);13build(a, 2 * node + 1, mid + 1, hi);14pull(node);15}16private void pull(int node) {17int L = 2 * node, R = L + 1;18sum[node] = sum[L] + sum[R];19if (max1[L] > max1[R]) { max1[node] = max1[L]; cnt[node] = cnt[L]; max2[node] = Math.max(max2[L], max1[R]); }20else if (max1[L] < max1[R]) { max1[node] = max1[R]; cnt[node] = cnt[R]; max2[node] = Math.max(max1[L], max2[R]); }21else { max1[node] = max1[L]; cnt[node] = cnt[L] + cnt[R]; max2[node] = Math.max(max2[L], max2[R]); }22}23private void applyMin(int node, long x) { // lower the maximum of this node to x (needs max2 < x)24if (x >= max1[node]) return;25sum[node] -= (max1[node] - x) * cnt[node];26max1[node] = x;27}28private void push(int node) { applyMin(2 * node, max1[node]); applyMin(2 * node + 1, max1[node]); }29void chmin(int l, int r, long x) { chmin(1, 0, n - 1, l, r, x); }30private void chmin(int node, int lo, int hi, int l, int r, long x) {31if (r < lo || hi < l || max1[node] <= x) return; // nothing to cut32if (l <= lo && hi <= r && max2[node] < x) { applyMin(node, x); return; } // only the maximum values change33push(node);34int mid = lo + (hi - lo) / 2;35chmin(2 * node, lo, mid, l, r, x);36chmin(2 * node + 1, mid + 1, hi, l, r, x);37pull(node);38}39long sum(int l, int r) { return sum(1, 0, n - 1, l, r); }40private long sum(int node, int lo, int hi, int l, int r) {41if (r < lo || hi < l) return 0;42if (l <= lo && hi <= r) return sum[node];43push(node);44int mid = lo + (hi - lo) / 2;45return sum(2 * node, lo, mid, l, r) + sum(2 * node + 1, mid + 1, hi, l, r);46}47long max(int l, int r) { return max(1, 0, n - 1, l, r); }48private long max(int node, int lo, int hi, int l, int r) {49if (r < lo || hi < l) return Long.MIN_VALUE;50if (l <= lo && hi <= r) return max1[node];51push(node);52int mid = lo + (hi - lo) / 2;53return Math.max(max(2 * node, lo, mid, l, r), max(2 * node + 1, mid + 1, hi, l, r));54}55}
2D segment tree
A **2D segment tree** is a segment tree over the rows in which every node holds a segment tree over the columns. The inner tree of an outer node summarises the rows of that node, column by column. A point update walks down the outer tree; at each outer node it updates the inner tree (for a leaf row it sets the value, for an inner row it recombines the two child rows' inner values). A rectangle query decomposes the row range into O(log R) outer nodes and queries each inner tree on the column range.
Both operations cost O(log R × log C). The price is memory: a naive layout needs 16 × R × C cells (4R by 4C). A 2D Fenwick tree is simpler and smaller for sums, but a 2D segment tree supports operations without an inverse, such as 2D minimum. Updating a whole row range or column range requires lazy propagation in both dimensions, which is rarely worth the complexity.
1class SegmentTree2D {2private final int rows, cols;3private final long[][] t;4SegmentTree2D(int rows, int cols) {5this.rows = rows; this.cols = cols;6t = new long[4 * rows][4 * cols];7}8void set(int x, int y, long v) { setX(1, 0, rows - 1, x, y, v); }9private void setX(int nx, int lx, int rx, int x, int y, long v) {10if (lx < rx) {11int mx = lx + (rx - lx) / 2;12if (x <= mx) setX(2 * nx, lx, mx, x, y, v); else setX(2 * nx + 1, mx + 1, rx, x, y, v);13}14setY(nx, lx, rx, 1, 0, cols - 1, y, v); // after the rows below are done15}16private void setY(int nx, int lx, int rx, int ny, int ly, int ry, int y, long v) {17if (ly == ry) {18if (lx == rx) t[nx][ny] = v; // a leaf row: the value itself19else t[nx][ny] = t[2 * nx][ny] + t[2 * nx + 1][ny]; // an inner row: combine the child rows20return;21}22int my = ly + (ry - ly) / 2;23if (y <= my) setY(nx, lx, rx, 2 * ny, ly, my, y, v); else setY(nx, lx, rx, 2 * ny + 1, my + 1, ry, y, v);24t[nx][ny] = t[nx][2 * ny] + t[nx][2 * ny + 1];25}26long sum(int x1, int y1, int x2, int y2) { return sumX(1, 0, rows - 1, x1, x2, y1, y2); }27private long sumX(int nx, int lx, int rx, int x1, int x2, int y1, int y2) {28if (x2 < lx || rx < x1) return 0;29if (x1 <= lx && rx <= x2) return sumY(nx, 1, 0, cols - 1, y1, y2);30int mx = lx + (rx - lx) / 2;31return sumX(2 * nx, lx, mx, x1, x2, y1, y2) + sumX(2 * nx + 1, mx + 1, rx, x1, x2, y1, y2);32}33private long sumY(int nx, int ny, int ly, int ry, int y1, int y2) {34if (y2 < ly || ry < y1) return 0;35if (y1 <= ly && ry <= y2) return t[nx][ny];36int my = ly + (ry - ly) / 2;37return sumY(nx, 2 * ny, ly, my, y1, y2) + sumY(nx, 2 * ny + 1, my + 1, ry, y1, y2);38}39}
Implicit segment tree
An **implicit** segment tree does not store its nodes in an array: nodes are objects that exist only when needed, and the range they cover is implied by the path from the root. It is the dynamic sparse tree of the previous lesson combined with lazy propagation, so it can handle range adds and range sums over an index range of 10^9 using memory proportional to the operations performed.
Because a range update may stop at a node whose children do not exist yet, the push-down step creates the missing children before handing them the tag. The same push is needed by queries. Each operation creates O(log size) nodes at most, so memory is O(operations × log size). The animation of the previous lesson shows the node creation; here the lazy tags are added on top.
1class ImplicitTree {2private static class Node { long sum, add; Node left, right; }3private final int size; // indices 0..size-14private final Node root = new Node();5int nodes = 1;6ImplicitTree(int size) { this.size = size; }7private static long len(int lo, int hi) { return (long) hi - lo + 1; }8private void apply(Node n, int lo, int hi, long v) { n.sum += v * len(lo, hi); n.add += v; }9private void push(Node n, int lo, int hi) { // create the children if needed, then hand down the tag10int mid = lo + (hi - lo) / 2;11if (n.left == null) { n.left = new Node(); nodes++; }12if (n.right == null) { n.right = new Node(); nodes++; }13if (n.add != 0) {14apply(n.left, lo, mid, n.add);15apply(n.right, mid + 1, hi, n.add);16n.add = 0;17}18}19void rangeAdd(int l, int r, long v) { rangeAdd(root, 0, size - 1, l, r, v); }20private void rangeAdd(Node n, int lo, int hi, int l, int r, long v) {21if (r < lo || hi < l) return;22if (l <= lo && hi <= r) { apply(n, lo, hi, v); return; }23push(n, lo, hi);24int mid = lo + (hi - lo) / 2;25rangeAdd(n.left, lo, mid, l, r, v);26rangeAdd(n.right, mid + 1, hi, l, r, v);27n.sum = n.left.sum + n.right.sum;28}29long sum(int l, int r) { return sum(root, 0, size - 1, l, r); }30private long sum(Node n, int lo, int hi, int l, int r) {31if (r < lo || hi < l) return 0;32if (l <= lo && hi <= r) return n.sum;33push(n, lo, hi);34int mid = lo + (hi - lo) / 2;35return sum(n.left, lo, mid, l, r) + sum(n.right, mid + 1, hi, l, r);36}37}
Persistent and dynamic sparse segment trees
Interactive · edit the inputversion 0 · 11 nodes
[0,5]#11
[0,2]#5
[0,1]#3
[0,0]#1
[1,1]#2
[2,2]#4
[3,5]#10
[3,4]#8
[3,3]#6
[4,4]#7
[5,5]#9
versions (root sums)
Version 0 is an ordinary segment tree of 11 nodes. A persistent tree never changes a node: an update copies just the nodes on the root-to-leaf path and shares every other subtree with the old version.
1record Node(Node left, Node right, long sum) {}2Node build(int[] a, int lo, int hi) {3if (lo == hi) return new Node(null, null, a[lo]);4int mid = lo + (hi - lo) / 2;5Node l = build(a, lo, mid), r = build(a, mid + 1, hi);6return new Node(l, r, l.sum + r.sum);7}8Node update(Node n, int lo, int hi, int i, long v) { // returns the NEW root; n stays untouched9if (lo == hi) return new Node(null, null, v);10int mid = lo + (hi - lo) / 2;11if (i <= mid) { Node l = update(n.left, lo, mid, i, v); return new Node(l, n.right, l.sum + n.right.sum); }12Node r = update(n.right, mid + 1, hi, i, v);13return new Node(n.left, r, n.left.sum + r.sum); // the other child is shared with the old version14}15Node add(Node n, int lo, int hi, int i, long v) { // dynamic sparse tree: a missing child means all zeros16if (n == null) n = new Node();17n.sum += v;18if (lo == hi) return n;19int mid = lo + (hi - lo) / 2;20if (i <= mid) n.left = add(n.left, lo, mid, i, v); else n.right = add(n.right, mid + 1, hi, i, v);21return n;22}23long sum(Node n, int lo, int hi, int l, int r) {24if (n == null || r < lo || hi < l) return 0; // missing node = zeros25if (l <= lo && hi <= r) return n.sum;26int mid = lo + (hi - lo) / 2;27return sum(n.left, lo, mid, l, r) + sum(n.right, mid + 1, hi, l, r);28}
Live state
nodes
11
Execution trace (dry run table)Show 6 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | nodes=11 | Version 0 is an ordinary segment tree of 11 nodes. A persistent tree never changes a node: an update copies just the nodes on the root-to-leaf path and shares every other subtree with the old version. |
| 2 | L8 | op=u2=9 copied=3 shared=8 total nodes=14 | u2=9: set a[2] = 9. 3 nodes copied along the path (shown as new); the other 8 nodes of this version are shared with version 0, which still answers its own queries unchanged. |
| 3 | L8 | op=u5=0 copied=3 shared=8 total nodes=17 | u5=0: set a[5] = 0. 3 nodes copied along the path (shown as new); the other 8 nodes of this version are shared with version 1, which still answers its own queries unchanged. |
| 4 | L25 | op=q1:0:5 return=24 | q1:0:5: in version 1, the sum of a[0..5] is 24. The old version is intact, so history can be queried. |
| 5 | L25 | op=q0:0:5 return=22 | q0:0:5: in version 0, the sum of a[0..5] is 22. The old version is intact, so history can be queried. |
| 6 | L13 | return=17 versions=3 | 3 versions for 17 nodes in total. Storing a full copy per version would take 33; path copying adds only O(log n) nodes per update. |
Practice ladder
Now try it yourself
Easy
- Range Sum Query - Mutable
Medium
- Count of Smaller Numbers After Self
- Range Sum Query 2D - Mutable
Advanced
- Rectangle Area II
- Count Integers in Intervals
- Booking Concert Tickets in Groups
Key takeaways
- A merge-sort tree stores sorted lists: O(n log n) memory, O(log² n) range counting, no cheap updates.
- Beats keep the maximum, its count and the strict second maximum so that range chmin can usually stop early.
- A 2D tree is a tree of trees: O(log R × log C) per operation; implicit trees create nodes only where operations touch them.