mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 21:22:43 +00:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa74adeda7 | ||
|
|
e11c9723f8 | ||
|
|
d2c645bef8 | ||
|
|
2f94b02e15 | ||
|
|
6f1cb93395 | ||
|
|
b310f410a2 | ||
|
|
436ff3080e | ||
|
|
d3f7b1c6f9 | ||
|
|
8a331f4b54 | ||
|
|
bc6eb3e66b | ||
|
|
dd929cca6c | ||
|
|
5987fdce7d | ||
|
|
6879afa61c | ||
|
|
40e965aa06 | ||
|
|
eea8d242e0 | ||
|
|
50b42108eb | ||
|
|
6f375e6c14 | ||
|
|
667bc06965 | ||
|
|
e63d517c9a | ||
|
|
4c297b4524 | ||
|
|
32dadf5928 |
No files matched your search
@@ -1,36 +0,0 @@
|
||||
#!/bin/sh
|
||||
# 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
|
||||
# half-staged solution would ship a page whose source is not in the commit.
|
||||
# In that case refuse and say what to stage.
|
||||
#
|
||||
# Only paths that became dirty during sync are staged. Anything already dirty
|
||||
# beforehand (hand-written `## Approach` prose in progress, a new index.mdx)
|
||||
# stays where the author left it.
|
||||
#
|
||||
# Bypass for a deliberate partial commit: git commit --no-verify
|
||||
set -eu
|
||||
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
unstaged=$(git status --porcelain -- work/1 | grep -v '^[MADR] ' || true)
|
||||
if [ -n "$unstaged" ]; then
|
||||
printf 'pre-commit: work/1 has unstaged changes; stage or stash them so the generated pages match this commit:\n%s\n' "$unstaged" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
before=$(mktemp)
|
||||
trap 'rm -f "$before"' EXIT
|
||||
git status --porcelain -- apps/docs/content apps/docs/redirects.ts | sort > "$before"
|
||||
report=$(bun run --silent sync)
|
||||
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 --
|
||||
fi
|
||||
@@ -46,7 +46,7 @@ flowchart LR
|
||||
|---|---|
|
||||
| `work/<1\|3\|7>/<Difficulty>/<Category>/<num>.<slug>.{js,py}` | Solutions, e.g. `work/1/Easy/Array/1.two-sum.py`. The top directory is the spaced-repetition bucket: `1` = first solve, `3`/`7` = the blind re-solves of that problem 3 and 7 days later. Machine-parsed header comment (title / `Difficulty:` / URL / `─` rule / statement) — preserve its exact shape |
|
||||
| `apps/cli/` | Flat Bun TS workspace: automation entries (shebang + top-level await) and libraries (no shebang, side-effect-free on import). Root `bun run` scripts delegate here |
|
||||
| `apps/docs/` | Blume site (`blume.config.ts`, `content/`, `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/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 (a phase's once, then hand-owned): 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 (between the description and the body) is rendered, not written: the MDX `wrapper` (`components/ProblemBadges.astro`, registered as `mdx.wrapper` in `components.ts` — Blume's page template renders the title and description itself with no slot before the body, so the wrapper, which MDX hands no route, reads it off `Astro.url`) 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. The reverse direction is the `PageFooter` slot (`components/BacklinksList.astro`): an Obsidian-style `<num> <name>` list under every topic landing and guide page of the problems whose badges link there, plus a topic's own folder members (so a topic no tag names, `hybrid-structures`, still lists its problems), in a native `<details>` open on load whose top-right chevron collapses it (no JS); problem pages are recognised by their `<num>. <name>` title and render no list. Which tags the campaign teaches is ONE map, `apps/docs/topic-landings.ts` (tag slug → landing routes), and where a badge points is ONE function beside it, `tagTarget()` — read by both components (so badges and backlinks cannot disagree) and the map by sync. `content/guides/` (the `Guides` header tab) is one page per tag outside that map, filed by hand into `(group)` categories — `(1-data-structures)/`, `(2-algorithms)/`, `(3-techniques)/` (hand-written `meta.ts` each; no route segment, so every guide stays `/guides/<slug>`). Sync finds a guide by slug anywhere under `guides/` and creates a placeholder only when none exists or it is empty — at the `guides/` root, for you to file into a category and give a Lucide `icon:` that no topic uses (not a `TOPIC_ICONS` value — sync warns on a collision — and, by convention, not one any other page uses either) — and after that owns exactly one key of it: `sidebar.badge`, the sidebar count, rewritten every run to the guide's backlink count (problem pages carrying the tag — equal to the `BacklinksList` rows, since a guide's tag has no landing; dropped at zero). Everything else is hand-owned (a guide whose tag no page carries any more is left alone, badge dropped), with frontmatter `list:` — the tag's LeetCode problem list, rendered by the same wrapper 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 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 |
|
||||
@@ -110,7 +110,7 @@ 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 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/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>/`. The topic `meta.ts` is script-owned and rewritten every run (label, curriculum order, `TOPIC_ICONS` icon, `display: "group"`); the phase `meta.ts` is written once when missing (`Phase N: Name`, `order: N`) and hand-owned after — rename its title freely. Never rewriting it loses nothing: its order and name are also the folder name, so a README change to either lands in a new folder with a fresh meta. 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`).
|
||||
|
||||
+95
-22
@@ -15,14 +15,19 @@
|
||||
* `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
|
||||
* renders no badges: the docs MDX `wrapper`
|
||||
* (`apps/docs/components/ProblemBadges.astro`) turns `topics:` and the
|
||||
* `video:` key `videos.ts` writes into the badge row above the title.
|
||||
* `video:` key `videos.ts` writes into the badge row under the description.
|
||||
* - 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.
|
||||
* the tag. Created once (a missing or empty file), then hand-owned except
|
||||
* for `sidebar.badge`, which sync keeps at the guide's backlink count (the
|
||||
* problem pages carrying the tag; dropped at zero); the tag's badge on a
|
||||
* problem page links to it. A guide may be filed into a category `(group)`
|
||||
* folder (e.g. `guides/(2-algorithms)/`) — no route segment, so
|
||||
* `/guides/<slug>` holds — and is found there by slug; a new placeholder
|
||||
* lands at the `guides/` root for a human to file (and give an `icon:`).
|
||||
*
|
||||
* 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
|
||||
@@ -30,13 +35,15 @@
|
||||
* its file sits under in `work/1/<Difficulty>/<Category>/` instead.
|
||||
*
|
||||
* 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.
|
||||
* The phase folder is a Blume `(group)` — a sidebar section 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. A phase folder's `meta.ts` is written once, when
|
||||
* missing, then hand-owned (its title is yours to rename); a topic's is
|
||||
* script-owned. 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
|
||||
@@ -309,7 +316,13 @@ 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. */
|
||||
/**
|
||||
* Blume `(group)` meta for a phase: the overview page's heading, in phase
|
||||
* order. Written only when the folder has none — the title is hand-owned after
|
||||
* that. Nothing is lost by never rewriting it: the order and name it is built
|
||||
* from are also the folder name (`phaseDir`), so a README change to either
|
||||
* lands in a new folder that gets a fresh meta.
|
||||
*/
|
||||
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`;
|
||||
}
|
||||
@@ -387,7 +400,7 @@ function frontmatterTopics(page: string): TopicTag[] {
|
||||
/**
|
||||
* 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
|
||||
* rendered as a badge by the docs MDX `wrapper`. Written once — the page
|
||||
* is hand-owned from then on.
|
||||
*/
|
||||
function guidePage(tag: TopicTag): string {
|
||||
@@ -407,6 +420,27 @@ function guidePage(tag: TopicTag): string {
|
||||
/** 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`;
|
||||
|
||||
/**
|
||||
* A guide's frontmatter with `sidebar.badge` set to its backlink count — the
|
||||
* one key sync owns on a hand-owned guide — or dropped (with a `sidebar:` left
|
||||
* empty) at zero, so the sidebar shows a count only where there is something
|
||||
* to count.
|
||||
*/
|
||||
function withBacklinkBadge(page: string, count: number): string {
|
||||
const fm = page.match(/^---\n[\s\S]*?\n---/);
|
||||
if (!fm) return page;
|
||||
let head = fm[0].replace(/^ {2}badge: .*\n/m, "");
|
||||
if (count > 0) {
|
||||
const line = ` badge: '${count}'`;
|
||||
head = /^sidebar:$/m.test(head)
|
||||
? head.replace(/^sidebar:$/m, `sidebar:\n${line}`)
|
||||
: head.replace(/\n---$/, `\nsidebar:\n${line}\n---`);
|
||||
} else {
|
||||
head = head.replace(/^sidebar:\n(?! {2})/m, "");
|
||||
}
|
||||
return head + page.slice(fm[0].length);
|
||||
}
|
||||
|
||||
// ── inline-code formatting heuristics ────────────────────────────
|
||||
|
||||
/** Identifiers worth backticking in prose, harvested from the code. */
|
||||
@@ -651,6 +685,7 @@ 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
|
||||
const tagged = new Map<string, number>(); // tag slug -> how many problem pages carry it
|
||||
|
||||
for (const sol of ordered) {
|
||||
const langs = Object.keys(sol.code).sort().join("+");
|
||||
@@ -666,6 +701,7 @@ for (const sol of ordered) {
|
||||
const existing = pages.get(sol.num);
|
||||
await mkdir(join(DOCS, dir), { recursive: true });
|
||||
|
||||
let written: string; // the page as it stands after this run, for its topics
|
||||
if (existing) {
|
||||
const page = await Bun.file(join(DOCS, existing)).text();
|
||||
const at = page.indexOf("## Solution");
|
||||
@@ -687,12 +723,16 @@ 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);
|
||||
written = next;
|
||||
} else {
|
||||
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);
|
||||
written = page;
|
||||
}
|
||||
for (const tag of frontmatterTopics(written)) {
|
||||
tags.set(tag.slug, tag);
|
||||
tagged.set(tag.slug, (tagged.get(tag.slug) ?? 0) + 1);
|
||||
}
|
||||
// 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}`)} },`);
|
||||
@@ -715,22 +755,55 @@ for (const sol of ordered) {
|
||||
}
|
||||
|
||||
// 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.
|
||||
// (an empty file counts as missing) and never rewritten. A guide is found by
|
||||
// slug anywhere under guides/ — a human files it into a category `(group)`,
|
||||
// which leaves its route alone — and a new one lands at the guides/ root. 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));
|
||||
const guideFiles = new Map<string, string>(); // slug -> path relative to DOCS
|
||||
for await (const rel of new Bun.Glob("guides/**/*.mdx").scan(DOCS)) guideFiles.set(basename(rel, ".mdx"), rel);
|
||||
for (const tag of guideTags) {
|
||||
const file = Bun.file(join(DOCS, "guides", `${tag.slug}.mdx`));
|
||||
const rel = guideFiles.get(tag.slug) ?? `guides/${tag.slug}.mdx`;
|
||||
const file = Bun.file(join(DOCS, rel));
|
||||
if ((await file.exists()) && file.size > 0) continue;
|
||||
await Bun.write(file, guidePage(tag));
|
||||
report.push([`tag ${tag.slug}`, `guides/${tag.slug}.mdx`, "created"]);
|
||||
report.push([`tag ${tag.slug}`, rel, "created"]);
|
||||
}
|
||||
|
||||
// Script-owned folder metas — one per live phase and topic folder, plus
|
||||
// guides/ — rewritten only when they differ.
|
||||
// Every guide's sidebar badge is its backlink count: the problem pages whose
|
||||
// tag badge links to it, which is every page carrying the tag, since a guide's
|
||||
// tag has no landing (`tagTarget()`). A guide whose tag a landing has since
|
||||
// claimed has no backlinks left. Counts move with every solved problem, so the
|
||||
// badge is rewritten whenever it differs; the rest of the guide is untouched.
|
||||
// The same pass warns when a guide's hand-picked `icon:` is one a topic group
|
||||
// owns (`TOPIC_ICONS`), so a guide row never reads as a topic row.
|
||||
const topicIcons = new Set(Object.values(TOPIC_ICONS));
|
||||
for await (const rel of new Bun.Glob("guides/**/*.mdx").scan(DOCS)) {
|
||||
const slug = basename(rel, ".mdx");
|
||||
const count = TOPIC_LANDINGS[slug] ? 0 : (tagged.get(slug) ?? 0);
|
||||
const page = await Bun.file(join(DOCS, rel)).text();
|
||||
const icon = page.match(/^---\n[\s\S]*?\n---/)?.[0].match(/^icon: (\S+)$/m)?.[1];
|
||||
if (icon && topicIcons.has(icon)) console.warn(`${rel}: icon ${JSON.stringify(icon)} is a topic's (TOPIC_ICONS); pick another`);
|
||||
const next = withBacklinkBadge(page, count);
|
||||
if (next === page) continue;
|
||||
await Bun.write(join(DOCS, rel), next);
|
||||
report.push([`${count} backlinks`, rel, "badged"]);
|
||||
}
|
||||
|
||||
// Phase metas: created when missing (an empty file counts as missing), never
|
||||
// rewritten, so a renamed phase title survives every sync.
|
||||
for (const [dir, phase] of phases) {
|
||||
const file = Bun.file(join(DOCS, dir, "meta.ts"));
|
||||
if ((await file.exists()) && file.size > 0) continue;
|
||||
await Bun.write(file, phaseMeta(phase));
|
||||
report.push([`phase ${phase.order}`, `${dir}/meta.ts`, "created"]);
|
||||
}
|
||||
|
||||
// Script-owned folder metas — one per live 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]] : []),
|
||||
];
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
* Find the NeetCode explanation video for every docs page that has none.
|
||||
*
|
||||
* A finished solution page carries frontmatter `video: <YouTube id>` —
|
||||
* NeetCode's walkthrough of the problem — which the docs `PageHeader` slot
|
||||
* NeetCode's walkthrough of the problem — which the docs MDX `wrapper`
|
||||
* (`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
|
||||
|
||||
@@ -9,9 +9,12 @@ export default defineComponents({
|
||||
mdx: {
|
||||
Carousel: "./components/Carousel.astro",
|
||||
CarouselImage: "./components/CarouselImage.astro",
|
||||
// MDX's own content wrapper: problem pages' topic + explanation badges, from
|
||||
// `topics:` / `video:` frontmatter, between the description and the body.
|
||||
wrapper: "./components/ProblemBadges.astro",
|
||||
},
|
||||
layout: {
|
||||
// Problem pages' topic + explanation badges, from `topics:` / `video:` frontmatter.
|
||||
PageHeader: "./components/ProblemBadges.astro",
|
||||
// Topic landing + guide pages' "what links here": every problem pointing at the page.
|
||||
PageFooter: "./components/BacklinksList.astro",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
/**
|
||||
* The `PageFooter` slot: an Obsidian-style backlinks list under a topic landing
|
||||
* or guide page — every problem page that points at it, as `<num> <name>`
|
||||
* rows in LeetCode-number order. Pages nothing points at (problem pages, the
|
||||
* overview, the changelog) render nothing.
|
||||
*
|
||||
* A problem points at a page when either
|
||||
* - one of its topic badges links there — the same `tagTarget()` answer
|
||||
* `ProblemBadges.astro` renders, so this list and the badges cannot drift
|
||||
* (a DFS problem is a backlink of the one DFS landing its badge picks); or
|
||||
* - it lives under the page's route (`/hybrid-structures/lru-cache` under
|
||||
* `/hybrid-structures`), so a topic no LeetCode tag names still lists its
|
||||
* own problems.
|
||||
*
|
||||
* Problem pages are recognised by their `<num>. <name>` title, the shape
|
||||
* `apps/cli/sync.ts` writes.
|
||||
*
|
||||
* The list is a native `<details>`, open on load: the chevron at the top
|
||||
* right of the header row collapses it like an accordion, with no JS.
|
||||
*/
|
||||
import { getEntry } from "astro:content";
|
||||
import data from "blume:data";
|
||||
import { withBase } from "blume/components/islands/base-path.ts";
|
||||
import Icon from "blume/components/Icon.astro";
|
||||
import { tagTarget, type TopicTag } from "../topic-landings.ts";
|
||||
|
||||
interface Props {
|
||||
route: string;
|
||||
}
|
||||
|
||||
interface Route {
|
||||
path: string;
|
||||
title: string;
|
||||
collection: string;
|
||||
entryId: string;
|
||||
}
|
||||
|
||||
const { route } = Astro.props;
|
||||
const routes: Route[] = data.routes;
|
||||
const served = new Set(routes.map((r) => r.path));
|
||||
|
||||
const problems = routes.flatMap((r) => {
|
||||
const m = r.title.match(/^(\d+)\.\s+(.+)$/);
|
||||
return m ? [{ route: r, num: Number(m[1]), name: m[2]! }] : [];
|
||||
});
|
||||
|
||||
// A problem page is never a backlink target: skip the per-problem lookups there.
|
||||
const backlinks = problems.some((p) => p.route.path === route)
|
||||
? []
|
||||
: (
|
||||
await Promise.all(
|
||||
problems.map(async (p) => {
|
||||
if (p.route.path.startsWith(`${route}/`)) return p;
|
||||
const entry = await getEntry(p.route.collection, p.route.entryId);
|
||||
const tags: TopicTag[] = entry?.data.topics ?? [];
|
||||
const page = { route: p.route.path, tags };
|
||||
return tags.some((tag) => tagTarget(tag, page, served).href === route) ? p : undefined;
|
||||
}),
|
||||
)
|
||||
)
|
||||
.filter((p) => p !== undefined)
|
||||
.sort((a, b) => a.num - b.num);
|
||||
---
|
||||
|
||||
{
|
||||
backlinks.length > 0 && (
|
||||
<details
|
||||
class="group not-prose mx-auto mt-12 max-w-content border-border border-t pt-6"
|
||||
open
|
||||
>
|
||||
<summary class="mb-2 flex cursor-pointer list-none items-center justify-between gap-2 text-muted-foreground transition-colors hover:text-foreground [&::-webkit-details-marker]:hidden">
|
||||
<h2 class="flex items-baseline gap-2 font-medium text-xs uppercase tracking-wide" id="backlinks">
|
||||
Backlinks
|
||||
<span class="tabular-nums">{backlinks.length}</span>
|
||||
</h2>
|
||||
<Icon class="shrink-0 transition-transform group-open:rotate-180" name="chevron-down" size={16} />
|
||||
</summary>
|
||||
<ul class="text-sm">
|
||||
{backlinks.map((p) => (
|
||||
<li>
|
||||
<a
|
||||
class="-mx-2 flex items-baseline gap-3 rounded-md px-2 py-1 text-foreground no-underline transition-colors hover:bg-muted"
|
||||
href={withBase(p.route.path)}
|
||||
>
|
||||
<span class="w-10 shrink-0 text-right font-mono text-muted-foreground text-xs tabular-nums">{p.num}</span>
|
||||
<span class="truncate">{p.name}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
)
|
||||
}
|
||||
@@ -1,60 +1,49 @@
|
||||
---
|
||||
/**
|
||||
* 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.
|
||||
* The MDX `wrapper`: the badge row between a page's description and its body.
|
||||
* Blume's page template renders the title and description itself and then
|
||||
* the MDX content, with no slot in between, so wrapping the content is the
|
||||
* only way to land right after the description. MDX hands a wrapper no
|
||||
* route, so it is read off the URL, and the page's frontmatter is then
|
||||
* 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 only their content.
|
||||
*
|
||||
* - `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.
|
||||
* `tagTarget()` picks the route (and between two landings, e.g. DFS); the
|
||||
* `PageFooter` backlinks list reads the same answer.
|
||||
* - `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";
|
||||
import { stripBase, withBase } from "blume/components/islands/base-path.ts";
|
||||
import { tagTarget, type TopicTag } from "../topic-landings.ts";
|
||||
|
||||
interface Props {
|
||||
route: string;
|
||||
}
|
||||
|
||||
interface TopicTag {
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
const { route } = Astro.props;
|
||||
// `/sub/tree-dfs/invert-binary-tree/` (or `.../index.html` in a static build)
|
||||
// -> `/tree-dfs/invert-binary-tree`, the base-less shape of `data.routes`.
|
||||
const route =
|
||||
stripBase(import.meta.env.BASE_URL ?? "/", Astro.url.pathname)
|
||||
.replace(/(\/index)?\.html$/u, "")
|
||||
.replace(/(.)\/+$/u, "$1");
|
||||
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 served = new Set<string>(data.routes.map((r: { path: string }) => r.path));
|
||||
|
||||
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 target = tagTarget(tag, { route, tags }, served);
|
||||
const external = target.kind === "external";
|
||||
return { name: tag.name, href: external ? target.href : withBase(target.href), accent: target.kind === "landing", external };
|
||||
});
|
||||
|
||||
const linkClass = "no-underline transition-opacity hover:opacity-80";
|
||||
@@ -62,7 +51,7 @@ 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">
|
||||
<div class="not-prose mb-6 flex flex-wrap gap-1.5">
|
||||
{badges.map((b) => (
|
||||
<a
|
||||
href={b.href}
|
||||
@@ -90,3 +79,5 @@ const linkClass = "no-underline transition-opacity hover:opacity-80";
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<slot />
|
||||
@@ -1,3 +1,3 @@
|
||||
import { defineMeta } from "blume";
|
||||
|
||||
export default defineMeta({ title: "Phase 1: Linear Structures", order: 1 });
|
||||
export default defineMeta({ title: "1. Linear Structures", order: 1 });
|
||||
@@ -1,3 +1,3 @@
|
||||
import { defineMeta } from "blume";
|
||||
|
||||
export default defineMeta({ title: "Phase 2: Nodal & Grid", order: 2 });
|
||||
export default defineMeta({ title: "2. Nodal & Grid", order: 2 });
|
||||
@@ -1,3 +1,3 @@
|
||||
import { defineMeta } from "blume";
|
||||
|
||||
export default defineMeta({ title: "Phase 3: Hierarchical", order: 3 });
|
||||
export default defineMeta({ title: "3. Hierarchical", order: 3 });
|
||||
@@ -1,3 +1,3 @@
|
||||
import { defineMeta } from "blume";
|
||||
|
||||
export default defineMeta({ title: "Phase 4: Relational", order: 4 });
|
||||
export default defineMeta({ title: "4. Relational", order: 4 });
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: '134. Gas Station'
|
||||
description: There are n gas stations along a circular route, where the amount of gas at the i^th station is gas[i]
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Greedy", slug: "greedy" }
|
||||
sidebar:
|
||||
label: 'Gas Station'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
### Example 1:
|
||||
- Input: `gas = [1,2,3,4,5], cost = [3,4,5,1,2]`
|
||||
- Output: `3`
|
||||
- Explanation: Start at station `3` (index `3`) and fill up with `4` unit of `gas`. Your tank = `0 + 4 = 4` Travel to station `4`. Your tank = `4 - 1 + 5 = 8` Travel to station `0`. Your tank = `8 - 2 + 1 = 7` Travel to station `1`. Your tank = `7 - 3 + 2 = 6` Travel to station `2`. Your tank = `6 - 4 + 3 = 5` Travel to station `3`. The `cost` is `5`. Your `gas` is just enough to travel back to station `3`. Therefore, return `3` as the starting index.
|
||||
|
||||
### Example 2:
|
||||
- Input: `gas = [2,3,4], cost = [3,4,3]`
|
||||
- Output: `-1`
|
||||
- Explanation: You can't start at station `0` or `1`, as there is not enough `gas` to travel to the next station. Let's start at station `2` and fill up with `4` unit of `gas`. Your tank = `0 + 4 = 4` Travel to station `0`. Your tank = `4 - 3 + 2 = 3` Travel to station `1`. Your tank = `3 - 3 + 3 = 3` You cannot travel back to station `2`, as it requires `4` unit of `gas` but you only have `3`. Therefore, you can't travel around the circuit once no matter where you start.
|
||||
|
||||
### Constraints:
|
||||
|
||||
- `n == gas.length == cost.length`
|
||||
- `1 <= n <= 10^5`
|
||||
- `0 <= gas[i], cost[i] <= 10^4`
|
||||
- The input is generated such that the answer is unique.
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
class Solution:
|
||||
def canCompleteCircuit(self, gas: list[int], cost: list[int]) -> int:
|
||||
start, tank, total = 0, 0, 0
|
||||
|
||||
for i in range(len(gas)):
|
||||
drive = gas[i] - cost[i]
|
||||
tank = tank + drive
|
||||
|
||||
if tank < 0:
|
||||
tank = 0
|
||||
start = i + 1
|
||||
total = total + drive
|
||||
|
||||
return start if total >= 0 else -1
|
||||
```
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: '53. Maximum Subarray'
|
||||
description: Given an integer array nums, find the subarray with the largest sum, and return its sum
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Divide and Conquer", slug: "divide-and-conquer" }
|
||||
- { name: "Dynamic Programming", slug: "dynamic-programming" }
|
||||
sidebar:
|
||||
label: 'Maximum Subarray'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
::::warning
|
||||
If you have figured out the O(n) solution, try coding another solution using the divide and conquer approach, which is more subtle.
|
||||
::::
|
||||
|
||||
### Example 1:
|
||||
- Input: `nums = [-2,1,-3,4,-1,2,1,-5,4]`
|
||||
- Output: `6`
|
||||
- Explanation: The subarray `[4,-1,2,1]` has the largest sum `6`.
|
||||
|
||||
### Example 2:
|
||||
- Input: `nums = [1]`
|
||||
- Output: `1`
|
||||
- Explanation: The subarray `[1]` has the largest sum `1`.
|
||||
|
||||
### Example 3:
|
||||
- Input: `nums = [5,4,-1,7,8]`
|
||||
- Output: `23`
|
||||
- Explanation: The subarray `[5,4,-1,7,8]` has the largest sum `23`.
|
||||
|
||||
### Constraints:
|
||||
|
||||
- `1 <= nums.length <= 10^5`
|
||||
- `-10^4 <= nums[i] <= 10^4`
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
class Solution:
|
||||
def maxSubArray(self, nums: list[int]) -> int:
|
||||
best, total = nums[0], nums[0]
|
||||
|
||||
for i in range(1, len(nums)):
|
||||
if total < 0:
|
||||
total = nums[i]
|
||||
else:
|
||||
total = total + nums[i]
|
||||
best = max(best, total)
|
||||
|
||||
return best
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: '55. Jump Game'
|
||||
description: You are given an integer array nums. You are initially positioned at the array's first index, and each element in the array represents your maximum jump length at that position
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Dynamic Programming", slug: "dynamic-programming" }
|
||||
- { name: "Greedy", slug: "greedy" }
|
||||
sidebar:
|
||||
label: 'Jump Game'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
### Example 1:
|
||||
- Input: `nums = [2,3,1,1,4]`
|
||||
- Output: `true`
|
||||
- Explanation: Jump `1` step from index `0` to `1`, then `3` steps to the last index.
|
||||
|
||||
### Example 2:
|
||||
- Input: `nums = [3,2,1,0,4]`
|
||||
- Output: `false`
|
||||
- Explanation: You will always arrive at index `3` no matter what. Its maximum jump length is `0`, which makes it impossible to reach the last index.
|
||||
|
||||
### Constraints:
|
||||
|
||||
- `1 <= nums.length <= 10^4`
|
||||
- `0 <= nums[i] <= 10^5`
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
class Solution:
|
||||
def canJump(self, nums: list[int]) -> bool:
|
||||
best, last = nums[0], len(nums) - 1
|
||||
|
||||
for i in range(last):
|
||||
if i > best:
|
||||
return False
|
||||
else:
|
||||
pos = nums[i] + i
|
||||
best = max(best, pos)
|
||||
|
||||
return best >= last
|
||||
```
|
||||
@@ -3,3 +3,67 @@ title: Greedy
|
||||
description: 'The best local choice is made at each step and is not changed later.'
|
||||
hidden: true
|
||||
---
|
||||
|
||||
## When to use greedy algorithms?
|
||||
|
||||
- Solving optimization problems.
|
||||
- Ask to optimize some value and calculate it.
|
||||
- keywords (min/max, longest/shortest, largest/smallest, etc.)
|
||||
|
||||
## Optimization problems
|
||||
|
||||
1. Return the minimum number of swaps required to make s1 equal to s2.
|
||||
2. Return the minimum number of steps to make s and t anagrams of each other.
|
||||
3. Given three integers a, b, and c, return the longest possible happy string, if there are multiple such strings return any of them. if there is no such string, return empty string.
|
||||
|
||||
## What are greedy algorithms?
|
||||
|
||||
- Greedy algorithms follow this format:
|
||||
- identify what choices you have at every step
|
||||
- find the best choice
|
||||
- just do what looks best until done
|
||||
- assume that the best choice is the one that will lead to the best outcome
|
||||
|
||||
## How do we know greedy works?
|
||||
- Write out a formal proof
|
||||
- too complicated to write out
|
||||
- too much time
|
||||
- better to use intuition
|
||||
- try a bunch of ideas
|
||||
- find counterexamples
|
||||
|
||||
## Steps to use greedy algorithms
|
||||
|
||||
<Steps>
|
||||
<Step title="Identify the different steps and choices">
|
||||
- Choices have to be comparable
|
||||
- must determine if a choice is better than another
|
||||
</Step>
|
||||
<Step title="Check if greedy works">
|
||||
- try finding small cases where greedy is not optimal
|
||||
- if cases pass, try to deduce why greedy is optimal
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Why use greedy algorithms?
|
||||
|
||||
- simple to code
|
||||
- doesn't use complex algorithms (BFS, DFS, etc.)
|
||||
- runtime is fast, usually 0(n) or O(nlogn)
|
||||
- however, greedy algorithms are not always optimal
|
||||
|
||||
## The Greedy Skeleton
|
||||
|
||||
```python
|
||||
def greedy(items):
|
||||
# 1. Sort by the greedy key (deadline, end time, size, ratio, ...)
|
||||
items.sort(key=greedy_key)
|
||||
result = 0
|
||||
state = initial_state()
|
||||
for item in items:
|
||||
# 2. Take the item only if the locally best choice is valid
|
||||
if is_feasible(item, state):
|
||||
result += take(item, state)
|
||||
# 3. Correctness relies on an exchange argument, not just passing tests
|
||||
return result
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
import { defineMeta } from "blume";
|
||||
|
||||
export default defineMeta({ title: "Greedy", order: 23, icon: "coins", display: "group" });
|
||||
@@ -1,3 +1,3 @@
|
||||
import { defineMeta } from "blume";
|
||||
|
||||
export default defineMeta({ title: "Phase 5: Decision Space", order: 5 });
|
||||
export default defineMeta({ title: "5. Decision Space", order: 5 });
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Array"
|
||||
list: "https://leetcode.com/problem-list/array/"
|
||||
icon: list-ordered
|
||||
sidebar:
|
||||
badge: '38'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Binary Tree"
|
||||
list: "https://leetcode.com/problem-list/binary-tree/"
|
||||
icon: git-branch
|
||||
sidebar:
|
||||
badge: '9'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Directed Acyclic Graph"
|
||||
list: "https://leetcode.com/problem-list/directed-acyclic-graph/"
|
||||
icon: route
|
||||
sidebar:
|
||||
badge: '1'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Doubly-Linked List"
|
||||
list: "https://leetcode.com/problem-list/doubly-linked-list/"
|
||||
icon: link-2
|
||||
sidebar:
|
||||
badge: '1'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Graph Theory"
|
||||
list: "https://leetcode.com/problem-list/graph/"
|
||||
icon: share-2
|
||||
sidebar:
|
||||
badge: '5'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "K-D Tree"
|
||||
list: "https://leetcode.com/problem-list/k-d-tree/"
|
||||
icon: axis-3d
|
||||
sidebar:
|
||||
badge: '1'
|
||||
---
|
||||
|
||||
:::note
|
||||
@@ -0,0 +1,3 @@
|
||||
import { defineMeta } from "blume";
|
||||
|
||||
export default defineMeta({ title: "Data Structures", order: 1 });
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Queue"
|
||||
list: "https://leetcode.com/problem-list/queue/"
|
||||
icon: list-end
|
||||
sidebar:
|
||||
badge: '1'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "String"
|
||||
list: "https://leetcode.com/problem-list/string/"
|
||||
icon: quote
|
||||
sidebar:
|
||||
badge: '16'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Tree"
|
||||
list: "https://leetcode.com/problem-list/tree/"
|
||||
icon: tree-deciduous
|
||||
sidebar:
|
||||
badge: '9'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Bidirectional Search"
|
||||
list: "https://leetcode.com/problem-list/bidirectional-search/"
|
||||
icon: move-horizontal
|
||||
sidebar:
|
||||
badge: '1'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Binary Lifting"
|
||||
list: "https://leetcode.com/problem-list/binary-lifting/"
|
||||
icon: chevrons-up
|
||||
sidebar:
|
||||
badge: '2'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Bit Manipulation"
|
||||
list: "https://leetcode.com/problem-list/bit-manipulation/"
|
||||
icon: binary
|
||||
sidebar:
|
||||
badge: '1'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Bracket Sequences"
|
||||
list: "https://leetcode.com/problem-list/bracket-sequences/"
|
||||
icon: parentheses
|
||||
sidebar:
|
||||
badge: '1'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Bucket Sort"
|
||||
list: "https://leetcode.com/problem-list/bucket-sort/"
|
||||
icon: container
|
||||
sidebar:
|
||||
badge: '2'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Complete Knapsack"
|
||||
list: "https://leetcode.com/problem-list/complete-knapsack/"
|
||||
icon: package-plus
|
||||
sidebar:
|
||||
badge: '1'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Divide and Conquer"
|
||||
list: "https://leetcode.com/problem-list/divide-and-conquer/"
|
||||
icon: split
|
||||
sidebar:
|
||||
badge: '5'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Floyd's Cycle Finding Algorithm"
|
||||
list: "https://leetcode.com/problem-list/floyds-cycle-finding-algorithm/"
|
||||
icon: repeat
|
||||
sidebar:
|
||||
badge: '1'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Knapsack Problem"
|
||||
list: "https://leetcode.com/problem-list/knapsack-problem/"
|
||||
icon: backpack
|
||||
sidebar:
|
||||
badge: '1'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Longest Common Subsequence"
|
||||
list: "https://leetcode.com/problem-list/longest-common-subsequence/"
|
||||
icon: diff
|
||||
sidebar:
|
||||
badge: '1'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Lowest Common Ancestor"
|
||||
list: "https://leetcode.com/problem-list/lowest-common-ancestor/"
|
||||
icon: git-merge
|
||||
sidebar:
|
||||
badge: '2'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Memoization"
|
||||
list: "https://leetcode.com/problem-list/memoization/"
|
||||
icon: database
|
||||
sidebar:
|
||||
badge: '1'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Merge Sort"
|
||||
list: "https://leetcode.com/problem-list/merge-sort/"
|
||||
icon: blend
|
||||
sidebar:
|
||||
badge: '1'
|
||||
---
|
||||
|
||||
:::note
|
||||
@@ -0,0 +1,3 @@
|
||||
import { defineMeta } from "blume";
|
||||
|
||||
export default defineMeta({ title: "Algorithms", order: 2 });
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Quickselect"
|
||||
list: "https://leetcode.com/problem-list/quickselect/"
|
||||
icon: crosshair
|
||||
sidebar:
|
||||
badge: '3'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Recursion"
|
||||
list: "https://leetcode.com/problem-list/recursion/"
|
||||
icon: iteration-ccw
|
||||
sidebar:
|
||||
badge: '3'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Simulation"
|
||||
list: "https://leetcode.com/problem-list/simulation/"
|
||||
icon: monitor-play
|
||||
sidebar:
|
||||
badge: '1'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Sorting"
|
||||
list: "https://leetcode.com/problem-list/sorting/"
|
||||
icon: arrow-down-wide-narrow
|
||||
sidebar:
|
||||
badge: '10'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Tournament Sort"
|
||||
list: "https://leetcode.com/problem-list/tournament-sort/"
|
||||
icon: trophy
|
||||
sidebar:
|
||||
badge: '1'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Combinatorics"
|
||||
list: "https://leetcode.com/problem-list/combinatorics/"
|
||||
icon: dices
|
||||
sidebar:
|
||||
badge: '1'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Counting"
|
||||
list: "https://leetcode.com/problem-list/counting/"
|
||||
icon: tally-5
|
||||
sidebar:
|
||||
badge: '3'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Design"
|
||||
list: "https://leetcode.com/problem-list/design/"
|
||||
icon: pencil-ruler
|
||||
sidebar:
|
||||
badge: '5'
|
||||
---
|
||||
|
||||
:::note
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Geometry"
|
||||
list: "https://leetcode.com/problem-list/geometry/"
|
||||
icon: shapes
|
||||
sidebar:
|
||||
badge: '1'
|
||||
---
|
||||
|
||||
:::note
|
||||
@@ -1,6 +1,9 @@
|
||||
---
|
||||
title: "Math"
|
||||
list: "https://leetcode.com/problem-list/math/"
|
||||
icon: calculator
|
||||
sidebar:
|
||||
badge: '6'
|
||||
---
|
||||
|
||||
:::note
|
||||
@@ -0,0 +1,3 @@
|
||||
import { defineMeta } from "blume";
|
||||
|
||||
export default defineMeta({ title: "Techniques", order: 3 });
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -15,7 +15,9 @@ export default [
|
||||
{ from: "/46", to: "/backtracking/permutations" },
|
||||
{ from: "/48", to: "/matrix-index-math/rotate-image" },
|
||||
{ from: "/49", to: "/hash-table/group-anagrams" },
|
||||
{ from: "/53", to: "/greedy/maximum-subarray" },
|
||||
{ from: "/54", to: "/matrix-index-math/spiral-matrix" },
|
||||
{ from: "/55", to: "/greedy/jump-game" },
|
||||
{ from: "/62", to: "/multi-d-grid-dp/unique-paths" },
|
||||
{ from: "/64", to: "/multi-d-grid-dp/minimum-path-sum" },
|
||||
{ from: "/70", to: "/1d-dynamic-programming/climbing-stairs" },
|
||||
@@ -29,6 +31,7 @@ export default [
|
||||
{ from: "/125", to: "/two-pointers/valid-palindrome" },
|
||||
{ from: "/127", to: "/graph-bfs/word-ladder" },
|
||||
{ from: "/133", to: "/graph-dfs/clone-graph" },
|
||||
{ from: "/134", to: "/greedy/gas-station" },
|
||||
{ from: "/141", to: "/linked-list/linked-list-cycle" },
|
||||
{ from: "/146", to: "/hybrid-structures/lru-cache" },
|
||||
{ from: "/150", to: "/stack/evaluate-reverse-polish-notation" },
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/**
|
||||
* LeetCode topic-tag slug → the campaign landing routes that teach it, in
|
||||
* default preference order. The one answer to "does the campaign cover this
|
||||
* tag", read by two sides that must agree:
|
||||
* tag", read by the sides that must agree:
|
||||
*
|
||||
* - `components/ProblemBadges.astro` links a covered tag's badge to its
|
||||
* landing page, and every other tag's badge to `/guides/<slug>`;
|
||||
* - `components/BacklinksList.astro` lists, under a landing or guide page,
|
||||
* every problem whose badge links there — both via `tagTarget()`;
|
||||
* - `apps/cli/sync.ts` creates a placeholder `content/guides/<slug>.mdx` for
|
||||
* every tag a problem page carries that is NOT a key here.
|
||||
*
|
||||
@@ -33,3 +35,33 @@ export const TOPIC_LANDINGS: Record<string, readonly string[]> = {
|
||||
"dynamic-programming": ["/1d-dynamic-programming", "/multi-d-grid-dp"],
|
||||
greedy: ["/greedy"],
|
||||
};
|
||||
|
||||
/** One of LeetCode's topic tags, as a problem page's `topics:` frontmatter carries it. */
|
||||
export interface TopicTag {
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a problem page's badge for `tag` points: a root-relative landing or
|
||||
* guide route (the caller adds the base), or LeetCode's problem list when the
|
||||
* site serves neither, so a badge never links to a 404.
|
||||
*
|
||||
* A tag naming two landings 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.
|
||||
*/
|
||||
export function tagTarget(
|
||||
tag: TopicTag,
|
||||
page: { route: string; tags: readonly TopicTag[] },
|
||||
served: ReadonlySet<string>,
|
||||
): { href: string; kind: "landing" | "guide" | "external" } {
|
||||
const landings = (TOPIC_LANDINGS[tag.slug] ?? []).filter((path) => served.has(path));
|
||||
const own = `/${page.route.split("/")[1]}`;
|
||||
const prefer = page.tags.some((t) => t.slug === "tree") ? "/tree-" : "/graph-";
|
||||
const landing = landings.includes(own) ? own : (landings.find((p) => p.startsWith(prefer)) ?? landings[0]);
|
||||
if (landing) return { href: landing, kind: "landing" };
|
||||
const guide = `/guides/${tag.slug}`;
|
||||
if (served.has(guide)) return { href: guide, kind: "guide" };
|
||||
return { href: `https://leetcode.com/problem-list/${tag.slug}/`, kind: "external" };
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
.DS_Store
|
||||
.env
|
||||
|
||||
# Ignore the output video from Git but not videos you import into src/.
|
||||
out
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"useTabs": false,
|
||||
"bracketSpacing": true,
|
||||
"tabWidth": 2
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
# Remotion video
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/remotion-dev/logo">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/remotion-dev/logo/raw/main/animated-logo-banner-dark.apng">
|
||||
<img alt="Animated Remotion Logo" src="https://github.com/remotion-dev/logo/raw/main/animated-logo-banner-light.gif">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Welcome to your Remotion project!
|
||||
|
||||
## Commands
|
||||
|
||||
**Install Dependencies**
|
||||
|
||||
```console
|
||||
npm i
|
||||
```
|
||||
|
||||
**Start Preview**
|
||||
|
||||
```console
|
||||
npm run dev
|
||||
```
|
||||
|
||||
**Render video**
|
||||
|
||||
```console
|
||||
npx remotion render
|
||||
```
|
||||
|
||||
**Upgrade Remotion**
|
||||
|
||||
```console
|
||||
npx remotion upgrade
|
||||
```
|
||||
|
||||
## Docs
|
||||
|
||||
Get started with Remotion by reading the [fundamentals page](https://www.remotion.dev/docs/the-fundamentals).
|
||||
|
||||
## Help
|
||||
|
||||
We provide help on our [Discord server](https://discord.gg/6VzzNDwUwV).
|
||||
|
||||
## Issues
|
||||
|
||||
Found an issue with Remotion? [File an issue here](https://github.com/remotion-dev/remotion/issues/new).
|
||||
|
||||
## License
|
||||
|
||||
Note that for some entities a company license is needed. [Read the terms here](https://github.com/remotion-dev/remotion/blob/main/LICENSE.md).
|
||||
@@ -1,3 +0,0 @@
|
||||
import { config } from "@remotion/eslint-config-flat";
|
||||
|
||||
export default config;
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "videos",
|
||||
"version": "1.0.0",
|
||||
"description": "My Remotion video",
|
||||
"repository": {},
|
||||
"license": "UNLICENSED",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@remotion/cli": "4.0.528",
|
||||
"@remotion/zod-types": "4.0.528",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"remotion": "4.0.528",
|
||||
"zod": "4.5.4",
|
||||
"@remotion/tailwind-v4": "4.0.528",
|
||||
"tailwindcss": "4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remotion/eslint-config-flat": "4.0.528",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/web": "0.0.166",
|
||||
"eslint": "9.19.0",
|
||||
"prettier": "3.8.1",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "remotion studio",
|
||||
"build": "remotion bundle",
|
||||
"upgrade": "remotion upgrade",
|
||||
"lint": "eslint src && tsc"
|
||||
},
|
||||
"sideEffects": [
|
||||
"*.css"
|
||||
]
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// See all configuration options: https://remotion.dev/docs/config
|
||||
// Each option also is available as a CLI flag: https://remotion.dev/docs/cli
|
||||
|
||||
// Note: When using the Node.JS APIs, the config file doesn't apply. Instead, pass options directly to the APIs
|
||||
|
||||
import { Config } from "@remotion/cli/config";
|
||||
import { enableTailwind } from '@remotion/tailwind-v4';
|
||||
|
||||
Config.setRspack(true);
|
||||
Config.setVideoImageFormat("jpeg");
|
||||
Config.setOverwriteOutput(true);
|
||||
Config.overrideBundlerConfig(enableTailwind);
|
||||
@@ -1,74 +0,0 @@
|
||||
import {
|
||||
AbsoluteFill,
|
||||
interpolate,
|
||||
Sequence,
|
||||
spring,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
} from "remotion";
|
||||
import { Logo } from "./HelloWorld/Logo";
|
||||
import { Subtitle } from "./HelloWorld/Subtitle";
|
||||
import { Title } from "./HelloWorld/Title";
|
||||
|
||||
export type HelloWorldProps = {
|
||||
readonly titleText: string;
|
||||
readonly titleColor: string;
|
||||
readonly logoColor1: string;
|
||||
readonly logoColor2: string;
|
||||
};
|
||||
|
||||
export const HelloWorld: React.FC<HelloWorldProps> = ({
|
||||
titleText: propOne,
|
||||
titleColor: propTwo,
|
||||
logoColor1,
|
||||
logoColor2,
|
||||
}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { durationInFrames, fps } = useVideoConfig();
|
||||
|
||||
// Animate from 0 to 1 after 25 frames
|
||||
const logoTranslationProgress = spring({
|
||||
frame: frame - 25,
|
||||
fps,
|
||||
config: {
|
||||
damping: 100,
|
||||
},
|
||||
});
|
||||
|
||||
// Move the logo up by 150 pixels once the transition starts
|
||||
const logoTranslation = interpolate(
|
||||
logoTranslationProgress,
|
||||
[0, 1],
|
||||
[0, -150],
|
||||
);
|
||||
|
||||
// Fade out the animation at the end
|
||||
const opacity = interpolate(
|
||||
frame,
|
||||
[durationInFrames - 25, durationInFrames - 15],
|
||||
[1, 0],
|
||||
{
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
},
|
||||
);
|
||||
|
||||
// A <AbsoluteFill> is just a absolutely positioned <div>!
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "white" }}>
|
||||
<AbsoluteFill style={{ opacity }}>
|
||||
<AbsoluteFill style={{ transform: `translateY(${logoTranslation}px)` }}>
|
||||
<Logo logoColor1={logoColor1} logoColor2={logoColor2} />
|
||||
</AbsoluteFill>
|
||||
{/* Sequences can shift the time for its children! */}
|
||||
<Sequence from={35}>
|
||||
<Title titleText={propOne} titleColor={propTwo} />
|
||||
</Sequence>
|
||||
{/* The subtitle will only enter on the 75th frame. */}
|
||||
<Sequence from={75}>
|
||||
<Subtitle />
|
||||
</Sequence>
|
||||
</AbsoluteFill>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { random, useVideoConfig } from "remotion";
|
||||
|
||||
const getCircumferenceOfArc = (rx: number, ry: number) => {
|
||||
return Math.PI * 2 * Math.sqrt((rx * rx + ry * ry) / 2);
|
||||
};
|
||||
|
||||
const rx = 135;
|
||||
const ry = 300;
|
||||
const cx = 960;
|
||||
const cy = 540;
|
||||
const arcLength = getCircumferenceOfArc(rx, ry);
|
||||
const strokeWidth = 30;
|
||||
|
||||
export const Arc: React.FC<{
|
||||
progress: number;
|
||||
rotation: number;
|
||||
rotateProgress: number;
|
||||
color1: string;
|
||||
color2: string;
|
||||
}> = ({ progress, rotation, rotateProgress, color1, color2 }) => {
|
||||
const { width, height } = useVideoConfig();
|
||||
|
||||
// Each svg Id must be unique to not conflict with each other
|
||||
const [gradientId] = useState(() => String(random(null)));
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
style={{
|
||||
position: "absolute",
|
||||
transform: `rotate(${rotation * rotateProgress}deg)`,
|
||||
}}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={gradientId} x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stopColor={color1} />
|
||||
<stop offset="100%" stopColor={color2} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<ellipse
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
rx={rx}
|
||||
ry={ry}
|
||||
fill="none"
|
||||
stroke={`url(#${gradientId})`}
|
||||
strokeDasharray={arcLength}
|
||||
strokeDashoffset={arcLength - arcLength * progress}
|
||||
strokeLinecap="round"
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { random, useVideoConfig } from "remotion";
|
||||
|
||||
export const Atom: React.FC<{
|
||||
scale: number;
|
||||
color1: string;
|
||||
color2: string;
|
||||
}> = ({ scale, color1, color2 }) => {
|
||||
const config = useVideoConfig();
|
||||
|
||||
// Each SVG ID must be unique to not conflict with each other
|
||||
const [gradientId] = useState(() => String(random(null)));
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox={`0 0 ${config.width} ${config.height}`}
|
||||
style={{
|
||||
position: "absolute",
|
||||
transform: `scale(${scale})`,
|
||||
}}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor={color1} />
|
||||
<stop offset="100%" stopColor={color2} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle
|
||||
r={70}
|
||||
cx={config.width / 2}
|
||||
cy={config.height / 2}
|
||||
fill={`url(#${gradientId})`}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
import {
|
||||
AbsoluteFill,
|
||||
interpolate,
|
||||
spring,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
} from "remotion";
|
||||
import { Arc } from "./Arc";
|
||||
import { Atom } from "./Atom";
|
||||
|
||||
export type LogoProps = {
|
||||
readonly logoColor1: string;
|
||||
readonly logoColor2: string;
|
||||
};
|
||||
|
||||
export const Logo: React.FC<LogoProps> = ({
|
||||
logoColor1: color1,
|
||||
logoColor2: color2,
|
||||
}) => {
|
||||
const videoConfig = useVideoConfig();
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
const development = spring({
|
||||
config: {
|
||||
damping: 100,
|
||||
mass: 0.5,
|
||||
},
|
||||
fps: videoConfig.fps,
|
||||
frame,
|
||||
});
|
||||
|
||||
const rotationDevelopment = spring({
|
||||
config: {
|
||||
damping: 100,
|
||||
mass: 0.5,
|
||||
},
|
||||
fps: videoConfig.fps,
|
||||
frame,
|
||||
});
|
||||
|
||||
const scale = spring({
|
||||
frame,
|
||||
config: {
|
||||
mass: 0.5,
|
||||
},
|
||||
fps: videoConfig.fps,
|
||||
});
|
||||
|
||||
const logoRotation = interpolate(
|
||||
frame,
|
||||
[0, videoConfig.durationInFrames],
|
||||
[0, 360],
|
||||
);
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
transform: `scale(${scale}) rotate(${logoRotation}deg)`,
|
||||
}}
|
||||
>
|
||||
<Arc
|
||||
rotateProgress={rotationDevelopment}
|
||||
progress={development}
|
||||
rotation={30}
|
||||
color1={color1}
|
||||
color2={color2}
|
||||
/>
|
||||
<Arc
|
||||
rotateProgress={rotationDevelopment}
|
||||
rotation={90}
|
||||
progress={development}
|
||||
color1={color1}
|
||||
color2={color2}
|
||||
/>
|
||||
<Arc
|
||||
rotateProgress={rotationDevelopment}
|
||||
rotation={-30}
|
||||
progress={development}
|
||||
color1={color1}
|
||||
color2={color2}
|
||||
/>
|
||||
<Atom scale={rotationDevelopment} color1={color1} color2={color2} />
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import React from "react";
|
||||
import { interpolate, useCurrentFrame } from "remotion";
|
||||
import { COLOR_1, FONT_FAMILY } from "./constants";
|
||||
|
||||
const subtitle: React.CSSProperties = {
|
||||
fontFamily: FONT_FAMILY,
|
||||
fontSize: 40,
|
||||
textAlign: "center",
|
||||
position: "absolute",
|
||||
bottom: 140,
|
||||
width: "100%",
|
||||
};
|
||||
|
||||
const codeStyle: React.CSSProperties = {
|
||||
color: COLOR_1,
|
||||
};
|
||||
|
||||
export const Subtitle: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const opacity = interpolate(frame, [0, 30], [0, 1]);
|
||||
return (
|
||||
<div style={{ ...subtitle, opacity }}>
|
||||
Edit <code style={codeStyle}>src/Root.tsx</code> and save to reload.
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
import React from "react";
|
||||
import { spring, useCurrentFrame, useVideoConfig } from "remotion";
|
||||
import { FONT_FAMILY } from "./constants";
|
||||
|
||||
const title: React.CSSProperties = {
|
||||
fontFamily: FONT_FAMILY,
|
||||
fontWeight: "bold",
|
||||
fontSize: 100,
|
||||
textAlign: "center",
|
||||
position: "absolute",
|
||||
bottom: 160,
|
||||
width: "100%",
|
||||
};
|
||||
|
||||
const word: React.CSSProperties = {
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
display: "inline-block",
|
||||
};
|
||||
|
||||
export const Title: React.FC<{
|
||||
readonly titleText: string;
|
||||
readonly titleColor: string;
|
||||
}> = ({ titleText, titleColor }) => {
|
||||
const videoConfig = useVideoConfig();
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
const words = titleText.split(" ");
|
||||
|
||||
return (
|
||||
<h1 style={title}>
|
||||
{words.map((t, i) => {
|
||||
const delay = i * 5;
|
||||
|
||||
const scale = spring({
|
||||
fps: videoConfig.fps,
|
||||
frame: frame - delay,
|
||||
config: {
|
||||
damping: 200,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<span
|
||||
key={t}
|
||||
style={{
|
||||
...word,
|
||||
color: titleColor,
|
||||
transform: `scale(${scale})`,
|
||||
}}
|
||||
>
|
||||
{t}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</h1>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
// Change any of these to update your video live.
|
||||
|
||||
export const COLOR_1 = "#86A8E7";
|
||||
|
||||
export const FONT_FAMILY = "SF Pro Text, Helvetica, Arial, sans-serif";
|
||||
@@ -1,45 +0,0 @@
|
||||
import "./index.css";
|
||||
import { Composition } from "remotion";
|
||||
import { HelloWorld } from "./HelloWorld";
|
||||
import { Logo } from "./HelloWorld/Logo";
|
||||
|
||||
// Each <Composition> is an entry in the sidebar!
|
||||
|
||||
export const RemotionRoot: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Composition
|
||||
// You can take the "id" to render a video:
|
||||
// npx remotion render HelloWorld
|
||||
id="HelloWorld"
|
||||
component={HelloWorld}
|
||||
durationInFrames={150}
|
||||
fps={30}
|
||||
width={1920}
|
||||
height={1080}
|
||||
// You can override these props for each render:
|
||||
// https://www.remotion.dev/docs/parametrized-rendering
|
||||
defaultProps={{
|
||||
titleText: "Welcome to Remotion",
|
||||
titleColor: "#000000",
|
||||
logoColor1: "#91EAE4",
|
||||
logoColor2: "#86A8E7",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Mount any React component to make it show up in the sidebar and work on it individually! */}
|
||||
<Composition
|
||||
id="OnlyLogo"
|
||||
component={Logo}
|
||||
durationInFrames={150}
|
||||
fps={30}
|
||||
width={1920}
|
||||
height={1080}
|
||||
defaultProps={{
|
||||
logoColor1: "#91dAE2",
|
||||
logoColor2: "#86A8E7",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
@@ -1,7 +0,0 @@
|
||||
// This is your entry file! Refer to it when you render:
|
||||
// npx remotion render <entry-file> HelloWorld out/video.mp4
|
||||
|
||||
import { registerRoot } from "remotion";
|
||||
import { RemotionRoot } from "./Root";
|
||||
|
||||
registerRoot(RemotionRoot);
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2018",
|
||||
"module": "Preserve",
|
||||
"moduleResolution": "Bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"lib": ["es2015"],
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noUnusedLocals": true
|
||||
},
|
||||
"exclude": ["remotion.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
134. Gas Station
|
||||
Difficulty: Medium
|
||||
https://leetcode.com/problems/gas-station/
|
||||
|
||||
──────────────────────────────────────────────────
|
||||
|
||||
There are n gas stations along a circular route, where the amount of
|
||||
gas at the i^th station is gas[i].
|
||||
|
||||
You have a car with an unlimited gas tank and it costs cost[i] of gas
|
||||
to travel from the i^th station to its next (i + 1)^th station. You
|
||||
begin the journey with an empty tank at one of the gas stations.
|
||||
|
||||
Given two integer arrays gas and cost, return the starting gas
|
||||
station's index if you can travel around the circuit once in the
|
||||
clockwise direction, otherwise return -1. If there exists a solution,
|
||||
it is guaranteed to be unique.
|
||||
|
||||
|
||||
|
||||
Example 1:
|
||||
|
||||
Input: gas = [1,2,3,4,5], cost = [3,4,5,1,2]
|
||||
Output: 3
|
||||
Explanation:
|
||||
Start at station 3 (index 3) and fill up with 4 unit of gas. Your
|
||||
tank = 0 + 4 = 4
|
||||
Travel to station 4. Your tank = 4 - 1 + 5 = 8
|
||||
Travel to station 0. Your tank = 8 - 2 + 1 = 7
|
||||
Travel to station 1. Your tank = 7 - 3 + 2 = 6
|
||||
Travel to station 2. Your tank = 6 - 4 + 3 = 5
|
||||
Travel to station 3. The cost is 5. Your gas is just enough to travel
|
||||
back to station 3.
|
||||
Therefore, return 3 as the starting index.
|
||||
|
||||
Example 2:
|
||||
|
||||
Input: gas = [2,3,4], cost = [3,4,3]
|
||||
Output: -1
|
||||
Explanation:
|
||||
You can't start at station 0 or 1, as there is not enough gas to
|
||||
travel to the next station.
|
||||
Let's start at station 2 and fill up with 4 unit of gas. Your tank =
|
||||
0 + 4 = 4
|
||||
Travel to station 0. Your tank = 4 - 3 + 2 = 3
|
||||
Travel to station 1. Your tank = 3 - 3 + 3 = 3
|
||||
You cannot travel back to station 2, as it requires 4 unit of gas but
|
||||
you only have 3.
|
||||
Therefore, you can't travel around the circuit once no matter where
|
||||
you start.
|
||||
|
||||
|
||||
|
||||
Constraints:
|
||||
|
||||
• n == gas.length == cost.length
|
||||
|
||||
• 1 <= n <= 10^5
|
||||
|
||||
• 0 <= gas[i], cost[i] <= 10^4
|
||||
|
||||
• The input is generated such that the answer is unique.
|
||||
"""
|
||||
|
||||
|
||||
class Solution:
|
||||
def canCompleteCircuit(self, gas: list[int], cost: list[int]) -> int:
|
||||
start, tank, total = 0, 0, 0
|
||||
|
||||
for i in range(len(gas)):
|
||||
drive = gas[i] - cost[i]
|
||||
tank = tank + drive
|
||||
|
||||
if tank < 0:
|
||||
tank = 0
|
||||
start = i + 1
|
||||
total = total + drive
|
||||
|
||||
return start if total >= 0 else -1
|
||||
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
53. Maximum Subarray
|
||||
Difficulty: Medium
|
||||
https://leetcode.com/problems/maximum-subarray/
|
||||
|
||||
──────────────────────────────────────────────────
|
||||
|
||||
Given an integer array nums, find the subarray with the largest sum,
|
||||
and return its sum.
|
||||
|
||||
|
||||
|
||||
Example 1:
|
||||
|
||||
Input: nums = [-2,1,-3,4,-1,2,1,-5,4]
|
||||
Output: 6
|
||||
Explanation: The subarray [4,-1,2,1] has the largest sum 6.
|
||||
|
||||
Example 2:
|
||||
|
||||
Input: nums = [1]
|
||||
Output: 1
|
||||
Explanation: The subarray [1] has the largest sum 1.
|
||||
|
||||
Example 3:
|
||||
|
||||
Input: nums = [5,4,-1,7,8]
|
||||
Output: 23
|
||||
Explanation: The subarray [5,4,-1,7,8] has the largest sum 23.
|
||||
|
||||
|
||||
|
||||
Constraints:
|
||||
|
||||
• 1 <= nums.length <= 10^5
|
||||
|
||||
• -10^4 <= nums[i] <= 10^4
|
||||
|
||||
|
||||
|
||||
Follow up: If you have figured out the O(n) solution, try coding
|
||||
another solution using the divide and conquer approach, which is more
|
||||
subtle.
|
||||
"""
|
||||
|
||||
|
||||
class Solution:
|
||||
def maxSubArray(self, nums: list[int]) -> int:
|
||||
best, total = nums[0], nums[0]
|
||||
|
||||
for i in range(1, len(nums)):
|
||||
if total < 0:
|
||||
total = nums[i]
|
||||
else:
|
||||
total = total + nums[i]
|
||||
best = max(best, total)
|
||||
|
||||
return best
|
||||
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
55. Jump Game
|
||||
Difficulty: Medium
|
||||
https://leetcode.com/problems/jump-game/
|
||||
|
||||
──────────────────────────────────────────────────
|
||||
|
||||
You are given an integer array nums. You are initially positioned at
|
||||
the array's first index, and each element in the array represents your
|
||||
maximum jump length at that position.
|
||||
|
||||
Return true if you can reach the last index, or false otherwise.
|
||||
|
||||
|
||||
|
||||
Example 1:
|
||||
|
||||
Input: nums = [2,3,1,1,4]
|
||||
Output: true
|
||||
Explanation: Jump 1 step from index 0 to 1, then 3 steps to the last
|
||||
index.
|
||||
|
||||
Example 2:
|
||||
|
||||
Input: nums = [3,2,1,0,4]
|
||||
Output: false
|
||||
Explanation: You will always arrive at index 3 no matter what. Its
|
||||
maximum jump length is 0, which makes it impossible to reach the last
|
||||
index.
|
||||
|
||||
|
||||
|
||||
Constraints:
|
||||
|
||||
• 1 <= nums.length <= 10^4
|
||||
|
||||
• 0 <= nums[i] <= 10^5
|
||||
"""
|
||||
|
||||
|
||||
class Solution:
|
||||
def canJump(self, nums: list[int]) -> bool:
|
||||
best, last = nums[0], len(nums) - 1
|
||||
|
||||
for i in range(last):
|
||||
if i > best:
|
||||
return False
|
||||
else:
|
||||
pos = nums[i] + i
|
||||
best = max(best, pos)
|
||||
|
||||
return best >= last
|
||||
Reference in new issue
Block a user