diff --git a/posts/algorithms/leetcode-daily.html b/posts/algorithms/leetcode-daily.html deleted file mode 100644 index 93d0262..0000000 --- a/posts/algorithms/leetcode-daily.html +++ /dev/null @@ -1,693 +0,0 @@ - - - - - - - - - - - - - - Barrett Ruth - - -
- -
- barrett@ruth:~$ /algorithms - -
-
-
-
-
-
-

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

-
    -
  • - Yes, we are allowed to sort the input. Re-frame the - problem: what we are actually doing is choosing distinct - i, j to satisfy some condition. The - order of nums does not matter—rather, its - contents do. Any input to this algorithm with - nums with the same contents will yield the same - result. If we were to modify nums instead 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 - nums then 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 i for all - i left-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 i and we've - found upper and lower index bounds j and - k respectively. We can pair - nums[j] with all elements up to an including - nums[k] (besides nums[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-j for all i. -
  • -
-

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.

-
-

- most beautiful item for each query - — 9/12/24 -

-
-

problem statement

-

- Given an array items of \((price, beauty)\) tuples, - answer each integer query of \(queries\). The answer to some - query[i] is the maximum beauty of an item with - \(price\leq\)items[i][0]. -

-

understanding the problem

-

- Focus on one aspect of the problem at a time. To answer a query, - we need to have considered: -

-
    -
  1. Items with a non-greater price
  2. -
  3. The beauty of all such items
  4. -
-

- Given some query, how can we efficiently identify the - “last” item with an acceptable price? Leverage the - most common pre-processing algorithm: sorting. Subsequently, we - can binary search items (keyed by price, of course) - to identify all considerable items in \(O(lg(n))\). -

-

- Great. Now we need to find the item with the largest beauty. - Naïvely considering all the element is a - correct approach—but is it correct? Considering our - binary search \(O(lg(n))\) and beauty search \(O(n)\) across - \(\Theta(n)\) queries with - len(items)<=len(queries)\(\leq10^5\), an - \(O(n^2lg(n))\) approach is certainly unacceptable. -

-

- Consider alternative approaches to responding to our queries. It - is clear that answering them in-order yields no benefit (i.e. we - have to consider each item all over again, per query)—could - we answer them in another order to save computations? -

-

- Visualizing our items from left-to-right, we's interested in - both increasing beauty and prices. If we can scan our items left - to right, we can certainly “accumulate” a running - maximal beauty. We can leverage sorting once again to answer our - queries left-to-right, then re-order them appropriately before - returning a final answer. Sorting both queries and - items with a linear scan will take \(O(nlg(n))\) - time, meeting the constraints. -

-

carrying out the plan

-

- A few specifics need to be understood before coding up the - approach: -

-
    -
  • - Re-ordering the queries: couple query[i] with - i, then sort. When responding to queries in sorted - order, we know where to place them in an output - container—index i. -
  • -
  • - The linear scan: accumulate a running maximal beauty, starting - at index 0. For some query query, we - want to consider all items with price less than or equal to - query. Therefore, loop until this condition is - violated— the previous index will represent the - last considered item. -
  • -
  • - Edge cases: it's perfectly possible the last considered - item is invalid (consider a query cheaper than the cheapest - item). Return 0 as specified by the problem - constraints. -
  • -
-
-
vector<int> maximumBeauty(vector<vector<int>>& items, vector<int>& queries) {
-  std::sort(items.begin(), items.end());
-  std::vector<pair<int, int>> sorted_queries;
-  sorted_queries.reserve(queries.size());
-  // couple queries with their indices
-  for (size_t i = 0; i < queries.size(); ++i) {
-    sorted_queries.emplace_back(queries[i], i);
-  }
-  std::sort(sorted_queries.begin(), sorted_queries.end());
-
-  int beauty = items[0][1];
-  size_t i = 0;
-  std::vector<int> ans(queries.size());
-
-  for (const auto [query, index] : sorted_queries) {
-    while (i < items.size() && items[i][0] <= query) {
-        beauty = std::max(beauty, items[i][1]);
-        ++i;
-    }
-    // invariant: items[i - 1] is the rightmost considerable item
-    ans[index] = i > 0 && items[i - 1][0] <= query ? beauty : 0;
-  }
-
-  return std::move(ans);
-
-

asymptotic complexity

-

- Let n=len(items) and m=len(queries). - There may be more items than queries, or vice versa. Note that a - “looser” upper bound can be found by analyzing the - runtime in terms of \(max\{n,m\}\). -

-

- Time Complexity: \(O(nlg(n)+mlg(m)+m)\in - O(nlg(n)+mlg(m))\). An argument can be made that because - queries[i],items[i][{0,1}]\(\leq10^9\), radix sort - can be leveraged to achieve a time complexity of \(O(d \cdot (n + - k + m + k))\in O(9\cdot (n + m))\in O(9n+9m)\in O(n+m)\). -

-

- Space Complexity: \(\Theta(1)\), considering that \(O(m)\) - space must be allocated. If queries/items - cannot be modified in-place, increase the space complexity by - \(m\)/\(n\) respectively. -

-
-

- shortest subarray with or at least k ii - — 9/11/24 -

-
-

problem statement

-

- Given an array of non-negative integers \(num\) and some \(k\), - find the length of the shortest non-empty subarray of nums such - that its element-wise bitwise OR is greater than or equal to - \(k\)—return -1 if no such array exists. -

-

developing an approach

-

Another convoluted, uninspired bitwise-oriented daily.

-

- Anways, we're looking for a subarray that satisfies a - condition. Considering all subarrays with - len(nums)\(\leq2\times10^5\) is impractical according - to the common rule of \(\approx10^8\) computations per second on - modern CPUs. -

-

- Say we's building some array xs. Adding another - element x to this sequence can only increase or - element-wise bitwise OR. Of course, it makes sense to do this. - However, consider xs after—it is certainly - possible that including x finally got us to at least - k. However, not all of the elements in the array are - useful now; we should remove some. -

-

- Which do we remove? Certainly not any from the - middle—we'd no longer be considering a subarray. We can - only remove from the beginning. -

-

- Now, how many times do we remove? While the element-wise bitwise - OR of xs is \(\geq k\), we can naïvely remove - from the start of xs to find the smallest subarray. -

-

- Lastly, what' the state of xs after these - removals? Now, we (may) have an answer and the element-wise - bitwise OR of xs is guaranteed to be \(\lt k\). - Inductively, expand the array to search for a better answer. -

-

- This approach is generally called a variable-sized “sliding - window”. Every element of - nums is only added (considered in the element-wise - bitwise OR) or removed (discard) one time, yielding an - asymptotically linear time complexity. In other words, this is a - realistic approach for our constraints. -

-

carrying out the plan

-

Plugging in our algorithm to my sliding window framework:

-
-
def minimumSubarrayLength(self, nums, k):
-        # provide a sentinel for "no window found"
-        ans = sys.maxsize
-        window = deque()
-        l = 0
-
-        # expand the window by default
-        for r in range(len(nums)):
-            # consider `nums[r]`
-            window.append(nums[r])
-            # shrink window while valid
-            while l <= r and reduce(operator.or_, window) >= k:
-                ans = min(ans, r - l + 1)
-                window.popleft()
-                l += 1
-
-        # normalize to -1 as requested
-        return -1 if ans == sys.maxsize else ans
-
-
-

Done, right? No. TLE.

-

- If you thought this solution would work, you move too fast. - Consider every aspect of an algorithm before implementing - it. In this case, we (I) overlooked one core question: -

-
    -
  1. How do we maintain our element-wise bitwise OR?
  2. -
-

- Calculating it by directly maintaining a window of length \(n\) - takes \(n\) time—with a maximum window size of \(n\), this - solution is \(O(n^2)\). -

-

- Let's try again. Adding an element is simple—OR it to - some cumulative value. Removing an element, not so much. - Considering some \(x\) to remove, we only unset one of its bits - from our aggregated OR if it's the “last” one of - these bits set across all numbers contributing to our aggregated - value. -

-

- Thus, to maintain our aggregate OR, we want to map bit - “indices” to counts. A hashmap (dictionary) or static - array will do just find. Adding/removing some \(x\) will - increment/decrement each the counter's bit count at its - respective position. I like to be uselessly specific - sometimes—choosing the latter approach, how big should our - array be? As many bits as represented by the largest of - \(nums\)—(or \(k\) itself): \[\lfloor \lg({max\{nums,k - \})}\rfloor+1\] -

-

Note that:

-
    -
  1. - Below we use the - change of base formula for logarithms - because \(log_2(x)\) is not available in python. -
  2. -
  3. - It's certainly possible that \(max\{nums, k\}=0\). To avoid - the invalid calculation \(log(0)\), take the larger of \(1\) and - this calculation. The number of digits will then (correctly) be - \(1\) in this special case. -
  4. -
-
-
def minimumSubarrayLength(self, nums, k):
-    ans = sys.maxsize
-
-    largest = max(*nums, k)
-    num_digits = floor((log(max(largest, 1))) / log(2)) + 1
-
-    counts = [0] * num_digits
-    l = 0
-
-    def update(x, delta):
-        for i in range(len(counts)):
-            if x & 1:
-                counts[i] += 1 * delta
-            x >>= 1
-
-    def bitwise_or():
-        return reduce(
-            operator.or_,
-            (1 << i if count else 0 for i, count in enumerate(counts)),
-            0
-        )
-
-    for r, num in enumerate(nums):
-        update(num, 1)
-        while l <= r and bitwise_or() >= k:
-            ans = min(ans, r - l + 1)
-            update(nums[l], -1)
-            l += 1
-
-    return -1 if ans == sys.maxsize else ans
-
-
-

asymptotic complexity

-

- Note that the size of the frequency map is bounded by - \(lg_{2}({10^9})\approx30\). -

-

- Space Complexity: Thus, the window uses \(O(1)\) space. -

-

- Time Complexity: \(\Theta(\)len(nums)\()\) - —every element of nums is considered at least - once and takes \(O(1)\) work each to find the element-wise bitwise - OR. -

-
-

- minimum array end - — 9/10/24 -

-
-

problem statement

-

- Given some \(x\) and \(n\), construct a strictly increasing array - (say - nums - ) of length \(n\) such that - nums[0] & nums[1] ... & nums[n - 1] == x - , where - & - denotes the bitwise AND operator. -

-

- Finally, return the minimum possible value of - nums[n - 1]. -

-

understanding the problem

-

- The main difficulty in this problem lies in understanding what is - being asked (intentionally or not, the phrasing is terrible). Some - initial notes: -

-
    -
  • The final array need not be constructed
  • -
  • - If the element-wise bitwise AND of an array equals - x if and only if each element has - x's bits set—and no other bit it set by - all elements -
  • -
  • - It makes sense to set nums[0] == x to ensure - nums[n - 1] is minimal -
  • -
-

developing an approach

-

- An inductive approach is helpful. Consider the natural question: - “If I had correctly generated nums[:i]”, - how could I find nums[i]? In other words, - how can I find the next smallest number such that - nums - 's element-wise bitwise AND is still \(x\)? -

-

- Hmm... this is tricky. Let's think of a similar problem to - glean some insight: “Given some \(x\), how can I find the - next smallest number?”. The answer is, of course, add one - (bear with me here). -

-

- We also know that all of nums[i] must have at least - \(x\)'s bits set. Therefore, we need to alter the unset bits - of nums[i]. -

-

- The key insight of this problem is combining these two ideas to - answer our question: - Just “add one” to nums[i - 1]'s - unset bits. Repeat this to find nums[n - 1]. -

-

- One last piece is missing—how do we know the element-wise - bitwise AND is exactly \(x\)? Because - nums[i > 0] only sets \(x\)'s unset bits, every - number in nums will have at least \(x\)'s bits - set. Further, no other bits will be set because \(x\) has them - unset. -

-

carrying out the plan

-

Let's flesh out the remaining parts of the algorithm:

-
    -
  • - len(nums) == n and we initialize - nums[0] == x. So, we need to “add one” - n - 1 times -
  • -
  • - How do we carry out the additions? We could iterate \(n - 1\) - times and simulate them. However, we already know how we want to - alter the unset bits of nums[0] inductively— - (add one) and how many times we want to do this (\(n - - 1\)). Because we're adding one \(n-1\) times to - \(x\)'s unset bits (right to left, of course), we simply - set its unset bits to those of \(n - 1\). -
  • -
-

- The implementation is relatively straightfoward. Traverse \(x\) - from least-to-most significant bit, setting its \(i\)th unset bit - to \(n - 1\)'s \(i\)th bit. Use a bitwise mask - mask to traverse \(x\). -

-
-
long long minEnd(int n, long long x) {
-    int bits_to_distribute = n - 1;
-    long long mask = 1;
-
-    while (bits_to_distribute > 0) {
-        if ((x & mask) == 0) {
-            // if the bit should be set, set it-otherwise, leave it alone
-            if ((bits_to_distribute & 1) == 1)
-                x |= mask;
-            bits_to_distribute >>= 1;
-        }
-        mask <<= 1;
-    }
-
-    return x;
-}
-
-

asymptotic complexity

-

- Space Complexity: \(\Theta(1)\)—a constant amount of - numeric variables are allocated regardless of \(n\) and \(x\). -

-

- Time Complexity: in the worst case, may need to traverse - the entirety of \(x\) to distribute every bit of \(n - 1\) to - \(x\). This occurs if and only if \(x\) is all ones (\(\exists - k\gt 0 : 2^k-1=x\))). \(x\) and \(n\) have \(lg(x)\) and \(lg(n)\) - bits respectively, so the solution is \(O(lg(x) + lg(n))\in - O(log(xn))\). \(1\leq x,n\leq 1e8\), so this runtime is bounded by - \(O(log(1e8^2))\in O(log(1e16))\in O(1)\). -

-
-
-
-
- - - - diff --git a/posts/algorithms/two-pointers.html b/posts/algorithms/two-pointers.html deleted file mode 100644 index 0f8ae85..0000000 --- a/posts/algorithms/two-pointers.html +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - - - - - - - - - Barrett Ruth - - -
- -
- barrett@ruth:~$ /algorithms - -
-
-
-
-
-
-

Two Pointers

- -
-
-

technique overview

-
-

- container with most water -

-
-
-

Sometimes, the mathematical solution is the simplest.

-

- The area of a container bounded by the ground and its columns at - positions \((l, r)\) is: \[ \text{area} = \text{width} \cdot - \text{length} = (r - l) \cdot \min\{height[l], height[r]\} \] -

-

- At its core, this is a maximization problem: maximize the - contained area. \[ \max\{(r - l) \cdot \min\{height[l], - height[r]\}\} \] -

-

- Given a new column position \(l_0 < l\) or \(r_0 < r\), the - contained area can only increase if the height of the - corresponding column increases. -

-

- The following correct solution surveys all containers, initialized - with the widest columns positions, that are valid candidates for a - potentially new largest area. A running maximizum, the answer, is - maintained. -

-
-
def maxArea(height: list[int]) -> int:
-    ans = 0
-    l, r = 0, len(height) - 1
-
-    while l < r:
-        width, min_height = r - l, min(height[l], height[r])
-        ans = max(ans, width * min_height)
-
-        while l < r and height[l] <= min_height:
-            l += 1
-        while l < r and height[r] <= min_height:
-            r -= 1
-
-    return ans
-
-
-
-

- boats to save people -

-
-
-

- Usually, the metaphorical problem description is a distraction. - However, I find that thinking within the confines of "boats" and - "people" yields an intuitive solution in this case. -

-

- Since only two people can fit in a boat at a time, pairing up - lightest and heaviest individuals will result in the least amount - of boats being used. -

-

- However, the weights are given in random order. Efficiently - pairing up the lightest and heaviest individuals, then, requires - the most common two-pointers prepreocessing step: sorting. -

-

- Finally, flesh out any remaining aspects of the implementation: -

-
    -
  1. If one person remains, give them a boat.
  2. -
  3. - If both people don't fit, use the heavier person—the - lighter could maybe fit with someone else. -
  4. -
-
-
def minimum_rescue_boats(people: list[int], limit: int) -> int:
-    ans = 0
-    l, r = 0, len(people) - 1
-
-    people.sort()
-
-    while l <= r:
-        if l == r:
-            ans += 1
-            break
-        elif people[l] + people[r] <= limit:
-            ans += 1
-            l += 1
-            r -= 1
-        else:
-            ans += 1
-            r -= 1
-
-    return ans
-
-
-
-
-
- - - -