Compare commits

...
Author SHA1 Message Date
Prad Nukala 987eb7ab5b feat(work): add solution for LeetCode 1448 Count Good Nodes in Binary Tree 2026-09-04 16:44:07 -04:00
Prad Nukala e4118fccd3 docs(docs): add MDX page for LeetCode 1448 Count Good Nodes in Binary Tree problem 2026-09-04 16:44:04 -04:00
Prad Nukala 95db4ca46c feat(work): add solution for Binary Tree Right Side View problem 2026-09-04 11:48:03 -04:00
Prad Nukala 0cc7ca607e docs(docs): add documentation for Binary Tree Right Side View problem 2026-09-04 11:47:59 -04:00
Prad Nukala 71ec39fe34 feat(work): add solution for Binary Tree Level Order Traversal (LC 102) 2026-09-04 11:33:20 -04:00
Prad Nukala 51decd23bd docs(docs): add documentation for Binary Tree Level Order Traversal (LeetCode 102) 2026-09-04 11:33:15 -04:00
Prad Nukala 90082518a6 feat(work): add solution for Lowest Common Ancestor of a Binary Tree 2026-09-03 14:17:54 -04:00
Prad Nukala e10a913546 docs(docs): add 236 Lowest Common Ancestor of a Binary Tree documentation 2026-09-03 14:17:51 -04:00
Prad Nukala fec94ed636 feat(work): add solution for Invert Binary Tree (LC 226) 2026-09-03 14:01:06 -04:00
Prad Nukala 30273d0030 docs(docs): add Invert Binary Tree documentation 2026-09-03 14:01:02 -04:00
Prad Nukala 50299defef feat(work): add Maximum Depth of Binary Tree solution 2026-09-03 13:52:57 -04:00
Prad Nukala c843be855e feat(docs): add Maximum Depth of Binary Tree documentation 2026-09-03 13:52:54 -04:00
Prad Nukala 1e0e03daaa feat(work): add Set Matrix Zeroes solution 2026-09-02 16:52:29 -04:00
Prad Nukala 7b182206dd docs(docs): add documentation for Set Matrix Zeroes problem 2026-09-02 16:52:26 -04:00
Prad Nukala faaaf52f85 feat(work): add Rotate Image and Spiral Matrix solutions 2026-09-02 16:49:41 -04:00
Prad Nukala 090f3d6719 docs(docs): add new problem pages for Rotate Image, Spiral Matrix, and Add Two Numbers 2026-09-02 16:49:39 -04:00
Prad Nukala a3ae16ad96 feat(work): add Group Anagrams solution 2026-09-01 15:56:02 -04:00
Prad Nukala 9faa0b0337 feat(work): implement correct addTwoNumbers algorithm for linked list addition 2026-09-01 14:39:45 -04:00
Prad Nukala cc29304903 docs(agents): clarify sync.ts contract description for solution section ownership 2026-09-01 14:22:01 -04:00
Prad Nukala c50b600103 feat(work): add Add Two Numbers solution 2026-09-01 14:21:57 -04:00
Prad Nukala a86d72c7db docs(docs): add Approach and Explanation sections with mermaid diagrams to problem pages 2026-09-01 14:21:54 -04:00
Prad Nukala e59d78e571 feat(cli): preserve custom sections after solution during docs sync 2026-09-01 14:21:50 -04:00
Prad Nukala a93bb068b6 fix(work): correct node insertion and eviction order in LRUCache 2026-09-01 13:59:03 -04:00
Prad Nukala 53b2d5ec8c docs(docs): clarify LRUCache insertion order in example MDX 2026-09-01 13:59:01 -04:00
Prad Nukala 5dda64e41e feat(work): add LRU Cache solution 2026-09-01 13:31:34 -04:00
Prad Nukala d798871fbc docs(docs): add LRU Cache problem documentation 2026-09-01 13:31:31 -04:00
Prad Nukala bb7324fe8c docs(AGENTS): add terminal dashboard info and expand documentation for stats, work layout, and tooling preferences 2026-08-31 18:56:57 -04:00
Prad Nukala d13859d31e refactor(docs): rename recent stats to heat and update dashboard rendering 2026-08-31 18:56:52 -04:00
Prad Nukala 664ea534ed feat(config): add stats script to package.json 2026-08-31 18:56:46 -04:00
Prad Nukala b86f36daf1 feat(cli): add terminal stats dashboard and solution detection utilities 2026-08-31 18:56:42 -04:00
Prad Nukala 7bae7b2920 feat(api): add concept temperature bands and expose in stats 2026-08-31 18:56:40 -04:00
Prad Nukala 64173857e7 feat(work): add new LeetCode solution files for 1426, 1832, 344, and 15 2026-08-31 16:50:30 -04:00
Prad Nukala 002c2c9c97 feat(work): add solutions for LeetCode 1365 and 268 problems 2026-08-31 16:36:29 -04:00
Prad Nukala fa8deac963 feat(work): add solution for LeetCode 387 First Unique Character in a String 2026-08-31 16:21:47 -04:00
Prad Nukala ef8d2b41e1 docs(todos): add review backlog planning markdown 2026-08-31 14:57:38 -04:00
87 changed files with 3956 additions and 51 deletions

No files matched your search

+10 -5
View File
@@ -23,6 +23,7 @@ flowchart LR
D1 -->|Sat midnight ET| Review[Review — Week N issue] 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] PI -->|"spaced-repetition.yml (6 AM ET cron, closed +3d/+7d)"| SR[Spaced Repetition — Month D issue]
D1 --> Charts[SVG charts + /api/stats] 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. - **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 # re-solves → work/3, work/7. Each section hides what its own bucket
# already holds; index+descriptions cached in # already holds; index+descriptions cached in
# ~/.local/share/leetcode/data.db — see apps/cli/db.ts) # ~/.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; 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) # most recently tested first, stamped in the db on exit 0)
bun run submit # submit ONE solution to LeetCode (newest scaffold first, nothing 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 ## 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/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/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/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/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, `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/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`). - `README.md` live charts are Worker endpoints (`/chart/*.svg`, `/badge/gate.svg`, 5-min Camo cache); the docs `/progress` page fetches `/api/stats` client-side (`apps/docs/islands/ProgressDashboard.tsx`).
- `apps/docs/blume.config.ts` — site base `/leetcode`; `README.md` campaign table doubles as topic-map input to `sync.ts` (`readmeTopics()`, `TOPIC_ALIASES`). - `apps/docs/blume.config.ts` — site base `/leetcode`; `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. - 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 ## 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. - **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, 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.
- **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.
- Local GitHub auth falls back to `gh auth token`; scripts run fine outside Actions. - Local GitHub auth falls back to `gh auth token`; scripts run fine outside Actions.
## Testing & QA ## Testing & QA
+1 -4
View File
@@ -10,14 +10,11 @@
* so relabelling a problem is enough to move it and titles stay readable. * so relabelling a problem is enough to move it and titles stay readable.
* The `problem` label is what marks a sub-issue as curriculum. * 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"; import type { GitHub } from "./github.ts";
const TITLE_RE = /^LC (\d+) · (.+)$/; 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. */ /** Deferred Hards enter the queue from this date, two per working day. */
const DEFER_FROM = "2026-09-28"; const DEFER_FROM = "2026-09-28";
+27
View File
@@ -19,6 +19,8 @@ import {
isoWeek, isoWeek,
rng, rng,
sample, sample,
TEMPERATURES,
temperatureOf,
weekdayOf, weekdayOf,
workingDay, workingDay,
} from "./srs.ts"; } 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", () => { describe("deterministic sampling", () => {
test("same seed, same picks", () => { test("same seed, same picks", () => {
const pool = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const pool = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
+50 -5
View File
@@ -1,6 +1,6 @@
/** /**
* SRS domain: ET dates, the work week, the interval ladder, deterministic * SRS domain: ET dates, the work week, the interval ladder, concept
* sampling, and the one write path for attempts. * 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 * 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 * 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; export const SCHEDULE: Record<string, number> = scheduleJson;
/** Week in which a topic was (or will be) taught. */ /** Date the schedule teaches a topic on, if it teaches it at all. */
export function topicWeek(topic: number): number | undefined { export function topicDate(topic: number): string | undefined {
for (const [date, t] of Object.entries(SCHEDULE)) { for (const [date, t] of Object.entries(SCHEDULE)) {
if (t === topic) return campaignWeek(date); if (t === topic) return date;
} }
return undefined; 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 ─────────────────────────────────────── // ── deterministic sampling ───────────────────────────────────────
/** FNV-1a → mulberry32: seeded PRNG so re-runs pick identical problems. */ /** 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 Result = "pass" | "fail";
export type Kind = "first" | "review" | "drill" | "gate"; 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 { export interface ProblemRow {
lc_number: number; lc_number: number;
issue: number; issue: number;
+146 -28
View File
@@ -1,19 +1,29 @@
/** /**
* GET /api/stats — the one JSON document behind the docs progress page. * GET /api/stats — the one JSON document behind the docs progress page and the
* Read-only aggregation over D1; shape is the page's contract, change both * `bun run stats` terminal dashboard. Read-only aggregation over D1; the shape
* together. A problem counts as "done" once it entered the ladder * is both readers' contract, so change all three together. A problem counts as
* (stage != 'new'). * "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 { import {
CAMPAIGN_DAYS, CAMPAIGN_DAYS,
CAMPAIGN_START, CAMPAIGN_START,
DIFFICULTIES,
DUE_WHERE, DUE_WHERE,
addDays, addDays,
campaignDay, campaignDay,
campaignWeek, campaignWeek,
daysBetween,
isoWeek, isoWeek,
STAGES, STAGES,
streak, streak,
temperatureOf,
topicDate,
type Temperature,
} from "./srs.ts"; } from "./srs.ts";
const PHASE_NAMES: Record<number, string> = { const PHASE_NAMES: Record<number, string> = {
@@ -24,6 +34,91 @@ const PHASE_NAMES: Record<number, string> = {
5: "V — Decision Space", 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> { export async function buildStats(db: D1Database, today: string): Promise<object> {
const { results: phaseRows } = await db const { results: phaseRows } = await db
.prepare( .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])); const ladder: Record<string, number> = Object.fromEntries(STAGES.map((s) => [s, 0]));
for (const row of ladderRows) ladder[row.stage] = row.n; 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 // 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 // the page speaks campaign weeks. The campaign never crosses a year
// boundary, so a plain offset converts safely. // 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 }>(); .all<{ week: number; issue: number | null; pass_rate: number | null; closed_on: string | null }>();
const gates = gateRows.map((g) => ({ ...g, week: g.week - isoWeekOffset })); const gates = gateRows.map((g) => ({ ...g, week: g.week - isoWeekOffset }));
// Review-queue depth for the next 14 days: everything due by that day. // Review-queue depth for the next 14 days: everything due by that day. One
const queue: { date: string; due: number }[] = []; // statement per day, but DUE_WHERE stays the only definition of "due" — the
for (let i = 0; i < 14; i++) { // batch is a single round trip, not fourteen.
const date = addDays(today, i); const dates = Array.from({ length: 14 }, (_, i) => addDays(today, i));
const row = await db const queueRows = await db.batch<{ n: number }>(
.prepare(`SELECT COUNT(*) AS n FROM problems WHERE ${DUE_WHERE}`) dates.map((date) =>
.bind(date) db.prepare(`SELECT COUNT(*) AS n FROM problems WHERE ${DUE_WHERE}`).bind(date),
.first<{ n: number }>(); ),
queue.push({ date, due: row?.n ?? 0 }); );
} const queue = dates.map((date, i) => ({ date, due: queueRows[i]?.results[0]?.n ?? 0 }));
const recent: { date: string; attempts: number; passes: number }[] = []; // Attempts per campaign day, every day present. This is the heatmap's data
for (let i = 6; i >= 0; i--) { // model — the docs page reads the tail of it for "last 7 days" rather than
const date = addDays(today, -i); // asking a second question that could answer differently.
const row = await db const end = addDays(CAMPAIGN_START, CAMPAIGN_DAYS - 1);
.prepare( const { results: heatRows } = await db
`SELECT COUNT(*) AS attempts, .prepare(
SUM(CASE WHEN result = 'pass' THEN 1 ELSE 0 END) AS passes `SELECT date,
FROM attempts WHERE date = ?`, COUNT(*) AS attempts,
) SUM(CASE WHEN result = 'pass' THEN 1 ELSE 0 END) AS passes
.bind(date) FROM attempts WHERE date BETWEEN ?1 AND ?2 GROUP BY date`,
.first<{ attempts: number; passes: number | null }>(); )
recent.push({ date, attempts: row?.attempts ?? 0, passes: row?.passes ?? 0 }); .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 { return {
generated: new Date().toISOString(), generated: new Date().toISOString(),
campaign: { day: campaignDay(today), week: campaignWeek(today), start: CAMPAIGN_START, days: CAMPAIGN_DAYS }, campaign: { day: campaignDay(today), week: campaignWeek(today), start: CAMPAIGN_START, days: CAMPAIGN_DAYS },
phases: [...phases.values()], phases: [...phases.values()],
ladder, ladder,
difficulty,
topics: await buildTopics(db, today),
gates, gates,
streak: await streak(db, today), streak: await streak(db, today),
queue, queue,
recent, heat,
}; };
} }
+87
View File
@@ -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;
}
+3 -1
View File
@@ -21,6 +21,7 @@
*/ */
import { join } from "node:path"; import { join } from "node:path";
import { SRS_API } from "./api.ts";
import { github } from "./github.ts"; import { github } from "./github.ts";
import { markdownTable, printTable, writeStepSummary } from "./report.ts"; import { markdownTable, printTable, writeStepSummary } from "./report.ts";
import { isImplemented } from "./source.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 // 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 // recompute that backfills whatever earlier runs missed) and re-runs write
// nothing. // 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()] const solved = [...work.entries()]
.flatMap(([lc, entry]) => [...entry.solved].sort().map((bucket) => ({ lc, bucket }))) .flatMap(([lc, entry]) => [...entry.solved].sort().map((bucket) => ({ lc, bucket })))
.sort((a, b) => a.lc - b.lc || a.bucket - b.bucket); .sort((a, b) => a.lc - b.lc || a.bucket - b.bucket);
+1
View File
@@ -4,6 +4,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"pick": "bun ./pick.ts", "pick": "bun ./pick.ts",
"stats": "bun ./stats.ts",
"test": "bun ./test.ts", "test": "bun ./test.ts",
"submit": "bun ./submit.ts", "submit": "bun ./submit.ts",
"sync": "bun ./sync.ts", "sync": "bun ./sync.ts",
+458
View File
@@ -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
View File
@@ -359,6 +359,24 @@ function ensureLabel(page: string, h: Header): string {
return page.replace(/^sidebar:$/m, `sidebar:\n${label}`); 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. // Existing docs pages, keyed by leetcode number from the frontmatter title.
const pages = new Map<number, string>(); // num -> path relative to DOCS 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()) { 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"]); report.push([workRef, existing, "ERROR: no '## Solution' heading"]);
continue; 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[] = []; const actions: string[] = [];
if (existing !== name) { if (existing !== name) {
await rename(join(DOCS, existing), join(DOCS, name)); await rename(join(DOCS, existing), join(DOCS, name));
+214
View File
@@ -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
View File
@@ -93,6 +93,25 @@ export function scanBucket(bucket: Bucket): Map<number, WorkFile[]> {
return files; 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. * 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(); const dates = firstSolved();
// An empty scaffold under work/1 is a problem still unsolved, not one owing // 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 }[] = []; const solved: { file: WorkFile; date: string }[] = [];
for (const files of scanBucket(1).values()) { for (const files of scanBucket(1).values()) {
const date = dates.get(files[0]!.lc); const date = dates.get(files[0]!.lc);
if (date === undefined) continue; // uncommitted: you only just solved it if (date === undefined) continue; // uncommitted: you only just solved it
const file = files.find((f) => const file = files.find(isSolution);
isImplemented(readFileSync(join(WORK, f.rel), "utf8"), f.rel.endsWith(".py") ? "py" : "js"),
);
if (file) solved.push({ file, date }); if (file) solved.push({ file, date });
} }
+18
View File
@@ -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` - `-10^9 <= target <= 10^9`
- Only one valid answer exists. - 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 ## Solution
```py ```py
@@ -45,3 +59,7 @@ class Solution:
seen[num] = i seen[num] = i
return [] 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` - `2 <= n <= 10^5`
- `0 <= height[i] <= 10^4` - `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 ## Solution
```py ```py
@@ -46,3 +62,7 @@ class Solution:
return res return res
``` ```
## Explanation
<YouTube id="UuiTKBwPgAo" title="Container with Most Water - Leetcode 11 - Python" />
@@ -23,6 +23,24 @@ sidebar:
- `1 <= prices.length <= 10^5` - `1 <= prices.length <= 10^5`
- `0 <= prices[i] <= 10^4` - `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 ## Solution
```py ```py
@@ -36,3 +54,7 @@ class Solution:
ans = max(ans, curr) ans = max(ans, curr)
return ans return ans
``` ```
## Explanation
<YouTube id="1pkOgXD63yU" title="Sliding Window: Best Time to Buy and Sell Stock - Leetcode 121 - Python" />
@@ -26,6 +26,20 @@ sidebar:
- `2 <= nums.length <= 500` - `2 <= nums.length <= 500`
- `0 <= nums[i] <= 100` - `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 ## Solution
```js ```js
@@ -37,6 +37,18 @@ startValue = 4 | startValue = 5 | nums
- `1 <= nums.length <= 100` - `1 <= nums.length <= 100`
- `-100 <= nums[i] <= 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 ## Solution
```js ```js
@@ -23,6 +23,19 @@ sidebar:
- `1 <= arr.length <= 1000` - `1 <= arr.length <= 1000`
- `0 <= arr[i] <= 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 ## Solution
```js ```js
@@ -27,6 +27,17 @@ sidebar:
- `1 <= nums.length <= 1000` - `1 <= nums.length <= 1000`
- `-10^6 <= nums[i] <= 10^6` - `-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 ## Solution
```js ```js
+26
View File
@@ -32,6 +32,28 @@ Notice that the solution set must not contain duplicate triplets.
- `3 <= nums.length <= 3000` - `3 <= nums.length <= 3000`
- `-10^5 <= nums[i] <= 10^5` - `-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 ## Solution
```py ```py
@@ -76,3 +98,7 @@ class Solution:
return result return result
``` ```
## Explanation
<YouTube id="jzZsG8n2R9A" title="3Sum - Leetcode 15 - Python" />
@@ -28,6 +28,21 @@ sidebar:
- `1 <= tokens.length <= 10^4` - `1 <= tokens.length <= 10^4`
- `tokens[i]` is either an operator: "+", "-", "*", or "/", or an integer in the range [-200, 200]. - `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 ## Solution
```py ```py
@@ -53,3 +68,7 @@ class Solution:
return stack[0] 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. - All the integers of `nums` are unique.
- `nums` is sorted and rotated between 1 and `n` times. - `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 ## Solution
```py ```py
@@ -53,3 +68,7 @@ class Solution:
return nums[lowest_index] 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` - `1 <= nums.length <= 100`
- `-100 <= nums[i] <= 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 ## Solution
```js ```js
@@ -40,3 +52,7 @@ var frequencySort = function (nums) {
return nums.sort((a, b) => freq[a] - freq[b] || b - a); 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` - `-1000 <= target <= 1000`
- The tests are generated such that there is exactly one solution. - 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 ## Solution
```py ```py
@@ -53,3 +69,7 @@ class Solution:
j-=1 j-=1
return [] 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` - `1 <= nums.length <= 10^5`
- `-10^9 <= nums[i] <= 10^9` - `-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 ## Solution
```js ```js
@@ -45,3 +58,7 @@ var containsDuplicate = function(nums) {
return false; 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` - `-30 <= nums[i] <= 30`
- The input is generated such that `answer[i]` is guaranteed to fit in a 32-bit integer. - 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 ## Solution
```js ```js
@@ -51,3 +62,7 @@ var productExceptSelf = function(nums) {
return answer; 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` - `0 <= nums[i] <= n`
- All the numbers of `nums` are unique. - 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 ## Solution
```js ```js
@@ -53,3 +65,7 @@ var missingNumber = function(nums) {
return -1; return -1;
}; };
``` ```
## Explanation
<YouTube id="WnPLSRLSANE" title="Missing Number - Blind 75 - Leetcode 268 - Python" />
@@ -20,6 +20,21 @@ sidebar:
- `0 <= left <= right < nums.length` - `0 <= left <= right < nums.length`
- At most `10^4` calls will be made to `sumRange`. - 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 ## Solution
```js ```js
@@ -50,3 +65,7 @@ class NumArray {
* var param_1 = obj.sumRange(left,right) * 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. - `nums` is an ascending array that is possibly rotated.
- `-10^4 <= target <= 10^4` - `-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 ## Solution
```js ```js
@@ -69,3 +92,7 @@ var search = function(nums, target) {
return -1; 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]`. - `k` is in the range `[1, the number of unique elements in the array]`.
- It is guaranteed that the answer is unique. - 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 ## Solution
```js ```js
@@ -49,3 +59,7 @@ var topKFrequent = function (nums, k) {
.slice(0, 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. - `nums` contains distinct values sorted in ascending order.
- `-10^4 <= target <= 10^4` - `-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 ## Solution
```py ```py
@@ -49,3 +65,7 @@ class Solution:
return (l + r) // 2 + 1 return (l + r) // 2 + 1
``` ```
## Explanation
<YouTube id="K-RYzDZkzCI" title="Search Insert Position - Binary Search - Leetcode 35 - Python" />
@@ -0,0 +1,37 @@
---
title: '48. Rotate Image'
description: You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise)
sidebar:
label: 'Rotate Image'
badge: 'Medium'
---
<Badge variant="accent">Matrix Index Math</Badge>
### Example 1:
- Input: `matrix = [[1,2,3],[4,5,6],[7,8,9]]`
- Output: `[[7,4,1],[8,5,2],[9,6,3]]`
### Example 2:
- Input: `matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]`
- Output: `[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]`
### Constraints:
- `n == matrix.length == matrix[i].length`
- `1 <= n <= 20`
- `-1000 <= matrix[i][j] <= 1000`
## Solution
```py
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
n = len(matrix)
for r in range(n):
for c in range(r + 1, n):
matrix[r][c], matrix[c][r] = matrix[c][r], matrix[r][c]
for r in range(n):
matrix[r].reverse()
```
@@ -27,6 +27,21 @@ sidebar:
- `0 <= strs[i].length <= 100` - `0 <= strs[i].length <= 100`
- `strs[i]` consists of lowercase English letters. - `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 ## Solution
```js ```js
@@ -49,3 +64,7 @@ var groupAnagrams = function(strs) {
return Object.values(anagrams); return Object.values(anagrams);
}; };
``` ```
## Explanation
<YouTube id="vzdNOK2oB2E" title="Group Anagrams - Categorize Strings by Count - Leetcode 49" />
@@ -0,0 +1,59 @@
---
title: '54. Spiral Matrix'
description: Given an m x n matrix, return all elements of the matrix in spiral order
sidebar:
label: 'Spiral Matrix'
badge: 'Medium'
---
<Badge variant="accent">Matrix Index Math</Badge>
### Example 1:
- Input: `matrix = [[1,2,3],[4,5,6],[7,8,9]]`
- Output: `[1,2,3,6,9,8,7,4,5]`
### Example 2:
- Input: `matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]`
- Output: `[1,2,3,4,8,12,11,10,9,5,6,7]`
### Constraints:
- `m == matrix.length`
- `n == matrix[i].length`
- `1 <= m, n <= 10`
- `-100 <= matrix[i][j] <= 100`
## Solution
```py
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
res = []
top, bottom = 0, len(matrix) - 1
left, right = 0, len(matrix[0]) - 1
while top <= bottom and left <= right:
# 1. Top Row
for c in range(left, right + 1):
res.append(matrix[top][c])
top += 1
# 2. Right Column
for r in range(top, bottom + 1):
res.append(matrix[r][right])
right -= 1
# 3. Bottom Row
if top <= bottom:
for c in range(right, left - 1, -1):
res.append(matrix[bottom][c])
bottom -= 1
# 4. Left Column
if left <= right:
for r in range(bottom, top - 1, -1):
res.append(matrix[r][left])
left += 1
return res
```
@@ -22,6 +22,21 @@ sidebar:
- `-1000 <= nums[i] <= 1000` - `-1000 <= nums[i] <= 1000`
- `-10^7 <= k <= 10^7` - `-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 ## Solution
```js ```js
@@ -49,3 +64,7 @@ var subarraySum = function(nums, k) {
return count 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. - All the integers in `nums` are unique.
- `nums` is sorted in ascending order. - `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 ## Solution
```js ```js
@@ -54,3 +70,7 @@ var search = function(nums, target) {
return -1; return -1;
}; };
``` ```
## Explanation
<YouTube id="s4DPM8ct1pI" title="Binary Search - Leetcode 704 - Python" />
@@ -0,0 +1,67 @@
---
title: '73. Set Matrix Zeroes'
description: Given an m x n integer matrix matrix, if an element is 0, set its entire row and column to 0's
sidebar:
label: 'Set Matrix Zeroes'
badge: 'Medium'
---
<Badge variant="accent">Matrix Index Math</Badge>
::::warning
You must do it in place.
::::
### Example 1:
- Input: `matrix = [[1,1,1],[1,0,1],[1,1,1]]`
- Output: `[[1,0,1],[0,0,0],[1,0,1]]`
### Example 2:
- Input: `matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]`
- Output: `[[0,0,0,0],[0,4,5,0],[0,3,1,0]]`
### Constraints:
- `m == matrix.length`
- `n == matrix[0].length`
- `1 <= m, n <= 200`
- `-2^31 <= matrix[i][j] <= 2^31 - 1`
- A straightforward solution using O(mn) space is probably a bad idea.
- A simple improvement uses O(m + n) space, but still not the best solution.
- Could you devise a constant space solution?
## Solution
```py
class Solution:
def setZeroes(self, matrix: List[List[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
m, n = len(matrix), len(matrix[0])
# 1. remember if row 0 or column 0 have their own zeros
row0_zero = any(matrix[0][c] == 0 for c in range(n))
col0_zero = any(matrix[r][0] == 0 for r in range(m))
# 2. use row 0 and column 0 as marker lists
for r in range(1, m):
for c in range(1, n):
if matrix[r][c] == 0:
matrix[r][0] = 0
matrix[0][c] = 0
# 3. zero the inner cells from the markers
for r in range(1, m):
for c in range(1, n):
if matrix[r][0] == 0 or matrix[0][c] == 0:
matrix[r][c] = 0
# 4. handle row 0 and column 0 last
if row0_zero:
for c in range(n):
matrix[0][c] = 0
if col0_zero:
for r in range(m):
matrix[r][0] = 0
```
@@ -25,6 +25,20 @@ sidebar:
- `1 <= temperatures.length <= 10^5` - `1 <= temperatures.length <= 10^5`
- `30 <= temperatures[i] <= 100` - `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 ## Solution
```py ```py
@@ -39,3 +53,7 @@ class Solution:
stack.append(i) stack.append(i)
return ans 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` - `1 <= m, n <= 100`
- `-10^4 <= matrix[i][j], target <= 10^4` - `-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 ## Solution
```py ```py
@@ -59,3 +85,7 @@ class Solution:
return True return True
return False 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. - All the values of `position` are unique.
- `0 < speed[i] <= 10^6` - `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 ## Solution
```py ```py
@@ -45,3 +61,7 @@ class Solution:
return len(stack) return len(stack)
``` ```
## Explanation
<YouTube id="Pr6T-3yB9RM" title="Car Fleet - Leetcode 853 - Python" />
@@ -26,6 +26,22 @@ sidebar:
- `piles.length <= h <= 10^9` - `piles.length <= h <= 10^9`
- `1 <= piles[i] <= 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 ## Solution
```js ```js
@@ -60,3 +76,7 @@ var minEatingSpeed = function(piles, h) {
return l; 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` - `-10^4 <= nums[i] <= 10^4`
- `nums` is sorted in non-decreasing order. - `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 ## Solution
```js ```js
@@ -53,3 +68,7 @@ var sortedSquares = function(nums) {
return ans; 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` - `-10^5 <= Node.val <= 10^5`
- pos is -1 or a valid index in the linked-list. - 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 ## Solution
```py ```py
@@ -54,3 +68,7 @@ class Solution:
return True return True
return False 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` - `1 <= sentence.length <= 1000`
- `sentence` consists of lowercase English letters. - `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 ## Solution
```js ```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` - `1 <= s.length, t.length <= 5 * 10^4`
- `s` and `t` consist of lowercase English letters. - `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 ## Solution
```js ```js
@@ -46,3 +59,7 @@ var isAnagram = function(s, t) {
return true; return true;
}; };
``` ```
## Explanation
<YouTube id="9UtInBqnCgA" title="Valid Anagram - Leetcode 242 - Python" />
@@ -28,6 +28,21 @@ sidebar:
- `0 <= s.length <= 10^5` - `0 <= s.length <= 10^5`
- `s` consists of English letters, digits, symbols and spaces. - `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 ## Solution
```py ```py
@@ -45,3 +60,7 @@ class Solution:
return ans 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` - `1 <= s.length <= 10^5`
- `s` consists of only lowercase English letters. - `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 ## Solution
```js ```js
@@ -45,3 +57,7 @@ var firstUniqChar = function (s) {
return -1; 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. - `s` consists of only uppercase English letters.
- `0 <= k <= s.length` - `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 ## Solution
```py ```py
@@ -45,3 +60,7 @@ class Solution:
res = max(res, r - l + 1) res = max(res, r - l + 1)
return res 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` - `1 <= s.length <= 5 * 10^5`
- `s` consists of uppercase and lowercase English letters and digits. - `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 ## Solution
```js ```js
@@ -47,3 +59,7 @@ var frequencySort = function (s) {
.join(""); .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` - `1 <= s1.length, s2.length <= 10^4`
- `s1` and `s2` consist of lowercase English letters. - `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 ## Solution
```py ```py
@@ -49,3 +67,7 @@ class Solution:
return False return False
``` ```
## Explanation
<YouTube id="UbyhOgBN834" title="Permutation in String - Leetcode 567 - Python" />
@@ -0,0 +1,57 @@
---
title: '2. Add Two Numbers'
description: You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order, and each of their nodes contains a single digit. Add the two numbers and return the sum as a linked list
sidebar:
label: 'Add Two Numbers'
badge: 'Medium'
---
<Badge variant="accent">Hybrid Structures</Badge>
### 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.
## Solution
```py
# 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]:
dummy = ListNode()
carry = 0
curr = dummy
while l1 or l2 or carry:
s = (l1.val if l1 else 0) + (l2.val if l2 else 0) + carry
carry, val = divmod(s, 10)
curr.next = ListNode(val)
curr = curr.next
l1 = l1.next if l1 else None
l2 = l2.next if l2 else None
return dummy.next
```
@@ -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]. - The number of nodes in the list is the range [0, 5000].
- `-5000 <= Node.val <= 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 ## Solution
```py ```py
@@ -48,3 +61,7 @@ class Solution:
curr = next_ curr = next_
return prev 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` - `-100 <= Node.val <= 100`
- Both list1 and list2 are sorted in non-decreasing order. - 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 ## Solution
```py ```py
@@ -64,3 +81,7 @@ class Solution:
current.next = list1 or list2 current.next = list1 or list2
return head 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. - 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. - 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 ## Solution
```py ```py
@@ -54,3 +76,7 @@ class MinStack:
# param_3 = obj.top() # param_3 = obj.top()
# param_4 = obj.getMin() # 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` - `1 <= s.length <= 10^4`
- `s` consists of parentheses only '()[]{}'. - `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 ## Solution
```py ```py
@@ -52,3 +70,7 @@ class Solution:
return True if not stack else False return True if not stack else False
``` ```
## Explanation
<YouTube id="WTzjTskDFMg" title="Valid Parentheses - Stack - Leetcode 20 - Python" />
@@ -0,0 +1,61 @@
---
title: '102. Binary Tree Level Order Traversal'
description: Given the root of a binary tree, return the level order traversal of its nodes' values. (i.e., from left to right, level by level)
sidebar:
label: 'Binary Tree Level Order Traversal'
badge: 'Medium'
---
<Badge variant="accent">Tree BFS</Badge>
### Example 1:
- Input: `root = [3,9,20,null,null,15,7]`
- Output: `[[3],[9,20],[15,7]]`
### Example 2:
- Input: `root = [1]`
- Output: `[[1]]`
### Example 3:
- Input: `root = []`
- Output: `[]`
### Constraints:
- The number of nodes in the tree is in the range [0, 2000].
- `-1000 <= Node.val <= 1000`
## Solution
```py
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
from collections import deque
class Solution:
# T: O(n) S: O(n)
def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
res = []
q = deque()
q.append(root)
while q:
qLen = len(q)
level = []
for i in range(qLen):
node = q.popleft()
if node:
level.append(node.val)
q.append(node.left)
q.append(node.right)
if level:
res.append(level)
return res
```
@@ -0,0 +1,39 @@
---
title: '104. Maximum Depth of Binary Tree'
description: Given the root of a binary tree, return its maximum depth
sidebar:
label: 'Maximum Depth of Binary Tree'
badge: 'Easy'
---
<Badge variant="accent">Tree DFS</Badge>
### Example 1:
- Input: `root = [3,9,20,null,null,15,7]`
- Output: `3`
### Example 2:
- Input: `root = [1,null,2]`
- Output: `2`
### Constraints:
- The number of nodes in the tree is in the range [0, 10^4].
- `-100 <= Node.val <= 100`
## Solution
```py
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right))
```
@@ -0,0 +1,52 @@
---
title: '1448. Count Good Nodes in Binary Tree'
description: Given a binary tree root, a node X in the tree is named good if in the path from root to X there are no nodes with a value greater than X
sidebar:
label: 'Count Good Nodes in Binary Tree'
badge: 'Medium'
---
<Badge variant="accent">Tree BFS</Badge>
### Example 1:
- Input: `root = [3,1,4,3,null,1,5]`
- Output: `4`
- Explanation: Nodes in blue are good. Root Node `(3`) is always a good node. Node `4` -> (3,4) is the maximum value in the path starting from the `root`. Node `5` -> (3,4,5) is the maximum value in the path Node `3` -> (3,1,3) is the maximum value in the path.
### Example 2:
- Input: `root = [3,3,null,4,2]`
- Output: `3`
- Explanation: Node `2` -> `(3`, `3`, `2`) is not good, because `"3`" is higher than it.
### Example 3:
- Input: `root = [1]`
- Output: `1`
- Explanation: Root is considered as good.
### Constraints:
- The number of nodes in the binary tree is in the range [1, 10^5].
- Each node's value is between [-10^4, 10^4].
## Solution
```py
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def goodNodes(self, root: TreeNode) -> int:
def dfs(node, max_val):
if not node:
return 0
is_good = 1 if node.val >= max_val else 0
new_max = max(max_val, node.val)
return is_good + dfs(node.left, new_max) + dfs(node.right, new_max)
return dfs(root, root.val)
```
@@ -0,0 +1,64 @@
---
title: '199. Binary Tree Right Side View'
description: Given the root of a binary tree, imagine yourself standing on the right side of it, return the values of the nodes you can see ordered from top to bottom
sidebar:
label: 'Binary Tree Right Side View'
badge: 'Medium'
---
<Badge variant="accent">Tree BFS</Badge>
### Example 1:
- Input: `root = [1,2,3,null,5,null,4]`
- Output: `[1,3,4]`
### Example 2:
- Input: `root = [1,2,3,4,null,null,null,5]`
- Output: `[1,3,4,5]`
### Example 3:
- Input: `root = [1,null,3]`
- Output: `[1,3]`
### Example 4:
- Input: `root = []`
- Output: `[]`
### Constraints:
- The number of nodes in the tree is in the range [0, 100].
- `-100 <= Node.val <= 100`
## Solution
```py
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
from collections import deque
class Solution:
def rightSideView(self, root: Optional[TreeNode]) -> List[int]:
res = []
q = deque([root])
while q:
rightSide = None
qLen = len(q)
for i in range(qLen):
node = q.popleft()
if node:
rightSide = node
q.append(node.left)
q.append(node.right)
if rightSide:
res.append(rightSide.val)
return res
```
@@ -0,0 +1,49 @@
---
title: '226. Invert Binary Tree'
description: Given the root of a binary tree, invert the tree, and return its root
sidebar:
label: 'Invert Binary Tree'
badge: 'Easy'
---
<Badge variant="accent">Tree DFS</Badge>
### Example 1:
- Input: `root = [4,2,7,1,3,6,9]`
- Output: `[4,7,2,9,6,3,1]`
### Example 2:
- Input: `root = [2,1,3]`
- Output: `[2,3,1]`
### Example 3:
- Input: `root = []`
- Output: `[]`
### Constraints:
- The number of nodes in the tree is in the range [0, 100].
- `-100 <= Node.val <= 100`
## Solution
```py
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
if not root:
return None
left = self.invertTree(root.left)
right = self.invertTree(root.right)
root.left = right
root.right = left
return root
```
@@ -0,0 +1,62 @@
---
title: '236. Lowest Common Ancestor of a Binary Tree'
description: Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree
sidebar:
label: 'Lowest Common Ancestor of a Binary Tree'
badge: 'Medium'
---
<Badge variant="accent">Tree DFS</Badge>
### Example 1:
- Input: `root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1`
- Output: `3`
- Explanation: The LCA of nodes `5` and `1` is `3`.
### Example 2:
- Input: `root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4`
- Output: `5`
- Explanation: The LCA of nodes `5` and `4` is `5`, since a node can be a descendant of itself according to the LCA definition.
### Example 3:
- Input: `root = [1,2], p = 1, q = 2`
- Output: `1`
### Constraints:
- The number of nodes in the tree is in the range [2, 10^5].
- `-10^9 <= Node.val <= 10^9`
- All Node.val are unique.
- `p != q`
- p and q will exist in the tree.
## Solution
```py
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def lowestCommonAncestor(
self, root: "TreeNode", p: "TreeNode", q: "TreeNode"
) -> "TreeNode":
if not root:
return None
if root == p or root == q:
return root
l = self.lowestCommonAncestor(root.left, p, q)
r = self.lowestCommonAncestor(root.right, p, q)
if l and r:
return root
else:
return l or r
```
@@ -28,6 +28,20 @@ sidebar:
- `1 <= s.length <= 2 * 10^5` - `1 <= s.length <= 2 * 10^5`
- `s` consists only of printable ASCII characters. - `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 ## Solution
```js ```js
@@ -49,3 +63,7 @@ var isPalindrome = function(s) {
return true; 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` - `1 <= s.length <= 10^5`
- `s[i]` is a printable ascii character. - `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 ## Solution
```js ```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` - `0 <= t.length <= 10^4`
- `s` and `t` consist only of lowercase English letters. - `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 ## Solution
```py ```py
@@ -42,3 +58,7 @@ class Solution:
return i == len(s) return i == len(s)
``` ```
## Explanation
<YouTube id="99RVfqklbCE" title="Is Subsequence - Leetcode 392" />
+177
View File
@@ -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.
+7 -3
View File
@@ -46,7 +46,8 @@ interface Stats {
}[]; }[];
streak: number; streak: number;
queue: { date: string; due: 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 { 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.phases) &&
Array.isArray(v.gates) && Array.isArray(v.gates) &&
Array.isArray(v.queue) && Array.isArray(v.queue) &&
Array.isArray(v.recent) && Array.isArray(v.heat) &&
typeof v.streak === "number" typeof v.streak === "number"
); );
} }
@@ -332,11 +333,14 @@ const GREENS = [
]; ];
function Recent({ stats }: { stats: Stats }) { 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 ( return (
<div style={card}> <div style={card}>
<div style={heading}>Last 7 days</div> <div style={heading}>Last 7 days</div>
<div style={{ display: "flex", gap: "0.4rem", flexWrap: "wrap" }}> <div style={{ display: "flex", gap: "0.4rem", flexWrap: "wrap" }}>
{stats.recent.map((r) => ( {week.map((r) => (
<span <span
key={r.date} key={r.date}
title={`${r.date}: ${r.passes}/${r.attempts} passed`} title={`${r.date}: ${r.passes}/${r.attempts} passed`}
+1
View File
@@ -15,6 +15,7 @@
"api:deploy": "cd apps/api && bun run deploy", "api:deploy": "cd apps/api && bun run deploy",
"api:test": "cd apps/api && bun test src", "api:test": "cd apps/api && bun test src",
"pick": "bun apps/cli/pick.ts", "pick": "bun apps/cli/pick.ts",
"stats": "bun apps/cli/stats.ts",
"sync": "bun apps/cli/sync.ts", "sync": "bun apps/cli/sync.ts",
"submit": "bun apps/cli/submit.ts", "submit": "bun apps/cli/submit.ts",
"test": "bun apps/cli/test.ts" "test": "bun apps/cli/test.ts"
@@ -0,0 +1,46 @@
"""
104. Maximum Depth of Binary Tree
Difficulty: Easy
https://leetcode.com/problems/maximum-depth-of-binary-tree/
──────────────────────────────────────────────────
Given the root of a binary tree, return its maximum depth.
A binary tree's maximum depth is the number of nodes along the
longest path from the root node down to the farthest leaf node.
Example 1:
Input: root = [3,9,20,null,null,15,7]
Output: 3
Example 2:
Input: root = [1,null,2]
Output: 2
Constraints:
• The number of nodes in the tree is in the range [0, 10^4].
• -100 <= Node.val <= 100
"""
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right))
@@ -0,0 +1,54 @@
"""
226. Invert Binary Tree
Difficulty: Easy
https://leetcode.com/problems/invert-binary-tree/
──────────────────────────────────────────────────
Given the root of a binary tree, invert the tree, and return its root.
Example 1:
Input: root = [4,2,7,1,3,6,9]
Output: [4,7,2,9,6,3,1]
Example 2:
Input: root = [2,1,3]
Output: [2,3,1]
Example 3:
Input: root = []
Output: []
Constraints:
• The number of nodes in the tree is in the range [0, 100].
• -100 <= Node.val <= 100
"""
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
if not root:
return None
left = self.invertTree(root.left)
right = self.invertTree(root.right)
root.left = right
root.right = left
return root
+47
View File
@@ -0,0 +1,47 @@
"""
48. Rotate Image
Difficulty: Medium
https://leetcode.com/problems/rotate-image/
──────────────────────────────────────────────────
You are given an n x n 2D matrix representing an image, rotate the
image by 90 degrees (clockwise).
You have to rotate the image in-place, which means you have to modify
the input 2D matrix directly. DO NOT allocate another 2D matrix and do
the rotation.
Example 1:
Input: matrix = [[1,2,3],[4,5,6],[7,8,9]]
Output: [[7,4,1],[8,5,2],[9,6,3]]
Example 2:
Input: matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
Output: [[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]
Constraints:
• n == matrix.length == matrix[i].length
• 1 <= n <= 20
• -1000 <= matrix[i][j] <= 1000
"""
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
n = len(matrix)
for r in range(n):
for c in range(r + 1, n):
matrix[r][c], matrix[c][r] = matrix[c][r], matrix[r][c]
for r in range(n):
matrix[r].reverse()
+66
View File
@@ -0,0 +1,66 @@
"""
54. Spiral Matrix
Difficulty: Medium
https://leetcode.com/problems/spiral-matrix/
──────────────────────────────────────────────────
Given an m x n matrix, return all elements of the matrix in spiral
order.
Example 1:
Input: matrix = [[1,2,3],[4,5,6],[7,8,9]]
Output: [1,2,3,6,9,8,7,4,5]
Example 2:
Input: matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
Output: [1,2,3,4,8,12,11,10,9,5,6,7]
Constraints:
• m == matrix.length
• n == matrix[i].length
• 1 <= m, n <= 10
• -100 <= matrix[i][j] <= 100
"""
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
res = []
top, bottom = 0, len(matrix) - 1
left, right = 0, len(matrix[0]) - 1
while top <= bottom and left <= right:
# 1. Top Row
for c in range(left, right + 1):
res.append(matrix[top][c])
top += 1
# 2. Right Column
for r in range(top, bottom + 1):
res.append(matrix[r][right])
right -= 1
# 3. Bottom Row
if top <= bottom:
for c in range(right, left - 1, -1):
res.append(matrix[bottom][c])
bottom -= 1
# 4. Left Column
if left <= right:
for r in range(bottom, top - 1, -1):
res.append(matrix[r][left])
left += 1
return res
@@ -0,0 +1,81 @@
"""
73. Set Matrix Zeroes
Difficulty: Medium
https://leetcode.com/problems/set-matrix-zeroes/
──────────────────────────────────────────────────
Given an m x n integer matrix matrix, if an element is 0, set its
entire row and column to 0's.
You must do it in place.
Example 1:
Input: matrix = [[1,1,1],[1,0,1],[1,1,1]]
Output: [[1,0,1],[0,0,0],[1,0,1]]
Example 2:
Input: matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]
Output: [[0,0,0,0],[0,4,5,0],[0,3,1,0]]
Constraints:
• m == matrix.length
• n == matrix[0].length
• 1 <= m, n <= 200
• -2^31 <= matrix[i][j] <= 2^31 - 1
Follow up:
• A straightforward solution using O(mn) space is probably a bad
idea.
• A simple improvement uses O(m + n) space, but still not the best
solution.
• Could you devise a constant space solution?
"""
class Solution:
def setZeroes(self, matrix: List[List[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
m, n = len(matrix), len(matrix[0])
# 1. remember if row 0 or column 0 have their own zeros
row0_zero = any(matrix[0][c] == 0 for c in range(n))
col0_zero = any(matrix[r][0] == 0 for r in range(m))
# 2. use row 0 and column 0 as marker lists
for r in range(1, m):
for c in range(1, n):
if matrix[r][c] == 0:
matrix[r][0] = 0
matrix[0][c] = 0
# 3. zero the inner cells from the markers
for r in range(1, m):
for c in range(1, n):
if matrix[r][0] == 0 or matrix[0][c] == 0:
matrix[r][c] = 0
# 4. handle row 0 and column 0 last
if row0_zero:
for c in range(n):
matrix[0][c] = 0
if col0_zero:
for r in range(m):
matrix[r][0] = 0
+125
View File
@@ -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,70 @@
"""
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]:
dummy = ListNode()
carry = 0
curr = dummy
while l1 or l2 or carry:
s = (l1.val if l1 else 0) + (l2.val if l2 else 0) + carry
carry, val = divmod(s, 10)
curr.next = ListNode(val)
curr = curr.next
l1 = l1.next if l1 else None
l2 = l2.next if l2 else None
return dummy.next
@@ -0,0 +1,66 @@
"""
102. Binary Tree Level Order Traversal
Difficulty: Medium
https://leetcode.com/problems/binary-tree-level-order-traversal/
──────────────────────────────────────────────────
Given the root of a binary tree, return the level order traversal of
its nodes' values. (i.e., from left to right, level by level).
Example 1:
Input: root = [3,9,20,null,null,15,7]
Output: [[3],[9,20],[15,7]]
Example 2:
Input: root = [1]
Output: [[1]]
Example 3:
Input: root = []
Output: []
Constraints:
• The number of nodes in the tree is in the range [0, 2000].
• -1000 <= Node.val <= 1000
"""
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
from collections import deque
class Solution:
# T: O(n) S: O(n)
def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
res = []
q = deque()
q.append(root)
while q:
qLen = len(q)
level = []
for i in range(qLen):
node = q.popleft()
if node:
level.append(node.val)
q.append(node.left)
q.append(node.right)
if level:
res.append(level)
return res
@@ -0,0 +1,94 @@
"""
1448. Count Good Nodes in Binary Tree
Difficulty: Medium
https://leetcode.com/problems/count-good-nodes-in-binary-tree/
──────────────────────────────────────────────────
Given a binary tree root, a node X in the tree is named good if in
the path from root to X there are no nodes with a value greater than
X.
Return the number of good nodes in the binary tree.
Example 1:
Input: root = [3,1,4,3,null,1,5]
Output: 4
Explanation: Nodes in blue are good.
Root Node (3) is always a good node.
Node 4 -> (3,4) is the maximum value in the path starting from the
root.
Node 5 -> (3,4,5) is the maximum value in the path
Node 3 -> (3,1,3) is the maximum value in the path.
Example 2:
Input: root = [3,3,null,4,2]
Output: 3
Explanation: Node 2 -> (3, 3, 2) is not good, because "3" is higher
than it.
Example 3:
Input: root = [1]
Output: 1
Explanation: Root is considered as good.
Constraints:
• The number of nodes in the binary tree is in the range [1, 10^5].
• Each node's value is between [-10^4, 10^4].
"""
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def goodNodes(self, root: TreeNode) -> int:
def dfs(node, max_val):
if not node:
return 0
is_good = 1 if node.val >= max_val else 0
new_max = max(max_val, node.val)
return is_good + dfs(node.left, new_max) + dfs(node.right, new_max)
return dfs(root, root.val)
@@ -0,0 +1,79 @@
"""
199. Binary Tree Right Side View
Difficulty: Medium
https://leetcode.com/problems/binary-tree-right-side-view/
──────────────────────────────────────────────────
Given the root of a binary tree, imagine yourself standing on the
right side of it, return the values of the nodes you can see ordered
from top to bottom.
Example 1:
Input: root = [1,2,3,null,5,null,4]
Output: [1,3,4]
Explanation:
Example 2:
Input: root = [1,2,3,4,null,null,null,5]
Output: [1,3,4,5]
Explanation:
Example 3:
Input: root = [1,null,3]
Output: [1,3]
Example 4:
Input: root = []
Output: []
Constraints:
• The number of nodes in the tree is in the range [0, 100].
• -100 <= Node.val <= 100
"""
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
from collections import deque
class Solution:
def rightSideView(self, root: Optional[TreeNode]) -> List[int]:
res = []
q = deque([root])
while q:
rightSide = None
qLen = len(q)
for i in range(qLen):
node = q.popleft()
if node:
rightSide = node
q.append(node.left)
q.append(node.right)
if rightSide:
res.append(rightSide.val)
return res
@@ -0,0 +1,76 @@
"""
236. Lowest Common Ancestor of a Binary Tree
Difficulty: Medium
https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/
──────────────────────────────────────────────────
Given a binary tree, find the lowest common ancestor (LCA) of two
given nodes in the tree.
According to the definition of LCA on Wikipedia: &ldquo;The lowest
common ancestor is defined between two nodes p and q as the lowest
node in T that has both p and q as descendants (where we allow a node
to be a descendant of itself).&rdquo;
Example 1:
Input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
Output: 3
Explanation: The LCA of nodes 5 and 1 is 3.
Example 2:
Input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
Output: 5
Explanation: The LCA of nodes 5 and 4 is 5, since a node can be a
descendant of itself according to the LCA definition.
Example 3:
Input: root = [1,2], p = 1, q = 2
Output: 1
Constraints:
• The number of nodes in the tree is in the range [2, 10^5].
• -10^9 <= Node.val <= 10^9
• All Node.val are unique.
• p != q
• p and q will exist in the tree.
"""
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def lowestCommonAncestor(
self, root: "TreeNode", p: "TreeNode", q: "TreeNode"
) -> "TreeNode":
if not root:
return None
if root == p or root == q:
return root
l = self.lowestCommonAncestor(root.left, p, q)
r = self.lowestCommonAncestor(root.right, p, q)
if l and r:
return root
else:
return l or r
@@ -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
+82
View File
@@ -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.
"""
+89
View File
@@ -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
+66
View File
@@ -0,0 +1,66 @@
"""
49. Group Anagrams
Difficulty: Medium
https://leetcode.com/problems/group-anagrams/
──────────────────────────────────────────────────
Given an array of strings strs, group the anagrams together. You can
return the answer in any order.
Example 1:
Input: strs = ["eat","tea","tan","ate","nat","bat"]
Output: [["bat"],["nat","tan"],["ate","eat","tea"]]
Explanation:
• There is no string in strs that can be rearranged to form "bat".
• The strings "nat" and "tan" are anagrams as they can be rearranged
to form each other.
• The strings "ate", "eat", and "tea" are anagrams as they can be
rearranged to form each other.
Example 2:
Input: strs = [""]
Output: [[""]]
Example 3:
Input: strs = ["a"]
Output: [["a"]]
Constraints:
• 1 <= strs.length <= 10^4
• 0 <= strs[i].length <= 100
• strs[i] consists of lowercase English letters.
"""
from collections import defaultdict
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
if not strs:
return strs
res = defaultdict(list)
for s in strs:
count = [0] * 26
for c in s:
count[ord(c) - ord("a")] += 1
res[tuple(count)].append(s)
return res.values()