-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #40 from paxtonfitzpatrick/main
catching up on past problems
- Loading branch information
Showing
9 changed files
with
584 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
# [Problem 1105: Filling Bookcase Shelves](https://leetcode.com/problems/filling-bookcase-shelves/description/?envType=daily-question) | ||
|
||
## Initial thoughts (stream-of-consciousness) | ||
|
||
- this is an interesting problem. We need to divide the sequences of books into subsequences of shelves, optimizing for the minimum sum of the shelves' heights we can achieve with shelves that are at most `shelfWidth` wide. | ||
- okay my initial thought is that a potential algorithm could go something like this: | ||
- We know the first book **must** go on the first shelf, so place it there. The height of the first shelf is now the height of the first book. | ||
- Then, for each subsequent book: | ||
- if the book can fit on the same shelf as the previous book without increasing the shelf's height (i.e., its height is $\le$ the curent shelf height (the height of the tallest book on the shelf so far) and its with + the width of all books placed on the shelf so far is $\le$ `shelfWidth`), then place it on the same shelf. | ||
- elif the book can't fit on the same shelf as the previous without exceeding `shelfWidth`, then we **must** place it on the next shelf | ||
- I think we'll then have to tackle the sub-problem of whether moving some of the preceding books from the last shelf to this next shelf would decrease the height of that last shelf without increasing the height of this next shelf... or maybe it's okay to increase this next shelf's height if doing so decreases the previous one's by a larger amount? This feels like it could get convoluted fast... | ||
- else, the book *can* fit on the same shelf as the previous but *would* increase the shelf's height, so we need to determine whether it's better to place it on the current shelf or start a new shelf. | ||
- is this conceptually the same sub-problem as the one above? Not sure... | ||
- I think the basic thing we're optimizing for is having tall books on the same shelf as other tall books whenever possible. This makes me think we might want to try to identify "optimal runs" of books in the array containing as many tall books as possible whose total width is $\le$ `shelfWidth`. Maybe I could: | ||
- sort a copy of the input list by book height to find the tallest books | ||
- then in the original list, search outwards (left & right) from the index of each tallest book to try to create groupings of books that contain as many tall books as possible. | ||
- How would I formalize "as many tall books as possible"? Maximizing the sum of the grouped books' heights doesn't seem quite right... | ||
- Since I want tall books together *and* short books together, maybe I could come up with a scoring system for groupings that penalizes books of different heights being in the same group? Something like trying to minimize the sum of the pairwise differences between heights of books in the same group? | ||
- anything that involves "pairwise" raises a red flag for me though because it hints at an $O(n^2)$ operation, which I'd like to avoid | ||
- Though I'm not sure how I'd do this without leaving the occasional odd short book on its own outside of any subarray, meaning it'd end up on its own shelf, which isn't good... | ||
- the general steps I wrote out above also make me think of recursion, since they entail figuring out which of two options is better (place a book on the current shelf or start a new shelf when either is possible) by optimizing the bookshelf downstream of both decisions and then comparing the result. | ||
- I think framing this as a recursion problem also resolves my confusion about the potential need to "backtrack" in the case where we're forced by `shelfWidth` to start a new shelf, in order to determine wether it'd be better to have placed some previous books on that new shelf as well -- if we simply test out the result of placing each book on the same shelf and on a new shelf, then we wouldn't have to worry about that because all of those combinations of books on a shelf would be covered. | ||
- the downside that testing both options (same shelf and new shelf) for *every* book, for *every other* would make the runtime $O(2^n)$, I think, which is pretty rough. | ||
- although... maybe I could reduce this significantly? Say we put book $n$ on a given shelf, then put book $n+1$ on that same shelf, then put book $n+2$ on a new shelf. Or, we put book $n$ on a given shelf, then put book $n+1$ on a new shelf, then also put book $n+2$ on a new shelf. In both cases, all subsequent calls in that branch of the recursion tree follow from a shelf that starts with book $n+2$. So if I can set up the recursive function so that it depends only on the current book and the amount of room left on the current shelf, then I could use something like `functools.lru_cache()` to memoize the results of equivalent recursive calls. I think this would end up covering the vast majority of calls if I can set it up right. | ||
- actually, I think `functools.lru_cache()` would be overkill since it adds a lot of overhead to enable introspection, discarding old results, etc. I think I'd be better off just using a regular dict instead. | ||
- also, even before the memoization, I think it should be slightly better than $O(2^n)$ because there will be instances of the second case I noted above where the next book *can't* be placed on the current shelf and **must** be placed on the next shelf. | ||
- I'm not 100% sure the recursive function can be written to take arguments that'll work for the memoization (hashable, shared between calls that should share a memoized result and not those that don't, etc.) the memoization, but I think I'll go with this for now and see if I can make it work | ||
- so how would I set this up recursively? | ||
- I think the "base case" will be when I call the recursive function on the last book. At that point, I'll want to return the height of the current shelf so that in the recursive case, I can add the result of a recursive call to the current shelf's height to get the height of the bookshelf past the current book. | ||
- actually, what I do will be slightly different for the two possible cases, because I'll need to compare the rest-of-bookshelf height between the two options to choose the optimal one: | ||
- if placing the next book on a new shelf, the rest-of-bookshelf height will be the current shelf's height plus the returned height | ||
- if placing the next book on the current shelf, the rest-of-bookshelf height will be the larger of the current shelf's height and the book's height, plus the returned height | ||
- and then I'll need to compare those two heights and return the smaller one | ||
- so I think I'll need to set the function up to take as arguments (at least): | ||
- the index of the current book | ||
- the height of the current shelf | ||
- the remaining width on the current shelf | ||
- possibly the cache object and `books` list, unless I make them available in scope some other way | ||
- and then I can format the book index and remaining shelf width as a string to use as a key in the cache dict | ||
- okay it's possible there are some additional details I haven't thought of yet, but I'm gonna try this | ||
|
||
## Refining the problem, round 2 thoughts | ||
|
||
## Attempted solution(s) | ||
|
||
```python | ||
class Solution: | ||
def minHeightShelves(self, books: List[List[int]], shelfWidth: int) -> int: | ||
return self._recurse_books(books, 0, shelfWidth, shelfWidth, 0, {}) | ||
|
||
def _recurse_books( | ||
self, | ||
books, | ||
curr_ix, | ||
full_shelf_width, | ||
shelf_width_left, | ||
curr_shelf_height, | ||
call_cache | ||
): | ||
# base case (no books left): | ||
if curr_ix == len(books): | ||
return curr_shelf_height | ||
|
||
cache_key = f'{curr_ix}-{shelf_width_left}' | ||
if cache_key in call_cache: | ||
return call_cache[cache_key] | ||
|
||
# test placing book on new shelf | ||
total_height_new_shelf = curr_shelf_height + self._recurse_books( | ||
books, | ||
curr_ix + 1, | ||
full_shelf_width, | ||
full_shelf_width - books[curr_ix][0], | ||
books[curr_ix][1], | ||
call_cache | ||
) | ||
|
||
# if book can fit on current shelf, also test placing it there | ||
if books[curr_ix][0] <= shelf_width_left: | ||
# check if current book is new tallest book on shelf | ||
if books[curr_ix][1] > curr_shelf_height: | ||
curr_shelf_height = books[curr_ix][1] | ||
|
||
total_height_curr_shelf = self._recurse_books( | ||
books, | ||
curr_ix + 1, | ||
full_shelf_width, | ||
shelf_width_left - books[curr_ix][0], | ||
curr_shelf_height, | ||
call_cache | ||
) | ||
if total_height_curr_shelf < total_height_new_shelf: | ||
call_cache[cache_key] = total_height_curr_shelf | ||
return total_height_curr_shelf | ||
else: | ||
call_cache[cache_key] = total_height_new_shelf | ||
return total_height_new_shelf | ||
|
||
call_cache[cache_key] = total_height_new_shelf | ||
return total_height_new_shelf | ||
|
||
``` | ||
|
||
![](https://github.com/user-attachments/assets/d79f6e5d-28f1-4a11-8254-92847001bbd1) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# [Problem 1460: Make Two Arrays Equal by Reversing Subarrays](https://leetcode.com/problems/make-two-arrays-equal-by-reversing-subarrays/description/?envType=daily-question) | ||
|
||
## Initial thoughts (stream-of-consciousness) | ||
|
||
- okay my first thought is: as long as all of the elements in `target` and `arr` are the same, regardless of their order, is it possible to do some series of reversals to make them equal? | ||
- I'm pretty sure it is... and the fact that this is an "easy" problem, while coming up with an algorithm for determining what subarrays to reverse in order to check for this seems challenging, also makes me think that. | ||
- I can't come up with a counterexample or logical reason why this wouldn't be the case, so I'll go with it. | ||
|
||
## Refining the problem, round 2 thoughts | ||
|
||
- I think the simplest way to do this will to be to sort both arrays and then test whether they're equal. That'll take $O(n\log n)$ time, which is fine. | ||
- Even though `return sorted(arr) == sorted(target)` would be better practice in general, for the purposes of this problem I'll sort the arrays in place since that'll cut cut the memory used in half. | ||
|
||
## Attempted solution(s) | ||
|
||
```python | ||
class Solution: | ||
def canBeEqual(self, target: List[int], arr: List[int]) -> bool: | ||
target.sort() | ||
arr.sort() | ||
return target == arr | ||
``` | ||
|
||
![](https://github.com/user-attachments/assets/d6c036c2-d3f9-4d4d-9521-82ad96ceebed) | ||
|
||
## Refining the problem further | ||
|
||
- okay I just realized there's actually a way to do this in $O(n)$ time instead of $O(n\log n)$. I'm not sure it'll *actually* be faster in practice, since my first solution was pretty fast -- and it'll definitely use more memory (though still $O(n)$) -- but it's super quick so worth trying. | ||
- basically as long as the number of occurrences of each element in the two arrays is the same, then they'll be the same when sorted. So we can skip the sorting and just compare the counts with a `collections.Counter()`: | ||
|
||
```python | ||
class Solution: | ||
def canBeEqual(self, target: List[int], arr: List[int]) -> bool: | ||
return Counter(target) == Counter(arr) | ||
``` | ||
|
||
![](https://github.com/user-attachments/assets/d902d70d-1725-4816-a099-a1e10a82eb10) | ||
|
||
So basically identical runtime and memory usage. I guess the upper limit of 1,000-element arrays isn't large enough for the asymptotic runtime improvement to make up for the overhead of constructing the `Counter`s, and the upper limit of 1,000 unique array values isn't large enough for the additional memory usage to make much of a difference. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
# [Problem 1508: Range Sum of Sorted Subarray Sums](https://leetcode.com/problems/range-sum-of-sorted-subarray-sums/description/?envType=daily-question) | ||
|
||
## Initial thoughts (stream-of-consciousness) | ||
|
||
- okay I'm guessing that finding the solution the way they describe in the problem setup is going to be too slow, otherwise that'd be kinda obvious... though the constraints say `nums` can only have up to 1,000 elements, which means a maximum of 500,500 sums, which isn't *that* crazy... | ||
- maybe there's a trick to figuring out which values in `nums` will contribute to the sums between `left` and `right` in the sorted array of sums? Or even just the first `right` sums? | ||
- I can't fathom why we're given `n` as an argument... at first I thought it might point towards it being useful in the expected approach, but even if so, we could just compute it from `nums` in $O(1)$ time... | ||
- For the brute force approach (i.e., doing it how the prompt describes), I can think of some potential ways to speed up computing the sums of all subarrays... e.g., we could cache the sums of subarrays between various indices `i` and `j`, and then use those whenever we need to compute the sum of another subarray that includes `nums[i:j]`. But I don't think these would end up getting re-used enough to make the trade-off of having to check `i`s and `j`s all the time worth it, just to save *part* of the $O(n)$ runtime of the already very fast `sum()` function. | ||
- ah, a better idea of how to speed that up: computing the sums of all continuous subarrays would take $O(n^3)$ time, because we compute $n^2$ sums, and `sum()` takes $O(n)$ time. But if I keep a running total for the inner loop and, for each element, add it to the running total and append that result, rather than recomputing the sum of all items up to the newest added one, that should reduce the runtime to $O(n^2)$. | ||
- This gave me another idea about "recycling" sums -- if I compute the cumulative sum for each element in `nums` and store those in a list `cumsums`, then I can compute the sum of any subarray `nums[i:j]` as `cumsums[j] - cumsum[i-1]`. Though unfortunately, I don't think this will actually save me any time since it still ends up being $n^2$ operations. | ||
- Nothing better is coming to me for this one, so I think I'm going to just implement the brute force approach and see if it's fast enough. Maybe I'll have an epiphany while I'm doing that. If not, I'll check out the editorial solution cause I'm pretty curious what's going on here. | ||
- The complexity for the brute force version is a bit rough... iterating `nums` and constructing list of sums will take $O(n^2)$ time and space, then sorting that list of sums will take $O(n^2 \log n^2)$ time, which is asymptotically equivalent to $O(n^2 \log n)$. | ||
|
||
## Refining the problem, round 2 thoughts | ||
|
||
|
||
## Attempted solution(s) | ||
|
||
```python | ||
class Solution: | ||
def rangeSum(self, nums: List[int], n: int, left: int, right: int) -> int: | ||
sums = [] | ||
for i in range(n): | ||
subsum = 0 | ||
for j in range(i, n): | ||
subsum += nums[j] | ||
sums.append(subsum) | ||
sums.sort() | ||
return sum(sums[left-1:right]) % (1e9 + 7) | ||
``` | ||
|
||
![](https://github.com/user-attachments/assets/fd4e974b-3abb-443e-ba30-40490e326f75) | ||
|
||
Wow, that's a lot better than I expected. Looks like most people actually went with this approach. I'll try the cumulative sum version I mentioned above just quickly too... | ||
|
||
```python | ||
class Solution: | ||
def rangeSum(self, nums: List[int], n: int, left: int, right: int) -> int: | ||
# accumulate is itertools.accumulate, add is operator.add, both already | ||
# imported in leetcode environment | ||
sums = list(accumulate(nums, add)) + nums[1:] | ||
for i in range(1, len(nums)-1): | ||
for j in range(i+1, len(nums)): | ||
sums.append(sums[j] - sums[i-1]) | ||
sums.sort() | ||
return sum(sums[left-1:right]) % (10**9 + 7) | ||
``` | ||
|
||
![](https://github.com/user-attachments/assets/8946b8e3-91b0-404d-b131-87fc15a8835d) | ||
|
||
Slightly slower, which I guess makes sense since it's doing basically the same thing but with a bit more overhead. |
Oops, something went wrong.