Command-line planner
Suggest Review
practise what you are avoiding
A good week on DP turns into three weeks on DP, while linked list, design and slide window quietly go a month untouched — and they are exactly what a coding round is made of. This script measures that drift against the repo's own history, then spends its picks on the topics that are owed them.
== Balance: attention vs importance ==
category n importance attention ratio last
Array 139 ######...... #........... 0.19 UNDER today
Linked list 24 ##.......... ............ 0.00 UNDER 36d
Design 45 #........... ............ 0.24 UNDER 23d
Binary Search 46 ###......... ##.......... 0.94 today
Tree 62 ####........ ####........ 1.12 1d
Recursion 29 ##.......... ####........ 1.94 over 1d
Dynamic Programming 94 #####....... ############ 2.19 over today
The table is the finding. The list of problems underneath it is one way to act on it.
No install, no dependencies
Running it
Standard library Python, run from the repo root. It reads files that are already in the repo and never touches the network, so it is as fast offline as on.
python3 script/suggest_review.py # the balanced plan
python3 script/suggest_review.py --top 20 --per-category 3
python3 script/suggest_review.py --only must # MUST rows only
python3 script/suggest_review.py --only top100liked
python3 script/suggest_review.py --section "Binary Search" --top 10
python3 script/suggest_review.py --no-balance # plain importance rank
python3 script/suggest_review.py --markdown doc/review_suggestions.md
python3 script/suggest_review.py --self-test # the unit tests
What it reads
Nothing is invented — all three signals are already in the repo.
| Source | What it contributes |
|---|---|
| README.md | The problem universe and this repo's own judgement of what matters: the
MUST marker, the curated-list tags (blind75,
neetcode150, neetcode250, top100liked), the
company tags, and the status column's OK/AGAIN plus its
* run of review passes. |
| git history | When each problem was last worked on — the commit that touched its solution file, or named its LC number in the subject. |
| data/progress.txt | When it was last practised, which is not the same thing: a re-read that produced no commit still counts. |
doc/must_lc_list.md is generated from the same README rows, so reading README
covers it — and a test asserts this script and extract_must_lc.py still agree
on which rows carry MUST.
Three factors, then a round-robin
How a problem gets chosen
A flat “most important, least recently seen” ranking makes bias worse, because the biggest sections hold the most important problems and would fill the whole list. So the score is only half the job; the other half is who gets to spend it.
MUST +5.0 · top100liked +2.5 · blind75 +2.5
(the NeetCode lists nest, so only the narrowest scores: 150 → +1.5, 250 → +0.7) ·
google +1.5 · other company tags +0.2 each, capped at +1.0 ·
Medium +0.5, Hard +0.3 · and +0.2 per recorded pass on an AGAIN row,
capped at +2.4.
1 − 0.5 ^ (days / half-life), half-life 21 days. Touched today scores 0;
never touched saturates near 1. The clock reads whichever is more recent, the git
touch or the practice-log entry.
Each README section's share of total importance against its share of recent attention. A section getting none of the practice it is owed nearly doubles its problems' scores; one getting twice its share is cut to 0.4.
Picks are then spent round-robin across categories in deficit order, capped at
--per-category. The cap and the score floor are a preference for breadth,
not a quota: once every eligible category has had its share, the rest of
--top is filled by score.
Why the pass count counts as difficulty, not progress.
The AGAIN marker in this repo never graduates — 854 of 1,191 tracked rows
carry it, some after 20+ passes. So a row that has already cost twelve passes is read as
an unclosed gap and scores higher, not lower. An OK row's passes are
real progress and score nothing.
Read this first
The balance table
ratio = share of recent attention ÷ share of importance. 1.00 is a fair share.
Below 0.5 the run marks the row UNDER — that is the bias to fix. Above 1.8 it
marks it over — that is where the practice has been pooling.
share of importance
share of recent attention
underlined ratio = UNDER
Array is the row worth staring at. Its last touched column says today, so nothing looks wrong — but it is holding 11% of the importance on 1.9% of the attention. It is being touched, one problem at a time, not worked.
Real runs, real data
What comes back
Three sections, always in this order: what you have been working on lately, the balance table, then the picks. The picks are listed most-neglected category first, not highest score first — that order is the recommendation.
$ python3 script/suggest_review.py --top 12
== Recent focus (last 30 days) ==
818 touches on 188 problems across 22 categories.
(69 bulk commits ignored — a commit touching more than 6 solution
files is an import, not a session.)
Dynamic Programming 166 ##############
Recursion 75 ######........
Breadth-First Search 69 ######........
Depth-First Search 58 #####.........
== Suggested review (12 problems, 12 categories) ==
Ordered by how neglected the category is, not by score — that order
is the recommendation.
# LC title diff category last score why
1 48 Rotate Image Medium Array 220d 27.85 MUST · top100liked · blind75 · google · AGAIN x13
2 5 Longest Palindromic Substring Medium String 33d 15.45 MUST · top100liked · blind75 · google · AGAIN x19
3 24 Swap Nodes in Pairs Medium Linked list 47d 20.31 MUST · top100liked · google · AGAIN x16
4 739 Daily Temperatures Medium Stack 47d 16.94 MUST · top100liked · neetcode150 · google · AGAIN x19
5 146 LRU Cache Medium Design 90d 15.00 top100liked · neetcode150 · google · AGAIN x10
6 347 Top K Frequent Elements Medium Sort 99d 15.16 top100liked · blind75 · google · AGAIN x8
7 19 Remove Nth Node From End Medium Two Pointers 29d 14.28 MUST · top100liked · blind75 · google · AGAIN x10
8 3 Longest Substring w/o Repeat Medium Hash Table 51d 15.61 MUST · top100liked · blind75 · google · AGAIN x16
9 55 Jump Game Medium Greedy 88d 16.64 MUST · top100liked · blind75 · google · AGAIN x11
10 1658 Min Ops to Reduce X to Zero Medium Slide Window 54d 14.63 MUST · google · AGAIN x8
11 33 Search in Rotated Sorted Array Medium Binary Search 28d 9.77 MUST · top100liked · blind75 · google · AGAIN x12
12 1943 Describe the Painting Medium Scan Line 31d 10.32 MUST · google · AGAIN x5
Twelve picks, twelve different categories. Notice the scores do not descend — the order is deficit, not score.
$ python3 script/suggest_review.py --only must --top 8
# LC title diff category last score why
1 48 Rotate Image Medium Array 220d 27.85 MUST · top100liked · blind75 · google · AGAIN x13
2 5 Longest Palindromic Substring Medium String 33d 15.45 MUST · top100liked · blind75 · google · AGAIN x19
3 24 Swap Nodes in Pairs Medium Linked list 47d 20.31 MUST · top100liked · google · AGAIN x16
4 739 Daily Temperatures Medium Stack 47d 16.94 MUST · top100liked · neetcode150 · google · AGAIN x19
5 253 Meeting Rooms II Medium Sort 32d 13.76 MUST · blind75 · google · AGAIN x12
6 19 Remove Nth Node From End Medium Two Pointers 29d 14.28 MUST · top100liked · blind75 · google · AGAIN x10
7 3 Longest Substring w/o Repeat Medium Hash Table 51d 15.61 MUST · top100liked · blind75 · google · AGAIN x16
8 55 Jump Game Medium Greedy 88d 16.64 MUST · top100liked · blind75 · google · AGAIN x11
The balance is still measured over the whole pool before --only narrows it —
“which topic am I neglecting” is not a question you can answer from inside a filter.
$ python3 script/suggest_review.py --section "Binary Search" --top 5
# LC title diff category last score why
1 33 Search in Rotated Sorted Array Medium Binary Search 28d 9.77 MUST · top100liked · blind75 · google · AGAIN x12
2 153 Find Minimum in Rotated Sorted Medium Binary Search 27d 9.60 MUST · top100liked · blind75 · google · AGAIN x16
3 74 Search a 2D Matrix Medium Binary Search 97d 7.09 top100liked · neetcode150 · google
4 658 Find K Closest Elements Medium Binary Search 27d 6.90 MUST · neetcode250 · google · AGAIN x22
5 875 Koko Eating Bananas Medium Binary Search 27d 6.52 MUST · neetcode150 · google · AGAIN x7
One category, so --per-category has nothing to spread across and the refill
delivers the full five by score. --section is repeatable and
case-insensitive, and it also matches a problem filed under a second section.
$ python3 script/suggest_review.py --no-balance --top 8
# LC title diff category last score why
1 48 Rotate Image Medium Array 220d 15.39 MUST · top100liked · blind75 · google · AGAIN x13
2 78 Subsets Medium Backtracking 509d 14.40 MUST · top100liked · neetcode150 · google · AGAIN x16
3 55 Jump Game Medium Greedy 88d 14.37 MUST · top100liked · blind75 · google · AGAIN x11
4 45 Jump Game II Medium Greedy 229d 13.59 MUST · top100liked · neetcode150 · google · AGAIN x8
5 94 Binary Tree Inorder Traversal Medium Tree 311d 12.80 MUST · top100liked · neetcode250 · google · AGAIN x9
6 3 Longest Substring w/o Repeat Medium Hash Table 51d 12.54 MUST · top100liked · blind75 · google · AGAIN x16
7 57 Insert Interval Medium Array 88d 11.82 MUST · blind75 · google · AGAIN x10
8 208 Implement Trie (Prefix Tree) Medium Tree 44d 11.80 MUST · top100liked · blind75 · google · AGAIN x14
Eight picks, five categories — two Greedy, two Tree, two Array. This is the shape the balancing exists to avoid, and it is why the default is not this.
Every flag
Options
--top, --per-category, --window,
--half-life and --bulk-limit must be greater than zero, and
--min-score-frac must be within 0–1; anything else is a usage error rather
than a traceback.
Shaping the list
| Flag | Default | What it does |
|---|---|---|
| --top N | 15 | How many problems to suggest. |
| --per-category N | 2 | Most picks any one category may take before every other eligible category has
had a turn. A preference, not a quota — the remainder of --top is
filled by score. |
| --no-balance | off | Rank by score alone. Useful for seeing what the balancing is actually changing. |
| --min-score-frac F | 0.30 | Skip a category whose best candidate scores below this fraction of the top pick. Stops a five-row section from taking a slot with a problem nothing recommends. |
Narrowing the pool
| Flag | Default | What it does |
|---|---|---|
| --only MARKER | — | One of must, blind75, neetcode150,
neetcode250, top100liked, google,
again. |
| --section NAME | — | A README section such as "Binary Search". Repeatable and
case-insensitive. |
| --difficulty D | — | Easy, Medium or Hard. Repeatable. |
| --all-sections | off | Put SQL, Shell Script, Concurrency and the unverified kamyu104 drafts back into the pool and the balance maths. |
Tuning the measurement
| Flag | Default | What it does |
|---|---|---|
| --window DAYS | 30 | How much history counts as “recent attention”. Shorter reacts faster and is noisier. |
| --half-life DAYS | 21 | How fast a problem goes stale. Roughly the point where a solved-once problem stops being recallable without re-deriving it. |
| --bulk-limit N | 6 | A commit touching more solution files than this is an import, not a session, and its file paths are ignored. |
Output and plumbing
| Flag | Default | What it does |
|---|---|---|
| --markdown PATH | — | Also write a markdown report, with the balance table and the picks as linked tables. |
| --json PATH | — | Also write the full result — every category's shares and every pick's factors — as JSON. |
| --self-test | — | Run the unit suite and exit. |
| --readme / --progress / --lists / --repo | repo root | Point the four inputs somewhere else. Mostly for tests. |
Two deliberate blind spots
What it refuses to count
Both of these change the answer materially, and both are the difference between measuring practice and measuring commits.
Bulk commits are not study sessions
This repo has commits that add 393 generated Java files at once. Counted naively they made every one of those problems look practised on the same day, and put 563 problems inside a 30-day window that actually saw 188. LC 48 read as “61 days ago” when the real gap was 220.
So a commit touching more than --bulk-limit solution files has its file paths
dropped. An LC number written into the subject by hand — update 131 py — still
counts, because that is somebody recording work.
Some sections are not DSA practice
SQL, Shell Script, Concurrency and the Newly Added (kamyu104 gap) drafts —
which have never been run against LeetCode's judge — are left out of the balance maths so
they cannot distort a category's share. --all-sections puts them back.
Why there is a test suite for a helper script
The parsers are the risky part
Three of the four inputs are hand-written files whose shape nobody controls. The failure mode is not a crash — it is a parser that quietly reads fewer rows than there are and hands back a plausible but shrunken plan.
python3 script/test_suggest_review.py # stdlib unittest, no deps
python3 script/test_suggest_review.py -v
python3 script/test_suggest_review.py ParseProgress
python3 script/suggest_review.py --self-test # the same suite
Every fixture is a line that is really in those files: a MUST in the status
cell versus the word “must” in prose, a duplicate README row for one LC, an annotation
containing a comma, a line wrapped mid-annotation, a DP: label, a stray period
between entries. A LiveFiles class then holds the real README.md
and data/progress.txt, so a format drift fails a test instead of silently
shrinking the plan.
One of several
Where this sits
The review plan answers when — a spaced-repetition schedule off the same practice log. The roadmap answers in what order, for a topic you have not learned yet. The random picker answers anything, just start.
This script answers a different question: given everything you have already done, which topic is the practice quietly skipping? Its output pairs naturally with LC Coach — that one grades the attempt, this one chooses it.
Full written reference: doc/utility-scripts.md · source: script/suggest_review.py