Compare commits

...
Author SHA1 Message Date
Prad Nukala 204e178fa4 feat(work): add solution for LeetCode 211 design add and search words data structure 2026-09-21 12:50:53 -04:00
Prad Nukala 61dc19fb29 docs(docs): add MDX page for LeetCode 211 design problem 2026-09-21 12:50:51 -04:00
Prad Nukala 5c644ca050 docs(schedule): clarify schedule JSON format, gate problem handling, and update week dates in README 2026-09-21 12:14:37 -04:00
Prad Nukala fb2d4c88c2 feat(work): add solution for LeetCode 208 Implement Trie (Prefix Tree) 2026-09-21 12:14:33 -04:00
Prad Nukala 1fa0731ea7 docs(docs): add MDX page for LeetCode 208 Implement Trie (Prefix Tree) 2026-09-21 12:14:29 -04:00
Prad Nukala 86107d5ef7 feat(api): add structured schedule plan with topics, problems, gate, sweep and reserved problem handling 2026-09-21 12:14:25 -04:00
Prad Nukala 00f44a1c47 docs(docs): add MDX page for Merge k Sorted Lists problem 2026-09-14 12:50:08 -04:00
Prad Nukala 24312437eb feat(work): add solution for LeetCode 23 Merge k Sorted Lists 2026-09-14 12:49:55 -04:00
Prad Nukala 4c9c8f39ae feat(work): add solution for LeetCode 973 K Closest Points to Origin 2026-09-14 12:37:15 -04:00
Prad Nukala 88ff63f5c4 docs(docs): add MDX page for LeetCode 973 K Closest Points to Origin 2026-09-14 12:37:11 -04:00
Prad Nukala 2066856d15 feat(work): add solution for LeetCode 215 Kth Largest Element in an Array 2026-09-14 12:21:55 -04:00
Prad Nukala 393d72c9fe docs(docs): add MDX page for LeetCode 215 Kth Largest Element in an Array 2026-09-14 12:21:51 -04:00
20 changed files with 1060 additions and 125 deletions

No files matched your search

+6 -1
View File
@@ -114,7 +114,12 @@ One issue per curriculum topic; owns the problem sub-issues.
```
`Scheduled` should come from the calendar of record — `apps/api/data/schedule.json`
(day → topic issue) — not guessed.
(`date → { topics: [issue, …] }`; a doubled day lists two) — not guessed.
Gate problems (`gate: [...]` in the same file) must exist in the catalog before their
Saturday, or the Worker refuses to create the review issue: create each as a
`set:optional` problem sub-issue of its natural topic. Being named in the plan keeps it
out of the drill pool, so it stays unseen until the gate.
## Reference tables
+2 -1
View File
@@ -33,6 +33,7 @@ flowchart LR
- **The picker owns the local review loop; the buckets are its state.** `work/` holds three buckets (`1` = first solve, `3`/`7` = blind re-solves, named after the ladder rungs) and `bun run pick` has three sections — Tab cycles them. Pools are reconciled from the filesystem plus one `git log` (`apps/cli/work.ts`): a first solve dates from the ET date of the commit that added its `work/1` file — the same push that closes the problem issue and starts the +3/+7 windows — and a window stops being owed the moment its bucket holds a file. Uncapped, unlike the review issue's `WINDOW_CAP`: that issue is one day's assignment, the picker is everything still owed. An empty scaffold under `work/1` owes nothing (it is unsolved), which is why `isImplemented()` lives in `apps/cli/source.ts` and is shared with `close-solved.ts`. leetcode-cli reads its output directory from `~/.leetcode/workspaces/<active>/config.json` and nowhere else — no flag, no env var — so `pick.ts` MERGES `workDir` into that file around the child (never replays saved bytes: the CLI owns the file too) and leaves it pointed at `work/1`, then treats the scaffold's existence as the acceptance test, since leetcode-cli exits 0 even when an expired session made it write nothing. The picker itself writes no state: pushing the re-solve is what moves the ladder, via `close-solved` → `/admin/solved` with that file's bucket.
- **The README charts are live, never committed.** They are Worker endpoints reading D1 per request (`Cache-Control: max-age=300`, honored by GitHub Camo), so a solved push moves them within ~5 minutes with no commit and no workflow of their own.
- **D1 owns SRS state; GitHub issues own the catalog; `apps/api/data/schedule.json` owns the calendar.** The catalog reconcile never invents rows and never overwrites SRS columns (`stage`, `next_review`). `Target Date` is the *only* Project field the Worker mirrors (`apps/api/src/mirror.ts`): failures are warnings, never lost D1 writes; topic rows' `Target Date` is never written. SRS stage and first-attempt result live **only** in D1 — do not re-add them as Project fields. The Project's `Set`/`Difficulty` single-selects are a projection of the issue labels, reconciled by `bun run sync-project-fields` — a best-effort final step of `close-solved.yml`, never the Worker.
- **The plan names days; the samplers fill the gaps.** `apps/api/data/schedule.json` maps a date to a `ScheduledDay` (`apps/api/src/srs.ts`): `topics` (one or more topic issues — a doubled day lists two, and the digest renders each with its core list then the optional "if time" list), `problems` (a set day: fixed numbers, rendered like a topic), `gate` (Saturday's blind set, verbatim — `createReviewIssue` refuses to create the issue if any number is not a catalog row, so gate problems must exist as `set:optional` sub-issues of their topic first), or `sweep` (every problem whose last attempt failed, taps to re-log, no drills). Every number a `gate` or `problems` entry names is in `RESERVED`, and the drill/gate samplers never draw it — that is what keeps a planned gate problem unseen until its Saturday. Dates absent from the plan still get the ladder's reviews and drills; a Saturday without a `gate` entry falls back to sampling this week's optional pools.
- **Determinism = idempotency.** Drill/gate sampling uses a seeded PRNG (`rng()` in `apps/api/src/srs.ts`, FNV-1a → mulberry32, seed = date / ISO week); the digest is keyed by ET date in `email_log`; one-tap links are unique on (problem, date, kind).
- **DST-proof crons:** each event has two UTC crons; code fires only on the computed ET hour (`etHour`), unit-checked in `apps/api/src/srs.test.ts`.
- **Chained workflows:** `GITHUB_TOKEN`-driven issue closes fire no `issues` events, so `close-topics.yml` chains off `workflow_run` of Close Solved instead. The Worker's webhook uses its own `GH_PAT`, so its events flow normally.
@@ -44,7 +45,7 @@ flowchart LR
| `work/<1\|3\|7>/<Difficulty>/<Category>/<num>.<slug>.{js,py}` | Solutions, e.g. `work/1/Easy/Array/1.two-sum.py`. The top directory is the spaced-repetition bucket: `1` = first solve, `3`/`7` = the blind re-solves of that problem 3 and 7 days later. Machine-parsed header comment (title / `Difficulty:` / URL / `─` rule / statement) — preserve its exact shape |
| `apps/cli/` | Flat Bun TS workspace: automation entries (shebang + top-level await) and libraries (no shebang, side-effect-free on import). Root `bun run` scripts delegate here |
| `apps/docs/` | Blume site (`blume.config.ts`, `content/`, `islands/`, `public/`). `content/(<category>)/<num>-<slug>.mdx` generated by sync |
| `apps/api/` | Cloudflare Worker workspace: `src/` modules, `data/schedule.json` (day → topic issue, human-edited, bundled at deploy), `migrations/` (append-only; `0002` is the +3/+7 ladder rebuild) |
| `apps/api/` | Cloudflare Worker workspace: `src/` modules, `data/schedule.json` (the plan: `date → { topics | problems | gate | sweep }`, human-edited, bundled at deploy), `migrations/` (append-only; `0002` is the +3/+7 ladder rebuild) |
| `.github/workflows/` | deploy (docs → Pages, Worker → Cloudflare, on every main push), close-solved (issues + `/admin/solved` + Project `Set`/`Difficulty`, needs `SRS_ADMIN_KEY` and `PROJECT_PAT`), close-topics, sync-d1 (issue edits → `/admin/reconcile`), spaced-repetition (daily 06:00 ET cron → today's review issue; `GITHUB_TOKEN` only, no secrets) |
## Development Commands
+10 -10
View File
@@ -212,7 +212,7 @@
<tr><td align="center"><code>B</code> ⭐</td><td align="center">295</td><td><a href="https://leetcode.com/problems/find-median-from-data-stream/">Find Median from Data Stream</a></td></tr>
<tr><td align="center"><code>B</code> ⭐</td><td align="center">502</td><td><a href="https://leetcode.com/problems/ipo/">IPO</a></td></tr>
<tr>
<td rowspan="3"><a href="https://github.com/prdlk/leetcode/issues/17"><strong>15. Trie</strong></a><br><sub>Wk 4 · Wed Sep 9</sub><br><sub>builds on 11</sub></td>
<td rowspan="3"><a href="https://github.com/prdlk/leetcode/issues/17"><strong>15. Trie</strong></a><br><sub>Wk 6 · Mon Sep 21</sub><br><sub>builds on 11</sub></td>
<td align="center"><code>A</code></td><td align="center">208</td><td><a href="https://leetcode.com/problems/implement-trie-prefix-tree/">Implement Trie</a></td>
</tr>
<tr><td align="center"><code>A</code></td><td align="center">211</td><td><a href="https://leetcode.com/problems/design-add-and-search-words-data-structure/">Add &amp; Search Words</a></td></tr>
@@ -234,7 +234,7 @@
</thead>
<tbody>
<tr>
<td rowspan="7"><a href="https://github.com/prdlk/leetcode/issues/18"><strong>16. Graph DFS</strong></a><br><sub>Wk 4 · Thu Sep 10</sub><br><sub>builds on 11 + visited set</sub></td>
<td rowspan="7"><a href="https://github.com/prdlk/leetcode/issues/18"><strong>16. Graph DFS</strong></a><br><sub>Wk 6 · Mon Sep 21</sub><br><sub>builds on 11 + visited set</sub></td>
<td align="center"><code>A</code></td><td align="center">200</td><td><a href="https://leetcode.com/problems/number-of-islands/">Number of Islands</a></td>
</tr>
<tr><td align="center"><code>A</code></td><td align="center">695</td><td><a href="https://leetcode.com/problems/max-area-of-island/">Max Area of Island</a></td></tr>
@@ -244,7 +244,7 @@
<tr><td align="center"><code>B</code></td><td align="center">399</td><td><a href="https://leetcode.com/problems/evaluate-division/">Evaluate Division</a></td></tr>
<tr><td align="center"><code>B</code></td><td align="center">261</td><td><a href="https://leetcode.com/problems/graph-valid-tree/">Graph Valid Tree</a></td></tr>
<tr>
<td rowspan="5"><a href="https://github.com/prdlk/leetcode/issues/19"><strong>17. Graph BFS</strong></a><br><sub>Wk 4 · Fri Sep 11</sub><br><sub>builds on 12 + visited set — shortest path unweighted</sub></td>
<td rowspan="5"><a href="https://github.com/prdlk/leetcode/issues/19"><strong>17. Graph BFS</strong></a><br><sub>Wk 6 · Tue Sep 22</sub><br><sub>builds on 12 + visited set — shortest path unweighted</sub></td>
<td align="center"><code>A</code></td><td align="center">994</td><td><a href="https://leetcode.com/problems/rotting-oranges/">Rotting Oranges</a></td>
</tr>
<tr><td align="center"><code>A</code></td><td align="center">127</td><td><a href="https://leetcode.com/problems/word-ladder/">Word Ladder</a></td></tr>
@@ -252,14 +252,14 @@
<tr><td align="center"><code>B</code></td><td align="center">433</td><td><a href="https://leetcode.com/problems/minimum-genetic-mutation/">Minimum Genetic Mutation</a></td></tr>
<tr><td align="center"><code>B</code></td><td align="center">286</td><td><a href="https://leetcode.com/problems/walls-and-gates/">Walls and Gates</a></td></tr>
<tr>
<td rowspan="4"><a href="https://github.com/prdlk/leetcode/issues/20"><strong>18. Topological Sort</strong></a><br><sub>Wk 5 · Mon Sep 14</sub><br><sub>builds on 16</sub></td>
<td rowspan="4"><a href="https://github.com/prdlk/leetcode/issues/20"><strong>18. Topological Sort</strong></a><br><sub>Wk 6 · Tue Sep 22</sub><br><sub>builds on 16</sub></td>
<td align="center"><code>A</code></td><td align="center">207</td><td><a href="https://leetcode.com/problems/course-schedule/">Course Schedule</a></td>
</tr>
<tr><td align="center"><code>A</code></td><td align="center">210</td><td><a href="https://leetcode.com/problems/course-schedule-ii/">Course Schedule II</a></td></tr>
<tr><td align="center"><code>B</code> ⭐</td><td align="center">269</td><td><a href="https://leetcode.com/problems/alien-dictionary/">Alien Dictionary</a></td></tr>
<tr><td align="center"><code>B</code></td><td align="center">1462</td><td><a href="https://leetcode.com/problems/course-schedule-iv/">Course Schedule IV</a></td></tr>
<tr>
<td rowspan="4"><a href="https://github.com/prdlk/leetcode/issues/21"><strong>19. Union-Find</strong></a><br><sub>Wk 5 · Tue Sep 15</sub><br><sub>builds on 16's problem class</sub></td>
<td rowspan="4"><a href="https://github.com/prdlk/leetcode/issues/21"><strong>19. Union-Find</strong></a><br><sub>Wk 6 · Wed Sep 23</sub><br><sub>builds on 16's problem class</sub></td>
<td align="center"><code>A</code></td><td align="center">684</td><td><a href="https://leetcode.com/problems/redundant-connection/">Redundant Connection</a></td>
</tr>
<tr><td align="center"><code>A</code></td><td align="center">323</td><td><a href="https://leetcode.com/problems/number-of-connected-components-in-an-undirected-graph/">Number of Connected Components</a></td></tr>
@@ -282,7 +282,7 @@
</thead>
<tbody>
<tr>
<td rowspan="8"><a href="https://github.com/prdlk/leetcode/issues/22"><strong>20. Backtracking</strong></a><br><sub>Wk 5 · Wed Sep 16</sub><br><sub>builds on 16 — then unlock LC 212 from topic 15</sub></td>
<td rowspan="8"><a href="https://github.com/prdlk/leetcode/issues/22"><strong>20. Backtracking</strong></a><br><sub>Wk 6 · Thu Sep 24</sub><br><sub>builds on 16 — then unlock LC 212 from topic 15</sub></td>
<td align="center"><code>A</code></td><td align="center">78</td><td><a href="https://leetcode.com/problems/subsets/">Subsets</a></td>
</tr>
<tr><td align="center"><code>A</code></td><td align="center">39</td><td><a href="https://leetcode.com/problems/combination-sum/">Combination Sum</a></td></tr>
@@ -293,7 +293,7 @@
<tr><td align="center"><code>B</code></td><td align="center">22</td><td><a href="https://leetcode.com/problems/generate-parentheses/">Generate Parentheses</a></td></tr>
<tr><td align="center"><code>B</code> ⭐</td><td align="center">52</td><td><a href="https://leetcode.com/problems/n-queens-ii/">N-Queens II</a></td></tr>
<tr>
<td rowspan="9"><a href="https://github.com/prdlk/leetcode/issues/23"><strong>21. 1-D Dynamic Programming</strong></a><br><sub>Wk 5 · Thu Sep 17</sub><br><sub>builds on 20</sub></td>
<td rowspan="9"><a href="https://github.com/prdlk/leetcode/issues/23"><strong>21. 1-D Dynamic Programming</strong></a><br><sub>Wk 6 · Fri Sep 25</sub><br><sub>builds on 20</sub></td>
<td align="center"><code>A</code></td><td align="center">70</td><td><a href="https://leetcode.com/problems/climbing-stairs/">Climbing Stairs</a></td>
</tr>
<tr><td align="center"><code>A</code></td><td align="center">746</td><td><a href="https://leetcode.com/problems/min-cost-climbing-stairs/">Min Cost Climbing Stairs</a></td></tr>
@@ -305,7 +305,7 @@
<tr><td align="center"><code>B</code></td><td align="center">91</td><td><a href="https://leetcode.com/problems/decode-ways/">Decode Ways</a></td></tr>
<tr><td align="center"><code>B</code></td><td align="center">647</td><td><a href="https://leetcode.com/problems/palindromic-substrings/">Palindromic Substrings</a></td></tr>
<tr>
<td rowspan="8"><a href="https://github.com/prdlk/leetcode/issues/24"><strong>22. Multi-D / Grid DP</strong></a><br><sub>Wk 5 · Fri Sep 18</sub><br><sub>builds on 21</sub></td>
<td rowspan="8"><a href="https://github.com/prdlk/leetcode/issues/24"><strong>22. Multi-D / Grid DP</strong></a><br><sub>Wk 7 · Mon Sep 28</sub><br><sub>builds on 21</sub></td>
<td align="center"><code>A</code></td><td align="center">62</td><td><a href="https://leetcode.com/problems/unique-paths/">Unique Paths</a></td>
</tr>
<tr><td align="center"><code>A</code></td><td align="center">64</td><td><a href="https://leetcode.com/problems/minimum-path-sum/">Minimum Path Sum</a></td></tr>
@@ -316,7 +316,7 @@
<tr><td align="center"><code>B</code></td><td align="center">221</td><td><a href="https://leetcode.com/problems/maximal-square/">Maximal Square</a></td></tr>
<tr><td align="center"><code>B</code> ⭐</td><td align="center">97</td><td><a href="https://leetcode.com/problems/interleaving-string/">Interleaving String</a></td></tr>
<tr>
<td rowspan="7"><a href="https://github.com/prdlk/leetcode/issues/25"><strong>23. Greedy</strong></a><br><sub>Wk 6 · Mon Sep 21</sub><br><sub>builds on 21 — includes Kadane's</sub></td>
<td rowspan="7"><a href="https://github.com/prdlk/leetcode/issues/25"><strong>23. Greedy</strong></a><br><sub>Wk 7 · Tue Sep 29</sub><br><sub>builds on 21 — includes Kadane's</sub></td>
<td align="center"><code>A</code></td><td align="center">53</td><td><a href="https://leetcode.com/problems/maximum-subarray/">Maximum Subarray</a></td>
</tr>
<tr><td align="center"><code>A</code></td><td align="center">55</td><td><a href="https://leetcode.com/problems/jump-game/">Jump Game</a></td></tr>
@@ -326,7 +326,7 @@
<tr><td align="center"><code>B</code></td><td align="center">918</td><td><a href="https://leetcode.com/problems/maximum-sum-circular-subarray/">Maximum Sum Circular Subarray</a></td></tr>
<tr><td align="center"><code>B</code></td><td align="center">763</td><td><a href="https://leetcode.com/problems/partition-labels/">Partition Labels</a></td></tr>
<tr>
<td rowspan="7"><a href="https://github.com/prdlk/leetcode/issues/26"><strong>24. Intervals</strong></a><br><sub>Wk 6 · Tue Sep 22</sub><br><sub>builds on 23</sub></td>
<td rowspan="7"><a href="https://github.com/prdlk/leetcode/issues/26"><strong>24. Intervals</strong></a><br><sub>Wk 7 · Wed Sep 30</sub><br><sub>builds on 23</sub></td>
<td align="center"><code>A</code></td><td align="center">57</td><td><a href="https://leetcode.com/problems/insert-interval/">Insert Interval</a></td>
</tr>
<tr><td align="center"><code>A</code></td><td align="center">56</td><td><a href="https://leetcode.com/problems/merge-intervals/">Merge Intervals</a></td></tr>
+10 -3
View File
@@ -14,8 +14,12 @@ Live at `https://srs-api.prdlk.workers.dev`.
labels, milestones. `catalog.ts` reconciles them into D1 nightly (and via
`/admin/reconcile`); it recomputes from scratch, never invents rows, and
never overwrites SRS-owned columns (`stage`, `next_review`).
- **The repo owns the schedule** — `data/schedule.json`, human-edited,
bundled at deploy. Git history is its audit log.
- **The repo owns the plan** — `data/schedule.json`, human-edited,
bundled at deploy. Git history is its audit log. Each date is a
`ScheduledDay`: `topics` (one or more topic issues), `problems` (a set day,
fixed numbers), `gate` (Saturday's blind set, verbatim), or `sweep` (redo
every miss, nothing new). Numbers a `gate` or `problems` entry names are
`RESERVED`: the drill and gate samplers never draw them.
- The GitHub Project mirror writes `Target Date` and nothing else: it is
best-effort, so failures log warnings and never block a D1 write, and topic
rows are never written. SRS stage and first-attempt result stay in D1 only —
@@ -47,7 +51,10 @@ raw `Date` math.
Blind drills draw only from topics **already learned**: scheduled in an
earlier week (the current week's optional pool is reserved for Saturday's
gate) and with at least one core problem on the ladder — a skipped learning
day never feeds drills just because its calendar week lapsed.
day never feeds drills just because its calendar week lapsed. A Saturday
whose plan fixes the `gate` set uses that list as-is; every number must be a
catalog row (a `set:optional` sub-issue of its topic), or the review issue is
not created — a `/done` on an uncatalogued number could never be logged.
## Routes
+26 -24
View File
@@ -1,26 +1,28 @@
{
"2026-08-17": 3,
"2026-08-18": 4,
"2026-08-24": 5,
"2026-08-25": 6,
"2026-08-26": 7,
"2026-08-27": 8,
"2026-08-28": 9,
"2026-08-31": 10,
"2026-09-01": 11,
"2026-09-02": 12,
"2026-09-03": 13,
"2026-09-04": 14,
"2026-09-07": 15,
"2026-09-08": 16,
"2026-09-09": 17,
"2026-09-10": 18,
"2026-09-11": 19,
"2026-09-14": 20,
"2026-09-15": 21,
"2026-09-16": 22,
"2026-09-17": 23,
"2026-09-18": 24,
"2026-09-21": 25,
"2026-09-22": 26
"2026-08-17": { "topics": [3] },
"2026-08-18": { "topics": [4] },
"2026-08-24": { "topics": [5] },
"2026-08-25": { "topics": [6] },
"2026-08-26": { "topics": [7] },
"2026-08-27": { "topics": [8] },
"2026-08-28": { "topics": [9] },
"2026-08-31": { "topics": [10] },
"2026-09-01": { "topics": [11] },
"2026-09-02": { "topics": [12] },
"2026-09-03": { "topics": [13] },
"2026-09-04": { "topics": [14] },
"2026-09-07": { "topics": [15] },
"2026-09-08": { "topics": [16] },
"2026-09-21": { "topics": [17, 18] },
"2026-09-22": { "topics": [19, 20] },
"2026-09-23": { "topics": [21] },
"2026-09-24": { "topics": [22] },
"2026-09-25": { "topics": [23] },
"2026-09-26": { "gate": [648, 1091, 802, 547, 90] },
"2026-09-28": { "topics": [24] },
"2026-09-29": { "topics": [25] },
"2026-09-30": { "topics": [26] },
"2026-10-01": { "problems": [399, 261, 909, 77, 52] },
"2026-10-02": { "sweep": true },
"2026-10-03": { "gate": [347, 1254, 1971, 131, 494, 986] }
}
+70 -24
View File
@@ -5,14 +5,16 @@
* the HTML and the plain-text alternative from that single tree (React Email
* `render`), so the two can never drift.
*
* Retrieval rules: review and drill rows carry number + difficulty only —
* never the topic, never a solution link. `DigestRow` has no field for
* either, so the rule holds by construction. The learning day's core list is
* the only labeled section. At most REVIEW_CAP (3) reviews per day; drills
* fill toward DAILY_CAP (6) only when the due queue is light. Overflow is
* levelled, not stacked: sendDigest gives every review past the cap a
* concrete future date, ≤ REVIEW_CAP per day, oldest first. Sunday is a
* two-line rest note.
* Retrieval rules: review, drill and sweep rows carry number + difficulty
* only — never the topic, never a solution link. `DigestRow` has no field for
* either, so the rule holds by construction. The plan's own sections — a
* learning day's topics (core, then the optional "if time" list) and a set
* day's problems — are the only labeled, linked ones. At most REVIEW_CAP (3)
* reviews per day; drills fill toward DAILY_CAP (6) only when the due queue
* is light, and never on a sweep day, which assigns every open miss and
* nothing new. Overflow is levelled, not stacked: sendDigest gives every
* review past the cap a concrete future date, ≤ REVIEW_CAP per day, oldest
* first. Sunday is a two-line rest note.
*
* Idempotency: email_log keys sends by ET date — a same-day re-send is a
* no-op unless forced; the body itself is deterministic (drill picks are
@@ -22,20 +24,23 @@
import {
CAMPAIGN_DAYS,
type ProblemRow,
RESERVED,
REVIEW_CAP,
SCHEDULE,
type ScheduledDay,
addDays,
campaignDay,
campaignWeek,
dueReviews,
isoWeek,
levelReviews,
lookupProblems,
pickDrills,
prettyDate,
streak,
weekdayOf,
} from "./srs.ts";
import { type DigestData, type DigestRow, renderDigest } from "./email.tsx";
import { type DigestData, type DigestRow, type TopicRow, renderDigest } from "./email.tsx";
import { signLink } from "./links.ts";
/** Total daily ceiling: drills only fill what the whole due queue leaves. */
@@ -63,23 +68,50 @@ async function tapRow(env: Env, p: ProblemRow, date: string, staged: boolean): P
};
}
/** A plan-named problem: linked to its issue, ticked once it has entered the ladder. */
function topicRow(env: Env, p: ProblemRow): TopicRow {
return {
lc: p.lc_number,
difficulty: p.difficulty,
url: `https://github.com/${env.REPO}/issues/${p.issue}`,
solved: p.stage !== "new",
};
}
/** Problems whose most recent attempt failed — the miss log a sweep day works through. */
async function openMisses(db: D1Database): Promise<ProblemRow[]> {
const { results } = await db
.prepare(
`SELECT p.* FROM problems p
JOIN attempts a ON a.lc_number = p.lc_number
WHERE a.id = (SELECT MAX(id) FROM attempts WHERE lc_number = p.lc_number)
AND a.result = 'fail' AND p.stage != 'retired'
ORDER BY a.date, p.lc_number`,
)
.all<ProblemRow>();
return results;
}
/** Everything the email needs, read straight out of D1 and the schedule. */
export async function collectDigest(env: Env, date: string): Promise<DigestData> {
const db = env.DB;
const rest = weekdayOf(date) === 0;
const plan: ScheduledDay = rest ? {} : (SCHEDULE[date] ?? {});
// Reviews surface at most REVIEW_CAP; drills fill toward DAILY_CAP only
// when the WHOLE due queue is light — a heavy backlog must never trade
// review slots for brand-new drill problems.
// review slots for brand-new drill problems — and never on a sweep day.
const due = rest ? [] : await dueReviews(db, date);
const capped = due.slice(0, REVIEW_CAP);
const drills = rest ? [] : await pickDrills(db, date, DAILY_CAP - Math.min(due.length, DAILY_CAP));
const drills =
rest || plan.sweep ? [] : await pickDrills(db, date, DAILY_CAP - Math.min(due.length, DAILY_CAP));
const data: DigestData = {
day: prettyDate(date),
progress: `Day ${campaignDay(date)} of ${CAMPAIGN_DAYS} · Week ${campaignWeek(date)}`,
streak: await streak(db, date),
rest,
topics: [],
reviews: await Promise.all(capped.map((p) => tapRow(env, p, date, true))),
carried: due.length - capped.length,
drills: await Promise.all(drills.map((p) => tapRow(env, p, date, false))),
@@ -88,28 +120,42 @@ export async function collectDigest(env: Env, date: string): Promise<DigestData>
progressUrl: `${env.DOCS_URL}/progress`,
};
// New topic — the only section allowed to name problems and link them.
const topicIssue = rest ? undefined : SCHEDULE[date];
if (topicIssue !== undefined) {
// New topics — the plan's sections, the only ones allowed to name problems
// and link them. Core first; the optional set is "if time", minus anything
// the plan has reserved for a gate or a set day.
for (const topicIssue of plan.topics ?? []) {
const topic = await db
.prepare("SELECT name FROM topics WHERE issue = ?")
.bind(topicIssue)
.first<{ name: string }>();
const { results: core } = await db
const { results } = await db
.prepare(
"SELECT * FROM problems WHERE topic_issue = ? AND set_label = 'core' ORDER BY lc_number",
"SELECT * FROM problems WHERE topic_issue = ? AND set_label IN ('core', 'optional') ORDER BY lc_number",
)
.bind(topicIssue)
.all<ProblemRow>();
data.topic = {
data.topics.push({
name: topic?.name ?? `#${topicIssue}`,
core: core.map((p) => ({
lc: p.lc_number,
difficulty: p.difficulty,
url: `https://github.com/${env.REPO}/issues/${p.issue}`,
solved: p.stage !== "new",
})),
};
core: results.filter((p) => p.set_label === "core").map((p) => topicRow(env, p)),
ifTime: results
.filter((p) => p.set_label === "optional" && !RESERVED.has(p.lc_number))
.map((p) => topicRow(env, p)),
});
}
// A set day assigns problems by number rather than by topic.
if (plan.problems) {
const { rows, missing } = await lookupProblems(db, plan.problems);
if (missing.length) console.warn(`digest ${date}: planned problems not in the catalog: ${missing.join(", ")}`);
data.assigned = rows.map((p) => topicRow(env, p));
}
// A sweep day works the miss log: every problem still standing on a
// failure, minus the ones today's review list already carries.
if (plan.sweep) {
const reviewed = new Set(capped.map((p) => p.lc_number));
const misses = (await openMisses(db)).filter((p) => !reviewed.has(p.lc_number));
data.sweep = await Promise.all(misses.map((p) => tapRow(env, p, date, true)));
}
// Saturday: the review issue already exists (created at midnight ET).
+85 -17
View File
@@ -83,7 +83,7 @@ export interface DigestRow {
failUrl: string;
}
/** A learning-day core problem — the only rows allowed a link. */
/** A plan-named problem — the only rows allowed a link. */
export interface TopicRow {
lc: number;
difficulty: string;
@@ -91,6 +91,13 @@ export interface TopicRow {
solved: boolean;
}
/** One learning-day topic: the core list, then the optional "if time" list. */
export interface TopicSection {
name: string;
core: TopicRow[];
ifTime: TopicRow[];
}
export interface DigestData {
/** "Tuesday, August 25" */
day: string;
@@ -98,11 +105,16 @@ export interface DigestData {
progress: string;
streak: number;
rest: boolean;
topic?: { name: string; core: TopicRow[] };
/** Topics taught today, in plan order — empty on every other day. */
topics: TopicSection[];
/** A set day's problems, assigned by number rather than by topic. */
assigned?: TopicRow[];
reviews: DigestRow[];
/** Reviews past the daily cap; levelled onto the coming days. */
carried: number;
drills: DigestRow[];
/** A sweep day's miss log: present (even empty) only on a sweep day. */
sweep?: DigestRow[];
gate?: { week: number; url: string };
yesterday: { total: number; failed: number[] };
gateRate?: number;
@@ -278,10 +290,15 @@ function Block({
// ── email ────────────────────────────────────────────────────────
function DigestEmail({ data }: { data: DigestData }) {
const load = data.reviews.length + data.drills.length;
const preview = data.rest
? "Rest day — nothing to do but rest."
: `${load} to work through today${data.topic ? `, starting with ${data.topic.name}` : ""}.`;
const load = data.reviews.length + data.drills.length + (data.sweep?.length ?? 0);
const lead = data.topics[0]
? `, starting with ${data.topics[0].name}`
: data.assigned
? ", starting with today's set"
: data.sweep
? " — sweep day, nothing new"
: "";
const preview = data.rest ? "Rest day — nothing to do but rest." : `${load} to work through today${lead}.`;
const passed = data.yesterday.total - data.yesterday.failed.length;
const missed = data.yesterday.failed.map((lc) => `LC ${lc}`).join(", ");
@@ -310,15 +327,47 @@ function DigestEmail({ data }: { data: DigestData }) {
{data.rest ? null : (
<>
{data.topic ? (
<Section style={{ marginTop: "28px" }}>
<Text style={label}>Something new today</Text>
{data.topics.map((topic, i) => (
<Section key={topic.name} style={{ marginTop: i === 0 ? "28px" : "20px" }}>
{i === 0 ? (
<Text style={label}>
{data.topics.length === 1 ? "Something new today" : "Two new things today"}
</Text>
) : null}
<Text style={{ color: INK, fontSize: "16px", fontWeight: 600, margin: "0 0 8px" }}>
{data.topic.name}
{topic.name}
</Text>
<Text style={hint}>Work through these from the top — a tick means it is already solved.</Text>
<TopicTable rows={data.topic.core} />
<TopicTable rows={topic.core} />
{topic.ifTime.length > 0 ? (
<>
<Text style={{ ...hint, margin: "12px 0 8px" }}>
If time — only once the core is done inside the clock.
</Text>
<TopicTable rows={topic.ifTime} />
</>
) : null}
</Section>
))}
{data.assigned ? (
<Section style={{ marginTop: "28px" }}>
<Text style={label}>Today's set</Text>
<Text style={hint}>
Picked by number, not by topic — all of them, in order, inside the clock.
</Text>
<TopicTable rows={data.assigned} />
</Section>
) : null}
{data.sweep ? (
<Block
title="Sweep — redo every red"
note="Every problem still standing on a miss. Nothing new today; clear these first."
rows={data.sweep}
staged
empty="The miss log is clear — nothing is standing on a fail."
/>
) : null}
<Block
@@ -409,16 +458,27 @@ function plainDigest(data: DigestData): string {
: ` · ${data.streak} day${data.streak === 1 ? "" : "s"} in a row, keep it going.`),
];
const linked = (p: TopicRow) =>
` LC ${p.lc} — ${DIFFICULTY[p.difficulty]?.label ?? p.difficulty}${p.solved ? " (done)" : ""} — ${p.url}`;
if (!data.rest) {
if (data.topic) {
for (const topic of data.topics) {
out.push(
"",
`SOMETHING NEW TODAY — ${data.topic.name}`,
`SOMETHING NEW TODAY — ${topic.name}`,
"Work through these from the top; the ones marked done you have already solved.",
...data.topic.core.map(
(p) =>
` LC ${p.lc} — ${DIFFICULTY[p.difficulty]?.label ?? p.difficulty}${p.solved ? " (done)" : ""} — ${p.url}`,
),
...topic.core.map(linked),
);
if (topic.ifTime.length > 0) {
out.push(" If time — only once the core is done inside the clock:", ...topic.ifTime.map(linked));
}
}
if (data.assigned) {
out.push(
"",
"TODAY'S SET",
"Picked by number, not by topic — all of them, in order, inside the clock.",
...data.assigned.map(linked),
);
}
@@ -436,6 +496,14 @@ function plainDigest(data: DigestData): string {
"No cold starts today.",
],
];
if (data.sweep) {
lists.unshift([
"SWEEP — REDO EVERY RED",
"Every problem still standing on a miss. Nothing new today; clear these first.",
data.sweep,
"The miss log is clear — nothing is standing on a fail.",
]);
}
for (const [title, note, rows, empty] of lists) {
out.push("", title);
if (rows.length === 0) {
+58 -40
View File
@@ -2,21 +2,27 @@
* Saturday review issue: created at midnight ET so the 8 AM digest can link
* to it; scored when the issue closes (webhook).
*
* The gate half is a topic-blind quiz — one unsolved optional problem per
* topic scheduled this week (which is why daily drills only draw from
* earlier weeks). The recap half is generated data, deliberately NOT a task
* list: the week's solved problems are already scheduled by the +3/+7 ladder,
* and re-assigning them on Saturday would be massed practice.
* The gate half is a topic-blind quiz. When the plan fixes the set
* (`gate` in data/schedule.json) that list IS the gate, verbatim — every
* number must be a catalog problem, or the issue is not created, because a
* `/done` on an uncatalogued number can never be logged. Otherwise the set
* is sampled: one unsolved optional problem per topic scheduled this week
* (which is why daily drills only draw from earlier weeks), never one the
* plan has reserved. The recap half is generated data, deliberately NOT a
* task list: the week's solved problems are already scheduled by the +3/+7
* ladder, and re-assigning them on Saturday would be massed practice.
*
* Label is `review` (the retired Actions system owned `gate`). Creation is
* idempotent by week; deferred Hards never appear.
* idempotent by week; deferred Hards never appear in a sampled set.
*/
import {
type ProblemRow,
RESERVED,
type Result,
SCHEDULE,
campaignWeek,
isoWeek,
lookupProblems,
rng,
sample,
streak,
@@ -32,7 +38,7 @@ function capitalized(difficulty: string): string {
return difficulty[0]!.toUpperCase() + difficulty.slice(1);
}
/** Unsolved problems of a topic in a set — never attempted, never drilled. */
/** Unsolved problems of a topic in a set — never attempted, drilled, or reserved. */
async function freshPool(db: D1Database, topic: number, set: string): Promise<ProblemRow[]> {
const { results } = await db
.prepare(
@@ -44,7 +50,7 @@ async function freshPool(db: D1Database, topic: number, set: string): Promise<Pr
)
.bind(topic, set)
.all<ProblemRow>();
return results;
return results.filter((p) => !RESERVED.has(p.lc_number));
}
export interface CreateReport {
@@ -67,40 +73,52 @@ export async function createReviewIssue(env: Env, gh: GitHub, date: string): Pro
return { created: false, issue: existing.issue, reason: `${title} exists (#${existing.issue})` };
}
// Topics scheduled this week; review-only weeks (Sep 23 onward) sample
// across every topic that still has unsolved optional problems.
let topics = Object.entries(SCHEDULE)
.filter(([d]) => campaignWeek(d) === week)
.sort(([a], [b]) => (a < b ? -1 : 1))
.map(([, t]) => t);
if (topics.length === 0) {
const { results } = await db
.prepare(
`SELECT DISTINCT topic_issue AS t FROM problems
WHERE set_label = 'optional' AND stage = 'new'
AND lc_number NOT IN (SELECT lc_number FROM drill_pool_used)
ORDER BY topic_issue`,
)
.all<{ t: number }>();
topics = sample(results.map((r) => r.t), MAX_GATE_PROBLEMS, rng(`gate-topics-${iso}`)).sort(
(a, b) => a - b,
);
}
if (topics.length === 0) return { created: false, reason: "no topics with unsolved pools" };
const random = rng(`gate-${iso}`);
// The plan's fixed set wins; otherwise sample this week's topics, and in a
// week without topics, across every topic that still has an unsolved pool.
const picks: { p: ProblemRow; fallback: boolean }[] = [];
for (const topic of topics) {
const optional = await freshPool(db, topic, "optional");
if (optional.length > 0) {
picks.push({ p: sample(optional, 1, random)[0]!, fallback: false });
continue;
const planned = SCHEDULE[date]?.gate;
if (planned) {
const { rows, missing } = await lookupProblems(db, planned);
if (missing.length) {
return {
created: false,
reason: `planned gate names problems outside the catalog: LC ${missing.join(", LC ")}`,
};
}
const core = await freshPool(db, topic, "core");
if (core.length > 0) picks.push({ p: sample(core, 1, random)[0]!, fallback: true });
// Both pools exhausted: the topic is fully solved; nothing to quiz.
picks.push(...rows.map((p) => ({ p, fallback: false })));
} else {
let topics = Object.entries(SCHEDULE)
.filter(([d]) => campaignWeek(d) === week)
.sort(([a], [b]) => (a < b ? -1 : 1))
.flatMap(([, day]) => day.topics ?? []);
if (topics.length === 0) {
const { results } = await db
.prepare(
`SELECT DISTINCT topic_issue AS t FROM problems
WHERE set_label = 'optional' AND stage = 'new'
AND lc_number NOT IN (SELECT lc_number FROM drill_pool_used)
ORDER BY topic_issue`,
)
.all<{ t: number }>();
topics = sample(results.map((r) => r.t), MAX_GATE_PROBLEMS, rng(`gate-topics-${iso}`)).sort(
(a, b) => a - b,
);
}
if (topics.length === 0) return { created: false, reason: "no topics with unsolved pools" };
const random = rng(`gate-${iso}`);
for (const topic of topics) {
const optional = await freshPool(db, topic, "optional");
if (optional.length > 0) {
picks.push({ p: sample(optional, 1, random)[0]!, fallback: false });
continue;
}
const core = await freshPool(db, topic, "core");
if (core.length > 0) picks.push({ p: sample(core, 1, random)[0]!, fallback: true });
// Both pools exhausted: the topic is fully solved; nothing to quiz.
}
if (picks.length === 0) return { created: false, reason: `every pool for week ${week} is exhausted` };
}
if (picks.length === 0) return { created: false, reason: `every pool for week ${week} is exhausted` };
// Recap: everything attempted this week, plus streak — data, not tasks.
const monday = weekMonday(date);
@@ -154,7 +172,7 @@ export async function createReviewIssue(env: Env, gh: GitHub, date: string): Pro
const milestone = await db
.prepare("SELECT milestone FROM topics WHERE issue = ? AND milestone IS NOT NULL")
.bind(topics[0])
.bind(picks[0]!.p.topic_issue)
.first<{ milestone: number }>();
const created = (await gh.rest(`/repos/${gh.repo}/issues`, {
+35
View File
@@ -8,6 +8,8 @@ import { describe, expect, test } from "bun:test";
import {
INTERVAL,
RESERVED,
SCHEDULE,
STAGES,
WINDOWS,
addDays,
@@ -21,6 +23,7 @@ import {
sample,
TEMPERATURES,
temperatureOf,
topicDate,
weekdayOf,
workingDay,
} from "./srs.ts";
@@ -125,6 +128,38 @@ describe("the rest day", () => {
});
});
describe("the plan", () => {
test("never books the rest day, and gates only on Saturday", () => {
for (const [date, day] of Object.entries(SCHEDULE)) {
expect(isRestDay(date)).toBe(false);
if (day.gate) expect(weekdayOf(date)).toBe(6);
if (day.topics) expect(weekdayOf(date)).toBeLessThan(6);
}
});
test("a topic is taught once; a doubled day answers for both", () => {
const seen = new Set<number>();
for (const day of Object.values(SCHEDULE)) {
for (const topic of day.topics ?? []) {
expect(seen.has(topic)).toBe(false);
seen.add(topic);
}
}
expect(topicDate(17)).toBe("2026-09-21");
expect(topicDate(18)).toBe("2026-09-21");
expect(topicDate(999)).toBeUndefined();
});
test("every gate and set problem is reserved from the samplers", () => {
for (const day of Object.values(SCHEDULE)) {
for (const lc of [...(day.gate ?? []), ...(day.problems ?? [])]) {
expect(RESERVED.has(lc)).toBe(true);
}
}
expect(RESERVED.size).toBeGreaterThan(0);
});
});
describe("the ladder", () => {
test("two rungs, named after the windows", () => {
expect(STAGES).toEqual(["new", "+3", "+7", "retired"]);
+60 -5
View File
@@ -126,12 +126,30 @@ export function prettyDate(date: string): string {
// ── the schedule (bundled; humans edit apps/api/data/schedule.json) ───
export const SCHEDULE: Record<string, number> = scheduleJson;
/**
* One calendar day of the plan. Most days teach topics (one, or two when the
* plan doubles up); the rest are named by what they assign instead: a fixed
* gate set for Saturday's review issue, a fixed problem set (a "B-set" day),
* or a sweep — redo every miss, nothing new. Absent dates are unplanned:
* the ladder still runs, the digest just has nothing new to say.
*/
export interface ScheduledDay {
/** Topic issues taught that day, in the order the digest presents them. */
topics?: number[];
/** Problems assigned by number instead of by topic. */
problems?: number[];
/** Saturday: the blind set, fixed by the plan rather than sampled. */
gate?: number[];
/** Miss-log sweep: every problem whose last attempt failed, and no new ones. */
sweep?: boolean;
}
export const SCHEDULE: Record<string, ScheduledDay> = scheduleJson;
/** Date the schedule teaches a topic on, if it teaches it at all. */
export function topicDate(topic: number): string | undefined {
for (const [date, t] of Object.entries(SCHEDULE)) {
if (t === topic) return date;
for (const [date, day] of Object.entries(SCHEDULE)) {
if (day.topics?.includes(topic)) return date;
}
return undefined;
}
@@ -142,6 +160,16 @@ export function topicWeek(topic: number): number | undefined {
return date === undefined ? undefined : campaignWeek(date);
}
/**
* Every problem the plan names outright — a gate's blind set or a set day's
* list. The plan owns these: the samplers (daily drills, a sampled gate)
* never draw them, so a gate problem stays unseen until its Saturday and a
* set day's problems wait for their day.
*/
export const RESERVED: ReadonlySet<number> = new Set(
Object.values(SCHEDULE).flatMap((day) => [...(day.gate ?? []), ...(day.problems ?? [])]),
);
// ── deterministic sampling ───────────────────────────────────────
/** FNV-1a → mulberry32: seeded PRNG so re-runs pick identical problems. */
@@ -248,6 +276,31 @@ export async function getProblem(db: D1Database, lc: number): Promise<ProblemRow
return db.prepare("SELECT * FROM problems WHERE lc_number = ?").bind(lc).first<ProblemRow>();
}
/**
* Catalog rows for a list of numbers the plan names, in the plan's order,
* plus the numbers the catalog does not know — a planned problem without an
* issue can never be logged, so callers decide whether that is fatal.
*/
export async function lookupProblems(
db: D1Database,
lcs: number[],
): Promise<{ rows: ProblemRow[]; missing: number[] }> {
if (lcs.length === 0) return { rows: [], missing: [] };
const { results } = await db
.prepare(`SELECT * FROM problems WHERE lc_number IN (${lcs.map(() => "?").join(",")})`)
.bind(...lcs)
.all<ProblemRow>();
const byNumber = new Map(results.map((p) => [p.lc_number, p]));
const rows: ProblemRow[] = [];
const missing: number[] = [];
for (const lc of lcs) {
const row = byNumber.get(lc);
if (row) rows.push(row);
else missing.push(lc);
}
return { rows, missing };
}
/**
* What "due" means, in one place: not retired, scheduled on/before ?1, and
* past its deferral if it has one. Every caller that asks "what is open on
@@ -358,8 +411,9 @@ export async function levelReviews(db: D1Database, date: string): Promise<number
* scheduled in an earlier week (the current week's optional pool is reserved
* for Saturday's gate) AND showing learning evidence: at least one of the
* topic's core problems has entered the ladder. A skipped learning day never
* feeds drills just because its calendar week lapsed. Never repeated,
* seeded by date. Boosted topics contribute up to 2 extra.
* feeds drills just because its calendar week lapsed. Problems the plan names
* outright (RESERVED) are never drawn. Never repeated, seeded by date.
* Boosted topics contribute up to 2 extra.
*/
export async function pickDrills(
db: D1Database,
@@ -382,6 +436,7 @@ export async function pickDrills(
)
.all<ProblemRow & { boost: number }>();
const pool = results.filter((p) => {
if (RESERVED.has(p.lc_number)) return false;
const w = topicWeek(p.topic_issue);
return w !== undefined && w < week;
});
@@ -0,0 +1,41 @@
---
title: '215. Kth Largest Element in an Array'
description: Given an integer array nums and an integer k, return the k^th largest element in the array
sidebar:
label: 'Kth Largest Element in an Array'
badge: 'Medium'
---
<Badge variant="accent">Heap / Priority Queue</Badge>
### Example 1:
- Input: `nums = [3,2,1,5,6,4], k = 2`
- Output: `5`
### Example 2:
- Input: `nums = [3,2,3,1,2,4,5,5,6], k = 4`
- Output: `4`
### Constraints:
- `1 <= k <= nums.length <= 10^5`
- `-10^4 <= nums[i] <= 10^4`
## Solution
```py
import heapq
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
for i in range(len(nums)):
nums[i] = -nums[i]
heapq.heapify(nums)
for _ in range(k - 1):
heapq.heappop(nums)
return -heapq.heappop(nums)
```
@@ -0,0 +1,48 @@
---
title: '973. K Closest Points to Origin'
description: Given an array of points where points[i] = [xi, yi] represents a point on the X-Y plane and an integer k, return the k closest points to the origin (0, 0)
sidebar:
label: 'K Closest Points to Origin'
badge: 'Medium'
---
<Badge variant="accent">Heap / Priority Queue</Badge>
### Example 1:
- Input: `points = [[1,3],[-2,2]], k = 1`
- Output: `[[-2,2]]`
- Explanation: The distance between `(1`, `3`) and the origin is sqrt(10). The distance between `(-2`, `2`) and the origin is sqrt(8). Since sqrt(8) < sqrt(10), `(-2`, `2`) is closer to the origin. We only want the closest `k = 1 points` from the origin, so the answer is just [[-2,2]].
### Example 2:
- Input: `points = [[3,3],[5,-1],[-2,4]], k = 2`
- Output: `[[3,3],[-2,4]]`
- Explanation: The answer [[-2,4],[3,3]] would also be accepted.
### Constraints:
- `1 <= k <= points.length <= 10^4`
- `-10^4 <= xi, yi <= 10^4`
## Solution
```py
import heapq
class Solution:
def kClosest(self, points: List[List[int]], k: int) -> List[List[int]]:
minHeap = []
for x, y in points:
dist = (x**2) + (y**2)
minHeap.append([dist, x, y])
heapq.heapify(minHeap)
res = []
while k > 0:
dist, x, y = heapq.heappop(minHeap)
res.append([x, y])
k -= 1
return res
```
@@ -0,0 +1,70 @@
---
title: '208. Implement Trie (Prefix Tree)'
description: A trie (pronounced as "try") or prefix tree is a tree data structure used to efficiently store and retrieve keys in a dataset of strings. There are various applications of this data structure, such as autocomplete and spellchecker
sidebar:
label: 'Implement Trie (Prefix Tree)'
badge: 'Medium'
---
<Badge variant="accent">Trie</Badge>
### Example 1:
- Input: ``
- Output: ``
- Explanation: Input ["Trie", "insert", "search", "search", "startsWith", "insert", "search"] `[[]`, `["apple"]`, `["apple"]`, `["app"]`, `["app"]`, `["app"]`, ["app"]] Output [null, null, true, false, true, null, true] Explanation Trie trie = new Trie(); trie.insert("apple"); trie.search("apple"); // return True trie.search("app"); // return False trie.startsWith("app"); // return True trie.insert("app"); trie.search("app"); // return True
### Constraints:
- `1 <= word.length, prefix.length <= 2000`
- `word` and `prefix` consist only of lowercase English letters.
- At most 3 * 10^4 calls in total will be made to insert, search, and startsWith.
## Solution
```py
class TrieNode:
def __init__(self):
self.children = {}
self.endOfWord = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str) -> None:
cur = self.root
for c in word:
if c not in cur.children:
cur.children[c] = TrieNode()
cur = cur.children[c]
cur.endOfWord = True
def search(self, word: str) -> bool:
cur = self.root
for c in word:
if c not in cur.children:
return False
cur = cur.children[c]
return cur.endOfWord
def startsWith(self, prefix: str) -> bool:
cur = self.root
for c in prefix:
if c not in cur.children:
return False
cur = cur.children[c]
return True
# Your Trie object will be instantiated and called as such:
# obj = Trie()
# obj.insert(word)
# param_2 = obj.search(word)
# param_3 = obj.startsWith(prefix)
```
@@ -0,0 +1,69 @@
---
title: '23. Merge k Sorted Lists'
description: You are given an array of k linked-lists lists, each linked-list is sorted in ascending order
sidebar:
label: 'Merge k Sorted Lists'
badge: 'Hard'
---
<Badge variant="accent">Heap / Priority Queue</Badge>
### Example 1:
- Input: `lists = [[1,4,5],[1,3,4],[2,6]]`
- Output: `[1,1,2,3,4,4,5,6]`
- Explanation: The linked-lists are: [ 1->4->5, 1->3->4, 2->6 ] merging them into one sorted linked list: 1->1->2->3->4->4->5->6
### Example 2:
- Input: `lists = []`
- Output: `[]`
### Example 3:
- Input: `lists = [[]]`
- Output: `[]`
### Constraints:
- `k == lists.length`
- `0 <= k <= 10^4`
- `0 <= lists[i].length <= 500`
- `-10^4 <= lists[i][j] <= 10^4`
- `lists[i]` is sorted in ascending order.
- The sum of lists[i].length will not exceed 10^4.
## Solution
```py
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
import heapq
class Solution:
def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
heap = []
# K log K
for i, node in enumerate(lists):
if node:
heapq.heappush(heap, (node.val, i, node))
D = ListNode()
cur = D
# n log k
while heap:
val, i, node = heapq.heappop(heap)
cur.next = node
cur = node
node = node.next
if node:
heapq.heappush(heap, (node.val, i, node))
# Time: O(N log k)
# Space: O(n)
return D.next
```
@@ -0,0 +1,65 @@
---
title: '211. Design Add and Search Words Data Structure'
description: Design a data structure that supports adding new words and finding if a string matches any previously added string
sidebar:
label: 'Design Add and Search Words Data Structure'
badge: 'Medium'
---
<Badge variant="accent">Trie</Badge>
### Constraints:
- `1 <= word.length <= 25`
- `word` in addWord consists of lowercase English letters.
- `word` in search consist of '.' or lowercase English letters.
- There will be at most 2 dots in `word` for search queries.
- At most 10^4 calls will be made to addWord and search.
## Solution
```py
class TrieNode:
def __init__(self):
self.children = {}
self.endOfWord = False
class WordDictionary:
def __init__(self):
self.root = TrieNode()
def addWord(self, word: str) -> None:
cur = self.root
for c in word:
if c not in cur.children:
cur.children[c] = TrieNode()
cur = cur.children[c]
cur.endOfWord = True
# Leverage Recursion
def search(self, word: str) -> bool:
def dfs(j, root):
cur = root
for i in range(j, len(word)):
c = word[i]
if c == ".":
for child in cur.children.values():
if dfs(i + 1, child):
return True
return False
else:
if c not in cur.children:
return False
cur = cur.children[c]
return cur.endOfWord
return dfs(0, self.root)
# Your WordDictionary object will be instantiated and called as such:
# obj = WordDictionary()
# obj.addWord(word)
# param_2 = obj.search(word)
```
@@ -0,0 +1,87 @@
"""
23. Merge k Sorted Lists
Difficulty: Hard
https://leetcode.com/problems/merge-k-sorted-lists/
──────────────────────────────────────────────────
You are given an array of k linked-lists lists, each linked-list is
sorted in ascending order.
Merge all the linked-lists into one sorted linked-list and return it.
Example 1:
Input: lists = [[1,4,5],[1,3,4],[2,6]]
Output: [1,1,2,3,4,4,5,6]
Explanation: The linked-lists are:
[
1->4->5,
1->3->4,
2->6
]
merging them into one sorted linked list:
1->1->2->3->4->4->5->6
Example 2:
Input: lists = []
Output: []
Example 3:
Input: lists = [[]]
Output: []
Constraints:
• k == lists.length
• 0 <= k <= 10^4
• 0 <= lists[i].length <= 500
• -10^4 <= lists[i][j] <= 10^4
• lists[i] is sorted in ascending order.
• The sum of lists[i].length will not exceed 10^4.
"""
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
import heapq
class Solution:
def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
heap = []
# K log K
for i, node in enumerate(lists):
if node:
heapq.heappush(heap, (node.val, i, node))
D = ListNode()
cur = D
# n log k
while heap:
val, i, node = heapq.heappop(heap)
cur.next = node
cur = node
node = node.next
if node:
heapq.heappush(heap, (node.val, i, node))
# Time: O(N log k)
# Space: O(n)
return D.next
@@ -0,0 +1,50 @@
"""
215. Kth Largest Element in an Array
Difficulty: Medium
https://leetcode.com/problems/kth-largest-element-in-an-array/
──────────────────────────────────────────────────
Given an integer array nums and an integer k, return the k^th largest
element in the array.
Note that it is the k^th largest element in the sorted order, not the
k^th distinct element.
Can you solve it without sorting?
Example 1:
Input: nums = [3,2,1,5,6,4], k = 2
Output: 5
Example 2:
Input: nums = [3,2,3,1,2,4,5,5,6], k = 4
Output: 4
Constraints:
• 1 <= k <= nums.length <= 10^5
• -10^4 <= nums[i] <= 10^4
"""
import heapq
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
for i in range(len(nums)):
nums[i] = -nums[i]
heapq.heapify(nums)
for _ in range(k - 1):
heapq.heappop(nums)
return -heapq.heappop(nums)
@@ -0,0 +1,64 @@
"""
973. K Closest Points to Origin
Difficulty: Medium
https://leetcode.com/problems/k-closest-points-to-origin/
──────────────────────────────────────────────────
Given an array of points where points[i] = [xi, yi] represents a
point on the X-Y plane and an integer k, return the k closest points
to the origin (0, 0).
The distance between two points on the X-Y plane is the Euclidean
distance (i.e., &radic;(x1 - x2)^2 + (y1 - y2)^2).
You may return the answer in any order. The answer is guaranteed to
be unique (except for the order that it is in).
Example 1:
Input: points = [[1,3],[-2,2]], k = 1
Output: [[-2,2]]
Explanation:
The distance between (1, 3) and the origin is sqrt(10).
The distance between (-2, 2) and the origin is sqrt(8).
Since sqrt(8) < sqrt(10), (-2, 2) is closer to the origin.
We only want the closest k = 1 points from the origin, so the answer
is just [[-2,2]].
Example 2:
Input: points = [[3,3],[5,-1],[-2,4]], k = 2
Output: [[3,3],[-2,4]]
Explanation: The answer [[-2,4],[3,3]] would also be accepted.
Constraints:
• 1 <= k <= points.length <= 10^4
• -10^4 <= xi, yi <= 10^4
"""
import heapq
class Solution:
def kClosest(self, points: List[List[int]], k: int) -> List[List[int]]:
minHeap = []
for x, y in points:
dist = (x**2) + (y**2)
minHeap.append([dist, x, y])
heapq.heapify(minHeap)
res = []
while k > 0:
dist, x, y = heapq.heappop(minHeap)
res.append([x, y])
k -= 1
return res
@@ -0,0 +1,103 @@
"""
208. Implement Trie (Prefix Tree)
Difficulty: Medium
https://leetcode.com/problems/implement-trie-prefix-tree/
──────────────────────────────────────────────────
A trie (pronounced as "try") or prefix tree is a tree data structure
used to efficiently store and retrieve keys in a dataset of strings.
There are various applications of this data structure, such as
autocomplete and spellchecker.
Implement the Trie class:
• Trie() Initializes the trie object.
• void insert(String word) Inserts the string word into the trie.
• boolean search(String word) Returns true if the string word is in
the trie (i.e., was inserted before), and false otherwise.
• boolean startsWith(String prefix) Returns true if there is a
previously inserted string word that has the prefix prefix, and false
otherwise.
Example 1:
Input
["Trie", "insert", "search", "search", "startsWith", "insert",
"search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
Output
[null, null, true, false, true, null, true]
Explanation
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // return True
trie.search("app"); // return False
trie.startsWith("app"); // return True
trie.insert("app");
trie.search("app"); // return True
Constraints:
• 1 <= word.length, prefix.length <= 2000
• word and prefix consist only of lowercase English letters.
• At most 3 * 10^4 calls in total will be made to insert, search,
and startsWith.
"""
class TrieNode:
def __init__(self):
self.children = {}
self.endOfWord = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str) -> None:
cur = self.root
for c in word:
if c not in cur.children:
cur.children[c] = TrieNode()
cur = cur.children[c]
cur.endOfWord = True
def search(self, word: str) -> bool:
cur = self.root
for c in word:
if c not in cur.children:
return False
cur = cur.children[c]
return cur.endOfWord
def startsWith(self, prefix: str) -> bool:
cur = self.root
for c in prefix:
if c not in cur.children:
return False
cur = cur.children[c]
return True
# Your Trie object will be instantiated and called as such:
# obj = Trie()
# obj.insert(word)
# param_2 = obj.search(word)
# param_3 = obj.startsWith(prefix)
@@ -0,0 +1,101 @@
"""
211. Design Add and Search Words Data Structure
Difficulty: Medium
https://leetcode.com/problems/design-add-and-search-words-data-structure/
──────────────────────────────────────────────────
Design a data structure that supports adding new words and finding if
a string matches any previously added string.
Implement the WordDictionary class:
• WordDictionary() Initializes the object.
• void addWord(word) Adds word to the data structure, it can be
matched later.
• bool search(word) Returns true if there is any string in the data
structure that matches word or false otherwise. word may contain dots
'.' where dots can be matched with any letter.
Example:
Input
["WordDictionary","addWord","addWord","addWord","search","search","search","search"]
[[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]]
Output
[null,null,null,null,false,true,true,true]
Explanation
WordDictionary wordDictionary = new WordDictionary();
wordDictionary.addWord("bad");
wordDictionary.addWord("dad");
wordDictionary.addWord("mad");
wordDictionary.search("pad"); // return False
wordDictionary.search("bad"); // return True
wordDictionary.search(".ad"); // return True
wordDictionary.search("b.."); // return True
Constraints:
• 1 <= word.length <= 25
• word in addWord consists of lowercase English letters.
• word in search consist of '.' or lowercase English letters.
• There will be at most 2 dots in word for search queries.
• At most 10^4 calls will be made to addWord and search.
"""
class TrieNode:
def __init__(self):
self.children = {}
self.endOfWord = False
class WordDictionary:
def __init__(self):
self.root = TrieNode()
def addWord(self, word: str) -> None:
cur = self.root
for c in word:
if c not in cur.children:
cur.children[c] = TrieNode()
cur = cur.children[c]
cur.endOfWord = True
# Leverage Recursion
def search(self, word: str) -> bool:
def dfs(j, root):
cur = root
for i in range(j, len(word)):
c = word[i]
if c == ".":
for child in cur.children.values():
if dfs(i + 1, child):
return True
return False
else:
if c not in cur.children:
return False
cur = cur.children[c]
return cur.endOfWord
return dfs(0, self.root)
# Your WordDictionary object will be instantiated and called as such:
# obj = WordDictionary()
# obj.addWord(word)
# param_2 = obj.search(word)