add flashcard generation tooling and binary search cards
- gen-flashcards.py: auto-generate recognition cards from all problem files - toolkit/gen-problem-cards.org: 199 auto-generated problem cards - 5 binary search tool cards (std::binary_search, std::lower_bound, comparison, two-sum pattern, sorting gotcha) - two-sum.org: add binary search C++ attempt - lc-org.el: add doom emacs localleader keybinding support
This commit is contained in:
@@ -67,10 +67,27 @@ class Solution:
|
|||||||
|
|
||||||
** TODO C++
|
** TODO C++
|
||||||
#+begin_src cpp :lc-problem 1
|
#+begin_src cpp :lc-problem 1
|
||||||
|
#include <algorithm>
|
||||||
class Solution {
|
class Solution {
|
||||||
public:
|
public:
|
||||||
vector<int> twoSum(vector<int>& nums, int target) {
|
vector<int> twoSum(vector<int>& nums, int target) {
|
||||||
|
std::sort(nums.begin(), nums.end());
|
||||||
|
for (int i=0; i< nums.size(); i++) {
|
||||||
|
int x = nums[i];
|
||||||
|
if (x > target) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
// TODO: c++ binary search, i forgot how to do this
|
||||||
|
int want = target - x
|
||||||
|
auto it = std::binary_learch(nums.begin() + i + 1, nums.end(), want);
|
||||||
|
if (it != nums.end() && *it == want) {
|
||||||
|
int wi = std::distance(nums.begin(), it)
|
||||||
|
return {i, wi}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
reuturn {};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
|
#+RESULTS:
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#+ANKI_DECK: study_deck_02
|
||||||
|
* When to use lower_bound vs binary_search :cpp:binary-search:algorithm:retrieval::recognition:
|
||||||
|
:PROPERTIES:
|
||||||
|
:END:
|
||||||
|
|
||||||
|
** Front
|
||||||
|
When should you use ~std::lower_bound~ instead of ~std::binary_search~?
|
||||||
|
|
||||||
|
** Back
|
||||||
|
Use ~lower_bound~ when you need the *position/index* of the element.
|
||||||
|
Use ~binary_search~ when you only need to know *if it exists* (bool).
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#+ANKI_DECK: study_deck_02
|
||||||
|
* Two Sum gotcha: sorting destroys original indices :cpp:two-sum:binary-search:retrieval::recognition:
|
||||||
|
:PROPERTIES:
|
||||||
|
:END:
|
||||||
|
|
||||||
|
** Front
|
||||||
|
If you sort ~nums~ in-place for Two Sum, what goes wrong?
|
||||||
|
|
||||||
|
** Back
|
||||||
|
Sorting changes the indices. If the problem requires returning *original* indices, store ~{value, original_index}~ pairs before sorting.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#+ANKI_DECK: study_deck_02
|
||||||
|
* std::binary_search: check element exists :cpp:binary-search:algorithm:retrieval::recognition:
|
||||||
|
:PROPERTIES:
|
||||||
|
:END:
|
||||||
|
|
||||||
|
** Front
|
||||||
|
What does ~std::binary_search~ return, and what must the range be?
|
||||||
|
|
||||||
|
** Back
|
||||||
|
Returns ~bool~ (true if element found). The range must be *sorted*.
|
||||||
|
|
||||||
|
#+begin_src cpp
|
||||||
|
bool found = std::binary_search(vec.begin(), vec.end(), value);
|
||||||
|
#+end_src
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#+ANKI_DECK: study_deck_02
|
||||||
|
* std::lower_bound: find position of element :cpp:binary-search:algorithm:retrieval::recognition:
|
||||||
|
:PROPERTIES:
|
||||||
|
:END:
|
||||||
|
|
||||||
|
** Front
|
||||||
|
What does ~std::lower_bound~ return?
|
||||||
|
|
||||||
|
** Back
|
||||||
|
An iterator to the first element *>=* the given value. Returns ~end()~ if no such element exists.
|
||||||
|
|
||||||
|
#+begin_src cpp
|
||||||
|
auto it = std::lower_bound(vec.begin(), vec.end(), value);
|
||||||
|
#+end_src
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#+ANKI_DECK: study_deck_02
|
||||||
|
* Task: Two Sum complement lookup with lower_bound :cpp:binary-search:two-sum:retrieval::production:
|
||||||
|
:PROPERTIES:
|
||||||
|
:END:
|
||||||
|
|
||||||
|
** Front
|
||||||
|
Given sorted ~nums~ and index ~i~, write C++ to check if ~target - nums[i]~ exists in the rest of the array using ~std::lower_bound~.
|
||||||
|
|
||||||
|
** Back
|
||||||
|
#+begin_src cpp
|
||||||
|
int complement = target - nums[i];
|
||||||
|
auto it = std::lower_bound(nums.begin() + i + 1, nums.end(), complement);
|
||||||
|
if (it != nums.end() && *it == complement) {
|
||||||
|
return {(int)i, (int)std::distance(nums.begin(), it)};
|
||||||
|
}
|
||||||
|
#+end_src
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Auto-generate Anki flashcards from problem .org files.
|
||||||
|
|
||||||
|
Generates one recognition card per problem:
|
||||||
|
Front: Problem number + name
|
||||||
|
Back: Problem statement (first paragraph)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASE = Path(__file__).parent
|
||||||
|
DSA = BASE / "dsa"
|
||||||
|
OUTPUT = BASE / "toolkit" / "gen-problem-cards.org"
|
||||||
|
|
||||||
|
TOPIC_DISPLAY = {
|
||||||
|
"1-d-dynamic-programming": "1D DP",
|
||||||
|
"2-d-dynamic-programming": "2D DP",
|
||||||
|
"advanced-graphs": "Advanced Graphs",
|
||||||
|
"arrays-hashing": "Arrays & Hashing",
|
||||||
|
"backtracking": "Backtracking",
|
||||||
|
"binary-search": "Binary Search",
|
||||||
|
"bit-manipulation": "Bit Manipulation",
|
||||||
|
"graphs": "Graphs",
|
||||||
|
"greedy": "Greedy",
|
||||||
|
"heap-priority-queue": "Heap / Priority Queue",
|
||||||
|
"intervals": "Intervals",
|
||||||
|
"linked-list": "Linked List",
|
||||||
|
"math-geometry": "Math & Geometry",
|
||||||
|
"sliding-window": "Sliding Window",
|
||||||
|
"stack": "Stack",
|
||||||
|
"trees": "Trees",
|
||||||
|
"tries": "Tries",
|
||||||
|
"two-pointers": "Two Pointers",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_file(path: Path) -> dict | None:
|
||||||
|
"""Extract problem metadata and statement from an .org file."""
|
||||||
|
text = path.read_text()
|
||||||
|
|
||||||
|
# Match heading: * TODO 0217. Contains Duplicate :easy:
|
||||||
|
m = re.search(
|
||||||
|
r"^\* (?:TODO|DONE) (\d{4})\. (.+?) :(easy|medium|hard):",
|
||||||
|
text,
|
||||||
|
re.MULTILINE,
|
||||||
|
)
|
||||||
|
if not m:
|
||||||
|
return None
|
||||||
|
|
||||||
|
num = m.group(1)
|
||||||
|
name = m.group(2).strip()
|
||||||
|
difficulty = m.group(3)
|
||||||
|
|
||||||
|
# Topic = parent directory name
|
||||||
|
topic_slug = path.parent.name
|
||||||
|
topic = TOPIC_DISPLAY.get(topic_slug, topic_slug.replace("-", " ").title())
|
||||||
|
|
||||||
|
# Problem statement: first non-empty, non-heading, non-property line after :END:
|
||||||
|
after_props = text[m.end():]
|
||||||
|
lines = after_props.split("\n")
|
||||||
|
statement_lines = []
|
||||||
|
in_block = False
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped == ":END:":
|
||||||
|
continue
|
||||||
|
if not stripped:
|
||||||
|
if statement_lines:
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
if stripped.startswith("#+") or stripped.startswith("*") or stripped.startswith(":"):
|
||||||
|
if statement_lines:
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
statement_lines.append(stripped)
|
||||||
|
|
||||||
|
statement = " ".join(statement_lines).strip()
|
||||||
|
# Clean up org markup for plain text
|
||||||
|
statement = re.sub(r"[~=*/_]", "", statement)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"num": num,
|
||||||
|
"name": name,
|
||||||
|
"difficulty": difficulty,
|
||||||
|
"topic": topic,
|
||||||
|
"statement": statement,
|
||||||
|
"path": path,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def generate_cards(problems: list[dict]) -> str:
|
||||||
|
"""Generate org-mode flashcard content."""
|
||||||
|
parts = []
|
||||||
|
parts.append("#+TITLE: Auto-Generated Problem Cards")
|
||||||
|
parts.append("#+ANKI_DECK: study_deck_02")
|
||||||
|
parts.append("")
|
||||||
|
|
||||||
|
for p in sorted(problems, key=lambda x: int(x["num"])):
|
||||||
|
num = p["num"]
|
||||||
|
name = p["name"]
|
||||||
|
diff = p["difficulty"]
|
||||||
|
topic = p["topic"]
|
||||||
|
stmt = p["statement"]
|
||||||
|
|
||||||
|
# Card: given problem name → what does it ask?
|
||||||
|
title = f"LC {num}. {name} — what does it ask?"
|
||||||
|
tags = f":leetcode:{diff}:{topic.replace(' ', '-').lower()}:retrieval::recognition:"
|
||||||
|
|
||||||
|
parts.append(f"* {title} {tags}")
|
||||||
|
parts.append(":PROPERTIES:")
|
||||||
|
parts.append(":ANKI_NOTE_TYPE: Basic")
|
||||||
|
parts.append(":END:")
|
||||||
|
parts.append("** Front")
|
||||||
|
parts.append(f"What does LeetCode {num} *{name}* ask you to do?")
|
||||||
|
parts.append("** Back")
|
||||||
|
parts.append(stmt)
|
||||||
|
parts.append("")
|
||||||
|
|
||||||
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
dry_run = "--dry-run" in sys.argv
|
||||||
|
|
||||||
|
org_files = sorted(DSA.rglob("*.org"))
|
||||||
|
org_files = [f for f in org_files if f.name != "udfs.org"]
|
||||||
|
print(f"Found {len(org_files)} problem files")
|
||||||
|
|
||||||
|
problems = []
|
||||||
|
skipped = 0
|
||||||
|
for f in org_files:
|
||||||
|
p = parse_file(f)
|
||||||
|
if p:
|
||||||
|
problems.append(p)
|
||||||
|
else:
|
||||||
|
skipped += 1
|
||||||
|
print(f" skipped: {f.name}")
|
||||||
|
|
||||||
|
print(f"Parsed: {len(problems)} problems, {skipped} skipped")
|
||||||
|
|
||||||
|
content = generate_cards(problems)
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
print(f"\nWould write {len(problems)} cards to {OUTPUT}")
|
||||||
|
# Show first 3 cards as preview
|
||||||
|
preview = content.split("\n\n\n")[:3]
|
||||||
|
print("\n--- Preview ---")
|
||||||
|
print("\n\n".join(preview))
|
||||||
|
else:
|
||||||
|
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
OUTPUT.write_text(content)
|
||||||
|
print(f"\nWrote {len(problems)} cards to {OUTPUT}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -71,22 +71,32 @@ Returns a plist (:problem :lang :input :code) or signals an error."
|
|||||||
(display-buffer (process-buffer proc)))))
|
(display-buffer (process-buffer proc)))))
|
||||||
(display-buffer buf)))
|
(display-buffer buf)))
|
||||||
|
|
||||||
(defvar lc-org-mode-map
|
(defvar lc-org-mode-map (make-sparse-keymap)
|
||||||
(let ((map (make-sparse-keymap))
|
"Keymap for `lc-org-mode'.")
|
||||||
(prefix (make-sparse-keymap)))
|
|
||||||
|
(when (and (boundp 'doom-version) (fboundp 'map!))
|
||||||
|
(map! :map lc-org-mode-map
|
||||||
|
:localleader
|
||||||
|
:prefix "l"
|
||||||
|
"s" #'lc-org-submit
|
||||||
|
"r" #'lc-org-run
|
||||||
|
"t" #'lc-org-status
|
||||||
|
"p" #'lc-org-show-problem
|
||||||
|
"d" #'lc-org-daily))
|
||||||
|
|
||||||
|
(unless (and (boundp 'doom-version) (fboundp 'map!))
|
||||||
|
(let ((prefix (make-sparse-keymap)))
|
||||||
(define-key prefix (kbd "s") #'lc-org-submit)
|
(define-key prefix (kbd "s") #'lc-org-submit)
|
||||||
(define-key prefix (kbd "r") #'lc-org-run)
|
(define-key prefix (kbd "r") #'lc-org-run)
|
||||||
(define-key prefix (kbd "t") #'lc-org-status)
|
(define-key prefix (kbd "t") #'lc-org-status)
|
||||||
(define-key prefix (kbd "p") #'lc-org-show-problem)
|
(define-key prefix (kbd "p") #'lc-org-show-problem)
|
||||||
(define-key prefix (kbd "d") #'lc-org-daily)
|
(define-key prefix (kbd "d") #'lc-org-daily)
|
||||||
(define-key map (kbd "C-c l") prefix)
|
(define-key lc-org-mode-map (kbd "C-c l") prefix)))
|
||||||
map)
|
|
||||||
"Keymap for `lc-org-mode'.")
|
|
||||||
|
|
||||||
;;;###autoload
|
;;;###autoload
|
||||||
(define-minor-mode lc-org-mode
|
(define-minor-mode lc-org-mode
|
||||||
"Minor mode for LeetCode CLI integration in org source blocks.
|
"Minor mode for LeetCode CLI integration in org source blocks.
|
||||||
Keybindings under C-c l:
|
Keybindings under localleader l (SPC m l or , l):
|
||||||
s submit — submit current block
|
s submit — submit current block
|
||||||
r run — run with test input
|
r run — run with test input
|
||||||
t status — check submission status
|
t status — check submission status
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user