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]withi, 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 toquery. 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 hasx's bits set—and no other bit it set by all elements -
It makes sense to set
nums[0] == xto ensurenums[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 initializenums[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)\).