- 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.
-- 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. -
-
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: -
--
-
- 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. - -
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
-
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\).
-
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)\). -
-