Handling Ranges Efficiently
Problem: Given an array nums, we want to repeatedly perform two operations:
- Update: Change
nums[i]toval(Point Update) or change a range of values (Range Update). - Query: Find the aggregate (sum, min, max) of the subarray
nums[L...R].
The Real-World Scenario: A Real-Time Sales Dashboard
Imagine you are building a real-time sales dashboard for a global retail chain. The chain has thousands of stores.
- Every time a sale is made, a store’s total revenue updates (Update).
- The CEO frequently checks the total revenue across a specific range of stores, like “Stores 100 to 500 in the Midwest region” (Query).
If you use a simple Array:
- Update:
nums[i] += sale. This takes O(1). - Query: Loop from
LtoRto sum values. This takes O(N). When the CEO hits “Refresh”, the system iterates through thousands of stores, causing the dashboard to lag.
If you use a Prefix Sum Array:
- Query:
prefix[R] - prefix[L-1]. This takes O(1). - Update: If store 0 makes a sale, you must update the prefix sum for every subsequent store. This takes O(N). Now, the dashboard loads instantly, but every single sale across the globe freezes the database.
The Solution: We need a balanced data structure that can perform both operations in O(log N) time. Enter the Segment Tree and Fenwick Tree.
1. Segment Tree: The Hierarchy of Intervals
A Segment Tree is a full binary tree where each node represents an interval [L, R] and stores the aggregate value (e.g., sum) for that interval.
- Root: Covers the entire array
[0, N-1]. - Leaves: Cover a single element
[i, i]. - Internal Nodes: Cover the merged range of their two children. If a node covers
[L, R], its left child covers[L, Mid]and its right child covers[Mid+1, R].
Analogy: Think of it as a corporate hierarchy. The leaves are the individual stores. The parent nodes are district managers summarizing their stores’ sales. The higher you go, the more stores are summarized, up to the CEO (the root node) who sees the global total. When a store makes a sale, only their direct managers up the chain need to update their summaries—a logarithmic number of updates!
Interactive: Range Sum Visualizer
Update values and query ranges to see how the segment tree aggregates data dynamically.
2. Segment Tree Implementation
The most common way to implement a Segment Tree is using a flattened 1D array (similar to a Binary Heap).
- If a node is at index
i, its left child is at2 * iand its right child is at2 * i + 1. - To safely accommodate all nodes, the size of the array is
4 * N(whereNis the size of the original array).
Complete Code (Point Updates)
3. Advanced: Lazy Propagation (Range Updates)
What if instead of updating a single index, we need to update an entire range at once? E.g., nums[L...R] += val.
Updating N elements one-by-one using the point update method would take O(N log N).
Lazy Propagation solves this by deferring updates to descendants. Instead of pushing the update all the way down to the leaves immediately, we mark the current node with a “lazy” tag and return. The update is only pushed down (“propagated”) when a subsequent query or update actually needs to visit those descendant nodes.
- Time Complexity for Range Update: O(log N)
- Space Complexity: O(4N) for tree + O(4N) for lazy array
4. Fenwick Tree (Binary Indexed Tree)
While Segment Trees are incredibly flexible (supporting max, min, sum, multiplication), they are bulky, requiring O(4N) space and recursively traversing the tree.
Fenwick Trees provide an ultra-compact, bitwise alternative for scenarios where you specifically need Prefix Sums.
- Space Complexity: O(N). It maps directly onto an array of size
N + 1. - Time Complexity: O(log N) for Update and Query.
- Limitation: It is strictly designed for commutative and invertible operations like Sum or Multiplication. Range Max/Min is extremely difficult to implement effectively.
The Bitwise Magic
A Fenwick Tree relies on the lowest set bit of an index to define intervals. The lowest set bit can be extracted using i & (-i) (due to Two’s Complement representation).
- Query(index): To get the prefix sum up to
index, you sum the values while iteratively removing the lowest set bit:index -= index & (-index). - Update(index, delta): To add
deltato an element, you update its node and all “parents” that encompass it by continually adding the lowest set bit:index += index & (-index).
Fenwick Tree Implementation
Summary: Segment Tree vs Fenwick Tree
| Feature | Segment Tree | Fenwick Tree |
|---|---|---|
| Space Complexity | O(4 * N) |
O(N) |
| Code Length | Longer, recursive | Extremely short, iterative |
| Flexibility | High (Sum, Min, Max, Custom Objects) | Low (Primarily Prefix Sums) |
| Range Updates | Yes (via Lazy Propagation) | Complex (requires 2 BITs for Range Update + Range Query) |
| Best Used For | Complex aggregate queries, Range max/min | Competitive programming speed, Memory-constrained environments |
In technical interviews, Segment Trees are significantly more prevalent due to their flexibility, especially when dealing with Range Maximum/Minimum queries. However, knowing the Fenwick Tree’s i & (-i) trick is a massive flex that demonstrates deep systems-level understanding.