+ 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\).
+
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. +
+ +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.
++ 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: +
+-
+
- Items with a non-greater price +
- The beauty of all such items +
+ 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—indexi. +
+ -
+ The linear scan: accumulate a running maximal beauty, starting
+ at index
0. For some queryquery, 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
0as specified by the problem + constraints. +
+
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(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:
+ +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: +
+-
+
- How do we maintain our element-wise bitwise OR? +
+ 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:
+-
+
- + Below we use the + change of base formula for logarithms + because \(log_2(x)\) is not available in python. + +
- + 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. + +
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
+
xif 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] == xto 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) == nand we initialize +nums[0] == x. So, we need to “add one” +n - 1times +
+ -
+ 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\).
+
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(1)\). +
+