Go Guide

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
  1. 01Choosing a structure
  2. 02Fenwick tree (binary indexed tree)
  3. 03Segment tree
  4. 04Sparse table: O(1) queries on static data
  5. 05Binary lifting: jumping up a tree
  6. 06Recognizing the pattern
  7. 07Summary
  8. 08Check your understanding
  9. 09Practice in Go

Choosing a structure#

StructureUpdateQuerySupportsCode size
prefix sumsO(n)O(1)sums (and anything invertible)tiny
Fenwick treeO(log n)O(log n)prefix sums, counts~15 lines
segment treeO(log n)O(log n)sum, min, max, gcd, any associative operation; range updates with lazy propagation~40 lines
sparse tablenone (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:

ibinaryi & -itree[i] covers
100011[1, 1]
200102[1, 2]
300111[3, 3]
401004[1, 4]
501011[5, 5]
601102[5, 6]
701111[7, 7]
810008[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.

fenwick.go
Go
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}))
}
Output
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:

segtree.go
Go
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)
}
Output
5 2 2 2
after a[4] = 9: 2 3
all 36 ranges match brute force: true

The 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 min
sparse.go
Go
package 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))
}
Output
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 1

Binary 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.

lca.go
Go
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])
	}
}
Output
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 1

Recognizing 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.

  1. Q1.Prefix sums answer range sums in O(1). Why do we need Fenwick or segment trees?

  2. Q2.In a Fenwick tree, i & -i gives:

  3. Q3.A segment tree query visits how many nodes per level, at most?

  4. Q4.A sparse table answers range minimum in O(1) because:

  5. 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.

Finished this lesson?

Mark it done to track your progress. It is saved in this browser only.