Skip to content

Commit 52edb23

Browse files
2 parents 51a0ecb + e9c2e9c commit 52edb23

File tree

4 files changed

+341
-0
lines changed

4 files changed

+341
-0
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ Each day (ideally) we'll attempt the daily [leetcode](https://leetcode.com) prob
3535
| July 24, 2024 | [2191](https://leetcode.com/problems/sort-the-jumbled-numbers/description/?envType=daily-question) | [Click here](https://github.com/ContextLab/leetcode-solutions/tree/main/problems/2191) | 🟡 Medium |
3636
| July 25, 2024 | [912](https://leetcode.com/problems/sort-an-array/description/?envType=daily-question) | [Click here](https://github.com/ContextLab/leetcode-solutions/tree/main/problems/912) | 🟡 Medium |
3737
| July 26, 2024 | [1334](https://leetcode.com/problems/find-the-city-with-the-smallest-number-of-neighbors-at-a-threshold-distance/description/?envType=daily-question) | [Click here](https://github.com/ContextLab/leetcode-solutions/tree/main/problems/1334) | 🟡 Medium |
38+
| July 27, 2024 | [2976](https://leetcode.com/problems/minimum-cost-to-convert-string-i/description/?envType=daily-question) | [Click here](https://github.com/ContextLab/leetcode-solutions/tree/main/problems/2976) | 🟡 Medium |
39+
| July 28, 2024 | [2045](https://leetcode.com/problems/second-minimum-time-to-reach-destination/description/?envType=daily-question) | [Click here](https://github.com/ContextLab/leetcode-solutions/tree/main/problems/2045) | 🔴 Hard |
40+
| July 29, 2024 | [1395](https://leetcode.com/problems/count-number-of-teams/description/?envType=daily-question) | [Click here](https://github.com/ContextLab/leetcode-solutions/tree/main/problems/1395) | 🟡 Medium |
3841

3942
# Join our discussion!
4043

problems/1395/jeremymanning.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# [Problem 1395: Count Number of Teams](https://leetcode.com/problems/count-number-of-teams/description/?envType=daily-question)
2+
3+
## Initial thoughts (stream-of-consciousness)
4+
- Enumerating every *possible* team will take $O(n^3)$ time. That seems long.
5+
- I think we could do this using two sets of queues (BFS) or stacks (DFS)-- but let's try queues:
6+
- Initialize `nTeams = 0`.
7+
- One queue stores *increasing* teams and the other stores *decreasing* teams
8+
- First, enqueue each soldier in both queues as a single item list of indices
9+
- Next, for each queue type, loop until the queue is empty:
10+
- Dequeue the next list (at the front of the queue). Let's say the last item in the list has index $i$.
11+
- For indices $i + 1$ to the end of the list of soldiers, add them to the teams if they satisfy the correct ordering. E.g., for the increasing queue, ensure that the next item (at index $j$) is greater than the current last item at index $i$. If the new team has length 3, increment `nTeams`. Otherwise enqueue any newly formed "valid" teams.
12+
- Finally, return `nTeams`.
13+
14+
## Refining the problem, round 2 thoughts
15+
- Any edge cases to consider?
16+
- If there are fewer than 3 soldiers, no teams are possible...but we're given that $3 \leq n \leq 1000$, so we don't need to consider this case
17+
- Strange `rating` values? We're given that `ratings` are all integers.
18+
- We're also told that the integers in `ratings` are unique-- we could probably leverage this to implement some sort of sorting-based solution 🤔...but let's see if the queue idea works first.
19+
- I think we can try this...
20+
21+
## Attempted solution(s)
22+
```python
23+
from collections import deque
24+
25+
class Solution:
26+
def numTeams(self, rating: List[int]) -> int:
27+
nTeams = 0
28+
increasing_queue = deque([[i] for i in range(len(rating))])
29+
decreasing_queue = increasing_queue.copy()
30+
31+
# look for increasing teams (where rating[i] < rating[j] < rating[k])
32+
while len(increasing_queue) > 0:
33+
next_team = increasing_queue.popleft()
34+
for i in range(next_team[-1], len(rating)):
35+
if rating[i] > rating[next_team[-1]]:
36+
if len(next_team) == 2:
37+
nTeams += 1
38+
else:
39+
new_team = next_team.copy()
40+
new_team.append(i)
41+
increasing_queue.append(new_team)
42+
43+
# now look for decreasing teams (where rating[i] > rating[j] > rating[k])
44+
while len(decreasing_queue) > 0:
45+
next_team = decreasing_queue.popleft()
46+
for i in range(next_team[-1], len(rating)):
47+
if rating[i] < rating[next_team[-1]]:
48+
if len(next_team) == 2:
49+
nTeams += 1
50+
else:
51+
new_team = next_team.copy()
52+
new_team.append(i)
53+
decreasing_queue.append(new_team)
54+
55+
return nTeams
56+
```
57+
- Given test cases pass
58+
- New test cases:
59+
- `rating = [4, 7, 1, 9, 10, 14, 3, 29, 1000, 950, 26, 44]`: pass
60+
- `rating = [62, 2, 35, 32, 4, 75, 48, 38, 28, 92, 8, 100, 68, 95, 63, 40, 42, 21, 47, 43, 89, 79, 14, 58, 85, 80, 15, 41, 10, 37, 30, 31, 24, 1, 23, 45, 53, 83, 65, 3, 49, 66, 6, 54, 34, 72, 29, 71, 52, 81, 98, 82, 18, 36, 88, 20, 12, 99, 22, 77, 97, 60, 17, 61, 27, 16, 76, 33, 69, 51, 19, 25, 46, 39, 74, 94, 67, 55, 96, 90, 93, 64, 26, 73, 87, 91, 78, 11, 5, 9, 57, 56, 50, 70, 44, 84, 86, 59, 13, 7]`: pass (note: this is a random permutation of the numbers 1...100)
61+
- Seems ok; submitting...
62+
63+
![Screenshot 2024-07-28 at 11 40 23 PM](https://github.com/user-attachments/assets/40a889c6-af6e-4574-9cca-deb374176779)
64+
65+
Uh oh...time limit exceeded! So: the algorithm seems correct (and even for this "failed" test case we get the right answer if I run it through "use as test case"). But this algorithm is still $O(n^3)$, which is apparently not good enough. Let's go back to the drawing board...
66+
67+
## Revised thoughts (back to stream of consciousness...)
68+
- I wonder if the sorting idea might work...or...hmmm... 🤔
69+
- The algorithm I implemented also requires us to re-compare elements several times. E.g., suppose we're considering building a team that includes index $i$. If we want to build an increasing team, then *any* item from index $0...(i - 1)$ that is less than `ratings[i]` could serve as the first element, and any item from index $(i + 1)...n$ that is greater than `ratings[i]` could serve as the third element. So if there are $a$ elements less than `ratings[i]` with indices less than `i` and $b$ elements greater than `ratings[i]` with indices greater than `i`, the total number of increasing teams is $ab$. We can use an analogous approach to track the number of decreasing teams (just flipping the greater than/less than signs).
70+
- Let's try something like:
71+
- For each element in turn (at index `i`):
72+
- track how many items to the left are bigger/smaller, and how many items to the right are bigger/smaller
73+
- the number of increasing teams we can make with element `i` as the second team member is `left_smaller * right_bigger + left_bigger * right_smaller`
74+
75+
New solution:
76+
```python
77+
class Solution:
78+
def numTeams(self, rating: List[int]) -> int:
79+
nTeams = 0
80+
for i in range(len(rating)):
81+
left_smaller, left_bigger, right_smaller, right_bigger = 0, 0, 0, 0
82+
for j in range(i): # j is to the left of i
83+
if rating[i] > rating[j]:
84+
left_smaller += 1
85+
else:
86+
left_bigger += 1
87+
88+
for j in range(i + 1, len(rating)): # j is to the right of i
89+
if rating[i] > rating[j]:
90+
right_smaller += 1
91+
else:
92+
right_bigger += 1
93+
94+
nTeams += left_smaller * right_bigger + left_bigger * right_smaller
95+
96+
return nTeams
97+
```
98+
- Ok: all of the test cases, including the ones that exceeded the time limit previously are now passing
99+
- With this algorithm I think we've gotten the runtime down to $O(n^2)$ (for filling in `left_smaller`, `left_bigger`, `right_smaller`, and `right_bigger` inside the main loop; the lefts and rights each take $O(n)$ steps to fill in, and those loops run for each element in turn (of which there are $n$).
100+
- Submitting...
101+
102+
![Screenshot 2024-07-29 at 12 33 40 AM](https://github.com/user-attachments/assets/94653f0e-cce9-47d7-b43a-62192dfb2679)
103+
104+
🎉!
105+
106+
107+
108+
109+
110+

problems/2045/jeremymanning.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# [Problem 2045: Second Minimum Time to Reach Destination](https://leetcode.com/problems/second-minimum-time-to-reach-destination/description/?envType=daily-question)
2+
3+
## Initial thoughts (stream-of-consciousness)
4+
- This is another graph problem (sort of a shortest path problem, but with a few differences)
5+
- There are a few twists in this problem:
6+
- The red/green traffic signals are going to change how we factor in edge lengths. We'll have to come up with a way of handling this, but essentially as we traverse a path we need to use the traffic rules to figure out how long it will take.
7+
- Finding the "second minimum time" is a little trickier than finding the (strict) minimum time. The standard shortest path algorithms would do something like replacing (in the adjacency matrix) the distances between two vertices if a new path had a shorter distance. But now I think we need to maintain *two* adjacency matrices: one for the minimum distances, and one for the second minimum distances (i.e., the minimum values that are strictly greater than whatever is in the minimum distance matrix.
8+
- Another important part of the problem is that we don't need to compute the paths between *every* pair of vertices, since we know that the only path that matters is from vertex 1 to vertex $n$. So instead of the Floyd-Warshall algorithm, which we used for [yesterday's problem](https://github.com/ContextLab/leetcode-solutions/blob/main/problems/2976/jeremymanning.md) and the [day before yesterday's problem](https://github.com/ContextLab/leetcode-solutions/blob/main/problems/1334/jeremymanning.md), I think we should use Dijkstra's algorithm for this problem (but modified as described above). Pasting in from yesterday's notes, here is the pseudocode for Dijkstra's algorithm:
9+
10+
![Screenshot 2024-07-26 at 11 33 36 PM](https://github.com/user-attachments/assets/e668dcc9-0f82-4b76-a965-45e35a22a72d)
11+
12+
- Ok...so now we have a general approach; let's figure out how to deal with these twists.
13+
14+
## Refining the problem, round 2 thoughts
15+
- Handling traffic rules:
16+
- We know that all signals start off as green at time 0
17+
- Let's say that the current signal status when we reach vertex `i` is `isGreen`, and the journey from vertex `i` to `j` takes `x` minutes. If `isGreen == True` and `x` is between 0 and `change` minutes, the status doesn't change (but now we only have `change - x` minutes before the next transition). Alternatively, if `change < x <= 2 * change`, then now `isGreen == False` and we'll need to add `y = 2 * change - x` minutes to the journey from `j` to the next destination. (If `j` is the endpoint-- i.e., vertex $n$, then we don't need to pay that extra cost.)
18+
- If `isGreen == False` then we need to wait until the signal turns green (this takes `y` minutes, as computed above) and then we proceed as though `isGreen == True`.
19+
- Only the final (up to) `2 * change` minutes of the journey matter with respect to accounting for traffic rules. I think we can compute the cost as something like `x + extra`, where `extra` is computed as follows:
20+
- Start with `extra = 0`
21+
- If there is time remaining until the signal turns green from the remaining journey (let's call that amount `y`), then `extra += y`. For the first journey (from vertex 1 to a neighbor) we know that `y == 0`.
22+
- Then take `remainder = x % (2 * change)`:
23+
- If `0 <= remainder < change` then keep track of how much less time we have on the *next* journey-- but we can leave the next vertex right away
24+
- If `change <= remainder < 2 * change` then we arrive when the signal is red, so on the next journey from the destination we'll need to wait `2 * change - remainder` minutes to leave.
25+
- Ok...so these notes are a little convoluted. But what I think I'm coming to is that we're going to need to keep track of `greenTimeSpent` (amount of time spent traveling to the current vertex during the most recent green signal-- *if the signal is green when we arrive*; if we arrive when the signal is red, `greenTimeSpent = 0`). And we also need to keep track of `timeUntilGreen`-- the amount of time left until we can leave the destination vertex. But `timeUntilGreen` only applies if we arrive when the signal is red; otherwise (if the signal is green), then we set `timeUntilGreen = 0`. Then, when we're computing travel times between vertices, we want to add `timeUntilGreen` to the stated travel time. And then we *subtract* `greenTimeSpent` when we need figure out the signal status upon arrival at the destination vertex.
26+
- Finding the second minimum time:
27+
- In the "standard" Dijkstra's algorithm, we continually replace the path distance from `i` to `j` with alternative smaller values if/when we find them. But in the "second minimum" version, we'll need to maintain two parallel representations of the path distances:
28+
- The first representation is the standard minimum path distance
29+
- The second representation (which stores the second minimums) replaces the path distance from `i` to `j` with an alternative smaller value only if (a) it's smaller than the current distance in the second minimum representation *and* it's strictly greater than whatever the minimum path distance from `i` to `j` is.
30+
- Functions to write:
31+
- `computeTravelTime(current_time, travel_time)`: returns the time needed to get to the next vertex, accounting for signal status
32+
- Actually...`travel_time` is always the same, so we can just use `computeTravelTime(current_time)`!
33+
- Then we just need to implement this modified version of Dijkstra's algorithm (i.e., breadth first search) to find the second minimum time needed to get from vertex 1 to $n$.
34+
- One potential edge case: what if there is only 1 path from vertex 1 to $n$? Then I think to get the second minimum time, we would need to double back along the trip between the nearest vertices (accounting for signal status)-- e.g., we'd need to add an extra loop (for some adjacent vertices `i` and `j`): `... --> i --> j --> i --> j --> ...`.
35+
36+
### Other notes
37+
- For each branch of the path we take, we need to track how long it took to get there and where we are "now"
38+
- Given the current time, we can compute the current signal status and update the next travel time accordingly
39+
- To store both the "minimum" and "second minimum" times, we can create a matrix of tuples: `min_times[i][j][0]` has the minimum time path from `i` to `j`, and `min_times[i][j][1]` has the second minimum time from `i` to `j`:
40+
- If we find a better time than `min_times[i][j][0]`:
41+
- `min_times[i][j][1] = min_times[i][j][0]`
42+
- `min_times[i][j][0] = new_best_time`
43+
- We can use a list of lists to represent the graph:
44+
- `graph[i]` stores the edges originating at `i`
45+
- Initialize all edges to empty lists
46+
- The travel time computation is organized around green/red cycles:
47+
- Each cycle takes `cycle = 2 * change` minutes
48+
- The signals start as green; each `2 * change` minutes they return to green
49+
- If (after rounding down to the nearest multiple of `change`) the current time (`current_time`) is an *odd* multiple of `change` (i.e., the lights are currently red; `(current_time // change) % 2 == 1`), we need to wait until the lights turn green, which will take `wait_time = cycle - (current_time % cycle)` minutes. The new time is `wait_time + time + current_time`.
50+
- If (after rounding down to the nearest multiple of `change`) the current time (`current_time`) is an *even* multiple of `change` (i.e., the lights are currently green; `(current_time // change) % 2 == 0`), then we can leave right away (`wait_time` is 0), so the new time is `time + current_time`.
51+
52+
## Attempted solution(s)
53+
```python
54+
class Solution:
55+
def secondMinimum(self, n: int, edges: List[List[int]], time: int, change: int) -> int:
56+
def computeTravelTime(current_time):
57+
# Calculate effective travel time considering traffic lights
58+
cycle = 2 * change
59+
if (current_time // change) % 2 == 1: # lights are red
60+
wait_time = cycle - (current_time % cycle)
61+
return current_time + wait_time + time
62+
else: # lights are green
63+
return current_time + time
64+
65+
# Build the graph
66+
graph = [[] for _ in range(n + 1)]
67+
for u, v in edges:
68+
graph[u].append(v)
69+
graph[v].append(u)
70+
71+
# List to keep track of the minimum and second minimum times to each vertex
72+
min_times = [[float('inf'), float('inf')] for _ in range(n + 1)]
73+
min_times[1][0] = 0
74+
75+
# Queue for BFS
76+
queue = [(0, 1)] # (current time, current vertex)
77+
78+
while queue:
79+
curr_time, vertex = queue.pop(0)
80+
81+
# Explore neighbors
82+
for neighbor in graph[vertex]:
83+
next_time = computeTravelTime(curr_time)
84+
85+
if next_time < min_times[neighbor][0]: # demote minimum time to second minimum time and update minimum time
86+
min_times[neighbor][1] = min_times[neighbor][0]
87+
min_times[neighbor][0] = next_time
88+
queue.append((next_time, neighbor))
89+
elif min_times[neighbor][0] < next_time < min_times[neighbor][1]: # update second minimum time
90+
min_times[neighbor][1] = next_time
91+
queue.append((next_time, neighbor))
92+
93+
# Return the second minimum time to reach vertex n
94+
return min_times[n][1]
95+
```
96+
- Given test cases pass
97+
- I'm out of time for thinking about this; submitting...
98+
99+
![Screenshot 2024-07-27 at 11 34 34 PM](https://github.com/user-attachments/assets/fcc0649a-02de-4839-9045-9f5746a79cd6)
100+
101+
😌 phew!
102+

0 commit comments

Comments
 (0)