Analysis of Range Query Algorithms Timothy Wei, Alan Lu Overleaf June 2023 Objective Suppose there is an array of integers arr with n ≤ 105 elements. There are q ≤ 105 queries each asking for the sum of the elements from index i to j inclusive. How can we answer the queries quickly? Table of Contents Brute Force Implementation Complexity Analysis Prefix Sums Implementation Complexity Analysis Fenwick Trees Segment Trees Brute Force A logical starting point would be to simply process each query by iterating through the elements in the range. Implementation //Input n, the length of the array, and q, the number of queries cin >> n >> q; //Get the array a from the input stream for (int i = 0; i < n; i++) { cin >> a[i]; } for (int i = 0; i < q; i++) { int u, v; //Get u and v, the endpoints of each query cin >> u >> v; int sum = 0; //Sum elements of a from u to v for (int j = u; j <= v; j++) { sum += a[j]; } cout << sum << ’ ’; } Complexity Analysis Each query iterates through at most n elements, and there are q queries total, so the overall complexity is O(nq). Not very fast. Prefix Sums However, note that we are computing the same sums multiple times. Consider the two queries (0, 3) and (0, 5). We are computing the range (0, 3) twice! This motivates us to use the following optimization: Theorem Let prefixSum[i] represent the sum of the numbers from index 0 to i. The sum of the numbers from index i to j is given by: ▶ If j = 0, the sum is prefixSum[i]. ▶ Otherwise, the sum is prefixSum[i] - prefixSum[j - 1]. Prefix Sums Example For example, suppose we have the following array: 0 5 -3 5 12 -6 8 -1 5 3 Then, the corresponding array prefixSum would be: 0 5 2 7 19 13 21 20 25 28 Now, suppose we want to find the sum of the elements in the range 3 to 7. Then, we would take prefixSum[7] - prefixSum[3 - 1] = 20 - 2 = 18. Implementation //Some previous code has been ommitted for brevity prefixSum[0] = a[0]; //Calculate prefixSum for (int i = 1; i < n; i++) { prefixSum[i] = prefixSum[i - 1] + a[i]; } for (int i = 0; i < q; i++) { int u, v; //Get u and v, the endpoints of each query cin >> u >> v; //Case 1: the first endpoint is 0 if (u == 0) { cout << prefixSum[v] << ’ ’; } //Case 2: both endpoints are non-zero else { cout << prefixSum[v] - prefixSum[u - 1] << ’ ’; } } Complexity Analysis Now, each query is nearly instantaneous at O(1) speed. Since there are q queries, the total complexity is O(q). Note that there is also O(n) preprocessing. So, the overall complexity is O(n + q). Splendid. Fenwick Trees We should be feeling pretty good about ourselves now. We can solve queries in O(1) time! It’s time to add an additional layer of complexity. Suppose we have another type of query that updates the value at a certain index to a different value. Then, if we were to apply prefix sums, we would have to recalculate the prefix sums for the entire array every time there is an update! This will make the time complexity O(n2 ), which is really slow. One way to speed this up is to apply Fenwick Trees. Fenwick Trees Consider the following array bit defined in the following way: Theorem Suppose bit[i] stores the sum of all numbers in the range (i - (i & -i), i]. Let query(i) represent the sum in the range [0, i]. Then query(i) = bit[i] + query(i - (i & -i)). Proof. Note that i & -i is the rightmost nonzero bit. For example, 11 = 10112 , so bit[10112 ] stores the sum in the range (10102 , 10112 ]. Then bit[10102 ] stores the sum in (10002 , 10102 ]. Then bit[10002 ] stores the sum in (0, 10002 ], so adding these together gives us query(10112 ). Segment Trees Suppose that we stored the sum of each half of the array, then each quarter, then each eighth... and so on. We can draw this as a tree, as shown: Updates Suppose that we wish add 3 to the second element, for example. What would happen? We would need to add three to all the nodes shown: Queries Ok, that wasn’t too bad. What if we want to find the sum of, for example, index 1 to 4? We would add the following nodes together: Notice that we don’t have to go all the way down on the right side, because we have already stored that sum in the [3-4] range.