Subarray OR Queries
Problem Overview
| Attribute | Value |
|---|---|
| Difficulty | Medium |
| Category | Range Queries |
| Time Limit | 1 second |
| Key Technique | Segment Tree / Sparse Table |
| CSES Link | Subarray OR Queries |
Learning Goals
After solving this problem, you will be able to:
- Understand why bitwise OR is an idempotent operation suitable for sparse tables
- Implement a segment tree for range OR queries
- Implement a sparse table for O(1) range queries with O(n log n) preprocessing
- Choose between segment tree and sparse table based on problem requirements
Problem Statement
Problem: Given an array of n integers, process q queries where each query asks for the bitwise OR of all elements in a subarray [l, r].
Input:
- Line 1: Two integers n and q (number of elements and queries)
- Line 2: n integers a_1, a_2, …, a_n
- Next q lines: Two integers l and r for each query (1-indexed)
Output:
- For each query, print the bitwise OR of elements in the range [l, r]
Constraints:
- 1 <= n, q <= 2 x 10^5
- 0 <= a_i <= 10^9
- 1 <= l <= r <= n
Example
Input:
5 3
1 2 3 4 5
1 3
2 4
1 5
Output:
3
7
7
Explanation:
-
Query 1: OR(1, 2, 3) = 1 2 3 = 01 10 11 = 11 = 3 -
Query 2: OR(2, 3, 4) = 2 3 4 = 010 011 100 = 111 = 7 -
Query 3: OR(1, 2, 3, 4, 5) = 001 010 011 100 101 = 111 = 7
Intuition: How to Think About This Problem
Pattern Recognition
Key Question: What property of bitwise OR makes this problem efficiently solvable?
Bitwise OR is associative (a | (b | c) = (a | b) | c) and idempotent (a | a = a). This means:
- We can combine results from subranges in any order
- Overlapping ranges in sparse tables give correct results
Breaking Down the Problem
- What are we looking for? The bitwise OR of all elements in [l, r]
- What information do we have? Static array with multiple queries
- What’s the relationship between input and output? OR accumulates bits - once a bit is set, it stays set
Analogies
Think of bitwise OR like collecting stamps. Each element has certain “stamps” (set bits). The OR of a range tells you all unique stamps collected from that range. If you have stamps from [1,3] and stamps from [2,4], combining them (even with overlap at [2,3]) still gives you the correct set of all stamps.
Solution 1: Brute Force
Idea
For each query, iterate through the range and compute the OR directly.
Algorithm
- For each query (l, r):
- Initialize result = 0
-
For each index i from l to r, compute result = arr[i] - Return result
Code
def solve_brute_force(n, arr, queries):
"""
Brute force: compute OR for each query by iterating through range.
Time: O(q * n) per query
Space: O(1)
"""
results = []
for l, r in queries:
or_result = 0
for i in range(l - 1, r): # Convert to 0-indexed
or_result |= arr[i]
results.append(or_result)
return results
Complexity
| Metric | Value | Explanation |
|---|---|---|
| Time | O(q * n) | Each query scans up to n elements |
| Space | O(1) | Only stores current OR result |
Why This Works (But Is Slow)
This correctly computes OR by examining every element, but with q = 210^5 queries and n = 210^5 elements, we get 4*10^10 operations - far too slow.
Solution 2: Segment Tree
Key Insight
The Trick: Build a segment tree where each node stores the OR of its range. Query any range in O(log n).
Data Structure
Each node in the segment tree stores the OR of all elements in its range.
| Node Property | Meaning |
|---|---|
tree[node] |
OR of all elements in the range covered by this node |
Merge Operation
tree[parent] = tree[left_child] | tree[right_child]
Why? OR is associative, so combining left and right subtree ORs gives the parent’s OR.
Dry Run Example
Let’s trace through building the tree for arr = [1, 2, 3, 4, 5]:
Array (0-indexed): [1, 2, 3, 4, 5]
Binary: [001, 010, 011, 100, 101]
Building segment tree (bottom-up):
Level 0 (leaves): [1] [2] [3] [4] [5] [0] [0] [0]
8 9 10 11 12 13 14 15
Level 1: [1|2=3] [3|4=7] [5|0=5] [0|0=0]
4 5 6 7
Level 2: [3|7=7] [5|0=5]
2 3
Level 3 (root): [7|5=7]
1
Query [2, 4] (1-indexed -> [1, 3] 0-indexed):
- Start at root, range [0, 7]
- Go to node 2 (range [0, 3]), need [1, 3]
- Node 4 (range [0, 1]): partial overlap, need index 1 -> value 2
- Node 5 (range [2, 3]): fully inside [1, 3] -> value 7
- Result: 2 | 7 = 7
Visual Diagram
[7] (root, range [0,7])
/ \
[7] [5]
/ \ / \
[3] [7] [5] [0]
/ \ / \
[1] [2][3][4] ...leaves...
Code
class SegmentTreeOR:
"""
Segment Tree for range OR queries.
Time: O(n) build, O(log n) query
Space: O(n)
"""
def __init__(self, arr):
self.n = len(arr)
self.size = 1
while self.size < self.n:
self.size *= 2
# Tree array: size * 2 for complete binary tree
self.tree = [0] * (2 * self.size)
# Fill leaves
for i in range(self.n):
self.tree[self.size + i] = arr[i]
# Build tree bottom-up
for i in range(self.size - 1, 0, -1):
self.tree[i] = self.tree[2 * i] | self.tree[2 * i + 1]
def query(self, l, r):
"""Query OR in range [l, r] (0-indexed)."""
result = 0
l += self.size
r += self.size
while l <= r:
if l % 2 == 1: # l is right child
result |= self.tree[l]
l += 1
if r % 2 == 0: # r is left child
result |= self.tree[r]
r -= 1
l //= 2
r //= 2
return result
def solve_segment_tree(n, arr, queries):
st = SegmentTreeOR(arr)
results = []
for l, r in queries:
results.append(st.query(l - 1, r - 1)) # Convert to 0-indexed
return results
Complexity
| Metric | Value | Explanation |
|---|---|---|
| Time (Build) | O(n) | Visit each node once |
| Time (Query) | O(log n) | Tree height is log n |
| Space | O(n) | Store 2n nodes |
Solution 3: Sparse Table (Optimal for Static Arrays)
Key Insight
The Trick: Since OR is idempotent (a a = a), overlapping ranges give correct results. We can answer queries in O(1)!
Data Structure
| State | Meaning |
|---|---|
sparse[i][j] |
OR of 2^j elements starting at index i |
| In plain English: sparse[i][j] = arr[i] | arr[i+1] | … | arr[i + 2^j - 1] |
State Transition
sparse[i][j] = sparse[i][j-1] | sparse[i + 2^(j-1)][j-1]
Why? We combine two adjacent blocks of size 2^(j-1) to form a block of size 2^j.
Query Strategy
For range [l, r], find the largest k where 2^k <= (r - l + 1), then:
answer = sparse[l][k] | sparse[r - 2^k + 1][k]
The two ranges may overlap, but since OR is idempotent, this is correct!
Dry Run Example
Building sparse table for arr = [1, 2, 3, 4, 5]:
Initial (j=0, blocks of size 1):
sparse[0][0] = 1, sparse[1][0] = 2, sparse[2][0] = 3, sparse[3][0] = 4, sparse[4][0] = 5
j=1 (blocks of size 2):
sparse[0][1] = sparse[0][0] | sparse[1][0] = 1 | 2 = 3
sparse[1][1] = sparse[1][0] | sparse[2][0] = 2 | 3 = 3
sparse[2][1] = sparse[2][0] | sparse[3][0] = 3 | 4 = 7
sparse[3][1] = sparse[3][0] | sparse[4][0] = 4 | 5 = 5
j=2 (blocks of size 4):
sparse[0][2] = sparse[0][1] | sparse[2][1] = 3 | 7 = 7
sparse[1][2] = sparse[1][1] | sparse[3][1] = 3 | 5 = 7
Query [1, 4] (0-indexed [0, 3]):
Length = 4, k = log2(4) = 2
Result = sparse[0][2] | sparse[0][2] = 7 | 7 = 7
Query [2, 4] (0-indexed [1, 3]):
Length = 3, k = log2(3) = 1 (floor)
Result = sparse[1][1] | sparse[2][1] = 3 | 7 = 7
(covers [1,2] and [2,3], overlap at index 2 is OK!)
Code
import math
class SparseTableOR:
"""
Sparse Table for range OR queries.
Time: O(n log n) build, O(1) query
Space: O(n log n)
"""
def __init__(self, arr):
self.n = len(arr)
self.LOG = max(1, self.n.bit_length())
# sparse[i][j] = OR of 2^j elements starting at index i
self.sparse = [[0] * self.LOG for _ in range(self.n)]
# Base case: blocks of size 1
for i in range(self.n):
self.sparse[i][0] = arr[i]
# Build sparse table
for j in range(1, self.LOG):
for i in range(self.n - (1 << j) + 1):
self.sparse[i][j] = (self.sparse[i][j-1] |
self.sparse[i + (1 << (j-1))][j-1])
# Precompute log values for O(1) query
self.log_table = [0] * (self.n + 1)
for i in range(2, self.n + 1):
self.log_table[i] = self.log_table[i // 2] + 1
def query(self, l, r):
"""Query OR in range [l, r] (0-indexed)."""
length = r - l + 1
k = self.log_table[length]
return self.sparse[l][k] | self.sparse[r - (1 << k) + 1][k]
def solve_sparse_table(n, arr, queries):
st = SparseTableOR(arr)
results = []
for l, r in queries:
results.append(st.query(l - 1, r - 1)) # Convert to 0-indexed
return results
Complexity
| Metric | Value | Explanation |
|---|---|---|
| Time (Build) | O(n log n) | Fill n * log(n) table entries |
| Time (Query) | O(1) | Direct table lookup |
| Space | O(n log n) | Store sparse table |
Common Mistakes
Mistake 1: Forgetting 1-indexed to 0-indexed Conversion
# WRONG
result = st.query(l, r) # l, r are 1-indexed from input
# CORRECT
result = st.query(l - 1, r - 1) # Convert to 0-indexed
Problem: CSES uses 1-indexed input, but arrays are 0-indexed. Fix: Always subtract 1 from both l and r.
Problem: OR of large values can exceed int range in intermediate operations.
Fix: Use long long for all OR computations.
Mistake 3: Incorrect Sparse Table Size
# WRONG
LOG = int(math.log2(n)) # Missing +1
# CORRECT
LOG = n.bit_length() # Correctly handles all sizes
Problem: Underestimating LOG can cause index out of bounds.
Fix: Use bit_length() or add 1 to floor(log2(n)).
Edge Cases
| Case | Input | Expected Output | Why | ||
|---|---|---|---|---|---|
| Single element query | [5], query(1,1) |
5 | OR of single element is itself | ||
| All zeros | [0,0,0], query(1,3) |
0 | 0 | 0 | 0 = 0 |
| Full range query | [1,2,3], query(1,3) |
3 | Tests root of segment tree | ||
| Adjacent elements | [1,2], query(1,2) |
3 | Minimum non-trivial case | ||
| Large values | [10^9, 10^9] |
10^9 | a | a = a (idempotent) | |
| Powers of 2 | [1,2,4,8], query(1,4) |
15 | All bits set, no overlap |
When to Use This Pattern
Use Segment Tree When:
- Array has updates (point or range updates)
- Need to support multiple operations (OR, AND, sum)
- Query complexity of O(log n) is acceptable
Use Sparse Table When:
- Array is static (no updates)
- Operation is idempotent (OR, AND, min, max, GCD)
- Need O(1) query time for many queries
- Memory for O(n log n) is available
Pattern Recognition Checklist:
- Static array with range queries? -> Consider Sparse Table
- Idempotent operation (OR, AND, min, max, GCD)? -> Sparse Table works
- Need updates? -> Must use Segment Tree
- Associative but not idempotent (sum, XOR)? -> Segment Tree or prefix sums
Related Problems
Easier (Do These First)
| Problem | Why It Helps |
|---|---|
| Static Range Sum Queries | Basic prefix sum, simpler than segment tree |
| Static Range Minimum Queries | Sparse table with min operation |
Similar Difficulty
| Problem | Key Difference |
|---|---|
| Range Xor Queries | XOR is associative but NOT idempotent, use prefix XOR |
| Bitwise ORs of Subarrays (LeetCode 898) | Count distinct OR values across all subarrays |
Harder (Do These After)
| Problem | New Concept |
|---|---|
| Range Update Queries | Lazy propagation in segment tree |
| Polynomial Queries | Complex lazy propagation |
Key Takeaways
- The Core Idea: Bitwise OR is associative and idempotent, enabling both segment tree and sparse table solutions.
- Time Optimization: From O(q*n) brute force to O(n log n + q) with sparse table.
- Space Trade-off: Sparse table uses O(n log n) space for O(1) queries.
- Pattern: This belongs to the “range query on idempotent operations” pattern.
Practice Checklist
Before moving on, make sure you can:
- Implement segment tree for OR queries from scratch
- Implement sparse table for OR queries from scratch
- Explain why overlapping ranges work in sparse tables (idempotency)
- Choose between segment tree and sparse table based on problem constraints
- Handle 1-indexed to 0-indexed conversion correctly
Additional Resources
- CP-Algorithms: Sparse Table
- CP-Algorithms: Segment Tree
- CSES Range Xor Queries - Similar bitwise range query