Skip to main content

Segment Tree

A segment tree is a binary tree data structure that allows answering range queries and updating values in an array in logarithmic time. Each leaf node of the segment tree represents a single element of the array, and each non-leaf node represents a segment (or range) of the array, covering the elements of its children.

Visualization



Why Segment Trees

Consider a scenario where we have an array of numbers, and we want to frequently update its values and also compute the sum of a range of elements. One could iterate through the range and calculate the sum. The array update will be O(1) and the range query will be O(n), but this would be inefficient with frequent queries. Segment trees make these operations efficient, performing both tasks in O(log n) time.

Anatomy of the Segment Tree

  • Leaves: The leaves of the tree represent individual elements of the input array.
  • Internal Nodes: Every internal node represents some segment of the array. For instance, if the left and right children of a node represent the ranges [a, b] and [c, d] respectively, then the parent node represents the range [a, d].

Operations

We can use linked-list tree data structure to store the binary tree but it will use significant overhead on each element for the left and right pointer. Instead, we can use array-based tree which allows easy access to left child and right child from any node using simple arithmetic. For example, if we store the root at index 1, then given any node at position index, we can access its children:

  • Left child: index * 2
  • Right child: index * 2 + 1

The number of nodes needed in this case 2 * 2^ceil(log2(n)) - 1, since we store root at 1, so the array length needed is 2 * 2^ceil(log2(n)) (need to check the visualization and implementation to see this)

Building the Tree O(n)

function build(tree: number[], node: number, a: number[], start: number, end: number): number {
if (start === end) {
tree[node] = a[start]
} else {
const mid = Math.floor((start + end) / 2)
const left = build(tree, node * 2, a, start, mid)
const right = build(tree, node * 2 + 1, a, mid + 1, end)

tree[node] = left + right
}

return tree[node]
}

Querying O(log n)

function query(tree: number[], node: number, start: number, end: number, l: number, r: number): number {
// query range out of the current segment
if (l > end || r < start) {
return 0
}

// query range includes the whole current segment
if (l <= start && r >= end) {
return tree[node]
}

const mid = Math.floor((start + end) / 2)

// start query on left and right
const rs = query(tree, node * 2, start, mid, l, r) + query(tree, node * 2, mid + 1, end, l, r)

return rs
}

Updating O(log n)

function update(tree: number[], node: number, start: number, end: number, ind: number, value: number): number {
if (start === end && start === ind) {
tree[node] = value
} else {
const mid = Math.floor((start + end) / 2)

if (ind >= start && ind <= mid) {
update(tree, node*2, start, mid, ind, value)
} else if (ind >= mid + 1 && ind <= end) {
update(tree, node*2 + 1, mid+1, end, ind, value)
}

tree[node] = tree[node*2] + tree[node*2 + 1]
}

return tree[node]
}

Using the above

export class SegmentTree {
tree: number[] = []
len: number

constructor(a: number[]) {
this.len = a.length
build(this.tree, 1, a, 0, a.length - 1)
}

update(ind: number, val: number) {
update(this.tree, 1, 0, this.len - 1, ind, val)
}

query(l: number, r: number): number {
return query(this.tree, 1, 0, this.len - 1, l, r)
}
}

Applications

Segment trees can be used for:

  • Sum of a given range.
  • Finding the minimum/maximum of a range.
  • GCD of a range.
  • Lazy propagation (an optimization for range updates).
  • And more!