493. Reverse Pairs
Problem Description
You are given an integer array nums
. Your task is to count how many reverse pairs exist in the array.
A reverse pair is defined as a pair of indices (i, j)
that satisfies both of these conditions:
- The first index comes before the second index:
0 <= i < j < nums.length
- The element at position
i
is more than twice the element at positionj
:nums[i] > 2 * nums[j]
For example, if nums = [1, 3, 2, 3, 1]
, the reverse pairs would be:
(1, 4)
becausenums[1] = 3 > 2 * nums[4] = 2 * 1 = 2
(3, 4)
becausenums[3] = 3 > 2 * nums[4] = 2 * 1 = 2
So the answer would be 2.
The solution uses a modified merge sort algorithm. During the merge process, it counts reverse pairs by comparing elements from the left half with twice the elements from the right half. The key insight is that if nums[i] > 2 * nums[j]
for some i
in the left half and j
in the right half, then all elements from i
to the end of the left half will also form reverse pairs with nums[j]
(since the arrays are sorted). This allows efficient counting of reverse pairs while maintaining the overall O(n log n)
time complexity of merge sort.
Intuition
The naive approach would be to check every pair (i, j)
where i < j
and count how many satisfy nums[i] > 2 * nums[j]
. This would take O(n²)
time, which is inefficient for large arrays.
The key observation is that this problem is similar to counting inversions in an array, but with a modified condition. In the classic inversion problem, we count pairs where nums[i] > nums[j]
and i < j
. Here, we need nums[i] > 2 * nums[j]
.
Why does merge sort work well here? During merge sort, we divide the array into two halves and recursively sort them. At each merge step, we have two sorted subarrays. The crucial insight is that for any element in the left subarray and any element in the right subarray, we naturally have i < j
(left indices come before right indices).
When we have two sorted halves, we can efficiently count reverse pairs. If we find that nums[i] > 2 * nums[j]
for some position i
in the left half and position j
in the right half, then due to the sorted property, all elements from position i
to the end of the left half will also satisfy this condition with nums[j]
. This means we can add (mid - i + 1)
pairs at once, rather than checking each one individually.
The beauty of this approach is that we can count the reverse pairs during the merge process without adding extra time complexity. We perform two passes during each merge:
- First pass: Count reverse pairs by comparing
nums[i]
with2 * nums[j]
- Second pass: Actually merge the two sorted halves to maintain the sorted property for the next level
This way, we leverage the divide-and-conquer nature of merge sort to reduce the problem from O(n²)
to O(n log n)
.
Solution Approach
The solution implements a modified merge sort algorithm to efficiently count reverse pairs. Let's walk through the implementation step by step:
Main Structure:
The solution uses a recursive merge_sort
function that takes left and right boundaries (l, r)
of the current subarray being processed.
Base Case:
if l >= r: return 0
When the subarray has one or zero elements, there are no pairs to count.
Divide Step:
mid = (l + r) >> 1 ans = merge_sort(l, mid) + merge_sort(mid + 1, r)
We divide the array at the midpoint and recursively count reverse pairs in both halves. The bit shift operation >> 1
is equivalent to integer division by 2.
Count Reverse Pairs:
i, j = l, mid + 1 while i <= mid and j <= r: if nums[i] <= 2 * nums[j]: i += 1 else: ans += mid - i + 1 j += 1
This is the crucial counting phase. We use two pointers:
i
traverses the left sorted half[l, mid]
j
traverses the right sorted half[mid + 1, r]
When nums[i] > 2 * nums[j]
, all elements from index i
to mid
in the left half will form reverse pairs with nums[j]
(since both halves are sorted). So we add (mid - i + 1)
to our count and move j
forward.
Merge Phase:
t = [] i, j = l, mid + 1 while i <= mid and j <= r: if nums[i] <= nums[j]: t.append(nums[i]) i += 1 else: t.append(nums[j]) j += 1
After counting, we perform the standard merge operation to combine the two sorted halves into a single sorted array. We use a temporary array t
to store the merged result.
Handle Remaining Elements:
t.extend(nums[i : mid + 1]) t.extend(nums[j : r + 1]) nums[l : r + 1] = t
We append any remaining elements from either half to the temporary array, then copy the sorted result back to the original array.
Time and Space Complexity:
- Time:
O(n log n)
- We havelog n
levels of recursion, and at each level, we process alln
elements once for counting and once for merging. - Space:
O(n)
- For the temporary array used during merging and the recursion stack.
The elegance of this solution lies in leveraging the sorted property of subarrays during merge sort to count reverse pairs in linear time at each level, avoiding the need for nested loops.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with nums = [2, 4, 3, 5, 1]
.
Initial Call: merge_sort(0, 4)
on [2, 4, 3, 5, 1]
Level 1 - First Split:
- Calculate
mid = 2
- Recursively call
merge_sort(0, 2)
for[2, 4, 3]
- Recursively call
merge_sort(3, 4)
for[5, 1]
Level 2 - Left Branch [2, 4, 3]
:
- Split into
[2, 4]
and[3]
- After recursive calls, we have sorted subarrays:
[2, 4]
and[3]
- Count phase: Compare elements from left
[2, 4]
with twice the elements from right[3]
i=0, j=0
: Is2 > 2*3
? No (2 ≤ 6), movei
forwardi=1, j=0
: Is4 > 2*3
? No (4 ≤ 6), movei
forward- No reverse pairs found
- Merge phase: Merge to get
[2, 3, 4]
Level 2 - Right Branch [5, 1]
:
- Split into
[5]
and[1]
- Count phase: Compare
[5]
with[1]
i=0, j=0
: Is5 > 2*1
? Yes (5 > 2), add 1 pair
- Merge phase: Merge to get
[1, 5]
Level 1 - Final Merge:
Now we merge [2, 3, 4]
and [1, 5]
- Count phase: Find reverse pairs between left and right halves
i=0, j=0
: Is2 > 2*1
? No (2 ≤ 2), movei
forwardi=1, j=0
: Is3 > 2*1
? Yes (3 > 2), add(2-1+1) = 2
pairs- This counts pairs
(1,3)
and(2,3)
where indices refer to original positions
- This counts pairs
j=1
: Is3 > 2*5
? No (3 ≤ 10), movei
forward- Continue until both pointers reach the end
- Merge phase: Merge to get
[1, 2, 3, 4, 5]
Total reverse pairs found: 0 + 1 + 2 = 3
The three reverse pairs in the original array are:
(1, 4)
:nums[1]=4 > 2*nums[4]=2
(2, 4)
:nums[2]=3 > 2*nums[4]=2
(3, 4)
:nums[3]=5 > 2*nums[4]=2
The algorithm efficiently counts these pairs by recognizing that once we find one element in the left sorted half that satisfies the condition, all subsequent elements will too.
Solution Implementation
1class Solution:
2 def reversePairs(self, nums: List[int]) -> int:
3 """
4 Count reverse pairs where i < j and nums[i] > 2 * nums[j]
5 using modified merge sort algorithm.
6
7 Args:
8 nums: List of integers
9
10 Returns:
11 Count of reverse pairs
12 """
13
14 def merge_sort_and_count(left: int, right: int) -> int:
15 """
16 Recursively divide array and count reverse pairs during merge.
17
18 Args:
19 left: Left boundary index (inclusive)
20 right: Right boundary index (inclusive)
21
22 Returns:
23 Count of reverse pairs in range [left, right]
24 """
25 # Base case: single element or invalid range
26 if left >= right:
27 return 0
28
29 # Divide the array into two halves
30 mid = (left + right) // 2
31
32 # Recursively count pairs in left half, right half, and across halves
33 count = merge_sort_and_count(left, mid) + merge_sort_and_count(mid + 1, right)
34
35 # Count reverse pairs across the two sorted halves
36 # For each element in left half, count elements in right half
37 # where nums[i] > 2 * nums[j]
38 i, j = left, mid + 1
39 while i <= mid and j <= right:
40 if nums[i] <= 2 * nums[j]:
41 i += 1
42 else:
43 # All elements from i to mid satisfy the condition
44 # with current nums[j]
45 count += mid - i + 1
46 j += 1
47
48 # Merge the two sorted halves
49 temp = []
50 i, j = left, mid + 1
51
52 # Merge elements in sorted order
53 while i <= mid and j <= right:
54 if nums[i] <= nums[j]:
55 temp.append(nums[i])
56 i += 1
57 else:
58 temp.append(nums[j])
59 j += 1
60
61 # Add remaining elements from left half
62 temp.extend(nums[i:mid + 1])
63
64 # Add remaining elements from right half
65 temp.extend(nums[j:right + 1])
66
67 # Copy sorted elements back to original array
68 nums[left:right + 1] = temp
69
70 return count
71
72 # Start merge sort from entire array
73 return merge_sort_and_count(0, len(nums) - 1)
74
1class Solution {
2 private int[] nums;
3 private int[] tempArray; // Temporary array for merge operation
4
5 /**
6 * Counts the number of reverse pairs in the array.
7 * A reverse pair is defined as (i, j) where i < j and nums[i] > 2 * nums[j].
8 *
9 * @param nums Input array
10 * @return Number of reverse pairs
11 */
12 public int reversePairs(int[] nums) {
13 this.nums = nums;
14 int length = nums.length;
15 this.tempArray = new int[length];
16
17 // Perform merge sort and count reverse pairs simultaneously
18 return mergeSort(0, length - 1);
19 }
20
21 /**
22 * Recursive merge sort that counts reverse pairs.
23 *
24 * @param left Left boundary index (inclusive)
25 * @param right Right boundary index (inclusive)
26 * @return Number of reverse pairs in the range [left, right]
27 */
28 private int mergeSort(int left, int right) {
29 // Base case: single element or invalid range
30 if (left >= right) {
31 return 0;
32 }
33
34 // Calculate middle point
35 int mid = (left + right) >> 1;
36
37 // Recursively count reverse pairs in left and right halves
38 int reversePairCount = mergeSort(left, mid) + mergeSort(mid + 1, right);
39
40 // Count reverse pairs between left and right halves
41 // Using two pointers to find pairs where nums[i] > 2 * nums[j]
42 int i = left;
43 int j = mid + 1;
44 while (i <= mid && j <= right) {
45 // Use long to avoid integer overflow when multiplying by 2
46 if (nums[i] <= nums[j] * 2L) {
47 i++;
48 } else {
49 // All elements from i to mid form reverse pairs with nums[j]
50 reversePairCount += mid - i + 1;
51 j++;
52 }
53 }
54
55 // Merge the two sorted halves
56 i = left;
57 j = mid + 1;
58 int tempIndex = 0;
59
60 // Merge elements in sorted order
61 while (i <= mid && j <= right) {
62 if (nums[i] <= nums[j]) {
63 tempArray[tempIndex++] = nums[i++];
64 } else {
65 tempArray[tempIndex++] = nums[j++];
66 }
67 }
68
69 // Copy remaining elements from left half
70 while (i <= mid) {
71 tempArray[tempIndex++] = nums[i++];
72 }
73
74 // Copy remaining elements from right half
75 while (j <= right) {
76 tempArray[tempIndex++] = nums[j++];
77 }
78
79 // Copy merged elements back to original array
80 for (i = left; i <= right; i++) {
81 nums[i] = tempArray[i - left];
82 }
83
84 return reversePairCount;
85 }
86}
87
1class Solution {
2public:
3 int reversePairs(vector<int>& nums) {
4 int n = nums.size();
5 // Temporary array for merge sort
6 vector<int> temp(n);
7
8 // Lambda function for merge sort with reverse pairs counting
9 function<int(int, int)> mergeSort = [&](int left, int right) -> int {
10 // Base case: single element or invalid range
11 if (left >= right) {
12 return 0;
13 }
14
15 // Calculate middle point
16 int mid = (left + right) >> 1;
17
18 // Recursively count reverse pairs in left and right halves
19 int count = mergeSort(left, mid) + mergeSort(mid + 1, right);
20
21 // Count reverse pairs across the two halves
22 // A reverse pair is when nums[i] > 2 * nums[j] where i < j
23 int i = left;
24 int j = mid + 1;
25 while (i <= mid && j <= right) {
26 // Use long long to prevent overflow when multiplying by 2
27 if (nums[i] <= 2LL * nums[j]) {
28 i++;
29 } else {
30 // All elements from i to mid form reverse pairs with nums[j]
31 count += mid - i + 1;
32 j++;
33 }
34 }
35
36 // Merge the two sorted halves
37 i = left;
38 j = mid + 1;
39 int k = 0;
40
41 // Merge elements in sorted order
42 while (i <= mid && j <= right) {
43 if (nums[i] <= nums[j]) {
44 temp[k++] = nums[i++];
45 } else {
46 temp[k++] = nums[j++];
47 }
48 }
49
50 // Copy remaining elements from left half
51 while (i <= mid) {
52 temp[k++] = nums[i++];
53 }
54
55 // Copy remaining elements from right half
56 while (j <= right) {
57 temp[k++] = nums[j++];
58 }
59
60 // Copy sorted elements back to original array
61 for (i = left; i <= right; i++) {
62 nums[i] = temp[i - left];
63 }
64
65 return count;
66 };
67
68 // Start merge sort from the entire array
69 return mergeSort(0, n - 1);
70 }
71};
72
1function reversePairs(nums: number[]): number {
2 const n = nums.length;
3 // Temporary array for merge sort
4 const temp: number[] = new Array(n);
5
6 // Recursive merge sort function with reverse pairs counting
7 const mergeSort = (left: number, right: number): number => {
8 // Base case: single element or invalid range
9 if (left >= right) {
10 return 0;
11 }
12
13 // Calculate middle point using bit shift for efficiency
14 const mid = (left + right) >> 1;
15
16 // Recursively count reverse pairs in left and right halves
17 let count = mergeSort(left, mid) + mergeSort(mid + 1, right);
18
19 // Count reverse pairs across the two halves
20 // A reverse pair is when nums[i] > 2 * nums[j] where i < j
21 let i = left;
22 let j = mid + 1;
23 while (i <= mid && j <= right) {
24 // Check if current element forms a reverse pair
25 // Cast to ensure no overflow when multiplying by 2
26 if (nums[i] <= 2 * nums[j]) {
27 i++;
28 } else {
29 // All elements from i to mid form reverse pairs with nums[j]
30 count += mid - i + 1;
31 j++;
32 }
33 }
34
35 // Merge the two sorted halves
36 i = left;
37 j = mid + 1;
38 let k = 0;
39
40 // Merge elements in sorted order into temporary array
41 while (i <= mid && j <= right) {
42 if (nums[i] <= nums[j]) {
43 temp[k++] = nums[i++];
44 } else {
45 temp[k++] = nums[j++];
46 }
47 }
48
49 // Copy remaining elements from left half
50 while (i <= mid) {
51 temp[k++] = nums[i++];
52 }
53
54 // Copy remaining elements from right half
55 while (j <= right) {
56 temp[k++] = nums[j++];
57 }
58
59 // Copy sorted elements back to original array
60 for (i = left; i <= right; i++) {
61 nums[i] = temp[i - left];
62 }
63
64 return count;
65 };
66
67 // Start merge sort from the entire array
68 return mergeSort(0, n - 1);
69}
70
Time and Space Complexity
Time Complexity: O(n log n)
The algorithm uses a modified merge sort approach. The merge sort divides the array into two halves recursively until single elements are reached, creating log n
levels in the recursion tree. At each level:
- The first while loop counts reverse pairs by comparing elements from left and right subarrays, taking
O(n)
time in total across all recursive calls at that level - The second while loop performs the standard merge operation, also taking
O(n)
time - The extend operations take
O(n)
time in worst case
Since we have log n
levels and each level does O(n)
work, the overall time complexity is O(n log n)
.
Space Complexity: O(n)
The space complexity consists of:
- The temporary array
t
used during merging, which can hold at mostn
elements across all recursive calls at the same level:O(n)
- The recursion call stack depth, which is
O(log n)
for the divide-and-conquer approach - Since
O(n) + O(log n) = O(n)
, the overall space complexity isO(n)
Common Pitfalls
1. Integer Overflow When Doubling Values
The Problem:
When calculating 2 * nums[j]
, integer overflow can occur if nums[j]
is very large. In Python, this isn't typically an issue due to arbitrary precision integers, but in languages like Java or C++, this can cause incorrect comparisons.
For example, if nums[j] = 2^30
, then 2 * nums[j] = 2^31
which overflows a 32-bit signed integer, potentially wrapping to a negative value and causing incorrect pair counting.
Solution:
Instead of comparing nums[i] > 2 * nums[j]
, rearrange the comparison to avoid multiplication:
# Instead of: if nums[i] > 2 * nums[j]: # Use: if nums[i] / 2.0 > nums[j]:
Or use long/64-bit integers in languages with fixed integer sizes:
# For languages with overflow concerns: if nums[i] > 2 * long(nums[j]):
2. Off-by-One Errors in Index Management
The Problem:
The algorithm uses inclusive boundaries [left, right]
which can lead to confusion. Common mistakes include:
- Using
mid
instead ofmid + 1
when starting the right half - Incorrect range calculations when counting pairs
- Wrong slice indices when copying back sorted elements
Solution: Be consistent with boundary conventions and carefully track indices:
# Ensure correct boundaries: # Left half: [left, mid] (inclusive) # Right half: [mid + 1, right] (inclusive) # When counting reverse pairs: count += mid - i + 1 # "+1" because mid is inclusive # When extending remaining elements: temp.extend(nums[i:mid + 1]) # mid + 1 for inclusive mid temp.extend(nums[j:right + 1]) # right + 1 for inclusive right
3. Modifying the Counting Logic During Merge
The Problem: A common mistake is trying to count reverse pairs during the actual merge phase instead of in a separate pass. This leads to incorrect counts because elements get moved and the relative positions change.
# WRONG: Trying to count during merge while i <= mid and j <= right: if nums[i] <= nums[j]: temp.append(nums[i]) i += 1 else: # Don't count here! Elements are being reordered if nums[i] > 2 * nums[j]: # This is wrong timing count += ... temp.append(nums[j]) j += 1
Solution: Always count reverse pairs in a separate pass before the merge operation:
# CORRECT: Count first, then merge # First pass: count reverse pairs i, j = left, mid + 1 while i <= mid and j <= right: if nums[i] <= 2 * nums[j]: i += 1 else: count += mid - i + 1 j += 1 # Second pass: merge the arrays i, j = left, mid + 1 # ... merge logic ...
4. Not Resetting Pointers Between Counting and Merging
The Problem:
After counting reverse pairs, the pointers i
and j
have moved. Forgetting to reset them before the merge phase causes incorrect merging.
Solution: Always reinitialize pointers before the merge phase:
# After counting phase i, j = left, mid + 1 # Reset pointers # Now perform merge
5. Incorrect Base Case Handling
The Problem:
Using if left > right:
instead of if left >= right:
in the base case. When left == right
, we have a single element which cannot form any pairs, so we should return 0.
Solution: Use the correct base case:
if left >= right: # Handles both empty and single-element cases return 0
Which algorithm should you use to find a node that is close to the root of the tree?
Recommended Readings
Segment Tree Faster Range Queries For this article we want to introduce the idea of a Segment Tree Segment Trees allow us to quickly perform range queries as well as range updates Suppose we have an array and we want to know the sum of a particular range of numbers as well
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
https assets algo monster cover_photos dnd svg Divide and Conquer The main idea of divide and conquer is to split the main problem into two smaller components assuming that each one of the components had already been solved recursively and then try to solve the bigger problem using the solutions
Want a Structured Path to Master System Design Too? Don’t Miss This!