Efficient Range Updates Using Increment and Decrement Technique
Introduction
The increment and decrement technique is an efficient way to perform range updates on an array. Instead of updating every element in the range explicitly, we use a clever approach to mark the start and end of a range with increments and decrements. Once all queries are processed, a prefix sum is applied to derive the final state of the array.
This technique significantly improves performance, especially for large arrays and multiple range update queries. Below, we explain the technique in detail with examples and visualizations.
Problem Statement
Suppose you are given an array of size n
initialized to all zeros and a list of range queries. Each query specifies a range [left, right]
where you need to increment all elements in this range by 1
.
For example:
- Array size:
n = 12
- Range queries:
[[1, 5], [2, 10], [4, 8]]
The goal is to determine the final state of the array after processing all range queries.
Technique Explanation
Step 1: Increment and Decrement Marking
For each range [left, right]
:
- Increment the element at index
left
by1
. - Decrement the element at index
right + 1
by1
(ifright + 1
is within bounds).
This step marks where a range starts and ends in the array.
Step 2: Apply Prefix Sum
After processing all range queries, compute the prefix sum of the array. This accumulates the effect of all ranges, resulting in the final state of the array.
Example Walkthrough
Input:
- Array size:
n = 12
- Range queries:
[[1, 5], [2, 10], [4, 8]]
Step 1: Increment and Decrement Marking
We start with an array initialized to zeros:
Initial array: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Process each query:
- Query
[1, 5]
:- Increment at index
1
:array[1] += 1
- Decrement at index
6
:array[6] -= 1
- Resulting array:
[0, 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0]
- Increment at index
- Query
[2, 10]
:- Increment at index
2
:array[2] += 1
- Decrement at index
11
:array[11] -= 1
- Resulting array:
[0, 1, 1, 0, 0, 0, -1, 0, 0, 0, 0, -1]
- Increment at index
- Query
[4, 8]
:- Increment at index
4
:array[4] += 1
- Decrement at index
9
:array[9] -= 1
- Resulting array:
[0, 1, 1, 0, 1, 0, -1, 0, 0, -1, 0, -1]
- Increment at index
Step 2: Apply Prefix Sum
Compute the prefix sum of the array to accumulate the effect of the range updates:
Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Array | 0 | 1 | 1 | 0 | 1 | 0 | -1 | 0 | 0 | -1 | 0 | -1 |
Compute the prefix sum:
Prefix sum: [0, 1, 2, 2, 3, 3, 2, 2, 2, 1, 1, 0]
Final Array:
The final state of the array after processing all range queries is:
[0, 1, 2, 2, 3, 3, 2, 2, 2, 1, 1, 0]
Visualization
Step 1: Marking Increments and Decrements
Initial array:
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
After processing [1, 5]:
[0, 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0]
After processing [2, 10]:
[0, 1, 1, 0, 0, 0, -1, 0, 0, 0, 0, -1]
After processing [4, 8]:
[0, 1, 1, 0, 1, 0, -1, 0, 0, -1, 0, -1]
Step 2: Prefix Sum Calculation
Prefix sum:
[0, 1, 2, 2, 3, 3, 2, 2, 2, 1, 1, 0]
Advantages of This Technique
- Efficiency: Each range update is
O(1)
since we only mark the start and end points. - Scalability: Applying the prefix sum is
O(n)
, making this approach suitable for large arrays and numerous queries. - Simplicity: The logic is straightforward and easy to implement.
Applications
- Cumulative Range Updates: Efficiently handle multiple range updates in problems like lighting up ranges in a street, adding intervals in a timeline, etc.
- Array Manipulations: This technique can also be extended for more complex operations like subtracting, multiplying, or assigning values within ranges.
Code Implementation
Java Code
import java.util.*;
public class Main {
public static int[] rangeUpdate(int n, int[][] queries) {
int[] prefixSum = new int[n];
for (int[] query : queries) {
int left = query[0];
int right = query[1];
prefixSum[left] += 1;
prefixSum[right + 1] -= 1;
}
for (int i = 1; i < n; i++) {
prefixSum[i] += prefixSum[i - 1];
}
return prefixSum;
}
public static void main(String[] args) {
int n = 12;
int[][] queries = {{1, 5}, {2, 10}, {4, 8}};
int[] result = rangeUpdate(n, queries);
System.out.println(Arrays.toString(result));
}
}
C++ Code
#include <vector>
#include <iostream>
using namespace std;
class Solution {
public:
vector<int> rangeUpdate(int n, vector<vector<int>>& queries) {
vector<int> prefixSum(n, 0);
for (auto &query : queries) {
int left = query[0];
int right = query[1];
prefixSum[left] += 1;
prefixSum[right + 1] -= 1;
}
for (int i = 1; i < n; i++) {
prefixSum[i] += prefixSum[i - 1];
}
return prefixSum;
}
};
int main() {
int n = 12;
vector<vector<int>> queries = {{1, 5}, {2, 10}, {4, 8}};
Solution solution;
vector<int> result = solution.rangeUpdate(n, queries);
for (int val : result) {
cout << val << " ";
}
return 0;
}
Python Code
class Solution:
def range_update(self, n, queries):
prefix_sum = [0] * n
for left, right in queries:
prefix_sum[left] += 1
prefix_sum[right + 1] -= 1
for i in range(1, n):
prefix_sum[i] += prefix_sum[i - 1]
return prefix_sum
n = 12
queries = [[1, 5], [2, 10], [4, 8]]
solution = Solution()
result = solution.range_update(n, queries)
print(result)
Complexity Analysis
Time Complexity
The time complexity of this approach is O(N + Q), where:
- N is the size of the array.
- Q is the number of ranges.
We traverse the query array once to apply increments and decrements and then we traverse the prefixSum
array to compute the prefix sum.
Space Complexity
The space complexity is O(N) for the array used to store intermediate results.