DSA · Advanced structures
Answer a range query without reading the range
A prefix-sum array answers any range sum in one subtraction — until someone changes a value and you rebuild all of it. A segment tree keeps the query and the update at O(log n). Build one over eight numbers here, watch a query light 9 of its 15 nodes, then meet the Fenwick tree and KMP.
Build the tree, then run a query on it →01 The idea
Precompute the halves, not the answers
You have an array and two kinds of request: "what is the sum of positions 2 to 6" and "position 3 just became 5". Handle them with a plain array and the query costs O(n) while the update costs O(1). Precompute prefix sums and you flip it — the query drops to O(1), but one changed value invalidates every prefix after it, so the update climbs to O(n). Either way, a workload that mixes the two costs you n per operation somewhere.
A segment tree refuses both extremes. It stores the answer for the whole array, for each half, for each quarter, and so on down to single elements. Any range you are asked about is a handful of those stored pieces glued together, and any single change touches only the pieces that contain it — one path from a leaf to the root. Both operations land at O(log n), and that symmetry is the entire reason the structure exists.
02 Worked example
Eight numbers, fifteen nodes, one query
The array is 5 2 7 1 9 3 6 4, and every number quoted below comes from it — right up to the update section further down, which deliberately changes a[3] so you can watch the repair. Build it first. Leaves hold the eight values; every level above adds pairs. Each row below prints a node's stored value once for every array position that node covers, so the spans line up as a pyramid — the tree stores each number once, not eight times.
Node numbering is the trick that makes the code short: the root is node 1, and node v has children 2v and 2v+1. So node 2 covers 0..3, node 5 covers 2..3, node 14 covers position 6 alone. Now ask for sum(2..6), which is 7 + 1 + 9 + 3 + 6 = 26. The query starts at the root and asks one question of every node it reaches: is your slice completely outside my range, completely inside it, or straddling the edge?
Count what the query never looked at. Node 4 said "outside" and its two leaves, 8 and 9, were never visited. Node 5 said "inside" and its two leaves, 10 and 11, were never visited either — the answer was already sitting in the parent. Node 6 cut two more the same way. That is 6 of the 15 nodes skipped, 9 visited, and only 3 of those nine actually contributed a number.
Be honest about the scoreboard at this size: a plain scan reads 5 cells, and the tree visited 9 nodes. At n = 8 the tree loses. What it buys is the growth rate — the visited count is bounded by about four nodes per level, and levels grow like log n. On 100,000 values no range query visits more than about 70 nodes, while the scan reads up to 100,000.
03 The code
Prefix sums, a tree, and two more tools
Put the two structures side by side on the same array. The prefix version wins the query outright and loses the update outright. The tree gives up O(1) queries to buy a cheap update, and everything else in this module is a variation on that bargain.
Prefix sums — O(1) query, O(n) update
pre[0] = 0for i in 0..n−1: pre[i+1] = pre[i] + a[i] # O(n) oncesum(l, r) = pre[r+1] − pre[l] # 33 − 7 = 26set a[3]=5: for j in 4..n: pre[j] += 4
Segment tree — O(log n) query, O(log n) update
query(v, lo, hi, l, r): if r < lo or hi < l: return 0 # outside if l <= lo and hi <= r: return t[v] # inside mid = (lo + hi) / 2 return query(2v, lo, mid, l, r) + query(2v+1, mid + 1, hi, l, r)
Why two base cases and not one? They do different jobs and neither is optional. Line 2 is what abandons a whole subtree — it is why nodes 8 and 9 were never read. Line 3 is what stops the descent early — it is why nodes 10 and 11 were never read even though they are inside the range. Delete line 3 and the recursion walks every leaf in the range: O(n), exactly the scan you were trying to avoid. Delete line 2 and this code does not merely get slow — a leaf outside the range fails line 3, computes mid = lo = hi, and recurses into itself forever.
Why does the query stay O(log n)? At any level of the tree, at most two nodes straddle the range — one at each end of it. Every other node at that level is settled by line 2 or line 3 without descending. So the visited count is capped at roughly four nodes per level, and there are log₂ n levels. On our 8-leaf tree that ceiling is loose: 9 visited against a 5-cell scan. At n = 100,000 it is under 70 against 100,000.
Why not just rebuild the prefix array after each update? One update is O(n). Interleave 100,000 updates with 100,000 queries and the prefix version does about 1010 operations, which is minutes. The tree does about 200,000 × 17 ≈ 3.4 million, which is milliseconds. The break-even is not close.
The point update, and why only one path changes
update(v, lo, hi, i, val): if lo == hi: t[v] = val; return # the leaf mid = (lo + hi) / 2 if i <= mid: update(2v, lo, mid, i, val) else: update(2v+1, mid + 1, hi, i, val) t[v] = t[2v] + t[2v+1] # repair going back up
Trace it on our array. Set a[3] from 1 to 5. The walk is 1 → 2 → 5 → 11. The root's mid is 3, and line 4's i <= mid test sends index 3 to the left child; node 2's mid is 1, so it goes right; node 5's mid is 2, so it goes right again. Write the leaf, then repair on the way out: node 11 becomes 5, node 5 goes 8 → 12, node 2 goes 15 → 19, the root goes 37 → 41. Four writes. Every other node is untouched, because no other node's slice contains index 3. The prefix-sum version rewrites pre[4..8] — five cells here, and n − i cells for an update at index i.
Fenwick tree — the same job in six lines
A Fenwick tree, also called a binary indexed tree or BIT, does prefix sums with point updates and nothing else. Same O(log n) on both, a quarter of the code, and a noticeably better constant. It is 1-indexed, and index i owns the block of values ending at i whose length is i's lowest set bit, which i & (−i) extracts.
add(i, delta): # 1-indexed, i = 1..n while i <= n: bit[i] += delta; i += i & (−i)prefix(i): # sum of a[1..i] s = 0 while i > 0: s += bit[i]; i −= i & (−i) return s # range: prefix(r) − prefix(l−1)
The same query, the same answer. Written 1-indexed our array is a[1..8] = 5 2 7 1 9 3 6 4, so the 0-indexed range 2..6 is 1-indexed 3..7 and sum(2..6) becomes prefix(7) − prefix(2). The tree holds bit = 5 7 7 15 9 12 6 37. prefix(7) walks 7 → 6 → 4 and adds 6 + 12 + 15 = 33; prefix(2) is just 7. That is 26 again. Look at the three blocks it used: 6, 12 and 15 are segment-tree nodes 14, 6 and 2. A Fenwick tree keeps the subset of segment-tree nodes that prefix sums need and throws the rest away — which is also why it cannot answer a range minimum. There is no "subtract one prefix from another" for a minimum.
KMP — never re-read a text character
Naive substring search restarts the pattern at every text position, so a text of length n and a pattern of length m cost O(n·m) in the worst case — searching for aaab inside a million as hits that ceiling exactly. KMP spends O(m) up front building the LPS array, then walks the text once and never moves the text pointer backwards.
| i | pattern[0..i] | longest proper prefix that is also a suffix | lps[i] |
|---|---|---|---|
| 0 | a | a single character has no proper prefix | 0 |
| 1 | ab | prefix a vs suffix b — no match | 0 |
| 2 | aba | a | 1 |
| 3 | abab | ab | 2 |
| 4 | ababc | nothing ends in c except the whole string | 0 |
Search ababc inside abababc. Positions 0 to 3 match, so j reaches 4. At text index 4 the text has a and the pattern wants c — mismatch. Instead of restarting, set j = lps[3] = 2: the last two matched characters were ab, which is already a prefix of the pattern, so those two comparisons are never repeated. Text index 4 then matches, and so do 5 and 6, giving a hit starting at index 2. Eight character comparisons in total, against eleven for the naive scan on this tiny input — and O(n) against O(n·m) as the strings grow.
05 Cheat sheet
Costs worth memorising
| Structure | Build | Range query | Point update | Space |
|---|---|---|---|---|
| Plain array | O(1) | O(n) | O(1) | n |
| Prefix-sum array | O(n) | O(1) | O(n) | n |
| Fenwick tree (BIT) | O(n log n) | O(log n) | O(log n) | n |
| Segment tree | O(n) | O(log n) | O(log n) | 4n |
| Segment tree + lazy | O(n) | O(log n) | O(log n) per range update | 8n |
| Substring search | Preprocess | Search | Extra memory |
|---|---|---|---|
| Naive restart at every position | — | O(n·m) | O(1) |
| KMP with the LPS array | O(m) | O(n) | O(m) |
06 Where & why
Rarely needed, and decisive when they are
Say the honest thing first: across a whole placement season you may meet one segment-tree problem and no KMP at all. The bulk of every round is arrays, strings, hashing, two pointers, stacks and plain trees. What makes this module worth an evening is that these problems carry no partial credit — you either notice that the updates make prefix sums illegal, or you do not solve it.
Reach for it when…
Use something else when…
| Situation | Better choice | Why |
|---|---|---|
| The array never changes | Prefix-sum array | O(1) per query, one pass to build, four lines. A tree here is showing off and costs you time on the clock. |
| You only need prefix sums with point updates | Fenwick tree | Same O(log n) both ways, a quarter of the code, better constant. Fewer places to make an off-by-one. |
| Range minimum on a static array | Sparse table | O(n log n) to build, then O(1) per query. It beats the tree precisely because there are no updates to survive. |
| All queries known up front, no updates | Mo's algorithm | O((n+q)√n) offline, and it handles answers a tree cannot merge — "how many distinct values in this range" has no combine. |
| You just need to find a substring in real code | The library's find | str.find, indexOf and memmem are already tuned and vectorised. Hand-written KMP is usually slower and always riskier. |
07 Interview questions
What interviewers actually ask
Eleven questions in the order a round escalates: what the structure is, how fast, why that fast, the sibling comparison, and the honest one at the end.
What is a segment tree?
How many nodes does a range query actually touch, and why is that O(log n)?
Why do you allocate 4n and not 2n?
Prefix sums answer a range sum in O(1). Why would anyone build a tree?
Segment tree or Fenwick tree?
How does a point update work, and why is it O(log n)?
What is lazy propagation, in one sentence?
What is the LPS array, and what does KMP do with it?
Why does KMP never move the text pointer backwards?
On a mismatch, why lps[j−1] and not lps[j]?
Be honest — would you ever write a segment tree in an actual interview?
08 Practice problems
Six problems, in order of bite
Every one is solvable with what is above: the two base cases, the repair-on-the-way-up update, i & (−i), and the LPS fallback rule.