Fenwick Trees (Binary Indexed Trees)
Compact structure for prefix sums and range queries.
1. Internal bit representation
Understanding how Fenwick Trees use binary representation for efficient operations:
Core concept:
- Fenwick Tree (BIT): Array-based structure where each index stores cumulative information
- Key insight: Use binary representation of indices to determine responsibility ranges
- Index convention: 1-indexed (index 0 unused or used as sentinel)
Binary representation and ranges:
Each index i is responsible for a range of elements determined by the least significant bit (LSB):
- LSB(i): The rightmost set bit in binary representation
- Range: Index
istores sum of elements from[i - LSB(i) + 1, i]
# LSB calculation using bit manipulation
def lsb(i):
return i & (-i)
# Examples:
# i = 12 (binary: 1100)
# -i in two's complement: 0100
# i & (-i) = 0100 = 4
# So index 12 covers 4 elements: [9, 10, 11, 12]
# i = 10 (binary: 1010)
# -i: 0110
# i & (-i) = 0010 = 2
# So index 10 covers 2 elements: [9, 10]
Visual representation:
Array indices: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Binary: 0001 0010 0011 0100 0101 0110 0111 1000 ...
LSB values: 1 2 1 4 1 2 1 8 1 2 1 4 1 2 1 16
Responsibility ranges:
tree[1] = arr[1] (covers 1 element)
tree[2] = arr[1..2] (covers 2 elements)
tree[3] = arr[3] (covers 1 element)
tree[4] = arr[1..4] (covers 4 elements)
tree[5] = arr[5] (covers 1 element)
tree[6] = arr[5..6] (covers 2 elements)
tree[7] = arr[7] (covers 1 element)
tree[8] = arr[1..8] (covers 8 elements)
...
Tree structure visualization:
Conceptual tree for array of size 8:
[1-8]
/ \
[1-4] [5-8]
/ \ / \
[1-2] [3-4] [5-6] [7-8]
/ \ / \ / \ / \
[1] [2] [3] [4][5] [6][7] [8]
Actual BIT array indices:
tree[8] covers [1-8]
tree[4] covers [1-4]
tree[6] covers [5-6]
tree[2] covers [1-2]
tree[7] covers [7]
...
Why this works:
- Efficient navigation: Adding/subtracting LSB moves to parent/child in O(log n)
- No explicit pointers: Structure encoded in binary representation
- Cache-friendly: Sequential array access
- Space-efficient: Only n+1 elements needed
Implementation:
class FenwickTree:
def __init__(self, n):
self.n = n
self.tree = [0] * (n + 1) # 1-indexed
def _lsb(self, i):
"""Get least significant bit"""
return i & (-i)
2. Point update and prefix sum
Core operations that make Fenwick Trees powerful:
Point update: Add value to single element
def update(self, i, delta):
"""Add delta to element at index i (1-indexed)"""
while i <= self.n:
self.tree[i] += delta
i += self._lsb(i) # Move to parent
How it works:
- Start at index
i - Update current node
- Move to parent by adding LSB:
i += LSB(i) - Repeat until out of bounds
Example: Update index 5 with delta = 3
Initial: tree = [0, 1, 3, 2, 10, 5, 11, 7, 36, ...]
Step 1: tree[5] += 3 → tree[5] = 8
Next: 5 + LSB(5) = 5 + 1 = 6
Step 2: tree[6] += 3 → tree[6] = 14
Next: 6 + LSB(6) = 6 + 2 = 8
Step 3: tree[8] += 3 → tree[8] = 39
Next: 8 + LSB(8) = 8 + 8 = 16 (out of bounds if n < 16)
Done! Updated 3 nodes in O(log n) time.
Time complexity: O(log n)
- At most log₂(n) updates (height of tree)
- Each update is O(1)
Prefix sum: Query sum from index 1 to i
def prefix_sum(self, i):
"""Get sum of elements from index 1 to i (1-indexed)"""
result = 0
while i > 0:
result += self.tree[i]
i -= self._lsb(i) # Move to previous range
return result
How it works:
- Start at index
i - Add current node’s value
- Move to previous non-overlapping range by subtracting LSB:
i -= LSB(i) - Repeat until i = 0
Example: Query prefix sum up to index 13
Query: prefix_sum(13)
Step 1: result += tree[13] (covers [13])
Next: 13 - LSB(13) = 13 - 1 = 12
Step 2: result += tree[12] (covers [9-12])
Next: 12 - LSB(12) = 12 - 4 = 8
Step 3: result += tree[8] (covers [1-8])
Next: 8 - LSB(8) = 8 - 8 = 0
Done! Summed ranges: [13] + [9-12] + [1-8] = [1-13]
Time complexity: O(log n)
Complete implementation:
class FenwickTree:
def __init__(self, arr):
"""Initialize from array (0-indexed input)"""
self.n = len(arr)
self.tree = [0] * (self.n + 1)
# Build tree efficiently
for i in range(self.n):
self.update(i + 1, arr[i])
def update(self, i, delta):
"""Add delta to index i (1-indexed)"""
while i <= self.n:
self.tree[i] += delta
i += i & (-i)
def prefix_sum(self, i):
"""Sum from index 1 to i (1-indexed)"""
result = 0
while i > 0:
result += self.tree[i]
i -= i & (-i)
return result
def range_sum(self, left, right):
"""Sum from left to right (1-indexed, inclusive)"""
return self.prefix_sum(right) - self.prefix_sum(left - 1)
Building the tree efficiently:
# Method 1: Using update (O(n log n))
def build_slow(arr):
bit = FenwickTree([0] * len(arr))
for i, val in enumerate(arr):
bit.update(i + 1, val)
return bit
# Method 2: Direct construction (O(n))
def build_fast(arr):
n = len(arr)
tree = [0] * (n + 1)
for i in range(1, n + 1):
tree[i] += arr[i - 1]
j = i + (i & -i)
if j <= n:
tree[j] += tree[i]
return tree
3. Range queries via prefix sums
Using prefix sums to answer various range queries:
Range sum query:
def range_sum(self, left, right):
"""Sum of elements from left to right (1-indexed, inclusive)"""
return self.prefix_sum(right) - self.prefix_sum(left - 1)
Example:
Array: [3, 2, -1, 6, 5, 4, -3, 3, 7, 2, 3]
Query: sum from index 3 to 7
Solution:
sum[3..7] = prefix_sum(7) - prefix_sum(2)
= sum[1..7] - sum[1..2]
= (3+2-1+6+5+4-3) - (3+2)
= 16 - 5
= 11
Point query (get single element value):
def point_query(self, i):
"""Get value at index i (1-indexed)"""
return self.range_sum(i, i)
Range update with range query:
For range updates, use difference array technique:
class FenwickTreeRangeUpdate:
def __init__(self, n):
self.n = n
self.tree = [0] * (n + 1)
def range_update(self, left, right, delta):
"""Add delta to all elements in [left, right]"""
self.update(left, delta)
self.update(right + 1, -delta)
def point_query(self, i):
"""Get value at index i after all updates"""
return self.prefix_sum(i)
def update(self, i, delta):
while i <= self.n:
self.tree[i] += delta
i += i & (-i)
def prefix_sum(self, i):
result = 0
while i > 0:
result += self.tree[i]
i -= i & (-i)
return result
Example: Range update
Initial array: [0, 0, 0, 0, 0, 0, 0, 0]
range_update(3, 6, 5): # Add 5 to indices 3-6
update(3, 5) → diff[3] += 5
update(7, -5) → diff[7] += -5
Difference array: [0, 0, 5, 0, 0, 0, -5, 0]
Prefix sums: [0, 0, 5, 5, 5, 5, 0, 0]
Result array: [0, 0, 5, 5, 5, 5, 0, 0] ✓
2D Fenwick Tree (for matrix range sums):
class FenwickTree2D:
def __init__(self, rows, cols):
self.rows = rows
self.cols = cols
self.tree = [[0] * (cols + 1) for _ in range(rows + 1)]
def update(self, row, col, delta):
"""Add delta to cell (row, col)"""
i = row
while i <= self.rows:
j = col
while j <= self.cols:
self.tree[i][j] += delta
j += j & (-j)
i += i & (-i)
def prefix_sum(self, row, col):
"""Sum of rectangle from (1,1) to (row, col)"""
result = 0
i = row
while i > 0:
j = col
while j > 0:
result += self.tree[i][j]
j -= j & (-j)
i -= i & (-i)
return result
def range_sum(self, r1, c1, r2, c2):
"""Sum of rectangle from (r1,c1) to (r2,c2)"""
return (self.prefix_sum(r2, c2)
- self.prefix_sum(r1 - 1, c2)
- self.prefix_sum(r2, c1 - 1)
+ self.prefix_sum(r1 - 1, c1 - 1))
Time complexity:
- 1D: O(log n) per operation
- 2D: O(log m × log n) per operation
4. Space/time trade-offs vs segment trees
Comparing Fenwick Trees with Segment Trees:
Fenwick Tree characteristics:
- Space: O(n) - single array
- Point update: O(log n)
- Range query: O(log n)
- Construction: O(n log n) or O(n) optimized
- Limitations: Only supports associative operations (sum, XOR, etc.)
Segment Tree characteristics:
- Space: O(4n) - tree array
- Point update: O(log n)
- Range query: O(log n)
- Construction: O(n)
- Advantages: Supports any associative operation (min, max, GCD, etc.)
Detailed comparison:
| Feature | Fenwick Tree | Segment Tree |
|---|---|---|
| Space | O(n) | O(4n) |
| Code complexity | Simple (~20 lines) | Moderate (~50 lines) |
| Constants | Lower | Higher |
| Cache performance | Better | Good |
| Operations | Sum, XOR, etc. | Any associative op |
| Range updates | Needs difference array | Native support |
| Lazy propagation | Not applicable | Supported |
| Point query | O(log n) | O(log n) or O(1)* |
| Implementation | Bit manipulation | Recursion/iteration |
*Segment tree can store original values at leaves
When to use Fenwick Tree:
✓ Need prefix sums or range sums ✓ Want minimal memory usage ✓ Prefer simple implementation ✓ Operations are invertible (sum, XOR) ✓ Competitive programming (faster to code)
When to use Segment Tree:
✓ Need range minimum/maximum queries ✓ Need range GCD, LCM queries ✓ Need lazy propagation for range updates ✓ Operations are not invertible ✓ Need to store additional information per node
Performance comparison (1M elements):
Operation Fenwick Tree Segment Tree
-------------------------------------------------
Memory 4 MB 16 MB
Build time ~20 ms ~15 ms
1M updates ~50 ms ~80 ms
1M range queries ~50 ms ~80 ms
Code lines ~20 ~50
Example: When Fenwick Tree fails:
# Range minimum query - Fenwick Tree CANNOT do this efficiently
# Must use Segment Tree or Sparse Table
# Why? Subtraction doesn't work for min:
# min(arr[1..7]) ≠ min(arr[1..7]) - min(arr[1..2])
# Segment Tree solution:
class SegmentTree:
def range_min(self, left, right):
return self._query(1, 1, self.n, left, right)
Hybrid approach:
# Use both for different queries on same array
class HybridStructure:
def __init__(self, arr):
self.fenwick = FenwickTree(arr) # For sum queries
self.segment = SegmentTree(arr) # For min/max queries
def range_sum(self, l, r):
return self.fenwick.range_sum(l, r)
def range_min(self, l, r):
return self.segment.range_min(l, r)
5. Common patterns and pitfalls
Best practices and mistakes to avoid:
Common patterns:
- Inversion count (count pairs where i < j but arr[i] > arr[j]):
def count_inversions(arr): # Coordinate compression sorted_arr = sorted(set(arr)) rank = {v: i+1 for i, v in enumerate(sorted_arr)} bit = FenwickTree(len(sorted_arr)) inversions = 0 for i in range(len(arr) - 1, -1, -1): r = rank[arr[i]] inversions += bit.prefix_sum(r - 1) bit.update(r, 1) return inversions - Dynamic median (maintain median with updates):
# Use two Fenwick Trees: one for counts, one for sums class DynamicMedian: def __init__(self, max_val): self.count_tree = FenwickTree(max_val) self.sum_tree = FenwickTree(max_val) def insert(self, val): self.count_tree.update(val, 1) self.sum_tree.update(val, val) def find_median(self): total = self.count_tree.prefix_sum(self.max_val) target = (total + 1) // 2 # Binary search for target-th element return self._binary_search(target) - Range frequency query:
# Count occurrences of value x in range [l, r] class RangeFrequency: def __init__(self, arr): self.positions = {} for i, val in enumerate(arr): if val not in self.positions: self.positions[val] = [] self.positions[val].append(i) def count(self, l, r, x): if x not in self.positions: return 0 pos = self.positions[x] # Binary search for range [l, r] left_idx = bisect_left(pos, l) right_idx = bisect_right(pos, r) return right_idx - left_idx
Common pitfalls:
- 0-indexed vs 1-indexed confusion:
# Wrong: Using 0-indexed directly bit = FenwickTree(n) bit.update(0, 5) # Error! Fenwick Tree is 1-indexed # Correct: Convert to 1-indexed bit.update(1, 5) # Update first element # Or use wrapper: def update_0indexed(self, i, delta): self.update(i + 1, delta) - Forgetting to handle negative values:
# Wrong: Fenwick Tree with negative indices # Fenwick Tree requires positive indices # Correct: Coordinate compression def compress(arr): sorted_unique = sorted(set(arr)) return {v: i+1 for i, v in enumerate(sorted_unique)} - Using Fenwick Tree for non-invertible operations:
# Wrong: Range minimum with Fenwick Tree # min(a, b) - min(c, d) doesn't give meaningful result # Correct: Use Segment Tree for min/max - Not initializing with correct size:
# Wrong: Size too small bit = FenwickTree(len(arr)) bit.update(len(arr), 5) # Out of bounds! # Correct: Account for 1-indexing bit = FenwickTree(len(arr)) bit.update(len(arr), 5) # OK if tree size is len(arr) - Inefficient construction:
# Slow: O(n log n) bit = FenwickTree(n) for i, val in enumerate(arr): bit.update(i + 1, val) # Fast: O(n) construction def build_optimized(arr): n = len(arr) tree = [0] * (n + 1) for i in range(1, n + 1): tree[i] += arr[i - 1] j = i + (i & -i) if j <= n: tree[j] += tree[i] return tree
Debugging tips:
- Print tree array to visualize state
- Verify LSB calculation:
i & (-i) - Check 1-indexed vs 0-indexed conversions
- Test with small arrays first
- Compare with brute force for correctness
6. Applications
Real-world uses of Fenwick Trees:
Competitive programming:
- Range sum queries:
- Given array, answer Q queries for sum in range [L, R]
- With updates: modify single elements
- Counting inversions:
- Count pairs (i, j) where i < j but arr[i] > arr[j]
- Used in sorting analysis, ranking systems
- Order statistics:
- Find k-th smallest element in dynamic array
- Maintain sorted order with insertions/deletions
Database systems:
- Aggregate queries:
- Fast SUM, COUNT queries over ranges
- Used in OLAP (Online Analytical Processing)
- Time-series data:
- Cumulative metrics (total sales, user counts)
- Sliding window aggregations
Graphics and gaming:
- 2D range queries:
- Sum of values in rectangular region
- Used in image processing, collision detection
- Particle systems:
- Count particles in spatial regions
- Update particle properties efficiently
Example problems:
Problem 1: Range Sum Query - Mutable (LeetCode 307)
class NumArray:
def __init__(self, nums):
self.nums = nums
self.bit = FenwickTree(nums)
def update(self, index, val):
delta = val - self.nums[index]
self.nums[index] = val
self.bit.update(index + 1, delta)
def sumRange(self, left, right):
return self.bit.range_sum(left + 1, right + 1)
Problem 2: Count of Smaller Numbers After Self (LeetCode 315)
def countSmaller(nums):
# Coordinate compression
sorted_nums = sorted(set(nums))
rank = {v: i+1 for i, v in enumerate(sorted_nums)}
bit = FenwickTree(len(sorted_nums))
result = []
for num in reversed(nums):
r = rank[num]
result.append(bit.prefix_sum(r - 1))
bit.update(r, 1)
return result[::-1]
Problem 3: Range Addition (LeetCode 370)
def getModifiedArray(length, updates):
bit = FenwickTree(length)
for start, end, inc in updates:
bit.update(start + 1, inc)
if end + 2 <= length:
bit.update(end + 2, -inc)
result = []
for i in range(1, length + 1):
result.append(bit.prefix_sum(i))
return result
Problem 4: 2D Matrix Range Sum (LeetCode 308)
class NumMatrix:
def __init__(self, matrix):
if not matrix or not matrix[0]:
return
self.matrix = matrix
self.bit = FenwickTree2D(len(matrix), len(matrix[0]))
for i in range(len(matrix)):
for j in range(len(matrix[0])):
self.bit.update(i + 1, j + 1, matrix[i][j])
def update(self, row, col, val):
delta = val - self.matrix[row][col]
self.matrix[row][col] = val
self.bit.update(row + 1, col + 1, delta)
def sumRegion(self, row1, col1, row2, col2):
return self.bit.range_sum(row1 + 1, col1 + 1,
row2 + 1, col2 + 1)
Performance in practice:
- Codeforces: Fenwick Tree preferred for speed
- LeetCode: Often overkill, but useful for hard problems
- Production systems: Used in analytics databases
- Real-time systems: Low latency for range queries
Advanced applications:
- Persistent Fenwick Tree: Support queries on historical versions
- Parallel Fenwick Tree: Concurrent updates with lock-free operations
- Compressed Fenwick Tree: For sparse arrays (coordinate compression)
- Multi-dimensional: 3D, 4D for spatial queries