mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 21:22:43 +00:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
204e178fa4 | ||
|
|
61dc19fb29 | ||
|
|
5c644ca050 | ||
|
|
fb2d4c88c2 | ||
|
|
1fa0731ea7 | ||
|
|
86107d5ef7 | ||
|
|
00f44a1c47 | ||
|
|
24312437eb | ||
|
|
4c9c8f39ae | ||
|
|
88ff63f5c4 | ||
|
|
2066856d15 | ||
|
|
393d72c9fe | ||
|
|
6c18301aa6 | ||
|
|
02f6a20cc1 | ||
|
|
9288aaa152 | ||
|
|
d5ea71dbc1 | ||
|
|
3e1a66eb37 | ||
|
|
9a2b80949b | ||
|
|
883dc69dba | ||
|
|
e3f0b5420a |
No files matched your search
@@ -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`
|
`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
|
## Reference tables
|
||||||
|
|
||||||
|
|||||||
@@ -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 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.
|
- **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.
|
- **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).
|
- **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`.
|
- **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.
|
- **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 |
|
| `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/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/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) |
|
| `.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
|
## Development Commands
|
||||||
|
|||||||
@@ -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">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 align="center"><code>B</code> ⭐</td><td align="center">502</td><td><a href="https://leetcode.com/problems/ipo/">IPO</a></td></tr>
|
||||||
<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>
|
<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>
|
||||||
<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 & Search Words</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 & Search Words</a></td></tr>
|
||||||
@@ -234,7 +234,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<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>
|
<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>
|
||||||
<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>
|
<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">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 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>
|
<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>
|
<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>
|
||||||
<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>
|
<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">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 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>
|
<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>
|
<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>
|
||||||
<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>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">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 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>
|
<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>
|
<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>
|
||||||
<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>
|
<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>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<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>
|
<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>
|
||||||
<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>
|
<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">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 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>
|
<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>
|
<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>
|
||||||
<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>
|
<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">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 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>
|
<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>
|
<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>
|
||||||
<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>
|
<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">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 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>
|
<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>
|
<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>
|
||||||
<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>
|
<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">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 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>
|
<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>
|
<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>
|
||||||
<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>
|
<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
@@ -14,8 +14,12 @@ Live at `https://srs-api.prdlk.workers.dev`.
|
|||||||
labels, milestones. `catalog.ts` reconciles them into D1 nightly (and via
|
labels, milestones. `catalog.ts` reconciles them into D1 nightly (and via
|
||||||
`/admin/reconcile`); it recomputes from scratch, never invents rows, and
|
`/admin/reconcile`); it recomputes from scratch, never invents rows, and
|
||||||
never overwrites SRS-owned columns (`stage`, `next_review`).
|
never overwrites SRS-owned columns (`stage`, `next_review`).
|
||||||
- **The repo owns the schedule** — `data/schedule.json`, human-edited,
|
- **The repo owns the plan** — `data/schedule.json`, human-edited,
|
||||||
bundled at deploy. Git history is its audit log.
|
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
|
- 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
|
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 —
|
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
|
Blind drills draw only from topics **already learned**: scheduled in an
|
||||||
earlier week (the current week's optional pool is reserved for Saturday's
|
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
|
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
|
## Routes
|
||||||
|
|
||||||
|
|||||||
+26
-24
@@ -1,26 +1,28 @@
|
|||||||
{
|
{
|
||||||
"2026-08-17": 3,
|
"2026-08-17": { "topics": [3] },
|
||||||
"2026-08-18": 4,
|
"2026-08-18": { "topics": [4] },
|
||||||
"2026-08-24": 5,
|
"2026-08-24": { "topics": [5] },
|
||||||
"2026-08-25": 6,
|
"2026-08-25": { "topics": [6] },
|
||||||
"2026-08-26": 7,
|
"2026-08-26": { "topics": [7] },
|
||||||
"2026-08-27": 8,
|
"2026-08-27": { "topics": [8] },
|
||||||
"2026-08-28": 9,
|
"2026-08-28": { "topics": [9] },
|
||||||
"2026-08-31": 10,
|
"2026-08-31": { "topics": [10] },
|
||||||
"2026-09-01": 11,
|
"2026-09-01": { "topics": [11] },
|
||||||
"2026-09-02": 12,
|
"2026-09-02": { "topics": [12] },
|
||||||
"2026-09-03": 13,
|
"2026-09-03": { "topics": [13] },
|
||||||
"2026-09-04": 14,
|
"2026-09-04": { "topics": [14] },
|
||||||
"2026-09-07": 15,
|
"2026-09-07": { "topics": [15] },
|
||||||
"2026-09-08": 16,
|
"2026-09-08": { "topics": [16] },
|
||||||
"2026-09-09": 17,
|
"2026-09-21": { "topics": [17, 18] },
|
||||||
"2026-09-10": 18,
|
"2026-09-22": { "topics": [19, 20] },
|
||||||
"2026-09-11": 19,
|
"2026-09-23": { "topics": [21] },
|
||||||
"2026-09-14": 20,
|
"2026-09-24": { "topics": [22] },
|
||||||
"2026-09-15": 21,
|
"2026-09-25": { "topics": [23] },
|
||||||
"2026-09-16": 22,
|
"2026-09-26": { "gate": [648, 1091, 802, 547, 90] },
|
||||||
"2026-09-17": 23,
|
"2026-09-28": { "topics": [24] },
|
||||||
"2026-09-18": 24,
|
"2026-09-29": { "topics": [25] },
|
||||||
"2026-09-21": 25,
|
"2026-09-30": { "topics": [26] },
|
||||||
"2026-09-22": 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
@@ -5,14 +5,16 @@
|
|||||||
* the HTML and the plain-text alternative from that single tree (React Email
|
* the HTML and the plain-text alternative from that single tree (React Email
|
||||||
* `render`), so the two can never drift.
|
* `render`), so the two can never drift.
|
||||||
*
|
*
|
||||||
* Retrieval rules: review and drill rows carry number + difficulty only —
|
* Retrieval rules: review, drill and sweep rows carry number + difficulty
|
||||||
* never the topic, never a solution link. `DigestRow` has no field for
|
* 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
|
* either, so the rule holds by construction. The plan's own sections — a
|
||||||
* the only labeled section. At most REVIEW_CAP (3) reviews per day; drills
|
* learning day's topics (core, then the optional "if time" list) and a set
|
||||||
* fill toward DAILY_CAP (6) only when the due queue is light. Overflow is
|
* day's problems — are the only labeled, linked ones. At most REVIEW_CAP (3)
|
||||||
* levelled, not stacked: sendDigest gives every review past the cap a
|
* reviews per day; drills fill toward DAILY_CAP (6) only when the due queue
|
||||||
* concrete future date, ≤ REVIEW_CAP per day, oldest first. Sunday is a
|
* is light, and never on a sweep day, which assigns every open miss and
|
||||||
* two-line rest note.
|
* 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
|
* 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
|
* no-op unless forced; the body itself is deterministic (drill picks are
|
||||||
@@ -22,20 +24,23 @@
|
|||||||
import {
|
import {
|
||||||
CAMPAIGN_DAYS,
|
CAMPAIGN_DAYS,
|
||||||
type ProblemRow,
|
type ProblemRow,
|
||||||
|
RESERVED,
|
||||||
REVIEW_CAP,
|
REVIEW_CAP,
|
||||||
SCHEDULE,
|
SCHEDULE,
|
||||||
|
type ScheduledDay,
|
||||||
addDays,
|
addDays,
|
||||||
campaignDay,
|
campaignDay,
|
||||||
campaignWeek,
|
campaignWeek,
|
||||||
dueReviews,
|
dueReviews,
|
||||||
isoWeek,
|
isoWeek,
|
||||||
levelReviews,
|
levelReviews,
|
||||||
|
lookupProblems,
|
||||||
pickDrills,
|
pickDrills,
|
||||||
prettyDate,
|
prettyDate,
|
||||||
streak,
|
streak,
|
||||||
weekdayOf,
|
weekdayOf,
|
||||||
} from "./srs.ts";
|
} 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";
|
import { signLink } from "./links.ts";
|
||||||
|
|
||||||
/** Total daily ceiling: drills only fill what the whole due queue leaves. */
|
/** 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. */
|
/** Everything the email needs, read straight out of D1 and the schedule. */
|
||||||
export async function collectDigest(env: Env, date: string): Promise<DigestData> {
|
export async function collectDigest(env: Env, date: string): Promise<DigestData> {
|
||||||
const db = env.DB;
|
const db = env.DB;
|
||||||
const rest = weekdayOf(date) === 0;
|
const rest = weekdayOf(date) === 0;
|
||||||
|
const plan: ScheduledDay = rest ? {} : (SCHEDULE[date] ?? {});
|
||||||
|
|
||||||
// Reviews surface at most REVIEW_CAP; drills fill toward DAILY_CAP only
|
// 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
|
// 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 due = rest ? [] : await dueReviews(db, date);
|
||||||
const capped = due.slice(0, REVIEW_CAP);
|
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 = {
|
const data: DigestData = {
|
||||||
day: prettyDate(date),
|
day: prettyDate(date),
|
||||||
progress: `Day ${campaignDay(date)} of ${CAMPAIGN_DAYS} · Week ${campaignWeek(date)}`,
|
progress: `Day ${campaignDay(date)} of ${CAMPAIGN_DAYS} · Week ${campaignWeek(date)}`,
|
||||||
streak: await streak(db, date),
|
streak: await streak(db, date),
|
||||||
rest,
|
rest,
|
||||||
|
topics: [],
|
||||||
reviews: await Promise.all(capped.map((p) => tapRow(env, p, date, true))),
|
reviews: await Promise.all(capped.map((p) => tapRow(env, p, date, true))),
|
||||||
carried: due.length - capped.length,
|
carried: due.length - capped.length,
|
||||||
drills: await Promise.all(drills.map((p) => tapRow(env, p, date, false))),
|
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`,
|
progressUrl: `${env.DOCS_URL}/progress`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// New topic — the only section allowed to name problems and link them.
|
// New topics — the plan's sections, the only ones allowed to name problems
|
||||||
const topicIssue = rest ? undefined : SCHEDULE[date];
|
// and link them. Core first; the optional set is "if time", minus anything
|
||||||
if (topicIssue !== undefined) {
|
// the plan has reserved for a gate or a set day.
|
||||||
|
for (const topicIssue of plan.topics ?? []) {
|
||||||
const topic = await db
|
const topic = await db
|
||||||
.prepare("SELECT name FROM topics WHERE issue = ?")
|
.prepare("SELECT name FROM topics WHERE issue = ?")
|
||||||
.bind(topicIssue)
|
.bind(topicIssue)
|
||||||
.first<{ name: string }>();
|
.first<{ name: string }>();
|
||||||
const { results: core } = await db
|
const { results } = await db
|
||||||
.prepare(
|
.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)
|
.bind(topicIssue)
|
||||||
.all<ProblemRow>();
|
.all<ProblemRow>();
|
||||||
data.topic = {
|
data.topics.push({
|
||||||
name: topic?.name ?? `#${topicIssue}`,
|
name: topic?.name ?? `#${topicIssue}`,
|
||||||
core: core.map((p) => ({
|
core: results.filter((p) => p.set_label === "core").map((p) => topicRow(env, p)),
|
||||||
lc: p.lc_number,
|
ifTime: results
|
||||||
difficulty: p.difficulty,
|
.filter((p) => p.set_label === "optional" && !RESERVED.has(p.lc_number))
|
||||||
url: `https://github.com/${env.REPO}/issues/${p.issue}`,
|
.map((p) => topicRow(env, p)),
|
||||||
solved: p.stage !== "new",
|
});
|
||||||
})),
|
}
|
||||||
};
|
|
||||||
|
// 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).
|
// Saturday: the review issue already exists (created at midnight ET).
|
||||||
|
|||||||
+85
-17
@@ -83,7 +83,7 @@ export interface DigestRow {
|
|||||||
failUrl: string;
|
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 {
|
export interface TopicRow {
|
||||||
lc: number;
|
lc: number;
|
||||||
difficulty: string;
|
difficulty: string;
|
||||||
@@ -91,6 +91,13 @@ export interface TopicRow {
|
|||||||
solved: boolean;
|
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 {
|
export interface DigestData {
|
||||||
/** "Tuesday, August 25" */
|
/** "Tuesday, August 25" */
|
||||||
day: string;
|
day: string;
|
||||||
@@ -98,11 +105,16 @@ export interface DigestData {
|
|||||||
progress: string;
|
progress: string;
|
||||||
streak: number;
|
streak: number;
|
||||||
rest: boolean;
|
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: DigestRow[];
|
||||||
/** Reviews past the daily cap; levelled onto the coming days. */
|
/** Reviews past the daily cap; levelled onto the coming days. */
|
||||||
carried: number;
|
carried: number;
|
||||||
drills: DigestRow[];
|
drills: DigestRow[];
|
||||||
|
/** A sweep day's miss log: present (even empty) only on a sweep day. */
|
||||||
|
sweep?: DigestRow[];
|
||||||
gate?: { week: number; url: string };
|
gate?: { week: number; url: string };
|
||||||
yesterday: { total: number; failed: number[] };
|
yesterday: { total: number; failed: number[] };
|
||||||
gateRate?: number;
|
gateRate?: number;
|
||||||
@@ -278,10 +290,15 @@ function Block({
|
|||||||
// ── email ────────────────────────────────────────────────────────
|
// ── email ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function DigestEmail({ data }: { data: DigestData }) {
|
function DigestEmail({ data }: { data: DigestData }) {
|
||||||
const load = data.reviews.length + data.drills.length;
|
const load = data.reviews.length + data.drills.length + (data.sweep?.length ?? 0);
|
||||||
const preview = data.rest
|
const lead = data.topics[0]
|
||||||
? "Rest day — nothing to do but rest."
|
? `, starting with ${data.topics[0].name}`
|
||||||
: `${load} to work through today${data.topic ? `, starting with ${data.topic.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 passed = data.yesterday.total - data.yesterday.failed.length;
|
||||||
const missed = data.yesterday.failed.map((lc) => `LC ${lc}`).join(", ");
|
const missed = data.yesterday.failed.map((lc) => `LC ${lc}`).join(", ");
|
||||||
@@ -310,15 +327,47 @@ function DigestEmail({ data }: { data: DigestData }) {
|
|||||||
|
|
||||||
{data.rest ? null : (
|
{data.rest ? null : (
|
||||||
<>
|
<>
|
||||||
{data.topic ? (
|
{data.topics.map((topic, i) => (
|
||||||
<Section style={{ marginTop: "28px" }}>
|
<Section key={topic.name} style={{ marginTop: i === 0 ? "28px" : "20px" }}>
|
||||||
<Text style={label}>Something new today</Text>
|
{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" }}>
|
<Text style={{ color: INK, fontSize: "16px", fontWeight: 600, margin: "0 0 8px" }}>
|
||||||
{data.topic.name}
|
{topic.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={hint}>Work through these from the top — a tick means it is already solved.</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>
|
</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}
|
) : null}
|
||||||
|
|
||||||
<Block
|
<Block
|
||||||
@@ -409,16 +458,27 @@ function plainDigest(data: DigestData): string {
|
|||||||
: ` · ${data.streak} day${data.streak === 1 ? "" : "s"} in a row, keep it going.`),
|
: ` · ${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.rest) {
|
||||||
if (data.topic) {
|
for (const topic of data.topics) {
|
||||||
out.push(
|
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.",
|
"Work through these from the top; the ones marked done you have already solved.",
|
||||||
...data.topic.core.map(
|
...topic.core.map(linked),
|
||||||
(p) =>
|
);
|
||||||
` LC ${p.lc} — ${DIFFICULTY[p.difficulty]?.label ?? p.difficulty}${p.solved ? " (done)" : ""} — ${p.url}`,
|
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.",
|
"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) {
|
for (const [title, note, rows, empty] of lists) {
|
||||||
out.push("", title);
|
out.push("", title);
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
|
|||||||
+31
-13
@@ -2,21 +2,27 @@
|
|||||||
* Saturday review issue: created at midnight ET so the 8 AM digest can link
|
* Saturday review issue: created at midnight ET so the 8 AM digest can link
|
||||||
* to it; scored when the issue closes (webhook).
|
* to it; scored when the issue closes (webhook).
|
||||||
*
|
*
|
||||||
* The gate half is a topic-blind quiz — one unsolved optional problem per
|
* The gate half is a topic-blind quiz. When the plan fixes the set
|
||||||
* topic scheduled this week (which is why daily drills only draw from
|
* (`gate` in data/schedule.json) that list IS the gate, verbatim — every
|
||||||
* earlier weeks). The recap half is generated data, deliberately NOT a task
|
* number must be a catalog problem, or the issue is not created, because a
|
||||||
* list: the week's solved problems are already scheduled by the +3/+7 ladder,
|
* `/done` on an uncatalogued number can never be logged. Otherwise the set
|
||||||
* and re-assigning them on Saturday would be massed practice.
|
* 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
|
* 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 {
|
import {
|
||||||
type ProblemRow,
|
type ProblemRow,
|
||||||
|
RESERVED,
|
||||||
type Result,
|
type Result,
|
||||||
SCHEDULE,
|
SCHEDULE,
|
||||||
campaignWeek,
|
campaignWeek,
|
||||||
isoWeek,
|
isoWeek,
|
||||||
|
lookupProblems,
|
||||||
rng,
|
rng,
|
||||||
sample,
|
sample,
|
||||||
streak,
|
streak,
|
||||||
@@ -32,7 +38,7 @@ function capitalized(difficulty: string): string {
|
|||||||
return difficulty[0]!.toUpperCase() + difficulty.slice(1);
|
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[]> {
|
async function freshPool(db: D1Database, topic: number, set: string): Promise<ProblemRow[]> {
|
||||||
const { results } = await db
|
const { results } = await db
|
||||||
.prepare(
|
.prepare(
|
||||||
@@ -44,7 +50,7 @@ async function freshPool(db: D1Database, topic: number, set: string): Promise<Pr
|
|||||||
)
|
)
|
||||||
.bind(topic, set)
|
.bind(topic, set)
|
||||||
.all<ProblemRow>();
|
.all<ProblemRow>();
|
||||||
return results;
|
return results.filter((p) => !RESERVED.has(p.lc_number));
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateReport {
|
export interface CreateReport {
|
||||||
@@ -67,12 +73,24 @@ export async function createReviewIssue(env: Env, gh: GitHub, date: string): Pro
|
|||||||
return { created: false, issue: existing.issue, reason: `${title} exists (#${existing.issue})` };
|
return { created: false, issue: existing.issue, reason: `${title} exists (#${existing.issue})` };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Topics scheduled this week; review-only weeks (Sep 23 onward) sample
|
// The plan's fixed set wins; otherwise sample this week's topics, and in a
|
||||||
// across every topic that still has unsolved optional problems.
|
// week without topics, across every topic that still has an unsolved pool.
|
||||||
|
const picks: { p: ProblemRow; fallback: boolean }[] = [];
|
||||||
|
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 ")}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
picks.push(...rows.map((p) => ({ p, fallback: false })));
|
||||||
|
} else {
|
||||||
let topics = Object.entries(SCHEDULE)
|
let topics = Object.entries(SCHEDULE)
|
||||||
.filter(([d]) => campaignWeek(d) === week)
|
.filter(([d]) => campaignWeek(d) === week)
|
||||||
.sort(([a], [b]) => (a < b ? -1 : 1))
|
.sort(([a], [b]) => (a < b ? -1 : 1))
|
||||||
.map(([, t]) => t);
|
.flatMap(([, day]) => day.topics ?? []);
|
||||||
if (topics.length === 0) {
|
if (topics.length === 0) {
|
||||||
const { results } = await db
|
const { results } = await db
|
||||||
.prepare(
|
.prepare(
|
||||||
@@ -89,7 +107,6 @@ export async function createReviewIssue(env: Env, gh: GitHub, date: string): Pro
|
|||||||
if (topics.length === 0) return { created: false, reason: "no topics with unsolved pools" };
|
if (topics.length === 0) return { created: false, reason: "no topics with unsolved pools" };
|
||||||
|
|
||||||
const random = rng(`gate-${iso}`);
|
const random = rng(`gate-${iso}`);
|
||||||
const picks: { p: ProblemRow; fallback: boolean }[] = [];
|
|
||||||
for (const topic of topics) {
|
for (const topic of topics) {
|
||||||
const optional = await freshPool(db, topic, "optional");
|
const optional = await freshPool(db, topic, "optional");
|
||||||
if (optional.length > 0) {
|
if (optional.length > 0) {
|
||||||
@@ -101,6 +118,7 @@ export async function createReviewIssue(env: Env, gh: GitHub, date: string): Pro
|
|||||||
// Both pools exhausted: the topic is fully solved; nothing to quiz.
|
// 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.
|
// Recap: everything attempted this week, plus streak — data, not tasks.
|
||||||
const monday = weekMonday(date);
|
const monday = weekMonday(date);
|
||||||
@@ -154,7 +172,7 @@ export async function createReviewIssue(env: Env, gh: GitHub, date: string): Pro
|
|||||||
|
|
||||||
const milestone = await db
|
const milestone = await db
|
||||||
.prepare("SELECT milestone FROM topics WHERE issue = ? AND milestone IS NOT NULL")
|
.prepare("SELECT milestone FROM topics WHERE issue = ? AND milestone IS NOT NULL")
|
||||||
.bind(topics[0])
|
.bind(picks[0]!.p.topic_issue)
|
||||||
.first<{ milestone: number }>();
|
.first<{ milestone: number }>();
|
||||||
|
|
||||||
const created = (await gh.rest(`/repos/${gh.repo}/issues`, {
|
const created = (await gh.rest(`/repos/${gh.repo}/issues`, {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { describe, expect, test } from "bun:test";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
INTERVAL,
|
INTERVAL,
|
||||||
|
RESERVED,
|
||||||
|
SCHEDULE,
|
||||||
STAGES,
|
STAGES,
|
||||||
WINDOWS,
|
WINDOWS,
|
||||||
addDays,
|
addDays,
|
||||||
@@ -21,6 +23,7 @@ import {
|
|||||||
sample,
|
sample,
|
||||||
TEMPERATURES,
|
TEMPERATURES,
|
||||||
temperatureOf,
|
temperatureOf,
|
||||||
|
topicDate,
|
||||||
weekdayOf,
|
weekdayOf,
|
||||||
workingDay,
|
workingDay,
|
||||||
} from "./srs.ts";
|
} 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", () => {
|
describe("the ladder", () => {
|
||||||
test("two rungs, named after the windows", () => {
|
test("two rungs, named after the windows", () => {
|
||||||
expect(STAGES).toEqual(["new", "+3", "+7", "retired"]);
|
expect(STAGES).toEqual(["new", "+3", "+7", "retired"]);
|
||||||
|
|||||||
+60
-5
@@ -126,12 +126,30 @@ export function prettyDate(date: string): string {
|
|||||||
|
|
||||||
// ── the schedule (bundled; humans edit apps/api/data/schedule.json) ───
|
// ── 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. */
|
/** Date the schedule teaches a topic on, if it teaches it at all. */
|
||||||
export function topicDate(topic: number): string | undefined {
|
export function topicDate(topic: number): string | undefined {
|
||||||
for (const [date, t] of Object.entries(SCHEDULE)) {
|
for (const [date, day] of Object.entries(SCHEDULE)) {
|
||||||
if (t === topic) return date;
|
if (day.topics?.includes(topic)) return date;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -142,6 +160,16 @@ export function topicWeek(topic: number): number | undefined {
|
|||||||
return date === undefined ? undefined : campaignWeek(date);
|
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 ───────────────────────────────────────
|
// ── deterministic sampling ───────────────────────────────────────
|
||||||
|
|
||||||
/** FNV-1a → mulberry32: seeded PRNG so re-runs pick identical problems. */
|
/** 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>();
|
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
|
* 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
|
* 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
|
* 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
|
* 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
|
* topic's core problems has entered the ladder. A skipped learning day never
|
||||||
* feeds drills just because its calendar week lapsed. Never repeated,
|
* feeds drills just because its calendar week lapsed. Problems the plan names
|
||||||
* seeded by date. Boosted topics contribute up to 2 extra.
|
* outright (RESERVED) are never drawn. Never repeated, seeded by date.
|
||||||
|
* Boosted topics contribute up to 2 extra.
|
||||||
*/
|
*/
|
||||||
export async function pickDrills(
|
export async function pickDrills(
|
||||||
db: D1Database,
|
db: D1Database,
|
||||||
@@ -382,6 +436,7 @@ export async function pickDrills(
|
|||||||
)
|
)
|
||||||
.all<ProblemRow & { boost: number }>();
|
.all<ProblemRow & { boost: number }>();
|
||||||
const pool = results.filter((p) => {
|
const pool = results.filter((p) => {
|
||||||
|
if (RESERVED.has(p.lc_number)) return false;
|
||||||
const w = topicWeek(p.topic_issue);
|
const w = topicWeek(p.topic_issue);
|
||||||
return w !== undefined && w < week;
|
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)
|
||||||
|
```
|
||||||
@@ -42,7 +42,14 @@ flowchart TD
|
|||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```js
|
<CodeGroup>
|
||||||
|
|
||||||
|
```py Python
|
||||||
|
class Solution:
|
||||||
|
def containsDuplicate(self, nums: List[int]) -> bool:
|
||||||
|
```
|
||||||
|
|
||||||
|
```js JavaScript
|
||||||
/**
|
/**
|
||||||
* @param {number[]} nums
|
* @param {number[]} nums
|
||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
@@ -59,6 +66,8 @@ var containsDuplicate = function(nums) {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
## Explanation
|
## Explanation
|
||||||
|
|
||||||
<YouTube id="3OamzN90kPg" title="Contains Duplicate - Leetcode 217 - Python" />
|
<YouTube id="3OamzN90kPg" title="Contains Duplicate - Leetcode 217 - Python" />
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
title: '271. Encode and Decode Strings'
|
||||||
|
description: Design an algorithm to encode a list of strings to a string. The encoded string is then sent over the network and is decoded back to the original list of strings
|
||||||
|
sidebar:
|
||||||
|
label: 'Encode and Decode Strings'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Array</Badge>
|
||||||
|
|
||||||
|
::::warning
|
||||||
|
Could you write a generalized algorithm to work on any possible set of characters?
|
||||||
|
::::
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `dummy_input = ["Hello","World"]`
|
||||||
|
- Output: `["Hello","World"]`
|
||||||
|
- Explanation: Machine `1`: Codec encoder = new Codec(); String msg = encoder.encode(strs); Machine `1` ---msg---> Machine `2` Machine `2`: Codec decoder = new Codec(); `String[] strs` = decoder.decode(msg);
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `dummy_input = [""]`
|
||||||
|
- Output: `[""]`
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- `1 <= strs.length <= 200`
|
||||||
|
- `0 <= strs[i].length <= 200`
|
||||||
|
- `strs[i]` contains any possible characters out of 256 valid ASCII characters.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Codec:
|
||||||
|
def encode(self, strs: List[str]) -> str:
|
||||||
|
"""Encodes a list of strings to a single string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def decode(self, s: str) -> List[str]:
|
||||||
|
"""Decodes a single string to a list of strings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Your Codec object will be instantiated and called as such:
|
||||||
|
# codec = Codec()
|
||||||
|
# codec.decode(codec.encode(strs))
|
||||||
|
```
|
||||||
@@ -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,54 @@
|
|||||||
|
---
|
||||||
|
title: '230. Kth Smallest Element in a BST'
|
||||||
|
description: Given the root of a binary search tree, and an integer k, return the k^th smallest value (1-indexed) of all the values of the nodes in the tree
|
||||||
|
sidebar:
|
||||||
|
label: 'Kth Smallest Element in a BST'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Binary Search Tree</Badge>
|
||||||
|
|
||||||
|
::::warning
|
||||||
|
If the BST is modified often (i.e., we can do insert and delete operations) and you need to find the kth smallest frequently, how would you optimize?
|
||||||
|
::::
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `root = [3,1,4,null,2], k = 1`
|
||||||
|
- Output: `1`
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `root = [5,3,6,2,4,null,null,1], k = 3`
|
||||||
|
- Output: `3`
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- The number of nodes in the tree is n.
|
||||||
|
- `1 <= k <= n <= 10^4`
|
||||||
|
- `0 <= Node.val <= 10^4`
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, val=0, left=None, right=None):
|
||||||
|
# self.val = val
|
||||||
|
# self.left = left
|
||||||
|
# self.right = right
|
||||||
|
class Solution:
|
||||||
|
def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:
|
||||||
|
n = 0
|
||||||
|
stack = []
|
||||||
|
cur = root
|
||||||
|
|
||||||
|
while cur or stack:
|
||||||
|
while cur:
|
||||||
|
stack.append(cur)
|
||||||
|
cur = cur.left
|
||||||
|
|
||||||
|
cur = stack.pop()
|
||||||
|
n += 1
|
||||||
|
if n == k:
|
||||||
|
return cur.val
|
||||||
|
cur = cur.right
|
||||||
|
```
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
title: '235. Lowest Common Ancestor of a Binary Search Tree'
|
||||||
|
description: Given a binary search tree (BST), find the lowest common ancestor (LCA) node of two given nodes in the BST
|
||||||
|
sidebar:
|
||||||
|
label: 'Lowest Common Ancestor of a Binary Search Tree'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Binary Search Tree</Badge>
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8`
|
||||||
|
- Output: `6`
|
||||||
|
- Explanation: The LCA of nodes `2` and `8` is `6`.
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4`
|
||||||
|
- Output: `2`
|
||||||
|
- Explanation: The LCA of nodes `2` and `4` is `2`, since a node can be a descendant of itself according to the LCA definition.
|
||||||
|
|
||||||
|
### Example 3:
|
||||||
|
- Input: `root = [2,1], p = 2, q = 1`
|
||||||
|
- Output: `2`
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- The number of nodes in the tree is in the range [2, 10^5].
|
||||||
|
- `-10^9 <= Node.val <= 10^9`
|
||||||
|
- All Node.val are unique.
|
||||||
|
- `p != q`
|
||||||
|
- p and q will exist in the BST.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, x):
|
||||||
|
# self.val = x
|
||||||
|
# self.left = None
|
||||||
|
# self.right = None
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def lowestCommonAncestor(
|
||||||
|
self, root: "TreeNode", p: "TreeNode", q: "TreeNode"
|
||||||
|
) -> "TreeNode":
|
||||||
|
cur = root
|
||||||
|
while cur:
|
||||||
|
if p.val < cur.val and q.val < cur.val:
|
||||||
|
cur = cur.left
|
||||||
|
elif p.val > cur.val and q.val > cur.val:
|
||||||
|
cur = cur.right
|
||||||
|
else:
|
||||||
|
return cur
|
||||||
|
```
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
title: '98. Validate Binary Search Tree'
|
||||||
|
description: Given the root of a binary tree, determine if it is a valid binary search tree (BST)
|
||||||
|
sidebar:
|
||||||
|
label: 'Validate Binary Search Tree'
|
||||||
|
badge: 'Medium'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Badge variant="accent">Binary Search Tree</Badge>
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
- Input: `root = [2,1,3]`
|
||||||
|
- Output: `true`
|
||||||
|
|
||||||
|
### Example 2:
|
||||||
|
- Input: `root = [5,1,4,null,null,3,6]`
|
||||||
|
- Output: `false`
|
||||||
|
- Explanation: The `root` node's value is `5` but its right child's value is `4`.
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
|
||||||
|
- The number of nodes in the tree is in the range [1, 10^4].
|
||||||
|
- `-2^31 <= Node.val <= 2^31 - 1`
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
```py
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, val=0, left=None, right=None):
|
||||||
|
# self.val = val
|
||||||
|
# self.left = left
|
||||||
|
# self.right = right
|
||||||
|
class Solution:
|
||||||
|
def isValidBST(self, root: Optional[TreeNode]) -> bool:
|
||||||
|
def valid(node, low, high):
|
||||||
|
if not node:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not (low < node.val < high):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return valid(node.left, low, node.val) and valid(node.right, node.val, high)
|
||||||
|
|
||||||
|
return valid(root, float("-inf"), float("inf"))
|
||||||
|
```
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: Introduction
|
title: Overview
|
||||||
description: Welcome to your new Blume docs.
|
description: Live SRS campaign dashboard — phases, ladder, review queue, gates, and recent attempts, fetched from the SRS Worker.
|
||||||
---
|
---
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Welcome to **Blume** — markdown-first docs powered by Astro and Vite.
|
Live view of the 56-day campaign. Everything below is fetched client-side
|
||||||
|
from the SRS Worker, so it is always current — no rebuild required.
|
||||||
|
|
||||||
Edit `docs/index.mdx` to get started, then run `blume dev`.
|
<ProgressDashboard />
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
title: Progress
|
|
||||||
description: Live SRS campaign dashboard — phases, ladder, review queue, gates, and recent attempts, fetched from the SRS Worker.
|
|
||||||
---
|
|
||||||
|
|
||||||
Live view of the 56-day campaign. Everything below is fetched client-side
|
|
||||||
from the SRS Worker, so it is always current — no rebuild required.
|
|
||||||
|
|
||||||
<ProgressDashboard />
|
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
---
|
||||||
|
hidden: true
|
||||||
|
---
|
||||||
|
|
||||||
# todo — draining the review backlog
|
# todo — draining the review backlog
|
||||||
|
|
||||||
every solution in `work/1` owes a blind re-solve at `work/3` and a second one at `work/7`. both buckets are empty, so 36 problems owe 72 re-solves and not one has been started. snapshot: `reviewqueues("2026-08-31")` from `apps/cli/work.ts` — what `bun run pick` shows — cross-checked against `/api/stats`.
|
every solution in `work/1` owes a blind re-solve at `work/3` and a second one at `work/7`. both buckets are empty, so 36 problems owe 72 re-solves and not one has been started. snapshot: `reviewqueues("2026-08-31")` from `apps/cli/work.ts` — what `bun run pick` shows — cross-checked against `/api/stats`.
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""
|
||||||
|
217. Contains Duplicate
|
||||||
|
Difficulty: Easy
|
||||||
|
https://leetcode.com/problems/contains-duplicate/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given an integer array nums, return true if any value appears at
|
||||||
|
least twice in the array, and return false if every element is
|
||||||
|
distinct.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: nums = [1,2,3,1]
|
||||||
|
|
||||||
|
Output: true
|
||||||
|
|
||||||
|
Explanation:
|
||||||
|
|
||||||
|
The element 1 occurs at the indices 0 and 3.
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: nums = [1,2,3,4]
|
||||||
|
|
||||||
|
Output: false
|
||||||
|
|
||||||
|
Explanation:
|
||||||
|
|
||||||
|
All elements are distinct.
|
||||||
|
|
||||||
|
Example 3:
|
||||||
|
|
||||||
|
Input: nums = [1,1,1,3,3,4,3,2,4,2]
|
||||||
|
|
||||||
|
Output: true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• 1 <= nums.length <= 10^5
|
||||||
|
|
||||||
|
• -10^9 <= nums[i] <= 10^9
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def containsDuplicate(self, nums: List[int]) -> bool:
|
||||||
|
|
||||||
@@ -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,92 @@
|
|||||||
|
"""
|
||||||
|
271. Encode and Decode Strings
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/encode-and-decode-strings/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Design an algorithm to encode a list of strings to a string. The
|
||||||
|
encoded string is then sent over the network and is decoded back to
|
||||||
|
the original list of strings.
|
||||||
|
|
||||||
|
Machine 1 (sender) has the function:
|
||||||
|
|
||||||
|
string encode(vector<string> strs) {
|
||||||
|
// ... your code
|
||||||
|
return encoded_string;
|
||||||
|
}
|
||||||
|
Machine 2 (receiver) has the function:
|
||||||
|
|
||||||
|
vector<string> decode(string s) {
|
||||||
|
//... your code
|
||||||
|
return strs;
|
||||||
|
}
|
||||||
|
|
||||||
|
So Machine 1 does:
|
||||||
|
|
||||||
|
string encoded_string = encode(strs);
|
||||||
|
|
||||||
|
and Machine 2 does:
|
||||||
|
|
||||||
|
vector<string> strs2 = decode(encoded_string);
|
||||||
|
|
||||||
|
strs2 in Machine 2 should be the same as strs in Machine 1.
|
||||||
|
|
||||||
|
Implement the encode and decode methods.
|
||||||
|
|
||||||
|
You are not allowed to solve the problem using any serialize methods
|
||||||
|
(such as eval).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: dummy_input = ["Hello","World"]
|
||||||
|
Output: ["Hello","World"]
|
||||||
|
Explanation:
|
||||||
|
Machine 1:
|
||||||
|
Codec encoder = new Codec();
|
||||||
|
String msg = encoder.encode(strs);
|
||||||
|
Machine 1 ---msg---> Machine 2
|
||||||
|
|
||||||
|
Machine 2:
|
||||||
|
Codec decoder = new Codec();
|
||||||
|
String[] strs = decoder.decode(msg);
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: dummy_input = [""]
|
||||||
|
Output: [""]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• 1 <= strs.length <= 200
|
||||||
|
|
||||||
|
• 0 <= strs[i].length <= 200
|
||||||
|
|
||||||
|
• strs[i] contains any possible characters out of 256 valid ASCII
|
||||||
|
characters.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Follow up: Could you write a generalized algorithm to work on any
|
||||||
|
possible set of characters?
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Codec:
|
||||||
|
def encode(self, strs: List[str]) -> str:
|
||||||
|
"""Encodes a list of strings to a single string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def decode(self, s: str) -> List[str]:
|
||||||
|
"""Decodes a single string to a list of strings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Your Codec object will be instantiated and called as such:
|
||||||
|
# codec = Codec()
|
||||||
|
# codec.decode(codec.encode(strs))
|
||||||
@@ -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., √(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)
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"""
|
||||||
|
230. Kth Smallest Element in a BST
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/kth-smallest-element-in-a-bst/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given the root of a binary search tree, and an integer k, return the
|
||||||
|
k^th smallest value (1-indexed) of all the values of the nodes in the
|
||||||
|
tree.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: root = [3,1,4,null,2], k = 1
|
||||||
|
Output: 1
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: root = [5,3,6,2,4,null,null,1], k = 3
|
||||||
|
Output: 3
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• The number of nodes in the tree is n.
|
||||||
|
|
||||||
|
• 1 <= k <= n <= 10^4
|
||||||
|
|
||||||
|
• 0 <= Node.val <= 10^4
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Follow up: If the BST is modified often (i.e., we can do insert and
|
||||||
|
delete operations) and you need to find the kth smallest frequently,
|
||||||
|
how would you optimize?
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, val=0, left=None, right=None):
|
||||||
|
# self.val = val
|
||||||
|
# self.left = left
|
||||||
|
# self.right = right
|
||||||
|
class Solution:
|
||||||
|
def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:
|
||||||
|
n = 0
|
||||||
|
stack = []
|
||||||
|
cur = root
|
||||||
|
|
||||||
|
while cur or stack:
|
||||||
|
while cur:
|
||||||
|
stack.append(cur)
|
||||||
|
cur = cur.left
|
||||||
|
|
||||||
|
cur = stack.pop()
|
||||||
|
n += 1
|
||||||
|
if n == k:
|
||||||
|
return cur.val
|
||||||
|
cur = cur.right
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
"""
|
||||||
|
235. Lowest Common Ancestor of a Binary Search Tree
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given a binary search tree (BST), find the lowest common ancestor
|
||||||
|
(LCA) node of two given nodes in the BST.
|
||||||
|
|
||||||
|
According to the definition of LCA on Wikipedia: “The lowest
|
||||||
|
common ancestor is defined between two nodes p and q as the lowest
|
||||||
|
node in T that has both p and q as descendants (where we allow a node
|
||||||
|
to be a descendant of itself).”
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
|
||||||
|
Output: 6
|
||||||
|
Explanation: The LCA of nodes 2 and 8 is 6.
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
|
||||||
|
Output: 2
|
||||||
|
Explanation: The LCA of nodes 2 and 4 is 2, since a node can be a
|
||||||
|
descendant of itself according to the LCA definition.
|
||||||
|
|
||||||
|
Example 3:
|
||||||
|
|
||||||
|
Input: root = [2,1], p = 2, q = 1
|
||||||
|
Output: 2
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• The number of nodes in the tree is in the range [2, 10^5].
|
||||||
|
|
||||||
|
• -10^9 <= Node.val <= 10^9
|
||||||
|
|
||||||
|
• All Node.val are unique.
|
||||||
|
|
||||||
|
• p != q
|
||||||
|
|
||||||
|
• p and q will exist in the BST.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, x):
|
||||||
|
# self.val = x
|
||||||
|
# self.left = None
|
||||||
|
# self.right = None
|
||||||
|
|
||||||
|
|
||||||
|
class Solution:
|
||||||
|
def lowestCommonAncestor(
|
||||||
|
self, root: "TreeNode", p: "TreeNode", q: "TreeNode"
|
||||||
|
) -> "TreeNode":
|
||||||
|
cur = root
|
||||||
|
while cur:
|
||||||
|
if p.val < cur.val and q.val < cur.val:
|
||||||
|
cur = cur.left
|
||||||
|
elif p.val > cur.val and q.val > cur.val:
|
||||||
|
cur = cur.right
|
||||||
|
else:
|
||||||
|
return cur
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""
|
||||||
|
98. Validate Binary Search Tree
|
||||||
|
Difficulty: Medium
|
||||||
|
https://leetcode.com/problems/validate-binary-search-tree/
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Given the root of a binary tree, determine if it is a valid binary
|
||||||
|
search tree (BST).
|
||||||
|
|
||||||
|
A valid BST is defined as follows:
|
||||||
|
|
||||||
|
• The left subtree of a node contains only nodes with keys strictly
|
||||||
|
less than the node's key.
|
||||||
|
|
||||||
|
• The right subtree of a node contains only nodes with keys strictly
|
||||||
|
greater than the node's key.
|
||||||
|
|
||||||
|
• Both the left and right subtrees must also be binary search trees.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example 1:
|
||||||
|
|
||||||
|
Input: root = [2,1,3]
|
||||||
|
Output: true
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
|
||||||
|
Input: root = [5,1,4,null,null,3,6]
|
||||||
|
Output: false
|
||||||
|
Explanation: The root node's value is 5 but its right child's value
|
||||||
|
is 4.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
|
||||||
|
• The number of nodes in the tree is in the range [1, 10^4].
|
||||||
|
|
||||||
|
• -2^31 <= Node.val <= 2^31 - 1
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Definition for a binary tree node.
|
||||||
|
# class TreeNode:
|
||||||
|
# def __init__(self, val=0, left=None, right=None):
|
||||||
|
# self.val = val
|
||||||
|
# self.left = left
|
||||||
|
# self.right = right
|
||||||
|
class Solution:
|
||||||
|
def isValidBST(self, root: Optional[TreeNode]) -> bool:
|
||||||
|
def valid(node, low, high):
|
||||||
|
if not node:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not (low < node.val < high):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return valid(node.left, low, node.val) and valid(node.right, node.val, high)
|
||||||
|
|
||||||
|
return valid(root, float("-inf"), float("inf"))
|
||||||
Reference in new issue
Block a user