A6 · Part 3 · Advanced
Segment trees & Fenwick trees
Prefix sums die the moment the array can change — these two structures buy back updates for a log factor.
17On this page
- Where the prefix-sum array falls over
- The segment tree: one node per range
- Build, query, update
- Range min, max, gcd — only the merge changes
- Struct nodes: when a node stores more than one number
- The iterative bottom-up segment tree
- Descending the tree: the leftmost index at or above a threshold
- Lazy propagation: range updates without touching every leaf
- My Calendar III: a dynamic tree and tags that never move
- Fenwick tree: the same job in a quarter of the code
- Why Fenwick usually wins in practice
- Count of smaller numbers after self — the classic BIT problem
- Fenwick binary descent: the k-th element in O(log n)
- Range add and range sum with two Fenwick trees
- Offline sort plus Fenwick: reverse pairs and range-sum counts
- Which one to reach for
- Recognizing it in an unseen problem
Where the prefix-sum array falls over
A prefix-sum array is unbeatable on a static array: O(n) to build, O(1) per range query. The moment an element can change, it collapses — updating index i invalidates every prefix from i onward, so each update is O(n). Skip the precompute and you flip the problem: O(1) updates, O(n) queries. Either way an interleaved workload of q operations costs O(n·q), and with n and q around 105 that's 1010 operations.
| Structure | Build | Range query | Point update | 10⁵ mixed ops |
|---|---|---|---|---|
| Raw array, loop each query | O(1) | O(n) | O(1) | ~10¹⁰ — too slow |
| Prefix-sum array | O(n) | O(1) | O(n) | ~10¹⁰ — too slow |
| Fenwick / segment tree | O(n) | O(log n) | O(log n) | ~1.7 × 10⁶ — fine |
The move both structures make is the same one binary search makes: refuse to store either extreme. Don't store every individual element (queries too slow) and don't store every prefix (updates too slow) — store O(n) carefully chosen partial aggregates such that any range is assembled from O(log n) of them and any element belongs to only O(log n) of them.
The segment tree: one node per range
Build a binary tree over index ranges. The root covers [0, n−1], each internal node splits its range in half, and leaves are single elements. Every node caches the aggregate of its range. A query for [l, r] descends from the root and stops the instant a node's range is fully contained — so an arbitrary range is covered by at most 2 nodes per level, i.e. O(log n) nodes total.
Build, query, update
Store the tree in a flat array with the heap layout: node 1 is the root, node
k's children are 2k and 2k+1. Every recursive call carries the range it owns
(lo, hi) so no range metadata is stored per node.
class SegmentTree {
constructor(nums) {
this.n = nums.length;
this.t = new Array(4 * this.n).fill(0); // 4n is the safe size — see the warning below
if (this.n > 0) this._build(nums, 1, 0, this.n - 1);
}
_build(nums, node, lo, hi) {
if (lo === hi) { this.t[node] = nums[lo]; return; } // leaf
const mid = (lo + hi) >> 1;
this._build(nums, 2 * node, lo, mid);
this._build(nums, 2 * node + 1, mid + 1, hi);
this.t[node] = this.t[2 * node] + this.t[2 * node + 1]; // merge children upward
}
// sum of nums[l..r] inclusive — O(log n)
query(l, r, node = 1, lo = 0, hi = this.n - 1) {
if (r < lo || hi < l) return 0; // no overlap — return the IDENTITY, not 0 blindly
if (l <= lo && hi <= r) return this.t[node]; // total overlap — cached answer, stop descending
const mid = (lo + hi) >> 1; // partial overlap — split and combine
return this.query(l, r, 2 * node, lo, mid)
+ this.query(l, r, 2 * node + 1, mid + 1, hi);
}
// set nums[i] = value — O(log n), touches exactly one root-to-leaf path
update(i, value, node = 1, lo = 0, hi = this.n - 1) {
if (lo === hi) { this.t[node] = value; return; }
const mid = (lo + hi) >> 1;
if (i <= mid) this.update(i, value, 2 * node, lo, mid);
else this.update(i, value, 2 * node + 1, mid + 1, hi);
this.t[node] = this.t[2 * node] + this.t[2 * node + 1]; // re-merge on the way back up
}
}
Dry run: query(2,5), then update(4,10), then query(2,5) again — on [3,1,4,1,5,9,2,6]
| Call | node [lo,hi] | Overlap with [2,5] | Returns |
|---|---|---|---|
| query(2,5) | 1 [0,7] | partial — split | node 2's answer + node 3's answer |
| 2 [0,3] | partial — split | node 4's answer + node 5's answer | |
| 4 [0,1] | none | 0 | |
| 5 [2,3] | full | t[5] = 5 | |
| 3 [4,7] | partial — split | node 6's answer + node 7's answer | |
| 6 [4,5] | full | t[6] = 14 | |
| 7 [6,7] | none | 0 | |
| query(2,5) total | 0 + 5 + 0 + 14 = 19 | ||
| update(4, 10) | leaf 12 [4,4] | — | t[12] = 10 |
| re-merge 6 [4,5] | — | t[6] = t[12] + t[13] = 10 + 9 = 19 | |
| re-merge 3 [4,7] | — | t[3] = t[6] + t[7] = 19 + 8 = 27 | |
| re-merge 1 [0,7] | — | t[1] = t[2] + t[3] = 9 + 27 = 36 | |
| query(2,5) again | same path | — | t[5]=5, t[6]=19 → 24 |
Only the nodes on the root-to-leaf chain for index 4 ever change — 12, then 6,
then 3, then 1 — which is exactly why update is O(log n) instead
of O(n). The first query(2,5) never even visits node 12; it reads
the already-cached t[6] = 14. The update then refreshed that cache to
19 on its way up, so bumping index 4 from 5 to 10 shifts the range answer from
19 to 24 without anyone re-summing the range.
⚠ Two sizing/identity traps
4n, not 2n. When n isn't a power of two the tree is unbalanced in the
heap layout and indices can reach almost 4n (for n = 2080 the largest is about 3.9n); 4n is
the standard safe over-allocation, which is easier to just use than to
compute a tighter bound. The no-overlap return must be the
operation's identity. Returning 0 is right for sum, catastrophically
wrong for min — a min tree must return
Infinity there, or every
query gets dragged to 0.
Range min, max, gcd — only the merge changes
Nothing about the traversal is sum-specific. Swap the merge function and the identity element and the same tree answers a different question. The only requirement is that the operation be associative — the tree combines sub-answers in a fixed nesting, so order of grouping must not matter.
| Query | merge(a, b) | identity (no-overlap return) | Fenwick can do it? |
|---|---|---|---|
| range sum | a + b | 0 | yes — subtraction inverts it |
| range min | Math.min(a, b) | Infinity | no — prefix min can't be un-done |
| range max | Math.max(a, b) | -Infinity | no |
| range gcd | gcd(a, b) | 0 | no |
| range XOR | a ^ b | 0 | yes — XOR is its own inverse |
| count of a value | a + b | 0 | yes |
That last column is the deep reason the two structures aren't interchangeable.
A Fenwick tree answers range queries as prefix(r) − prefix(l−1),
which needs an invertible operation. Min has no inverse — you cannot
recover min(l..r) from min(0..r) and min(0..l−1) — so range-min genuinely
requires a segment tree (or, if the array never changes, a sparse table).
Struct nodes: when a node stores more than one number
So far every node held one number and the merge was one operator. Some questions need a node to remember several facts about its range, so that two halves can be combined without re-reading either. The standard example is the maximum subarray sum (the best non-empty contiguous run) with point updates, which a Fenwick tree cannot do and a prefix-sum array cannot do once values change.
Ask what you would need to know about a left half L and a right half R to get
the answer for the joined range. The best subarray of L∪R is one of three
things: entirely inside L, entirely inside R, or crossing the middle,
in which case it is a suffix of L glued to a prefix of R. So each node stores
four numbers: sum (the whole range), prefix (the best
run starting at the left end), suffix (the best run ending at the
right end) and best (the best run anywhere). Each of the four can
itself be derived from the children's four:
| field of the merged node | how it comes from L and R |
|---|---|
sum | L.sum + R.sum |
prefix | the better of L.prefix and (L.sum + R.prefix): stop inside L, or take all of L and keep going |
suffix | the better of R.suffix and (R.sum + L.suffix) |
best | the best of L.best, R.best and (L.suffix + R.prefix) |
class MaxSubarrayTree {
constructor(nums) {
this.n = nums.length;
this.t = new Array(4 * this.n);
if (this.n > 0) this._build(nums, 1, 0, this.n - 1);
}
static leaf(v) { return { sum: v, prefix: v, suffix: v, best: v }; } // non-empty runs only
static merge(a, b) { // order matters: a is the LEFT half
return {
sum: a.sum + b.sum,
prefix: Math.max(a.prefix, a.sum + b.prefix),
suffix: Math.max(b.suffix, b.sum + a.suffix),
best: Math.max(a.best, b.best, a.suffix + b.prefix),
};
}
_build(nums, node, lo, hi) {
if (lo === hi) { this.t[node] = MaxSubarrayTree.leaf(nums[lo]); return; }
const mid = (lo + hi) >> 1;
this._build(nums, 2 * node, lo, mid);
this._build(nums, 2 * node + 1, mid + 1, hi);
this.t[node] = MaxSubarrayTree.merge(this.t[2 * node], this.t[2 * node + 1]);
}
update(i, value, node = 1, lo = 0, hi = this.n - 1) {
if (lo === hi) { this.t[node] = MaxSubarrayTree.leaf(value); return; }
const mid = (lo + hi) >> 1;
if (i <= mid) this.update(i, value, 2 * node, lo, mid);
else this.update(i, value, 2 * node + 1, mid + 1, hi);
this.t[node] = MaxSubarrayTree.merge(this.t[2 * node], this.t[2 * node + 1]);
}
// best subarray sum inside nums[l..r] — O(log n)
query(l, r, node = 1, lo = 0, hi = this.n - 1) {
if (r < lo || hi < l) return null; // no zero-like identity exists, so "nothing" is null
if (l <= lo && hi <= r) return this.t[node];
const mid = (lo + hi) >> 1;
const left = this.query(l, r, 2 * node, lo, mid);
const right = this.query(l, r, 2 * node + 1, mid + 1, hi);
if (left === null) return right;
if (right === null) return left;
return MaxSubarrayTree.merge(left, right);
}
best(l, r) { return this.query(l, r).best; }
}
Dry run: build on [1, −3, 4, 2], then set index 1 to 5
| node (range) | sum | prefix | suffix | best | how |
|---|---|---|---|---|---|
| [0,1] | −2 | 1 | −2 | 1 | merge(1, −3): prefix max(1, 1−3), suffix max(−3, −3+1), best max(1, −3, 1−3) |
| [2,3] | 6 | 6 | 6 | 6 | merge(4, 2): everything is positive, so all four fields grow |
| [0,3] root | 4 | 4 | 6 | 6 | best max(1, 6, suffix(−2) + prefix(6) = 4) is the run [4, 2] |
| update(1, 5): only the leaf, [0,1] and the root are rebuilt | |||||
| [0,1] | 6 | 6 | 6 | 6 | merge(1, 5) |
| [0,3] root | 12 | 12 | 12 | 12 | best max(6, 6, suffix 6 + prefix 6 = 12): the whole array |
The cost per query and per update is still O(log n); only the constant grows, because a node is four numbers and a merge is a handful of operations. The principle is the useful part: when a question about a range cannot be answered from one number per half, add the facts that can, until the merge closes. Longest run of equal values, the number of runs, "is this range sorted" and range-gcd-with-count are all built this way.
⚠ Struct merges are not commutative
merge(a, b) and merge(b, a) give different answers,
because prefix and suffix depend on which half is on the left. The recursive
query above is safe because it always merges the left result with the right
one. Also note there is no zero-like identity element: with empty runs not
allowed, a range that overlaps nothing has no "empty" node of ordinary numbers
that is neutral (a true one needs sum 0 and -Infinity in the other
three fields, which is easy to get wrong), so the query returns
null and the two if lines skip it. Initialising an empty node to zeros would silently allow the
empty subarray and break all-negative inputs.
The iterative bottom-up segment tree
The recursive tree needs 4n cells and pays for a function call per level. When
you can use the half-open convention, there is a much shorter version with a
flat array of exactly 2n cells: leaf i lives at index n + i, and
node k is the merge of nodes 2k and 2k+1. Building is a single backwards loop,
updating is "write the leaf, then walk to the root", and a query walks inward
from both ends of the range at once.
class BottomUpTree {
constructor(nums, merge, identity) {
this.n = nums.length;
this.merge = merge;
this.identity = identity;
this.t = new Array(2 * this.n).fill(identity);
for (let i = 0; i < this.n; i++) this.t[this.n + i] = nums[i]; // leaves
for (let k = this.n - 1; k >= 1; k--) this.t[k] = merge(this.t[2 * k], this.t[2 * k + 1]);
}
update(i, value) {
let k = i + this.n;
this.t[k] = value;
for (k >>= 1; k >= 1; k >>= 1) this.t[k] = this.merge(this.t[2 * k], this.t[2 * k + 1]);
}
// merge of nums[l..r-1], half-open — O(log n)
query(l, r) {
let left = this.identity, right = this.identity;
for (l += this.n, r += this.n; l < r; l >>= 1, r >>= 1) {
if (l & 1) left = this.merge(left, this.t[l++]); // l is a right child: take it, step past it
if (r & 1) right = this.merge(this.t[--r], right); // r is exclusive: step back, then take it
}
return this.merge(left, right);
}
}
Here is why the loop works. At each level, l is the leftmost node
whose range is still inside the query, and r is one past the
rightmost. If l is odd it is a right child, so its parent
would also cover an element left of the query: take l alone and
move one step right. If r is odd then r - 1 is a left
child whose parent would cover an element right of the query: take
r - 1 alone. Then both indices halve and the same argument holds one
level up. Each level takes at most two nodes, so a query is O(log n).
Dry run: query(2, 6) on [3, 1, 4, 1, 5, 9, 2, 6] with sum
| level | l, r | l odd? | r odd? | running result |
|---|---|---|---|---|
| leaves | 10, 14 | no | no | 0 |
| parents | 5, 7 | yes: take t[5] = 4 + 1 = 5, l becomes 6 | yes: r becomes 6, take t[6] = 5 + 9 = 14 | 5 + 14 = 19 |
| grandparents | 3, 3 | l < r fails, stop | 19 | |
The answer 19 matches the recursive tree's query(2, 5) from the
dry run above, and it took two reads. Note the half-open range: the
second argument is one past the last index.
⚠ Why there are two accumulators
For sum and min a single accumulator would do, since the order of merging does
not change the answer. But the loop collects nodes from the left edge moving
right and from the right edge moving left. For an operation that is not
commutative (string concatenation, the struct merge above, matrix product) the
left pieces must be merged on the left side and the right pieces on the right
side, which is what
left and right do. With them the
code is correct for non-commutative merges too, including when n is not a
power of two. Passing a merge to the constructor means you must also pass
its identity, and it must be a true identity for that merge.
Use this version when you want the smallest, fastest code for point update and range query. Its limits: lazy propagation is awkward without the recursive structure, and the "descend by comparing the children" idiom in the next section is easier on the recursive layout.
Descending the tree: the leftmost index at or above a threshold
A segment tree can also answer questions about positions, not just ranges. Take an array of capacities and the request "find the leftmost bin with capacity at least x, then use some of it." Scanning is O(n). If each node stores the maximum of its range, you can decide in O(1) whether a range contains any bin with capacity ≥ x, and descend straight to the answer: go left if the left child's maximum is enough, otherwise go right.
class MaxTree {
constructor(nums) {
this.n = nums.length;
this.t = new Array(4 * this.n).fill(-Infinity);
if (this.n > 0) this._build(nums, 1, 0, this.n - 1);
}
_build(nums, node, lo, hi) {
if (lo === hi) { this.t[node] = nums[lo]; return; }
const mid = (lo + hi) >> 1;
this._build(nums, 2 * node, lo, mid);
this._build(nums, 2 * node + 1, mid + 1, hi);
this.t[node] = Math.max(this.t[2 * node], this.t[2 * node + 1]);
}
update(i, value, node = 1, lo = 0, hi = this.n - 1) {
if (lo === hi) { this.t[node] = value; return; }
const mid = (lo + hi) >> 1;
if (i <= mid) this.update(i, value, 2 * node, lo, mid);
else this.update(i, value, 2 * node + 1, mid + 1, hi);
this.t[node] = Math.max(this.t[2 * node], this.t[2 * node + 1]);
}
// smallest index i >= from with nums[i] >= x, or -1 — O(log n)
firstAtLeast(x, from = 0, node = 1, lo = 0, hi = this.n - 1) {
if (hi < from || this.t[node] < x) return -1; // out of range, or no bin here is big enough
if (lo === hi) return lo;
const mid = (lo + hi) >> 1;
const found = this.firstAtLeast(x, from, 2 * node, lo, mid);
return found !== -1 ? found : this.firstAtLeast(x, from, 2 * node + 1, mid + 1, hi);
}
}
Without the from argument the search is one root-to-leaf walk:
at every node, the left child either has a bin that fits (go there; the answer
is the leftmost one) or it does not and the right child must. With
from some nodes straddle the start position, but a straddling
node exists at only one position per level, and any node fully to the right of
from is either rejected in O(1) by its maximum or contains the
answer, so the total is still O(log n). Dry run on
[5, 3, 9, 2, 7, 8, 1, 6]:
| call | node [lo, hi] and its max | decision |
|---|---|---|
| firstAtLeast(8) | [0,7], max 9 | 9 ≥ 8, split |
| [0,3], max 9 | 9 ≥ 8, split: try the left child first | |
| [0,1], max 5 | 5 < 8, return −1 at once | |
| [2,3], max 9 | ≥ 8, split | |
| [2,2], 9 | leaf, return 2 | |
| firstAtLeast(8, 3) | [0,3], max 9 | 9 is at index 2, before from, but the node's max cannot tell; split |
| [0,1], then [2,2] inside [2,3] (max 9, split) | both end before 3: return −1 at once | |
| [2,3] and [3,3], max 2 | [3,3] has 2 < 8: return −1, so the whole left half fails | |
| [4,7], max 8 | 8 ≥ 8, split; [4,5] has max 8, split; [4,4] is 7 < 8 | |
| [5,5] | leaf, 8 ≥ 8, return 5 |
⚠ After you use a bin, write the new capacity back
The tree answers "where"; you still have to call
update with the
reduced capacity, or the same bin is offered again. A bin that is used up
entirely can be set to -Infinity (or 0, when every request is
positive), so it is never chosen again. Check -1 before using the
result as an index.
The same shape answers "leftmost position whose prefix sum reaches k" (store sums, compare against the remaining amount while descending) and, with a min tree, "first index whose value is at most x". Whenever a loop over positions asks for the first one that satisfies a monotone test on a range aggregate, descend instead.
Lazy propagation: range updates without touching every leaf
Now let updates be ranges too: "add 5 to everything in [l, r]." Doing that with point updates is O(n log n) per operation — worse than a plain loop. The fix is to be lazy: when a node's range is fully inside the update range, apply the change to that node's aggregate only and leave an IOU on it saying "my children still owe this." The IOU is pushed down one level at a time, and only when someone actually descends through that node.
The whole technique is two rules. Push before you look — any node you visit must settle its debt before you read or split it. Stop at total coverage — record the IOU and return without recursing. Together they keep every range update at O(log n).
class LazySumTree {
constructor(n) {
this.n = n;
this.t = new Array(4 * n).fill(0);
this.lazy = new Array(4 * n).fill(0); // pending "+x to every element in my range"
}
_push(node, lo, hi) {
const add = this.lazy[node];
if (add === 0) return;
this.t[node] += add * (hi - lo + 1); // a range add of x raises the SUM by x * width
if (lo !== hi) { // leaves have nobody to hand the debt to
this.lazy[2 * node] += add;
this.lazy[2 * node + 1] += add;
}
this.lazy[node] = 0;
}
rangeAdd(l, r, add, node = 1, lo = 0, hi = this.n - 1) {
this._push(node, lo, hi); // settle before doing anything else
if (r < lo || hi < l) return;
if (l <= lo && hi <= r) { // fully covered — take the IOU and STOP
this.lazy[node] += add;
this._push(node, lo, hi); // apply to this node so its parent can re-merge
return;
}
const mid = (lo + hi) >> 1;
this.rangeAdd(l, r, add, 2 * node, lo, mid);
this.rangeAdd(l, r, add, 2 * node + 1, mid + 1, hi);
this.t[node] = this.t[2 * node] + this.t[2 * node + 1];
}
query(l, r, node = 1, lo = 0, hi = this.n - 1) {
this._push(node, lo, hi); // same rule on the read path
if (r < lo || hi < l) return 0;
if (l <= lo && hi <= r) return this.t[node];
const mid = (lo + hi) >> 1;
return this.query(l, r, 2 * node, lo, mid)
+ this.query(l, r, 2 * node + 1, mid + 1, hi);
}
}
⚠ Forgetting
* (hi - lo + 1), and mixing update kinds
Adding x to a range of width w raises that node's sum by x·w, not by
x. On a min/max tree it really is just += x (adding a constant
shifts the minimum by that constant), so the multiplier is sum-specific — get
it wrong and small tests still pass because width-1 leaves are correct.
Separately: "add x to a range" and "assign x to a range" are different lazy
values and cannot share one field naively — if a problem needs both, store the
assignment tag and the pending add together, and apply assignment first.
My Calendar III: a dynamic tree and tags that never move
A booking [start, end) arrives, and after each one you must report
the largest number of bookings that overlap at any single moment. Times go up
to 109, so an array indexed by time is impossible, and bookings
arrive online, so you cannot compress coordinates in advance. This is a
range-add, global-max problem, which is exactly what a segment tree with lazy
updates does, if the tree is built on demand.
Two ideas combine. First, a dynamic tree: nodes are objects created
only when an update descends into them, so the tree over [0, 109] has
about log₂(109) ≈ 30 levels but only O(log C) new nodes per booking.
Second, tags that never move. In the earlier lazy tree you pushed a
pending add down to the children before looking at them. For "add to a range,
ask for the maximum" there is a simpler trick: leave the tag on the node where
the update stopped, and let every ancestor recompute its maximum as
its own tag + the larger child maximum. Nothing is ever pushed down,
so no node ever needs to be visited just to settle a debt.
class MyCalendarThree {
constructor() {
this.root = this._node();
}
_node() { return { max: 0, add: 0, left: null, right: null }; }
// book [startTime, endTime) and return the largest overlap so far — O(log C) per call
book(startTime, endTime) {
this._add(this.root, 0, 1000000000, startTime, endTime - 1); // closed [l, r] inside the tree
return this.root.max;
}
_add(node, lo, hi, l, r) {
if (r < lo || hi < l) return;
if (l <= lo && hi <= r) { // fully covered: tag this node and stop
node.max++;
node.add++;
return;
}
const mid = Math.floor((lo + hi) / 2);
if (node.left === null) node.left = this._node();
if (node.right === null) node.right = this._node();
this._add(node.left, lo, mid, l, r);
this._add(node.right, mid + 1, hi, l, r);
node.max = node.add + Math.max(node.left.max, node.right.max); // the tag still applies to everything below
}
}
The invariant is that node.max is the largest overlap anywhere in
the node's range, counting the tags on the node itself and below it but not on
its ancestors. When a booking fully covers a node, every point in it gains one
booking, so both the maximum and the stored tag go up by one. A parent
re-derives its maximum from its children's maxima plus its own tag. The root has
no ancestors, so root.max is the global answer.
| call | effect | returns |
|---|---|---|
| book(10, 20) | covers 10..19 | 1 |
| book(50, 60) | covers 50..59, disjoint from the first | 1 |
| book(10, 40) | 10..19 now has 2 overlapping | 2 |
| book(5, 15) | 10..14 is inside all of [10,20), [10,40), [5,15) | 3 |
| book(5, 10) | 5..9 has two, and 10 is not included in [5,10) | 3 |
| book(25, 55) | 50..54 now has two; no point reaches 4 | 3 |
⚠ Half-open bookings and the tag-never-moves rule
A booking
[start, end) excludes end, so the closed
range inside the tree is [start, end - 1]; forgetting the
- 1 makes book(5, 10) and book(10, 20)
appear to overlap at 10. And the shortcut is specific to this problem: tags
that never move work because adding x to a whole range shifts its maximum by
exactly x. A range sum needs the range width in the update, and an
assignment tag does not commute with an add tag, so those need the
push-down version from the lazy section.
Memory is about O(n log C) nodes for n bookings, because each booking creates at most a few nodes per level. When all the bookings are known in advance, an even simpler method sweeps sorted start and end events and counts, but the online version above is the one that survives a follow-up of "what if they arrive one at a time?"
Fenwick tree: the same job in a quarter of the code
A Binary Indexed Tree does point-update / prefix-query with one flat array and
two three-line loops. The idea: store in t[k] the sum of the
k & -k elements ending at k, where k & -k
isolates the lowest set bit. Then any prefix is assembled by repeatedly
stripping the lowest set bit, and any index is updated by repeatedly adding it
— both take as many steps as there are bits, so O(log n).
class Fenwick {
constructor(n) {
this.n = n;
this.t = new Array(n + 1).fill(0); // 1-INDEXED internally; index 0 is unusable
}
// add delta at 0-indexed position i — O(log n)
update(i, delta) {
for (let k = i + 1; k <= this.n; k += k & -k) this.t[k] += delta;
}
// sum of nums[0..i] inclusive — O(log n)
prefix(i) {
let sum = 0;
for (let k = i + 1; k > 0; k -= k & -k) sum += this.t[k];
return sum;
}
range(l, r) {
return this.prefix(r) - (l > 0 ? this.prefix(l - 1) : 0); // needs an invertible op
}
// O(n) build — much better than n calls to update(), which is O(n log n)
static from(nums) {
const f = new Fenwick(nums.length);
for (let i = 0; i < nums.length; i++) f.t[i + 1] += nums[i];
for (let k = 1; k <= f.n; k++) {
const parent = k + (k & -k);
if (parent <= f.n) f.t[parent] += f.t[k]; // push each cell into the one that contains it
}
return f;
}
}
⚠ Fenwick stores deltas, not values
update(i, delta) adds; it does not assign. To set
nums[i] = v you must keep the raw array alongside and call
update(i, v - nums[i]), then write nums[i] = v.
Passing the new value straight in is the single most common Fenwick bug, and
it produces plausible-looking wrong answers rather than a crash. Related: the
1-indexing is not stylistic — k & -k is 0 when k is 0, so a
0-indexed update loop never terminates.
Why Fenwick usually wins in practice
Both are O(log n), but the constants differ a lot. The Fenwick array is n+1 entries versus 4n; the loops are iterative with no recursion, no range bookkeeping and no branching; and access is a tight sequence of index arithmetic that the cache handles well. Expect a 2–4× real-time speedup and roughly a quarter of the code. It is also far easier to write correctly under interview pressure — two loops with no off-by-one range logic.
The price is expressiveness. Fenwick does point-update + prefix-query of an invertible operation, and (via a difference array) range-update + point-query. Everything else — range min/max, range update and range query together (a two-tree trick exists for sums, but the segment tree is the general tool), storing anything richer than a number per node — is segment tree territory.
Count of smaller numbers after self — the classic BIT problem
For each element, how many elements to its right are strictly smaller? Brute force is O(n²). The trick is to sweep right-to-left over value ranks instead of positions: a Fenwick tree over ranks makes "how many already-seen values rank below this one?" a single prefix query.
// O(n log n) time, O(n) space
function countSmaller(nums) {
// coordinate compression: values can be huge/negative, ranks are 0..m-1
const sorted = [...new Set(nums)].sort((a, b) => a - b);
const rank = new Map(sorted.map((v, i) => [v, i]));
const bit = new Fenwick(sorted.length);
const res = new Array(nums.length);
for (let i = nums.length - 1; i >= 0; i--) { // right to left: "seen" == "to my right"
const r = rank.get(nums[i]);
res[i] = r > 0 ? bit.prefix(r - 1) : 0; // count of seen values with a STRICTLY lower rank
bit.update(r, 1); // now this element counts as seen
}
return res;
}
Coordinate compression is the reusable half of this idea: whenever you want a Fenwick indexed by value but values are unbounded, sort the distinct values and index by rank. The same right-to-left + BIT skeleton solves counting inversions, "reverse pairs," and range-sum-count problems — recognising the skeleton is worth more than memorising any one of them.
Fenwick binary descent: the k-th element in O(log n)
Suppose a Fenwick tree holds counts, so prefix(i) is how
many items have a value at index ≤ i. A natural question is the reverse one:
"what is the smallest index whose prefix count reaches k?", which is the k-th
smallest item of a multiset that supports insert and delete. Binary searching
over prefix works but costs O(log² n): a log for the search and a
log for each prefix. The Fenwick layout lets you do it in one O(log n) walk.
Recall that t[k] covers the block of k & -k items
ending at k. Start at position 0 and try to move right by the biggest power of
two, then the next biggest, and so on. A jump of step from
pos lands on pos + step, and because
pos only has bits larger than step set, the cell
t[pos + step] covers exactly the step items just after
pos. So you can ask "would taking this whole block still leave the
prefix below the target?" and decide each bit of the answer from the high end,
exactly as in a binary search over bits.
// smallest 0-indexed i with prefix(i) >= target, or f.n if the total is smaller.
// Every entry must be >= 0 so that prefix sums never decrease. O(log n).
function lowerBound(f, target) {
let top = 1;
while (top * 2 <= f.n) top *= 2; // the largest power of two that fits
let pos = 0, remaining = target;
for (let step = top; step > 0; step >>= 1) {
if (pos + step <= f.n && f.t[pos + step] < remaining) {
pos += step; // the whole block is still too small: jump over it
remaining -= f.t[pos];
}
}
return pos; // pos items have a prefix below target, so the answer is item number pos (0-indexed)
}
Dry run: values 1 to 8 live at indices 0 to 7, with counts
[1, 0, 2, 0, 1, 0, 0, 3], so the multiset is 1, 3, 3, 5, 8, 8, 8 and
the 4th smallest should be 5, which is index 4. The Fenwick cells are
t[4] = 1+0+2+0 = 3, t[6] = 1+0 = 1,
t[5] = 1 and t[8] = 7.
| step | pos + step | t[pos + step] | < remaining? | pos after | remaining after |
|---|---|---|---|---|---|
| 8 | 8 | 7 | 7 < 4? no | 0 | 4 |
| 4 | 4 | 3 | 3 < 4? yes, jump | 4 | 1 |
| 2 | 6 | 1 | 1 < 1? no | 4 | 1 |
| 1 | 5 | 1 | 1 < 1? no | 4 | 1 |
| return pos = 4, which is value 5 | 4 | ||||
⚠ Three ways the descent goes wrong
Negative entries. The jump test assumes prefix sums only grow. With a
negative cell the condition no longer separates "too small" from "enough" and
the result is meaningless; this works for counts, not for arbitrary deltas.
Not enough items. If the total is below the target, the loop jumps as far
as it can and returns
f.n, which is one past the last index.
Check for it. The strict comparison. The test is <, not
<=: you stop at the last position whose prefix is still
below the target, and the answer is the element after it. Using
<= finds the first position whose prefix is strictly greater
than the target, which is a different question.
This is the engine behind "k-th smallest in a dynamic multiset", "find the person at position k after removals" (keep a 1 per present person, delete by subtracting 1) and "k-th missing number" style questions. It only works on a Fenwick tree built over counts; the segment tree does the same thing by descending with the left child's total, and handles more kinds of nodes.
Range add and range sum with two Fenwick trees
The earlier table says a Fenwick tree over a difference array supports range update with a point query. To get range updates and range sums you can use a segment tree with lazy tags, or you can pay two Fenwick trees and a little algebra. Derive it, because the formula is easy to forget but easy to rebuild.
Let d[j] be the difference array, d[j] = a[j] - a[j-1]
(with a[-1] = 0), so a[k] is the sum of
d[0..k]. A range add of x on [l, r] changes just two entries:
d[l] += x and d[r+1] -= x. The prefix sum of
a up to i is
S(i) = Σk≤i a[k] = Σk≤i Σj≤k d[j] = Σj≤i d[j] · (i − j + 1) = (i + 1) · Σj≤i d[j] − Σj≤i j · d[j]
The middle step counts how many times each d[j] is included: once
for each k from j to i, which is i − j + 1 times. The last form needs only two
prefix sums: one over d[j] and one over j · d[j]. Keep
one Fenwick tree for each.
class RangeFenwick {
constructor(n) {
this.n = n;
this.byD = new Fenwick(n); // prefix sums of d[j]
this.byJD = new Fenwick(n); // prefix sums of j * d[j]
}
_addAt(j, x) { // d[j] += x
this.byD.update(j, x);
this.byJD.update(j, x * j);
}
// add x to every element in [l, r] — O(log n)
rangeAdd(l, r, x) {
this._addAt(l, x);
if (r + 1 < this.n) this._addAt(r + 1, -x); // nothing to cancel past the end
}
_prefix(i) {
if (i < 0) return 0;
return (i + 1) * this.byD.prefix(i) - this.byJD.prefix(i);
}
// sum of a[l..r] — O(log n)
rangeSum(l, r) {
return this._prefix(r) - this._prefix(l - 1);
}
}
This reuses the Fenwick class from earlier in the chapter. Dry run
on five zeros: rangeAdd(1, 3, 5) sets d[1] = 5 and
d[4] = −5, so the stored arrays are:
| j | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| d[j] | 0 | 5 | 0 | 0 | −5 |
| j · d[j] | 0 | 5 | 0 | 0 | −20 |
Now rangeSum(0, 2) is S(2) − S(−1) with
S(2) = 3 · (0 + 5 + 0) − (0 + 5 + 0) = 10, which is a[0] + a[1] + a[2] =
0 + 5 + 5. And S(4) = 5 · (0 + 5 + 0 + 0 − 5) − (0 + 5 + 0 + 0 − 20) = 0 + 15 =
15, which is 5 + 5 + 5 over the whole array. Each call is four Fenwick
operations, O(log n), with no recursion and no tags to push.
⚠ Initial values, and the overflow of j · d[j]
The structure above starts from all zeros. If the array starts with values,
either feed them in as range adds (
rangeAdd(i, i, a[i]) for each
i), or keep a static prefix-sum array of the original values and add it to
every answer. Also, j · d[j] can reach n times the largest update,
so when n and the updates are both large the totals can pass
253, and Number arithmetic then silently loses digits;
use BigInt if the limits are that large.
Offline sort plus Fenwick: reverse pairs and range-sum counts
The count-of-smaller sweep generalises. Whenever a question is "for each element, count the earlier (or later) elements that stand in some order relation to it", sweep in index order, keep a Fenwick tree indexed by compressed value, and for each element ask for a range of values among the elements already inserted. Two questions show the pattern with a twist: the value you compare against is not the element itself.
Reverse pairs: count i < j with nums[i] > 2 · nums[j]
For each j, the number of earlier elements larger than 2·nums[j] is "how many have I inserted" minus "how many inserted are at most 2·nums[j]". The value 2·nums[j] need not be in the array, so find its position among the sorted distinct values with a binary search for how many values are ≤ it.
// number of entries in the ascending array sorted that are <= x
function countAtMost(sorted, x) {
let lo = 0, hi = sorted.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (sorted[mid] <= x) lo = mid + 1;
else hi = mid;
}
return lo;
}
// number of entries in the ascending array sorted that are < x
function countLess(sorted, x) {
let lo = 0, hi = sorted.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (sorted[mid] < x) lo = mid + 1;
else hi = mid;
}
return lo;
}
// O(n log n) time, O(n) space
function reversePairs(nums) {
const sorted = [...new Set(nums)].sort((a, b) => a - b);
const bit = new Fenwick(sorted.length);
let pairs = 0;
for (let j = 0; j < nums.length; j++) {
const upTo = countAtMost(sorted, 2 * nums[j]); // ranks 0..upTo-1 are values <= 2 * nums[j]
const notBigger = upTo > 0 ? bit.prefix(upTo - 1) : 0; // inserted values that are NOT bigger
pairs += j - notBigger; // j values were inserted so far
bit.update(countLess(sorted, nums[j]), 1); // the rank of nums[j] is how many values are below it
}
return pairs;
}
On [1, 3, 2, 3, 1] the sorted distinct values are 1, 2, 3 with
ranks 0, 1, 2. The answer should be 2, from the pairs (index 1, index 4) and
(index 3, index 4), since 3 > 2·1:
| j | nums[j] | 2·nums[j] | values ≤ it | inserted so far | not bigger | pairs added |
|---|---|---|---|---|---|---|
| 0 | 1 | 2 | 2 (ranks 0, 1) | 0 | 0 | 0 |
| 1 | 3 | 6 | 3 | 1 | 1 | 0 |
| 2 | 2 | 4 | 3 | 2 | 2 | 0 |
| 3 | 3 | 6 | 3 | 3 | 3 | 0 |
| 4 | 1 | 2 | 2 | 4 | 2 (the values 1 and 2) | 2 |
At j = 4 the inserted values are 1, 3, 2, 3. Those at most 2 are 1 and 2, so two are not bigger, and 4 − 2 = 2 are bigger: the two 3s.
Count of range sum: how many subarrays have a sum in [lower, upper]
Let P be the prefix sums with P[0] = 0. A subarray
ending at j and starting after i has sum
P[j] − P[i], so you need the number of earlier prefix values
P[i] in the window
[P[j] − upper, P[j] − lower]. That is a range query on
the Fenwick tree over compressed prefix values, the difference of two prefix
counts.
// O(n log n) time, O(n) space; needs countAtMost and countLess from above
function countRangeSum(nums, lower, upper) {
const prefix = [0];
for (const x of nums) prefix.push(prefix[prefix.length - 1] + x);
const sorted = [...new Set(prefix)].sort((a, b) => a - b);
const bit = new Fenwick(sorted.length);
let total = 0;
for (const p of prefix) {
const from = countLess(sorted, p - upper); // ranks below this are too small
const to = countAtMost(sorted, p - lower); // ranks below this hold values at most p - lower
const atMost = to > 0 ? bit.prefix(to - 1) : 0;
const below = from > 0 ? bit.prefix(from - 1) : 0;
total += atMost - below;
bit.update(countLess(sorted, p), 1); // insert AFTER counting, so only earlier prefixes are matched
}
return total;
}
On nums = [−2, 5, −1] with the window [−2, 2], the prefix sums are
[0, −2, 3, 2] and the answer is 3 (the subarrays [−2], [−2, 5, −1] and [−1]):
| p | window [p − 2, p + 2] | earlier prefixes in the window | added |
|---|---|---|---|
| 0 | [−2, 2] | none yet | 0 |
| −2 | [−4, 0] | 0 | 1 |
| 3 | [1, 5] | none of 0, −2 | 0 |
| 2 | [0, 4] | 0 and 3 | 2 |
⚠ Insert after counting, and strict versus inclusive bounds
If you insert
p before querying, the element pairs with itself
and, whenever 0 is inside [lower, upper], an empty subarray is counted. Use
countLess for the lower edge of the window and
countAtMost for the upper edge; swapping them drops or adds the
values that sit exactly on the edge. Also remember the prefix array has n + 1
entries, not n.
The reusable skeleton is: (1) turn the question into "for each j, count earlier i whose value falls in a window that depends on j"; (2) compress the values you will insert; (3) sweep, query the window as the difference of two prefix counts, then insert. Merge sort can solve the same two problems in the same time, but the Fenwick version is shorter to write once the helper exists, and each element is counted the moment it is inserted (the values still have to be known up front for the compression).
Say it like this → "Queries and updates are
interleaved, so a prefix-sum array would cost O(n) per update. I'll use a
Fenwick tree — point update and prefix query both O(log n), and range sum is
just the difference of two prefixes. If the problem needed range minimum, or
range updates as well as range queries, I'd move to a segment tree with lazy
propagation instead, since min has no inverse and Fenwick can't do it."
Which one to reach for
| Workload | Use | Why |
|---|---|---|
| Static array, many range queries | Prefix-sum array | O(1) queries; a tree is pure overhead |
| Static array, range min/max only | Sparse table | O(n log n) build, O(1) query, no updates |
| Point update + prefix/range sum | Fenwick | smallest, fastest, hardest to get wrong |
| Range update + point query | Fenwick over a difference array | add x at l, subtract x at r+1; point value = prefix sum |
| Point update + range min/max/gcd | Segment tree | non-invertible merge — prefixes can't be subtracted |
| Range update + range query | Segment tree + lazy | the only one of the three that can defer work |
| Rich per-node state (max subarray, merge sort tree) | Segment tree | a node can hold a struct, not just a number |
| 2D grid sums with updates | 2D Fenwick | nested loops over both dimensions, O(log² n) |
| Range add + range sum, no tags to push | Two Fenwick trees | prefix sums of d[j] and of j·d[j] give any prefix sum |
| k-th smallest in a multiset with inserts and deletes | Fenwick descent | one O(log n) walk over the counts, instead of a binary search over prefix queries |
| Leftmost position that fits, with updates | Segment tree of maxima, descended | each node's max says whether the left half can serve the request |
| Count earlier elements in a value window | Offline sweep + Fenwick over ranks | reverse pairs, range-sum counts, inversions |
| Online range add, global max, huge coordinates | Dynamic segment tree | nodes created on demand; tags stay on the node |
One sentence to keep
A prefix-sum array is a segment tree that gave up on updates; a Fenwick tree
is a segment tree that gave up on everything except invertible prefixes. Start
at the cheapest one that still answers the question, and only climb when the
workload forces you to.
Recognizing it in an unseen problem
- Queries and updates are interleaved over the same array — that single word "update" is what rules out a prefix-sum array
- Constraints around n, q ≥ 105 with per-query work implied — O(n·q) is 1010, so an O(log n) per operation structure is the intended answer
- Brute force is "recompute the range every time"; the fix is caching O(n) partial aggregates so any range is O(log n) of them
- Sum-like and invertible (sum, XOR, count) → Fenwick. Min/max/gcd, or updates that span ranges → segment tree
- "How many earlier/later elements are smaller/larger" or "count inversions" → sweep in one direction with a Fenwick over compressed value ranks, not over positions
- Distinguish from a heap: a heap gives you the global min/max with updates, but cannot answer a specific range. Distinguish from a sorted structure: if you need order statistics plus ranges, that's a Fenwick over ranks
- If the array never changes after construction, stop — prefix sums or a sparse table, and say why you didn't build a tree
- The answer for a range cannot be built from one number per half (best subarray, longest run, is-it-sorted) → a segment tree whose node stores a small struct, merged left-to-right
- "Find the first / leftmost / k-th position such that ..." with updates → descend a tree of aggregates (maxima in a segment tree, counts in a Fenwick) instead of scanning
- "Count pairs i < j with a relation between nums[i] and a transform of nums[j]" → sweep j, query a window of ranks among the inserted values, then insert
- Range add and range sum with a tight code budget → two Fenwick trees; times up to 109 arriving online → a dynamic segment tree
Before you move on
- Write a segment tree's build/query/update from memory, and explain why the no-overlap return must be the operation's identity element.
- Explain why a Fenwick tree can answer range sum but not range minimum, in terms of invertibility.
- Write a Fenwick tree's update and prefix loops, and explain why 1-indexing (not 0-indexing) is required for
k & -kto terminate. - State the two lazy-propagation rules — push before you look, stop at total coverage — and say what breaks if you skip either one.
- Given a new problem, decide whether it needs a prefix-sum array, a Fenwick tree, or a segment tree (with or without lazy propagation), based on whether updates and non-invertible queries are both present.
- Define the four fields of a max-subarray node and say why its merge is not commutative and has no simple identity element.
- Write the iterative bottom-up tree from memory, say why the query uses a left and a right accumulator, and why the range is half-open.
- Descend a tree of maxima to find the leftmost bin that fits, and descend a Fenwick tree of counts to find the k-th element; say what each needs in order to work (monotone test, non-negative counts).
- Derive the two-Fenwick formula for a range-add range-sum from the difference array.
- Turn "count pairs with a relation" into a sweep with a Fenwick tree over compressed values, and say why you insert after counting.
- Explain why a dynamic segment tree can leave its tags in place for range add with global max, and why that shortcut does not carry over to sums or assignments.
Opens in the editor: write it, run it, and check it against real tests.
All 159 for this level →5 questions, pass at 4. The check runs in your browser and the result stays on this device, so it is a self-check and not a certificate.
Finished reading?
0 of 48 chapters read in this series