3.4.15 · D2Trees

Visual walkthrough — Segment tree — build, range query, point update

2,438 words11 min readBack to topic

We use the array throughout, because you can hold four numbers in your head while the shape of the argument does the heavy lifting.


Step 1 — The slow question we are trying to beat

WHAT. We have a row of boxes. Each box holds a number. Someone keeps asking: "add up the numbers from box to box ." Here (short for left) and (short for right) are just two box positions — the start and end of the stretch we care about.

WHY. Before inventing a clever structure, feel the pain of the obvious method. The obvious method is: walk every box from to and keep a running total. If the stretch is the whole array, that walk touches all boxes.

PICTURE. Below, the amber bracket marks a wanted stretch . The naive method sweeps the cyan arrow across every box inside the bracket, one addition per box.

The cost of one such sweep is additions — as many as there are boxes in the stretch. For a stretch covering the whole array that is additions. We want to answer without touching every box. That is the entire mission.


Step 2 — The one idea: precompute the sum of chunks

WHAT. Instead of adding boxes one at a time when the question arrives, we precompute (compute ahead of time) the total of certain chunks of consecutive boxes and store each total in its own little memory.

WHY. If a chunk's total is already sitting in memory, and a question happens to want exactly that chunk, we answer in one look — no walking. The trick is choosing chunks smartly so that any stretch someone asks for can be pieced together from a few stored chunks.

PICTURE. The whole array is one big chunk (top). We store its total, . Then we cut it into a left half and a right half and store each of those totals too. The amber labels are the stored sums.

The symbol means: how many times do I halve to reach ? For : , that is halvings, so .


Step 3 — Cutting all the way down: naming every chunk

WHAT. We keep cutting each chunk into two halves until a chunk is a single box. We name a chunk by the range of box indices it covers, written (low) is its first box, (high) is its last box.

WHY. Naming the chunk by its own lets us describe the cut with a single, honest formula instead of hand-waving. The cut point is the middle index:

Term by term: adds the two ends, dividing by finds their midpoint, and (the floor, meaning "round down to a whole number") snaps that midpoint to a real box index. The left half is then and the right half is — note the so the two halves do not share a box.

PICTURE. The full binary tree of chunks for . Read it top-down: each parent chunk is sliced at its into the two children below it. The leaves (bottom row) are single boxes.

This tree of chunks is the segment tree. Every idea from here on is just "walk this tree cleverly."


Step 4 — Filling the tree (build): bottom-up sums

WHAT. Now we compute and store each chunk's total. We fill it bottom-up: a leaf's total is just its one box; a bigger chunk's total is its left child's total plus its right child's total.

WHY. A chunk's boxes are exactly the boxes of its left half plus the boxes of its right half, with no box shared and none missing (that was the point of the ). Because sum is additive over disjoint pieces, the parent's total is honestly the sum of the two children's totals — we never need to re-add the original boxes.

Here is a node's address in a flat array called . We use the heap address trick: the root lives at address , and a node at address finds its children at (left) and (right). So is "my left child's stored total" and is "my right child's stored total."

PICTURE. The same tree, now with each node's stored total in amber. Watch the green arrows: two children flow up into their parent. Leaves fill first (they need no children), then , then , then the root .

The tree has nodes (all the chunks), each filled exactly once, so build costs .


Step 5 — Answering a query: the three-case walk

WHAT. To answer "sum over " we walk down from the root. At each node (chunk ) we compare that chunk to the wanted stretch and land in exactly one of three situations.

WHY. These three exhaust every possibility of how two ranges can sit relative to each other:

  • No overlap ( or ): the chunk sits entirely outside the wanted stretch. It contributes nothing, so we return — the value that adds nothing, the identity of sum.
  • Total overlap ( and ): the chunk sits entirely inside the wanted stretch. Its whole precomputed total counts, so we return and stop — no need to look deeper. This early stop is the whole speedup.
  • Partial overlap: the wanted boundary slices through this chunk. We cannot use it wholesale, so we split — ask each child and add their answers.

PICTURE. Querying . Each chunk is tinted by its case: grey = no overlap (skipped), amber = total overlap (its total is taken whole, and we stop), cyan-outline = partial (we recurse through it). The amber chunks we keep — and — are exactly the pieces that tile the wanted stretch.

Why ? On each of the levels, at most chunks are ever the "partial" kind (two hugging the left boundary , two hugging the right boundary ); everything between them is one clean total-overlap grab. So work per query .


Step 6 — Changing one box (point update): repair the path

WHAT. Suppose box changes to a new value. We must fix the stored totals — but only the ones that actually contain box .

WHY. Box lives in exactly one leaf, and that leaf lies inside exactly one chunk at each level — a single root-to-leaf path. Every chunk off that path still holds a correct total (box was never part of it). So we rewrite the leaf, then walk back up recomputing each parent as .

PICTURE. Updating . The amber path root is repaired; the greyed subtree is untouched because it never contained box .


Step 7 — Edge and degenerate cases (never leave a gap)

WHAT / WHY / PICTURE — the corners the code must survive.

  1. Single-box query . The walk descends straight to that leaf; every side chunk is no-overlap. Cost still .
  2. Whole-array query . The root is total overlap immediately — one grab, here. The best case.
  3. Empty / out-of-order stretch (). Every node is no-overlap, so the sum returned is — the correct answer for "no boxes."
  4. not a power of two (like our is, but is not). The tree becomes lopsided — some leaves sit one level deeper. Heap addressing can then reach indices near , which is exactly why the array is sized .
  5. A different combine, not sum. For a minimum tree the no-overlap case must return (the value that never wins a min), for maximum return , for product return . Always return the identity of your combine — the value that changes nothing.

The one-picture summary

One tree does all three jobs. Build flows up (children into parents, once). Query walks down choosing among no/total/partial overlap, grabbing whole chunks where it can (). Update walks down to one leaf then repairs back up along a single path (). The shared secret is halving: only levels exist, so any traversal is short.

Recall Feynman retelling — say it back in plain words

Imagine a row of boxes with numbers. Adding a stretch of them by hand means touching every box — slow. So beforehand I glue the boxes into a pyramid: each brick is the total of the two bricks under it, all the way up to one brick holding the grand total. Building that pyramid touches each box once. Now when someone asks for a stretch, I stand at the top and walk down. If a brick sits fully inside the stretch, I grab its number and don't look deeper — that's the shortcut. If a brick sits fully outside, I ignore it. If the stretch cuts through a brick, I look at its two halves and add what they give me. Because the pyramid is only about layers tall, I only ever inspect a handful of bricks per layer — fast. If one box changes, I don't rebuild the pyramid. Only the bricks stacked directly above that box are wrong, so I fix that one column from the box up to the top. Everything else was already right.

Recall Self-test

Why does a total-overlap node return immediately? ::: Its entire range is wanted, so its precomputed total is exactly what we need — recursing deeper can only re-derive the same number. Why exactly levels? ::: Each level halves the chunk size; you can halve only about times before reaching a single box. Which nodes change on an update to index ? ::: Only the root-to-leaf path through box ; every other chunk never contained . What must no-overlap return for a max-tree? ::: , the identity of .


See also: Prefix Sum (the -query, -update rival), Fenwick Tree (BIT) (a lighter cousin for sums), Lazy Propagation (to make range updates fast too), Binary Tree and Divide and Conquer (the shapes this is built from).