Data Structures/Fenwick & Segment Trees

Lesson 4.74,246 words

Fenwick & Segment Trees

A prefix-sum array answers a range sum in O(1)O(1) but pays O(n)O(n) per update; a plain array updates in O(1)O(1) but pays O(n)O(n) per range sum. Fenwick and segment trees give us both in O(logn)O(\log n).

╌╌╌╌

We have an array and two operations we want to interleave freely: update a single entry, and ask for the sum of a contiguous range . The two obvious data structures each ace one operation and fail the other. Keep as is and an update is a single write in , but a range sum scans the range in . Precompute a prefix-sum array and a range sum collapses to in , but now a single update to disturbs every with , an repair. We want a structure that splits the difference and does both in .

The tradeoff that motivates these structures. A range sum on a plain array must scan the whole range, (top). A prefix-sum array answers that sum as one subtraction , but a single update to then dirties every later prefix , to repair (bottom). Each structure is on one operation and on the other.

The idea, in the spirit of the previous lessons on augmenting trees with subtree summaries,1 is to store partial sums over blocks so that any prefix is the sum of a few blocks and any single element lives in only a few blocks. Two classic structures realize this: the Fenwick tree, which is compact and exploits the binary representation of the index, and the segment tree, which is more general and handles any associative aggregate plus range updates.

Fenwick trees: indexing by the low bit

A Fenwick tree (or binary indexed tree) is a 1-indexed array where stores the sum of a contiguous block of ending at index . The length of that block is , the value of the lowest set bit of :

That is, covers the half-open range . This relies on two's-complement arithmetic: is flip every bit of , then add one. Trace it for in five bits:

Why does this always isolate the lowest set bit? Write as some prefix of bits, then the lowest , then a run of trailing zeros: . Flipping gives , and adding carries through the trailing ones and stops at the flipped : . Below the lowest set bit both numbers are all zeros; at it both have a ; above it every bit of is the complement of the corresponding bit of . The AND therefore keeps exactly one bit — the lowest set bit. Concretely: since , so covers indices ; since , so covers the whole prefix ; and for every odd , so odd entries cover just themselves.

Here is the whole structure for the running array . Each bracket is one Fenwick entry, storing the sum of the cells it spans:

The Fenwick tree for . Each covers the block of length ending at and stores that block's sum: the odd entries store single cells, and store pairs, stores the sum of , and stores the total. Every index is covered by exactly one bracket per "level," and each level halves the number of brackets.

Reading the brackets off into an array: . The entry ; the entry . Nothing else is stored — the structure is this one array.

Prefix sum. To compute we peel off blocks from the right. accounts for the topmost block ending at ; the rest of the prefix ends at , so we jump there and repeat, clearing one set bit each step until we reach .

Algorithm:PrefixSum(F,i)\textsc{PrefixSum}(F, i)return A[1]++A[i]A[1] + \dots + A[i]
  1. 1
    s0s \gets 0
  2. 2
    while i>0i > 0 do
  3. 3
    ss+F[i]s \gets s + F[i]
  4. 4
    iilowbit(i)i \gets i - \lowbit(i)
    clear the lowest set bit
  5. 5
    return ss

Trace it on the running array, , for :

step (binary)readrunning next
1
2
3

The loop stops at and returns ; a direct check gives . Each subtraction clears the lowest set bit of — and each cleared bit contributed one block: covers , covers , covers . The three blocks tile with no gaps and no overlaps.

That tiling is not an accident of . The binary expansion of any index is a sum of powers of two, and the walk peels those powers off from the smallest up. For a larger index like , the visit sequence is :

follows the set bits of . The walk reads three entries whose blocks — of length , of length , of length — tile the prefix exactly. One block per set bit, so at most reads.

Point update. When changes by , every whose block contains must change by . Repeatedly adding the low bit visits those indices and no others: starting at , each step moves to the next larger block that covers , until we run past .

Algorithm:Update(F,k,δ)\textsc{Update}(F, k, \delta) — add δ\delta to A[k]A[k]
  1. 1
    while knk \le n do
  2. 2
    F[k]F[k]+δF[k] \gets F[k] + \delta
  3. 3
    kk+lowbit(k)k \gets k + \lowbit(k)
    move to the next covering block

Trace — add to — on the running array:

step (binary)writenext
1
2
3, stop

Exactly the right entries changed: covers alone, covers , and covers — every block that contains index , and no other. covers only , so the walk correctly skips it. A follow-up now reads , as it must.

The two walks are mirror images on the same number line. climbs to larger indices by adding the low bit, visiting every block that owns the changed element; descends to smaller indices by subtracting it, peeling off the blocks that tile the prefix. The same hop drives both, in opposite directions.

The lowbit duality on . climbs by adding the low bit (top, accent); descends by clearing it (bottom, grey). Both walks ride the same hop in opposite directions.

A range sum is then two prefix queries:

so both update and range sum cost , with occupying a single array of words and no pointers.2 Building takes by a linear in-place pass described below, rather than separate updates.

The implicit tree

The array is a flattened forest, and seeing the tree explains both walks. Define . Under this map every index at most hangs below a power of two, and each node's block is the disjoint union of its own array cell and its children's blocks:

Check it at : the children of are (since ), (since ), and (since ), and indeed

The two operations are just the two natural walks in this forest. follows parent pointers from toward the root — the leaf-to-root path — which, by the correctness lemma, is the set of blocks containing . hops across the forest from one subtree root to the next one on its left; each hop discards a fully-counted subtree. Since always has a strictly larger low bit, no root-ward path is longer than the number of bit positions, — the tree is implicitly balanced, with no rotations, no pointers, and no bookkeeping beyond the index arithmetic itself.

The parent map also gives the construction promised above in one left-to-right pass: initialize , then for add into if that parent exists. By the time the loop reaches , every descendant of has already deposited its sum, so each entry is finished exactly when it is passed along — additions total, versus for separate calls to .

The one catch: this works because sums are invertible — we recover by subtracting two prefixes. For non-invertible aggregates like or , is meaningless, and we need a structure that queries an arbitrary range directly. That is the segment tree.

fenwick.pypython
class Fenwick:
  """
    1-indexed internally; the public API is 0-indexed over `size` slots.\n
  """

  def __init__(self, size: int) -> None:
    self.size: int = size
    self._tree: list[float] = [0.0 for _ in range(size + 1)]

  def add(self, index: int, delta: float) -> None:
    """
      Add `delta` to the element at `index` (0-indexed).\n
    """
    # walk to each slot whose partial sum covers `index`, climbing by low bit.
    position: int = index + 1
    while position <= self.size:
      self._tree[position] += delta
      position += position & (-position)

  def prefix_sum(self, count: int) -> float:
    """
      Sum of the first `count` elements — the range `[0, count)`.\n
    """
    # accumulate the partial sums covering [0, count), stepping down by low bit.
    position: int = min(count, self.size)
    total: float = 0.0
    while position > 0:
      total += self._tree[position]
      position -= position & (-position)
    return total

  def range_sum(self, low: int, high: int) -> float:
    """
      Sum of the elements in `[low, high)`.\n
    """
    return self.prefix_sum(high) - self.prefix_sum(low)
fenwick_2d.pypython
class Fenwick2D:
  """
    1-indexed internally; the public API is 0-indexed over `rows` x `columns`.\n
  """

  def __init__(self, rows: int, columns: int) -> None:
    self.rows: int = rows
    self.columns: int = columns
    self._tree: list[list[float]] = [
      [0.0 for _ in range(columns + 1)] for _ in range(rows + 1)
    ]

  def add(self, row: int, column: int, delta: float) -> None:
    """
      Add `delta` to the cell at `(row, column)` (both 0-indexed).\n
    """
    # walk both axes upward, adding delta to every covering rectangle.
    outer: int = row + 1
    while outer <= self.rows:
      inner: int = column + 1
      while inner <= self.columns:
        self._tree[outer][inner] += delta
        inner += inner & (-inner)
      outer += outer & (-outer)

  def prefix_sum(self, row_count: int, column_count: int) -> float:
    """
      Sum over the rectangle `[0, row_count) x [0, column_count)` — the\n
      cells above and to the left of the given corner.\n
    """
    # walk both axes downward, summing the partial rectangles along the way.
    outer: int = min(row_count, self.rows)
    total: float = 0.0
    while outer > 0:
      inner: int = min(column_count, self.columns)
      while inner > 0:
        total += self._tree[outer][inner]
        inner -= inner & (-inner)
      outer -= outer & (-outer)

    return total

  def range_sum(
    self,
    low_row: int,
    low_column: int,
    high_row: int,
    high_column: int,
  ) -> float:
    """
      Sum over the half-open rectangle\n
      `[low_row, high_row) x [low_column, high_column)`,\n
      by inclusion-exclusion of four corner prefix sums.\n
    """
    return (
      self.prefix_sum(high_row, high_column)
      - self.prefix_sum(low_row, high_column)
      - self.prefix_sum(high_row, low_column)
      + self.prefix_sum(low_row, low_column)
    )

Segment trees: a balanced tree of canonical ranges

A segment tree over is a balanced binary tree whose leaves are the array entries and whose every internal node stores the aggregate of the contiguous range its subtree spans. The root covers ; a node covering with splits at into children covering and . The stored aggregate can be sum, , , or : any associative operation (formally, any monoid), since a node's value is its two children's values combined.

A segment tree over 8 elements; the canonical nodes covering are highlighted

Query. To aggregate we descend from the root. At a node covering : if lies entirely inside we return its stored value without recursing (a canonical node); if it is disjoint from we return the monoid identity; otherwise we recurse into both children and combine. The query range decomposes into canonical nodes, at most two per level of the tree, so a range query costs . In the figure, is covered by the three shaded nodes , , , and their union is exactly .

A worked query

Build the tree over the running array bottom-up: the leaves take 's values, and each internal node sums its children — , , , , then and , and the root . Now run and record every node the recursion touches:

nodevaluerelation to action
straddlesrecurse into both children
straddlesrecurse into both children
straddlesrecurse into both children
disjointreturn
insidereturn (canonical)
insidereturn (canonical)
straddlesrecurse into both children
insidereturn (canonical)
disjointreturn

The answer is , and directly: . The recursion visited of the tree's nodes; on a larger tree the proportion collapses, since only the two root-to-endpoint paths are ever explored.

on the tree for , values shown. Solid accent nodes are the canonical decomposition (); dashed accent nodes were visited but straddle or miss the range — the straddling ones recurse, the disjoint ones ( at index , the covering ) return the identity . Plain nodes are never touched.

Point update. To change , update the corresponding leaf and walk back up to the root, recomputing each ancestor as the combination of its (now-updated) children — one node per level, work. Setting (it was ) in the tree above rewrites exactly one root-to-leaf path: the leaf becomes , then , then , then the root . Four writes, no other node consulted. Building the tree bottom-up visits each of the nodes once, so construction is , and the tree needs at most (commonly allocated as ) nodes, roughly to a Fenwick tree's memory.

segment_tree.pypython
from collections.abc import Callable, Sequence
from typing import Generic, TypeVar

Value = TypeVar("Value")

def _add(left: float, right: float) -> float:
  """
    Default combine operation: numeric addition (range-sum trees).\n
  """
  return left + right

class SegmentTree(Generic[Value]):
  """
    A range-aggregate tree; defaults to range-sum over numbers.\n
  """

  def __init__(
    self,
    data: Sequence[Value],
    combine: Callable[[Value, Value], Value] = _add,
    identity: Value = 0,
  ) -> None:
    """
      Build the tree over `data` for the aggregate `combine`, whose\n
      neutral element is `identity` (0 for sum, +inf for min, …).\n
    """
    # store the aggregate and its identity for later queries.
    self.size: int = len(data)
    self._combine: Callable[[Value, Value], Value] = combine
    self._identity: Value = identity

    # leaves live in the back half; internal nodes start as identity.
    self._tree: list[Value] = [identity for _ in range(2 * self.size)]
    self._tree[self.size:] = list(data)

    # fill internal nodes bottom-up, each the combine of its two children.
    for parent in range(self.size - 1, 0, -1):
      self._tree[parent] = combine(
        self._tree[2 * parent],
        self._tree[2 * parent + 1],
      )

  def update(self, index: int, value: Value) -> None:
    """
      Set the element at `index` to `value`, refreshing its ancestors.\n
    """
    # write the new value at its leaf.
    position: int = index + self.size
    self._tree[position] = value

    # walk to the root, recombining each ancestor from its children.
    position //= 2
    while position >= 1:
      self._tree[position] = self._combine(
        self._tree[2 * position],
        self._tree[2 * position + 1],
      )
      position //= 2

  def query(self, low: int, high: int) -> Value:
    """
      Aggregate over the half-open range `[low, high)`.\n
    """
    # start at the leaf bounds of the range.
    result: Value = self._identity
    left: int = low + self.size
    right: int = high + self.size

    # climb both ends inward, folding in any node that falls outside its parent.
    while left < right:
      if left & 1:
        result = self._combine(result, self._tree[left])
        left += 1
      if right & 1:
        right -= 1
        result = self._combine(result, self._tree[right])
      left //= 2
      right //= 2

    return result

Lazy propagation: range updates in

A point-update segment tree still pays to add a value to a whole range element by element. Lazy propagation fixes this. When an update applies to a range that exactly covers a node's interval, we apply it to that node's aggregate and stash a pending tag on the node instead of recursing into its children. The tag is pushed down to the children only later, lazily, when a subsequent query or update actually needs to enter that subtree.

Lazy push-down. A pending tag on is applied to that node's aggregate; only when a query descends does the tag flow to the children and

A worked range update

Run on the original tree for . The range decomposes into the canonical nodes and — the same decomposition a query would compute. At each canonical node we apply the update to the stored sum in (a over a node covering cells adds ) and record the tag:

  • : sum , tag ;
  • : sum , tag ;
  • on the way back up, recompute the ancestors: and the root .

Six nodes touched in total; the ten nodes below the two tags still hold their old sums. They are stale, but harmlessly so — the tags above them record the correction, and no read can reach a stale node without first passing a tag.

Snapshot after . The two canonical nodes absorb the update eagerly (sum ) and hold a tag; their ancestors are recomputed on the way out. Everything beneath a tag (grey) is stale, and stays stale until a later descent pushes the tag down.

Now query against this state. The recursion enters the root and must descend past the tagged node , because straddles . Before recursing, it pushes the tag down: gets sum and tag ; gets sum and tag ; the tag on is cleared. The query then proceeds normally — is inside and returns ; on the right, is inside and returns without touching the tag below it. The answer is , which checks out against the updated array : .

Two details make the scheme correct in general. First, tags must compose: two pending and tags on the same node collapse to , so a node never holds more than one tag. Second, a node's stored aggregate is always correct for its own subtree assuming all tags strictly above it have been applied — that is the invariant the push-down preserves, and it is what lets a canonical node answer a query without any descent.

With lazy tags both range update and range query run in . This is the segment tree's decisive advantage over the Fenwick tree: it supports non-invertible aggregates (, ) and whole-range modifications, at the cost of more memory and a more involved implementation.3

lazy_segment_tree.pypython
from collections.abc import Sequence
from typing import Optional

class SegmentNode:
  """
    One node of the tree: the half-open-free, inclusive interval `[low, high]`\n
    it spans, the sum over that interval, a pending `lazy` add not yet pushed\n
    to its children, and links to the left and right child nodes.\n
    A node is a leaf exactly when `low == high`.\n
  """

  def __init__(self, low: int, high: int) -> None:
    self.low: int = low
    self.high: int = high
    self.total: int = 0
    self.lazy: int = 0
    self.left: Optional[SegmentNode] = None
    self.right: Optional[SegmentNode] = None

  @property
  def is_leaf(self) -> bool:
    """
      Whether this node covers a single array element.\n
    """
    return self.low == self.high

  def __repr__(self) -> str:
    return f"SegmentNode([{self.low}, {self.high}], total={self.total})"

class LazySegmentTree:
  """
    A range-add / range-sum tree over a fixed-length integer array.\n
    `update(low, high, delta)` adds `delta` to every element in the\n
    inclusive range `[low, high]`; `query(low, high)` returns their sum.\n
  """

  def __init__(self, data: Sequence[int]) -> None:
    """
      Build the tree over a copy of `data` in O(n).\n
      An empty sequence yields a tree whose every query returns 0.\n
    """
    self.size: int = len(data)
    self.root: Optional[SegmentNode] = (
      self._build(0, self.size - 1, data) if self.size > 0 else None
    )

  def _build(
    self,
    low: int,
    high: int,
    data: Sequence[int],
  ) -> SegmentNode:
    """
      Recursively construct the subtree spanning `[low, high]`, storing the\n
      true sum at every node bottom-up.\n
    """
    # a leaf spans one element and holds it directly.
    node = SegmentNode(low, high)
    if low == high:
      node.total = data[low]
      return node

    # split at the midpoint and build both halves.
    middle: int = (low + high) // 2
    node.left = self._build(low, middle, data)
    node.right = self._build(middle + 1, high, data)

    # carry the children's sums up to this node.
    node.total = node.left.total + node.right.total
    return node

  def _apply(self, node: SegmentNode, delta: int) -> None:
    """
      Add `delta` to every element under `node`: bump its sum by the count\n
      of elements it spans and stash the per-element tag for its children.\n
    """
    span: int = node.high - node.low + 1
    node.total += delta * span
    node.lazy += delta

  def _push_down(self, node: SegmentNode) -> None:
    """
      Flush a pending tag to both children, honoring the promise just in\n
      time, the moment a query or update needs to descend past `node`.\n
    """
    if node.lazy != 0 and node.left is not None and node.right is not None:
      self._apply(node.left, node.lazy)
      self._apply(node.right, node.lazy)
      node.lazy = 0

  def update(self, low: int, high: int, delta: int) -> None:
    """
      Add `delta` to every element in the inclusive range `[low, high]`.\n
    """
    if self.root is not None and low <= high:
      self._update(self.root, low, high, delta)

  def _update(
    self,
    node: SegmentNode,
    low: int,
    high: int,
    delta: int,
  ) -> None:
    """
      Apply the range add at `node`, tagging it whole when its interval\n
      lies inside `[low, high]` and otherwise recursing into the children\n
      whose intervals overlap the update range.\n
    """
    # disjoint: nothing under this node is touched.
    if high < node.low or node.high < low:
      return

    # canonical node: tag it whole instead of descending.
    if low <= node.low and node.high <= high:
      self._apply(node, delta)
      return

    # partial overlap: flush the tag, then recurse into both children.
    self._push_down(node)
    assert node.left is not None and node.right is not None
    self._update(node.left, low, high, delta)
    self._update(node.right, low, high, delta)

    # recompute this node's sum from the updated children.
    node.total = node.left.total + node.right.total

  def query(self, low: int, high: int) -> int:
    """
      Sum of the elements in the inclusive range `[low, high]`.\n
      An empty or out-of-order range sums to 0.\n
    """
    if self.root is None or low > high:
      return 0
    return self._query(self.root, low, high)

  def _query(self, node: SegmentNode, low: int, high: int) -> int:
    """
      Aggregate the canonical nodes covering `[low, high]`, pushing pending\n
      tags down as the recursion enters each subtree.\n
    """
    # disjoint contributes nothing; a canonical node contributes its sum.
    if high < node.low or node.high < low:
      return 0
    if low <= node.low and node.high <= high:
      return node.total

    # partial overlap: flush the tag, then sum both children.
    self._push_down(node)
    assert node.left is not None and node.right is not None
    return (
      self._query(node.left, low, high)
      + self._query(node.right, low, high)
    )

Choosing between them

Both give point-update and range-query; the choice is about generality versus footprint.

  • Fenwick tree. Pick it when the aggregate is an invertible group operation (sum, xor) and you only need point updates. It is a single array, cache-friendly, a dozen lines of code, and the constant factors are tiny. Range sum is .
  • Segment tree. Pick it when you need or any non-invertible aggregate, or range updates via lazy propagation. It is strictly more general, and you pay for it with to the memory and a more involved implementation.

One extension stretches the Fenwick tree further than it first appears. To support range update + point query for sums, keep a Fenwick tree over the difference array : adding to becomes two point updates (, ), and reading becomes . With a second Fenwick tree tracking a correction term, even range update + range sum works. What no Fenwick variant recovers is a non-invertible aggregate — a range cannot be assembled from prefix information, because has no inverse to subtract with.

workloadstructure
point update, range sum / xorFenwick tree
range add, point readFenwick tree over the difference array
point update, range segment tree
range update, range querysegment tree with lazy propagation

In short: Fenwick is the specialist, the segment tree the generalist. For range-sum-query-mutable, a Fenwick tree suffices; when the skyline or a range-assign problem demands over a mutable range, use the segment tree with lazy propagation.

The segment tree's larger family

Neither structure is in the classic textbooks; they come from competitive programming and the systems literature, and both extend into a large family of range-query structures.

Persistence and offline queries. Because a point update touches only the nodes on one root-to-leaf path, a segment tree is naturally made persistent by path-copying (the same trick that persists a balanced BST): each update spawns a new version in extra space, and old versions stay queryable. A persistent segment tree answers offline questions like "the -th smallest value in the subarray " by querying the difference of two versions, a standard tool for range-rank queries.

When per side isn't enough. For simpler needs, sqrt decomposition splits the array into blocks and answers range queries in with almost no code, sometimes the pragmatic choice for non-associative or awkward aggregates. At the other extreme, segment tree beats (Ji Ruyi's technique) supports range operations like "clamp every element to at most " in amortized , which no lazy tag alone can do, by storing the two largest distinct values per node and pruning branches where the update is a no-op.

Higher dimensions and richer keys. The 2-D Fenwick tree in this lesson generalizes to a Fenwick tree of Fenwick trees for rectangle sums, and a merge-sort tree (a segment tree whose nodes store sorted subarrays) answers "how many values in are " in . The common thread: any aggregate you can compute from two children in slots into the segment tree's divide-and-combine skeleton.4

Takeaways

  • A static prefix-sum array answers range sums in but updates in ; a plain array updates in but sums in . Fenwick and segment trees achieve both in .
  • A Fenwick tree is a 1-indexed array where holds the sum of the block , with . Prefix sum walks down clearing low bits; update walks up adding low bits, each .
  • Fenwick range sum relies on invertibility: . It fails for .
  • A segment tree stores each node's range aggregate (any associative op). Build , point update , and range query by decomposing into canonical nodes.
  • Lazy propagation defers a range update by tagging canonical nodes and pushing tags down only when needed, giving range update + range query.
  • Fenwick = tiny, fast, sum-like invertible aggregates; segment tree = general and lazy range ops, at to the memory.

Footnotes

  1. CLRS, Ch. 14, Augmenting Data Structures (§14.2): attach summary fields to nodes and maintain them through updates, the general method both structures specialize.
  2. Skiena, §3.x, Range Queries / Augmented Structures: the binary indexed tree as a minimal-overhead structure for dynamic prefix sums.
  3. Erickson, Ch., Data Structures: segment trees over canonical ranges, range decomposition, and lazy propagation for range updates.
  4. Fenwick, A new data structure for cumulative frequency tables (1994), the binary indexed tree; the persistent segment tree, sqrt decomposition, segment-tree-beats, and merge-sort-tree techniques are standard in the competitive-programming literature (e.g. the CP-Algorithms references).
Practice

╌╌ END ╌╌