mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 13:12:44 +00:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc29304903 | ||
|
|
c50b600103 | ||
|
|
a86d72c7db | ||
|
|
e59d78e571 | ||
|
|
a93bb068b6 | ||
|
|
53b2d5ec8c | ||
|
|
5dda64e41e | ||
|
|
d798871fbc | ||
|
|
bb7324fe8c | ||
|
|
d13859d31e | ||
|
|
664ea534ed | ||
|
|
b86f36daf1 | ||
|
|
7bae7b2920 | ||
|
|
64173857e7 | ||
|
|
002c2c9c97 | ||
|
|
fa8deac963 | ||
|
|
ef8d2b41e1 |
No files matched your search
@@ -23,6 +23,7 @@ flowchart LR
|
||||
D1 -->|Sat midnight ET| Review[Review — Week N issue]
|
||||
PI -->|"spaced-repetition.yml (6 AM ET cron, closed +3d/+7d)"| SR[Spaced Repetition — Month D issue]
|
||||
D1 --> Charts[SVG charts + /api/stats]
|
||||
Charts -->|bun run stats| TUI[terminal dashboard]
|
||||
```
|
||||
|
||||
- **Reconcile, don't react.** `close-solved.ts`, `close-topics.ts`, and the Worker's catalog sync recompute desired state from scratch each run: re-runs are no-ops, backfills need no special casing, closing is one-directional. `close-solved` posts its *whole* implemented set to `/admin/solved`, not just the issues it closed this run.
|
||||
@@ -56,6 +57,10 @@ bun run pick # scaffold a solution via leetcode-cli (fuzzy picker; Ta
|
||||
# re-solves → work/3, work/7. Each section hides what its own bucket
|
||||
# already holds; index+descriptions cached in
|
||||
# ~/.local/share/leetcode/data.db — see apps/cli/db.ts)
|
||||
bun run stats # terminal dashboard over GET /api/stats — four panes (Overview,
|
||||
# Ladder, Concepts, Activity); Tab/1-4 switches, ↑↓ scrolls,
|
||||
# r refetches, q quits. Outside a TTY it prints every pane once.
|
||||
# SRS_API points it at a local Worker like close-solved does.
|
||||
bun run test # run ONE solution against LeetCode's judge (not a test suite;
|
||||
# most recently tested first, stamped in the db on exit 0)
|
||||
bun run submit # submit ONE solution to LeetCode (newest scaffold first, nothing
|
||||
@@ -92,12 +97,13 @@ curl -X POST -H "Authorization: Bearer $LINK_KEY" \
|
||||
|
||||
## Important Files
|
||||
|
||||
- `apps/api/src/srs.ts` — SRS domain: the ladder (`new → +3 → +7 → retired`; fail resets to `+3`; first-ever log enters at `+3`), `WINDOWS`/`STAGES` (the one vocabulary shared by the charts, the `work/<n>` buckets and the review issues), ET date math, `workingDay()` (no scheduled date is ever a Sunday), seeded sampling, `logAttempt()` (the ONE write path — email taps, webhook, gate scoring all converge here), and overload leveling: at most `REVIEW_CAP` (3) reviews surface per day — `levelReviews()` (run by `sendDigest` on real sends only, never dry) gives everything past the cap a concrete future WORKING day, ≤ 3 per day, oldest first, instead of letting the due pile grow.
|
||||
- `apps/api/src/srs.ts` — SRS domain: the ladder (`new → +3 → +7 → retired`; fail resets to `+3`; first-ever log enters at `+3`), `WINDOWS`/`STAGES`/`TEMPERATURES`/`DIFFICULTIES`/`SETS` (the one vocabulary shared by the charts, the `work/<n>` buckets, the review issues and the stats dashboard), ET date math, `workingDay()` (no scheduled date is ever a Sunday), `temperatureOf()` (days since a concept was last exercised → `hot`/`fresh`/`fading`/`cold`, bands derived from `WINDOWS` so they cannot drift; read-side only, nothing is scheduled off it), seeded sampling, `logAttempt()` (the ONE write path — email taps, webhook, gate scoring all converge here), and overload leveling: at most `REVIEW_CAP` (3) reviews surface per day — `levelReviews()` (run by `sendDigest` on real sends only, never dry) gives everything past the cap a concrete future WORKING day, ≤ 3 per day, oldest first, instead of letting the due pile grow.
|
||||
- `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: only `## Solution` onward is script-owned on existing pages; human prose is never touched; never hand-write solution pages or edit inside `## Solution`.
|
||||
- `apps/cli/work.ts` — the `work/` layout: bucket paths, `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/cli/sync.ts` — work→docs contract: on existing pages only the `## Solution` section itself is script-owned, bounded by `solutionEnd()` at the next unfenced top-level heading; human prose before it and any sections after it (`## Approach`, `## Explanation`) are never touched. Never hand-write solution pages or edit inside `## Solution`.
|
||||
- `apps/cli/work.ts` — the `work/` layout: bucket paths, `isSolution()`/`solvedInBucket()` (the stub test, shared with `close-solved.ts`), `firstSolved()` (one `git log`, ET dates, keyed by LC number so past layout moves don't matter), and `reviewQueues()` (what each window still owes). Everything that needs to know where a solution file lives, or when it was solved, goes through here.
|
||||
- `apps/api/src/stats.ts` / `apps/cli/stats.ts` / `apps/cli/tui.ts` / `apps/cli/api.ts` — the read side. `buildStats()` is the ONE aggregation: phases, ladder, difficulty, per-topic temperature, gates, the 14-day due queue, and `heat` (every campaign day, zeros and future days included — the docs page windows the tail off it rather than asking a second question). Two readers consume it, the docs island and `bun run stats`, so its shape is both their contract: change all three together. `tui.ts` is hand-rolled ANSI (bars, sparklines, heat cells, alternate screen) on charts.ts's palette, and every chart differs by GLYPH as well as by colour so a piped or `NO_COLOR` dashboard still reads. The TUI recomputes nothing: the only local fact it shows is the `work/` solution count, which is inventory, not schedule.
|
||||
- `README.md` live charts are Worker endpoints (`/chart/*.svg`, `/badge/gate.svg`, 5-min Camo cache); the docs `/progress` page fetches `/api/stats` client-side (`apps/docs/islands/ProgressDashboard.tsx`).
|
||||
- `apps/docs/blume.config.ts` — site base `/leetcode`; `README.md` campaign table doubles as topic-map input to `sync.ts` (`readmeTopics()`, `TOPIC_ALIASES`).
|
||||
- Old `.github/srs/` JSON state, its `srs-*` Actions, and the one-shot `import-srs.ts` importer are RETIRED and deleted — do not resurrect; git history is the record.
|
||||
@@ -105,8 +111,7 @@ curl -X POST -H "Authorization: Bearer $LINK_KEY" \
|
||||
## Runtime/Tooling Preferences
|
||||
|
||||
- **Bun only**: run scripts with `bun apps/cli/<name>.ts` (or the root `bun run` aliases), install with `bun install --frozen-lockfile`. One workspace (`apps/*`), one root `bun.lock`. Node 22 appears only in `deploy.yml` because Blume requires it.
|
||||
- **No root tsconfig, no ESLint, no Prettier** — match surrounding style by hand. `apps/api/` has a strict `tsconfig.json`; `Env` types are generated (`bunx wrangler types`), never hand-written.
|
||||
- **Near-zero runtime npm dependencies** — SVG, PNG, HMAC (WebCrypto), GraphQL are hand-rolled; Actions scripts use Bun builtins + `fetch` only. The **one** exception is the digest email: `apps/api/src/email.tsx` renders React Email (`react`, `react-dom`, `@react-email/components`, `@react-email/render`) inside the Worker, because hand-rolling table-based email HTML that survives Gmail *and* Outlook is not worth owning. It costs ~235 KB gzipped of a 3 MB budget, needs no `nodejs_compat` (it resolves to `renderToReadableStream`), and requires `"jsx": "react-jsx"` in `apps/api/tsconfig.json`. Do not extend this exception to any other module.
|
||||
- **Near-zero runtime npm dependencies** — SVG, PNG, terminal ANSI, HMAC (WebCrypto), GraphQL are hand-rolled; Actions scripts use Bun builtins + `fetch` only. The **one** exception is the digest email: `apps/api/src/email.tsx` renders React Email (`react`, `react-dom`, `@react-email/components`, `@react-email/render`) inside the Worker, because hand-rolling table-based email HTML that survives Gmail *and* Outlook is not worth owning. It costs ~235 KB gzipped of a 3 MB budget, needs no `nodejs_compat` (it resolves to `renderToReadableStream`), and requires `"jsx": "react-jsx"` in `apps/api/tsconfig.json`. Do not extend this exception to any other module.
|
||||
- Local GitHub auth falls back to `gh auth token`; scripts run fine outside Actions.
|
||||
|
||||
## Testing & QA
|
||||
|
||||
@@ -10,14 +10,11 @@
|
||||
* so relabelling a problem is enough to move it and titles stay readable.
|
||||
* The `problem` label is what marks a sub-issue as curriculum.
|
||||
*/
|
||||
import { workingDay } from "./srs.ts";
|
||||
import { DIFFICULTIES, SETS, workingDay } from "./srs.ts";
|
||||
import type { GitHub } from "./github.ts";
|
||||
|
||||
const TITLE_RE = /^LC (\d+) · (.+)$/;
|
||||
|
||||
const DIFFICULTIES = ["easy", "medium", "hard"] as const;
|
||||
const SETS = ["core", "optional", "deferred"] as const;
|
||||
|
||||
/** Deferred Hards enter the queue from this date, two per working day. */
|
||||
const DEFER_FROM = "2026-09-28";
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
isoWeek,
|
||||
rng,
|
||||
sample,
|
||||
TEMPERATURES,
|
||||
temperatureOf,
|
||||
weekdayOf,
|
||||
workingDay,
|
||||
} from "./srs.ts";
|
||||
@@ -132,6 +134,31 @@ describe("the ladder", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("concept temperature", () => {
|
||||
test("the bands are the ladder's own windows", () => {
|
||||
// Boundaries are inclusive, and the bands tile the whole age axis with no
|
||||
// gap: whatever the windows become, every age still lands in exactly one.
|
||||
expect(TEMPERATURES).toEqual(["hot", "fresh", "fading", "cold"]);
|
||||
expect(temperatureOf(0)).toBe("hot");
|
||||
expect(temperatureOf(WINDOWS[0])).toBe("hot");
|
||||
expect(temperatureOf(WINDOWS[0] + 1)).toBe("fresh");
|
||||
expect(temperatureOf(WINDOWS[1])).toBe("fresh");
|
||||
expect(temperatureOf(WINDOWS[1] + 1)).toBe("fading");
|
||||
expect(temperatureOf(WINDOWS[1] * 2)).toBe("fading");
|
||||
expect(temperatureOf(WINDOWS[1] * 2 + 1)).toBe("cold");
|
||||
});
|
||||
|
||||
test("never attempted is cold, not unknown", () => {
|
||||
expect(temperatureOf(null)).toBe("cold");
|
||||
});
|
||||
|
||||
test("every age of the campaign has exactly one band", () => {
|
||||
for (let age = 0; age <= CALENDAR.length; age++) {
|
||||
expect(TEMPERATURES).toContain(temperatureOf(age));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("deterministic sampling", () => {
|
||||
test("same seed, same picks", () => {
|
||||
const pool = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
|
||||
+50
-5
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* SRS domain: ET dates, the work week, the interval ladder, deterministic
|
||||
* sampling, and the one write path for attempts.
|
||||
* SRS domain: ET dates, the work week, the interval ladder, concept
|
||||
* temperature, deterministic sampling, and the one write path for attempts.
|
||||
*
|
||||
* D1 is the single source of truth. The stage names the review a problem must
|
||||
* pass NEXT (`+3` = due 3 working days after the last clean solve). Passing
|
||||
@@ -128,14 +128,20 @@ export function prettyDate(date: string): string {
|
||||
|
||||
export const SCHEDULE: Record<string, number> = scheduleJson;
|
||||
|
||||
/** Week in which a topic was (or will be) taught. */
|
||||
export function topicWeek(topic: number): number | undefined {
|
||||
/** Date the schedule teaches a topic on, if it teaches it at all. */
|
||||
export function topicDate(topic: number): string | undefined {
|
||||
for (const [date, t] of Object.entries(SCHEDULE)) {
|
||||
if (t === topic) return campaignWeek(date);
|
||||
if (t === topic) return date;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Week in which a topic was (or will be) taught. */
|
||||
export function topicWeek(topic: number): number | undefined {
|
||||
const date = topicDate(topic);
|
||||
return date === undefined ? undefined : campaignWeek(date);
|
||||
}
|
||||
|
||||
// ── deterministic sampling ───────────────────────────────────────
|
||||
|
||||
/** FNV-1a → mulberry32: seeded PRNG so re-runs pick identical problems. */
|
||||
@@ -182,6 +188,45 @@ export const STAGES: Stage[] = ["new", "+3", "+7", "retired"];
|
||||
export type Result = "pass" | "fail";
|
||||
export type Kind = "first" | "review" | "drill" | "gate";
|
||||
|
||||
/**
|
||||
* Catalog vocabulary, carried by the `diff:*` / `set:*` issue labels and
|
||||
* stored verbatim in `problems`. Listed low to high / most to least required,
|
||||
* which is the order every chart, table and digest section prints them in.
|
||||
*/
|
||||
export const DIFFICULTIES = ["easy", "medium", "hard"] as const;
|
||||
export type Difficulty = (typeof DIFFICULTIES)[number];
|
||||
export const SETS = ["core", "optional", "deferred"] as const;
|
||||
export type SetLabel = (typeof SETS)[number];
|
||||
|
||||
/**
|
||||
* How warm a concept is: the age of a topic's most recent attempt, bucketed by
|
||||
* the ladder's own windows so the two cannot drift. Touched inside the first
|
||||
* window it is `hot`, inside the second `fresh`, inside one more full second
|
||||
* window `fading`; older than that — or never attempted — it is `cold`.
|
||||
*
|
||||
* A read-side label only: nothing is scheduled off it. It answers the question
|
||||
* the ladder cannot, because the ladder tracks problems and a topic can go
|
||||
* quiet for a fortnight while not one of its problems comes due.
|
||||
*/
|
||||
export const TEMPERATURES = ["hot", "fresh", "fading", "cold"] as const;
|
||||
export type Temperature = (typeof TEMPERATURES)[number];
|
||||
|
||||
/** Warmest band first, with the age it tolerates. Past the last one is cold. */
|
||||
const TEMPERATURE_MAX_AGE: [Temperature, number][] = [
|
||||
["hot", WINDOWS[0]],
|
||||
["fresh", WINDOWS[1]],
|
||||
["fading", WINDOWS[1] * 2],
|
||||
];
|
||||
|
||||
/** Days since a topic's last attempt → its band; null (never) is cold. */
|
||||
export function temperatureOf(age: number | null): Temperature {
|
||||
if (age === null) return "cold";
|
||||
for (const [temperature, max] of TEMPERATURE_MAX_AGE) {
|
||||
if (age <= max) return temperature;
|
||||
}
|
||||
return "cold";
|
||||
}
|
||||
|
||||
export interface ProblemRow {
|
||||
lc_number: number;
|
||||
issue: number;
|
||||
|
||||
+146
-28
@@ -1,19 +1,29 @@
|
||||
/**
|
||||
* GET /api/stats — the one JSON document behind the docs progress page.
|
||||
* Read-only aggregation over D1; shape is the page's contract, change both
|
||||
* together. A problem counts as "done" once it entered the ladder
|
||||
* (stage != 'new').
|
||||
* GET /api/stats — the one JSON document behind the docs progress page and the
|
||||
* `bun run stats` terminal dashboard. Read-only aggregation over D1; the shape
|
||||
* is both readers' contract, so change all three together. A problem counts as
|
||||
* "done" once it entered the ladder (stage != 'new').
|
||||
*
|
||||
* The temperature block is the one figure here that is not a count of rows:
|
||||
* srs.ts turns a topic's days-since-last-attempt into a band, so "which
|
||||
* concepts have gone cold" has exactly one definition and the dashboard is
|
||||
* free to colour it however it likes.
|
||||
*/
|
||||
import {
|
||||
CAMPAIGN_DAYS,
|
||||
CAMPAIGN_START,
|
||||
DIFFICULTIES,
|
||||
DUE_WHERE,
|
||||
addDays,
|
||||
campaignDay,
|
||||
campaignWeek,
|
||||
daysBetween,
|
||||
isoWeek,
|
||||
STAGES,
|
||||
streak,
|
||||
temperatureOf,
|
||||
topicDate,
|
||||
type Temperature,
|
||||
} from "./srs.ts";
|
||||
|
||||
const PHASE_NAMES: Record<number, string> = {
|
||||
@@ -24,6 +34,91 @@ const PHASE_NAMES: Record<number, string> = {
|
||||
5: "V — Decision Space",
|
||||
};
|
||||
|
||||
/** One concept: catalog coverage plus how long since it was last exercised. */
|
||||
interface TopicStat {
|
||||
issue: number;
|
||||
name: string;
|
||||
milestone: number | null;
|
||||
/** Campaign week the schedule teaches it in; null when unscheduled. */
|
||||
week: number | null;
|
||||
/** False while the schedule has not reached it — temperature is moot. */
|
||||
taught: boolean;
|
||||
total: number;
|
||||
done: number;
|
||||
retired: number;
|
||||
due: number;
|
||||
/** ET date of the most recent attempt on any of its problems. */
|
||||
last: string | null;
|
||||
/** Days since `last`; null when never attempted. */
|
||||
age: number | null;
|
||||
temperature: Temperature;
|
||||
}
|
||||
|
||||
async function buildTopics(db: D1Database, today: string): Promise<TopicStat[]> {
|
||||
// Three shapes, three statements: batch() carries one row type, and casting
|
||||
// its results apart would cost more than the round trips it saves.
|
||||
const { results: coverage } = await db
|
||||
.prepare(
|
||||
`SELECT t.issue AS issue, t.name AS name, t.milestone AS milestone,
|
||||
COUNT(p.lc_number) AS total,
|
||||
SUM(CASE WHEN p.stage != 'new' THEN 1 ELSE 0 END) AS done,
|
||||
SUM(CASE WHEN p.stage = 'retired' THEN 1 ELSE 0 END) AS retired
|
||||
FROM topics t LEFT JOIN problems p ON p.topic_issue = t.issue
|
||||
GROUP BY t.issue
|
||||
ORDER BY t.issue`,
|
||||
)
|
||||
.all<{
|
||||
issue: number;
|
||||
name: string;
|
||||
milestone: number | null;
|
||||
total: number;
|
||||
done: number | null;
|
||||
retired: number | null;
|
||||
}>();
|
||||
|
||||
const { results: activity } = await db
|
||||
.prepare(
|
||||
`SELECT p.topic_issue AS issue, MAX(a.date) AS last
|
||||
FROM attempts a JOIN problems p ON p.lc_number = a.lc_number
|
||||
GROUP BY p.topic_issue`,
|
||||
)
|
||||
.all<{ issue: number; last: string | null }>();
|
||||
const lastByTopic = new Map<number, string>();
|
||||
for (const row of activity) {
|
||||
if (row.last) lastByTopic.set(row.issue, row.last);
|
||||
}
|
||||
|
||||
const { results: dueRows } = await db
|
||||
.prepare(
|
||||
`SELECT topic_issue AS issue, COUNT(*) AS n
|
||||
FROM problems WHERE ${DUE_WHERE} GROUP BY topic_issue`,
|
||||
)
|
||||
.bind(today)
|
||||
.all<{ issue: number; n: number }>();
|
||||
const dueByTopic = new Map<number, number>();
|
||||
for (const row of dueRows) dueByTopic.set(row.issue, row.n);
|
||||
|
||||
return coverage.map((row) => {
|
||||
const taughtOn = topicDate(row.issue);
|
||||
const last = lastByTopic.get(row.issue) ?? null;
|
||||
const age = last === null ? null : daysBetween(last, today);
|
||||
return {
|
||||
issue: row.issue,
|
||||
name: row.name,
|
||||
milestone: row.milestone,
|
||||
week: taughtOn === undefined ? null : campaignWeek(taughtOn),
|
||||
taught: taughtOn !== undefined && taughtOn <= today,
|
||||
total: row.total,
|
||||
done: row.done ?? 0,
|
||||
retired: row.retired ?? 0,
|
||||
due: dueByTopic.get(row.issue) ?? 0,
|
||||
last,
|
||||
age,
|
||||
temperature: temperatureOf(age),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function buildStats(db: D1Database, today: string): Promise<object> {
|
||||
const { results: phaseRows } = await db
|
||||
.prepare(
|
||||
@@ -65,6 +160,21 @@ export async function buildStats(db: D1Database, today: string): Promise<object>
|
||||
const ladder: Record<string, number> = Object.fromEntries(STAGES.map((s) => [s, 0]));
|
||||
for (const row of ladderRows) ladder[row.stage] = row.n;
|
||||
|
||||
// Same shape as `ladder`: every band present, zeros included, in DIFFICULTIES
|
||||
// order so a reader can index it without sorting.
|
||||
const { results: diffRows } = await db
|
||||
.prepare(
|
||||
`SELECT difficulty,
|
||||
COUNT(*) AS total,
|
||||
SUM(CASE WHEN stage != 'new' THEN 1 ELSE 0 END) AS done
|
||||
FROM problems GROUP BY difficulty`,
|
||||
)
|
||||
.all<{ difficulty: string; total: number; done: number }>();
|
||||
const difficulty = DIFFICULTIES.map((name) => {
|
||||
const row = diffRows.find((r) => r.difficulty === name);
|
||||
return { difficulty: name, total: row?.total ?? 0, done: row?.done ?? 0 };
|
||||
});
|
||||
|
||||
// gates rows are keyed by ISO week (that's the digest/gate contract), but
|
||||
// the page speaks campaign weeks. The campaign never crosses a year
|
||||
// boundary, so a plain offset converts safely.
|
||||
@@ -74,39 +184,47 @@ export async function buildStats(db: D1Database, today: string): Promise<object>
|
||||
.all<{ week: number; issue: number | null; pass_rate: number | null; closed_on: string | null }>();
|
||||
const gates = gateRows.map((g) => ({ ...g, week: g.week - isoWeekOffset }));
|
||||
|
||||
// Review-queue depth for the next 14 days: everything due by that day.
|
||||
const queue: { date: string; due: number }[] = [];
|
||||
for (let i = 0; i < 14; i++) {
|
||||
const date = addDays(today, i);
|
||||
const row = await db
|
||||
.prepare(`SELECT COUNT(*) AS n FROM problems WHERE ${DUE_WHERE}`)
|
||||
.bind(date)
|
||||
.first<{ n: number }>();
|
||||
queue.push({ date, due: row?.n ?? 0 });
|
||||
}
|
||||
// Review-queue depth for the next 14 days: everything due by that day. One
|
||||
// statement per day, but DUE_WHERE stays the only definition of "due" — the
|
||||
// batch is a single round trip, not fourteen.
|
||||
const dates = Array.from({ length: 14 }, (_, i) => addDays(today, i));
|
||||
const queueRows = await db.batch<{ n: number }>(
|
||||
dates.map((date) =>
|
||||
db.prepare(`SELECT COUNT(*) AS n FROM problems WHERE ${DUE_WHERE}`).bind(date),
|
||||
),
|
||||
);
|
||||
const queue = dates.map((date, i) => ({ date, due: queueRows[i]?.results[0]?.n ?? 0 }));
|
||||
|
||||
const recent: { date: string; attempts: number; passes: number }[] = [];
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const date = addDays(today, -i);
|
||||
const row = await db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) AS attempts,
|
||||
SUM(CASE WHEN result = 'pass' THEN 1 ELSE 0 END) AS passes
|
||||
FROM attempts WHERE date = ?`,
|
||||
)
|
||||
.bind(date)
|
||||
.first<{ attempts: number; passes: number | null }>();
|
||||
recent.push({ date, attempts: row?.attempts ?? 0, passes: row?.passes ?? 0 });
|
||||
}
|
||||
// Attempts per campaign day, every day present. This is the heatmap's data
|
||||
// model — the docs page reads the tail of it for "last 7 days" rather than
|
||||
// asking a second question that could answer differently.
|
||||
const end = addDays(CAMPAIGN_START, CAMPAIGN_DAYS - 1);
|
||||
const { results: heatRows } = await db
|
||||
.prepare(
|
||||
`SELECT date,
|
||||
COUNT(*) AS attempts,
|
||||
SUM(CASE WHEN result = 'pass' THEN 1 ELSE 0 END) AS passes
|
||||
FROM attempts WHERE date BETWEEN ?1 AND ?2 GROUP BY date`,
|
||||
)
|
||||
.bind(CAMPAIGN_START, end)
|
||||
.all<{ date: string; attempts: number; passes: number | null }>();
|
||||
const byDate = new Map(heatRows.map((r) => [r.date, r]));
|
||||
const heat = Array.from({ length: CAMPAIGN_DAYS }, (_, i) => {
|
||||
const date = addDays(CAMPAIGN_START, i);
|
||||
const row = byDate.get(date);
|
||||
return { date, attempts: row?.attempts ?? 0, passes: row?.passes ?? 0 };
|
||||
});
|
||||
|
||||
return {
|
||||
generated: new Date().toISOString(),
|
||||
campaign: { day: campaignDay(today), week: campaignWeek(today), start: CAMPAIGN_START, days: CAMPAIGN_DAYS },
|
||||
phases: [...phases.values()],
|
||||
ladder,
|
||||
difficulty,
|
||||
topics: await buildTopics(db, today),
|
||||
gates,
|
||||
streak: await streak(db, today),
|
||||
queue,
|
||||
recent,
|
||||
heat,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* The SRS Worker, from the CLI side: one base URL and one read-side payload.
|
||||
*
|
||||
* `SRS_API` points a script at a `wrangler dev` instance instead of production
|
||||
* — the same switch close-solved.ts uses for its `/admin/solved` push, which is
|
||||
* why the default lives here rather than in either script.
|
||||
*
|
||||
* The payload is narrowed with a guard rather than cast: D1 is the only source
|
||||
* of SRS truth, so if the Worker answers something else the caller must say so
|
||||
* out loud, not render half a dashboard off undefined fields.
|
||||
*/
|
||||
import { STAGES, TEMPERATURES, type Stage, type Temperature } from "../api/src/srs.ts";
|
||||
|
||||
export const SRS_API = process.env.SRS_API ?? "https://srs-api.prdlk.workers.dev";
|
||||
|
||||
// The rungs and the bands come straight from the Worker's domain module — the
|
||||
// same import work.ts makes for WINDOWS — so a change to either vocabulary
|
||||
// reaches this dashboard without a second list to remember.
|
||||
export { STAGES, TEMPERATURES, type Stage, type Temperature };
|
||||
|
||||
export interface Phase {
|
||||
milestone: number;
|
||||
name: string;
|
||||
core_done: number;
|
||||
core_total: number;
|
||||
optional_done: number;
|
||||
optional_total: number;
|
||||
deferred_done: number;
|
||||
deferred_total: number;
|
||||
}
|
||||
|
||||
export interface Topic {
|
||||
issue: number;
|
||||
name: string;
|
||||
milestone: number | null;
|
||||
week: number | null;
|
||||
taught: boolean;
|
||||
total: number;
|
||||
done: number;
|
||||
retired: number;
|
||||
due: number;
|
||||
last: string | null;
|
||||
age: number | null;
|
||||
temperature: Temperature;
|
||||
}
|
||||
|
||||
export interface Stats {
|
||||
generated: string;
|
||||
campaign: { day: number; week: number; start: string; days: number };
|
||||
phases: Phase[];
|
||||
ladder: Record<Stage, number>;
|
||||
difficulty: { difficulty: string; total: number; done: number }[];
|
||||
topics: Topic[];
|
||||
/** week is the 1-based campaign week — the Worker converts from ISO. */
|
||||
gates: { week: number; issue: number | null; pass_rate: number | null; closed_on: string | null }[];
|
||||
streak: number;
|
||||
queue: { date: string; due: number }[];
|
||||
/** Every campaign day, zeros and future days included. */
|
||||
heat: { date: string; attempts: number; passes: number }[];
|
||||
}
|
||||
|
||||
function isStats(value: unknown): value is Stats {
|
||||
if (typeof value !== "object" || value === null) return false;
|
||||
const v = value as Record<string, unknown>;
|
||||
return (
|
||||
typeof v.campaign === "object" &&
|
||||
v.campaign !== null &&
|
||||
typeof v.ladder === "object" &&
|
||||
v.ladder !== null &&
|
||||
typeof v.streak === "number" &&
|
||||
Array.isArray(v.phases) &&
|
||||
Array.isArray(v.difficulty) &&
|
||||
Array.isArray(v.topics) &&
|
||||
Array.isArray(v.gates) &&
|
||||
Array.isArray(v.queue) &&
|
||||
Array.isArray(v.heat)
|
||||
);
|
||||
}
|
||||
|
||||
/** GET /api/stats. Throws with the reason; there is no local substitute. */
|
||||
export async function fetchStats(signal?: AbortSignal): Promise<Stats> {
|
||||
const res = await fetch(`${SRS_API}/api/stats`, { signal });
|
||||
if (!res.ok) throw new Error(`GET /api/stats -> ${res.status} ${await res.text()}`);
|
||||
const body: unknown = await res.json();
|
||||
if (!isStats(body)) throw new Error("GET /api/stats -> unrecognised payload shape");
|
||||
return body;
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
*/
|
||||
import { join } from "node:path";
|
||||
|
||||
import { SRS_API } from "./api.ts";
|
||||
import { github } from "./github.ts";
|
||||
import { markdownTable, printTable, writeStepSummary } from "./report.ts";
|
||||
import { isImplemented } from "./source.ts";
|
||||
@@ -162,7 +163,8 @@ for (const num of [...work.keys()].sort((a, b) => a - b)) {
|
||||
// rung named, so the whole implemented set can go over on every push (a total
|
||||
// recompute that backfills whatever earlier runs missed) and re-runs write
|
||||
// nothing.
|
||||
const SRS_API = process.env.SRS_API ?? "https://srs-api.prdlk.workers.dev";
|
||||
// SRS_API (apps/cli/api.ts) is the switch that points this at a wrangler dev
|
||||
// instance instead of production.
|
||||
const solved = [...work.entries()]
|
||||
.flatMap(([lc, entry]) => [...entry.solved].sort().map((bucket) => ({ lc, bucket })))
|
||||
.sort((a, b) => a.lc - b.lc || a.bucket - b.bucket);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"pick": "bun ./pick.ts",
|
||||
"stats": "bun ./stats.ts",
|
||||
"test": "bun ./test.ts",
|
||||
"submit": "bun ./submit.ts",
|
||||
"sync": "bun ./sync.ts",
|
||||
|
||||
Executable
+458
@@ -0,0 +1,458 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* `bun run stats` — the campaign dashboard, in the terminal.
|
||||
*
|
||||
* Four panes over ONE source: GET /api/stats. D1 owns SRS state, so this
|
||||
* fetches the Worker's aggregate rather than recomputing anything from work/
|
||||
* or from git — a second answer to "how far along am I" is exactly the drift
|
||||
* the campaign is built to avoid. The one local fact shown is the work/ file
|
||||
* inventory, which is inventory, not schedule. Unreachable Worker = an error,
|
||||
* never a reconstructed guess.
|
||||
*
|
||||
* The panes:
|
||||
* Overview campaign clock, solved counts, phase bars, recent activity
|
||||
* Ladder the +3/+7 rungs and the 14-day due queue
|
||||
* Concepts every topic by temperature — what has gone cold
|
||||
* Activity the campaign heatmap and the weekly gates
|
||||
*
|
||||
* Outside a TTY (a pipe, CI) it prints every pane once and exits, so the same
|
||||
* command works in a scrollback or a file.
|
||||
*/
|
||||
import {
|
||||
fetchStats,
|
||||
SRS_API,
|
||||
STAGES,
|
||||
TEMPERATURES,
|
||||
type Stats,
|
||||
type Temperature,
|
||||
type Topic,
|
||||
} from "./api.ts";
|
||||
import {
|
||||
BLUE,
|
||||
FG,
|
||||
GREEN,
|
||||
GREENS,
|
||||
LINE,
|
||||
MUTED,
|
||||
RED,
|
||||
YELLOW,
|
||||
bar,
|
||||
bold,
|
||||
cell,
|
||||
fg,
|
||||
heatCell,
|
||||
openScreen,
|
||||
padTo,
|
||||
rule,
|
||||
sparkline,
|
||||
stackedBar,
|
||||
width,
|
||||
} from "./tui.ts";
|
||||
import { BUCKETS, WINDOWS, solvedInBucket } from "./work.ts";
|
||||
|
||||
// ── shared vocabulary → colour ───────────────────────────────────
|
||||
|
||||
// Temperature is an action ramp, not a thermometer: green is warm enough to
|
||||
// leave alone, red is the concept that wants revisiting. The band names carry
|
||||
// the heat, the colours carry the urgency — and the glyphs carry both again,
|
||||
// so a piped or NO_COLOR dashboard still separates the bands.
|
||||
const TEMP_COLOR: Record<Temperature, string> = {
|
||||
hot: GREENS[4]!,
|
||||
fresh: GREEN,
|
||||
fading: YELLOW,
|
||||
cold: RED,
|
||||
};
|
||||
|
||||
const TEMP_GLYPH: Record<Temperature, string> = {
|
||||
hot: "█",
|
||||
fresh: "▓",
|
||||
fading: "▒",
|
||||
cold: "░",
|
||||
};
|
||||
|
||||
const DIFF_COLOR: Record<string, string> = { easy: GREEN, medium: YELLOW, hard: RED };
|
||||
|
||||
const LABEL = 15; // left-hand label column, shared by every pane
|
||||
|
||||
/** "2026-08-31" → "Mon 31", read as a plain calendar date, never localised. */
|
||||
function dayLabel(iso: string): string {
|
||||
const [y, m, d] = iso.split("-").map(Number);
|
||||
const weekday = new Date(Date.UTC(y!, m! - 1, d!)).getUTCDay();
|
||||
return `${["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][weekday]} ${String(d).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
// ── overview ─────────────────────────────────────────────────────
|
||||
|
||||
function overview(s: Stats, cols: number): string[] {
|
||||
const bars = Math.max(10, Math.min(48, cols - LABEL - 18));
|
||||
const lines: string[] = [];
|
||||
|
||||
const { day, days, week } = s.campaign;
|
||||
lines.push(rule("campaign", cols));
|
||||
lines.push(
|
||||
`${cell("Day", LABEL)}${bar(day, days, bars)} ${fg(FG, `${day}/${days}`)}` +
|
||||
fg(MUTED, ` week ${week}`),
|
||||
);
|
||||
const streak = s.streak > 0 ? fg(GREENS[4]!, `${s.streak} day${s.streak === 1 ? "" : "s"}`) : fg(MUTED, "none");
|
||||
lines.push(`${cell("Streak", LABEL)}${streak}`);
|
||||
|
||||
const total = s.difficulty.reduce((n, d) => n + d.total, 0);
|
||||
const done = s.difficulty.reduce((n, d) => n + d.done, 0);
|
||||
lines.push("");
|
||||
lines.push(rule("solved", cols));
|
||||
lines.push(
|
||||
`${cell("All", LABEL)}${bar(done, total, bars)} ${fg(FG, `${done}/${total}`)}` +
|
||||
fg(MUTED, ` ${total > 0 ? Math.round((done / total) * 100) : 0}%`),
|
||||
);
|
||||
for (const d of s.difficulty) {
|
||||
lines.push(
|
||||
`${cell(d.difficulty, LABEL)}${bar(d.done, d.total, bars, DIFF_COLOR[d.difficulty] ?? GREEN)}` +
|
||||
` ${fg(MUTED, `${d.done}/${d.total}`)}`,
|
||||
);
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push(rule("phases", cols));
|
||||
lines.push(
|
||||
`${" ".repeat(LABEL)}${fg(GREEN, "█")} ${fg(MUTED, "core")} ${fg(BLUE, "█")} ${fg(MUTED, "optional")} ` +
|
||||
`${fg(RED, "█")} ${fg(MUTED, "deferred")} ${fg(LINE, "░")} ${fg(MUTED, "remaining")}`,
|
||||
);
|
||||
// Every phase bar is scaled to the LARGEST phase, not to itself: phases are
|
||||
// not the same size, and equal-length bars would hide that.
|
||||
const scale = Math.max(
|
||||
1,
|
||||
...s.phases.map((p) => p.core_total + p.optional_total + p.deferred_total),
|
||||
);
|
||||
for (const p of s.phases) {
|
||||
const phaseDone = p.core_done + p.optional_done + p.deferred_done;
|
||||
const phaseTotal = p.core_total + p.optional_total + p.deferred_total;
|
||||
const segments = [
|
||||
{ value: p.core_done, color: GREEN },
|
||||
{ value: p.optional_done, color: BLUE },
|
||||
{ value: p.deferred_done, color: RED },
|
||||
{ value: phaseTotal - phaseDone, color: LINE, glyph: "░" },
|
||||
];
|
||||
lines.push(
|
||||
`${cell(p.name, LABEL - 1)} ${stackedBar(segments, scale, bars)} ${fg(MUTED, `${phaseDone}/${phaseTotal}`)}`,
|
||||
);
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push(rule("last 14 days", cols));
|
||||
const window = s.heat.slice(Math.max(0, day - 14), day);
|
||||
const attempts = window.reduce((n, h) => n + h.attempts, 0);
|
||||
const passes = window.reduce((n, h) => n + h.passes, 0);
|
||||
lines.push(
|
||||
`${cell("Attempts", LABEL)}${sparkline(window.map((h) => h.attempts))} ` +
|
||||
fg(MUTED, `${attempts} attempts · ${passes} passed`),
|
||||
);
|
||||
|
||||
// The one local fact on the dashboard: solutions on disk, per
|
||||
// spaced-repetition bucket. Inventory, not schedule — D1's ladder is the
|
||||
// schedule, and the Ladder pane is where it is shown. Empty scaffolds do not
|
||||
// count, the same rule close-solved.ts closes issues by.
|
||||
lines.push("");
|
||||
lines.push(rule("work/ on disk", cols));
|
||||
lines.push(
|
||||
cell("Solutions", LABEL) +
|
||||
BUCKETS.map((b) => `${fg(FG, String(solvedInBucket(b)))} ${fg(MUTED, `in work/${b}`)}`).join(
|
||||
fg(MUTED, " · "),
|
||||
),
|
||||
);
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
// ── ladder ───────────────────────────────────────────────────────
|
||||
|
||||
function ladder(s: Stats, cols: number): string[] {
|
||||
const bars = Math.max(10, Math.min(48, cols - LABEL - 18));
|
||||
const lines: string[] = [rule("ladder", cols)];
|
||||
lines.push(fg(MUTED, `${" ".repeat(LABEL)}the review each problem owes next`));
|
||||
|
||||
const rungs = STAGES.map((stage) => s.ladder[stage] ?? 0);
|
||||
const peak = Math.max(1, ...rungs);
|
||||
STAGES.forEach((stage, i) => {
|
||||
const n = rungs[i]!;
|
||||
// new is the untouched pile and retired is the finished one; only the
|
||||
// middle rungs are live work, so only they get the live colour.
|
||||
const color = stage === "new" ? LINE : stage === "retired" ? GREENS[4]! : GREEN;
|
||||
lines.push(`${cell(stage, LABEL)}${bar(n, peak, bars, color)} ${fg(FG, String(n))}`);
|
||||
});
|
||||
|
||||
lines.push("");
|
||||
lines.push(rule("due queue", cols));
|
||||
const today = s.queue[0];
|
||||
lines.push(
|
||||
fg(MUTED, `${" ".repeat(LABEL)}cumulative: everything the ladder wants by that day`) +
|
||||
(today ? fg(FG, ` · ${today.due} due today`) : ""),
|
||||
);
|
||||
const depth = Math.max(1, ...s.queue.map((q) => q.due));
|
||||
for (const q of s.queue) {
|
||||
const color = q.due > 3 ? RED : q.due > 0 ? GREEN : LINE;
|
||||
lines.push(
|
||||
`${cell(dayLabel(q.date), LABEL)}${bar(q.due, depth, bars, color)} ${fg(MUTED, String(q.due))}`,
|
||||
);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
// ── concepts ─────────────────────────────────────────────────────
|
||||
|
||||
/** Coldest first, and everything the schedule has not reached yet last. */
|
||||
function byUrgency(a: Topic, b: Topic): number {
|
||||
if (a.taught !== b.taught) return a.taught ? -1 : 1;
|
||||
return (
|
||||
TEMPERATURES.indexOf(b.temperature) - TEMPERATURES.indexOf(a.temperature) ||
|
||||
(b.age ?? Infinity) - (a.age ?? Infinity) ||
|
||||
a.issue - b.issue
|
||||
);
|
||||
}
|
||||
|
||||
// Row layout: "● " + name + temp + coverage bar + done/total + last + due.
|
||||
// Everything but the name is fixed, so the name absorbs the terminal width.
|
||||
const TEMP_W = 7;
|
||||
const COVER_W = 10;
|
||||
const COUNT_W = 6;
|
||||
const AGE_W = 6;
|
||||
const DUE_W = 3;
|
||||
const CONCEPT_FIXED = 3 + (TEMP_W + 1) + (COVER_W + 1) + (COUNT_W + 1) + (AGE_W + 1) + DUE_W + 1;
|
||||
|
||||
function concepts(s: Stats, cols: number): string[] {
|
||||
const lines: string[] = [rule("concepts by temperature", cols)];
|
||||
// Thresholds are the ladder's windows; print them rather than restate them.
|
||||
const bands: [Temperature, string][] = [
|
||||
["hot", `≤${WINDOWS[0]}d`],
|
||||
["fresh", `≤${WINDOWS[1]}d`],
|
||||
["fading", `≤${WINDOWS[1] * 2}d`],
|
||||
["cold", "older, or never"],
|
||||
];
|
||||
lines.push(
|
||||
fg(MUTED, " last exercised — ") +
|
||||
bands
|
||||
.map(([t, when]) => `${fg(TEMP_COLOR[t], `${TEMP_GLYPH[t]} ${t}`)} ${fg(MUTED, when)}`)
|
||||
.join(fg(MUTED, " · ")),
|
||||
);
|
||||
|
||||
const taught = s.topics.filter((t) => t.taught);
|
||||
const counts = TEMPERATURES.map((t) => ({
|
||||
value: taught.filter((x) => x.temperature === t).length,
|
||||
color: TEMP_COLOR[t],
|
||||
glyph: TEMP_GLYPH[t],
|
||||
}));
|
||||
lines.push("");
|
||||
lines.push(` ${stackedBar(counts, Math.max(1, taught.length), Math.max(10, Math.min(60, cols - 4)))}`);
|
||||
lines.push(
|
||||
` ${TEMPERATURES.map((t, i) => `${fg(TEMP_COLOR[t], TEMP_GLYPH[t])} ${counts[i]!.value} ${fg(MUTED, t)}`).join(" ")}` +
|
||||
fg(MUTED, ` of ${taught.length} taught so far`),
|
||||
);
|
||||
|
||||
const nameW = Math.max(12, Math.min(34, cols - CONCEPT_FIXED));
|
||||
lines.push("");
|
||||
lines.push(
|
||||
fg(
|
||||
MUTED,
|
||||
` ${cell("concept", nameW)} ${cell("temp", TEMP_W)} ` +
|
||||
`${cell("coverage", COVER_W + 1 + COUNT_W)} ${cell("last", AGE_W)} ${cell("due", DUE_W, "right")}`,
|
||||
),
|
||||
);
|
||||
|
||||
for (const t of s.topics.slice().sort(byUrgency)) {
|
||||
// An unreached concept has no temperature to report — it shows the week
|
||||
// the schedule gets to it instead, so the list stays one list.
|
||||
const color = t.taught ? TEMP_COLOR[t.temperature] : LINE;
|
||||
const age =
|
||||
!t.taught ? "—" : t.age === null ? "never" : t.age <= 0 ? "today" : `${t.age}d`;
|
||||
lines.push(
|
||||
` ${fg(color, t.taught ? TEMP_GLYPH[t.temperature] : "·")} ${fg(t.taught ? FG : MUTED, cell(t.name, nameW))} ` +
|
||||
`${fg(color, cell(t.taught ? t.temperature : `wk ${t.week ?? "?"}`, TEMP_W))} ` +
|
||||
`${bar(t.done, t.total, COVER_W, t.taught ? GREEN : LINE)} ` +
|
||||
`${fg(MUTED, cell(`${t.done}/${t.total}`, COUNT_W))} ${fg(MUTED, cell(age, AGE_W))} ` +
|
||||
`${t.due > 0 ? fg(GREEN, cell(String(t.due), DUE_W, "right")) : fg(LINE, cell("·", DUE_W, "right"))}`,
|
||||
);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
// ── activity ─────────────────────────────────────────────────────
|
||||
|
||||
const WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
||||
|
||||
function activity(s: Stats, cols: number): string[] {
|
||||
const lines: string[] = [rule("attempts per campaign day", cols)];
|
||||
lines.push(fg(MUTED, " the README heatmap, same ramp — Sunday is the rest day"));
|
||||
lines.push("");
|
||||
|
||||
// CAMPAIGN_START is a Monday, so the grid is simply the series in weeks of
|
||||
// seven: column = week, row = weekday, exactly as the SVG lays it out.
|
||||
const weeks = Math.ceil(s.heat.length / 7);
|
||||
lines.push(
|
||||
` ${fg(MUTED, Array.from({ length: weeks }, (_, w) => ` ${String(w + 1).padStart(2)}`).join(""))}`,
|
||||
);
|
||||
WEEKDAYS.forEach((name, row) => {
|
||||
const cells = Array.from({ length: weeks }, (_, w) => {
|
||||
const entry = s.heat[w * 7 + row];
|
||||
if (entry === undefined) return " ";
|
||||
// The marker sits in the gutter, not inside the cell: a foreground
|
||||
// escape within a background run would end the background early.
|
||||
const marker = s.campaign.day - 1 === w * 7 + row ? fg(FG, "▸") : " ";
|
||||
return marker + heatCell(entry.attempts, " ");
|
||||
}).join("");
|
||||
lines.push(` ${fg(MUTED, cell(name, 4))}${cells}`);
|
||||
});
|
||||
lines.push("");
|
||||
lines.push(
|
||||
` ${fg(MUTED, "less ")}${GREENS.map((_, level) => heatCell(level, " ")).join("")}${fg(MUTED, " more")}` +
|
||||
fg(MUTED, " · one cell per day, colour = attempts, ▸ marks today"),
|
||||
);
|
||||
|
||||
lines.push("");
|
||||
lines.push(rule("weekly gates", cols));
|
||||
if (s.gates.length === 0) {
|
||||
lines.push(fg(MUTED, " no gate has been scored yet"));
|
||||
}
|
||||
const gateW = Math.max(10, Math.min(24, cols - 30));
|
||||
for (const g of s.gates) {
|
||||
// Gates were stored as an integer percent before they were stored as a
|
||||
// fraction; both are in the table, so normalise on read.
|
||||
const rate = g.pass_rate === null ? null : g.pass_rate <= 1 ? g.pass_rate : g.pass_rate / 100;
|
||||
const color = rate === null ? LINE : rate >= 0.7 ? GREEN : RED;
|
||||
lines.push(
|
||||
` ${fg(FG, cell(`Week ${g.week}`, 9))}` +
|
||||
`${bar(rate ?? 0, 1, gateW, color)}` +
|
||||
` ${fg(color, cell(rate === null ? "open" : `${Math.round(rate * 100)}%`, 5))}` +
|
||||
` ${fg(MUTED, g.issue === null ? "" : `#${g.issue}`)}${fg(MUTED, g.closed_on ? ` closed ${g.closed_on}` : "")}`,
|
||||
);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
// ── panes ────────────────────────────────────────────────────────
|
||||
|
||||
const PANES: { name: string; render: (s: Stats, cols: number) => string[] }[] = [
|
||||
{ name: "Overview", render: overview },
|
||||
{ name: "Ladder", render: ladder },
|
||||
{ name: "Concepts", render: concepts },
|
||||
{ name: "Activity", render: activity },
|
||||
];
|
||||
|
||||
// ── non-interactive: every pane, once ────────────────────────────
|
||||
|
||||
let stats: Stats;
|
||||
try {
|
||||
stats = await fetchStats();
|
||||
} catch (err) {
|
||||
console.error(`stats: ${err instanceof Error ? err.message : String(err)}`);
|
||||
console.error(`stats: D1 is the only source of SRS truth — nothing to fall back to (SRS_API=${SRS_API}).`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!process.stdout.isTTY) {
|
||||
const cols = 80;
|
||||
for (const pane of PANES) {
|
||||
console.log(bold(pane.name.toUpperCase()));
|
||||
for (const line of pane.render(stats, cols)) console.log(line);
|
||||
console.log("");
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// ── interactive ──────────────────────────────────────────────────
|
||||
|
||||
let pane = 0;
|
||||
let offset = 0;
|
||||
let note = "";
|
||||
|
||||
// Quitting resolves this; the finally below is what restores the terminal, so
|
||||
// a thrown render never leaves the shell in raw mode on the alternate screen.
|
||||
const { promise: quit, resolve: leave } = Promise.withResolvers<void>();
|
||||
const screen = openScreen(onKey, draw);
|
||||
|
||||
function frame(): string[] {
|
||||
const cols = screen.columns;
|
||||
const tabs = PANES.map((p, i) =>
|
||||
i === pane ? bold(fg(FG, ` ${p.name} `)) : fg(MUTED, ` ${p.name} `),
|
||||
).join(fg(LINE, "│"));
|
||||
const clock = fg(MUTED, `day ${stats.campaign.day}/${stats.campaign.days} · streak ${stats.streak} `);
|
||||
const header = padTo(tabs, Math.max(0, cols - width(clock))) + clock;
|
||||
|
||||
// header + rule + body + footer fills the terminal exactly.
|
||||
const body = PANES[pane]!.render(stats, cols);
|
||||
const height = Math.max(1, screen.rows - 3);
|
||||
offset = Math.max(0, Math.min(offset, Math.max(0, body.length - height)));
|
||||
const scrolled =
|
||||
body.length > height ? ` · ${offset + 1}-${Math.min(offset + height, body.length)}/${body.length}` : "";
|
||||
|
||||
return [
|
||||
header,
|
||||
fg(LINE, "─".repeat(cols)),
|
||||
...body.slice(offset, offset + height),
|
||||
fg(MUTED, `Tab/←→ pane · ↑↓ scroll · r refresh · q quit${scrolled}`) + (note ? ` ${fg(YELLOW, note)}` : ""),
|
||||
];
|
||||
}
|
||||
|
||||
function draw(): void {
|
||||
screen.draw(frame());
|
||||
}
|
||||
|
||||
function onKey(chunk: string): void {
|
||||
switch (chunk) {
|
||||
// Raw mode delivers Ctrl-C as a byte, not a signal, so it is handled here.
|
||||
case "q":
|
||||
case "\x03":
|
||||
case "\x1b":
|
||||
leave();
|
||||
return;
|
||||
case "\t":
|
||||
case "\x1b[C":
|
||||
case "l":
|
||||
pane = (pane + 1) % PANES.length;
|
||||
offset = 0;
|
||||
break;
|
||||
case "\x1b[Z":
|
||||
case "\x1b[D":
|
||||
case "h":
|
||||
pane = (pane + PANES.length - 1) % PANES.length;
|
||||
offset = 0;
|
||||
break;
|
||||
case "\x1b[A":
|
||||
case "k":
|
||||
offset = Math.max(0, offset - 1);
|
||||
break;
|
||||
case "\x1b[B":
|
||||
case "j":
|
||||
offset += 1;
|
||||
break;
|
||||
case "r":
|
||||
note = "refreshing…";
|
||||
draw();
|
||||
void fetchStats()
|
||||
.then((fresh) => {
|
||||
stats = fresh;
|
||||
note = "";
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
note = `refresh failed: ${err instanceof Error ? err.message : String(err)}`;
|
||||
})
|
||||
.finally(draw);
|
||||
return;
|
||||
default:
|
||||
if (chunk >= "1" && chunk <= String(PANES.length)) {
|
||||
pane = Number(chunk) - 1;
|
||||
offset = 0;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
||||
try {
|
||||
draw();
|
||||
await quit;
|
||||
} finally {
|
||||
screen.close();
|
||||
}
|
||||
+21
-1
@@ -359,6 +359,24 @@ function ensureLabel(page: string, h: Header): string {
|
||||
return page.replace(/^sidebar:$/m, `sidebar:\n${label}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset just past the script-owned `## Solution` section: the next top-level
|
||||
* heading that is not inside a code fence, or end of file. Sections a human
|
||||
* appends after the solution (`## Explanation`, …) survive regeneration.
|
||||
*/
|
||||
function solutionEnd(page: string, from: number): number {
|
||||
const lines = page.slice(from).split("\n");
|
||||
let offset = from + lines[0]!.length + 1; // past the '## Solution' line itself
|
||||
let fenced = false;
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const line = lines[i]!;
|
||||
if (line.startsWith("```")) fenced = !fenced;
|
||||
else if (!fenced && line.startsWith("## ")) return offset;
|
||||
offset += line.length + 1;
|
||||
}
|
||||
return page.length;
|
||||
}
|
||||
|
||||
// Existing docs pages, keyed by leetcode number from the frontmatter title.
|
||||
const pages = new Map<number, string>(); // num -> path relative to DOCS
|
||||
for (const rel of (await Array.fromAsync(new Bun.Glob("**/*.mdx").scan(DOCS))).sort()) {
|
||||
@@ -386,7 +404,9 @@ for (const sol of ordered) {
|
||||
report.push([workRef, existing, "ERROR: no '## Solution' heading"]);
|
||||
continue;
|
||||
}
|
||||
const next = ensureLabel(page.slice(0, at) + solutionSection(sol), sol.header);
|
||||
const tail = page.slice(solutionEnd(page, at));
|
||||
const body = page.slice(0, at) + solutionSection(sol) + (tail ? `\n${tail}` : "");
|
||||
const next = ensureLabel(body, sol.header);
|
||||
const actions: string[] = [];
|
||||
if (existing !== name) {
|
||||
await rename(join(DOCS, existing), join(DOCS, name));
|
||||
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Terminal drawing primitives for the stats dashboard: colour, block-glyph
|
||||
* charts, and the alternate-screen plumbing.
|
||||
*
|
||||
* Hand-rolled for the same reason apps/api/src/charts.ts and png.ts are — the
|
||||
* repo carries near-zero runtime dependencies, and everything a progress
|
||||
* dashboard needs is a bar, a sparkline and a coloured cell. The palette is
|
||||
* charts.ts's, so the terminal, the README SVGs and the digest email are the
|
||||
* same picture in three renderers.
|
||||
*
|
||||
* Side-effect-free on import except for reading the colour environment once;
|
||||
* nothing is written to the terminal until openScreen() is called.
|
||||
*/
|
||||
|
||||
// ── palette (apps/api/src/charts.ts) ─────────────────────────────
|
||||
|
||||
export const FG = "#fafafa"; // zinc-50 text
|
||||
export const MUTED = "#a1a1aa"; // zinc-400 secondary text
|
||||
export const LINE = "#27272a"; // zinc-800 structure / empty track
|
||||
export const GREEN = "#16a34a"; // green-600 (core / pass)
|
||||
export const BLUE = "#2563eb"; // blue-600 (optional)
|
||||
export const YELLOW = "#eab308"; // yellow-500 (attention)
|
||||
export const RED = "#f87171"; // red-400 (fail, on dark)
|
||||
|
||||
/** GitHub dark-mode contribution hues; level 0 is the empty cell. */
|
||||
export const GREENS = ["#27272a", "#0e4429", "#006d32", "#26a641", "#39d353"];
|
||||
|
||||
// ── colour ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Honour NO_COLOR and pipes, and let FORCE_COLOR override both — the dashboard
|
||||
* is legible without colour, and `bun run stats | less -R` should still be.
|
||||
*/
|
||||
export const COLOR =
|
||||
process.env.FORCE_COLOR !== undefined
|
||||
? process.env.FORCE_COLOR !== "0"
|
||||
: process.env.NO_COLOR === undefined && process.stdout.isTTY === true;
|
||||
|
||||
const RESET = "\x1b[0m";
|
||||
|
||||
function channels(hex: string): string {
|
||||
return `${parseInt(hex.slice(1, 3), 16)};${parseInt(hex.slice(3, 5), 16)};${parseInt(hex.slice(5, 7), 16)}`;
|
||||
}
|
||||
|
||||
export function fg(hex: string, text: string): string {
|
||||
return COLOR ? `\x1b[38;2;${channels(hex)}m${text}${RESET}` : text;
|
||||
}
|
||||
|
||||
export function bg(hex: string, text: string): string {
|
||||
return COLOR ? `\x1b[48;2;${channels(hex)}m${text}${RESET}` : text;
|
||||
}
|
||||
|
||||
export function bold(text: string): string {
|
||||
return COLOR ? `\x1b[1m${text}${RESET}` : text;
|
||||
}
|
||||
|
||||
const ANSI = /\x1b\[[0-9;]*m/g;
|
||||
|
||||
/** Printable width — every glyph used here is one cell wide. */
|
||||
export function width(text: string): number {
|
||||
return text.replace(ANSI, "").length;
|
||||
}
|
||||
|
||||
/** Pad or cut a *plain* string to exactly n cells; colour it afterwards. */
|
||||
export function cell(text: string, n: number, align: "left" | "right" = "left"): string {
|
||||
const clipped = text.length > n ? `${text.slice(0, Math.max(0, n - 1))}…` : text;
|
||||
return align === "left" ? clipped.padEnd(n) : clipped.padStart(n);
|
||||
}
|
||||
|
||||
/** Pad an already-coloured string to n cells. */
|
||||
export function padTo(text: string, n: number): string {
|
||||
return text + " ".repeat(Math.max(0, n - width(text)));
|
||||
}
|
||||
|
||||
// ── charts ───────────────────────────────────────────────────────
|
||||
|
||||
const EIGHTHS = ["", "▏", "▎", "▍", "▌", "▋", "▊", "▉"];
|
||||
const SPARKS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
||||
|
||||
/**
|
||||
* Filled and empty. The two differ by GLYPH as well as by colour, so a bar
|
||||
* still reads under NO_COLOR or in a pipe — every chart here has to survive
|
||||
* `bun run stats > notes.txt`.
|
||||
*/
|
||||
const FILL = "█";
|
||||
const TRACK = "░";
|
||||
|
||||
/**
|
||||
* One bar over a muted track, `cells` wide. Sub-cell remainders use the
|
||||
* eighth-block glyphs, so one solved problem out of 161 still shows.
|
||||
*/
|
||||
export function bar(value: number, total: number, cells: number, color = GREEN): string {
|
||||
const units = Math.round(Math.max(0, Math.min(1, total > 0 ? value / total : 0)) * cells * 8);
|
||||
const filled = FILL.repeat(Math.floor(units / 8)) + EIGHTHS[units % 8]!;
|
||||
return fg(color, filled) + fg(LINE, TRACK.repeat(Math.max(0, cells - filled.length)));
|
||||
}
|
||||
|
||||
export interface Segment {
|
||||
value: number;
|
||||
color: string;
|
||||
/** Defaults to the solid block; pass TRACK-ish glyphs for "not done yet". */
|
||||
glyph?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Segments laid end to end, `total` mapped to `cells`. Anything short of
|
||||
* `total` is left BLANK, not tracked: bars sharing a scale must show their
|
||||
* different lengths. A caller wanting a visible remainder passes it as a
|
||||
* segment of its own.
|
||||
*
|
||||
* Non-zero segments are floored but never vanish; the overshoot that floor
|
||||
* creates is taken back from the widest segment, which can spare it.
|
||||
*/
|
||||
export function stackedBar(segments: Segment[], total: number, cells: number): string {
|
||||
if (total <= 0) return " ".repeat(cells);
|
||||
const widths = segments.map((s) =>
|
||||
s.value > 0 ? Math.max(1, Math.floor((s.value / total) * cells)) : 0,
|
||||
);
|
||||
let over = widths.reduce((a, b) => a + b, 0) - cells;
|
||||
while (over > 0) {
|
||||
const i = widths.indexOf(Math.max(...widths));
|
||||
if (widths[i]! <= 1) break;
|
||||
widths[i]!--;
|
||||
over--;
|
||||
}
|
||||
const drawn = widths.map((n, i) => fg(segments[i]!.color, (segments[i]!.glyph ?? FILL).repeat(n)));
|
||||
return drawn.join("") + " ".repeat(Math.max(0, cells - widths.reduce((a, b) => a + b, 0)));
|
||||
}
|
||||
|
||||
/**
|
||||
* One cell of the campaign heatmap. With colour it is the GREENS ramp the
|
||||
* README and the digest use; without it, the same five levels as shading.
|
||||
*/
|
||||
export function heatCell(attempts: number, body: string): string {
|
||||
const level = Math.min(attempts, GREENS.length - 1);
|
||||
return COLOR ? bg(GREENS[level]!, body) : [" ", "·", "▒", "▓", "█"][level]!.repeat(body.length);
|
||||
}
|
||||
|
||||
/** One glyph per value, scaled to the largest. All-zero renders as a floor. */
|
||||
export function sparkline(values: number[], color = GREEN): string {
|
||||
const peak = Math.max(...values, 0);
|
||||
const line = values
|
||||
.map((v) => (peak === 0 ? SPARKS[0]! : SPARKS[Math.min(7, Math.round((v / peak) * 7))]!))
|
||||
.join("");
|
||||
return fg(color, line);
|
||||
}
|
||||
|
||||
// ── layout ───────────────────────────────────────────────────────
|
||||
|
||||
/** A section rule that fills the width: `── name ────────────`. */
|
||||
export function rule(title: string, cells: number): string {
|
||||
const label = ` ${title} `;
|
||||
return (
|
||||
fg(LINE, "──") + fg(MUTED, label) + fg(LINE, "─".repeat(Math.max(0, cells - 2 - label.length)))
|
||||
);
|
||||
}
|
||||
|
||||
// ── screen ───────────────────────────────────────────────────────
|
||||
|
||||
const ALT_ON = "\x1b[?1049h\x1b[?25l";
|
||||
const ALT_OFF = "\x1b[?25h\x1b[?1049l";
|
||||
|
||||
export interface Screen {
|
||||
readonly rows: number;
|
||||
readonly columns: number;
|
||||
/** Draw a frame, clipped to the terminal height. */
|
||||
draw(lines: string[]): void;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take over the terminal: alternate buffer, hidden cursor, raw keys. `onKey`
|
||||
* receives the raw chunk (`"\t"`, `"\x1b[B"`, `"q"`, …) — decoding belongs to
|
||||
* the app, which is the only thing that knows what a key means.
|
||||
*
|
||||
* The caller MUST close() in a finally: raw mode survives a thrown exception.
|
||||
*/
|
||||
export function openScreen(onKey: (chunk: string) => void, onResize: () => void): Screen {
|
||||
const out = process.stdout;
|
||||
const input = process.stdin;
|
||||
|
||||
out.write(ALT_ON);
|
||||
input.setRawMode?.(true);
|
||||
input.setEncoding("utf8");
|
||||
input.resume();
|
||||
input.on("data", onKey);
|
||||
out.on("resize", onResize);
|
||||
|
||||
let closed = false;
|
||||
return {
|
||||
get rows() {
|
||||
return out.rows ?? 24;
|
||||
},
|
||||
get columns() {
|
||||
return out.columns ?? 80;
|
||||
},
|
||||
draw(lines: string[]): void {
|
||||
const visible = lines.slice(0, this.rows);
|
||||
// Home, then erase each line as it is rewritten plus everything below the
|
||||
// last one. No full clear, so a redraw does not flash, and no trailing
|
||||
// newline, which would scroll the alternate screen by a row per frame.
|
||||
out.write(`\x1b[H${visible.map((l) => `${l}\x1b[K`).join("\r\n")}\x1b[J`);
|
||||
},
|
||||
close(): void {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
input.off("data", onKey);
|
||||
out.off("resize", onResize);
|
||||
input.setRawMode?.(false);
|
||||
input.pause();
|
||||
out.write(ALT_OFF);
|
||||
},
|
||||
};
|
||||
}
|
||||
+21
-4
@@ -93,6 +93,25 @@ export function scanBucket(bucket: Bucket): Map<number, WorkFile[]> {
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this file a real solution rather than an empty scaffold? The same call
|
||||
* close-solved.ts makes before it closes an issue — a stub under work/1 is a
|
||||
* problem still unsolved, and owes no review.
|
||||
*/
|
||||
export function isSolution(file: WorkFile): boolean {
|
||||
const source = readFileSync(join(WORK, file.rel), "utf8");
|
||||
return isImplemented(source, file.rel.endsWith(".py") ? "py" : "js");
|
||||
}
|
||||
|
||||
/** Problems in a bucket with a solution on disk; scaffolds do not count. */
|
||||
export function solvedInBucket(bucket: Bucket): number {
|
||||
let solved = 0;
|
||||
for (const files of scanBucket(bucket).values()) {
|
||||
if (files.some(isSolution)) solved++;
|
||||
}
|
||||
return solved;
|
||||
}
|
||||
|
||||
/**
|
||||
* LC number -> ET date of the commit that first added a work/ file for it.
|
||||
*
|
||||
@@ -153,14 +172,12 @@ export function reviewQueues(today: string): Map<Window, Owed[]> {
|
||||
const dates = firstSolved();
|
||||
|
||||
// An empty scaffold under work/1 is a problem still unsolved, not one owing
|
||||
// a review — the same call close-solved.ts makes before it closes an issue.
|
||||
// a review.
|
||||
const solved: { file: WorkFile; date: string }[] = [];
|
||||
for (const files of scanBucket(1).values()) {
|
||||
const date = dates.get(files[0]!.lc);
|
||||
if (date === undefined) continue; // uncommitted: you only just solved it
|
||||
const file = files.find((f) =>
|
||||
isImplemented(readFileSync(join(WORK, f.rel), "utf8"), f.rel.endsWith(".py") ? "py" : "js"),
|
||||
);
|
||||
const file = files.find(isSolution);
|
||||
if (file) solved.push({ file, date });
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,20 @@ Can you come up with an algorithm that is less than O(n^2) time complexity?
|
||||
- `-10^9 <= target <= 10^9`
|
||||
- Only one valid answer exists.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["twoSum(nums, target)"]) --> I["seen = {} — value to index"]
|
||||
I --> L{"more (i, num) in nums?"}
|
||||
L -- no --> E(["return []"])
|
||||
L -- yes --> C["complement = target - num"]
|
||||
C --> H{"complement in seen?"}
|
||||
H -- yes --> R(["return [seen[complement], i]"])
|
||||
H -- no --> W["seen[num] = i"]
|
||||
W --> L
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
@@ -45,3 +59,7 @@ class Solution:
|
||||
seen[num] = i
|
||||
return []
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="KLlXCFG5TnA" title="Two Sum - Leetcode 1 - HashMap - Python" />
|
||||
@@ -27,6 +27,22 @@ Notice that you may not slant the container.
|
||||
- `2 <= n <= 10^5`
|
||||
- `0 <= height[i] <= 10^4`
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["maxArea(height)"]) --> I["res = 0, left = 0, right = len - 1"]
|
||||
I --> W{"left < right?"}
|
||||
W -- no --> E(["return res"])
|
||||
W -- yes --> A["area = (right - left) * min(height[left], height[right])"]
|
||||
A --> M["res = max(res, area)"]
|
||||
M --> C{"height[left] < height[right]?"}
|
||||
C -- yes --> L["left += 1 — drop the shorter wall"]
|
||||
C -- no --> R["right -= 1 — drop the shorter wall"]
|
||||
L --> W
|
||||
R --> W
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
@@ -46,3 +62,7 @@ class Solution:
|
||||
|
||||
return res
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="UuiTKBwPgAo" title="Container with Most Water - Leetcode 11 - Python" />
|
||||
@@ -23,6 +23,24 @@ sidebar:
|
||||
- `1 <= prices.length <= 10^5`
|
||||
- `0 <= prices[i] <= 10^4`
|
||||
|
||||
## Approach
|
||||
|
||||
::::warning
|
||||
The diagram traces the code below literally. That code does not run: `curr` and `ans` are never initialised, so the first `curr > left` test raises `NameError`, and `left` holds a price rather than an index, so `prices[left]` indexes by value.
|
||||
::::
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["maxProfit(prices)"]) --> I["left = min(prices)"]
|
||||
I --> F{"more right in range(len(prices))?"}
|
||||
F -- no --> E(["return ans"])
|
||||
F -- yes --> W{"curr > left?"}
|
||||
W -- yes --> P["curr -= prices[left]; left += 1"]
|
||||
P --> W
|
||||
W -- no --> M["ans = max(ans, curr)"]
|
||||
M --> F
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
@@ -36,3 +54,7 @@ class Solution:
|
||||
ans = max(ans, curr)
|
||||
return ans
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="1pkOgXD63yU" title="Sliding Window: Best Time to Buy and Sell Stock - Leetcode 121 - Python" />
|
||||
+14
@@ -26,6 +26,20 @@ sidebar:
|
||||
- `2 <= nums.length <= 500`
|
||||
- `0 <= nums[i] <= 100`
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["smallerNumbersThanCurrent(nums)"]) --> F["freq: count every value in nums"]
|
||||
F --> O["sorted = keys of freq, ascending"]
|
||||
O --> I["count = 0, smaller = {}"]
|
||||
I --> L{"more num in sorted?"}
|
||||
L -- yes --> A["smaller[num] = count — everything already passed is smaller"]
|
||||
A --> B["count += freq[num]"]
|
||||
B --> L
|
||||
L -- no --> M(["return nums.map(n => smaller[n])"])
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
|
||||
@@ -37,6 +37,18 @@ startValue = 4 | startValue = 5 | nums
|
||||
- `1 <= nums.length <= 100`
|
||||
- `-100 <= nums[i] <= 100`
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["minStartValue(nums)"]) --> I["prefix = [nums[0]]"]
|
||||
I --> L{"more i in 1..n-1?"}
|
||||
L -- yes --> P["prefix.push(prefix[i-1] + nums[i])"]
|
||||
P --> L
|
||||
L -- no --> M["min = smallest running total"]
|
||||
M --> R(["return max(1, 1 - min) — lift the dip to 1"])
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
|
||||
@@ -23,6 +23,19 @@ sidebar:
|
||||
- `1 <= arr.length <= 1000`
|
||||
- `0 <= arr[i] <= 1000`
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["countElements(arr)"]) --> I["arrSet = Set(arr), count = 0"]
|
||||
I --> L{"more n in arr?"}
|
||||
L -- no --> E(["return count"])
|
||||
L -- yes --> H{"arrSet has n + 1?"}
|
||||
H -- yes --> C["count++"]
|
||||
C --> L
|
||||
H -- no --> L
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
|
||||
@@ -27,6 +27,17 @@ sidebar:
|
||||
- `1 <= nums.length <= 1000`
|
||||
- `-10^6 <= nums[i] <= 10^6`
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["runningSum(nums)"]) --> I["prefix = [nums[0]]"]
|
||||
I --> L{"more i in 1..n-1?"}
|
||||
L -- yes --> P["prefix.push(prefix[i-1] + nums[i])"]
|
||||
P --> L
|
||||
L -- no --> E(["return prefix"])
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
|
||||
@@ -32,6 +32,28 @@ Notice that the solution set must not contain duplicate triplets.
|
||||
- `3 <= nums.length <= 3000`
|
||||
- `-10^5 <= nums[i] <= 10^5`
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["threeSum(nums)"]) --> O["nums.sort() — duplicates become adjacent"]
|
||||
O --> F{"more i in 0..n-1?"}
|
||||
F -- no --> E(["return result"])
|
||||
F -- yes --> D{"nums[i] == nums[i-1]?"}
|
||||
D -- yes --> F
|
||||
D -- no --> P["left = i+1, right = n-1, target = -nums[i]"]
|
||||
P --> W{"left < right?"}
|
||||
W -- no --> F
|
||||
W -- yes --> C["current = nums[left] + nums[right]"]
|
||||
C --> Q{"current vs target"}
|
||||
Q -- equal --> A["append triplet, skip equal neighbours, left += 1, right -= 1"]
|
||||
A --> W
|
||||
Q -- "current < target" --> L["left += 1 — need a bigger sum"]
|
||||
L --> W
|
||||
Q -- "current > target" --> R["right -= 1 — need a smaller sum"]
|
||||
R --> W
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
@@ -76,3 +98,7 @@ class Solution:
|
||||
|
||||
return result
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="jzZsG8n2R9A" title="3Sum - Leetcode 15 - Python" />
|
||||
@@ -28,6 +28,21 @@ sidebar:
|
||||
- `1 <= tokens.length <= 10^4`
|
||||
- `tokens[i]` is either an operator: "+", "-", "*", or "/", or an integer in the range [-200, 200].
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["evalRPN(tokens)"]) --> I["stack = [], operations = + - * /"]
|
||||
I --> L{"more token c?"}
|
||||
L -- no --> E(["return stack[0]"])
|
||||
L -- yes --> Q{"c is an operator?"}
|
||||
Q -- no --> N["stack.append(int(c))"]
|
||||
N --> L
|
||||
Q -- yes --> P["y = stack.pop(), x = stack.pop() — order matters"]
|
||||
P --> C["stack.append(operations[c](x, y))"]
|
||||
C --> L
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
@@ -53,3 +68,7 @@ class Solution:
|
||||
|
||||
return stack[0]
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="iu0082c4HDE" title="Evaluate Reverse Polish Notation - Leetcode 150 - Python" />
|
||||
@@ -35,6 +35,21 @@ Notice that rotating an array [a[0], a[1], a[2], ..., a[n-1]] 1 time results in
|
||||
- All the integers of `nums` are unique.
|
||||
- `nums` is sorted and rotated between 1 and `n` times.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["findMin(nums)"]) --> I["l = 0, r = n-1, lowest_index = -1"]
|
||||
I --> W{"l <= r?"}
|
||||
W -- no --> E(["return nums[lowest_index]"])
|
||||
W -- yes --> M["m = (l + r) // 2"]
|
||||
M --> C{"nums[m] <= nums[-1]?"}
|
||||
C -- yes --> A["in the right sorted run: record lowest_index = m, r = m - 1"]
|
||||
A --> W
|
||||
C -- no --> B["still in the left sorted run: l = m + 1"]
|
||||
B --> W
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
@@ -53,3 +68,7 @@ class Solution:
|
||||
|
||||
return nums[lowest_index]
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="nIVW4P8b1VA" title="Find Minimum in Rotated Sorted Array - Binary Search - Leetcode 153 - Python" />
|
||||
@@ -27,6 +27,18 @@ sidebar:
|
||||
- `1 <= nums.length <= 100`
|
||||
- `-100 <= nums[i] <= 100`
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["frequencySort(nums)"]) --> F["freq: count every value in nums"]
|
||||
F --> C["sort nums with a two-key comparator"]
|
||||
C --> K1["primary: freq[a] - freq[b] — rarer first"]
|
||||
C --> K2["tie-break: b - a — larger value first"]
|
||||
K1 --> R(["return nums"])
|
||||
K2 --> R
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
@@ -40,3 +52,7 @@ var frequencySort = function (nums) {
|
||||
return nums.sort((a, b) => freq[a] - freq[b] || b - a);
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="Evq1SfUbhBg" title="Sort Array by Increasing Frequency - Leetcode 1636 - Python" />
|
||||
@@ -35,6 +35,22 @@ Your solution must use only constant extra space.
|
||||
- `-1000 <= target <= 1000`
|
||||
- The tests are generated such that there is exactly one solution.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["twoSum(numbers, target)"]) --> I["i = 0, j = len - 1"]
|
||||
I --> W{"i < j?"}
|
||||
W -- no --> E(["return []"])
|
||||
W -- yes --> C["c = numbers[i] + numbers[j]"]
|
||||
C --> Q{"c vs target"}
|
||||
Q -- equal --> R(["return [i+1, j+1] — 1-indexed"])
|
||||
Q -- "c < target" --> L["i += 1 — grow the sum"]
|
||||
L --> W
|
||||
Q -- "c > target" --> H["j -= 1 — shrink the sum"]
|
||||
H --> W
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
@@ -53,3 +69,7 @@ class Solution:
|
||||
j-=1
|
||||
return []
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="cQ1Oz4ckceM" title="TWO SUM II - Amazon Coding Interview Question - Leetcode 167 - Python" />
|
||||
@@ -27,6 +27,19 @@ sidebar:
|
||||
- `1 <= nums.length <= 10^5`
|
||||
- `-10^9 <= nums[i] <= 10^9`
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["containsDuplicate(nums)"]) --> I["freq = {}"]
|
||||
I --> L{"more n in nums?"}
|
||||
L -- no --> E(["return false"])
|
||||
L -- yes --> C["freq[n] = (freq[n] or 0) + 1"]
|
||||
C --> Q{"freq[n] >= 2?"}
|
||||
Q -- yes --> R(["return true"])
|
||||
Q -- no --> L
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
@@ -45,3 +58,7 @@ var containsDuplicate = function(nums) {
|
||||
return false;
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="3OamzN90kPg" title="Contains Duplicate - Leetcode 217 - Python" />
|
||||
@@ -27,6 +27,17 @@ Can you solve the problem in O(1) extra space complexity? (The output array does
|
||||
- `-30 <= nums[i] <= 30`
|
||||
- The input is generated such that `answer[i]` is guaranteed to fit in a 32-bit integer.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["productExceptSelf(nums)"]) --> I["answer = ones(n), rightArr = ones(n)"]
|
||||
I --> P1["Pass 1 — left to right: answer[i] = nums[i-1] * answer[i-1]"]
|
||||
P1 --> P2["Pass 2 — right to left: rightArr[i] = nums[i+1] * rightArr[i+1]"]
|
||||
P2 --> P3["Pass 3: answer[i] *= rightArr[i]"]
|
||||
P3 --> E(["return answer — no division needed"])
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
@@ -51,3 +62,7 @@ var productExceptSelf = function(nums) {
|
||||
return answer;
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="bNvIQI2wAjk" title="Product of Array Except Self - Leetcode 238 - Python" />
|
||||
@@ -34,6 +34,18 @@ Could you implement a solution using only O(1) extra space complexity and O(n) r
|
||||
- `0 <= nums[i] <= n`
|
||||
- All the numbers of `nums` are unique.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["missingNumber(nums)"]) --> I["numSet = Set(nums), expectedCount = n + 1"]
|
||||
I --> L{"more i in 0..expectedCount-1?"}
|
||||
L -- yes --> Q{"numSet has i?"}
|
||||
Q -- no --> R(["return i"])
|
||||
Q -- yes --> L
|
||||
L -- no --> E(["return -1"])
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
@@ -53,3 +65,7 @@ var missingNumber = function(nums) {
|
||||
return -1;
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="WnPLSRLSANE" title="Missing Number - Blind 75 - Leetcode 268 - Python" />
|
||||
@@ -20,6 +20,21 @@ sidebar:
|
||||
- `0 <= left <= right < nums.length`
|
||||
- At most `10^4` calls will be made to `sumRange`.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph build["constructor(nums) — O(n) once"]
|
||||
A["prefix = [0]"] --> B{"more n in nums?"}
|
||||
B -- yes --> C["prefix.push(last + n)"]
|
||||
C --> B
|
||||
end
|
||||
build --> Q
|
||||
subgraph Q["sumRange(left, right) — O(1) per call"]
|
||||
D(["return prefix[right + 1] - prefix[left]"])
|
||||
end
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
@@ -50,3 +65,7 @@ class NumArray {
|
||||
* var param_1 = obj.sumRange(left,right)
|
||||
*/
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="2pndAmo_sMA" title="Range Sum Query Immutable - Leetcode 303 - Python" />
|
||||
@@ -32,6 +32,29 @@ You must write an algorithm with O(log n) runtime complexity.
|
||||
- `nums` is an ascending array that is possibly rotated.
|
||||
- `-10^4 <= target <= 10^4`
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["search(nums, target)"]) --> I["l = 0, r = n - 1"]
|
||||
I --> W{"l <= r?"}
|
||||
W -- no --> E(["return -1"])
|
||||
W -- yes --> M["mid = floor((l + r) / 2)"]
|
||||
M --> F{"nums[mid] == target?"}
|
||||
F -- yes --> R(["return mid"])
|
||||
F -- no --> H{"nums[l] <= nums[mid]? — left half sorted"}
|
||||
H -- yes --> A{"target outside [nums[l], nums[mid]]?"}
|
||||
A -- yes --> A1["l = mid + 1 — search the right half"]
|
||||
A -- no --> A2["r = mid - 1"]
|
||||
H -- no --> B{"target outside [nums[mid], nums[r]]?"}
|
||||
B -- yes --> B1["r = mid - 1 — search the left half"]
|
||||
B -- no --> B2["l = mid + 1"]
|
||||
A1 --> W
|
||||
A2 --> W
|
||||
B1 --> W
|
||||
B2 --> W
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
@@ -69,3 +92,7 @@ var search = function(nums, target) {
|
||||
return -1;
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="U8XENwh8Oy8" title="Search in rotated sorted array - Leetcode 33 - Python" />
|
||||
@@ -31,6 +31,16 @@ Your algorithm's time complexity must be better than O(n log n), where n is the
|
||||
- `k` is in the range `[1, the number of unique elements in the array]`.
|
||||
- It is guaranteed that the answer is unique.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["topKFrequent(nums, k)"]) --> F["freq: count every value in nums"]
|
||||
F --> K["keys of freq, cast to Number"]
|
||||
K --> O["sort by freq[b] - freq[a] — most frequent first"]
|
||||
O --> R(["return slice(0, k)"])
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
@@ -49,3 +59,7 @@ var topKFrequent = function (nums, k) {
|
||||
.slice(0, k);
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="YPTqKIgVk-k" title="Top K Frequent Elements - Bucket Sort - Leetcode 347 - Python" />
|
||||
@@ -31,6 +31,22 @@ You must write an algorithm with O(log n) runtime complexity.
|
||||
- `nums` contains distinct values sorted in ascending order.
|
||||
- `-10^4 <= target <= 10^4`
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["searchInsert(nums, target)"]) --> I["l = 0, r = n - 1"]
|
||||
I --> W{"l <= r?"}
|
||||
W -- yes --> M["m = (l + r) // 2"]
|
||||
M --> Q{"nums[m] vs target"}
|
||||
Q -- equal --> R(["return m"])
|
||||
Q -- "nums[m] < target" --> L["l = m + 1"]
|
||||
L --> W
|
||||
Q -- "nums[m] > target" --> H["r = m - 1"]
|
||||
H --> W
|
||||
W -- no --> E(["return (l + r) // 2 + 1 — the insert slot"])
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
@@ -49,3 +65,7 @@ class Solution:
|
||||
|
||||
return (l + r) // 2 + 1
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="K-RYzDZkzCI" title="Search Insert Position - Binary Search - Leetcode 35 - Python" />
|
||||
@@ -27,6 +27,21 @@ sidebar:
|
||||
- `0 <= strs[i].length <= 100`
|
||||
- `strs[i]` consists of lowercase English letters.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["groupAnagrams(strs)"]) --> K["sorted[i] = letters of strs[i], sorted — the anagram key"]
|
||||
K --> I["anagrams = {}"]
|
||||
I --> L{"more i in 0..n-1?"}
|
||||
L -- no --> E(["return Object.values(anagrams)"])
|
||||
L -- yes --> Q{"anagrams has sorted[i]?"}
|
||||
Q -- no --> N["anagrams[sorted[i]] = [strs[i]]"]
|
||||
Q -- yes --> P["anagrams[sorted[i]].push(strs[i])"]
|
||||
N --> L
|
||||
P --> L
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
@@ -49,3 +64,7 @@ var groupAnagrams = function(strs) {
|
||||
return Object.values(anagrams);
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="vzdNOK2oB2E" title="Group Anagrams - Categorize Strings by Count - Leetcode 49" />
|
||||
@@ -22,6 +22,21 @@ sidebar:
|
||||
- `-1000 <= nums[i] <= 1000`
|
||||
- `-10^7 <= k <= 10^7`
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["subarraySum(nums, k)"]) --> I["map = {0: 1} — the empty prefix; sum = 0, count = 0"]
|
||||
I --> L{"more n in nums?"}
|
||||
L -- no --> E(["return count"])
|
||||
L -- yes --> A["sum += n"]
|
||||
A --> Q{"map has sum - k?"}
|
||||
Q -- yes --> C["count += map.get(sum - k) — every earlier prefix that closes a window"]
|
||||
Q -- no --> W["map.set(sum, count of sum + 1)"]
|
||||
C --> W
|
||||
W --> L
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
@@ -49,3 +64,7 @@ var subarraySum = function(nums, k) {
|
||||
return count
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="fFVZt-6sgyo" title="Subarray Sum Equals K - Prefix Sums - Leetcode 560 - Python" />
|
||||
@@ -29,6 +29,22 @@ You must write an algorithm with O(log n) runtime complexity.
|
||||
- All the integers in `nums` are unique.
|
||||
- `nums` is sorted in ascending order.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["search(nums, target)"]) --> I["left = 0, right = n - 1"]
|
||||
I --> W{"left <= right?"}
|
||||
W -- no --> E(["return -1"])
|
||||
W -- yes --> M["mid = left + floor((right - left) / 2)"]
|
||||
M --> Q{"nums[mid] vs target"}
|
||||
Q -- equal --> R(["return mid"])
|
||||
Q -- "nums[mid] < target" --> L["left = mid + 1"]
|
||||
L --> W
|
||||
Q -- "nums[mid] > target" --> H["right = mid - 1"]
|
||||
H --> W
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
@@ -54,3 +70,7 @@ var search = function(nums, target) {
|
||||
return -1;
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="s4DPM8ct1pI" title="Binary Search - Leetcode 704 - Python" />
|
||||
@@ -25,6 +25,20 @@ sidebar:
|
||||
- `1 <= temperatures.length <= 10^5`
|
||||
- `30 <= temperatures[i] <= 100`
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["dailyTemperatures(temperatures)"]) --> I["ans = zeros(n), stack = [] — indices of days still waiting"]
|
||||
I --> L{"more (i, temp)?"}
|
||||
L -- no --> E(["return ans"])
|
||||
L -- yes --> W{"stack non-empty and temperatures[stack[-1]] < temp?"}
|
||||
W -- yes --> P["prev = stack.pop(); ans[prev] = i - prev — today resolves that day"]
|
||||
P --> W
|
||||
W -- no --> A["stack.append(i)"]
|
||||
A --> L
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
@@ -39,3 +53,7 @@ class Solution:
|
||||
stack.append(i)
|
||||
return ans
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="cTBiBSnjO3c" title="Daily Temperatures - Monotonic Stack - Leetcode 739 - Python" />
|
||||
@@ -27,6 +27,32 @@ You must write a solution in O(log(m * n)) time complexity.
|
||||
- `1 <= m, n <= 100`
|
||||
- `-10^4 <= matrix[i][j], target <= 10^4`
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["searchMatrix(matrix, target)"]) --> I["top = 0, bot = ROWS - 1"]
|
||||
I --> W1{"top <= bot?"}
|
||||
W1 -- yes --> R1["row = (top + bot) // 2"]
|
||||
R1 --> C1{"target vs that row's range"}
|
||||
C1 -- "above matrix[row][-1]" --> U["top = row + 1"]
|
||||
C1 -- "below matrix[row][0]" --> V["bot = row - 1"]
|
||||
C1 -- inside --> B(["break — row found"])
|
||||
U --> W1
|
||||
V --> W1
|
||||
W1 -- no --> F(["return False — no row can hold it"])
|
||||
B --> P["row = (top + bot) // 2; l = 0, r = COLS - 1"]
|
||||
P --> W2{"l <= r?"}
|
||||
W2 -- no --> G(["return False"])
|
||||
W2 -- yes --> M["m = (l + r) // 2"]
|
||||
M --> C2{"target vs matrix[row][m]"}
|
||||
C2 -- greater --> X["l = m + 1"]
|
||||
C2 -- smaller --> Y["r = m - 1"]
|
||||
C2 -- equal --> T(["return True"])
|
||||
X --> W2
|
||||
Y --> W2
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
@@ -59,3 +85,7 @@ class Solution:
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="Ber2pi2C0j0" title="Search a 2D Matrix - Leetcode 74 - Python" />
|
||||
@@ -32,6 +32,22 @@ sidebar:
|
||||
- All the values of `position` are unique.
|
||||
- `0 < speed[i] <= 10^6`
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["carFleet(target, position, speed)"]) --> O["zip and sort by position, descending — nearest the target first"]
|
||||
O --> I["stack = [] — arrival time of each fleet's leader"]
|
||||
I --> L{"more (pos, spd)?"}
|
||||
L -- no --> E(["return len(stack) — one entry per fleet"])
|
||||
L -- yes --> T["time = (target - pos) / spd"]
|
||||
T --> Q{"stack empty or time > stack[-1]?"}
|
||||
Q -- yes --> A["stack.append(time) — slower, so it starts a new fleet"]
|
||||
Q -- no --> B["discard — it catches the car ahead and joins that fleet"]
|
||||
A --> L
|
||||
B --> L
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
@@ -45,3 +61,7 @@ class Solution:
|
||||
|
||||
return len(stack)
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="Pr6T-3yB9RM" title="Car Fleet - Leetcode 853 - Python" />
|
||||
@@ -26,6 +26,22 @@ sidebar:
|
||||
- `piles.length <= h <= 10^9`
|
||||
- `1 <= piles[i] <= 10^9`
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["minEatingSpeed(piles, h)"]) --> I["l = 1, r = max(piles)"]
|
||||
I --> W{"l < r?"}
|
||||
W -- no --> E(["return l — the smallest workable speed"])
|
||||
W -- yes --> M["mid = floor((l + r) / 2)"]
|
||||
M --> K["kWorks(mid): hours = sum of ceil(p / mid)"]
|
||||
K --> Q{"hours <= h?"}
|
||||
Q -- yes --> A["r = mid — keep mid, try slower"]
|
||||
Q -- no --> B["l = mid + 1 — too slow, speed up"]
|
||||
A --> W
|
||||
B --> W
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
@@ -60,3 +76,7 @@ var minEatingSpeed = function(piles, h) {
|
||||
return l;
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="U2SozAs9RzA" title="Koko Eating Bananas - Binary Search - Leetcode 875 - Python" />
|
||||
@@ -27,6 +27,21 @@ Squaring each element and sorting the new array is very trivial, could you find
|
||||
- `-10^4 <= nums[i] <= 10^4`
|
||||
- `nums` is sorted in non-decreasing order.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["sortedSquares(nums)"]) --> I["ans = Array(n), left = 0, right = n - 1"]
|
||||
I --> L{"more i from n-1 down to 0?"}
|
||||
L -- no --> E(["return ans"])
|
||||
L -- yes --> C{"abs(nums[left]) < abs(nums[right])?"}
|
||||
C -- yes --> R["square = nums[right]; right--"]
|
||||
C -- no --> F["square = nums[left]; left++"]
|
||||
R --> W["ans[i] = square * square — fill from the back, largest first"]
|
||||
F --> W
|
||||
W --> L
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
@@ -53,3 +68,7 @@ var sortedSquares = function(nums) {
|
||||
return ans;
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="FPCZsG_AkUg" title="Squares of a Sorted Array - Leetcode 977 - Python" />
|
||||
@@ -33,6 +33,20 @@ Can you solve it using O(1) (i.e. constant) memory?
|
||||
- `-10^5 <= Node.val <= 10^5`
|
||||
- pos is -1 or a valid index in the linked-list.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["hasCycle(head)"]) --> I["fast = slow = head"]
|
||||
I --> W{"fast and fast.next?"}
|
||||
W -- no --> E(["return False — ran off the end, no cycle"])
|
||||
W -- yes --> A["fast = fast.next.next — two steps"]
|
||||
A --> B["slow = slow.next — one step"]
|
||||
B --> Q{"fast is slow?"}
|
||||
Q -- yes --> R(["return True — the gap closed, so it loops"])
|
||||
Q -- no --> W
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
@@ -54,3 +68,7 @@ class Solution:
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="gBTe7lFR3vc" title="Linked List Cycle - Floyd's Tortoise and Hare - Leetcode 141 - Python" />
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: '146. LRU Cache'
|
||||
description: Design a data structure that follows the constraints of a Least Recently Used (LRU) cache
|
||||
sidebar:
|
||||
label: 'LRU Cache'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
<Badge variant="accent">Hybrid Structures</Badge>
|
||||
|
||||
### Example 1:
|
||||
- Input: ``
|
||||
- Output: ``
|
||||
- Explanation: Input ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"] `[[2]`, [1, 1], [2, 2], `[1]`, [3, 3], `[2]`, [4, 4], `[1]`, `[3]`, [4]] Output [null, null, null, `1`, null, `-1`, null, `-1`, `3`, 4] Explanation LRUCache lRUCache = new LRUCache(2); lRUCache.put(1, `1`); // cache is {1=1} lRUCache.put(2, `2`); // cache is {1=1, 2=2} lRUCache.get(1); // return `1` lRUCache.put(3, `3`); // LRU `key` was `2`, evicts `key 2`, cache is {1=1, 3=3} lRUCache.get(2); // returns `-1` (not found) lRUCache.put(4, `4`); // LRU `key` was `1`, evicts `key 1`, cache is {4=4, 3=3} lRUCache.get(1); // return `-1` (not found) lRUCache.get(3); // return `3` lRUCache.get(4); // return `4`
|
||||
|
||||
### Constraints:
|
||||
|
||||
- `1 <= capacity <= 3000`
|
||||
- `0 <= key <= 10^4`
|
||||
- `0 <= value <= 10^5`
|
||||
- At most 2 * 10^5 calls will be made to get and put.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph state["state — dict for O(1) lookup, doubly linked list for O(1) reorder"]
|
||||
H["head — least recent"] --- N["... nodes ..."] --- T["tail — most recent"]
|
||||
end
|
||||
subgraph get["get(key)"]
|
||||
G1{"key in cache?"} -- no --> G2(["return -1"])
|
||||
G1 -- yes --> G3["_remove_node then _add_node — move it beside the tail"]
|
||||
G3 --> G4(["return node.value"])
|
||||
end
|
||||
subgraph put["put(key, value)"]
|
||||
P1{"key in cache?"} -- yes --> P2["update value, _remove_node then _add_node"]
|
||||
P1 -- no --> P3["new Node, cache[key] = node, _add_node"]
|
||||
P3 --> P4{"len(cache) > capacity?"}
|
||||
P4 -- yes --> P5["evict head.next — the least recent — and del cache[lru.key]"]
|
||||
P4 -- no --> P6(["done"])
|
||||
P2 --> P6
|
||||
P5 --> P6
|
||||
end
|
||||
state --> get
|
||||
state --> put
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
class Node:
|
||||
def __init__(self, key=0, value=0):
|
||||
self.key = key
|
||||
self.value = value
|
||||
self.prev = None
|
||||
self.next = None
|
||||
|
||||
|
||||
class LRUCache:
|
||||
def __init__(self, capacity: int):
|
||||
self.capacity = capacity
|
||||
self.cache = {}
|
||||
self.head = Node()
|
||||
self.tail = Node()
|
||||
self.head.next = self.tail # pyright: ignore[reportAttributeAccessIssue]
|
||||
self.tail.prev = self.head # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
def _remove_node(self, node):
|
||||
prev_node = node.prev
|
||||
next_node = node.next
|
||||
prev_node.next = next_node
|
||||
next_node.prev = prev_node
|
||||
|
||||
def _add_node(self, node):
|
||||
# Insert just before the tail (most recent at end)
|
||||
node.next = self.tail
|
||||
node.prev = self.tail.prev
|
||||
self.tail.prev.next = node # pyright: ignore[reportAttributeAccessIssue]
|
||||
self.tail.prev = node
|
||||
|
||||
def get(self, key: int) -> int:
|
||||
if key not in self.cache:
|
||||
return -1
|
||||
|
||||
node = self.cache[key]
|
||||
self._remove_node(node)
|
||||
self._add_node(node)
|
||||
return node.value
|
||||
|
||||
def put(self, key: int, value: int) -> None:
|
||||
if key in self.cache:
|
||||
node = self.cache[key]
|
||||
node.value = value
|
||||
self._remove_node(node)
|
||||
self._add_node(node)
|
||||
else:
|
||||
new_node = Node(key, value)
|
||||
self.cache[key] = new_node
|
||||
self._add_node(new_node)
|
||||
|
||||
if len(self.cache) > self.capacity:
|
||||
lru = self.head.next # Oldest node is next to the head
|
||||
self._remove_node(lru)
|
||||
del self.cache[lru.key] # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
|
||||
# Your LRUCache object will be instantiated and called as such:
|
||||
# obj = LRUCache(capacity)
|
||||
# param_1 = obj.get(key)
|
||||
# obj.put(key,value)
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="7ABFKPK2hD4" title="LRU Cache - Twitch Interview Question - Leetcode 146" />
|
||||
@@ -22,6 +22,19 @@ sidebar:
|
||||
- `1 <= sentence.length <= 1000`
|
||||
- `sentence` consists of lowercase English letters.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["checkIfPangram(sentence)"]) --> I["freq = {}"]
|
||||
I --> L{"more char c?"}
|
||||
L -- yes --> C["freq[c] = (freq[c] or 0) + 1"]
|
||||
C --> L
|
||||
L -- no --> Q{"distinct keys in freq == 26?"}
|
||||
Q -- yes --> R(["return true"])
|
||||
Q -- no --> F(["return false"])
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
|
||||
@@ -25,6 +25,19 @@ What if the inputs contain Unicode characters? How would you adapt your solution
|
||||
- `1 <= s.length, t.length <= 5 * 10^4`
|
||||
- `s` and `t` consist of lowercase English letters.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["isAnagram(s, t)"]) --> I["freq: count every char of s"]
|
||||
I --> L{"more char c in t?"}
|
||||
L -- no --> E(["return true"])
|
||||
L -- yes --> Q{"freq[c] missing or 0?"}
|
||||
Q -- yes --> R(["return false — t has a char s cannot cover"])
|
||||
Q -- no --> D["freq[c] -= 1"]
|
||||
D --> L
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
@@ -46,3 +59,7 @@ var isAnagram = function(s, t) {
|
||||
return true;
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="9UtInBqnCgA" title="Valid Anagram - Leetcode 242 - Python" />
|
||||
@@ -28,6 +28,21 @@ sidebar:
|
||||
- `0 <= s.length <= 10^5`
|
||||
- `s` consists of English letters, digits, symbols and spaces.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["lengthOfLongestSubstring(s)"]) --> I["left = 0, ans = 0, window = set()"]
|
||||
I --> L{"more (right, c) in s?"}
|
||||
L -- no --> E(["return ans"])
|
||||
L -- yes --> W{"c already in window?"}
|
||||
W -- yes --> P["window.remove(s[left]); left += 1 — shrink until c is free"]
|
||||
P --> W
|
||||
W -- no --> A["window.add(c)"]
|
||||
A --> M["ans = max(ans, right - left + 1)"]
|
||||
M --> L
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
@@ -45,3 +60,7 @@ class Solution:
|
||||
|
||||
return ans
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="wiGpQwVHdE0" title="Longest Substring Without Repeating Characters - Leetcode 3 - Python" />
|
||||
@@ -26,6 +26,18 @@ sidebar:
|
||||
- `1 <= s.length <= 10^5`
|
||||
- `s` consists of only lowercase English letters.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["firstUniqChar(s)"]) --> F["Pass 1 — freq: count every char of s"]
|
||||
F --> L{"Pass 2 — more i in 0..len-1?"}
|
||||
L -- no --> E(["return -1"])
|
||||
L -- yes --> Q{"freq[s[i]] == 1?"}
|
||||
Q -- yes --> R(["return i — leftmost wins because the scan is in order"])
|
||||
Q -- no --> L
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
@@ -45,3 +57,7 @@ var firstUniqChar = function (s) {
|
||||
return -1;
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="rBENYgWy3xU" title="First Unique Character in a String - Leetcode 387 - Python" />
|
||||
@@ -24,6 +24,21 @@ sidebar:
|
||||
- `s` consists of only uppercase English letters.
|
||||
- `0 <= k <= s.length`
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["characterReplacement(s, k)"]) --> I["count = {}, res = 0, l = 0, maxF = 0"]
|
||||
I --> L{"more (r, c) in s?"}
|
||||
L -- no --> E(["return res"])
|
||||
L -- yes --> A["count[c] += 1; maxF = max(maxF, count[c])"]
|
||||
A --> W{"(r - l + 1) - maxF > k? — too many chars to replace"}
|
||||
W -- yes --> P["count[s[l]] -= 1; l += 1 — shrink from the left"]
|
||||
P --> W
|
||||
W -- no --> M["res = max(res, r - l + 1)"]
|
||||
M --> L
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
@@ -45,3 +60,7 @@ class Solution:
|
||||
res = max(res, r - l + 1)
|
||||
return res
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="gqXU1UyA8pk" title="Longest Repeating Character Replacement - Leetcode 424 - Python" />
|
||||
@@ -28,6 +28,18 @@ sidebar:
|
||||
- `1 <= s.length <= 5 * 10^5`
|
||||
- `s` consists of uppercase and lowercase English letters and digits.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["frequencySort(s)"]) --> F["freq: count every char of s"]
|
||||
F --> C["split s and sort with a two-key comparator"]
|
||||
C --> K1["primary: freq[b] - freq[a] — most frequent first"]
|
||||
C --> K2["tie-break: a.localeCompare(b) — alphabetical"]
|
||||
K1 --> R(["join and return"])
|
||||
K2 --> R
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
@@ -47,3 +59,7 @@ var frequencySort = function (s) {
|
||||
.join("");
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="OXdXc9HTrIg" title="Sort Characters By Frequency - Leetcode 451 - Python" />
|
||||
@@ -22,6 +22,24 @@ sidebar:
|
||||
- `1 <= s1.length, s2.length <= 10^4`
|
||||
- `s1` and `s2` consist of lowercase English letters.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["checkInclusion(s1, s2)"]) --> G{"k = len(s1) > len(s2)?"}
|
||||
G -- yes --> X(["return False"])
|
||||
G -- no --> I["need = Counter(s1), window = Counter()"]
|
||||
I --> L{"more (right, c) in s2?"}
|
||||
L -- no --> E(["return False"])
|
||||
L -- yes --> A["window[c] += 1"]
|
||||
A --> B{"right >= k? — window is now longer than k"}
|
||||
B -- yes --> C["drop s2[right - k] from window, deleting the key at 0"]
|
||||
B -- no --> Q
|
||||
C --> Q{"window == need?"}
|
||||
Q -- yes --> R(["return True"])
|
||||
Q -- no --> L
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
@@ -49,3 +67,7 @@ class Solution:
|
||||
|
||||
return False
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="UbyhOgBN834" title="Permutation in String - Leetcode 567 - Python" />
|
||||
@@ -29,6 +29,19 @@ A linked list can be reversed either iteratively or recursively. Could you imple
|
||||
- The number of nodes in the list is the range [0, 5000].
|
||||
- `-5000 <= Node.val <= 5000`
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["reverseList(head)"]) --> I["prev = None, curr = head"]
|
||||
I --> W{"curr?"}
|
||||
W -- no --> E(["return prev — the old tail is the new head"])
|
||||
W -- yes --> A["next_ = curr.next — save it before overwriting"]
|
||||
A --> B["curr.next = prev — flip the link"]
|
||||
B --> C["prev = curr; curr = next_ — step forward"]
|
||||
C --> W
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
@@ -48,3 +61,7 @@ class Solution:
|
||||
curr = next_
|
||||
return prev
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="G0_I-ZF0S38" title="Reverse Linked List - Iterative AND Recursive - Leetcode 206 - Python" />
|
||||
@@ -26,6 +26,23 @@ sidebar:
|
||||
- `-100 <= Node.val <= 100`
|
||||
- Both list1 and list2 are sorted in non-decreasing order.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["mergeTwoLists(list1, list2)"]) --> G1{"list1 empty?"}
|
||||
G1 -- yes --> X1(["return list2"])
|
||||
G1 -- no --> G2{"list2 empty?"}
|
||||
G2 -- yes --> X2(["return list1"])
|
||||
G2 -- no --> H["head = the smaller first node; advance that list"]
|
||||
H --> C["current = head"]
|
||||
C --> W{"list1 and list2 both non-empty?"}
|
||||
W -- yes --> P["current.next = the smaller node; advance that list; current = current.next"]
|
||||
P --> W
|
||||
W -- no --> T["current.next = list1 or list2 — append the leftover tail"]
|
||||
T --> E(["return head"])
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
@@ -64,3 +81,7 @@ class Solution:
|
||||
current.next = list1 or list2
|
||||
return head
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="XIdigk956u0" title="Merge Two Sorted Lists - Leetcode 21 - Python" />
|
||||
@@ -23,6 +23,28 @@ You must implement a solution with O(1) time complexity for each function.
|
||||
- Methods pop, top and getMin operations will always be called on non-empty stacks.
|
||||
- At most 3 * 10^4 calls will be made to push, pop, top, and getMin.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph state["two parallel stacks — same depth, always"]
|
||||
A["stack — the values"]
|
||||
B["minStack — the minimum as of that depth"]
|
||||
end
|
||||
state --> P
|
||||
subgraph P["push(value)"]
|
||||
P1["stack.append(value)"] --> P2["minStack.append(min(value, minStack[-1]))"]
|
||||
end
|
||||
state --> O
|
||||
subgraph O["pop()"]
|
||||
O1["pop both stacks together"]
|
||||
end
|
||||
state --> R
|
||||
subgraph R["top() / getMin() — O(1)"]
|
||||
R1(["stack[-1] / minStack[-1]"])
|
||||
end
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
@@ -54,3 +76,7 @@ class MinStack:
|
||||
# param_3 = obj.top()
|
||||
# param_4 = obj.getMin()
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="qkLl7nAwDPo" title="Design Min Stack - Amazon Interview Question - Leetcode 155 - Python" />
|
||||
@@ -33,6 +33,24 @@ sidebar:
|
||||
- `1 <= s.length <= 10^4`
|
||||
- `s` consists of parentheses only '()[]{}'.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["isValid(s)"]) --> I["stack = [], pairs = closer to opener"]
|
||||
I --> L{"more char c?"}
|
||||
L -- no --> Z{"stack empty?"}
|
||||
Z -- yes --> T(["return True"])
|
||||
Z -- no --> U(["return False — unclosed openers left"])
|
||||
L -- yes --> Q{"c is a closer?"}
|
||||
Q -- no --> A["stack.append(c) — it is an opener"]
|
||||
A --> L
|
||||
Q -- yes --> M{"stack non-empty and pairs[c] == stack[-1]?"}
|
||||
M -- yes --> P["stack.pop() — matched"]
|
||||
P --> L
|
||||
M -- no --> F(["return False — mismatch"])
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
@@ -52,3 +70,7 @@ class Solution:
|
||||
|
||||
return True if not stack else False
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="WTzjTskDFMg" title="Valid Parentheses - Stack - Leetcode 20 - Python" />
|
||||
@@ -28,6 +28,20 @@ sidebar:
|
||||
- `1 <= s.length <= 2 * 10^5`
|
||||
- `s` consists only of printable ASCII characters.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["isPalindrome(s)"]) --> N["normal = s stripped of non-alphanumerics, lowercased"]
|
||||
N --> I["i = 0, j = normal.length - 1"]
|
||||
I --> W{"i < j?"}
|
||||
W -- no --> E(["return true — the pointers met in the middle"])
|
||||
W -- yes --> Q{"normal[i] != normal[j]?"}
|
||||
Q -- yes --> F(["return false"])
|
||||
Q -- no --> A["i++; j-- — step both inward"]
|
||||
A --> W
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
@@ -49,3 +63,7 @@ var isPalindrome = function(s) {
|
||||
return true;
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="jJXJ16kPFWg" title="Valid Palindrome - Leetcode 125 - Python" />
|
||||
@@ -25,6 +25,18 @@ You must do this by modifying the input array in-place with O(1) extra memory.
|
||||
- `1 <= s.length <= 10^5`
|
||||
- `s[i]` is a printable ascii character.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["reverseString(s)"]) --> I["i = 0, j = s.length - 1"]
|
||||
I --> W{"i < j?"}
|
||||
W -- no --> E(["done — s was reversed in place, nothing returned"])
|
||||
W -- yes --> A["swap s[i] and s[j]"]
|
||||
A --> B["i++; j--"]
|
||||
B --> W
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```js
|
||||
@@ -43,3 +55,7 @@ var reverseString = function(s) {
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="_d0T_2Lk2qA" title="Reverse String - 3 Ways - Leetcode 344 - Python" />
|
||||
@@ -26,6 +26,22 @@ Suppose there are lots of incoming s, say s1, s2, ..., sk where k >= 10^9, and y
|
||||
- `0 <= t.length <= 10^4`
|
||||
- `s` and `t` consist only of lowercase English letters.
|
||||
|
||||
## Approach
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S(["isSubsequence(s, t)"]) --> G{"len(s) > len(t)?"}
|
||||
G -- yes --> X(["return False"])
|
||||
G -- no --> I["i = 0 into s, j = 0 into t"]
|
||||
I --> W{"i < len(s) and j < len(t)?"}
|
||||
W -- no --> E(["return i == len(s) — every char of s was matched"])
|
||||
W -- yes --> Q{"s[i] == t[j]?"}
|
||||
Q -- yes --> A["i += 1 — consume the match"]
|
||||
Q -- no --> B["j += 1 — always advance t"]
|
||||
A --> B
|
||||
B --> W
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
@@ -42,3 +58,7 @@ class Solution:
|
||||
|
||||
return i == len(s)
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
<YouTube id="99RVfqklbCE" title="Is Subsequence - Leetcode 392" />
|
||||
@@ -0,0 +1,177 @@
|
||||
# todo — draining the review backlog
|
||||
|
||||
every solution in `work/1` owes a blind re-solve at `work/3` and a second one at `work/7`. both buckets are empty, so 36 problems owe 72 re-solves and not one has been started. snapshot: `reviewqueues("2026-08-31")` from `apps/cli/work.ts` — what `bun run pick` shows — cross-checked against `/api/stats`.
|
||||
|
||||
at 10 problems a day *including* the campaign's own load, 72 re-solves do not fit in one week: this week absorbs 38, the `+3` rung finishes **sat sep 5** (campaign day 20), the last `+7` clears **fri sep 18** (day 33), and normal srs resumes **sat sep 19** — see [when standard srs resumes](#when-standard-srs-resumes). saturday gates are deliberately left out of the count.
|
||||
|
||||
## rules this plan follows
|
||||
|
||||
- **≤ 10 problems a day, everything counted.** each day's new core set and the on-time reviews it earns are booked first; the backlog takes what is left — 7 slots on a light day, 2 on a heavy one.
|
||||
- **coverage before depth.** all 36 `+3` re-solves come first — one blind pass over everything — before any problem gets its `+7`.
|
||||
- **oldest first.** lc 387 and lc 1365 have been sitting 50 days.
|
||||
- **≥ 3 days between a problem's own `+3` and `+7`.** two re-solves in one day test nothing.
|
||||
- **sunday is not a slot.** `workingday()` never mints one; neither does this list.
|
||||
- **re-solve blind, then push.** `bun run pick` (tab cycles to the 3-day and 7-day sections) scaffolds straight into `work/3`/`work/7`. do not open `work/1` first. pushing the file is the log: `close-solved.yml` posts the bucket to `/admin/solved` and d1 moves the problem up the ladder.
|
||||
|
||||
## this week
|
||||
|
||||
### mon aug 31 — 3 new + 7 backlog = 10/10
|
||||
|
||||
*topic 08 — linked list ([#10](https://github.com/prdlk/leetcode/issues/10), closed today)*
|
||||
|
||||
| bucket | problem | difficulty | where | why |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `work/1` | [lc 21 · merge two sorted lists](https://github.com/prdlk/leetcode/issues/78) | easy | linked list | done today |
|
||||
| `work/1` | [lc 206 · reverse linked list](https://github.com/prdlk/leetcode/issues/77) | easy | linked list | done today |
|
||||
| `work/1` | [lc 141 · linked list cycle](https://github.com/prdlk/leetcode/issues/79) | easy | hash table | done today |
|
||||
| `work/3` | lc 387 · first unique character in a string | easy | hash table | 50d stale — first solved 2026-07-12 |
|
||||
| `work/3` | lc 1365 · how many numbers are smaller than the current number | easy | array | 50d stale — first solved 2026-07-12 |
|
||||
| `work/3` | lc 268 · missing number | easy | array | 14d stale — first solved 2026-08-17 |
|
||||
| `work/3` | lc 344 · reverse string | easy | two pointers | 14d stale — first solved 2026-08-17 |
|
||||
| `work/3` | lc 1426 · counting elements | easy | array | 14d stale — first solved 2026-08-17 |
|
||||
| `work/3` | lc 1832 · check if the sentence is pangram | easy | hash table | 14d stale — first solved 2026-08-17 |
|
||||
| `work/3` | [lc 15 · 3sum](https://github.com/prdlk/leetcode/issues/43) | medium | array | 13d stale — first solved 2026-08-18 |
|
||||
|
||||
### tue sep 1 — 2 new + 8 backlog = 10/10
|
||||
|
||||
*topic 09 — hybrid structures ([#11](https://github.com/prdlk/leetcode/issues/11))*
|
||||
|
||||
| bucket | problem | difficulty | where | why |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `work/1` | [lc 146 · lru cache](https://github.com/prdlk/leetcode/issues/87) | medium | topic 09 — hybrid structures | new core problem |
|
||||
| `work/1` | [lc 2 · add two numbers](https://github.com/prdlk/leetcode/issues/88) | medium | topic 09 — hybrid structures | new core problem |
|
||||
| `work/3` | [lc 49 · group anagrams](https://github.com/prdlk/leetcode/issues/30) | medium | array | 13d stale — first solved 2026-08-18 |
|
||||
| `work/3` | [lc 125 · valid palindrome](https://github.com/prdlk/leetcode/issues/41) | easy | two pointers | 13d stale — first solved 2026-08-18 |
|
||||
| `work/3` | [lc 167 · two sum ii - input array is sorted](https://github.com/prdlk/leetcode/issues/42) | medium | array | 13d stale — first solved 2026-08-18 |
|
||||
| `work/3` | [lc 217 · contains duplicate](https://github.com/prdlk/leetcode/issues/27) | easy | array | 13d stale — first solved 2026-08-18 |
|
||||
| `work/3` | [lc 238 · product of array except self](https://github.com/prdlk/leetcode/issues/37) | medium | array | 13d stale — first solved 2026-08-18 |
|
||||
| `work/3` | [lc 242 · valid anagram](https://github.com/prdlk/leetcode/issues/28) | easy | hash table | 13d stale — first solved 2026-08-18 |
|
||||
| `work/3` | [lc 303 · range sum query - immutable](https://github.com/prdlk/leetcode/issues/36) | easy | array | 13d stale — first solved 2026-08-18 |
|
||||
| `work/3` | [lc 560 · subarray sum equals k](https://github.com/prdlk/leetcode/issues/38) | medium | array | 13d stale — first solved 2026-08-18 |
|
||||
|
||||
### wed sep 2 — 3 new + 7 backlog = 10/10
|
||||
|
||||
*topic 10 — matrix index math ([#12](https://github.com/prdlk/leetcode/issues/12))*
|
||||
|
||||
| bucket | problem | difficulty | where | why |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `work/1` | [lc 48 · rotate image](https://github.com/prdlk/leetcode/issues/91) | medium | topic 10 — matrix index math | new core problem |
|
||||
| `work/1` | [lc 54 · spiral matrix](https://github.com/prdlk/leetcode/issues/92) | medium | topic 10 — matrix index math | new core problem |
|
||||
| `work/1` | [lc 73 · set matrix zeroes](https://github.com/prdlk/leetcode/issues/93) | medium | topic 10 — matrix index math | new core problem |
|
||||
| `work/3` | lc 1413 · minimum value to get positive step by step sum | easy | array | 13d stale — first solved 2026-08-18 |
|
||||
| `work/3` | lc 1480 · running sum of 1d array | easy | array | 13d stale — first solved 2026-08-18 |
|
||||
| `work/3` | [lc 33 · search in rotated sorted array](https://github.com/prdlk/leetcode/issues/60) | medium | array | 12d stale — first solved 2026-08-19 |
|
||||
| `work/3` | [lc 704 · binary search](https://github.com/prdlk/leetcode/issues/58) | easy | array | 12d stale — first solved 2026-08-19 |
|
||||
| `work/3` | [lc 875 · koko eating bananas](https://github.com/prdlk/leetcode/issues/59) | medium | array | 12d stale — first solved 2026-08-19 |
|
||||
| `work/3` | [lc 977 · squares of a sorted array](https://github.com/prdlk/leetcode/issues/46) | easy | array | 12d stale — first solved 2026-08-19 |
|
||||
| `work/3` | [lc 121 · best time to buy and sell stock](https://github.com/prdlk/leetcode/issues/50) | easy | array | 11d stale — first solved 2026-08-20 |
|
||||
|
||||
### thu sep 3 — 3 new + 3 on-time reviews + 4 backlog = 10/10
|
||||
|
||||
*topic 11 — tree dfs ([#13](https://github.com/prdlk/leetcode/issues/13))*
|
||||
|
||||
| bucket | problem | difficulty | where | why |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `work/1` | [lc 226 · invert binary tree](https://github.com/prdlk/leetcode/issues/96) | easy | topic 11 — tree dfs | new core problem |
|
||||
| `work/1` | [lc 104 · maximum depth of binary tree](https://github.com/prdlk/leetcode/issues/97) | easy | topic 11 — tree dfs | new core problem |
|
||||
| `work/1` | [lc 236 · lowest common ancestor of a binary tree](https://github.com/prdlk/leetcode/issues/98) | medium | topic 11 — tree dfs | new core problem |
|
||||
| `work/3` | [lc 21 · merge two sorted lists](https://github.com/prdlk/leetcode/issues/78) | easy | linked list | on time — solved mon aug 31 |
|
||||
| `work/3` | [lc 206 · reverse linked list](https://github.com/prdlk/leetcode/issues/77) | easy | linked list | on time — solved mon aug 31 |
|
||||
| `work/3` | [lc 141 · linked list cycle](https://github.com/prdlk/leetcode/issues/79) | easy | hash table | on time — solved mon aug 31 |
|
||||
| `work/3` | [lc 1 · two sum](https://github.com/prdlk/leetcode/issues/29) | easy | array | 7d stale — first solved 2026-08-24 |
|
||||
| `work/3` | [lc 11 · container with most water](https://github.com/prdlk/leetcode/issues/44) | medium | array | 7d stale — first solved 2026-08-24 |
|
||||
| `work/3` | [lc 392 · is subsequence](https://github.com/prdlk/leetcode/issues/45) | easy | two pointers | 7d stale — first solved 2026-08-24 |
|
||||
| `work/3` | [lc 3 · longest substring without repeating characters](https://github.com/prdlk/leetcode/issues/51) | medium | hash table | 6d stale — first solved 2026-08-25 |
|
||||
|
||||
### fri sep 4 — 3 new + 2 on-time reviews + 5 backlog = 10/10
|
||||
|
||||
*topic 12 — tree bfs ([#14](https://github.com/prdlk/leetcode/issues/14))*
|
||||
|
||||
| bucket | problem | difficulty | where | why |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `work/1` | [lc 102 · binary tree level order traversal](https://github.com/prdlk/leetcode/issues/106) | medium | topic 12 — tree bfs | new core problem |
|
||||
| `work/1` | [lc 199 · binary tree right side view](https://github.com/prdlk/leetcode/issues/107) | medium | topic 12 — tree bfs | new core problem |
|
||||
| `work/1` | [lc 1448 · count good nodes in binary tree](https://github.com/prdlk/leetcode/issues/108) | medium | topic 12 — tree bfs | new core problem |
|
||||
| `work/3` | [lc 146 · lru cache](https://github.com/prdlk/leetcode/issues/87) | medium | topic 09 — hybrid structures | on time — solved tue sep 1 |
|
||||
| `work/3` | [lc 2 · add two numbers](https://github.com/prdlk/leetcode/issues/88) | medium | topic 09 — hybrid structures | on time — solved tue sep 1 |
|
||||
| `work/3` | [lc 424 · longest repeating character replacement](https://github.com/prdlk/leetcode/issues/52) | medium | hash table | 6d stale — first solved 2026-08-25 |
|
||||
| `work/3` | [lc 567 · permutation in string](https://github.com/prdlk/leetcode/issues/53) | medium | hash table | 6d stale — first solved 2026-08-25 |
|
||||
| `work/3` | [lc 35 · search insert position](https://github.com/prdlk/leetcode/issues/63) | easy | array | 5d stale — first solved 2026-08-26 |
|
||||
| `work/3` | [lc 74 · search a 2d matrix](https://github.com/prdlk/leetcode/issues/61) | medium | array | 5d stale — first solved 2026-08-26 |
|
||||
| `work/3` | [lc 153 · find minimum in rotated sorted array](https://github.com/prdlk/leetcode/issues/62) | medium | array | 5d stale — first solved 2026-08-26 |
|
||||
|
||||
### sat sep 5 — 3 on-time reviews + 7 backlog = 10/10
|
||||
|
||||
*no new topic — saturday.*
|
||||
|
||||
| bucket | problem | difficulty | where | why |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `work/3` | [lc 48 · rotate image](https://github.com/prdlk/leetcode/issues/91) | medium | topic 10 — matrix index math | on time — solved wed sep 2 |
|
||||
| `work/3` | [lc 54 · spiral matrix](https://github.com/prdlk/leetcode/issues/92) | medium | topic 10 — matrix index math | on time — solved wed sep 2 |
|
||||
| `work/3` | [lc 73 · set matrix zeroes](https://github.com/prdlk/leetcode/issues/93) | medium | topic 10 — matrix index math | on time — solved wed sep 2 |
|
||||
| `work/3` | [lc 20 · valid parentheses](https://github.com/prdlk/leetcode/issues/67) | easy | string | 4d stale — first solved 2026-08-27 |
|
||||
| `work/3` | [lc 150 · evaluate reverse polish notation](https://github.com/prdlk/leetcode/issues/69) | medium | array | 4d stale — first solved 2026-08-27 |
|
||||
| `work/3` | [lc 155 · min stack](https://github.com/prdlk/leetcode/issues/68) | medium | stack | 4d stale — first solved 2026-08-27 |
|
||||
| `work/3` | [lc 739 · daily temperatures](https://github.com/prdlk/leetcode/issues/72) | medium | array | 3d stale — first solved 2026-08-28 |
|
||||
| `work/3` | [lc 853 · car fleet](https://github.com/prdlk/leetcode/issues/73) | medium | array | 3d stale — first solved 2026-08-28 |
|
||||
| `work/7` | lc 387 · first unique character in a string | easy | hash table | `+3` was mon aug 31 |
|
||||
| `work/7` | lc 1365 · how many numbers are smaller than the current number | easy | array | `+3` was mon aug 31 |
|
||||
|
||||
### sun sep 6 — rest day
|
||||
|
||||
nothing. by design.
|
||||
|
||||
## carry-over
|
||||
|
||||
the 34 re-solves that do not fit this week, at the same cap and the same rules. booked = that day's new core set plus the on-time reviews it owes.
|
||||
|
||||
| day | topic | booked | backlog re-solves | load |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **mon sep 7** | topic 13 — binary search tree | 10 | *none — the day is already full* | 10/10 |
|
||||
| **tue sep 8** | topic 14 — heap / priority queue | 7 | `work/7` — lc 268, lc 344, lc 1426 | 10/10 |
|
||||
| **wed sep 9** | topic 15 — trie | 5 | `work/7` — lc 1832, lc 15, lc 49, lc 125, lc 167 | 10/10 |
|
||||
| **thu sep 10** | topic 16 — graph dfs | 9 | `work/7` — lc 217 | 10/10 |
|
||||
| **fri sep 11** | topic 17 — graph bfs | 8 | `work/7` — lc 238, lc 242 | 10/10 |
|
||||
| **sat sep 12** | *none — saturday* | 2 | `work/7` — lc 303, lc 560, lc 1413, lc 1480, lc 33, lc 704, lc 875, lc 977 | 10/10 |
|
||||
| **mon sep 14** | topic 18 — topological sort | 10 | *none — the day is already full* | 10/10 |
|
||||
| **tue sep 15** | topic 19 — union-find | 5 | `work/7` — lc 121, lc 1, lc 11, lc 392, lc 3 | 10/10 |
|
||||
| **wed sep 16** | topic 20 — backtracking | 5 | `work/7` — lc 424, lc 567, lc 35, lc 74, lc 153 | 10/10 |
|
||||
| **thu sep 17** | topic 21 — 1-d dynamic programming | 8 | `work/7` — lc 20, lc 150 | 10/10 |
|
||||
| **fri sep 18** | topic 22 — multi-d / grid dp | 7 | `work/7` — lc 155, lc 739, lc 853 | 10/10 |
|
||||
|
||||
## when standard srs resumes
|
||||
|
||||
- **sat sep 5** (campaign day 20) — the `+3` rung is empty. every one of the 36 has been recalled blind once; nothing is unretrieved any more.
|
||||
- **fri sep 18** (campaign day 33) — the last backlog `+7` lands. `work/3` and `work/7` hold a file for all 36, so `reviewqueues()` owes nothing; the 28 that have a d1 row are `retired` on the ladder, and the 8 listed at the bottom of this file never had one.
|
||||
- **sat sep 19 onward — standard srs.** the only reviews left are the ones the campaign itself minted: solve a problem, its `+3` and `+7` come back on `workingday(solve + 3)` and `workingday(solve + 7)`, the digest asks for ≤ `review_cap` (3) a day, `levelreviews()` pushes any excess forward, and `bun run pick`'s 3-day and 7-day sections show only what is due. no dated plan needed after this — the digest *is* the plan.
|
||||
|
||||
campaign-native load per working day once the backlog is gone, all of it inside the cap:
|
||||
|
||||
| day | topic | new | `work/3` | `work/7` | load |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| **sat sep 19** | *review-only* | 0 | 3 | 0 | 3/10 |
|
||||
| **mon sep 21** | topic 23 — greedy | 3 | 5 | 2 | 10/10 |
|
||||
| **tue sep 22** | topic 24 — intervals | 3 | 1 | 2 | 6/10 |
|
||||
| **wed sep 23** | *review-only* | 0 | 0 | 3 | 3/10 |
|
||||
| **thu sep 24** | *review-only* | 0 | 3 | 3 | 6/10 |
|
||||
| **fri sep 25** | *review-only* | 0 | 3 | 3 | 6/10 |
|
||||
| **sat sep 26** | *review-only* | 0 | 0 | 0 | 0/10 |
|
||||
| **mon sep 28** | *review-only* | 0 | 0 | 3 | 3/10 |
|
||||
| **tue sep 29** | *review-only* | 0 | 0 | 3 | 3/10 |
|
||||
|
||||
two things to watch there. **mon sep 21 is 10/10 on campaign load alone** — topic 23's core set, two `+7`s, and five of the six `+3`s that land there (thursday's three slide off sunday onto monday, friday's three land natively, and the sixth is pushed to tuesday exactly as `levelreviews()` would) — so one slipped day re-opens a backlog. and from **wed sep 30** the ladder runs dry: `apps/api/data/schedule.json` books its last topic tue sep 22, so the review-only weeks that close the campaign are drills, optional sets and the weekly gates, none of which this file counts.
|
||||
|
||||
## not reviews
|
||||
|
||||
three july scaffolds in `work/1` are still empty, so they owe no re-solve — they are unsolved problems, and `bun run pick` still lists them in its new-problem section:
|
||||
|
||||
| problem | difficulty |
|
||||
| --- | --- |
|
||||
| [lc 347 · top k frequent elements](https://github.com/prdlk/leetcode/issues/31) | medium |
|
||||
| lc 451 · sort characters by frequency | medium |
|
||||
| lc 1636 · sort array by increasing frequency | easy |
|
||||
|
||||
## what this plan exposes
|
||||
|
||||
- **8 of the 36 have no problem issue** (387, 1365, 268, 344, 1426, 1832, 1413, 1480) — pre-campaign solves. the catalog reconcile never invents rows, so d1 does not know them and no digest can ever surface them. `bun run pick` is the only thing that will ask for these reviews.
|
||||
- **`/api/stats` reports a stage that does not exist**: `ladder: { new: 128, "+3": 3, "+7": 0, retired: 0, "+2": 30 }`. `+2` is not in `stages` (`apps/api/src/srs.ts`), so 30 rows sit on a pre-`0002` rung — which is why the digest queue plateaus at 32 due instead of tracking the tables above. that wants a migration, not a re-solve.
|
||||
@@ -46,7 +46,8 @@ interface Stats {
|
||||
}[];
|
||||
streak: number;
|
||||
queue: { date: string; due: number }[];
|
||||
recent: { date: string; attempts: number; passes: number }[];
|
||||
/** Every campaign day, zeros included; this page reads only the tail. */
|
||||
heat: { date: string; attempts: number; passes: number }[];
|
||||
}
|
||||
|
||||
function isStats(value: unknown): value is Stats {
|
||||
@@ -60,7 +61,7 @@ function isStats(value: unknown): value is Stats {
|
||||
Array.isArray(v.phases) &&
|
||||
Array.isArray(v.gates) &&
|
||||
Array.isArray(v.queue) &&
|
||||
Array.isArray(v.recent) &&
|
||||
Array.isArray(v.heat) &&
|
||||
typeof v.streak === "number"
|
||||
);
|
||||
}
|
||||
@@ -332,11 +333,14 @@ const GREENS = [
|
||||
];
|
||||
|
||||
function Recent({ stats }: { stats: Stats }) {
|
||||
// heat spans the whole campaign, future days and all. The window wanted here
|
||||
// is the seven days ending today, and campaign.day is that day, 1-based.
|
||||
const week = stats.heat.slice(Math.max(0, stats.campaign.day - 7), stats.campaign.day);
|
||||
return (
|
||||
<div style={card}>
|
||||
<div style={heading}>Last 7 days</div>
|
||||
<div style={{ display: "flex", gap: "0.4rem", flexWrap: "wrap" }}>
|
||||
{stats.recent.map((r) => (
|
||||
{week.map((r) => (
|
||||
<span
|
||||
key={r.date}
|
||||
title={`${r.date}: ${r.passes}/${r.attempts} passed`}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"api:deploy": "cd apps/api && bun run deploy",
|
||||
"api:test": "cd apps/api && bun test src",
|
||||
"pick": "bun apps/cli/pick.ts",
|
||||
"stats": "bun apps/cli/stats.ts",
|
||||
"sync": "bun apps/cli/sync.ts",
|
||||
"submit": "bun apps/cli/submit.ts",
|
||||
"test": "bun apps/cli/test.ts"
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
146. LRU Cache
|
||||
Difficulty: Medium
|
||||
https://leetcode.com/problems/lru-cache/
|
||||
|
||||
──────────────────────────────────────────────────
|
||||
|
||||
Design a data structure that follows the constraints of a Least
|
||||
Recently Used (LRU) cache.
|
||||
|
||||
Implement the LRUCache class:
|
||||
|
||||
• LRUCache(int capacity) Initialize the LRU cache with positive size
|
||||
capacity.
|
||||
|
||||
• int get(int key) Return the value of the key if the key exists,
|
||||
otherwise return -1.
|
||||
|
||||
• void put(int key, int value) Update the value of the key if the
|
||||
key exists. Otherwise, add the key-value pair to the cache. If the
|
||||
number of keys exceeds the capacity from this operation, evict the
|
||||
least recently used key.
|
||||
|
||||
The functions get and put must each run in O(1) average time
|
||||
complexity.
|
||||
|
||||
|
||||
|
||||
Example 1:
|
||||
|
||||
Input
|
||||
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get",
|
||||
"get"]
|
||||
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
|
||||
Output
|
||||
[null, null, null, 1, null, -1, null, -1, 3, 4]
|
||||
|
||||
Explanation
|
||||
LRUCache lRUCache = new LRUCache(2);
|
||||
lRUCache.put(1, 1); // cache is {1=1}
|
||||
lRUCache.put(2, 2); // cache is {1=1, 2=2}
|
||||
lRUCache.get(1); // return 1
|
||||
lRUCache.put(3, 3); // LRU key was 2, evicts key 2, cache is {1=1,
|
||||
3=3}
|
||||
lRUCache.get(2); // returns -1 (not found)
|
||||
lRUCache.put(4, 4); // LRU key was 1, evicts key 1, cache is {4=4,
|
||||
3=3}
|
||||
lRUCache.get(1); // return -1 (not found)
|
||||
lRUCache.get(3); // return 3
|
||||
lRUCache.get(4); // return 4
|
||||
|
||||
|
||||
|
||||
Constraints:
|
||||
|
||||
• 1 <= capacity <= 3000
|
||||
|
||||
• 0 <= key <= 10^4
|
||||
|
||||
• 0 <= value <= 10^5
|
||||
|
||||
• At most 2 * 10^5 calls will be made to get and put.
|
||||
"""
|
||||
|
||||
|
||||
class Node:
|
||||
def __init__(self, key=0, value=0):
|
||||
self.key = key
|
||||
self.value = value
|
||||
self.prev = None
|
||||
self.next = None
|
||||
|
||||
|
||||
class LRUCache:
|
||||
def __init__(self, capacity: int):
|
||||
self.capacity = capacity
|
||||
self.cache = {}
|
||||
self.head = Node()
|
||||
self.tail = Node()
|
||||
self.head.next = self.tail # pyright: ignore[reportAttributeAccessIssue]
|
||||
self.tail.prev = self.head # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
def _remove_node(self, node):
|
||||
prev_node = node.prev
|
||||
next_node = node.next
|
||||
prev_node.next = next_node
|
||||
next_node.prev = prev_node
|
||||
|
||||
def _add_node(self, node):
|
||||
# Insert just before the tail (most recent at end)
|
||||
node.next = self.tail
|
||||
node.prev = self.tail.prev
|
||||
self.tail.prev.next = node # pyright: ignore[reportAttributeAccessIssue]
|
||||
self.tail.prev = node
|
||||
|
||||
def get(self, key: int) -> int:
|
||||
if key not in self.cache:
|
||||
return -1
|
||||
|
||||
node = self.cache[key]
|
||||
self._remove_node(node)
|
||||
self._add_node(node)
|
||||
return node.value
|
||||
|
||||
def put(self, key: int, value: int) -> None:
|
||||
if key in self.cache:
|
||||
node = self.cache[key]
|
||||
node.value = value
|
||||
self._remove_node(node)
|
||||
self._add_node(node)
|
||||
else:
|
||||
new_node = Node(key, value)
|
||||
self.cache[key] = new_node
|
||||
self._add_node(new_node)
|
||||
|
||||
if len(self.cache) > self.capacity:
|
||||
lru = self.head.next # Oldest node is next to the head
|
||||
self._remove_node(lru)
|
||||
del self.cache[lru.key] # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
|
||||
# Your LRUCache object will be instantiated and called as such:
|
||||
# obj = LRUCache(capacity)
|
||||
# param_1 = obj.get(key)
|
||||
# obj.put(key,value)
|
||||
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
2. Add Two Numbers
|
||||
Difficulty: Medium
|
||||
https://leetcode.com/problems/add-two-numbers/
|
||||
|
||||
──────────────────────────────────────────────────
|
||||
|
||||
You are given two non-empty linked lists representing two
|
||||
non-negative integers. The digits are stored in reverse order, and
|
||||
each of their nodes contains a single digit. Add the two numbers and
|
||||
return the sum as a linked list.
|
||||
|
||||
You may assume the two numbers do not contain any leading zero,
|
||||
except the number 0 itself.
|
||||
|
||||
|
||||
|
||||
Example 1:
|
||||
|
||||
Input: l1 = [2,4,3], l2 = [5,6,4]
|
||||
Output: [7,0,8]
|
||||
Explanation: 342 + 465 = 807.
|
||||
|
||||
Example 2:
|
||||
|
||||
Input: l1 = [0], l2 = [0]
|
||||
Output: [0]
|
||||
|
||||
Example 3:
|
||||
|
||||
Input: l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
|
||||
Output: [8,9,9,9,0,0,0,1]
|
||||
|
||||
|
||||
|
||||
Constraints:
|
||||
|
||||
• The number of nodes in each linked list is in the range [1, 100].
|
||||
|
||||
• 0 <= Node.val <= 9
|
||||
|
||||
• It is guaranteed that the list represents a number that does not
|
||||
have leading zeros.
|
||||
"""
|
||||
|
||||
|
||||
# Definition for singly-linked list.
|
||||
# class ListNode:
|
||||
# def __init__(self, val=0, next=None):
|
||||
# self.val = val
|
||||
# self.next = next
|
||||
class Solution:
|
||||
def addTwoNumbers(
|
||||
self, l1: Optional[ListNode], l2: Optional[ListNode]
|
||||
) -> Optional[ListNode]:
|
||||
head = ListNode()
|
||||
carry = 0
|
||||
l1curr = head
|
||||
l2curr = head
|
||||
while head:
|
||||
l1.val + l2.val
|
||||
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
1365. How Many Numbers Are Smaller Than the Current Number
|
||||
Difficulty: Easy
|
||||
https://leetcode.com/problems/how-many-numbers-are-smaller-than-the-current-number/
|
||||
|
||||
──────────────────────────────────────────────────
|
||||
|
||||
Given the array nums, for each nums[i] find out how many numbers in
|
||||
the array are smaller than it. That is, for each nums[i] you have to
|
||||
count the number of valid j's such that j != i and nums[j] < nums[i].
|
||||
|
||||
Return the answer in an array.
|
||||
|
||||
|
||||
|
||||
Example 1:
|
||||
|
||||
Input: nums = [8,1,2,2,3]
|
||||
Output: [4,0,1,1,3]
|
||||
Explanation:
|
||||
For nums[0]=8 there exist four smaller numbers than it (1, 2, 2 and
|
||||
3).
|
||||
For nums[1]=1 does not exist any smaller number than it.
|
||||
For nums[2]=2 there exist one smaller number than it (1).
|
||||
For nums[3]=2 there exist one smaller number than it (1).
|
||||
For nums[4]=3 there exist three smaller numbers than it (1, 2 and 2).
|
||||
|
||||
Example 2:
|
||||
|
||||
Input: nums = [6,5,4,8]
|
||||
Output: [2,1,0,3]
|
||||
|
||||
Example 3:
|
||||
|
||||
Input: nums = [7,7,7,7]
|
||||
Output: [0,0,0,0]
|
||||
|
||||
|
||||
|
||||
Constraints:
|
||||
|
||||
• 2 <= nums.length <= 500
|
||||
|
||||
• 0 <= nums[i] <= 100
|
||||
"""
|
||||
|
||||
from collections import Counter
|
||||
|
||||
|
||||
class Solution:
|
||||
def smallerNumbersThanCurrent(self, nums: List[int]) -> List[int]:
|
||||
freq = Counter(nums)
|
||||
sorted_nums = sorted(freq.keys())
|
||||
smaller = {}
|
||||
count = 0
|
||||
for num in sorted_nums:
|
||||
smaller[num] = count
|
||||
count += freq[num]
|
||||
|
||||
return [smaller[num] for num in nums]
|
||||
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
1426. Counting Elements
|
||||
Difficulty: Easy
|
||||
https://leetcode.com/problems/counting-elements/
|
||||
|
||||
──────────────────────────────────────────────────
|
||||
|
||||
Given an integer array arr, count how many elements x there are, such
|
||||
that x + 1 is also in arr. If there are duplicates in arr, count them
|
||||
separately.
|
||||
|
||||
|
||||
|
||||
Example 1:
|
||||
|
||||
Input: arr = [1,2,3]
|
||||
Output: 2
|
||||
Explanation: 1 and 2 are counted cause 2 and 3 are in arr.
|
||||
|
||||
Example 2:
|
||||
|
||||
Input: arr = [1,1,3,3,5,5,7,7]
|
||||
Output: 0
|
||||
Explanation: No numbers are counted, cause there is no 2, 4, 6, or 8
|
||||
in arr.
|
||||
|
||||
|
||||
|
||||
Constraints:
|
||||
|
||||
• 1 <= arr.length <= 1000
|
||||
|
||||
• 0 <= arr[i] <= 1000
|
||||
"""
|
||||
|
||||
|
||||
class Solution:
|
||||
def countElements(self, arr: List[int]) -> int:
|
||||
arr_set = set(arr)
|
||||
count = 0
|
||||
for n in arr:
|
||||
sum = n + 1
|
||||
if sum in arr_set:
|
||||
count += 1
|
||||
return count
|
||||
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
268. Missing Number
|
||||
Difficulty: Easy
|
||||
https://leetcode.com/problems/missing-number/
|
||||
|
||||
──────────────────────────────────────────────────
|
||||
|
||||
Given an array nums containing n distinct numbers in the range [0,
|
||||
n], return the only number in the range that is missing from the
|
||||
array.
|
||||
|
||||
|
||||
|
||||
Example 1:
|
||||
|
||||
Input: nums = [3,0,1]
|
||||
|
||||
Output: 2
|
||||
|
||||
Explanation:
|
||||
|
||||
n = 3 since there are 3 numbers, so all numbers are in the range
|
||||
[0,3]. 2 is the missing number in the range since it does not appear
|
||||
in nums.
|
||||
|
||||
Example 2:
|
||||
|
||||
Input: nums = [0,1]
|
||||
|
||||
Output: 2
|
||||
|
||||
Explanation:
|
||||
|
||||
n = 2 since there are 2 numbers, so all numbers are in the range
|
||||
[0,2]. 2 is the missing number in the range since it does not appear
|
||||
in nums.
|
||||
|
||||
Example 3:
|
||||
|
||||
Input: nums = [9,6,4,2,3,5,7,0,1]
|
||||
|
||||
Output: 8
|
||||
|
||||
Explanation:
|
||||
|
||||
n = 9 since there are 9 numbers, so all numbers are in the range
|
||||
[0,9]. 8 is the missing number in the range since it does not appear
|
||||
in nums.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Constraints:
|
||||
|
||||
• n == nums.length
|
||||
|
||||
• 1 <= n <= 10^4
|
||||
|
||||
• 0 <= nums[i] <= n
|
||||
|
||||
• All the numbers of nums are unique.
|
||||
|
||||
|
||||
|
||||
Follow up: Could you implement a solution using only O(1) extra space
|
||||
complexity and O(n) runtime complexity?
|
||||
"""
|
||||
|
||||
|
||||
class Solution:
|
||||
def missingNumber(self, nums: List[int]) -> int:
|
||||
n = len(nums)
|
||||
expected_sum = (n * (n + 1)) // 2
|
||||
actual_sum = sum(nums)
|
||||
return expected_sum - actual_sum
|
||||
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
1832. Check if the Sentence Is Pangram
|
||||
Difficulty: Easy
|
||||
https://leetcode.com/problems/check-if-the-sentence-is-pangram/
|
||||
|
||||
──────────────────────────────────────────────────
|
||||
|
||||
A pangram is a sentence where every letter of the English alphabet
|
||||
appears at least once.
|
||||
|
||||
Given a string sentence containing only lowercase English letters,
|
||||
return true if sentence is a pangram, or false otherwise.
|
||||
|
||||
|
||||
|
||||
Example 1:
|
||||
|
||||
Input: sentence = "thequickbrownfoxjumpsoverthelazydog"
|
||||
Output: true
|
||||
Explanation: sentence contains at least one of every letter of the
|
||||
English alphabet.
|
||||
|
||||
Example 2:
|
||||
|
||||
Input: sentence = "leetcode"
|
||||
Output: false
|
||||
|
||||
|
||||
|
||||
Constraints:
|
||||
|
||||
• 1 <= sentence.length <= 1000
|
||||
|
||||
• sentence consists of lowercase English letters.
|
||||
"""
|
||||
|
||||
|
||||
class Solution:
|
||||
def checkIfPangram(self, sentence: str) -> bool:
|
||||
return len(set(sentence.lower())) == 26
|
||||
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
387. First Unique Character in a String
|
||||
Difficulty: Easy
|
||||
https://leetcode.com/problems/first-unique-character-in-a-string/
|
||||
|
||||
──────────────────────────────────────────────────
|
||||
|
||||
Given a string s, find the first non-repeating character in it and
|
||||
return its index. If it does not exist, return -1.
|
||||
|
||||
|
||||
|
||||
Example 1:
|
||||
|
||||
Input: s = "leetcode"
|
||||
|
||||
Output: 0
|
||||
|
||||
Explanation:
|
||||
|
||||
The character 'l' at index 0 is the first character that does not
|
||||
occur at any other index.
|
||||
|
||||
Example 2:
|
||||
|
||||
Input: s = "loveleetcode"
|
||||
|
||||
Output: 2
|
||||
|
||||
Example 3:
|
||||
|
||||
Input: s = "aabb"
|
||||
|
||||
Output: -1
|
||||
|
||||
|
||||
|
||||
Constraints:
|
||||
|
||||
• 1 <= s.length <= 10^5
|
||||
|
||||
• s consists of only lowercase English letters.
|
||||
"""
|
||||
|
||||
from collections import Counter
|
||||
|
||||
|
||||
class Solution:
|
||||
def firstUniqChar(self, s: str) -> int:
|
||||
map = Counter(s)
|
||||
for i, c in enumerate(s):
|
||||
if map[c] == 1:
|
||||
return i
|
||||
return -1
|
||||
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
344. Reverse String
|
||||
Difficulty: Easy
|
||||
https://leetcode.com/problems/reverse-string/
|
||||
|
||||
──────────────────────────────────────────────────
|
||||
|
||||
Write a function that reverses a string. The input string is given as
|
||||
an array of characters s.
|
||||
|
||||
You must do this by modifying the input array in-place with O(1)
|
||||
extra memory.
|
||||
|
||||
|
||||
|
||||
Example 1:
|
||||
|
||||
Input: s = ["h","e","l","l","o"]
|
||||
Output: ["o","l","l","e","h"]
|
||||
|
||||
Example 2:
|
||||
|
||||
Input: s = ["H","a","n","n","a","h"]
|
||||
Output: ["h","a","n","n","a","H"]
|
||||
|
||||
|
||||
|
||||
Constraints:
|
||||
|
||||
• 1 <= s.length <= 10^5
|
||||
|
||||
• s[i] is a printable ascii character.
|
||||
"""
|
||||
|
||||
|
||||
class Solution:
|
||||
def reverseString(self, s: List[str]) -> None:
|
||||
l = 0
|
||||
r = len(s) - 1
|
||||
while l < r:
|
||||
s[l], s[r] = s[r], s[l]
|
||||
l += 1
|
||||
r -= 1
|
||||
|
||||
"""
|
||||
Do not return anything, modify s in-place instead.
|
||||
"""
|
||||
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
15. 3Sum
|
||||
Difficulty: Medium
|
||||
https://leetcode.com/problems/3sum/
|
||||
|
||||
──────────────────────────────────────────────────
|
||||
|
||||
Given an integer array nums, return all the triplets [nums[i],
|
||||
nums[j], nums[k]] such that i != j, i != k, and j != k, and nums[i] +
|
||||
nums[j] + nums[k] == 0.
|
||||
|
||||
Notice that the solution set must not contain duplicate triplets.
|
||||
|
||||
|
||||
|
||||
Example 1:
|
||||
|
||||
Input: nums = [-1,0,1,2,-1,-4]
|
||||
Output: [[-1,-1,2],[-1,0,1]]
|
||||
Explanation:
|
||||
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0.
|
||||
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0.
|
||||
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0.
|
||||
The distinct triplets are [-1,0,1] and [-1,-1,2].
|
||||
Notice that the order of the output and the order of the triplets
|
||||
does not matter.
|
||||
|
||||
Example 2:
|
||||
|
||||
Input: nums = [0,1,1]
|
||||
Output: []
|
||||
Explanation: The only possible triplet does not sum up to 0.
|
||||
|
||||
Example 3:
|
||||
|
||||
Input: nums = [0,0,0]
|
||||
Output: [[0,0,0]]
|
||||
Explanation: The only possible triplet sums up to 0.
|
||||
|
||||
|
||||
|
||||
Constraints:
|
||||
|
||||
• 3 <= nums.length <= 3000
|
||||
|
||||
• -10^5 <= nums[i] <= 10^5
|
||||
"""
|
||||
|
||||
|
||||
class Solution:
|
||||
def threeSum(self, nums: list[int]) -> list[list[int]]:
|
||||
nums.sort()
|
||||
result = []
|
||||
n = len(nums)
|
||||
|
||||
for i in range(n):
|
||||
# skip all zero
|
||||
if i > 0 and nums[i] == nums[i - 1]:
|
||||
continue
|
||||
|
||||
# two pointers
|
||||
left = i + 1
|
||||
right = n - 1
|
||||
target = -nums[i]
|
||||
|
||||
while left < right:
|
||||
current = nums[left] + nums[right]
|
||||
|
||||
if current == target:
|
||||
result.append([nums[i], nums[left], nums[right]])
|
||||
|
||||
# skip duplicates
|
||||
while left < right and nums[left] == nums[left + 1]:
|
||||
left += 1
|
||||
while left < right and nums[right] == nums[right - 1]:
|
||||
right -= 1
|
||||
|
||||
# shift pointers
|
||||
left += 1
|
||||
right -= 1
|
||||
|
||||
# since sorted, if current < target, then move left
|
||||
elif current < target:
|
||||
left += 1
|
||||
# since sorted, if current > target, then move right
|
||||
else:
|
||||
right -= 1
|
||||
|
||||
return result
|
||||
Reference in new issue
Block a user