493. Reverse Pairs
Problem Description
The problem requires us to find the number of reverse pairs in an integer array nums
. A reverse pair is defined by a pair of indices (i, j)
where i
is less than j
and the element at the i
-th position of the array is more than twice the element at the j
-th position. In other words, we need to count the number of times where nums[i] > 2 * nums[j]
for 0 <= i < j < nums.length
.
Intuition
This problem is a variant of the classic "counting inversions" problem, which is a common problem in sorting and can be solved by a modified merge-sort algorithm. However, in this case, due to the condition nums[i] > 2 * nums[j]
, we cannot directly apply a standard merge-sort approach.
The intuition behind the solution is to use a data structure that enables us to efficiently update and query the number of elements that meet the reverse pair criteria. A Binary Indexed Tree (BIT), which is also known as a Fenwick Tree, or a Segment Tree could be used for this purpose. These structures allow us to perform both update and query operations in logarithmic time, which is ideal for problems like this one where we have range query requirements and update needs.
The core of the solution is to iterate over the elements in reverse, and for each element, query how many elements before it are considered to be in a reverse pair with it (using the BIT or Segment Tree to do this efficiently). Finally, we update our data structure with the current element to account for future queries. We also preprocess the elements to create a mapping of elements to their ranks (as indices in BIT or Segment Tree) because these data structures typically require zero-based or continuous indexing.
Learn more about Segment Tree, Binary Search, Divide and Conquer and Merge Sort patterns.
Solution Approach
The solution approach involves using a Binary Indexed Tree (BIT), also known as a Fenwick Tree, to efficiently count the reverse pairs. The BIT is a data structure that allows us to perform get and update prefix sums in logarithmic time complexity. Here's a step-by-step breakdown of how the solution works:
-
Preprocessing:
- We create a set of unique values twice - once with all current elements and once with their double value. This step is important to avoid dealing with the potential issue of large values in the BIT.
- We then sort these values to assign a rank or index to each distinct value. The use of indexing is crucial because BITs operate on indices, and this also helps compress the values to a smaller range.
-
Mapping:
- A mapping is created from the actual value in the array to the rank/position in our sorted set. This allows us to translate the elements of our original array to the indices that we'll use in the BIT, as BITs are index-based.
-
BIT Initialization:
- We initialize our BIT with the number of unique ranks as its size.
-
Iterating and Updating BIT:
- We iterate through the original array in reverse order.
- For each element
num
, we perform a query (get prefix sum) operation form[num] - 1
on our BIT. This gives us the count of reverse pairs found so far for that particular element.- The reason for querying
m[num] - 1
is to get the count of all the elements that are less than the current elementnum
. Since we're querying in reverse, this would give us the number of previous elements thatnum
could form a reverse pair with.
- The reason for querying
- Next, we update our BIT with
m[num * 2]
, incrementing the count at this rank/index, preparing for future queries that might pair with double the current element.
-
Return Answer:
- The variable
ans
is updated at every iteration with the result of each query, cumulatively storing the total number of reverse pairs. After iterating through all the elements,ans
contains the final count of reverse pairs, which we return.
- The variable
By leveraging the BIT, the time complexity is reduced to O(nlogn), as both updates and queries to the BIT are O(logn). This is in contrast to a brute force solution that would be O(n^2) due to nested loops for comparing elements.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
To illustrate the solution approach, let's consider a small example with the integer array nums = [5, 3, 1, 2]
.
Step 1: Preprocessing
First, we need to consider all the elements and their double value:
- Original elements:
[5, 3, 1, 2]
- Double values:
[10, 6, 2, 4]
Combine and remove duplicates: [5, 3, 1, 2, 10, 6, 4]
Next, we sort these values to determine the rank of each one:
- Sorted:
[1, 2, 3, 4, 5, 6, 10]
Step 2: Mapping
We map each original value to its rank in the sorted array:
1 -> 0
2 -> 1
3 -> 2
5 -> 4
Step 3: BIT Initialization
Initialize a BIT with a length equal to the number of unique values (7 in this case).
Step 4: Iterating and Updating BIT
Now iterate through nums
in reverse order, and for each element num
:
- Perform a BIT query to get the count of elements less than
num
(ranking-wise). These counts correspond to the number of reverse pairs found for that element. - Update the BIT with the double of
num
to prepare for future queries.
Let's walk through the steps:
-
Starting with element
2
:- Perform BIT query for elements less than
2
(which is of rank1
):BIT_query(0)
– found 0 pairs. - Update BIT with
4
(the double of2
, which is of rank3
):BIT_update(3, 1)
.
- Perform BIT query for elements less than
-
Next,
1
:- Perform BIT query for elements less than
1
(which is of rank0
):BIT_query(-1)
– found 0 pairs. (Note: querying-1
effectively means we are looking for nothing, thus always 0) - Update BIT with
2
(the double of1
, which is of rank1
):BIT_update(1, 1)
.
- Perform BIT query for elements less than
-
Then,
3
:- Perform BIT query for elements less than
3
(which is of rank2
):BIT_query(1)
– found 1 pair (previously updated by1
). - Update BIT with
6
(the double of3
, which is of rank5
):BIT_update(5, 1)
.
- Perform BIT query for elements less than
-
Finally,
5
:- Perform BIT query for elements less than
5
(which is of rank4
):BIT_query(3)
– found 1 pair (previously updated by2
). - Update BIT with
10
(the double of5
, which is not in the array but would be of the last rank6
):BIT_update(6, 1)
.
- Perform BIT query for elements less than
Step 5: Return Answer
- The variable
ans
is updated at every iteration with the result of each query, which are 0, 0, 1, and 1, respectively, soans = 2
. After completing the iteration, we returnans
as the total count of reverse pairs, which is2
for this example.
The answers derived in this walkthrough correspond to the pairs (i, j)
such that nums[i] > 2 * nums[j]
: (2, 1)
and (3, 1)
(note that the indices are based on 0-indexing).
Solution Implementation
1# Import the List type from the typing module for type hints.
2from typing import List
3
4# Define the BinaryIndexedTree class for performing efficient
5# frequency queries and updates on an array.
6class BinaryIndexedTree:
7 # Initialize the binary indexed tree with size n.
8 def __init__(self, n: int):
9 self.size = n
10 self.tree_array = [0] * (n + 1)
11
12 # Define a static method to get the lowest set bit of a number.
13 @staticmethod
14 def lowbit(x: int) -> int:
15 return x & -x
16
17 # Update method for increasing the value at index x by 'delta'.
18 def update(self, x: int, delta: int):
19 while x <= self.size:
20 self.tree_array[x] += delta
21 x += BinaryIndexedTree.lowbit(x)
22
23 # Query method to calculate the prefix sum up to index x.
24 def query(self, x: int) -> int:
25 sum_ = 0
26 while x > 0:
27 sum_ += self.tree_array[x]
28 x -= BinaryIndexedTree.lowbit(x)
29 return sum_
30
31
32# Define the Solution class which will use the BinaryIndexedTree to solve problems.
33class Solution:
34 # The reversePairs function counts the number of important reverse pairs in nums.
35 def reversePairs(self, nums: List[int]) -> int:
36 all_values = set() # Use a set to store all unique numbers and their double.
37
38 # Populate the set with each number and its double.
39 for num in nums:
40 all_values.add(num)
41 all_values.add(num * 2)
42
43 # Sort and enumerate the unique values starting from index 1.
44 sorted_values = sorted(all_values)
45 index_mapping = {value: index for index, value in enumerate(sorted_values, 1)}
46
47 # Initialize the answer counter to 0.
48 ans = 0
49 tree = BinaryIndexedTree(len(index_mapping))
50
51 # Iterate through nums in reverse order.
52 for num in reversed(nums):
53 # Query the tree to find how many numbers are smaller than num.
54 ans += tree.query(index_mapping[num] - 1)
55 # Update the tree to indicate another number that is double the current number.
56 tree.update(index_mapping[num * 2], 1)
57
58 # Return the final count of important reverse pairs.
59 return ans
60
1public class Solution {
2 // A method for reversing pairs in the array and returning the count
3 public int reversePairs(int[] nums) {
4 // TreeSet to store unique numbers and their double values
5 TreeSet<Long> treeSet = new TreeSet<>();
6 for (int num : nums) {
7 treeSet.add((long) num);
8 treeSet.add((long) num * 2);
9 }
10
11 // Mapping each unique number to its index position
12 Map<Long, Integer> indexMapping = new HashMap<>();
13 int index = 0;
14 for (long num : treeSet) {
15 indexMapping.put(num, ++index);
16 }
17
18 // Using a Binary Indexed Tree (Fenwick Tree) for maintaining prefix sums
19 BinaryIndexedTree binaryIndexedTree = new BinaryIndexedTree(indexMapping.size());
20
21 // The count of reverse pairs
22 int count = 0;
23
24 // Traverse the array from right to left
25 for (int i = nums.length - 1; i >= 0; --i) {
26 int x = indexMapping.get((long) nums[i]);
27 // Query the count of elements smaller than the current number
28 count += binaryIndexedTree.query(x - 1);
29 // Update the count of elements which are double the current number
30 binaryIndexedTree.update(indexMapping.get((long) nums[i] * 2), 1);
31 }
32 return count; // Return the total count of the reverse pairs
33 }
34}
35
36// Class representing the Binary Indexed Tree (Fenwick Tree)
37class BinaryIndexedTree {
38 private int size; // Size of the tree
39 private int[] tree; // Actual tree represented as an array
40
41 // Constructor to initialize the tree with the given size
42 public BinaryIndexedTree(int size) {
43 this.size = size;
44 tree = new int[size + 1]; // One extra index for easier calculations
45 }
46
47 // Method to update the tree with the given value (delta) at the given index (x)
48 public void update(int index, int delta) {
49 while (index <= size) {
50 tree[index] += delta; // Add delta to the current index
51 index += lowbit(index); // Move to the next index that needs to be updated
52 }
53 }
54
55 // Method to query the prefix sum up to the given index (x)
56 public int query(int index) {
57 int sum = 0;
58 while (index > 0) {
59 sum += tree[index]; // Add the value at the current index to sum
60 index -= lowbit(index); // Move to the parent index
61 }
62 return sum;
63 }
64
65 // Utility method to calculate the lowest bit (-x) of a number (x)
66 public static int lowbit(int x) {
67 return x & -x;
68 }
69}
70
1#include <vector>
2#include <set>
3#include <unordered_map>
4using namespace std;
5
6class BinaryIndexedTree {
7public:
8 int size;
9 vector<int> tree_array;
10
11 // Constructor to initialize the Binary Indexed Tree with a given size
12 BinaryIndexedTree(int _size)
13 : size(_size)
14 , tree_array(_size + 1) {}
15
16 // Increment the value at index x in the Binary Indexed Tree by 'delta'
17 void update(int x, int delta) {
18 while (x <= size) {
19 tree_array[x] += delta;
20 x += lowbit(x); // Go to the next index to update
21 }
22 }
23
24 // Compute the prefix sum up to index x
25 int query(int x) {
26 int sum = 0;
27 while (x > 0) {
28 sum += tree_array[x]; // Add the value at index x to the sum
29 x -= lowbit(x); // Move to the previous index
30 }
31 return sum;
32 }
33
34 // Helper function to get the lowest bit of the binary representation of x
35 int lowbit(int x) {
36 return x & -x;
37 }
38};
39
40class Solution {
41public:
42 int reversePairs(vector<int>& nums) {
43 set<long long> all_numbers;
44 // Store each number and its double in the set to normalize the values
45 for (int num : nums) {
46 all_numbers.insert(num);
47 all_numbers.insert(static_cast<long long>(num) * 2);
48 }
49
50 unordered_map<long long, int> value_to_index;
51 int index = 0;
52 // Assign a unique index to each number in the normalized set
53 for (long long num : all_numbers) value_to_index[num] = ++index;
54
55 // Instantiate a Binary Indexed Tree with the normalized index size
56 BinaryIndexedTree tree(m.size());
57 int pairs_count = 0; // Initialize pairs counter
58
59 // Iterate over the numbers from the end to the beginning
60 for (int i = nums.size() - 1; i >= 0; --i) {
61 // Query the number of elements less than current number.
62 pairs_count += tree.query(value_to_index[nums[i]] - 1);
63 // Update the BIT with the current number's double
64 tree.update(value_to_index[static_cast<long long>(nums[i]) * 2], 1);
65 }
66
67 return pairs_count; // Return the final count of reverse pairs
68 }
69};
70
1// Define the size of the binary indexed tree
2let size: number;
3// An array to represent the binary indexed tree
4let treeArray: number[];
5
6// Function to initialize the Binary Indexed Tree with a given size
7function initializeBIT(_size: number): void {
8 size = _size;
9 treeArray = new Array(_size + 1).fill(0);
10}
11
12// Function to increment the value at index `x` in the Binary Indexed Tree by `delta`
13function update(x: number, delta: number): void {
14 while (x <= size) {
15 treeArray[x] += delta;
16 x += lowbit(x); // Go to the next index to update
17 }
18}
19
20// Function to compute the prefix sum up to index `x`
21function query(x: number): number {
22 let sum = 0;
23 while (x > 0) {
24 sum += treeArray[x]; // Add the value at index `x` to `sum`
25 x -= lowbit(x); // Move to the previous index
26 }
27 return sum;
28}
29
30// Helper function to get the lowest bit of the binary representation of `x`
31function lowbit(x: number): number {
32 return x & -x;
33}
34
35// Function to count the reverse pairs in the array `nums`
36function reversePairs(nums: number[]): number {
37 const allNumbers = new Set<number>();
38 // Store each number and its double in the set to normalize the values
39 nums.forEach(num => {
40 allNumbers.add(num);
41 allNumbers.add(num * 2);
42 });
43
44 const valueToIndex = new Map<number, number>();
45 let index = 0;
46 // Assign a unique index to each number in the normalized set
47 allNumbers.forEach(num => {
48 valueToIndex.set(num, ++index);
49 });
50
51 // Instantiate a Binary Indexed Tree with the normalized index size
52 initializeBIT(allNumbers.size);
53 let pairsCount = 0; // Initialize pairs counter
54
55 // Iterate over the numbers from the end to the beginning
56 for (let i = nums.length - 1; i >= 0; --i) {
57 // Query the number of elements less than the current number
58 pairsCount += query(valueToIndex.get(nums[i])! - 1);
59 // Update the BIT with the current number's double
60 update(valueToIndex.get(nums[i] * 2)!, 1);
61 }
62
63 return pairsCount; // Return the final count of reverse pairs
64}
65
Time and Space Complexity
The given Python code implements a solution to count reverse pairs in an array using a Binary Indexed Tree (also known as a Fenwick Tree). Let's analyze its time and space complexity:
Time Complexity
-
Initialization: Creating the
BinaryIndexedTree
object takesO(n)
time as it initializesself.c
withn+1
zeros. -
Creating set and sorting:
- Adding every
num
andnum*2
to the sets
takesO(n)
time, wheren
is the number of elements innums
. - The
sorted(s)
function then sorts the unique elements, which takesO(u log u)
time, whereu
represents the number of unique elements (after considering bothnum
andnum*2
for everynum
).
- Adding every
-
HashMap Creation: Creating the hashmap
m
(to assign ranks) takesO(u)
given thatu
is the number of elements inalls
. -
Loop Through nums and Update Tree:
- The for loop iterates
n
times since it enumerates all elements innums
. - Inside the loop, the
tree.query
function calls takeO(log n)
time each (because the range of the Binary Indexed Tree is based on the number of unique elementsu
, andu
can be at most2n
). - The
tree.update
function is also called within the loop which similarly takesO(log n)
time per call.
Therefore:
- The time for all the queries is
O(n log n)
. - The time for all the updates is also
O(n log n)
.
- The for loop iterates
If n
is the number of elements in the input nums
and u
is the number of unique elements after considering both num
and num*2
, the overall time complexity is O(n + u log u + u + n log n)
. Since u
can be at most 2n
, the time complexity simplifies to O(n log n)
.
Space Complexity
-
Space for the BinaryIndexedTree: The tree uses an array of size
n+1
for storing the cumulative frequencies which results inO(n)
space complexity. -
Space for Set and HashMap: The set and hashmap together take up
O(u)
space whereu
is the number of unique elements after considering bothnum
andnum*2
. -
Space for Sorted List: The
sorted
call produces a sorted list of all unique elements which occupiesO(u)
space.
Since u
can be at most 2n
, the overall space complexity of the code is O(n)
(since O(n + u)
simplifies to O(n)
if u <= 2n
).
Learn more about how to find time and space complexity quickly using problem constraints.
A heap is a ...?
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 algomonster s3 us east 2 amazonaws com 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
Want a Structured Path to Master System Design Too? Don’t Miss This!