+ count the number of fair pairs + — 9/13/24 +
+problem statement
+
+ Given an array nums of integers and upper/lower
+ integer bounds upper/lower respectively,
+ return the number of unique valid index pairs such that: \[i\neq
+ j,lower\leq nums[i]+nums[j]\leq upper\]
+
understanding the problem
+
+ This is another sleeper daily in which a bit of thinking in the
+ beginning pays dividends. Intuitively, I think it makes sense to
+ reduce the “dimensionality” of the problem. Choosing
+ both i and j concurrently seems tricky,
+ so let's assume we've found a valid i. What
+ must be true? Well: \[i\neq j,lower-nums[i]\leq nums[j]\leq
+ upper-nums[i]\]
+
+ It doesn't seem like we've made much progress. If nums
+ is a sequence of random integers,
+ there's truly no way to find all j satisfying
+ this condition efficiently.
+
+ The following question naturally arises: can we modify our input
+ to find such j efficiently? Recall our goal: find the
+ smallest/largest j to fit within our altered bounds—in other
+ words, find the smallest \(x\) less/greater than or equal to a
+ number. If binary search bells aren't clanging in your head
+ right now, I'm not sure what to say besides keep practicing.
+
+ So, it would be nice to sort nums to find such
+ j relatively quickly. However:
+ are we actually allowed to do this? This is the core
+ question I think everyone skips over. Maybe it is trivial but it
+ is important to emphasize:
+
-
+
-
+ Yes, we are allowed to sort the input. Re-frame the
+ problem: what we are actually doing is choosing distinct
+
i,jto satisfy some condition. The + order ofnumsdoes not matter—rather, its + contents do. Any input to this algorithm with +numswith the same contents will yield the same + result. If we were to modifynumsinstead of + rearrange it, this would be invalid because we could be + introducing/taking away valid index combinations. +
+
+ Let's consider our solution a bit more before implementing + it: +
+-
+
-
+ Is the approach feasible? We're sorting
+
numsthen binary searching over it considering all +i, which will take around \(O(nlg(n))\) time. +len(nums)\(\leq10^5\), so this is fine. +
+ -
+ How do we avoid double-counting? The logic so far makes no
+ effort. If we consider making all pairs with indices
+ less than
ifor all +ileft-to-right, we'll be considering all + valid pairs with no overlap. This is a common pattern—take + a moment to justify it to yourself. +
+ -
+ Exactly how many elements do we count? Okay, we're
+ considering some rightmost index
iand we've + found upper and lower index boundsjand +krespectively. We can pair +nums[j]with all elements up to an including +nums[k](besidesnums[j]). There are + exactly \(k-j\) of these. If the indexing confuses you, draw it + out and prove it to yourself. +
+ -
+ How do we get our final answer? Accumulate all
+
k-jfor alli. +
+
carrying out the plan
+
+ The following approach implements our logic quite elegantly and
+ directly. The third and fourth arguments to the
+ bisect calls specify lo (inclusive) and
+ hi (exclusive) bounds for our search space, mirroring
+ the criteria that we search across all indices \(\lt i\).
+
def countFairPairs(self, nums, lower, upper):
+ nums.sort()
+ ans = 0
+
+ for i, num in enumerate(nums):
+ k = bisect_left(nums, lower - num, 0, i)
+ j = bisect_right(nums, upper - num, 0, i)
+
+ ans += k - j
+
+ return ans
+ optimizing the approach
++ If we interpret the criteria this way, the above approach is + relatively efficient. To improve this approach, we'll need to + reinterpret the constraints. Forget about the indexing and + consider the constraint in aggregate. We want to find all \(i,j\) + with \(x=nums[i]+nums[j]\) such that \(i\neq j,lower\leq x\leq + upper\). +
+
+ We still need to reduce the “dimensionality” of
+ the problem—there are just too many moving parts to consider
+ at once. This seems challening. Let's simplify the problem to
+ identify helpful ideas: pretend lower does not exist
+ (and, of course, that nums is sorted).
+
+ We're looking for all index pairs with sum \(\leq upper\). + And behold: (almost) two sum in the wild. This can be accomplished + with a two-pointers approach—this post is getting quite long + so we'll skip over why this is the case—but the main + win here is that we can solve this simplified version of our + problem in \(O(n)\). +
++ Are we any closer to actually solving the problem? Now, we have + the count of index pairs \(\leq upper\). Is this our answer? + No—some may be too small, namely, with sum \(\lt lower\). + Let's exclude those by running our two-pointer approach with + and upper bound of \(lower-1\) (we want to include \(lower\)). + Now, our count reflects the total number of index pairs with a sum + in our interval bound. +
++ Note that this really is just running a prefix sum/using the + “inclusion-exclusion” principle/however you want to + phrase it. +
+def countFairPairs(self, nums, lower, upper):
+ nums.sort()
+ ans = 0
+
+ def pairs_leq(x: int) -> int:
+ pairs = 0
+ l, r = 0, len(nums) - 1
+ while l < r:
+ if nums[l] + nums[r] <= x:
+ pairs += r - l
+ l += 1
+ else:
+ r -= 1
+ return pairs
+
+ return pairs_leq(upper) - pairs_leq(lower - 1)
+ some more considerations
++ The second approach is asymptotically equivalent. However, + it's still worth considering for two reasons: +
+-
+
-
+ If an interviewer says “assume
numsis + sorted” or “how can we do + better?”—you're cooked. +
+ - + (Much) more importantly, it's extremely valuable to be able + to reconceptualize a problem and look at it from + different angles. Not being locked in on a solution shows + perseverance, curiosity, and strong problem-solving abilities. + +
asymptotic complexity
+
+ Time Complexity: \(O(nlg(n))\) for both—\(O(n)\) if
+ nums is sorted with respect to the second approach.
+
Space Complexity: \(\Theta(1)\) for both.
+