Skip to content

Commit d14f1ba

Browse files
Merge pull request #124 from romankurnovskii/problems-2092-3716-3693
Add solutions and explanations for problems 2092, 3693, 3716
2 parents 11abbbe + e323ba2 commit d14f1ba

File tree

6 files changed

+282
-0
lines changed

6 files changed

+282
-0
lines changed

explanations/2092/en.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
## Explanation
2+
3+
### Strategy (The "Why")
4+
5+
**Restate the problem:** We have n people, and person 0 initially knows a secret. At time 0, person 0 shares the secret with `firstPerson`. Then, through a series of meetings at different times, the secret spreads. When two people meet at the same time, if either knows the secret, both learn it. However, the secret can only spread within the same time frame - connections formed at one time don't carry over to future times unless those people remain connected to person 0.
6+
7+
**1.1 Constraints & Complexity:**
8+
9+
- **Input Size:** n can be up to 10^5, and there can be up to 10^5 meetings.
10+
- **Time Complexity:** O(M log M) where M is the number of meetings - we sort meetings by time (O(M log M)) and process them with Union-Find operations (nearly O(1) per operation).
11+
- **Space Complexity:** O(n) for the Union-Find data structure.
12+
- **Edge Case:** If no meetings occur, only person 0 and firstPerson know the secret.
13+
14+
**1.2 High-level approach:**
15+
16+
The goal is to track which people know the secret by processing meetings in chronological order. At each time step, we temporarily connect people who meet, let the secret spread within that group, then only keep connections to person 0. People not connected to person 0 "forget" their temporary connections.
17+
18+
**1.3 Brute force vs. optimized strategy:**
19+
20+
- **Brute Force:** For each time, create a graph of all meetings, run BFS/DFS to find all connected components that include person 0, then reset. This would be O(M \* n) in worst case.
21+
- **Optimized Strategy:** Use Union-Find to efficiently connect people at the same time, then reset only those not connected to person 0. This is O(M log M) due to sorting.
22+
- **Optimization:** Union-Find provides nearly constant-time union and find operations, making it ideal for this dynamic connectivity problem.
23+
24+
**1.4 Decomposition:**
25+
26+
1. Sort all meetings by time to process them chronologically.
27+
2. Initialize Union-Find data structure and connect person 0 with firstPerson.
28+
3. For each unique time, process all meetings at that time by connecting the participants.
29+
4. After processing each time block, reset connections for people not connected to person 0.
30+
5. Return all people who are connected to person 0 after all meetings.
31+
32+
### Steps (The "How")
33+
34+
**2.1 Initialization & Example Setup:**
35+
36+
Let's use the example: `n = 6, meetings = [[1,2,5],[2,3,8],[1,5,10]], firstPerson = 1`
37+
38+
- Initially, person 0 and person 1 know the secret.
39+
- We initialize parent array: `parent = [0, 1, 2, 3, 4, 5]`
40+
- After connecting 0 and 1: `parent = [0, 0, 2, 3, 4, 5]`
41+
42+
**2.2 Start Processing:**
43+
44+
We sort meetings by time: `[[1,2,5], [2,3,8], [1,5,10]]` (already sorted).
45+
46+
**2.3 Trace Walkthrough:**
47+
48+
| Time | Meeting | Action | Parent Array After Union | After Reset |
49+
| ---- | -------- | ------------- | ------------------------ | ------------------------------------------------ |
50+
| 5 | [1,2,5] | Union 1 and 2 | [0, 0, 0, 3, 4, 5] | [0, 0, 0, 3, 4, 5] (all connected to 0) |
51+
| 8 | [2,3,8] | Union 2 and 3 | [0, 0, 0, 0, 4, 5] | [0, 0, 0, 0, 4, 5] (all connected to 0) |
52+
| 10 | [1,5,10] | Union 1 and 5 | [0, 0, 0, 0, 4, 0] | [0, 0, 0, 0, 4, 0] (all except 4 connected to 0) |
53+
54+
**2.4 Increment and Loop:**
55+
56+
After processing each time block, we check which people are still connected to person 0. Those not connected have their parent reset to themselves, effectively "forgetting" the temporary connection.
57+
58+
**2.5 Return Result:**
59+
60+
The result is `[0, 1, 2, 3, 5]` - all people connected to person 0 after all meetings have been processed.

explanations/3693/en.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
## Explanation
2+
3+
### Strategy (The "Why")
4+
5+
**Restate the problem:** We need to climb a staircase with n+1 steps (numbered 0 to n), starting at step 0. From step i, we can jump to step i+1, i+2, or i+3. The cost of jumping from step i to step j is `costs[j] + (j - i)^2`. We want to find the minimum total cost to reach step n.
6+
7+
**1.1 Constraints & Complexity:**
8+
9+
- **Input Size:** n can be up to 10^5, and each cost is between 1 and 10^4.
10+
- **Time Complexity:** O(n) - we iterate through all n steps once, computing the minimum cost for each step.
11+
- **Space Complexity:** O(1) - we use a rolling array of size 3 instead of storing all n values, trading space for constant memory usage.
12+
- **Edge Case:** If n = 1, we can only jump from step 0 to step 1 with cost `costs[0] + 1`.
13+
14+
**1.2 High-level approach:**
15+
16+
The goal is to compute the minimum cost to reach each step by considering the three possible previous steps (i-1, i-2, i-3) and choosing the one with minimum total cost.
17+
18+
**1.3 Brute force vs. optimized strategy:**
19+
20+
- **Brute Force:** Recursively try all paths from step 0 to step n, which would be O(3^n) exponential time.
21+
- **Optimized Strategy:** Use dynamic programming with a rolling array. For each step i, we only need the costs of steps i-1, i-2, and i-3, so we can use a 3-element array and update it in place. This is O(n) time and O(1) space.
22+
- **Optimization:** Since we only need the last 3 values at any time, we use a rolling array instead of storing all n values, reducing space from O(n) to O(1).
23+
24+
**1.4 Decomposition:**
25+
26+
1. Initialize three variables to represent the minimum cost to reach the previous three steps (initially all 0 for step 0).
27+
2. For each step from 1 to n, calculate the minimum cost by considering jumps from the three previous steps.
28+
3. Update the rolling array by shifting values: the oldest value is discarded, and the new value becomes the minimum cost for the current step.
29+
4. Return the cost to reach step n.
30+
31+
### Steps (The "How")
32+
33+
**2.1 Initialization & Example Setup:**
34+
35+
Let's use the example: `n = 4, costs = [1, 2, 3, 4]`
36+
37+
- We start at step 0 with cost 0.
38+
- Initialize: `v0 = 0, v1 = 0, v2 = 0` (costs to reach steps -2, -1, 0, which are all 0)
39+
40+
**2.2 Start Processing:**
41+
42+
We iterate through each cost in the costs array, computing the minimum cost to reach each step.
43+
44+
**2.3 Trace Walkthrough:**
45+
46+
| Step | Cost | v0 | v1 | v2 | Calculation | New v2 |
47+
|------|------|----|----|----|-------------|--------|
48+
| 0 | - | 0 | 0 | 0 | Initial state | - |
49+
| 1 | 1 | 0 | 0 | 0 | min(0+9, 0+4, 0+1) + 1 = 1 | 1 |
50+
| 2 | 2 | 0 | 0 | 1 | min(0+9, 0+4, 1+1) + 2 = 4 | 4 |
51+
| 3 | 3 | 0 | 1 | 4 | min(0+9, 1+4, 4+1) + 3 = 8 | 8 |
52+
| 4 | 4 | 1 | 4 | 8 | min(1+9, 4+4, 8+1) + 4 = 13 | 13 |
53+
54+
After each iteration, we shift: `v0, v1, v2 = v1, v2, new_value`
55+
56+
**2.4 Increment and Loop:**
57+
58+
For each step i, we compute:
59+
- Jump from i-3: `v0 + 9 + costs[i-1]`
60+
- Jump from i-2: `v1 + 4 + costs[i-1]`
61+
- Jump from i-1: `v2 + 1 + costs[i-1]`
62+
63+
We take the minimum of these three options.
64+
65+
**2.5 Return Result:**
66+
67+
The result is `13`, which is the minimum cost to reach step 4. The optimal path is 0 → 1 → 2 → 4 with costs: (1+1) + (2+1) + (4+4) = 2 + 3 + 8 = 13.
68+

explanations/3716/en.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
## Explanation
2+
3+
### Strategy (The "Why")
4+
5+
**Restate the problem:** We need to find customers at risk of churning based on their subscription history. A customer is at churn risk if they: (1) currently have an active subscription (last event is not cancel), (2) have performed at least one downgrade, (3) their current monthly amount is less than 50% of their historical maximum, and (4) have been a subscriber for at least 60 days.
6+
7+
**1.1 Constraints & Complexity:**
8+
9+
- **Input Size:** The subscription_events table can have many rows per user, with multiple events over time.
10+
- **Time Complexity:** O(n log n) where n is the number of events - window functions require sorting, and the EXISTS clause checks for downgrades.
11+
- **Space Complexity:** O(n) for the CTE that stores window function results.
12+
- **Edge Case:** A user who cancels and then starts again should only be considered based on their current active subscription period.
13+
14+
**1.2 High-level approach:**
15+
16+
The goal is to use window functions to compute aggregate statistics (max event date, max monthly amount, subscription duration) for each user, then filter based on the churn risk criteria.
17+
18+
**1.3 Brute force vs. optimized strategy:**
19+
20+
- **Brute Force:** Use multiple subqueries or self-joins to compute max dates, max amounts, and check for downgrades separately for each user. This would be O(n^2) in worst case.
21+
- **Optimized Strategy:** Use window functions (MAX() OVER()) to compute all aggregates in a single pass, then filter in the WHERE clause. This is O(n log n) due to window function sorting.
22+
- **Optimization:** Window functions allow us to compute multiple aggregates simultaneously without multiple table scans, and the EXISTS clause efficiently checks for downgrade events.
23+
24+
**1.4 Decomposition:**
25+
26+
1. Create a CTE that computes window functions for each event: max event date, max monthly amount, and subscription duration (max date - min date).
27+
2. Filter to only the most recent event for each user (where event_date = max_event_date).
28+
3. Apply churn risk criteria: active subscription, has downgrade, current amount < 50% of max, duration >= 60 days.
29+
4. Order results by days_as_subscriber DESC, then user_id ASC.
30+
31+
### Steps (The "How")
32+
33+
**2.1 Initialization & Example Setup:**
34+
35+
Let's consider a user with events:
36+
- 2024-01-01: start premium ($29.99)
37+
- 2024-02-15: downgrade standard ($19.99)
38+
- 2024-03-20: downgrade basic ($9.99)
39+
40+
**2.2 Start Processing:**
41+
42+
The CTE computes for each row:
43+
- `max_event_date`: 2024-03-20 (for all rows of this user)
44+
- `max_historical_amount`: $29.99 (for all rows)
45+
- `days_as_subscriber`: 79 days (2024-03-20 - 2024-01-01)
46+
47+
**2.3 Trace Walkthrough:**
48+
49+
| user_id | event_date | max_event_date | event_type | monthly_amount | max_historical_amount | days_as_subscriber | Keep? |
50+
|---------|------------|----------------|------------|----------------|----------------------|-------------------|-------|
51+
| 501 | 2024-01-01 | 2024-03-20 | start | 29.99 | 29.99 | 79 | No (not latest) |
52+
| 501 | 2024-02-15 | 2024-03-20 | downgrade | 19.99 | 29.99 | 79 | No (not latest) |
53+
| 501 | 2024-03-20 | 2024-03-20 | downgrade | 9.99 | 29.99 | 79 | Yes (meets all criteria) |
54+
55+
After filtering to latest event and applying criteria:
56+
- Active? Yes (downgrade, not cancel)
57+
- Has downgrade? Yes (EXISTS finds downgrade events)
58+
- Current < 50% max? Yes (9.99 / 29.99 = 33.3% < 50%)
59+
- Duration >= 60? Yes (79 >= 60)
60+
61+
**2.4 Increment and Loop:**
62+
63+
The window functions process all events for all users, computing aggregates efficiently. The WHERE clause then filters to only churn risk customers.
64+
65+
**2.5 Return Result:**
66+
67+
The result includes user 501 with current_plan='basic', current_monthly_amount=9.99, max_historical_amount=29.99, and days_as_subscriber=79, ordered by days_as_subscriber DESC, user_id ASC.
68+

solutions/2092/01.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
class Solution:
2+
def findAllPeople(self, n: int, meetings: List[List[int]], firstPerson: int) -> List[int]:
3+
parent = list(range(n))
4+
5+
def find(x):
6+
if parent[x] != x:
7+
parent[x] = find(parent[x])
8+
return parent[x]
9+
10+
def union(a, b):
11+
parent[find(b)] = find(a)
12+
13+
# Sort meetings by time
14+
meetings.sort(key=lambda x: x[2])
15+
16+
# Initially, person 0 and firstPerson know the secret
17+
union(0, firstPerson)
18+
19+
i = 0
20+
while i < len(meetings):
21+
time = meetings[i][2]
22+
involved = []
23+
24+
# Process all meetings at the same time
25+
while i < len(meetings) and meetings[i][2] == time:
26+
x, y, _ = meetings[i]
27+
union(x, y)
28+
involved.append(x)
29+
involved.append(y)
30+
i += 1
31+
32+
# Reset connections that don't include person 0
33+
# Only people connected to person 0 keep the secret
34+
root0 = find(0)
35+
for p in involved:
36+
if find(p) != root0:
37+
parent[p] = p
38+
39+
# Return all people connected to person 0
40+
return [i for i in range(n) if find(i) == find(0)]
41+

solutions/3693/01.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class Solution:
2+
def climbStairs(self, n: int, costs: List[int]) -> int:
3+
# Use rolling array of size 3 to save space
4+
# dp[i] represents minimum cost to reach step i
5+
v0 = v1 = v2 = 0
6+
7+
for i, c in enumerate(costs):
8+
# From step i, we can reach step i+1, i+2, or i+3
9+
# Cost = previous cost + jump cost (1, 4, or 9) + step cost
10+
v = min(v0 + 9, v1 + 4, v2 + 1) + c
11+
v0, v1, v2 = v1, v2, v
12+
13+
return v2
14+

solutions/3716/01.sql

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
WITH query_cte AS (
2+
SELECT
3+
user_id,
4+
event_date,
5+
MAX(event_date) OVER(PARTITION BY user_id) max_event_date,
6+
event_type,
7+
plan_name current_plan,
8+
monthly_amount,
9+
MAX(monthly_amount) OVER(PARTITION BY user_id) max_historical_amount,
10+
DATEDIFF(MAX(event_date) OVER(PARTITION BY user_id), MIN(event_date) OVER(PARTITION BY user_id)) days_as_subscriber
11+
FROM subscription_events
12+
)
13+
SELECT
14+
user_id,
15+
current_plan,
16+
monthly_amount current_monthly_amount,
17+
max_historical_amount,
18+
days_as_subscriber
19+
FROM query_cte q
20+
WHERE event_date = max_event_date
21+
AND EXISTS (
22+
SELECT 1
23+
FROM query_cte q1
24+
WHERE q.user_id = q1.user_id
25+
AND q1.event_type = 'downgrade'
26+
)
27+
AND days_as_subscriber > 59
28+
AND monthly_amount / CAST(max_historical_amount AS FLOAT) <= 0.5
29+
AND event_type <> 'cancel'
30+
ORDER BY days_as_subscriber DESC, user_id;
31+

0 commit comments

Comments
 (0)