2563. Count the Number of Fair Pairs
Problem Description
You are given a 0-indexed integer array nums
of size n
and two integers lower
and upper
. Your task is to count the number of "fair pairs" in the array.
A pair (i, j)
is considered fair if it satisfies both of the following conditions:
- The indices must satisfy
0 <= i < j < n
(meaningi
comes beforej
in the array) - The sum of the elements at these indices must be within the given range:
lower <= nums[i] + nums[j] <= upper
For example, if nums = [0, 1, 7, 4]
, lower = 3
, and upper = 6
:
- Pair
(0, 1)
:nums[0] + nums[1] = 0 + 1 = 1
, which is not in range[3, 6]
- not fair - Pair
(0, 2)
:nums[0] + nums[2] = 0 + 7 = 7
, which is not in range[3, 6]
- not fair - Pair
(0, 3)
:nums[0] + nums[3] = 0 + 4 = 4
, which is in range[3, 6]
- fair - Pair
(1, 2)
:nums[1] + nums[2] = 1 + 7 = 8
, which is not in range[3, 6]
- not fair - Pair
(1, 3)
:nums[1] + nums[3] = 1 + 4 = 5
, which is in range[3, 6]
- fair - Pair
(2, 3)
:nums[2] + nums[3] = 7 + 4 = 11
, which is not in range[3, 6]
- not fair
So the answer would be 2 fair pairs.
The solution uses sorting and binary search to efficiently count fair pairs. After sorting the array, for each element nums[i]
, it finds the range of valid nums[j]
values (where j > i
) that would create a sum within [lower, upper]
. This is done by finding indices using binary search: the smallest index j
where nums[j] >= lower - nums[i]
and the smallest index k
where nums[k] >= upper - nums[i] + 1
. The count of valid pairs with nums[i]
as the first element is then k - j
.
Intuition
The naive approach would be to check every possible pair (i, j)
where i < j
, calculate their sum, and count how many fall within [lower, upper]
. This would take O(n²)
time, which might be too slow for large arrays.
The key insight is that once we fix the first element nums[i]
, we're looking for all nums[j]
(where j > i
) such that:
lower <= nums[i] + nums[j] <= upper
We can rearrange this inequality to isolate nums[j]
:
lower - nums[i] <= nums[j] <= upper - nums[i]
This means for a fixed nums[i]
, we need to count how many elements in the remaining array fall within the range [lower - nums[i], upper - nums[i]]
.
If the array is sorted, all valid nums[j]
values will form a contiguous segment. Why? Because in a sorted array, if some nums[j]
satisfies nums[j] >= lower - nums[i]
, then all elements after it that are still <= upper - nums[i]
will also be valid.
This leads us to use binary search on a sorted array:
- Sort the array first
- For each
nums[i]
, find the leftmost position wherenums[j] >= lower - nums[i]
(this is the start of valid range) - Find the leftmost position where
nums[j] >= upper - nums[i] + 1
(this is one position past the end of valid range) - The count of valid pairs with
nums[i]
as the first element is the difference between these two positions
The reason we search for upper - nums[i] + 1
instead of upper - nums[i]
is because we want to find the first element that's too large to be valid, which gives us the exclusive upper bound of our range.
By sorting once (O(n log n)
) and using binary search for each element (O(n log n)
total), we achieve an overall time complexity of O(n log n)
, which is much better than the naive O(n²)
approach.
Learn more about Two Pointers, Binary Search and Sorting patterns.
Solution Approach
The implementation follows the sorting and binary search strategy outlined in the reference approach:
Step 1: Sort the array
nums.sort()
We sort the array in ascending order to enable binary search. This allows us to efficiently find ranges of valid pairs.
Step 2: Initialize counter
ans = 0
This variable will accumulate the total count of fair pairs.
Step 3: Iterate through each element as the first element of the pair
for i, x in enumerate(nums):
We iterate through each element x
(which is nums[i]
) that will serve as the first element in our pair (i, j)
.
Step 4: Find the lower bound of valid second elements
j = bisect_left(nums, lower - x, lo=i + 1)
Using bisect_left
, we find the smallest index j
where nums[j] >= lower - x
. The parameter lo=i + 1
ensures we only search among indices greater than i
(to maintain i < j
). This gives us the starting position of valid nums[j]
values.
Step 5: Find the upper bound of valid second elements
k = bisect_left(nums, upper - x + 1, lo=i + 1)
We find the smallest index k
where nums[k] >= upper - x + 1
. This gives us the first position that's beyond our valid range. Again, lo=i + 1
ensures we only look at indices after i
.
Step 6: Count valid pairs for current nums[i]
ans += k - j
The number of valid nums[j]
values is k - j
. These are all the elements in the range [j, k)
that, when paired with nums[i]
, produce a sum within [lower, upper]
.
Why this works:
- For a fixed
nums[i]
, we neednums[j]
such thatlower - nums[i] <= nums[j] <= upper - nums[i]
- In the sorted array, all such
nums[j]
form a contiguous segment from indexj
to indexk-1
bisect_left(nums, lower - x, lo=i + 1)
finds where this segment startsbisect_left(nums, upper - x + 1, lo=i + 1)
finds where this segment ends (exclusive)- The difference
k - j
gives us the count of valid pairs withnums[i]
as the first element
Time Complexity: O(n log n)
- sorting takes O(n log n)
, and we perform two binary searches for each of the n
elements, resulting in O(n log n)
for the search operations.
Space Complexity: O(1)
or O(n)
depending on the sorting algorithm used (Python's sort uses Timsort which can use up to O(n)
auxiliary space).
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 walk through a small example to illustrate the solution approach.
Example: nums = [0, 1, 7, 4, 4, 5]
, lower = 3
, upper = 6
Step 1: Sort the array
After sorting: nums = [0, 1, 4, 4, 5, 7]
Step 2: Process each element as the first element of a pair
Let's trace through each iteration:
i = 0, x = 0:
- Need to find
nums[j]
where3 - 0 <= nums[j] <= 6 - 0
- So we need
3 <= nums[j] <= 6
j = bisect_left([1, 4, 4, 5, 7], 3, lo=1)
= 2 (first element >= 3 is at index 2, which is 4)k = bisect_left([1, 4, 4, 5, 7], 7, lo=1)
= 5 (first element >= 7 is at index 5, which is 7)- Valid pairs with nums[0]=0: indices 2, 3, 4 (values 4, 4, 5)
- Count: 5 - 2 = 3 pairs
i = 1, x = 1:
- Need
nums[j]
where3 - 1 <= nums[j] <= 6 - 1
- So we need
2 <= nums[j] <= 5
j = bisect_left([4, 4, 5, 7], 2, lo=2)
= 2 (first element >= 2 is at index 2, which is 4)k = bisect_left([4, 4, 5, 7], 6, lo=2)
= 5 (first element >= 6 is at index 5, which is 7)- Valid pairs with nums[1]=1: indices 2, 3, 4 (values 4, 4, 5)
- Count: 5 - 2 = 3 pairs
i = 2, x = 4:
- Need
nums[j]
where3 - 4 <= nums[j] <= 6 - 4
- So we need
-1 <= nums[j] <= 2
j = bisect_left([4, 5, 7], -1, lo=3)
= 3 (all elements are >= -1)k = bisect_left([4, 5, 7], 3, lo=3)
= 3 (no elements in range [3,∞) starting from index 3 are < 3)- Valid pairs with nums[2]=4: none (no values in our range)
- Count: 3 - 3 = 0 pairs
i = 3, x = 4:
- Need
nums[j]
where-1 <= nums[j] <= 2
j = bisect_left([5, 7], -1, lo=4)
= 4k = bisect_left([5, 7], 3, lo=4)
= 4- Valid pairs with nums[3]=4: none
- Count: 4 - 4 = 0 pairs
i = 4, x = 5:
- Need
nums[j]
where3 - 5 <= nums[j] <= 6 - 5
- So we need
-2 <= nums[j] <= 1
j = bisect_left([7], -2, lo=5)
= 5k = bisect_left([7], 2, lo=5)
= 5- Valid pairs with nums[4]=5: none
- Count: 5 - 5 = 0 pairs
i = 5, x = 7:
- This is the last element, so no pairs can be formed (no j > i exists)
Total fair pairs: 3 + 3 + 0 + 0 + 0 = 6
The fair pairs are:
- (0,4): 0 + 4 = 4 ✓
- (0,4): 0 + 4 = 4 ✓ (second 4)
- (0,5): 0 + 5 = 5 ✓
- (1,4): 1 + 4 = 5 ✓
- (1,4): 1 + 4 = 5 ✓ (second 4)
- (1,5): 1 + 5 = 6 ✓
Solution Implementation
1from typing import List
2from bisect import bisect_left
3
4class Solution:
5 def countFairPairs(self, nums: List[int], lower: int, upper: int) -> int:
6 # Sort the array to enable binary search
7 nums.sort()
8
9 # Initialize counter for fair pairs
10 count = 0
11
12 # Iterate through each element as the first element of a pair
13 for i, num in enumerate(nums):
14 # Find the leftmost position where nums[j] >= lower - num
15 # This gives us the starting index of valid second elements
16 left_bound = bisect_left(nums, lower - num, lo=i + 1)
17
18 # Find the leftmost position where nums[k] > upper - num
19 # This gives us the index just after the last valid second element
20 right_bound = bisect_left(nums, upper - num + 1, lo=i + 1)
21
22 # The number of valid pairs with nums[i] as first element
23 # is the count of elements in range [left_bound, right_bound)
24 count += right_bound - left_bound
25
26 return count
27
1class Solution {
2 /**
3 * Counts the number of fair pairs (i, j) where i < j and lower <= nums[i] + nums[j] <= upper
4 *
5 * @param nums The input array of integers
6 * @param lower The lower bound for the sum of a fair pair
7 * @param upper The upper bound for the sum of a fair pair
8 * @return The count of fair pairs
9 */
10 public long countFairPairs(int[] nums, int lower, int upper) {
11 // Sort the array to enable binary search
12 Arrays.sort(nums);
13
14 long fairPairCount = 0;
15 int arrayLength = nums.length;
16
17 // For each element, find valid pairs using binary search
18 for (int i = 0; i < arrayLength; ++i) {
19 // Find the leftmost index where nums[i] + nums[j] >= lower
20 // This means nums[j] >= lower - nums[i]
21 int leftBoundIndex = binarySearchLeftmost(nums, lower - nums[i], i + 1);
22
23 // Find the leftmost index where nums[i] + nums[j] > upper
24 // This means nums[j] >= upper - nums[i] + 1
25 int rightBoundIndex = binarySearchLeftmost(nums, upper - nums[i] + 1, i + 1);
26
27 // The count of valid pairs for current nums[i] is the range [leftBoundIndex, rightBoundIndex)
28 fairPairCount += rightBoundIndex - leftBoundIndex;
29 }
30
31 return fairPairCount;
32 }
33
34 /**
35 * Binary search to find the leftmost index where nums[index] >= target
36 *
37 * @param nums The sorted array to search in
38 * @param target The target value to search for
39 * @param startIndex The starting index for the search (inclusive)
40 * @return The leftmost index where nums[index] >= target, or nums.length if all elements < target
41 */
42 private int binarySearchLeftmost(int[] nums, int target, int startIndex) {
43 int left = startIndex;
44 int right = nums.length;
45
46 // Binary search for the leftmost position where nums[mid] >= target
47 while (left < right) {
48 int mid = (left + right) >> 1; // Equivalent to (left + right) / 2
49
50 if (nums[mid] >= target) {
51 // Target found or current element is greater, search left half
52 right = mid;
53 } else {
54 // Current element is less than target, search right half
55 left = mid + 1;
56 }
57 }
58
59 return left;
60 }
61}
62
1class Solution {
2public:
3 long long countFairPairs(vector<int>& nums, int lower, int upper) {
4 long long result = 0;
5
6 // Sort the array to enable binary search
7 sort(nums.begin(), nums.end());
8
9 // For each element, find valid pairs where nums[i] + nums[j] is in [lower, upper]
10 for (int i = 0; i < nums.size(); ++i) {
11 // Find the first index j where nums[i] + nums[j] >= lower
12 // This means nums[j] >= lower - nums[i]
13 auto leftBound = lower_bound(nums.begin() + i + 1, nums.end(), lower - nums[i]);
14
15 // Find the first index k where nums[i] + nums[k] > upper
16 // This means nums[k] >= upper - nums[i] + 1
17 auto rightBound = lower_bound(nums.begin() + i + 1, nums.end(), upper - nums[i] + 1);
18
19 // Count of valid pairs with nums[i] as the first element
20 // All indices in [leftBound, rightBound) form valid pairs with nums[i]
21 result += rightBound - leftBound;
22 }
23
24 return result;
25 }
26};
27
1/**
2 * Counts the number of fair pairs in an array where the sum is between lower and upper bounds (inclusive)
3 * @param nums - The input array of numbers
4 * @param lower - The lower bound for the sum of pairs
5 * @param upper - The upper bound for the sum of pairs
6 * @returns The count of fair pairs where lower <= nums[i] + nums[j] <= upper
7 */
8function countFairPairs(nums: number[], lower: number, upper: number): number {
9 /**
10 * Binary search to find the leftmost position where nums[mid] >= target
11 * @param target - The target value to search for
12 * @param startIndex - The starting index for the search
13 * @returns The index of the leftmost element >= target
14 */
15 const binarySearchLeftmost = (target: number, startIndex: number): number => {
16 let left = startIndex;
17 let right = nums.length;
18
19 while (left < right) {
20 const mid = (left + right) >> 1; // Equivalent to Math.floor((left + right) / 2)
21
22 if (nums[mid] >= target) {
23 // Target might be at mid or to the left
24 right = mid;
25 } else {
26 // Target is definitely to the right
27 left = mid + 1;
28 }
29 }
30
31 return left;
32 };
33
34 // Sort the array in ascending order to enable binary search
35 nums.sort((a, b) => a - b);
36
37 let fairPairsCount = 0;
38
39 // For each element, find valid pairs using binary search
40 for (let i = 0; i < nums.length; ++i) {
41 // Find the first index where nums[j] >= lower - nums[i]
42 // This gives us the start of the valid range for nums[j]
43 const lowerBoundIndex = binarySearchLeftmost(lower - nums[i], i + 1);
44
45 // Find the first index where nums[j] >= upper - nums[i] + 1
46 // This gives us the index just after the valid range for nums[j]
47 const upperBoundIndex = binarySearchLeftmost(upper - nums[i] + 1, i + 1);
48
49 // The number of valid pairs with nums[i] is the difference between indices
50 fairPairsCount += upperBoundIndex - lowerBoundIndex;
51 }
52
53 return fairPairsCount;
54}
55
Time and Space Complexity
Time Complexity: O(n × log n)
The time complexity consists of two main parts:
- Sorting the array takes
O(n × log n)
time - The for loop runs
n
times, and within each iteration:bisect_left
is called twice, each takingO(log n)
time for binary search- Other operations are
O(1)
- Total per iteration:
O(log n)
- Overall loop complexity:
O(n × log n)
Since both sorting and the loop have O(n × log n)
complexity, the total time complexity is O(n × log n)
.
Space Complexity: O(log n)
The space complexity comes from:
- The sorting algorithm (typically Timsort in Python) uses
O(log n)
space for its recursion stack - The variables
ans
,i
,x
,j
, andk
useO(1)
space - No additional data structures are created
Therefore, the total space complexity is O(log n)
, where n
is the length of the array nums
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Forgetting to Handle the Index Constraint After Sorting
The Problem:
After sorting the array, the original indices are lost. A common mistake is to think that we still need to maintain the original i < j
constraint from the problem statement. This leads to incorrect implementations where developers try to track original indices or apply unnecessary filtering.
Why It Happens:
The problem states that pairs (i, j)
must satisfy 0 <= i < j < n
, which refers to indices in the original array. After sorting, some might think they need to preserve this original ordering relationship.
The Correct Understanding:
Once we sort the array, we're working with a new arrangement. The constraint i < j
in the sorted array is sufficient because:
- We're counting pairs, not returning the actual index pairs
- Any pair of distinct elements will be counted exactly once (when the smaller index is
i
and the larger isj
) - The sum
nums[i] + nums[j]
is the same regardless of which element comes first in the original array
Pitfall 2: Incorrect Boundary Calculation for Binary Search
The Problem:
Using bisect_right
instead of bisect_left
for finding the upper boundary, or incorrectly calculating the search target as upper - num
instead of upper - num + 1
.
Incorrect Implementation:
# WRONG: This will include elements where nums[j] == upper - num right_bound = bisect_right(nums, upper - num, lo=i + 1)
Why It's Wrong:
We want to find the first index where nums[j] > upper - num
, which is equivalent to finding where nums[j] >= upper - num + 1
. Using bisect_right
with upper - num
would give us the position after all elements equal to upper - num
, but this doesn't correctly handle the case when no such elements exist.
Correct Implementation:
# CORRECT: Find first position where nums[j] >= upper - num + 1 right_bound = bisect_left(nums, upper - num + 1, lo=i + 1)
Pitfall 3: Off-by-One Error in Binary Search Range
The Problem:
Using lo=i
instead of lo=i+1
in the binary search calls, which would allow pairing an element with itself.
Incorrect Implementation:
# WRONG: This allows i == j, counting an element paired with itself left_bound = bisect_left(nums, lower - num, lo=i)
Why It's Wrong:
The problem requires i < j
, meaning we cannot pair an element with itself. Starting the search from i
would include nums[i]
in the potential partners for itself when 2 * nums[i]
falls within [lower, upper]
.
Correct Implementation:
# CORRECT: Start searching from i+1 to ensure i < j left_bound = bisect_left(nums, lower - num, lo=i + 1)
Pitfall 4: Integer Overflow in Languages with Fixed Integer Size
The Problem:
In languages like Java or C++, calculating nums[i] + nums[j]
directly might cause integer overflow if the values are large.
Solution for Other Languages:
# In Python, this is not an issue due to arbitrary precision integers # But in Java/C++, you might need to rearrange the comparison: # Instead of: nums[i] + nums[j] <= upper # Use: nums[j] <= upper - nums[i] # This avoids the addition that could overflow
Example Demonstrating Pitfall 2:
Consider nums = [1, 2, 3]
, lower = 3
, upper = 4
:
- For
nums[0] = 1
, we want pairs where3 <= 1 + nums[j] <= 4
- This means
2 <= nums[j] <= 3
- Valid values are
nums[1] = 2
andnums[2] = 3
With incorrect boundary:
bisect_right(nums, 3, lo=1)
returns 3 (position after the last 3)bisect_left(nums, 2, lo=1)
returns 1- Count: 3 - 1 = 2 ✓ (happens to work in this case)
But consider nums = [1, 2, 3, 4]
, lower = 3
, upper = 4
:
- For
nums[0] = 1
, we want2 <= nums[j] <= 3
bisect_right(nums, 3, lo=1)
returns 3- But
bisect_left(nums, 4, lo=1)
would give us 3 as well - This wouldn't correctly exclude
nums[3] = 4
With correct boundary:
bisect_left(nums, upper - num + 1, lo=1)
=bisect_left(nums, 4, lo=1)
= 3- This correctly identifies that index 3 is the first position outside our valid range
Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?
Recommended Readings
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
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
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Want a Structured Path to Master System Design Too? Don’t Miss This!