Trees & graphs · Lesson 30 of 62 · about 60 min
Range queries: Fenwick & segment trees
Answer range sums and minimums while the data changes: Fenwick trees, segment trees, sparse tables for static data, and binary lifting for ancestors in a tree.
On this page · 9 sections
Choosing a structure#
| Structure | Update | Query | Supports | Code size |
|---|---|---|---|---|
| prefix sums | O(n) | O(1) | sums (and anything invertible) | tiny |
| Fenwick tree | O(log n) | O(log n) | prefix sums, counts | ~15 lines |
| segment tree | O(log n) | O(log n) | sum, min, max, gcd, any associative operation; range updates with lazy propagation | ~40 lines |
| sparse table | none (static) | O(1) | min, max, gcd (idempotent) | ~20 lines |
Fenwick tree (binary indexed tree)#
A Fenwick tree is an array tree[1..n] where tree[i] stores the sum of a block of elements ending at i, and the block's length is the lowest set bit of i, i & -i:
| i | binary | i & -i | tree[i] covers |
|---|---|---|---|
| 1 | 0001 | 1 | [1, 1] |
| 2 | 0010 | 2 | [1, 2] |
| 3 | 0011 | 1 | [3, 3] |
| 4 | 0100 | 4 | [1, 4] |
| 5 | 0101 | 1 | [5, 5] |
| 6 | 0110 | 2 | [5, 6] |
| 7 | 0111 | 1 | [7, 7] |
| 8 | 1000 | 8 | [1, 8] |
- Prefix sum up to i: add
tree[i], then remove the lowest bit (i -= i & -i) and repeat. Prefix(7) = tree[7] + tree[6] + tree[4]: blocks [7], [5,6], [1,4]. - Add to element i: update
tree[i], then add the lowest bit (i += i & -i) to reach the next block that also contains i: 5 → 6 → 8.
Both loops strip or add one bit per step, so they take at most log n steps.
package main
import (
"fmt"
"slices"
)
// Fenwick supports point updates and prefix sums in O(log n). Indexes are 0-based outside, 1-based inside.
type Fenwick struct{ tree []int }
func NewFenwick(n int) *Fenwick { return &Fenwick{make([]int, n+1)} }
func (f *Fenwick) Add(i, delta int) {
for i++; i < len(f.tree); i += i & -i {
f.tree[i] += delta
}
}
// Prefix returns the sum of elements [0, i).
func (f *Fenwick) Prefix(i int) int {
s := 0
for ; i > 0; i -= i & -i {
s += f.tree[i]
}
return s
}
func (f *Fenwick) Range(l, r int) int { return f.Prefix(r+1) - f.Prefix(l) } // inclusive
// countSmallerAfter: for each element, how many smaller elements are to its right?
func countSmallerAfter(nums []int) []int {
// Coordinate compression: map values to ranks 0..m-1 so the tree stays small.
sorted := slices.Clone(nums)
slices.Sort(sorted)
sorted = slices.Compact(sorted)
f := NewFenwick(len(sorted))
out := make([]int, len(nums))
for i := len(nums) - 1; i >= 0; i-- {
rank, _ := slices.BinarySearch(sorted, nums[i])
out[i] = f.Prefix(rank) // how many seen values have a smaller rank
f.Add(rank, 1)
}
return out
}
func main() {
nums := []int{5, 8, 6, 3, 2, 7, 2, 6}
f := NewFenwick(len(nums))
for i, v := range nums {
f.Add(i, v)
}
fmt.Println("sum[2..6] =", f.Range(2, 6))
f.Add(4, 10) // nums[4] += 10
fmt.Println("after nums[4] += 10, sum[2..6] =", f.Range(2, 6))
fmt.Println("internal tree:", f.tree[1:])
fmt.Println("smaller after self:", countSmallerAfter([]int{5, 2, 6, 1}))
}sum[2..6] = 20
after nums[4] += 10, sum[2..6] = 30
internal tree: [5 13 6 22 12 19 2 49]
smaller after self: [2 1 1 0]countSmallerAfter is a classic "hard" problem that becomes ten lines with a Fenwick tree: walk from the right, and for each value ask how many already-seen values are smaller. Coordinate compression (replacing values by their rank among the distinct values) keeps the tree as small as the number of distinct values, even if values are up to 10⁹.
Segment tree#
A segment tree is a binary tree where every node stores the answer for a range: the root covers everything, its children cover the two halves, and so on down to single elements. It works for any associative operation: sum, min, max, gcd, even "the maximum subarray sum in this range" if a node stores a few values.
- Query [l, r]: if a node's range is completely inside, use its value; completely outside, ignore it; partly overlapping, ask both children. At most four nodes per level are visited, so O(log n).
- Update one element: change the leaf and recompute its ancestors: O(log n).
Step through a query. Notice how whole subtrees are used or skipped without going down to the leaves:
The compact iterative version stores the tree in a slice of size 2n: leaves at n..2n-1, and node i's children at 2i and 2i+1. Here it computes range minimums:
package main
import (
"fmt"
"math"
)
// MinTree answers range-minimum queries with point updates.
type MinTree struct {
n int
t []int
}
func NewMinTree(a []int) *MinTree {
n := len(a)
t := make([]int, 2*n)
copy(t[n:], a) // leaves
for i := n - 1; i > 0; i-- {
t[i] = min(t[2*i], t[2*i+1]) // each parent combines its two children
}
return &MinTree{n, t}
}
func (m *MinTree) Set(i, v int) {
i += m.n
m.t[i] = v
for i /= 2; i > 0; i /= 2 {
m.t[i] = min(m.t[2*i], m.t[2*i+1])
}
}
// Min returns the minimum of a[l..r], inclusive.
func (m *MinTree) Min(l, r int) int {
res := math.MaxInt
for l, r = l+m.n, r+m.n+1; l < r; l, r = l/2, r/2 {
if l&1 == 1 { // l is a right child: take it alone, then move right
res = min(res, m.t[l])
l++
}
if r&1 == 1 { // r is exclusive; if it is a right child, its left sibling is inside
r--
res = min(res, m.t[r])
}
}
return res
}
func main() {
a := []int{5, 8, 6, 3, 2, 7, 2, 6}
m := NewMinTree(a)
fmt.Println(m.Min(0, 2), m.Min(2, 6), m.Min(5, 7), m.Min(0, 7))
m.Set(4, 9) // a[4] = 9
fmt.Println("after a[4] = 9:", m.Min(2, 6), m.Min(3, 4))
// Check against a brute-force scan on every range.
ok := true
for l := range a {
for r := l; r < len(a); r++ {
want := math.MaxInt
for _, v := range []int{5, 8, 6, 3, 9, 7, 2, 6}[l : r+1] {
want = min(want, v)
}
ok = ok m.Min(l, r) == want
}
}
fmt.Println("all 36 ranges match brute force:", ok)
}5 2 2 2
after a[4] = 9: 2 3
all 36 ranges match brute force: trueThe final loop is worth copying into your own tests: comparing a clever structure against a brute-force version on every small input catches nearly all off-by-one bugs.
Range updates: lazy propagation
"Add 5 to every element in [l, r]" would touch O(n) leaves. Lazy propagation stores the pending addition on the highest nodes that fully cover the range and pushes it down to children only when a later query or update needs to go below them. Both range updates and range queries stay O(log n). Reach for it when a problem mixes range updates with range queries (booking systems, interval coverage).
Sparse table: O(1) queries on static data#
If the array never changes and the operation is idempotent (applying it twice to the same element changes nothing: min, max, gcd, bitwise AND/OR), precompute sp[k][i] = the answer for the block of length 2^k starting at i. Any range [l, r] is covered by two blocks of the same power-of-two length that overlap in the middle:
range [2, 8], length 7, k = 2 (block length 4):
block 1: [2, 5] block 2: [5, 8] overlap at 5 is harmless for minpackage main
import (
"fmt"
"math/bits"
)
type Sparse struct{ sp [][]int }
func NewSparse(a []int) *Sparse {
levels := bits.Len(uint(len(a)))
sp := make([][]int, levels)
sp[0] = append([]int(nil), a...)
for k := 1; k < levels; k++ {
half := 1 << (k - 1)
sp[k] = make([]int, len(a)-(1<<k)+1)
for i := range sp[k] {
sp[k][i] = min(sp[k-1][i], sp[k-1][i+half]) // two halves of length 2^(k-1)
}
}
return &Sparse{sp}
}
// Min of a[l..r] in O(1): two overlapping blocks of length 2^k.
func (s *Sparse) Min(l, r int) int {
k := bits.Len(uint(r-l+1)) - 1
return min(s.sp[k][l], s.sp[k][r-(1<<k)+1])
}
func main() {
s := NewSparse([]int{5, 8, 6, 3, 2, 7, 2, 6, 4, 1})
for k, row := range s.sp {
fmt.Printf("blocks of %d: %v\n", 1<<k, row)
}
fmt.Println(s.Min(2, 8), s.Min(0, 2), s.Min(7, 9))
}blocks of 1: [5 8 6 3 2 7 2 6 4 1]
blocks of 2: [5 6 3 2 2 2 2 4 1]
blocks of 4: [3 2 2 2 2 2 1]
blocks of 8: [2 2 1]
2 5 1Binary lifting: jumping up a tree#
The same "powers of two" idea works on trees. Store up[k][v], the 2^k-th ancestor of v. Since up[k][v] = up[k-1][ up[k-1][v] ], the whole table takes O(n log n) to build. Then:
- k-th ancestor: one jump per set bit of k. The 13th ancestor is 8 + 4 + 1 jumps.
- Lowest common ancestor (LCA) of u and v: lift the deeper node to the same depth, then, from the largest jump down, jump both nodes together whenever their ancestors differ. They end up just below the LCA.
LCA answers "distance between two nodes in a tree" (depth[u] + depth[v] - 2·depth[lca]), which shows up in network routing, file-system paths and phylogenetic trees.
package main
import "fmt"
const LOG = 4 // enough for trees with up to 16 levels
func main() {
// 0
// / \
// 1 2
// / \ \
// 3 4 5
// / / \
// 6 7 8
parent := []int{0, 0, 0, 1, 1, 2, 3, 5, 5} // the root is its own parent
n := len(parent)
depth := make([]int, n)
for v := 1; v < n; v++ {
depth[v] = depth[parent[v]] + 1 // parents come before children here
}
var up [LOG][]int
up[0] = parent
for k := 1; k < LOG; k++ {
up[k] = make([]int, n)
for v := range n {
up[k][v] = up[k-1][up[k-1][v]] // two jumps of 2^(k-1)
}
}
kth := func(v, k int) int {
for j := 0; j < LOG; j++ {
if k>>j&1 == 1 {
v = up[j][v]
}
}
return v
}
lca := func(u, v int) int {
if depth[u] < depth[v] {
u, v = v, u
}
u = kth(u, depth[u]-depth[v]) // same depth
if u == v {
return u
}
for j := LOG - 1; j >= 0; j-- {
if up[j][u] != up[j][v] {
u, v = up[j][u], up[j][v]
}
}
return parent[u]
}
fmt.Println("2nd ancestor of 6:", kth(6, 2), " 3rd ancestor of 7:", kth(7, 3))
for _, q := range [][2]int{{6, 4}, {6, 7}, {7, 8}, {3, 6}} {
a := lca(q[0], q[1])
fmt.Printf("lca(%d,%d) = %d, distance %d\n", q[0], q[1], a, depth[q[0]]+depth[q[1]]-2*depth[a])
}
}2nd ancestor of 6: 1 3rd ancestor of 7: 0
lca(6,4) = 1, distance 3
lca(6,7) = 0, distance 6
lca(7,8) = 5, distance 2
lca(3,6) = 3, distance 1Recognizing the pattern#
- Many range queries and updates: Fenwick (sums, counts) or segment tree (anything associative).
- "How many elements to the left/right are smaller/greater?": Fenwick tree over compressed values.
- Range updates plus range queries: segment tree with lazy propagation.
- Static array, many min/max queries: sparse table.
- Ancestors, distances or LCA in a tree with many queries: binary lifting.
Summary#
- Prefix sums are perfect for static data; changing data needs O(log n) structures.
- Fenwick trees store blocks sized by the lowest set bit; queries strip bits, updates add them.
- Segment trees store an answer per range and combine O(log n) nodes per query; lazy propagation adds range updates.
- Sparse tables answer idempotent queries in O(1) with two overlapping power-of-two blocks.
- Binary lifting jumps 2^k ancestors at a time for k-th ancestor and LCA in O(log n).
Check your understanding
Answer before scrolling back up. If you miss one, reread the matching section, then try again.
Q1.Prefix sums answer range sums in O(1). Why do we need Fenwick or segment trees?
Q2.In a Fenwick tree,
i & -igives:Q3.A segment tree query visits how many nodes per level, at most?
Q4.A sparse table answers range minimum in O(1) because:
Q5.Binary lifting stores up[k][v] = the 2^k-th ancestor of v. Finding the 13th ancestor takes:
Practice in Go
Write a table test first, then the solution. On LeetCode, choose Go as the language. Open a hint only after 15 minutes stuck.
Show hint
Fenwick tree: Update applies the difference; SumRange = prefix(r+1) - prefix(l).
Show hint
Walk from the right; a Fenwick tree over (compressed) values counts how many smaller ones you have seen.
Show hint
A Fenwick tree of Fenwick trees: loop i and j with the same lowbit steps.
Show hint
Difference array over sorted times, or a segment tree with range add and max.
Show hint
Binary lifting: one jump per set bit of k.
Show hint
For one query, recursion is enough. For many queries, binary lifting.
Finished this lesson?
Mark it done to track your progress. It is saved in this browser only.