diff --git a/posts/algorithms/leetcode-daily.html b/posts/algorithms/leetcode-daily.html index 3a09b9d..1b577c5 100644 --- a/posts/algorithms/leetcode-daily.html +++ b/posts/algorithms/leetcode-daily.html @@ -38,6 +38,186 @@

Leetcode Daily

+
+

+ shortest subarray with or at least k ii + — 9/12/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. +

+

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

- -

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:

- -

- 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) {
+            

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;
 
@@ -163,21 +342,22 @@
 
     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)\). +

-

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))=O(log(1e16))=O(16)=O(1)\). -