What is Fenwick Tree Binary Lifting?
Fenwick Tree Binary Lifting is a bitwise technique used to search for prefix sum thresholds in a Binary Indexed Tree (BIT) in O(log N) time. While standard binary search on prefix sums takes time, binary lifting exploits the internal power-of-2 structure of the Fenwick tree to find the target index in a single pass.
The Problem: Finding a Prefix Sum Threshold
- Assume we have a Fenwick tree representing an array of non-negative frequencies. We want to find the smallest index such that the prefix sum is at least :
- This operation is essential for maintaining dynamic order statistics (e.g. finding the -th smallest element in a dynamic set).
1. The Standard Binary Search
- The most straightforward approach is to binary search the index range :
# O(log^2 N) search
low, high = 1, N
ans = -1
while low <= high:
mid = (low + high) // 2
if query(mid) >= K:
ans = mid
high = mid - 1
else:
low = mid + 1- Since each
query(mid)takes time, the overall complexity is .
2. The Binary Lifting Solution
- A Fenwick tree node
tree[idx]stores the sum of elements in a range of lengthidx & -idx(the largest power of 2 dividingidx). - Because the ranges are structured as powers of 2, we can build the target index bit by bit, from the most significant bit down to 0, similar to binary lifting on trees:
-
- Start with
idx = 0andcurrent_sum = 0.
- Start with
-
- Iterate from the largest power of 2 () down to ().
-
- For each power : if
idx + P <= Nandcurrent_sum + tree[idx + P] < K, we updateidx += Pandcurrent_sum += tree[idx].
- For each power : if
-
- At the end of the loop,
idx + 1is the first index where the prefix sum is at least .
- At the end of the loop,
Mathematical Analogy & Trace
- Imagine finding a target distance on a road by jumping in decreasing powers of 2 (e.g., 16 miles, 8 miles, 4 miles, 2 miles, 1 mile).
- At each step, you check if making the jump would overshoot your destination. If it doesn’t, you make the jump; if it does, you stay in place and try the next smaller jump.
Tracing a Query for
- Suppose our Fenwick tree has size , storing frequencies
[1, 0, 2, 0, 1, 3, 0, 1]. treevalues:tree[1] = 1tree[2] = 1tree[3] = 2tree[4] = 3(stores sum ofarr[1..4] = 1+0+2+0)tree[5] = 1tree[6] = 4(stores sum ofarr[5..6] = 1+3)tree[7] = 0tree[8] = 8(stores sum ofarr[1..8] = 8)
- Let’s find the first index with prefix sum :
- Start:
idx = 0,sum = 0. Largest power is . - Step 1 ():
idx + 4 = 4().sum + tree[4] = 0 + 3 = 3 < 6Jump!- Update:
idx = 4,sum = 3.
- Step 2 ():
idx + 2 = 6().sum + tree[6] = 3 + 4 = 7 \not< 6Do not jump!
- Step 3 ():
idx + 1 = 5().sum + tree[5] = 3 + 1 = 4 < 6Jump!- Update:
idx = 5,sum = 4.
- Loop Ends: The result index is
idx + 1 = 6. - Verification: Prefix sum at index 5 is 4. Prefix sum at index 6 is 7 . The first index is indeed 6. Correct!
Time & Space Complexity
Complexity Table
| Method | Time Complexity | Space Complexity | Constant Factor |
|---|---|---|---|
| Binary Search | for tree | High (requires query iterations) | |
| Binary Lifting | Very Low (runs in exactly loops) |
Implementation
-
Below are the implementations of a Fenwick Tree containing point updates, prefix queries, and the binary lifting prefix-search algorithm. Python · Cpp · Java Script · Java
Languages:
class FenwickTree:
def __init__(self, size):
self.size = size
self.tree = [0] * (size + 1)
def update(self, idx, val):
while idx <= self.size:
self.tree[idx] += val
idx += idx & -idx
def query(self, idx):
s = 0
while idx > 0:
s += self.tree[idx]
idx -= idx & -idx
return s
# Find the smallest index where prefix sum >= K (O(log N))
def find_kth(self, k):
idx = 0
curr_sum = 0
# Find largest power of 2
power = 1
while (power << 1) <= self.size:
power <<= 1
while power > 0:
if idx + power <= self.size:
# Compare cumulative sum with K
if curr_sum + self.tree[idx + power] < k:
idx += power
curr_sum += self.tree[idx]
power >>= 1
# The target index is the next element
return idx + 1
# Example usage
bit = FenwickTree(8)
freq = [0, 1, 0, 2, 0, 1, 3, 0, 1]
for i in range(1, 9):
bit.update(i, freq[i])
print("First index with sum >= 6:", bit.find_kth(6)) # Output: 6#include <iostream>
#include <vector>
class FenwickTree {
private:
int size;
std::vector<int> tree;
public:
FenwickTree(int n) : size(n), tree(n + 1, 0) {}
void update(int idx, int val) {
for (; idx <= size; idx += idx & -idx) {
tree[idx] += val;
}
}
int query(int idx) {
int sum = 0;
for (; idx > 0; idx -= idx & -idx) {
sum += tree[idx];
}
return sum;
}
// O(log N) binary lifting to find first index with prefix sum >= K
int find_kth(int k) {
int idx = 0;
int curr_sum = 0;
int power = 1;
while ((power << 1) <= size) {
power <<= 1;
}
for (; power > 0; power >>= 1) {
if (idx + power <= size) {
if (curr_sum + tree[idx + power] < k) {
idx += power;
curr_sum += tree[idx];
}
}
}
return idx + 1;
}
};
int main() {
FenwickTree bit(8);
std::vector<int> freq = {0, 1, 0, 2, 0, 1, 3, 0, 1};
for (int i = 1; i <= 8; ++i) {
bit.update(i, freq[i]);
}
std::cout << "First index with sum >= 6: " << bit.find_kth(6) << "\n"; // 6
return 0;
}class FenwickTree {
constructor(size) {
this.size = size;
this.tree = new Array(size + 1).fill(0);
}
update(idx, val) {
for (; idx <= this.size; idx += idx & -idx) {
this.tree[idx] += val;
}
}
query(idx) {
let sum = 0;
for (; idx > 0; idx -= idx & -idx) {
sum += this.tree[idx];
}
return sum;
}
findKth(k) {
let idx = 0;
let currSum = 0;
let power = 1;
while ((power << 1) <= this.size) {
power <<= 1;
}
for (; power > 0; power >>= 1) {
if (idx + power <= this.size) {
if (currSum + this.tree[idx + power] < k) {
idx += power;
currSum += this.tree[idx];
}
}
}
return idx + 1;
}
}
const bit = new FenwickTree(8);
const freq = [0, 1, 0, 2, 0, 1, 3, 0, 1];
for (let i = 1; i <= 8; i++) {
bit.update(i, freq[i]);
}
console.log("First index with sum >= 6:", bit.findKth(6)); // 6public class FenwickBinaryLifting {
static class FenwickTree {
private final int size;
private final int[] tree;
public FenwickTree(int n) {
this.size = n;
this.tree = new int[n + 1];
}
public void update(int idx, int val) {
for (; idx <= size; idx += idx & -idx) {
tree[idx] += val;
}
}
public int query(int idx) {
int sum = 0;
for (; idx > 0; idx -= idx & -idx) {
sum += tree[idx];
}
return sum;
}
public int findKth(int k) {
int idx = 0;
int currSum = 0;
int power = 1;
while ((power << 1) <= size) {
power <<= 1;
}
for (; power > 0; power >>= 1) {
if (idx + power <= size) {
if (currSum + tree[idx + power] < k) {
idx += power;
currSum += tree[idx];
}
}
}
return idx + 1;
}
}
public static void main(String[] args) {
FenwickTree bit = new FenwickTree(8);
int[] freq = {0, 1, 0, 2, 0, 1, 3, 0, 1};
for (int i = 1; i <= 8; i++) {
bit.update(i, freq[i]);
}
System.out.println("First index with sum >= 6: " + bit.findKth(6)); // 6
}
}
Key Takeaways
- Power-of-2 Alignment — By iterating downwards in powers of 2, binary lifting jumps directly along the boundaries of the Fenwick tree.
- O(log N) Efficiency — Replaces nested searches with a single traversal, optimizing constant factors in query-intensive problems.
More Learn
Resources
Related Pages
- Submask Bitwise Iteration – O(3^N) submask dynamic programming
- Linear Basis Algorithm – Vector space XOR operations
- DSA Algo & System Design – Master DSA checklist