Compare commits

...
Author SHA1 Message Date
Prad Nukala 5b6a33088c feat(work): add Minimum Path Sum & Longest Common Subsequence solutions; refactor Unique Paths DP implementation 2026-09-28 13:57:07 -04:00
Prad Nukala e9f76a32be feat: complete lc-62 2026-09-28 12:39:28 -04:00
Prad Nukala 951f958bd9 docs(docs): clarify phase-based content paths and frontmatter ownership in documentation 2026-09-28 11:20:55 -04:00
Prad Nukala d2888360e2 docs(docs): add ProblemBadges component, layout slot, and enrich pages with topics/video frontmatter, plus guide placeholders and topic‑landing map. 2026-09-28 11:20:41 -04:00
Prad Nukala b035d1ffa0 feat(config): add optional frontmatter schemas and Guides tab 2026-09-28 11:19:36 -04:00
Prad Nukala 0473a3651f feat(cli): sync LeetCode topic tags, generate guide placeholders, and write video frontmatter instead of embeds 2026-09-28 11:19:32 -04:00
Prad Nukala a9550769f0 docs(agents): correct bullet prefix for finished topic rule 2026-09-25 14:15:49 -04:00
Prad Nukala 51c8c771c6 feat(config): backdate GitHub releases via custom fetch implementation 2026-09-25 14:15:44 -04:00
Prad Nukala 7cedd235b5 docs(cli): clarify changelog dating uses timever tag instead of API publish date 2026-09-25 14:15:41 -04:00
Prad Nukala 107de04587 docs(agents): clarify release body wording for finished topic rule 2026-09-25 14:07:07 -04:00
Prad Nukala 8787246db7 refactor(cli): simplify release topic naming and summary generation 2026-09-25 14:06:09 -04:00
Prad Nukala 85b54b565e feat(config): add Changelog tab to docs navigation 2026-09-25 14:06:05 -04:00
Prad Nukala 8e8522a773 feat(cli): add release name handling for topic releases 2026-09-25 14:05:52 -04:00
Prad Nukala 6f9e4ecb3b chore: clean up content phase organization 2026-09-25 14:00:22 -04:00
Prad Nukala 5dc2265ee5 chore(config): add GitHub repository metadata to docs config 2026-09-25 13:36:50 -04:00
Prad Nukala 0ca130156d chore: clean up content phase organization 2026-09-25 13:30:06 -04:00
Prad Nukala 0d64df58f1 chore: clean up content phase organization 2026-09-25 13:27:06 -04:00
Prad Nukala 88d1767184 docs: replace legacy line‑dot‑left‑horizontal icons with unified dot icon across docs 2026-09-25 13:17:35 -04:00
Prad Nukala 93381f2b69 style(cli): change default page icon to "dot" 2026-09-25 13:17:31 -04:00
Prad Nukala 58ab9db1d7 docs(docs): add page icon frontmatter to problem pages 2026-09-25 13:14:25 -04:00
Prad Nukala e189bb876e feat(cli): add page icon frontmatter support 2026-09-25 13:14:22 -04:00
Prad Nukala 314f2bff10 docs(docs): rename progress_1.mdx to keywords.mdx 2026-09-25 13:11:13 -04:00
Prad Nukala 082b67c44d docs(docs): add Keywords reference page 2026-09-25 13:10:59 -04:00
Prad Nukala ad7c0a2972 docs(readme): update documentation URL to lc.prd.lk 2026-09-24 16:26:18 -04:00
Prad Nukala 931c2c64be docs(docs): remove /leetcode prefix from homepage tile URLs 2026-09-24 16:26:15 -04:00
Prad Nukala 0a99557aac feat(config): update deployment site URL and reformat font variant definitions 2026-09-24 16:26:12 -04:00
Prad Nukala 7875b43981 docs(api): update docs URL and origin endpoints 2026-09-24 16:26:10 -04:00
Prad Nukala f34a9bb56d ci(git): sync redirects in pre-commit hook 2026-09-24 16:15:59 -04:00
Prad Nukala bfeadc0cdf docs(docs): add generated short URL redirects for LeetCode problem pages 2026-09-24 16:15:55 -04:00
Prad Nukala ca05c32f6e feat(config): add custom redirects to Blume config 2026-09-24 16:15:52 -04:00
Prad Nukala ad129b99fd feat(cli): generate short numeric redirects for problem pages 2026-09-24 16:15:49 -04:00
Prad Nukala 717feb7f70 build(config): switch to local Fontsource fonts for offline Blume builds 2026-09-24 16:07:17 -04:00
Prad Nukala bf0ac34a43 chore: clean up content phase organization 2026-09-24 15:57:38 -04:00
Prad Nukala 403a2d7f23 chore: clean up obsolete examples 2026-09-24 15:32:21 -04:00
Prad Nukala 3445c47ad2 docs(docs): change homepage icon to play 2026-09-24 15:17:19 -04:00
Prad Nukala 24a9c52260 docs(docs): add hidden index pages for algorithm topics and progress dashboard component 2026-09-24 15:17:08 -04:00
Prad Nukala 9d535c6826 feat(cli): preserve leading numeric prefixes in Blume group slugs 2026-09-24 15:17:04 -04:00
Prad Nukala a2d32c5f06 docs: add icons to topic meta definitions 2026-09-24 14:45:12 -04:00
Prad Nukala c69582552b feat(cli): add sidebar icons mapping for topics 2026-09-24 14:45:09 -04:00
Prad Nukala f0f479d6cf feat: add lc 46, 39, 78 2026-09-24 14:36:19 -04:00
Prad Nukala ac4dc654fc feat(work): add solution for LeetCode 323 number of connected components problem 2026-09-23 15:40:39 -04:00
Prad Nukala 273148c08b feat: add lc 684 2026-09-23 15:22:26 -04:00
Prad Nukala 7960dee431 docs(docs): adjust default zoom to 1.6× for improved label readability 2026-09-22 15:10:30 -04:00
241 changed files with 5208 additions and 1882 deletions

No files matched your search

+5 -6
View File
@@ -5,14 +5,13 @@ description: Write the `## Approach` section — a Mermaid flowchart that traces
# Problem approach diagrams
Every solution page (`apps/docs/content/(<category>)/<num>-<slug>.mdx`) has three
hand-owned sections around the script-owned one:
Every solution page (`apps/docs/content/(<phase>)/<topic>/<num>-<slug>.mdx`) has a
hand-owned section before the script-owned one:
```
frontmatter → <Badge> → ::::warning (maybe) → ### Example N / ### Constraints
frontmatter → ::::warning (maybe) → ### Example N / ### Constraints
## Approach ← this skill (Mermaid flowchart of the code below)
## Solution ← bun run sync owns this; never edit inside it
## Explanation ← bun run videos --write (NeetCode embed); not this skill
```
`sync.ts` regenerates only `## Solution`, so an `## Approach` placed **before** it
@@ -144,7 +143,7 @@ there (then `bun run sync`).
- Do not write prose approaches, complexity tables or alternative algorithms — the
section is one flowchart (plus the warning callout when the code is broken).
- Do not touch `## Solution` or anything after it; `## Explanation` is filled by
`bun run videos --write`.
- Do not touch `## Solution` or anything after it, nor the frontmatter `topics:` /
`video:` keys (`bun run sync` / `bun run videos --write` own them).
- Do not add an approach to `reference/` pages or `index.mdx` — only numbered
solution pages (`title: 'NN. …'`) carry one.
+7 -6
View File
@@ -1,8 +1,9 @@
#!/bin/sh
# Pre-commit: the docs pages under apps/docs/content are generated from work/1
# by `bun run sync`, and deploy.yml publishes whatever main holds. Regenerate
# them now and stage what sync changed, so a solution and its page land in the
# same commit and a stale `## Solution` never reaches main.
# Pre-commit: the docs pages under apps/docs/content (and the `/<num>` short
# links in apps/docs/redirects.ts) are generated from work/1 by `bun run sync`,
# and deploy.yml publishes whatever main holds. Regenerate them now and stage
# what sync changed, so a solution, its page and its redirect land in the same
# commit and a stale `## Solution` never reaches main.
#
# Sync reads the working tree, not the index, so the pages it writes only
# match this commit when work/1 has nothing unstaged: an untracked or
@@ -26,9 +27,9 @@ fi
before=$(mktemp)
trap 'rm -f "$before"' EXIT
git status --porcelain -- apps/docs/content | sort > "$before"
git status --porcelain -- apps/docs/content apps/docs/redirects.ts | sort > "$before"
report=$(bun run --silent sync)
generated=$(git status --porcelain -- apps/docs/content | sort | comm -13 "$before" - | cut -c4-)
generated=$(git status --porcelain -- apps/docs/content apps/docs/redirects.ts | sort | comm -13 "$before" - | cut -c4-)
if [ -n "$generated" ]; then
printf '%s\n' "$report" | grep -v 'skipped (in sync)' >&2
printf '%s\n' "$generated" | xargs git add --
+37 -1
View File
@@ -7,7 +7,7 @@ on:
workflow_dispatch:
inputs:
dry_run:
description: Report matches without closing anything
description: Report matches and would-be releases without closing or releasing anything
type: boolean
default: false
@@ -61,3 +61,39 @@ jobs:
exit 0
fi
bun apps/cli/sync-project-fields.ts
# One timever release per finished topic (every core sub-issue closed),
# tagged at the instant the last core problem closed. It reads the same rule
# close-topics uses (apps/cli/topics.ts) straight off the sub-issues, so the
# push that finishes a topic releases it in this run — no waiting for Close
# Topics, which chains after this workflow. Reconciling: a run releases
# every finished topic still missing one, so a manual dispatch backfills and
# a problem closed by hand in the UI is picked up by the next run.
release:
needs: close
# Like close-topics: a partially failed close still closed some issues,
# and the script only reads state, so run unless the run was cancelled.
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
permissions:
contents: write # push tags, create releases
issues: read
steps:
# Full history: each tag targets the last main commit before its topic
# finished, and backfilled tags reach far behind HEAD.
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
# The tagger of the annotated, backdated tags.
- run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# No install step: the script only uses Bun builtins + fetch.
- run: bun apps/cli/release-topics.ts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DRY_RUN: ${{ inputs.dry_run }}
+6
View File
@@ -30,7 +30,13 @@ jobs:
- uses: oven-sh/setup-bun@v2 # skip if you use npm/pnpm
- run: bun install --frozen-lockfile
# blume.config.ts pulls the /changelog pages from this repo's GitHub
# releases (githubReleases source) at build time. The token authenticates
# that fetch — required if the repo is private, and it lifts the shared
# runner's unauthenticated API rate limit. `contents: read` covers it.
- run: bun run build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/upload-pages-artifact@v3
with:
+13 -9
View File
@@ -14,6 +14,7 @@ flowchart LR
work -->|close-solved.yml| PI[problem issues]
work -->|close-solved.yml POST /admin/solved| D1[(D1 = SRS state)]
PI -->|close-topics.yml| TI[topic issues]
PI -->|close-solved.yml release job| Rel[timever release per finished topic]
GH[GitHub issues = catalog] -->|nightly reconcile| D1
D1 -->|8 AM ET cron| Mail[digest email + one-tap links]
Mail -->|GET /log| D1
@@ -37,6 +38,7 @@ flowchart LR
- **Determinism = idempotency.** Drill/gate sampling uses a seeded PRNG (`rng()` in `apps/api/src/srs.ts`, FNV-1a → mulberry32, seed = date / ISO week); the digest is keyed by ET date in `email_log`; one-tap links are unique on (problem, date, kind).
- **DST-proof crons:** each event has two UTC crons; code fires only on the computed ET hour (`etHour`), unit-checked in `apps/api/src/srs.test.ts`.
- **Chained workflows:** `GITHUB_TOKEN`-driven issue closes fire no `issues` events, so `close-topics.yml` chains off `workflow_run` of Close Solved instead. The Worker's webhook uses its own `GH_PAT`, so its events flow normally.
- **One "finished topic" rule, two consumers.** `apps/cli/topics.ts` (`coreSet()`: every `set:core` sub-issue closed, zero core = never) is read by `close-topics.ts` (closes the topic issue) and `release-topics.ts` (the `release` job of `close-solved.yml`). The release reads the sub-issues, not the topic issue, so the push that finishes a topic releases it in the same run instead of waiting for Close Topics to chain after it. Version = [timever](https://gist.github.com/twolfson/de1b004dd22536b8e668) (`YYYYMMDD.HHMMSS.NNNNNNNNN`, UTC) of the instant the last core problem closed; the tag is annotated and backdated to that instant, on the last first-parent main commit before it; the body is a changelog snapshot at that instant (a one-sentence summary first — Blume's changelog makes it the page's meta description — then closed on the finish day / earlier / open at release, with solution links at the tag). A release is found by its `<!-- release-topics: #N -->` body marker, never by tag name, so re-closing a core problem never mints a second one; an existing release keeps its tag and has its name (the topic title) and body rewritten whenever the render changes. The docs `/changelog` is these releases (`githubReleases` in `apps/docs/blume.config.ts`, fetched at build with `GITHUB_TOKEN`), dated by the timever tag, not `published_at` (which the API cannot backdate): `releasesDatedByTag` rewrites each timever release's `published_at` before Blume maps it, via `custom(githubReleasesSource(…))` because the config-level `githubReleases()` takes no `fetchImpl`. Changing the tag format breaks the changelog dates.
## Key Directories
@@ -44,10 +46,10 @@ flowchart LR
|---|---|
| `work/<1\|3\|7>/<Difficulty>/<Category>/<num>.<slug>.{js,py}` | Solutions, e.g. `work/1/Easy/Array/1.two-sum.py`. The top directory is the spaced-repetition bucket: `1` = first solve, `3`/`7` = the blind re-solves of that problem 3 and 7 days later. Machine-parsed header comment (title / `Difficulty:` / URL / `─` rule / statement) — preserve its exact shape |
| `apps/cli/` | Flat Bun TS workspace: automation entries (shebang + top-level await) and libraries (no shebang, side-effect-free on import). Root `bun run` scripts delegate here |
| `apps/docs/` | Blume site (`blume.config.ts`, `content/`, `components/` + `components.ts`, `islands/`, `public/`). `content/<topic>/<num>-<slug>.mdx` plus one `meta.ts` per topic folder, all generated by sync: the folder is the README campaign topic slug (`tree-dfs/`, `heap-priority-queue/`) and a real route segment (`/tree-dfs/invert-binary-tree`), the `meta.ts` carries the exact label and topic number so the sidebar reads in curriculum order, and the page badge is the `work/` data-structure category. A hand-written `<topic>/index.mdx` (`hidden: true`) is that topic's landing page: Blume ≥ 1.7.3 links the sidebar group row to it (`sliding-window/index.mdx` → `/sliding-window`) and the hidden flag drops the duplicate row. Two kinds of custom MDX component, both import-free on every page: `islands/<Name>.tsx` is hydrated React (auto-discovered; `ProgressDashboard`, `DependencySpine` — the README topic map as data, zoom/pan, nodes link to whatever route Blume's `/api/docs/navigation.json` reports for the topic folder, inert until a topic has pages), `components/<Name>.astro` registered in `components.ts` is static markup plus a custom element for behaviour, the way Blume's own `Frame` is built (`Carousel`/`CarouselImage`: `src` takes a URL, `/file.png`, or the page-relative `../../public/file.png`) |
| `apps/docs/` | Blume site (`blume.config.ts`, `content/`, `components/` + `components.ts`, `islands/`, `public/`). `content/(0N-<phase>)/<topic>/<num>-<slug>.mdx` plus one `meta.ts` per phase and per topic folder, all generated by sync: the phase folder is a `(group)` (sidebar section, no route segment), the topic folder is the README campaign topic slug (`tree-dfs/`, `heap-priority-queue/`) and a real route segment (`/tree-dfs/invert-binary-tree`), the `meta.ts` files carry the exact labels, phase/topic numbers and icons so the sidebar reads in curriculum order with each topic a collapsible group. A problem page's badge row (above the title) is rendered, not written: the `PageHeader` slot (`components/ProblemBadges.astro`, wired in `components.ts`) reads the page's `topics:` (LeetCode's topic tags, from sync) and `video:` (NeetCode YouTube id, from `bun run videos`) frontmatter — both declared via `frontmatter.extend` in `blume.config.ts`, hand-rolled Standard Schemas, no zod — and links a tag the campaign teaches to its topic landing page (accent), any other tag to its guide page `/guides/<slug>` (neutral; LeetCode's problem list if the guide is gone), and the video as a red `Explanation` badge. Which tags the campaign teaches is ONE map, `apps/docs/topic-landings.ts` (tag slug → landing routes), read by both that component and sync. `content/guides/<slug>.mdx` (the `Guides` header tab) is one page per tag outside that map: sync creates a placeholder when the file is missing or empty and never touches it again (hand-owned; a guide whose tag no page carries any more is left alone), with frontmatter `list:` — the tag's LeetCode problem list, rendered by the same slot as an orange `LeetCode list` badge; sync also writes `guides/meta.ts`. A hand-written `(0N-<phase>)/<topic>/index.mdx` (`hidden: true`) is that topic's landing page: Blume ≥ 1.7.3 links the sidebar group row to it (`sliding-window/index.mdx` → `/sliding-window`) and the hidden flag drops the duplicate row. Two kinds of custom MDX component, both import-free on every page: `islands/<Name>.tsx` is hydrated React (auto-discovered; `ProgressDashboard`, `DependencySpine` — the README topic map as data, zoom/pan, nodes link to whatever route Blume's `/api/docs/navigation.json` reports for the topic folder, inert until a topic has pages), `components/<Name>.astro` registered in `components.ts` is static markup plus a custom element for behaviour, the way Blume's own `Frame` is built (`Carousel`/`CarouselImage`: `src` takes a URL, `/file.png`, or the page-relative `../../public/file.png`) |
| `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) |
| `.githooks/` | Tracked git hooks; `bun install` points `core.hooksPath` here (root `prepare` script, no husky). `pre-commit` runs `bun run sync` and stages only the pages sync changed, so a solution and its page land in one commit; it refuses to run while `work/1` has anything unstaged (sync reads the tree, so the page would not match the commit), and leaves pre-existing dirt under `apps/docs/content` (in-progress `## Approach` prose) unstaged. `git commit --no-verify` for a deliberate partial commit |
| `.github/workflows/` | deploy (docs → Pages with `GITHUB_TOKEN` for the releases-backed changelog, Worker → Cloudflare, on every main push), close-solved (issues + `/admin/solved` + Project `Set`/`Difficulty`, needs `SRS_ADMIN_KEY` and `PROJECT_PAT`; then the `release` job: timever tag + release per finished topic, `contents: write`), close-topics, sync-d1 (issue edits → `/admin/reconcile`), spaced-repetition (daily 06:00 ET cron → today's review issue; `GITHUB_TOKEN` only, no secrets) |
| `.githooks/` | Tracked git hooks; `bun install` points `core.hooksPath` here (root `prepare` script, no husky). `pre-commit` runs `bun run sync` and stages only what sync changed (pages, `apps/docs/redirects.ts`), so a solution, its page and its `/<num>` short link land in one commit; it refuses to run while `work/1` has anything unstaged (sync reads the tree, so the page would not match the commit), and leaves pre-existing dirt under `apps/docs/content` (in-progress `## Approach` prose) unstaged. `git commit --no-verify` for a deliberate partial commit |
## Development Commands
@@ -67,13 +69,15 @@ bun run test # run ONE solution against LeetCode's judge (not a test
# most recently tested first, stamped in the db on exit 0)
bun run submit # submit ONE solution to LeetCode (newest scaffold first, nothing
# hidden; last_submitted stamped in the db on exit 0)
bun run sync # work/1 → apps/docs/content/ pages (re-solves are never published)
bun run videos # docs pages with no <YouTube> embed → NeetCode video per page
# (channel search scraped, no API key; --write appends the
# `## Explanation` embed; "no NeetCode match" pages stay untouched)
bun run sync # work/1 → apps/docs/content/ pages + apps/docs/redirects.ts (/<num> short
# links; re-solves are never published)
bun run videos # docs pages with no `video:` frontmatter → NeetCode video per page
# (channel search scraped, no API key; --write adds the
# `video:` key; "no NeetCode match" pages stay untouched)
bun run dev|build # Blume docs site (runs in apps/docs/)
bun run close-solved -- --dry-run # issue reconcilers (also DRY_RUN=1)
bun run close-topics -- --dry-run
bun run release-topics -- --dry-run # prints every would-be topic release body
# close-solved also pushes the solved set to the Worker; point it at a local
# one and nothing production is touched:
SRS_API=http://localhost:8787 SRS_ADMIN_KEY=$LINK_KEY bun run close-solved -- --dry-run
@@ -106,11 +110,11 @@ curl -X POST -H "Authorization: Bearer $LINK_KEY" \
- `apps/api/src/index.ts` — router + cron dispatch; `apps/api/wrangler.jsonc` — bindings (`DB`, `EMAIL`), crons, vars; secrets `GH_PAT`/`WEBHOOK_SECRET`/`LINK_KEY` via `wrangler secret put`.
- `apps/api/src/digest.ts` / `apps/api/src/email.tsx` — the daily digest, split data/presentation. `digest.ts` reads D1 into a `DigestData`; `email.tsx` owns every colour and every sentence, and renders both the HTML and (via its own `plainDigest`, not React Email's `plainText` mode, which flattens the tables) the text alternative. The retrieval rules hold by construction: `DigestRow` has no title and no issue field, so a review or drill line *cannot* leak the topic or a solution link. Subject is `(Day N/56) LeetCode Daily Digest`.
- `apps/api/src/png.ts` — hand-rolled PNG encoder (RGB8, one IDAT, zlib via `CompressionStream("deflate")`, CRC32, 5×7 bitmap font). It exists because every major email client refuses remote SVG, so `/chart/heatmap.png` rasters the heatmap for the digest while `/chart/heatmap.svg` keeps serving the README byte-for-byte. Both come from one `heatmapCells()` so the two pictures cannot drift.
- `apps/cli/sync.ts` — work→docs contract: on existing pages only the `<Badge>` line and the `## Solution` section are script-owned, the latter bounded by `solutionEnd()` at the next unfenced top-level heading; human prose before it and any sections after it are never touched. Never hand-write solution pages or edit inside `## Solution`. Two classifications, two homes: the sidebar group is the README topic (`readmeTopics()`, first listing wins for a problem under two topics; a warm-up outside the plan groups under its category at `order: 0`, ahead of topic 1) and the badge is the `work/1/<Difficulty>/<Category>/` category — topic folders and their `meta.ts` are rewritten every run, a page is `rename`d when its topic changes, and a folder every page has left is deleted unless it still holds a hand-written `index.mdx` (sync never writes, moves, or deletes one). Never use `(group)` folders here: they add no route segment, so a topic `index.mdx` inside one collides with the root `/`. The two hand-owned sections around the solution have their own owners: `## Approach` (a Mermaid flowchart tracing the page's code, placed *before* `## Solution` so a re-sync keeps it) is written per the `problem-approach` skill (`/problem-approach` fills every page missing one); `## Explanation` (the NeetCode `<YouTube>` embed, after `## Solution`) is appended by `bun run videos --write` (`apps/cli/videos.ts`).
- `apps/cli/sync.ts` — work→docs contract: on existing pages only the frontmatter `icon:` and the `## Solution` section are regenerated, and `topics:` (LeetCode's `topicTags` as `{ name, slug }`, fetched by GraphQL) is filled once — on create, or on any run that finds it missing — never refreshed (delete the key to refetch; offline, the page is written without it and the next sync retries). `## Solution` is the latter bounded by `solutionEnd()` at the next unfenced top-level heading; human prose before it and any sections after it are never touched. Never hand-write solution pages or edit inside `## Solution`. Two classifications, two homes: the sidebar group is the README topic (`readmeTopics()`, first listing wins for a problem under two topics; a warm-up outside the plan groups under its category at `order: 0`, ahead of topic 1) and the `work/1/<Difficulty>/<Category>/` category, which only places an unplanned problem — the page's topic badges are LeetCode's tags, not either classification. Topics nest under their README phase (`## Phase N — Name` headings, shortened via `PHASE_ALIASES`): `content/(0N-<phase>)/<topic>/`, both `meta.ts` script-owned and rewritten every run (phase: `Phase N: Name`, `order: N`; topic: label, curriculum order, `TOPIC_ICONS` icon, `display: "group"`). The phase folder is a Blume `(group)` — no route segment, so `/tree-dfs` is unchanged and a landing page there is one level deep, never at `/` — and the topic folder is the real segment; never put a topic `index.mdx` directly inside a `(group)`. A page is `rename`d when its topic or phase changes; a folder every page has left is deleted (climbing into an emptied phase folder) unless it still holds a hand-written `index.mdx` (sync never writes, moves, or deletes one — so page-relative asset paths in a moved landing page are yours to fix). Slugs come from `slugify()`, which glues a leading number to the next token (`1d-dynamic-programming`) because Blume strips `<digits>-` prefixes from routes. The hand-owned parts have their own owners: `## Approach` (a Mermaid flowchart tracing the page's code, placed *before* `## Solution` so a re-sync keeps it) is written per the `problem-approach` skill (`/problem-approach` fills every page missing one); the frontmatter `video:` (the NeetCode walkthrough, rendered as the red `Explanation` badge) is added by `bun run videos --write` (`apps/cli/videos.ts`).
- `apps/cli/work.ts` — the `work/` layout: bucket paths, `isSolution()`/`solvedInBucket()` (the stub test, shared with `close-solved.ts`), `firstSolved()` (one `git log`, ET dates, keyed by LC number so past layout moves don't matter), and `reviewQueues()` (what each window still owes). Everything that needs to know where a solution file lives, or when it was solved, goes through here.
- `apps/api/src/stats.ts` / `apps/cli/stats.ts` / `apps/cli/tui.ts` / `apps/cli/api.ts` — the read side. `buildStats()` is the ONE aggregation: phases, ladder, difficulty, per-topic temperature, gates, the 14-day due queue, and `heat` (every campaign day, zeros and future days included — the docs page windows the tail off it rather than asking a second question). Two readers consume it, the docs island and `bun run stats`, so its shape is both their contract: change all three together. `tui.ts` is hand-rolled ANSI (bars, sparklines, heat cells, alternate screen) on charts.ts's palette, and every chart differs by GLYPH as well as by colour so a piped or `NO_COLOR` dashboard still reads. The TUI recomputes nothing: the only local fact it shows is the `work/` solution count, which is inventory, not schedule.
- `README.md` live charts are Worker endpoints (`/chart/*.svg`, `/badge/gate.svg`, 5-min Camo cache); the docs `/progress` page fetches `/api/stats` client-side (`apps/docs/islands/ProgressDashboard.tsx`).
- `apps/docs/blume.config.ts` — site base `/leetcode`; `README.md` campaign table doubles as topic-map input to `sync.ts` (`readmeTopics()`, `TOPIC_ALIASES`).
- `apps/docs/blume.config.ts` — site base `/leetcode`; imports `apps/docs/redirects.ts`, sync-generated (`/<num>` → `/<topic>/<slug>`, one row per page, exact root-relative paths — Blume adds the base and, for the static Pages build, emits a meta-refresh page per rule plus `blume-redirects.json`); `README.md` campaign table doubles as topic-map input to `sync.ts` (`readmeTopics()`, `TOPIC_ALIASES`, `PHASE_ALIASES`).
- Old `.github/srs/` JSON state, its `srs-*` Actions, and the one-shot `import-srs.ts` importer are RETIRED and deleted — do not resurrect; git history is the record.
## Runtime/Tooling Preferences
+1 -1
View File
@@ -5,7 +5,7 @@
<p align="center">
<a href="https://x.com/basedprad"><img alt="follow" src="https://shieldcn.dev/x/follow/basedprad.svg" /></a>
<a href="https://leetcode.com/progress"><img alt="Custom badge" src="https://shieldcn.dev/badge/view-@prdlk.svg?logo=leetcode" /></a>
<a href="https://prdlk.github.io/leetcode"><img alt="badge" src="https://shieldcn.dev/badge/Read-Documentation.svg?logo=lu%3ABook" /></a>
<a href="https://lc.prd.lk"><img alt="badge" src="https://shieldcn.dev/badge/Read-Documentation.svg?logo=lu%3ABook" /></a>
</p>
<p align="center">
+2 -2
View File
@@ -32,8 +32,8 @@
"REPO": "prdlk/leetcode",
"FROM_EMAIL": "srs@prdlk.com",
"TO_EMAIL": "prnk28@gmail.com",
"DOCS_URL": "https://prdlk.github.io/leetcode",
"DOCS_ORIGIN": "https://prdlk.github.io,https://lc.prad.nu",
"DOCS_URL": "https://lc.prd.lk",
"DOCS_ORIGIN": "https://lc.prd.lk,https://prdlk.github.io",
"PUBLIC_URL": "https://srs-api.prdlk.workers.dev"
}
+3 -40
View File
@@ -7,6 +7,7 @@
* `set:optional` and `set:deferred` are extra credit. A topic is finished when
* every one of its core sub-issues is closed; optional/deferred state is
* ignored, matching the day rule in each topic body ("Core problems first").
* The rule lives in topics.ts, shared with release-topics.ts.
*
* Like close-solved.ts this reconciles state instead of reacting to an event
* payload: re-runs are no-ops, backfilling needs no special casing, and only
@@ -19,6 +20,7 @@
*/
import { github } from "./github.ts";
import { markdownTable, printTable, writeStepSummary } from "./report.ts";
import { coreSet, readProblems } from "./topics.ts";
const DRY =
process.argv.includes("--dry-run") ||
@@ -53,52 +55,13 @@ for await (const raw of gh.list(`/repos/${gh.repo}/issues?labels=topic&state=ope
}
topics.sort((a, b) => a.number - b.number);
// ── core sub-issue state per topic ───────────────────────────────
interface Core {
/** Core sub-issues, closed and open alike, lowest number first. */
all: number[];
/** The core sub-issues still open — non-empty means the topic stays open. */
pending: number[];
}
/**
* Read a topic's core sub-issue state. The sub_issues payload carries the full
* issue objects (state + labels), so no per-problem follow-up request is needed.
*/
async function readCore(topic: number): Promise<Core> {
const all: number[] = [];
const pending: number[] = [];
for await (const raw of gh.list(`/repos/${gh.repo}/issues/${topic}/sub_issues`)) {
if (!raw || typeof raw !== "object") continue;
if (!("number" in raw) || typeof raw.number !== "number") continue;
if (!("state" in raw) || typeof raw.state !== "string") continue;
if (!("labels" in raw) || !Array.isArray(raw.labels)) continue;
const core = raw.labels.some(
(label) =>
label &&
typeof label === "object" &&
"name" in label &&
label.name === "set:core",
);
if (!core) continue;
all.push(raw.number);
if (raw.state === "open") pending.push(raw.number);
}
all.sort((a, b) => a - b);
pending.sort((a, b) => a - b);
return { all, pending };
}
// ── reconcile ────────────────────────────────────────────────────
const report: string[][] = [];
let closed = 0;
for (const topic of topics) {
const core = await readCore(topic.number);
const core = coreSet(await readProblems(gh, topic.number));
const progress = `${core.all.length - core.pending.length}/${core.all.length}`;
// A topic with no core sub-issues has nothing to complete: never close it,
+2 -1
View File
@@ -10,7 +10,8 @@
"sync": "bun ./sync.ts",
"videos": "bun ./videos.ts",
"close-solved": "bun ./close-solved.ts",
"close-topics": "bun ./close-topics.ts"
"close-topics": "bun ./close-topics.ts",
"release-topics": "bun ./release-topics.ts"
},
"devDependencies": {
"@clack/prompts": "^1.7.0",
+340
View File
@@ -0,0 +1,340 @@
#!/usr/bin/env bun
/**
* Publish one GitHub release per finished topic, versioned by timever
* (`YYYYMMDD.HHMMSS.NNNNNNNNN`, UTC — https://gist.github.com/twolfson/de1b004dd22536b8e668).
*
* "Finished" is topics.ts's rule — every core sub-issue closed — the same one
* close-topics.ts closes the topic issue on, and the version is the instant
* the LAST core problem closed. Keying off the sub-issues rather than the
* topic issue lets this run as the tail of Close Solved: the push that closes
* a topic's final core problem releases it in the same run, without waiting
* for close-topics to chain after it. The topic issue itself closes seconds
* later, on the same day.
*
* Each release is a changelog snapshot at that instant: the topic's problem
* sub-issues grouped as closed on the finish day (ET), closed earlier, and
* open at release, each with its set, difficulty, and the solution files as
* they stood in the tagged commit — the last main commit before the finish.
*
* The tag is annotated and backdated (tagger date = finish), so
* `git tag --sort=taggerdate` reads as the campaign's history even for tags
* minted by a backfill. The release's own published date cannot be backdated
* (the API has no field for it), so the docs changelog dates each entry from
* this timever tag instead (`releasesDatedByTag` in apps/docs/blume.config.ts)
* — the tag format is that contract.
*
* Reconciles instead of reacting: every finished topic without a release gets
* one, so a first run backfills the whole campaign; an existing release keeps
* its tag and commit and only has its body rewritten when render() would say
* something else. Re-runs with nothing new are no-ops.
* A release is found by the marker comment in its body, never by tag name, so
* reopening and re-closing a core problem can never mint a second release.
* A tag that exists without its release (a run that died between the two) is
* reused, not re-created.
*
* bun apps/cli/release-topics.ts # tag, push, release, fix bodies
* bun apps/cli/release-topics.ts --dry-run # print the would-be releases
*
* Auth: GH_TOKEN / GITHUB_TOKEN, else `gh auth token` (needs contents: write).
* Git: a clone with full history, an `origin` it may push tags to, and a
* committer identity (the tagger).
*/
import { etDate } from "../api/src/srs.ts";
import { github } from "./github.ts";
import { markdownTable, printTable, writeStepSummary } from "./report.ts";
import { coreSet, labelValue, readProblems, type Problem } from "./topics.ts";
import { ROOT, parseWorkPath } from "./work.ts";
const DRY =
process.argv.includes("--dry-run") ||
process.env.DRY_RUN === "1" ||
process.env.DRY_RUN === "true";
/** The docs site (apps/docs/blume.config.ts `deployment.site`); sync publishes `/<num>` short links. */
const DOCS_SITE = "https://lc.prd.lk";
const TIMEVER = "https://gist.github.com/twolfson/de1b004dd22536b8e668";
const gh = await github();
function git(...args: string[]): string {
const run = Bun.spawnSync(["git", ...args], { cwd: ROOT });
if (run.exitCode !== 0) throw new Error(`git ${args.join(" ")}: ${run.stderr.toString().trim()}`);
return run.stdout.toString().trim();
}
// ── topics ───────────────────────────────────────────────────────
interface Topic {
number: number;
title: string;
milestone: string | null;
body: string;
}
/** Narrow one /issues element; the `topic` label filter cannot exclude PRs. */
function readTopic(value: unknown): Topic | undefined {
if (!value || typeof value !== "object") return;
if ("pull_request" in value) return;
if (!("number" in value) || typeof value.number !== "number") return;
if (!("title" in value) || typeof value.title !== "string") return;
const milestone =
"milestone" in value &&
value.milestone &&
typeof value.milestone === "object" &&
"title" in value.milestone &&
typeof value.milestone.title === "string"
? value.milestone.title
: null;
const body = "body" in value && typeof value.body === "string" ? value.body : "";
return { number: value.number, title: value.title, milestone, body };
}
interface Finished {
topic: Topic;
problems: Problem[];
core: number[];
finishedAt: string;
}
// Closed and open topics alike: the rule reads the sub-issues, so a topic
// finished moments ago (close-topics not yet run) releases now.
const finished: Finished[] = [];
for await (const raw of gh.list(`/repos/${gh.repo}/issues?labels=topic&state=all`)) {
const topic = readTopic(raw);
if (!topic) continue;
const problems = await readProblems(gh, topic.number);
const core = coreSet(problems);
if (core.finishedAt) finished.push({ topic, problems, core: core.all, finishedAt: core.finishedAt });
}
// Oldest first, so a backfill creates releases in campaign order.
finished.sort((a, b) => a.finishedAt.localeCompare(b.finishedAt) || a.topic.number - b.topic.number);
// ── existing releases ────────────────────────────────────────────
/** Topic -> its release. Name and body are kept in sync with the render on every run. */
const released = new Map<number, { id: number; tag: string; name: string; body: string }>();
for await (const raw of gh.list(`/repos/${gh.repo}/releases`)) {
if (!raw || typeof raw !== "object") continue;
if (!("id" in raw) || typeof raw.id !== "number") continue;
if (!("tag_name" in raw) || typeof raw.tag_name !== "string") continue;
if (!("body" in raw) || typeof raw.body !== "string") continue;
const name = "name" in raw && typeof raw.name === "string" ? raw.name : "";
const m = raw.body.match(/<!-- release-topics: #(\d+) -->/);
if (m) released.set(Number(m[1]), { id: raw.id, tag: raw.tag_name, name, body: raw.body });
}
// ── timever ──────────────────────────────────────────────────────
/**
* Timever of an instant. The API reports whole seconds, so SUBTIME is the
* tiebreak: two topics finishing in the same second (one push closing both
* last core problems) get consecutive nanoseconds, in (instant, topic) order.
*/
const tags = new Map<number, string>(); // topic -> tag
{
let prev = "";
let ns = 0;
for (const f of finished) {
const iso = new Date(f.finishedAt).toISOString(); // 2026-09-24T18:36:38.000Z
const base = `${iso.slice(0, 10).replaceAll("-", "")}.${iso.slice(11, 19).replaceAll(":", "")}`;
ns = base === prev ? ns + 1 : Number(iso.slice(20, 23)) * 1_000_000;
prev = base;
tags.set(f.topic.number, `${base}.${String(ns).padStart(9, "0")}`);
}
}
// ── release body ─────────────────────────────────────────────────
const ET_WHEN = new Intl.DateTimeFormat("en-US", {
timeZone: "America/New_York",
weekday: "short",
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
});
const ET_DAY = new Intl.DateTimeFormat("en-US", {
timeZone: "America/New_York",
month: "short",
day: "numeric",
});
/** Topic body sections worth carrying into the changelog (skipped while still `TODO`). */
const NOTES = ["Trigger", "Invariant", "Trap"] as const;
function bodyField(body: string, field: string): string | undefined {
return body.match(new RegExp(`^\\*\\*${field}:\\*\\*\\s*(.+)$`, "m"))?.[1]?.trim();
}
function bodySection(body: string, heading: string): string | undefined {
const m = body.match(new RegExp(`^### ${heading}\\n([\\s\\S]*?)(?=^### |(?![\\s\\S]))`, "m"));
const text = m?.[1]?.trim();
return text && text !== "TODO" ? text : undefined;
}
function render(f: Finished, tag: string, sha: string, files: Map<number, string[]>): string {
const { topic, problems } = f;
const finishDay = etDate(new Date(f.finishedAt));
// Snapshot at the finish instant: a problem closed later was open then.
const closedBy = (p: Problem) => p.closedAt !== null && p.closedAt <= f.finishedAt;
const onDay = problems.filter((p) => closedBy(p) && etDate(new Date(p.closedAt!)) === finishDay);
const earlier = problems.filter((p) => closedBy(p) && etDate(new Date(p.closedAt!)) !== finishDay);
const open = problems.filter((p) => !closedBy(p));
const row = (p: Problem): string[] => {
const lc = Number(p.title.match(/^LC (\d+)/)?.[1]);
const code = closedBy(p)
? (files.get(lc) ?? []).map(
(path) => `[${path.slice(path.lastIndexOf(".") + 1)}](https://github.com/${gh.repo}/blob/${tag}/${encodeURI(path)})`,
)
: [];
return [
`#${p.number}`,
p.title,
labelValue(p, "set:") ?? "—",
labelValue(p, "diff:") ?? "—",
closedBy(p) ? ET_DAY.format(new Date(p.closedAt!)) : "—",
code.length ? [...code, `[docs](${DOCS_SITE}/${lc})`].join(" · ") : "—",
];
};
const table = (list: Problem[]) =>
markdownTable([["Issue", "Problem", "Set", "Difficulty", "Closed (ET)", "Solution"], ...list.map(row)]);
// Blume's changelog derives each page's meta description from the opening
// prose, stripping markdown and line breaks without adding a space — so lead
// with one self-contained sentence and keep the fields on one `·` line.
const when = ET_WHEN.format(new Date(f.finishedAt));
const summary =
`Finished ${when} ET: all ${f.core.length} core problems closed, ` +
`${onDay.length} ${onDay.length === 1 ? "problem" : "problems"} closed that day.`;
const fields: string[] = [];
const scheduled = bodyField(topic.body, "Scheduled");
if (scheduled) fields.push(`**Scheduled:** ${scheduled}`);
if (topic.milestone) fields.push(`**Phase:** ${topic.milestone}`);
fields.push(`**Topic:** #${topic.number}`);
const buildsOn = bodyField(topic.body, "Builds on");
if (buildsOn) fields.push(`**Builds on:** ${buildsOn}`);
// The marker is how a later run finds this release; see the file header.
const sections = [`<!-- release-topics: #${topic.number} -->\n${summary}`, fields.join(" · ")];
sections.push(`### Closed ${ET_DAY.format(new Date(f.finishedAt))} — the day the topic finished\n\n${table(onDay)}`);
if (earlier.length) sections.push(`### Closed earlier\n\n${table(earlier)}`);
if (open.length) sections.push(`### Open at release\n\n${table(open)}`);
const notes = NOTES.flatMap((h) => {
const text = bodySection(topic.body, h);
return text ? [`**${h}.** ${text}`] : [];
});
if (notes.length) sections.push(`### Notes\n\n${notes.join("\n\n")}`);
sections.push(
`---\n\n\`${tag}\` is [timever](${TIMEVER}) for the UTC instant the core set completed; ` +
`it tags ${sha.slice(0, 7)}, the last main commit before it.`,
);
return `${sections.join("\n\n")}\n`;
}
// ── reconcile ────────────────────────────────────────────────────
const latest = finished.at(-1)?.topic.number;
const report: string[][] = [];
const previews: string[] = [];
let created = 0;
let updated = 0;
for (const f of finished) {
const { topic } = f;
const finishedEt = etDate(new Date(f.finishedAt));
const existing = released.get(topic.number);
// An existing release keeps its tag and commit; only its name and body are
// reconciled. Its tag may postdate this clone (released by another run), so
// fetch it when missing — read-only, so a dry run does it too.
const tag = existing?.tag ?? tags.get(topic.number)!;
if (existing && Bun.spawnSync(["git", "rev-parse", "-q", "--verify", `refs/tags/${tag}`], { cwd: ROOT }).exitCode !== 0) {
git("fetch", "-q", "origin", `refs/tags/${tag}:refs/tags/${tag}`);
}
const sha = existing
? git("rev-parse", `${tag}^{commit}`)
: git("rev-list", "-1", "--first-parent", `--before=${f.finishedAt}`, "HEAD");
if (!sha) {
report.push([`#${topic.number}`, topic.title, finishedEt, tag, "ERROR: no commit before finish"]);
continue;
}
// LC number -> its first-solve files in the tagged tree. Keyed by file name,
// like firstSolved(): tags before the bucket split see `work/<Difficulty>/…`,
// and the work/3 + work/7 re-solves are not the solve that closed the issue.
const files = new Map<number, string[]>();
for (const path of git("ls-tree", "-r", "--name-only", sha, "--", "work").split("\n")) {
const bucket = parseWorkPath(path.slice("work/".length))?.bucket;
const lc = path.match(/\/(\d+)\.[^/]+\.(?:js|py)$/)?.[1];
if (!lc || (bucket && bucket !== 1)) continue;
files.set(Number(lc), [...(files.get(Number(lc)) ?? []), path]);
}
const body = render(f, tag, sha, files);
const name = topic.title;
if (existing) {
if (existing.body === body && existing.name === name) {
report.push([`#${topic.number}`, topic.title, finishedEt, tag, "released"]);
} else if (DRY) {
previews.push(`## ${name} (${tag})\n\n${body}`);
report.push([`#${topic.number}`, topic.title, finishedEt, tag, "would update"]);
} else {
await gh.api(`/repos/${gh.repo}/releases/${existing.id}`, {
method: "PATCH",
body: JSON.stringify({ name, body }),
});
report.push([`#${topic.number}`, topic.title, finishedEt, tag, "updated"]);
updated++;
}
continue;
}
if (DRY) {
previews.push(`## ${name} (${tag})\n\n${body}`);
report.push([`#${topic.number}`, topic.title, finishedEt, tag, "would release"]);
continue;
}
const remote = git("ls-remote", "--tags", "origin", `refs/tags/${tag}`);
if (!remote) {
const local = Bun.spawnSync(["git", "rev-parse", "-q", "--verify", `refs/tags/${tag}`], { cwd: ROOT });
if (local.exitCode !== 0) {
const tagged = Bun.spawnSync(
["git", "tag", "-a", tag, sha, "-m", `${topic.title} (#${topic.number})\n\nCore set complete: ${f.core.map((n) => `#${n}`).join(", ")}`],
{ cwd: ROOT, env: { ...process.env, GIT_COMMITTER_DATE: f.finishedAt } },
);
if (tagged.exitCode !== 0) throw new Error(`git tag ${tag}: ${tagged.stderr.toString().trim()}`);
}
git("push", "origin", `refs/tags/${tag}`);
}
await gh.api(`/repos/${gh.repo}/releases`, {
method: "POST",
body: JSON.stringify({
tag_name: tag,
name,
body,
make_latest: topic.number === latest ? "true" : "false",
}),
});
report.push([`#${topic.number}`, topic.title, finishedEt, tag, "released now"]);
created++;
}
// ── report ───────────────────────────────────────────────────────
if (previews.length) console.log(`${previews.join("\n")}\n`);
const rows = [["topic", "title", "finished (ET)", "tag", "status"], ...report];
printTable(rows);
const would = (status: string) => report.filter((r) => r[4] === status).length;
console.log(
`\n${finished.length} finished topics · ` +
(DRY
? `would release ${would("would release")}, update ${would("would update")}`
: `released ${created}, updated ${updated}`),
);
await writeStepSummary(
`### release-topics${DRY ? " (dry run)" : ""}\n\n` +
`${finished.length} finished topics, ${released.size} already released.\n\n` +
(report.length ? `${markdownTable(rows)}\n` : "No finished topics.\n") +
(previews.length ? `\n${previews.join("\n")}` : ""),
);
+329 -52
View File
@@ -2,37 +2,60 @@
/**
* Sync work/1 leetcode solutions into apps/docs/content/ pages.
*
* - New problems get a full page (frontmatter, badge, warning, examples,
* - New problems get a full page (frontmatter, warning, examples,
* constraints, solution) in the gold-standard format.
* - Already-ported pages only get their script-owned parts regenerated — the
* `<Badge>` line and the `## Solution` section — so hand-curated prose is
* never touched.
* frontmatter `icon:` (`PAGE_ICON`) and the `## Solution` section — so
* hand-curated prose is never touched.
* - When a problem is solved in both JavaScript and Python the solution
* renders as a <CodeGroup> (Python first); single-language solutions
* render as a plain fence.
* - Every page carries LeetCode's own topic tags as frontmatter `topics:`
* (`{ name, slug }` per tag, LeetCode's order), fetched from the GraphQL
* `topicTags` once — when the page is created, or on any later run that
* finds the key missing — and never refreshed after, so a sync with nothing
* new makes no request. Delete the key to refetch it. The page itself
* renders no badges: the docs `PageHeader` slot
* (`apps/docs/components/ProblemBadges.astro`) turns `topics:` and the
* `video:` key `videos.ts` writes into the badge row above the title.
* - Every tag a page carries that no campaign topic teaches (not a key of
* `apps/docs/topic-landings.ts`) gets a placeholder guide page,
* `guides/<slug>.mdx`, with `list:` pointing at LeetCode's problem list for
* the tag. Created once (a missing or empty file), then hand-owned; the tag's
* badge on a problem page links to it.
*
* Two classifications, two homes. The sidebar group is the campaign TOPIC
* from the README table (`tree-dfs/`, ordered by topic number via a
* script-owned `meta.ts`); the page badge is the data-structure CATEGORY the
* file sits under in `work/1/<Difficulty>/<Category>/`. A problem outside the
* plan (warm-ups) has no topic, so it groups under its category instead.
* The sidebar group is the campaign TOPIC from the README table (`tree-dfs/`,
* ordered by topic number via a script-owned `meta.ts`). A problem outside the
* plan (warm-ups) has no topic, so it groups under the data-structure CATEGORY
* its file sits under in `work/1/<Difficulty>/<Category>/` instead.
*
* Topic folders are real route segments (`/tree-dfs/invert-binary-tree`), not
* `(group)` folders, so a hand-written `index.mdx` in one is the topic's
* landing page at `/tree-dfs` and Blume links the sidebar group row to it.
* Sync never writes or moves an `index.mdx`, and never removes a folder that
* still holds one.
* Topics nest under their README PHASE: `(01-linear-structures)/tree-dfs/`.
* The phase folder is a Blume `(group)` — a sidebar section with its own
* script-owned `meta.ts`, adding no route segment — while the topic folder is
* a real segment (`/tree-dfs/invert-binary-tree`), so a hand-written
* `index.mdx` in it is the topic's landing page at `/tree-dfs` and Blume links
* the sidebar group row to it. Sync never writes or moves an `index.mdx`, and
* never removes a folder that still holds one. An unplanned category group has
* no phase and sits at the content root.
*
* Only the first-solve bucket is read: work/3 and work/7 hold blind re-solves
* of problems whose page already exists (see work.ts), and a fresh scaffold
* there would otherwise wipe the page's `## Solution`.
*
* Every page also gets a short link: `/<num>` → `/<topic>/<slug>` (so
* `/leetcode/217` opens Contains Duplicate). The rules live in the generated
* `apps/docs/redirects.ts`, which `blume.config.ts` imports — Blume takes exact
* root-relative paths and prepends the deployment base itself.
*/
import { mkdir, readdir, rename, rmdir, unlink } from "node:fs/promises";
import { basename, dirname, join } from "node:path";
import { TOPIC_LANDINGS } from "../docs/topic-landings.ts";
const ROOT = join(import.meta.dir, "..", "..");
const WORK = join(ROOT, "work", "1");
const DOCS = join(ROOT, "apps", "docs", "content");
const REDIRECTS = join(ROOT, "apps", "docs", "redirects.ts");
type Lang = "js" | "py";
@@ -171,24 +194,86 @@ function parseHeader(header: string): Header {
// ── README topic map ─────────────────────────────────────────────
/** A campaign phase: `## Phase N — Name · Weeks …` in the README. */
interface Phase {
order: number;
name: string;
}
/** A campaign topic: `<strong>NN. Name</strong>` in the README table. */
interface Topic {
order?: number; // README topic number; absent for an unplanned problem's category group
name: string;
phase?: Phase; // absent for an unplanned category group
}
/** README phase names → the short labels the overview page uses. */
const PHASE_ALIASES: Record<string, string> = {
"Nodal & Grid Structures": "Nodal & Grid",
"Hierarchical Structures": "Hierarchical",
"Relational Structures": "Relational",
"Decision-Space Exploration": "Decision Space",
};
const ROMAN = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"];
const TOPIC_ALIASES: Record<string, string> = {
"Hash-Based Lookup": "Hash Table",
};
/**
* Sidebar icon per group, keyed by the (post-alias) topic name — the same key an
* unplanned problem's category group uses, so `Stack` the category and `Stack`
* the topic share one row. Lucide names; Blume resolves them at build time and
* reports a diagnostic for a typo, but an unmapped group is caught here first.
*/
const TOPIC_ICONS: Record<string, string> = {
// README topics, in curriculum order
"Hash Table": "hash",
"Prefix Sum": "sigma",
"Two Pointers": "arrow-left-right",
"Sliding Window": "app-window",
"Binary Search": "search",
Stack: "layers",
"Monotonic Stack": "trending-up",
"Linked List": "link",
"Hybrid Structures": "combine",
"Matrix Index Math": "grid-3x3",
"Tree DFS": "folder-tree",
"Tree BFS": "network",
"Binary Search Tree": "git-fork",
"Heap / Priority Queue": "triangle",
Trie: "spell-check",
"Graph DFS": "waypoints",
"Graph BFS": "radar",
"Topological Sort": "workflow",
"Union-Find": "merge",
Backtracking: "undo-2",
"1-D Dynamic Programming": "columns-3",
"Multi-D / Grid DP": "table",
Greedy: "coins",
Intervals: "calendar-range",
// work/1 categories that can surface as an unplanned group
Array: "brackets",
String: "type",
Tree: "network",
"Depth-First Search": "folder-tree",
};
/** LC number -> topic. A problem listed under two topics keeps the first. */
async function readmeTopics(): Promise<Map<number, Topic>> {
const md = await Bun.file(join(ROOT, "README.md")).text();
const map = new Map<number, Topic>();
let phase: Phase | undefined;
let topic: Topic | null = null;
for (const line of md.split("\n")) {
const p = line.match(/^## Phase ([IVX]+) — (.+?) · /);
if (p) {
const name = p[2]!.replaceAll("&amp;", "&");
phase = { order: ROMAN.indexOf(p[1]!) + 1, name: PHASE_ALIASES[name] ?? name };
}
const t = line.match(/<strong>(\d+)\.\s*([^<]+)<\/strong>/);
if (t) topic = { order: Number(t[1]), name: TOPIC_ALIASES[t[2]!.trim()] ?? t[2]!.trim() };
if (t) topic = { order: Number(t[1]), name: TOPIC_ALIASES[t[2]!.trim()] ?? t[2]!.trim(), phase };
if (!topic) continue;
const n = line.match(/<td align="center">(\d+)<\/td>/);
if (n && !map.has(Number(n[1]))) map.set(Number(n[1]), topic);
@@ -197,14 +282,131 @@ async function readmeTopics(): Promise<Map<number, Topic>> {
}
/**
* Blume folder meta: exact topic label (the folder slug loses `/` and case) and
* curriculum order. An unplanned category group (warm-ups) has no README
* number and sorts at 0, ahead of topic 1, so the sidebar opens on the basics.
* Folder slug for a topic or phase name. Blume reads a leading `<digits>-` on a
* folder as a sidebar-order prefix and strips it from the route (`1-d-…` →
* `/d-…`), so a slug that would start that way keeps the digits glued to the
* next token (`1d-dynamic-programming`).
*/
function slugify(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "")
.replace(/^(\d+)-/, "$1");
}
/**
* Content folder for a topic, relative to `DOCS`: `(0N-<phase>)/<topic>` for a
* planned topic (the phase is a `(group)`, so the route is still `/<topic>`),
* bare `<topic>` for an unplanned category group.
*/
function topicDir(topic: Topic): string {
const slug = slugify(topic.name);
return topic.phase ? `${phaseDir(topic.phase)}/${slug}` : slug;
}
function phaseDir(phase: Phase): string {
return `(${String(phase.order).padStart(2, "0")}-${slugify(phase.name)})`;
}
/** Blume `(group)` meta for a phase: the overview page's heading, in phase order. */
function phaseMeta(phase: Phase): string {
return `import { defineMeta } from "blume";\n\nexport default defineMeta({ title: ${JSON.stringify(`Phase ${phase.order}: ${phase.name}`)}, order: ${phase.order} });\n`;
}
/**
* Blume folder meta for a topic: exact label (the folder slug loses `/` and
* case), curriculum order, the sidebar icon, and `display: "group"` so the
* topic collapses under its phase heading. An unplanned category group
* (warm-ups) has no README number and sorts at 0, ahead of phase 1, so the
* sidebar opens on the basics.
*/
function groupMeta(topic: Topic): string {
return `import { defineMeta } from "blume";\n\nexport default defineMeta({ title: ${JSON.stringify(topic.name)}, order: ${topic.order ?? 0} });\n`;
const icon = TOPIC_ICONS[topic.name];
if (!icon) console.warn(`no icon for group ${JSON.stringify(topic.name)}: add it to TOPIC_ICONS`);
const fields = [`title: ${JSON.stringify(topic.name)}`, `order: ${topic.order ?? 0}`];
if (icon) fields.push(`icon: ${JSON.stringify(icon)}`);
fields.push(`display: "group"`);
return `import { defineMeta } from "blume";\n\nexport default defineMeta({ ${fields.join(", ")} });\n`;
}
// ── LeetCode topic tags ──────────────────────────────────────────
/** One of LeetCode's topic tags. `slug` is not derivable from `name` (`Graph Theory` → `graph`). */
interface TopicTag {
name: string;
slug: string;
}
/**
* The problem's topic tags, in LeetCode's order — or undefined, warned, when
* LeetCode cannot answer (offline commit, unknown slug): the page is written
* without `topics:` and the next sync fills it in.
*/
async function fetchTopicTags(sol: Solution): Promise<TopicTag[] | undefined> {
try {
const res = await fetch("https://leetcode.com/graphql", {
method: "POST",
headers: {
"content-type": "application/json",
"user-agent": "Mozilla/5.0",
referer: `https://leetcode.com/problems/${sol.slug}/`,
},
body: JSON.stringify({
query: "query topicTags($titleSlug: String!) { question(titleSlug: $titleSlug) { topicTags { name slug } } }",
variables: { titleSlug: sol.slug },
}),
});
if (!res.ok) throw new Error(`POST /graphql -> ${res.status} ${res.statusText}`);
const data = (await res.json()) as { data?: { question?: { topicTags?: TopicTag[] } | null } };
const tags = data.data?.question?.topicTags;
if (!Array.isArray(tags)) throw new Error("POST /graphql -> no such question");
return tags.map(({ name, slug }) => ({ name, slug }));
} catch (err) {
console.warn(`no topics for ${sol.num}.${sol.slug}: ${err instanceof Error ? err.message : err}`);
return undefined;
}
}
/** The frontmatter `topics:` block, one flow mapping per tag (JSON strings are valid YAML). */
function topicsYaml(tags: TopicTag[]): string {
if (tags.length === 0) return "topics: []";
return ["topics:", ...tags.map((t) => ` - { name: ${JSON.stringify(t.name)}, slug: ${JSON.stringify(t.slug)} }`)].join("\n");
}
/** The frontmatter's `topics:` as sync wrote them (or a human edited them); [] when absent. */
function frontmatterTopics(page: string): TopicTag[] {
const fm = page.match(/^---\n([\s\S]*?)\n---/)?.[1];
const data: unknown = fm ? Bun.YAML.parse(fm) : undefined;
if (!data || typeof data !== "object" || !("topics" in data) || !Array.isArray(data.topics)) return [];
return data.topics.filter((t): t is TopicTag => typeof t?.name === "string" && typeof t?.slug === "string");
}
// ── guides ───────────────────────────────────────────────────────
/**
* Placeholder for a tag no campaign topic teaches: the `/guides/<slug>` page a
* problem's topic badge links to. `list:` is the tag's LeetCode problem list,
* rendered as a badge by the docs `PageHeader` slot. Written once — the page
* is hand-owned from then on.
*/
function guidePage(tag: TopicTag): string {
return [
"---",
`title: ${JSON.stringify(tag.name)}`,
`list: ${JSON.stringify(`https://leetcode.com/problem-list/${tag.slug}/`)}`,
"---",
"",
":::note",
`This guide has not been written yet. The LeetCode list above has every problem tagged ${tag.name}.`,
":::",
"",
].join("\n");
}
/** Folder meta for `guides/`: its own header tab (`navigation.tabs` in blume.config.ts), so only the label. */
const GUIDES_META = `import { defineMeta } from "blume";\n\nexport default defineMeta({ title: "Guides" });\n`;
// ── inline-code formatting heuristics ────────────────────────────
/** Identifiers worth backticking in prose, harvested from the code. */
@@ -306,11 +508,10 @@ function warningText(h: Header): string {
);
}
function badgeLine(sol: Solution): string {
return `<Badge variant="accent">${sol.category}</Badge>`;
}
/** Sidebar/page icon for every problem page (Lucide, resolved by Blume). */
const PAGE_ICON = "dot";
function renderPage(sol: Solution): string {
function renderPage(sol: Solution, tags: TopicTag[] | undefined): string {
const h = sol.header;
const ids = identifiers(sol);
const description = h.statement[0]?.replace(/\.\s*$/, "") ?? "";
@@ -320,13 +521,13 @@ function renderPage(sol: Solution): string {
"---",
`title: '${h.num}. ${h.title.replaceAll("'", "''")}'`,
/[:#]/.test(description) ? `description: '${description.replaceAll("'", "''")}'` : `description: ${description}`,
`icon: ${PAGE_ICON}`,
...(tags ? [topicsYaml(tags)] : []),
"sidebar:",
` label: '${h.title.replaceAll("'", "''")}'`,
` badge: '${h.difficulty}'`,
"---",
"",
badgeLine(sol),
"",
);
const warning = warningText(h);
@@ -383,6 +584,11 @@ for await (const rel of new Bun.Glob("**/*.{js,py}").scan(WORK)) {
}
const topics = await readmeTopics();
// An unplanned problem whose work/ category is also a README topic name (a
// `Hash Table` warm-up) files under that topic, in its phase, not a second
// root-level `hash-table/`.
const topicByName = new Map<string, Topic>();
for (const topic of topics.values()) topicByName.set(topic.name, topic);
/** Insert a number-free `sidebar.label` into existing frontmatter when missing. */
function ensureLabel(page: string, h: Header): string {
@@ -392,10 +598,30 @@ function ensureLabel(page: string, h: Header): string {
return page.replace(/^sidebar:$/m, `sidebar:\n${label}`);
}
/** Set the frontmatter's top-level `icon:` to `PAGE_ICON`, inserting it before `sidebar:`. */
function ensureIcon(page: string): string {
const fm = page.match(/^---\n[\s\S]*?\n---/);
if (!fm) return page;
const line = `icon: ${PAGE_ICON}`;
const next = /^icon: .*$/m.test(fm[0])
? fm[0].replace(/^icon: .*$/m, line)
: fm[0].replace(/^sidebar:$/m, `${line}\nsidebar:`);
return next + page.slice(fm[0].length);
}
/** Insert `topics:` before `sidebar:` when the frontmatter lacks it; an existing list is kept as is. */
async function ensureTopics(page: string, sol: Solution): Promise<string> {
const fm = page.match(/^---\n[\s\S]*?\n---/);
if (!fm || /^topics:/m.test(fm[0])) return page;
const tags = await fetchTopicTags(sol);
if (!tags) return page;
return fm[0].replace(/^sidebar:$/m, `${topicsYaml(tags)}\nsidebar:`) + page.slice(fm[0].length);
}
/**
* Offset just past the script-owned `## Solution` section: the next top-level
* heading that is not inside a code fence, or end of file. Sections a human
* appends after the solution (`## Explanation`, …) survive regeneration.
* appends after the solution survive regeneration.
*/
function solutionEnd(page: string, from: number): number {
const lines = page.slice(from).split("\n");
@@ -420,22 +646,25 @@ for (const rel of (await Array.fromAsync(new Bun.Glob("**/*.mdx").scan(DOCS))).s
const report: [string, string, string][] = [];
const ordered = [...solutions.values()].sort((a, b) => a.num - b.num);
const groups = new Map<string, Topic>(); // folder -> topic, for meta.ts
const groups = new Map<string, Topic>(); // topic folder -> topic, for meta.ts
const phases = new Map<string, Phase>(); // phase folder -> phase, for meta.ts
const vacated = new Set<string>(); // folders a page moved out of; removed below if empty
const redirects: string[] = []; // `{ from: "/<num>", to: "/<topic>/<slug>" }` rows, numeric order
const tags = new Map<string, TopicTag>(); // every tag slug any problem page carries -> tag
for (const sol of ordered) {
const langs = Object.keys(sol.code).sort().join("+");
const workRef = `${sol.num}.${sol.slug} (${langs})`;
// <topic>/ folder (a real route segment, so its index.mdx is the topic landing
// page) + leetcode-number prefix: numeric sidebar order, prefix stripped from the URL.
// An unplanned problem's category may share a folder with a numbered topic
// (`Hash Table` warm-ups under topic 1); the numbered one owns the meta.
const topic = topics.get(sol.num) ?? { name: sol.category };
const group = topic.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
if (groups.get(group)?.order === undefined) groups.set(group, topic);
const name = `${group}/${sol.num}-${sol.slug}.mdx`;
// (0N-<phase>)/<topic>/ folder (the topic is a real route segment, so its
// index.mdx is the landing page) + leetcode-number prefix: numeric sidebar
// order, prefix stripped from the URL.
const topic = topics.get(sol.num) ?? topicByName.get(sol.category) ?? { name: sol.category };
const dir = topicDir(topic);
groups.set(dir, topic);
if (topic.phase) phases.set(phaseDir(topic.phase), topic.phase);
const name = `${dir}/${sol.num}-${sol.slug}.mdx`;
const existing = pages.get(sol.num);
await mkdir(join(DOCS, group), { recursive: true });
await mkdir(join(DOCS, dir), { recursive: true });
if (existing) {
const page = await Bun.file(join(DOCS, existing)).text();
@@ -446,7 +675,7 @@ for (const sol of ordered) {
}
const tail = page.slice(solutionEnd(page, at));
const body = page.slice(0, at) + solutionSection(sol) + (tail ? `\n${tail}` : "");
const next = ensureLabel(body, sol.header).replace(/^<Badge variant="accent">.*<\/Badge>$/m, badgeLine(sol));
const next = await ensureTopics(ensureIcon(ensureLabel(body, sol.header)), sol);
const actions: string[] = [];
if (existing !== name) {
await rename(join(DOCS, existing), join(DOCS, name));
@@ -458,29 +687,77 @@ for (const sol of ordered) {
actions.push("updated");
}
report.push([workRef, name, actions.join(" + ") || "skipped (in sync)"]);
for (const tag of frontmatterTopics(next)) tags.set(tag.slug, tag);
} else {
await Bun.write(join(DOCS, name), renderPage(sol));
const page = renderPage(sol, await fetchTopicTags(sol));
await Bun.write(join(DOCS, name), page);
report.push([workRef, name, "created"]);
for (const tag of frontmatterTopics(page)) tags.set(tag.slug, tag);
}
// The route drops the phase `(group)` and the page's number prefix.
redirects.push(` { from: "/${sol.num}", to: ${JSON.stringify(`/${slugify(topic.name)}/${sol.slug}`)} },`);
}
// Script-owned short links, one per page; imported by blume.config.ts.
{
const module = [
"// Generated by `bun run sync` (apps/cli/sync.ts) — do not edit.",
"// `/leetcode/<num>` → that problem's page; exact paths, base added by Blume.",
"export default [",
...redirects,
"];",
"",
].join("\n");
if (!(await Bun.file(REDIRECTS).exists()) || (await Bun.file(REDIRECTS).text()) !== module) {
await Bun.write(REDIRECTS, module);
report.push([`${redirects.length} redirects`, "apps/docs/redirects.ts", "written"]);
}
}
// Script-owned folder meta: one per live group, rewritten only when it differs.
for (const [group, topic] of groups) {
const path = join(DOCS, group, "meta.ts");
const meta = groupMeta(topic);
if ((await Bun.file(path).exists()) && (await Bun.file(path).text()) === meta) continue;
await Bun.write(path, meta);
report.push([`topic ${topic.order ?? "-"}`, `${group}/meta.ts`, "written"]);
// One guide page per tag that no campaign topic teaches, created when missing
// (an empty file counts as missing) and never rewritten. A guide whose tag no
// page carries any more is left alone: it is hand-owned.
const guideTags = [...tags.values()]
.filter((tag) => !TOPIC_LANDINGS[tag.slug])
.sort((a, b) => a.slug.localeCompare(b.slug));
for (const tag of guideTags) {
const file = Bun.file(join(DOCS, "guides", `${tag.slug}.mdx`));
if ((await file.exists()) && file.size > 0) continue;
await Bun.write(file, guidePage(tag));
report.push([`tag ${tag.slug}`, `guides/${tag.slug}.mdx`, "created"]);
}
// A group every page left behind is dead: drop its meta and the folder itself.
for (const dir of vacated) {
if (groups.has(dir)) continue;
const left = await readdir(join(DOCS, dir));
if (left.some((f) => f !== "meta.ts")) continue;
if (left.length) await unlink(join(DOCS, dir, "meta.ts"));
await rmdir(join(DOCS, dir));
report.push(["", dir, "removed (empty)"]);
// Script-owned folder metas — one per live phase and topic folder, plus
// guides/ — rewritten only when they differ.
const metas: [label: string, dir: string, meta: string][] = [
...[...phases].map(([dir, phase]): [string, string, string] => [`phase ${phase.order}`, dir, phaseMeta(phase)]),
...[...groups].map(([dir, topic]): [string, string, string] => [`topic ${topic.order ?? "-"}`, dir, groupMeta(topic)]),
...(guideTags.length ? [["guides", "guides", GUIDES_META] as [string, string, string]] : []),
];
for (const [label, dir, meta] of metas) {
const path = join(DOCS, dir, "meta.ts");
if ((await Bun.file(path).exists()) && (await Bun.file(path).text()) === meta) continue;
await Bun.write(path, meta);
report.push([label, `${dir}/meta.ts`, "written"]);
}
// A folder every page left behind is dead: drop its meta and the folder itself,
// then the phase folder above it if that emptied too. A folder that still holds
// anything else (a hand-written index.mdx, another topic) stops the climb.
for (const start of vacated) {
for (let dir = start; dir !== "."; dir = dirname(dir)) {
if (groups.has(dir) || phases.has(dir)) break;
let left: string[];
try {
left = await readdir(join(DOCS, dir));
} catch {
break; // already removed via a sibling's climb
}
if (left.some((f) => f !== "meta.ts")) break;
if (left.length) await unlink(join(DOCS, dir, "meta.ts"));
await rmdir(join(DOCS, dir));
report.push(["", dir, "removed (empty)"]);
}
}
const w0 = Math.max(...report.map((r) => r[0].length), 9);
+75
View File
@@ -0,0 +1,75 @@
/**
* Topic issues and their problem sub-issues: the ONE reading of "is this topic
* finished", shared by close-topics.ts (closes the topic issue) and
* release-topics.ts (publishes the topic's timever release) so the two can
* never disagree about when a topic was done.
*
* Rule: a topic is finished when it has a core set (`set:core` sub-issues) and
* every one of them is closed. Optional and deferred problems are extra credit
* and never hold a topic open. A topic with no core sub-issues is never
* finished — that would be indistinguishable from a mis-labelled problem set.
*
* Library: side-effect-free on import; every call takes the caller's github().
*/
import type { GitHub } from "./github.ts";
/** One problem sub-issue, narrowed from the sub_issues payload. */
export interface Problem {
number: number;
/** `LC <num> · <Name>`. */
title: string;
/** ISO instant the issue closed; null while it is open. */
closedAt: string | null;
labels: string[];
}
export interface Core {
/** Core sub-issues, closed and open alike, lowest number first. */
all: number[];
/** The core sub-issues still open — non-empty means the topic stays open. */
pending: number[];
/** When the last core sub-issue closed; null until the topic is finished. */
finishedAt: string | null;
}
/**
* Every sub-issue of a topic. The sub_issues payload carries the full issue
* objects (state + labels), so no per-problem follow-up request is needed.
*/
export async function readProblems(gh: GitHub, topic: number): Promise<Problem[]> {
const problems: Problem[] = [];
for await (const raw of gh.list(`/repos/${gh.repo}/issues/${topic}/sub_issues`)) {
if (!raw || typeof raw !== "object") continue;
if (!("number" in raw) || typeof raw.number !== "number") continue;
if (!("title" in raw) || typeof raw.title !== "string") continue;
if (!("state" in raw) || typeof raw.state !== "string") continue;
if (!("labels" in raw) || !Array.isArray(raw.labels)) continue;
const closedAt =
raw.state === "closed" && "closed_at" in raw && typeof raw.closed_at === "string"
? raw.closed_at
: null;
const labels = raw.labels.flatMap((label: unknown) =>
label && typeof label === "object" && "name" in label && typeof label.name === "string"
? [label.name]
: [],
);
problems.push({ number: raw.number, title: raw.title, closedAt, labels });
}
return problems.sort((a, b) => a.number - b.number);
}
/** The value of a `prefix:value` label (`set:` → `core`), if the problem has one. */
export function labelValue(problem: Problem, prefix: string): string | undefined {
return problem.labels.find((l) => l.startsWith(prefix))?.slice(prefix.length);
}
export function coreSet(problems: Problem[]): Core {
const core = problems.filter((p) => labelValue(p, "set:") === "core");
const pending = core.filter((p) => !p.closedAt).map((p) => p.number);
// ISO instants from the API share one format, so string order is time order.
const finishedAt =
core.length && !pending.length
? core.map((p) => p.closedAt!).reduce((a, b) => (a > b ? a : b))
: null;
return { all: core.map((p) => p.number), pending, finishedAt };
}
+22 -24
View File
@@ -2,24 +2,23 @@
/**
* Find the NeetCode explanation video for every docs page that has none.
*
* A finished solution page ends with `## Explanation` holding one
* `<YouTube id="…" title="…" />` — NeetCode's walkthrough of the problem.
* `sync.ts` never writes that section (it owns only `## Solution`), so pages
* accumulate without one. This script lists every numbered page lacking a
* `<YouTube` embed and looks each up on NeetCode's two channels, main channel
* first, accepting only a title that names the page's exact LC number
* (`… - Leetcode 226 - …`) and is not a #short. No YouTube API key: the
* channel search page embeds its results as `ytInitialData` JSON, which is
* all we parse. Ranking is the channel's own — the first exact-number match
* wins.
* A finished solution page carries frontmatter `video: <YouTube id>` —
* NeetCode's walkthrough of the problem — which the docs `PageHeader` slot
* (`apps/docs/components/ProblemBadges.astro`) renders as the red
* "Explanation" badge beside the topic badges. `sync.ts` never writes that key
* (it owns `icon:`, `topics:` and `## Solution`), so pages accumulate without
* one. This script lists every numbered page lacking `video:` and looks each
* up on NeetCode's two channels, main channel first, accepting only a title
* that names the page's exact LC number (`… - Leetcode 226 - …`) and is not a
* #short. No YouTube API key: the channel search page embeds its results as
* `ytInitialData` JSON, which is all we parse. Ranking is the channel's own —
* the first exact-number match wins.
*
* bun apps/cli/videos.ts # report: page → video (or "no match")
* bun apps/cli/videos.ts --write # also append `## Explanation` + embed
* bun apps/cli/videos.ts --write # also add `video:` to the frontmatter
*
* --write appends at end of file (below `## Solution`, where sync.ts leaves
* human sections alone) and adds the `## Explanation` heading only when the
* page lacks it. A page with no match is left untouched, so re-runs are
* no-ops once every page has its embed.
* --write inserts the key before `sidebar:`. A page with no match is left
* untouched, so re-runs are no-ops once every page has its video.
*/
import { join } from "node:path";
@@ -126,23 +125,21 @@ interface Page {
text: string;
}
/** Numbered solution pages without a `<YouTube` embed, by LC number. */
/** The page's frontmatter block, `---` to `---`. */
const FRONTMATTER = /^---\n[\s\S]*?\n---/;
/** Numbered solution pages without a `video:` key, by LC number. */
async function pagesMissingVideo(): Promise<Page[]> {
const pages: Page[] = [];
for (const rel of await Array.fromAsync(new Bun.Glob("**/*.mdx").scan(DOCS))) {
const text = await Bun.file(join(DOCS, rel)).text();
const t = text.match(/^title: '(\d+)\./m);
if (t && !text.includes("<YouTube")) pages.push({ num: Number(t[1]), rel, text });
const fm = text.match(FRONTMATTER)?.[0] ?? "";
if (t && !/^video:/m.test(fm)) pages.push({ num: Number(t[1]), rel, text });
}
return pages.sort((a, b) => a.num - b.num);
}
function withEmbed(page: Page, video: Video): string {
const embed = `<YouTube id="${video.id}" title="${video.title.replaceAll('"', "'")}" />`;
const heading = /^## Explanation\s*$/m.test(page.text) ? "" : "## Explanation\n\n";
return `${page.text.replace(/\s*$/, "\n")}\n${heading}${embed}\n`;
}
// ── main ─────────────────────────────────────────────────────────
const missing = await pagesMissingVideo();
@@ -162,7 +159,8 @@ for (const page of missing) {
found++;
let status = "";
if (WRITE) {
await Bun.write(join(DOCS, page.rel), withEmbed(page, video));
const next = page.text.replace(FRONTMATTER, (fm) => fm.replace(/^sidebar:$/m, `video: ${JSON.stringify(video.id)}\nsidebar:`));
await Bun.write(join(DOCS, page.rel), next);
status = "written";
}
rows.push([page.rel, `https://youtu.be/${video.id}`, `${video.title} (${video.channel})`, status]);
+159 -9
View File
@@ -1,13 +1,163 @@
import { defineConfig } from "blume";
import type { StandardSchema } from "blume/core/standard-schema.ts";
import { custom, filesystem } from "blume/sources";
import { githubReleasesSource } from "blume/sources/github-releases.ts";
import redirects from "./redirects";
export default defineConfig({
title: "Leetcode",
description: "Documentation for LeetCode solutions",
deployment: {
site: "https://prdlk.github.io",
base: "/leetcode",
},
content: {
root: "content",
/**
* GitHub releases → /changelog, dated by the topic's real finish instant.
*
* Blume dates a release entry by `published_at`, which the GitHub API cannot
* backdate — so the topic releases backfilled on one day (apps/cli/
* release-topics.ts) would all read that day. Their tags are timever
* (`YYYYMMDD.HHMMSS.NNNNNNNNN`, UTC) of the moment the topic finished, so this
* fetch rewrites each timever release's `published_at` to that instant before
* Blume maps it; anything else passes through untouched. The config-level
* `githubReleases()` accepts no `fetchImpl`, hence the `custom()` wrap of the
* underlying source (the `blume/sources/*.ts` path the Blume docs use for
* custom sources), with the context the built-in adapter would receive.
*/
const TIMEVER = /^(\d{4})(\d{2})(\d{2})\.(\d{2})(\d{2})(\d{2})\.\d{9}$/;
const releasesDatedByTag: typeof fetch = async (input, init) => {
const res = await fetch(input, init);
if (!res.ok) return res;
const releases: unknown = await res.json();
if (Array.isArray(releases)) {
for (const release of releases) {
const m =
typeof release?.tag_name === "string"
? release.tag_name.match(TIMEVER)
: null;
if (m)
release.published_at = `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}Z`;
}
}
// Blume reads only `ok` and the JSON body, so the original headers (whose
// content-length/encoding describe the upstream bytes) are not carried over.
return Response.json(releases, { status: res.status });
};
// Blume's defaults (`inter`, `ibm-plex-mono`) are fetched from fonts.gstatic.com
// on every build, which has timed out on the Pages runner and, fully offline,
// silently ships no fonts at all. The same two families from Fontsource, as
// local files, keep the build network-free. Blume resolves `src` from this
// directory; the workspace links these packages under ./node_modules.
const inter = {
name: "Inter",
variants: [
{
src: "./node_modules/@fontsource-variable/inter/files/inter-latin-wght-normal.woff2",
weight: "100..900",
},
],
};
const plexMono = {
name: "IBM Plex Mono",
variants: [
{
src: "./node_modules/@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-400-normal.woff2",
weight: 400,
},
{
src: "./node_modules/@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-500-normal.woff2",
weight: 500,
},
],
};
/**
* Custom frontmatter, rendered as the badge row by
* components/ProblemBadges.astro. Problem pages: `topics:` is LeetCode's topic
* tags (written by apps/cli/sync.ts), `video:` the NeetCode walkthrough's
* YouTube id (apps/cli/videos.ts). Guide pages (content/guides/, created by
* sync): `list:` is the tag's LeetCode problem list. Blume rejects undeclared
* keys and takes any Standard Schema; these are hand-rolled so the docs
* workspace needs no zod. All optional — every other page carries none.
*/
const optional = <T>(
message: string,
valid: (value: unknown) => value is T,
): StandardSchema<unknown, T | undefined> => ({
"~standard": {
version: 1,
vendor: "prdlk",
validate: (value) =>
value === undefined || valid(value)
? { value }
: { issues: [{ message }] },
},
});
const topicsSchema = optional(
"expected a list of { name, slug } LeetCode topic tags",
(value): value is { name: string; slug: string }[] =>
Array.isArray(value) &&
value.every(
(t) => typeof t?.name === "string" && typeof t?.slug === "string",
),
);
const videoSchema = optional(
"expected an 11-character YouTube video id",
(value): value is string =>
typeof value === "string" && /^[\w-]{11}$/.test(value),
);
const listSchema = optional(
"expected a https://leetcode.com/problem-list/<slug>/ URL",
(value): value is string =>
typeof value === "string" &&
/^https:\/\/leetcode\.com\/problem-list\/[\w-]+\/$/.test(value),
);
export default defineConfig({
title: "prdlk",
description: "Documentation for my LeetCode solutions",
logo: "./public/logo.svg",
deployment: {
site: "https://lc.prd.lk",
},
github: {
owner: "prdlk",
repo: "leetcode",
},
lastModified: "git",
redirects,
frontmatter: {
extend: { topics: topicsSchema, video: videoSchema, list: listSchema },
},
content: {
sources: [
filesystem({ root: "content" }),
custom(
githubReleasesSource(
{
name: "changelog",
prefix: "changelog",
owner: "prdlk",
repo: "leetcode",
fetchImpl: releasesDatedByTag,
},
// What resolve.ts hands the built-in adapter: `.blume/cache/<name>/`,
// and a refresh on every load (the source reads nothing else).
{
projectRoot: import.meta.dirname,
cacheDir: `${import.meta.dirname}/.blume/cache/changelog`,
mode: "build",
},
),
),
],
},
theme: {
fonts: { display: inter, body: inter, mono: plexMono },
},
navigation: {
sidebar: {
display: "flat", // "flat" | "group" | "page"
},
tabs: [
{ label: "Guides", path: "/guides" },
{ label: "Changelog", path: "/changelog" },
],
},
});
+7 -3
View File
@@ -1,7 +1,7 @@
/**
* Project MDX components, available on every page with no import. Static
* `.astro` files ship no framework JS (a custom element carries any
* behaviour); interactive React lives in `islands/` instead.
* Project MDX components, available on every page with no import, and layout
* slot overrides. Static `.astro` files ship no framework JS (a custom element
* carries any behaviour); interactive React lives in `islands/` instead.
*/
import { defineComponents } from "blume";
@@ -10,4 +10,8 @@ export default defineComponents({
Carousel: "./components/Carousel.astro",
CarouselImage: "./components/CarouselImage.astro",
},
layout: {
// Problem pages' topic + explanation badges, from `topics:` / `video:` frontmatter.
PageHeader: "./components/ProblemBadges.astro",
},
});
+92
View File
@@ -0,0 +1,92 @@
---
/**
* The `PageHeader` slot: the badge row above a page's title. Blume hands the
* slot only the route, so the page's frontmatter is looked up through the
* route manifest — the same `getEntry` call Blume's own page template makes.
* Pages with none of the keys below (landing pages, the changelog) render
* nothing.
*
* - `topics:` (problem pages, written by `apps/cli/sync.ts`) is LeetCode's own
* topic tags. A tag the campaign teaches (`topic-landings.ts`) links to that
* topic's landing page and reads accent; any other tag links to its guide
* page, `/guides/<slug>` (sync creates one per such tag), and reads neutral.
* - `video:` (problem pages, written by `apps/cli/videos.ts`) is the NeetCode
* walkthrough's YouTube id, the red "Explanation" badge at the end of the row.
* - `list:` (guide pages, written by sync on create) is the tag's LeetCode
* problem list, an orange "LeetCode list" badge.
*
* A tag naming two campaign topics (`Depth-First Search` is both Tree DFS and
* Graph DFS) links to the page's own topic — its route's first segment — when
* that is one of them; otherwise DFS/BFS prefer the tree topic for a problem
* LeetCode also tags `Tree` and the graph topic for the rest. A route the site
* does not serve (a landing or guide page removed) falls back to LeetCode's
* problem list for the tag, so a badge never links to a 404.
*/
import { getEntry } from "astro:content";
import data from "blume:data";
import Badge from "blume/components/content/Badge.astro";
import { withBase } from "blume/components/islands/base-path.ts";
import { TOPIC_LANDINGS } from "../topic-landings.ts";
interface Props {
route: string;
}
interface TopicTag {
name: string;
slug: string;
}
const { route } = Astro.props;
const at = data.routes.find((r: { path: string }) => r.path === route);
const entry = at ? await getEntry(at.collection, at.entryId) : undefined;
const tags: TopicTag[] = entry?.data.topics ?? [];
const video: string | undefined = entry?.data.video;
const list: string | undefined = entry?.data.list;
const served = new Set(data.routes.map((r: { path: string }) => r.path));
const own = `/${route.split("/")[1]}`;
const prefer = tags.some((t) => t.slug === "tree") ? "/tree-" : "/graph-";
const badges = tags.map((tag) => {
const landings = (TOPIC_LANDINGS[tag.slug] ?? []).filter((path) => served.has(path));
const landing = landings.includes(own) ? own : (landings.find((p) => p.startsWith(prefer)) ?? landings[0]);
if (landing) return { name: tag.name, href: withBase(landing), accent: true, external: false };
const guide = `/guides/${tag.slug}`;
if (served.has(guide)) return { name: tag.name, href: withBase(guide), accent: false, external: false };
return { name: tag.name, href: `https://leetcode.com/problem-list/${tag.slug}/`, accent: false, external: true };
});
const linkClass = "no-underline transition-opacity hover:opacity-80";
---
{
(badges.length > 0 || video || list) && (
<div class="not-prose mx-auto mb-4 flex max-w-content flex-wrap gap-1.5">
{badges.map((b) => (
<a
href={b.href}
target={b.external ? "_blank" : undefined}
rel={b.external ? "noopener" : undefined}
class={linkClass}
>
<Badge variant={b.accent ? "accent" : "default"}>{b.name}</Badge>
</a>
))}
{video && (
<a href={`https://www.youtube.com/watch?v=${video}`} target="_blank" rel="noopener" class={linkClass}>
<Badge variant="danger" icon="youtube">
Explanation
</Badge>
</a>
)}
{list && (
<a href={list} target="_blank" rel="noopener" class={linkClass}>
<Badge variant="warning" icon="list">
LeetCode list
</Badge>
</a>
)}
</div>
)
}
@@ -1,13 +1,16 @@
---
title: '153. Find Minimum in Rotated Sorted Array'
description: 'Suppose an array of length n sorted in ascending order is rotated between 1 and n times. For example, the array nums = [0,1,2,4,5,6,7] might become:'
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Binary Search", slug: "binary-search" }
video: "nIVW4P8b1VA"
sidebar:
label: 'Find Minimum in Rotated Sorted Array'
badge: 'Medium'
---
<Badge variant="accent">Array</Badge>
::::warning
Notice that rotating an array [a[0], a[1], a[2], ..., a[n-1]] 1 time results in the array [a[n-1], a[0], a[1], a[2], ..., a[n-2]].
::::
@@ -68,7 +71,3 @@ class Solution:
return nums[lowest_index]
```
## Explanation
<YouTube id="nIVW4P8b1VA" title="Find Minimum in Rotated Sorted Array - Binary Search - Leetcode 153 - Python" />
@@ -1,13 +1,16 @@
---
title: '33. Search in Rotated Sorted Array'
description: There is an integer array nums sorted in ascending order (with distinct values)
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Binary Search", slug: "binary-search" }
video: "U8XENwh8Oy8"
sidebar:
label: 'Search in Rotated Sorted Array'
badge: 'Medium'
---
<Badge variant="accent">Array</Badge>
::::warning
You must write an algorithm with O(log n) runtime complexity.
::::
@@ -92,7 +95,3 @@ var search = function(nums, target) {
return -1;
};
```
## Explanation
<YouTube id="U8XENwh8Oy8" title="Search in rotated sorted array - Leetcode 33 - Python" />
@@ -1,13 +1,16 @@
---
title: '35. Search Insert Position'
description: Given a sorted array of distinct integers and a target value, return the index if the target is found. If not, return the index where it would be if it were inserted in order
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Binary Search", slug: "binary-search" }
video: "K-RYzDZkzCI"
sidebar:
label: 'Search Insert Position'
badge: 'Easy'
---
<Badge variant="accent">Array</Badge>
::::warning
You must write an algorithm with O(log n) runtime complexity.
::::
@@ -65,7 +68,3 @@ class Solution:
return (l + r) // 2 + 1
```
## Explanation
<YouTube id="K-RYzDZkzCI" title="Search Insert Position - Binary Search - Leetcode 35 - Python" />
@@ -1,13 +1,16 @@
---
title: '704. Binary Search'
description: Given an array of integers nums which is sorted in ascending order, and an integer target, write a function to search target in nums. If target exists, then return its index. Otherwise, return -1
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Binary Search", slug: "binary-search" }
video: "s4DPM8ct1pI"
sidebar:
label: 'Binary Search'
badge: 'Easy'
---
<Badge variant="accent">Array</Badge>
::::warning
You must write an algorithm with O(log n) runtime complexity.
::::
@@ -70,7 +73,3 @@ var search = function(nums, target) {
return -1;
};
```
## Explanation
<YouTube id="s4DPM8ct1pI" title="Binary Search - Leetcode 704 - Python" />
@@ -1,13 +1,17 @@
---
title: '74. Search a 2D Matrix'
description: 'You are given an m x n integer matrix matrix with the following two properties:'
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Binary Search", slug: "binary-search" }
- { name: "Matrix", slug: "matrix" }
video: "Ber2pi2C0j0"
sidebar:
label: 'Search a 2D Matrix'
badge: 'Medium'
---
<Badge variant="accent">Array</Badge>
::::warning
You must write a solution in O(log(m * n)) time complexity.
::::
@@ -85,7 +89,3 @@ class Solution:
return True
return False
```
## Explanation
<YouTube id="Ber2pi2C0j0" title="Search a 2D Matrix - Leetcode 74 - Python" />
@@ -1,13 +1,16 @@
---
title: '875. Koko Eating Bananas'
description: Koko loves to eat bananas. There are n piles of bananas, the i^th pile has piles[i] bananas. The guards have gone and will come back in h hours
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Binary Search", slug: "binary-search" }
video: "U2SozAs9RzA"
sidebar:
label: 'Koko Eating Bananas'
badge: 'Medium'
---
<Badge variant="accent">Array</Badge>
### Example 1:
- Input: `piles = [3,6,7,11], h = 8`
- Output: `4`
@@ -76,7 +79,3 @@ var minEatingSpeed = function(piles, h) {
return l;
};
```
## Explanation
<YouTube id="U2SozAs9RzA" title="Koko Eating Bananas - Binary Search - Leetcode 875 - Python" />
@@ -0,0 +1,5 @@
---
title: Binary Search
description: 'Half of a sorted range is removed at each step until the target is found.'
hidden: true
---
@@ -0,0 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Binary Search", order: 5, icon: "search", display: "group" });
@@ -1,13 +1,16 @@
---
title: '1. Two Sum'
description: You are given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Hash Table", slug: "hash-table" }
video: "KLlXCFG5TnA"
sidebar:
label: 'Two Sum'
badge: 'Easy'
---
<Badge variant="accent">Array</Badge>
::::warning
Can you come up with an algorithm that is less than O(n^2) time complexity?
::::
@@ -59,7 +62,3 @@ class Solution:
seen[num] = i
return []
```
## Explanation
<YouTube id="KLlXCFG5TnA" title="Two Sum - Leetcode 1 - HashMap - Python" />
@@ -1,13 +1,15 @@
---
title: '1832. Check if the Sentence Is Pangram'
description: A pangram is a sentence where every letter of the English alphabet appears at least once
icon: dot
topics:
- { name: "Hash Table", slug: "hash-table" }
- { name: "String", slug: "string" }
sidebar:
label: 'Check if the Sentence Is Pangram'
badge: 'Easy'
---
<Badge variant="accent">Hash Table</Badge>
### Example 1:
- Input: `sentence = "thequickbrownfoxjumpsoverthelazydog"`
- Output: `true`
@@ -1,13 +1,17 @@
---
title: '217. Contains Duplicate'
description: 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
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Hash Table", slug: "hash-table" }
- { name: "Sorting", slug: "sorting" }
video: "3OamzN90kPg"
sidebar:
label: 'Contains Duplicate'
badge: 'Easy'
---
<Badge variant="accent">Array</Badge>
### Example 1:
- Input: `nums = [1,2,3,1]`
- Output: `true`
@@ -42,32 +46,8 @@ flowchart TD
## Solution
<CodeGroup>
```py Python
```py
class Solution:
def containsDuplicate(self, nums: List[int]) -> bool:
return len(set(nums)) != len(nums)
```
```js JavaScript
/**
* @param {number[]} nums
* @return {boolean}
*/
var containsDuplicate = function(nums) {
const freq = {};
for (let n of nums) {
freq[n] = (freq[n] || 0) + 1;
if (freq[n] >= 2) {
return true;
}
}
return false;
};
```
</CodeGroup>
## Explanation
<YouTube id="3OamzN90kPg" title="Contains Duplicate - Leetcode 217 - Python" />
@@ -1,13 +1,17 @@
---
title: '242. Valid Anagram'
description: Given two strings s and t, return true if t is an anagram of s, and false otherwise
icon: dot
topics:
- { name: "Hash Table", slug: "hash-table" }
- { name: "String", slug: "string" }
- { name: "Sorting", slug: "sorting" }
video: "9UtInBqnCgA"
sidebar:
label: 'Valid Anagram'
badge: 'Easy'
---
<Badge variant="accent">Hash Table</Badge>
::::warning
What if the inputs contain Unicode characters? How would you adapt your solution to such a case?
::::
@@ -59,7 +63,3 @@ var isAnagram = function(s, t) {
return true;
};
```
## Explanation
<YouTube id="9UtInBqnCgA" title="Valid Anagram - Leetcode 242 - Python" />
@@ -1,13 +1,22 @@
---
title: '347. Top K Frequent Elements'
description: Given an integer array nums and an integer k, return the k most frequent elements. You may return the answer in any order
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Hash Table", slug: "hash-table" }
- { name: "Divide and Conquer", slug: "divide-and-conquer" }
- { name: "Sorting", slug: "sorting" }
- { name: "Heap (Priority Queue)", slug: "heap-priority-queue" }
- { name: "Bucket Sort", slug: "bucket-sort" }
- { name: "Counting", slug: "counting" }
- { name: "Quickselect", slug: "quickselect" }
video: "YPTqKIgVk-k"
sidebar:
label: 'Top K Frequent Elements'
badge: 'Medium'
---
<Badge variant="accent">Array</Badge>
::::warning
Your algorithm's time complexity must be better than O(n log n), where n is the array's size.
::::
@@ -59,7 +68,3 @@ var topKFrequent = function (nums, k) {
.slice(0, k);
};
```
## Explanation
<YouTube id="YPTqKIgVk-k" title="Top K Frequent Elements - Bucket Sort - Leetcode 347 - Python" />
@@ -1,13 +1,18 @@
---
title: '387. First Unique Character in a String'
description: Given a string s, find the first non-repeating character in it and return its index. If it does not exist, return -1
icon: dot
topics:
- { name: "Hash Table", slug: "hash-table" }
- { name: "String", slug: "string" }
- { name: "Queue", slug: "queue" }
- { name: "Counting", slug: "counting" }
video: "rBENYgWy3xU"
sidebar:
label: 'First Unique Character in a String'
badge: 'Easy'
---
<Badge variant="accent">Hash Table</Badge>
### Example 1:
- Input: `s = "leetcode"`
- Output: `0`
@@ -57,7 +62,3 @@ var firstUniqChar = function (s) {
return -1;
};
```
## Explanation
<YouTube id="rBENYgWy3xU" title="First Unique Character in a String - Leetcode 387 - Python" />
@@ -1,13 +1,20 @@
---
title: '451. Sort Characters By Frequency'
description: Given a string s, sort it in decreasing order based on the frequency of the characters. The frequency of a character is the number of times it appears in the string
icon: dot
topics:
- { name: "Hash Table", slug: "hash-table" }
- { name: "String", slug: "string" }
- { name: "Sorting", slug: "sorting" }
- { name: "Heap (Priority Queue)", slug: "heap-priority-queue" }
- { name: "Bucket Sort", slug: "bucket-sort" }
- { name: "Counting", slug: "counting" }
video: "OXdXc9HTrIg"
sidebar:
label: 'Sort Characters By Frequency'
badge: 'Medium'
---
<Badge variant="accent">Hash Table</Badge>
### Example 1:
- Input: `s = "tree"`
- Output: `"eert"`
@@ -59,7 +66,3 @@ var frequencySort = function (s) {
.join("");
};
```
## Explanation
<YouTube id="OXdXc9HTrIg" title="Sort Characters By Frequency - Leetcode 451 - Python" />
@@ -1,13 +1,18 @@
---
title: '49. Group Anagrams'
description: Given an array of strings strs, group the anagrams together. You can return the answer in any order
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Hash Table", slug: "hash-table" }
- { name: "String", slug: "string" }
- { name: "Sorting", slug: "sorting" }
video: "vzdNOK2oB2E"
sidebar:
label: 'Group Anagrams'
badge: 'Medium'
---
<Badge variant="accent">Array</Badge>
### Example 1:
- Input: `strs = ["eat","tea","tan","ate","nat","bat"]`
- Output: `[["bat"],["nat","tan"],["ate","eat","tea"]]`
@@ -64,7 +69,3 @@ var groupAnagrams = function(strs) {
return Object.values(anagrams);
};
```
## Explanation
<YouTube id="vzdNOK2oB2E" title="Group Anagrams - Categorize Strings by Count - Leetcode 49" />
@@ -0,0 +1,5 @@
---
title: Hash Table
description: 'A key maps to a value in constant time, which removes the need for a full scan.'
hidden: true
---
@@ -0,0 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Hash Table", order: 1, icon: "hash", display: "group" });
@@ -0,0 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Phase 1: Linear Structures", order: 1 });
@@ -1,13 +1,17 @@
---
title: '739. Daily Temperatures'
description: Given an array of integers temperatures represents the daily temperatures, return an array answer such that answer[i] is the number of days you have to wait after the i^th day to get a warmer temperature. If there is no future day for which this is possible, keep answer[i] == 0 instead
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Stack", slug: "stack" }
- { name: "Monotonic Stack", slug: "monotonic-stack" }
video: "cTBiBSnjO3c"
sidebar:
label: 'Daily Temperatures'
badge: 'Medium'
---
<Badge variant="accent">Array</Badge>
### Example 1:
- Input: `temperatures = [73,74,75,71,69,72,76,73]`
- Output: `[1,1,4,2,1,1,0,0]`
@@ -53,7 +57,3 @@ class Solution:
stack.append(i)
return ans
```
## Explanation
<YouTube id="cTBiBSnjO3c" title="Daily Temperatures - Monotonic Stack - Leetcode 739 - Python" />
@@ -1,13 +1,18 @@
---
title: '853. Car Fleet'
description: There are n cars at given miles away from the starting mile 0, traveling to reach the mile target
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Stack", slug: "stack" }
- { name: "Sorting", slug: "sorting" }
- { name: "Monotonic Stack", slug: "monotonic-stack" }
video: "Pr6T-3yB9RM"
sidebar:
label: 'Car Fleet'
badge: 'Medium'
---
<Badge variant="accent">Array</Badge>
### Example 1:
- Input: `target = 12, position = [10,8,0,5,3], speed = [2,4,1,1,3]`
- Output: `3`
@@ -61,7 +66,3 @@ class Solution:
return len(stack)
```
## Explanation
<YouTube id="Pr6T-3yB9RM" title="Car Fleet - Leetcode 853 - Python" />
@@ -0,0 +1,5 @@
---
title: Monotonic Stack
description: 'A stack keeps its items in sorted order, which finds the next larger or smaller item in one pass.'
hidden: true
---
@@ -0,0 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Monotonic Stack", order: 7, icon: "trending-up", display: "group" });
@@ -1,13 +1,16 @@
---
title: '238. Product of Array Except Self'
description: Given an integer array nums, return an array answer such that answer[i] is equal to the product of all the elements of nums except nums[i]
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Prefix Sum", slug: "prefix-sum" }
video: "bNvIQI2wAjk"
sidebar:
label: 'Product of Array Except Self'
badge: 'Medium'
---
<Badge variant="accent">Array</Badge>
::::warning
You must write an algorithm that runs in O(n) time and without using the division operation.
Can you solve the problem in O(1) extra space complexity? (The output array does not count as extra space for space complexity analysis.)
@@ -62,7 +65,3 @@ var productExceptSelf = function(nums) {
return answer;
};
```
## Explanation
<YouTube id="bNvIQI2wAjk" title="Product of Array Except Self - Leetcode 238 - Python" />
@@ -1,13 +1,17 @@
---
title: '303. Range Sum Query - Immutable'
description: Given an integer array nums, handle multiple queries of the following type
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Design", slug: "design" }
- { name: "Prefix Sum", slug: "prefix-sum" }
video: "2pndAmo_sMA"
sidebar:
label: 'Range Sum Query - Immutable'
badge: 'Easy'
---
<Badge variant="accent">Array</Badge>
### Example 1:
- Input: `["NumArray", "sumRange", "sumRange", "sumRange"]` `[[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]`
- Output: `[null, 1, -1, -3]`
@@ -65,7 +69,3 @@ class NumArray {
* var param_1 = obj.sumRange(left,right)
*/
```
## Explanation
<YouTube id="2pndAmo_sMA" title="Range Sum Query Immutable - Leetcode 303 - Python" />
@@ -1,13 +1,17 @@
---
title: '560. Subarray Sum Equals K'
description: Given an array of integers nums and an integer k, return the total number of subarrays whose sum equals to k
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Hash Table", slug: "hash-table" }
- { name: "Prefix Sum", slug: "prefix-sum" }
video: "fFVZt-6sgyo"
sidebar:
label: 'Subarray Sum Equals K'
badge: 'Medium'
---
<Badge variant="accent">Array</Badge>
### Example 1:
- Input: `nums = [1,1,1], k = 2`
- Output: `2`
@@ -64,7 +68,3 @@ var subarraySum = function(nums, k) {
return count
};
```
## Explanation
<YouTube id="fFVZt-6sgyo" title="Subarray Sum Equals K - Prefix Sums - Leetcode 560 - Python" />
@@ -0,0 +1,5 @@
---
title: Prefix Sum
description: 'A running total is stored at each index, so the sum of any range is one subtraction.'
hidden: true
---
@@ -0,0 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Prefix Sum", order: 2, icon: "sigma", display: "group" });
@@ -1,13 +1,16 @@
---
title: '121. Best Time to Buy and Sell Stock'
description: You are given an array prices where prices[i] is the price of a given stock on the i^th day
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Dynamic Programming", slug: "dynamic-programming" }
video: "1pkOgXD63yU"
sidebar:
label: 'Best Time to Buy and Sell Stock'
badge: 'Easy'
---
<Badge variant="accent">Array</Badge>
### Example 1:
- Input: `prices = [7,1,5,3,6,4]`
- Output: `5`
@@ -54,7 +57,3 @@ class Solution:
ans = max(ans, curr)
return ans
```
## Explanation
<YouTube id="1pkOgXD63yU" title="Sliding Window: Best Time to Buy and Sell Stock - Leetcode 121 - Python" />
@@ -1,13 +1,17 @@
---
title: '3. Longest Substring Without Repeating Characters'
description: Given a string s, find the length of the longest substring without duplicate characters
icon: dot
topics:
- { name: "Hash Table", slug: "hash-table" }
- { name: "String", slug: "string" }
- { name: "Sliding Window", slug: "sliding-window" }
video: "wiGpQwVHdE0"
sidebar:
label: 'Longest Substring Without Repeating Characters'
badge: 'Medium'
---
<Badge variant="accent">Hash Table</Badge>
### Example 1:
- Input: `s = "abcabcbb"`
- Output: `3`
@@ -60,7 +64,3 @@ class Solution:
return ans
```
## Explanation
<YouTube id="wiGpQwVHdE0" title="Longest Substring Without Repeating Characters - Leetcode 3 - Python" />
@@ -1,13 +1,17 @@
---
title: '424. Longest Repeating Character Replacement'
description: You are given a string s and an integer k. You can choose any character of the string and change it to any other uppercase English character. You can perform this operation at most k times
icon: dot
topics:
- { name: "Hash Table", slug: "hash-table" }
- { name: "String", slug: "string" }
- { name: "Sliding Window", slug: "sliding-window" }
video: "gqXU1UyA8pk"
sidebar:
label: 'Longest Repeating Character Replacement'
badge: 'Medium'
---
<Badge variant="accent">Hash Table</Badge>
### Example 1:
- Input: `s = "ABAB", k = 2`
- Output: `4`
@@ -60,7 +64,3 @@ class Solution:
res = max(res, r - l + 1)
return res
```
## Explanation
<YouTube id="gqXU1UyA8pk" title="Longest Repeating Character Replacement - Leetcode 424 - Python" />
@@ -1,13 +1,18 @@
---
title: '567. Permutation in String'
description: Given two strings s1 and s2, return true if s2 contains a permutation of s1, or false otherwise
icon: dot
topics:
- { name: "Hash Table", slug: "hash-table" }
- { name: "Two Pointers", slug: "two-pointers" }
- { name: "String", slug: "string" }
- { name: "Sliding Window", slug: "sliding-window" }
video: "UbyhOgBN834"
sidebar:
label: 'Permutation in String'
badge: 'Medium'
---
<Badge variant="accent">Hash Table</Badge>
### Example 1:
- Input: `s1 = "ab", s2 = "eidbaooo"`
- Output: `true`
@@ -67,7 +72,3 @@ class Solution:
return False
```
## Explanation
<YouTube id="UbyhOgBN834" title="Permutation in String - Leetcode 567 - Python" />
@@ -55,7 +55,7 @@ Without an invariant, shrinking the window becomes arbitrary. You no longer know
The window size never changes. The main task is updating the window state efficiently when one element enters and one leaves.
:::
![Sliding Window Fixed Size](../../public/sliding-window-fixed-size.svg)
![Sliding Window Fixed Size](../../../public/sliding-window-fixed-size.svg)
### 2. Flexible-size windows with a validity rule
@@ -63,7 +63,7 @@ The window size never changes. The main task is updating the window state effici
Here the window expands until invalid, then shrinks until valid again.
:::
![Flexible-size windows](../../public/sliding-window-flexible.svg)
![Flexible-size windows](../../../public/sliding-window-flexible.svg)
### 3. Frequency-tracking windows
@@ -72,7 +72,7 @@ Here the window expands until invalid, then shrinks until valid again.
The difficulty comes from keeping the bookkeeping correct as characters enter and leave.
:::
![Frequency-tracking windows](../../public/sliding-window-frequency.svg)
![Frequency-tracking windows](../../../public/sliding-window-frequency.svg)
## How Sliding Window Differs From Other Two-Pointer Families
@@ -0,0 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Sliding Window", order: 4, icon: "app-window", display: "group" });
@@ -1,13 +1,17 @@
---
title: '150. Evaluate Reverse Polish Notation'
description: You are given an array of strings tokens that represents an arithmetic expression in a Reverse Polish Notation
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Math", slug: "math" }
- { name: "Stack", slug: "stack" }
video: "iu0082c4HDE"
sidebar:
label: 'Evaluate Reverse Polish Notation'
badge: 'Medium'
---
<Badge variant="accent">Array</Badge>
### Example 1:
- Input: `tokens = ["2","1","+","3","*"]`
- Output: `9`
@@ -68,7 +72,3 @@ class Solution:
return stack[0]
```
## Explanation
<YouTube id="iu0082c4HDE" title="Evaluate Reverse Polish Notation - Leetcode 150 - Python" />
@@ -1,13 +1,16 @@
---
title: '155. Min Stack'
description: Design a stack that supports push, pop, top, and retrieving the minimum element in constant time
icon: dot
topics:
- { name: "Stack", slug: "stack" }
- { name: "Design", slug: "design" }
video: "qkLl7nAwDPo"
sidebar:
label: 'Min Stack'
badge: 'Medium'
---
<Badge variant="accent">Stack</Badge>
::::warning
You must implement a solution with O(1) time complexity for each function.
::::
@@ -76,7 +79,3 @@ class MinStack:
# param_3 = obj.top()
# param_4 = obj.getMin()
```
## Explanation
<YouTube id="qkLl7nAwDPo" title="Design Min Stack - Amazon Interview Question - Leetcode 155 - Python" />
@@ -1,13 +1,17 @@
---
title: '20. Valid Parentheses'
description: Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid
icon: dot
topics:
- { name: "String", slug: "string" }
- { name: "Stack", slug: "stack" }
- { name: "Bracket Sequences", slug: "bracket-sequences" }
video: "WTzjTskDFMg"
sidebar:
label: 'Valid Parentheses'
badge: 'Easy'
---
<Badge variant="accent">String</Badge>
### Example 1:
- Input: `s = "()"`
- Output: `true`
@@ -70,7 +74,3 @@ class Solution:
return True if not stack else False
```
## Explanation
<YouTube id="WTzjTskDFMg" title="Valid Parentheses - Stack - Leetcode 20 - Python" />
@@ -0,0 +1,5 @@
---
title: Stack
description: 'Items are added and removed from one end, so the last item added is the first item removed.'
hidden: true
---
@@ -0,0 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Stack", order: 6, icon: "layers", display: "group" });
@@ -1,13 +1,17 @@
---
title: '11. Container With Most Water'
description: You are given an integer array height of length n. There are n vertical lines drawn such that the two endpoints of the i^th line are (i, 0) and (i, height[i])
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Two Pointers", slug: "two-pointers" }
- { name: "Greedy", slug: "greedy" }
video: "UuiTKBwPgAo"
sidebar:
label: 'Container With Most Water'
badge: 'Medium'
---
<Badge variant="accent">Array</Badge>
::::warning
Notice that you may not slant the container.
::::
@@ -62,7 +66,3 @@ class Solution:
return res
```
## Explanation
<YouTube id="UuiTKBwPgAo" title="Container with Most Water - Leetcode 11 - Python" />
@@ -1,13 +1,16 @@
---
title: '125. Valid Palindrome'
description: A phrase is a palindrome if, after converting all uppercase letters into lowercase letters and removing all non-alphanumeric characters, it reads the same forward and backward. Alphanumeric characters include letters and numbers
icon: dot
topics:
- { name: "Two Pointers", slug: "two-pointers" }
- { name: "String", slug: "string" }
video: "jJXJ16kPFWg"
sidebar:
label: 'Valid Palindrome'
badge: 'Easy'
---
<Badge variant="accent">Two Pointers</Badge>
### Example 1:
- Input: `s = "A man, a plan, a canal: Panama"`
- Output: `true`
@@ -63,7 +66,3 @@ var isPalindrome = function(s) {
return true;
};
```
## Explanation
<YouTube id="jJXJ16kPFWg" title="Valid Palindrome - Leetcode 125 - Python" />
@@ -1,13 +1,17 @@
---
title: '15. 3Sum'
description: Given an integer array nums, return all the triplets [nums[i], nums[j], nums[k]] such that i != j, i != k, and j != k, and nums[i] + nums[j] + nums[k] == 0
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Two Pointers", slug: "two-pointers" }
- { name: "Sorting", slug: "sorting" }
video: "jzZsG8n2R9A"
sidebar:
label: '3Sum'
badge: 'Medium'
---
<Badge variant="accent">Array</Badge>
::::warning
Notice that the solution set must not contain duplicate triplets.
::::
@@ -98,7 +102,3 @@ class Solution:
return result
```
## Explanation
<YouTube id="jzZsG8n2R9A" title="3Sum - Leetcode 15 - Python" />
@@ -1,13 +1,17 @@
---
title: '167. Two Sum II - Input Array Is Sorted'
description: Given a 1-indexed array of integers numbers that is already sorted in non-decreasing order, find two numbers such that they add up to a specific target number. Let these two numbers be numbers[index1] and numbers[index2] where 1 <= index1 < index2 <= numbers.length
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Two Pointers", slug: "two-pointers" }
- { name: "Binary Search", slug: "binary-search" }
video: "cQ1Oz4ckceM"
sidebar:
label: 'Two Sum II - Input Array Is Sorted'
badge: 'Medium'
---
<Badge variant="accent">Array</Badge>
::::warning
Your solution must use only constant extra space.
::::
@@ -69,7 +73,3 @@ class Solution:
j-=1
return []
```
## Explanation
<YouTube id="cQ1Oz4ckceM" title="TWO SUM II - Amazon Coding Interview Question - Leetcode 167 - Python" />
@@ -1,13 +1,16 @@
---
title: '344. Reverse String'
description: Write a function that reverses a string. The input string is given as an array of characters s
icon: dot
topics:
- { name: "Two Pointers", slug: "two-pointers" }
- { name: "String", slug: "string" }
video: "_d0T_2Lk2qA"
sidebar:
label: 'Reverse String'
badge: 'Easy'
---
<Badge variant="accent">Two Pointers</Badge>
:::warning
You must do this by modifying the input array in-place with O(1) extra memory.
:::
@@ -55,7 +58,3 @@ var reverseString = function(s) {
}
};
```
## Explanation
<YouTube id="_d0T_2Lk2qA" title="Reverse String - 3 Ways - Leetcode 344 - Python" />
@@ -1,13 +1,17 @@
---
title: '392. Is Subsequence'
description: Given two strings s and t, return true if s is a subsequence of t, or false otherwise
icon: dot
topics:
- { name: "Two Pointers", slug: "two-pointers" }
- { name: "String", slug: "string" }
- { name: "Dynamic Programming", slug: "dynamic-programming" }
video: "99RVfqklbCE"
sidebar:
label: 'Is Subsequence'
badge: 'Easy'
---
<Badge variant="accent">Two Pointers</Badge>
::::warning
Suppose there are lots of incoming s, say s1, s2, ..., sk where k >= 10^9, and you want to check one by one to see if t has its subsequence. In this scenario, how would you change your code?
::::
@@ -58,7 +62,3 @@ class Solution:
return i == len(s)
```
## Explanation
<YouTube id="99RVfqklbCE" title="Is Subsequence - Leetcode 392" />
@@ -1,13 +1,17 @@
---
title: '977. Squares of a Sorted Array'
description: Given an integer array nums sorted in non-decreasing order, return an array of the squares of each number sorted in non-decreasing order
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Two Pointers", slug: "two-pointers" }
- { name: "Sorting", slug: "sorting" }
video: "FPCZsG_AkUg"
sidebar:
label: 'Squares of a Sorted Array'
badge: 'Easy'
---
<Badge variant="accent">Array</Badge>
::::warning
Squaring each element and sorting the new array is very trivial, could you find an O(n) solution using a different approach?
::::
@@ -68,7 +72,3 @@ var sortedSquares = function(nums) {
return ans;
};
```
## Explanation
<YouTube id="FPCZsG_AkUg" title="Squares of a Sorted Array - Leetcode 977 - Python" />
@@ -0,0 +1,5 @@
---
title: Two Pointers
description: 'Two indices move through the data from different positions and meet at the answer.'
hidden: true
---
@@ -0,0 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Two Pointers", order: 3, icon: "arrow-left-right", display: "group" });
@@ -1,13 +1,18 @@
---
title: '146. LRU Cache'
description: Design a data structure that follows the constraints of a Least Recently Used (LRU) cache
icon: dot
topics:
- { name: "Hash Table", slug: "hash-table" }
- { name: "Linked List", slug: "linked-list" }
- { name: "Design", slug: "design" }
- { name: "Doubly-Linked List", slug: "doubly-linked-list" }
video: "7ABFKPK2hD4"
sidebar:
label: 'LRU Cache'
badge: 'Medium'
---
<Badge variant="accent">Hash Table</Badge>
### Example 1:
- Input: ``
- Output: ``
@@ -109,7 +114,3 @@ class LRUCache:
# param_1 = obj.get(key)
# obj.put(key,value)
```
## Explanation
<YouTube id="7ABFKPK2hD4" title="LRU Cache - Twitch Interview Question - Leetcode 146" />
@@ -1,13 +1,17 @@
---
title: '2. Add Two Numbers'
description: You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order, and each of their nodes contains a single digit. Add the two numbers and return the sum as a linked list
icon: dot
topics:
- { name: "Linked List", slug: "linked-list" }
- { name: "Math", slug: "math" }
- { name: "Recursion", slug: "recursion" }
video: "wgFPrzTjm7s"
sidebar:
label: 'Add Two Numbers'
badge: 'Medium'
---
<Badge variant="accent">Linked List</Badge>
### Example 1:
- Input: `l1 = [2,4,3], l2 = [5,6,4]`
- Output: `[7,0,8]`
@@ -69,7 +73,3 @@ class Solution:
return dummy.next
```
## Explanation
<YouTube id="wgFPrzTjm7s" title="Add Two Numbers - Leetcode 2 - Python" />
@@ -0,0 +1,5 @@
---
title: Hybrid Structures
description: 'Two structures are combined, such as a hash map and a linked list, to get the best properties of each.'
hidden: true
---
@@ -1,3 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Hybrid Structures", order: 9 });
export default defineMeta({ title: "Hybrid Structures", order: 9, icon: "combine", display: "group" });
@@ -1,13 +1,18 @@
---
title: '141. Linked List Cycle'
description: Given head, the head of a linked list, determine if the linked list has a cycle in it
icon: dot
topics:
- { name: "Hash Table", slug: "hash-table" }
- { name: "Linked List", slug: "linked-list" }
- { name: "Two Pointers", slug: "two-pointers" }
- { name: "Floyd's Cycle Finding Algorithm", slug: "floyds-cycle-finding-algorithm" }
video: "gBTe7lFR3vc"
sidebar:
label: 'Linked List Cycle'
badge: 'Easy'
---
<Badge variant="accent">Hash Table</Badge>
::::warning
Can you solve it using O(1) (i.e. constant) memory?
::::
@@ -68,7 +73,3 @@ class Solution:
return True
return False
```
## Explanation
<YouTube id="gBTe7lFR3vc" title="Linked List Cycle - Floyd's Tortoise and Hare - Leetcode 141 - Python" />
@@ -1,13 +1,16 @@
---
title: '206. Reverse Linked List'
description: Given the head of a singly linked list, reverse the list, and return the reversed list
icon: dot
topics:
- { name: "Linked List", slug: "linked-list" }
- { name: "Recursion", slug: "recursion" }
video: "G0_I-ZF0S38"
sidebar:
label: 'Reverse Linked List'
badge: 'Easy'
---
<Badge variant="accent">Linked List</Badge>
::::warning
A linked list can be reversed either iteratively or recursively. Could you implement both?
::::
@@ -61,7 +64,3 @@ class Solution:
curr = next_
return prev
```
## Explanation
<YouTube id="G0_I-ZF0S38" title="Reverse Linked List - Iterative AND Recursive - Leetcode 206 - Python" />
@@ -1,13 +1,16 @@
---
title: '21. Merge Two Sorted Lists'
description: You are given the heads of two sorted linked lists list1 and list2
icon: dot
topics:
- { name: "Linked List", slug: "linked-list" }
- { name: "Recursion", slug: "recursion" }
video: "XIdigk956u0"
sidebar:
label: 'Merge Two Sorted Lists'
badge: 'Easy'
---
<Badge variant="accent">Linked List</Badge>
### Example 1:
- Input: `list1 = [1,2,4], list2 = [1,3,4]`
- Output: `[1,1,2,3,4,4]`
@@ -81,7 +84,3 @@ class Solution:
current.next = list1 or list2
return head
```
## Explanation
<YouTube id="XIdigk956u0" title="Merge Two Sorted Lists - Leetcode 21 - Python" />
@@ -0,0 +1,5 @@
---
title: Linked List
description: 'Each node points to the next node, and the pointers give the path through the data.'
hidden: true
---
@@ -0,0 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Linked List", order: 8, icon: "link", display: "group" });
@@ -1,13 +1,17 @@
---
title: '48. Rotate Image'
description: You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise)
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Math", slug: "math" }
- { name: "Matrix", slug: "matrix" }
video: "fMSJSS7eO1w"
sidebar:
label: 'Rotate Image'
badge: 'Medium'
---
<Badge variant="accent">Array</Badge>
### Example 1:
- Input: `matrix = [[1,2,3],[4,5,6],[7,8,9]]`
- Output: `[[7,4,1],[8,5,2],[9,6,3]]`
@@ -51,7 +55,3 @@ class Solution:
for r in range(n):
matrix[r].reverse()
```
## Explanation
<YouTube id="fMSJSS7eO1w" title="Rotate Image - Matrix - Leetcode 48" />
@@ -1,13 +1,17 @@
---
title: '54. Spiral Matrix'
description: Given an m x n matrix, return all elements of the matrix in spiral order
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Matrix", slug: "matrix" }
- { name: "Simulation", slug: "simulation" }
video: "BJnMZNwUk1M"
sidebar:
label: 'Spiral Matrix'
badge: 'Medium'
---
<Badge variant="accent">Array</Badge>
### Example 1:
- Input: `matrix = [[1,2,3],[4,5,6],[7,8,9]]`
- Output: `[1,2,3,6,9,8,7,4,5]`
@@ -75,7 +79,3 @@ class Solution:
return res
```
## Explanation
<YouTube id="BJnMZNwUk1M" title="Spiral Matrix - Microsoft Interview Question - Leetcode 54" />
@@ -1,13 +1,17 @@
---
title: '73. Set Matrix Zeroes'
description: Given an m x n integer matrix matrix, if an element is 0, set its entire row and column to 0's
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Hash Table", slug: "hash-table" }
- { name: "Matrix", slug: "matrix" }
video: "T41rL0L3Pnw"
sidebar:
label: 'Set Matrix Zeroes'
badge: 'Medium'
---
<Badge variant="accent">Array</Badge>
::::warning
You must do it in place.
::::
@@ -94,7 +98,3 @@ class Solution:
for r in range(m):
matrix[r][0] = 0
```
## Explanation
<YouTube id="T41rL0L3Pnw" title="Set Matrix Zeroes - In-place - Leetcode 73" />
@@ -0,0 +1,5 @@
---
title: Matrix Index Math
description: 'A two-dimensional grid is read as a flat array through a formula, and the neighbors are found by arithmetic.'
hidden: true
---
@@ -1,3 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Matrix Index Math", order: 10 });
export default defineMeta({ title: "Matrix Index Math", order: 10, icon: "grid-3x3", display: "group" });
@@ -0,0 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Phase 2: Nodal & Grid", order: 2 });
@@ -1,13 +1,18 @@
---
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
icon: dot
topics:
- { name: "Tree", slug: "tree" }
- { name: "Depth-First Search", slug: "depth-first-search" }
- { name: "Binary Search Tree", slug: "binary-search-tree" }
- { name: "Binary Tree", slug: "binary-tree" }
video: "5LUXSvjmGCw"
sidebar:
label: 'Kth Smallest Element in a BST'
badge: 'Medium'
---
<Badge variant="accent">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?
::::
@@ -69,7 +74,3 @@ class Solution:
return cur.val
cur = cur.right
```
## Explanation
<YouTube id="5LUXSvjmGCw" title="Kth Smallest Element in a BST - Leetcode 230 - Python" />
@@ -1,13 +1,20 @@
---
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
icon: dot
topics:
- { name: "Tree", slug: "tree" }
- { name: "Depth-First Search", slug: "depth-first-search" }
- { name: "Binary Search Tree", slug: "binary-search-tree" }
- { name: "Binary Tree", slug: "binary-tree" }
- { name: "Binary Lifting", slug: "binary-lifting" }
- { name: "Lowest Common Ancestor", slug: "lowest-common-ancestor" }
video: "gs2LMfuOR9k"
sidebar:
label: 'Lowest Common Ancestor of a Binary Search Tree'
badge: 'Medium'
---
<Badge variant="accent">Tree</Badge>
### Example 1:
- Input: `root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8`
- Output: `6`
@@ -70,7 +77,3 @@ class Solution:
else:
return cur
```
## Explanation
<YouTube id="gs2LMfuOR9k" title="Lowest Common Ancestor of a Binary Search Tree - Leetcode 235 - Python" />
@@ -1,13 +1,18 @@
---
title: '98. Validate Binary Search Tree'
description: Given the root of a binary tree, determine if it is a valid binary search tree (BST)
icon: dot
topics:
- { name: "Tree", slug: "tree" }
- { name: "Depth-First Search", slug: "depth-first-search" }
- { name: "Binary Search Tree", slug: "binary-search-tree" }
- { name: "Binary Tree", slug: "binary-tree" }
video: "s6ATEkipzow"
sidebar:
label: 'Validate Binary Search Tree'
badge: 'Medium'
---
<Badge variant="accent">Tree</Badge>
### Example 1:
- Input: `root = [2,1,3]`
- Output: `true`
@@ -61,7 +66,3 @@ class Solution:
return valid(root, float("-inf"), float("inf"))
```
## Explanation
<YouTube id="s6ATEkipzow" title="Validate Binary Search Tree - Depth First Search - Leetcode 98" />
@@ -0,0 +1,5 @@
---
title: Binary Search Tree
description: 'Each node holds a value that is larger than its left subtree and smaller than its right subtree.'
hidden: true
---
@@ -1,3 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Binary Search Tree", order: 13 });
export default defineMeta({ title: "Binary Search Tree", order: 13, icon: "git-fork", display: "group" });
@@ -1,13 +1,19 @@
---
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
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Divide and Conquer", slug: "divide-and-conquer" }
- { name: "Sorting", slug: "sorting" }
- { name: "Heap (Priority Queue)", slug: "heap-priority-queue" }
- { name: "Quickselect", slug: "quickselect" }
video: "XEmy13g1Qxc"
sidebar:
label: 'Kth Largest Element in an Array'
badge: 'Medium'
---
<Badge variant="accent">Array</Badge>
### Example 1:
- Input: `nums = [3,2,1,5,6,4], k = 2`
- Output: `5`
@@ -53,7 +59,3 @@ class Solution:
return -heapq.heappop(nums)
```
## Explanation
<YouTube id="XEmy13g1Qxc" title="Kth Largest Element in an Array - Quick Select - Leetcode 215 - Python" />
@@ -1,13 +1,19 @@
---
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
icon: dot
topics:
- { name: "Linked List", slug: "linked-list" }
- { name: "Divide and Conquer", slug: "divide-and-conquer" }
- { name: "Heap (Priority Queue)", slug: "heap-priority-queue" }
- { name: "Merge Sort", slug: "merge-sort" }
- { name: "Tournament Sort", slug: "tournament-sort" }
video: "q5a5OiGbT6Q"
sidebar:
label: 'Merge k Sorted Lists'
badge: 'Hard'
---
<Badge variant="accent">Linked List</Badge>
### Example 1:
- Input: `lists = [[1,4,5],[1,3,4],[2,6]]`
- Output: `[1,1,2,3,4,4,5,6]`
@@ -88,7 +94,3 @@ class Solution:
# Space: O(n)
return D.next
```
## Explanation
<YouTube id="q5a5OiGbT6Q" title="Merge K Sorted Lists - Leetcode 23 - Python" />
@@ -1,13 +1,22 @@
---
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)
icon: dot
topics:
- { name: "Array", slug: "array" }
- { name: "Math", slug: "math" }
- { name: "Divide and Conquer", slug: "divide-and-conquer" }
- { name: "Geometry", slug: "geometry" }
- { name: "Sorting", slug: "sorting" }
- { name: "Heap (Priority Queue)", slug: "heap-priority-queue" }
- { name: "Quickselect", slug: "quickselect" }
- { name: "K-D Tree", slug: "k-d-tree" }
video: "rI2EBUEMfTk"
sidebar:
label: 'K Closest Points to Origin'
badge: 'Medium'
---
<Badge variant="accent">Array</Badge>
### Example 1:
- Input: `points = [[1,3],[-2,2]], k = 1`
- Output: `[[-2,2]]`
@@ -63,7 +72,3 @@ class Solution:
return res
```
## Explanation
<YouTube id="rI2EBUEMfTk" title="K Closest Points to Origin - Heap / Priority Queue - Leetcode 973 - Python" />
@@ -0,0 +1,5 @@
---
title: Heap / Priority Queue
description: 'The largest or smallest item is always at the top, and it is removed in logarithmic time.'
hidden: true
---
@@ -1,3 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Heap / Priority Queue", order: 14 });
export default defineMeta({ title: "Heap / Priority Queue", order: 14, icon: "triangle", display: "group" });
@@ -0,0 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Phase 3: Hierarchical", order: 3 });
@@ -1,13 +1,17 @@
---
title: '102. Binary Tree Level Order Traversal'
description: Given the root of a binary tree, return the level order traversal of its nodes' values. (i.e., from left to right, level by level)
icon: dot
topics:
- { name: "Tree", slug: "tree" }
- { name: "Breadth-First Search", slug: "breadth-first-search" }
- { name: "Binary Tree", slug: "binary-tree" }
video: "6ZnyEApgFYg"
sidebar:
label: 'Binary Tree Level Order Traversal'
badge: 'Medium'
---
<Badge variant="accent">Tree</Badge>
### Example 1:
- Input: `root = [3,9,20,null,null,15,7]`
- Output: `[[3],[9,20],[15,7]]`
@@ -79,7 +83,3 @@ class Solution:
return res
```
## Explanation
<YouTube id="6ZnyEApgFYg" title="Binary Tree Level Order Traversal - BFS - Leetcode 102" />
@@ -1,13 +1,18 @@
---
title: '1448. Count Good Nodes in Binary Tree'
description: Given a binary tree root, a node X in the tree is named good if in the path from root to X there are no nodes with a value greater than X
icon: dot
topics:
- { name: "Tree", slug: "tree" }
- { name: "Depth-First Search", slug: "depth-first-search" }
- { name: "Breadth-First Search", slug: "breadth-first-search" }
- { name: "Binary Tree", slug: "binary-tree" }
video: "7cp5imvDzl4"
sidebar:
label: 'Count Good Nodes in Binary Tree'
badge: 'Medium'
---
<Badge variant="accent">Tree</Badge>
### Example 1:
- Input: `root = [3,1,4,3,null,1,5]`
- Output: `4`
@@ -66,7 +71,3 @@ class Solution:
return dfs(root, root.val)
```
## Explanation
<YouTube id="7cp5imvDzl4" title="Microsoft's Most Asked Question 2021 - Count Good Nodes in a Binary Tree - Leetcode 1448 - Python" />
@@ -1,13 +1,18 @@
---
title: '199. Binary Tree Right Side View'
description: Given the root of a binary tree, imagine yourself standing on the right side of it, return the values of the nodes you can see ordered from top to bottom
icon: dot
topics:
- { name: "Tree", slug: "tree" }
- { name: "Depth-First Search", slug: "depth-first-search" }
- { name: "Breadth-First Search", slug: "breadth-first-search" }
- { name: "Binary Tree", slug: "binary-tree" }
video: "d4zLyf32e3I"
sidebar:
label: 'Binary Tree Right Side View'
badge: 'Medium'
---
<Badge variant="accent">Tree</Badge>
### Example 1:
- Input: `root = [1,2,3,null,5,null,4]`
- Output: `[1,3,4]`
@@ -82,7 +87,3 @@ class Solution:
return res
```
## Explanation
<YouTube id="d4zLyf32e3I" title="Binary Tree Right Side View - Breadth First Search - Leetcode 199" />
@@ -0,0 +1,5 @@
---
title: Tree BFS
description: 'All nodes at one level are visited before the nodes at the next level.'
hidden: true
---
@@ -0,0 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Tree BFS", order: 12, icon: "network", display: "group" });
@@ -1,13 +1,18 @@
---
title: '104. Maximum Depth of Binary Tree'
description: Given the root of a binary tree, return its maximum depth
icon: dot
topics:
- { name: "Tree", slug: "tree" }
- { name: "Depth-First Search", slug: "depth-first-search" }
- { name: "Breadth-First Search", slug: "breadth-first-search" }
- { name: "Binary Tree", slug: "binary-tree" }
video: "hTM3phVI6YQ"
sidebar:
label: 'Maximum Depth of Binary Tree'
badge: 'Easy'
---
<Badge variant="accent">Tree</Badge>
### Example 1:
- Input: `root = [3,9,20,null,null,15,7]`
- Output: `3`
@@ -47,7 +52,3 @@ class Solution:
return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right))
```
## Explanation
<YouTube id="hTM3phVI6YQ" title="Maximum Depth of Binary Tree - 3 Solutions - Leetcode 104 - Python" />
@@ -1,13 +1,18 @@
---
title: '226. Invert Binary Tree'
description: Given the root of a binary tree, invert the tree, and return its root
icon: dot
topics:
- { name: "Tree", slug: "tree" }
- { name: "Depth-First Search", slug: "depth-first-search" }
- { name: "Breadth-First Search", slug: "breadth-first-search" }
- { name: "Binary Tree", slug: "binary-tree" }
video: "OnSn2XEQ4MY"
sidebar:
label: 'Invert Binary Tree'
badge: 'Easy'
---
<Badge variant="accent">Tree</Badge>
### Example 1:
- Input: `root = [4,2,7,1,3,6,9]`
- Output: `[4,7,2,9,6,3,1]`
@@ -58,7 +63,3 @@ class Solution:
return root
```
## Explanation
<YouTube id="OnSn2XEQ4MY" title="Invert Binary Tree - Depth First Search - Leetcode 226" />
@@ -1,13 +1,18 @@
---
title: '236. Lowest Common Ancestor of a Binary Tree'
description: Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree
icon: dot
topics:
- { name: "Tree", slug: "tree" }
- { name: "Depth-First Search", slug: "depth-first-search" }
- { name: "Binary Tree", slug: "binary-tree" }
- { name: "Binary Lifting", slug: "binary-lifting" }
- { name: "Lowest Common Ancestor", slug: "lowest-common-ancestor" }
sidebar:
label: 'Lowest Common Ancestor of a Binary Tree'
badge: 'Medium'
---
<Badge variant="accent">Tree</Badge>
### Example 1:
- Input: `root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1`
- Output: `3`
@@ -0,0 +1,5 @@
---
title: Tree DFS
description: 'Each branch is followed to its end before the next branch is examined.'
hidden: true
---
@@ -0,0 +1,3 @@
import { defineMeta } from "blume";
export default defineMeta({ title: "Tree DFS", order: 11, icon: "folder-tree", display: "group" });
Loaded 100 of 241 files, more files were not shown because too many files have changed in this diff. Show more