diff --git a/apps/docs/components.ts b/apps/docs/components.ts index d0cfbb0..1bd3c9c 100644 --- a/apps/docs/components.ts +++ b/apps/docs/components.ts @@ -13,5 +13,7 @@ export default defineComponents({ 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", }, }); diff --git a/apps/docs/components/BacklinksList.astro b/apps/docs/components/BacklinksList.astro new file mode 100644 index 0000000..eb7d976 --- /dev/null +++ b/apps/docs/components/BacklinksList.astro @@ -0,0 +1,84 @@ +--- +/** + * The `PageFooter` slot: an Obsidian-style backlinks list under a topic landing + * or guide page — every problem page that points at it, as ` ` + * 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 `. ` title, the shape + * `apps/cli/sync.ts` writes. + */ +import { getEntry } from "astro:content"; +import data from "blume:data"; +import { withBase } from "blume/components/islands/base-path.ts"; +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 && ( +
+ + +
+ ) +} diff --git a/apps/docs/components/ProblemBadges.astro b/apps/docs/components/ProblemBadges.astro index 4d7008a..6d0bbfb 100644 --- a/apps/docs/components/ProblemBadges.astro +++ b/apps/docs/components/ProblemBadges.astro @@ -10,33 +10,23 @@ * 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/` (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 { tagTarget, type TopicTag } 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; @@ -44,17 +34,12 @@ 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(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"; diff --git a/apps/docs/content/(05-decision-space)/greedy/134-gas-station.mdx b/apps/docs/content/(05-decision-space)/greedy/134-gas-station.mdx new file mode 100644 index 0000000..58b6b89 --- /dev/null +++ b/apps/docs/content/(05-decision-space)/greedy/134-gas-station.mdx @@ -0,0 +1,35 @@ +--- +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: +``` diff --git a/apps/docs/topic-landings.ts b/apps/docs/topic-landings.ts index 87a9c33..e8310ab 100644 --- a/apps/docs/topic-landings.ts +++ b/apps/docs/topic-landings.ts @@ -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/`; + * - `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/.mdx` for * every tag a problem page carries that is NOT a key here. * @@ -33,3 +35,33 @@ export const TOPIC_LANDINGS: Record = { "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, +): { 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" }; +}