diff --git a/posts/algorithms/leetcode-daily.html b/posts/algorithms/leetcode-daily.html index 09e5a7d..ae7534c 100644 --- a/posts/algorithms/leetcode-daily.html +++ b/posts/algorithms/leetcode-daily.html @@ -38,6 +38,206 @@

Leetcode Daily

+
+

+ 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: +

+ +

+ Let's consider our solution a bit more before implementing + it: +

+ +

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: +

+
    +
  1. + If an interviewer says “assume nums is + sorted” or “how can we do + better?”—you're cooked. +
  2. +
  3. + (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. +
  4. +
+

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.

+