diff --git a/docs/autopilot.md b/docs/autopilot.md index 740c152..995613f 100644 --- a/docs/autopilot.md +++ b/docs/autopilot.md @@ -47,7 +47,7 @@ The owner's priority since 2026-11-03: proper exam practice. The design, decisio - [x] 3. Review queue (Leitner) at `/review`, with "N due" on the Dashboard. (PR #59, stacked on #58) - [ ] 4. Question banks for Operating Systems and Advanced Algorithms, then at least 4 questions per taught lesson everywhere. Also set `lesson` (and `section`) on the 259 migrated questions, which only know their quiz. Split into runs: - [x] 4a. Operating Systems bank: 67 questions, every one with `lesson` and `section`. (PR #62, stacked on #59) - - [ ] 4b. Advanced Algorithms bank (24 lessons). + - [x] 4b. Advanced Algorithms bank: 80 questions across all 24 lessons, each with `lesson` and `section`. (PR #63, stacked on #62) - [ ] 4c. Bring other courses up to 4 questions per taught lesson. - [ ] 4d. Set `lesson`/`section` on the 259 migrated questions. - [ ] 5. Generated questions (subnets, scheduling, Big-O first). @@ -79,6 +79,7 @@ Nothing yet. Newest first. One line per run: date and run, what was done, PR link, merged or open. +- 2026-11-05 10:40 (run B): exam phase 4b, Advanced Algorithms question bank (80 questions; master method, hashing and probing, red-black trees, DP, greedy and NP worked by script with fresh inputs, every claim checked against its lesson). Every course with quizzes or a bank now has an exam except Computer Literacy and The Exam Mind. PR #63 (stacked on #62), open. - 2026-11-05 06:10 (run A): exam phase 4a, Operating Systems question bank (67 questions across all 17 lessons, calculation answers worked by script, every claim checked against its lesson; new test that every `section` is a real heading). The OS exam now works. PR #62 (stacked on #59), open. - 2026-11-04 10:40 (run B): exam phase 3, the review queue (Leitner boxes 1/2/4/8/16 days in `src/lib/review.ts`, `/review` with instant feedback and interleaved courses, quizzes and exams feed it, header "Review" badge when due, Dashboard panel). Noted that main already gained three of the Algorithm Design animations proposed on 2026-11-04 (stability, greedy coins 1/3/4, linear search best/worst). PR #59 (stacked on #58), open. - 2026-11-04 06:10 (run A): exam phase 2, the course exam page (20 mixed questions allocated by module size, shuffled options, optional 90 s/question timer, per-module results, missed questions with explanations and links back, attempts saved; "Take a course exam" on course pages). First rebased #53 onto main, where a graphics quiz option had been reworded, and re-checked the bank against main. PR #58 (stacked on #53), open. diff --git a/src/content/advanced-algorithms/questions.ts b/src/content/advanced-algorithms/questions.ts new file mode 100644 index 0000000..a84dfe2 --- /dev/null +++ b/src/content/advanced-algorithms/questions.ts @@ -0,0 +1,1139 @@ +/* Question bank for this course. Ids are permanent: review history is keyed by them. + Format and rules: docs/exams.md. Numbers in calculation questions were worked again + by script, with fresh inputs rather than the lessons' own examples. */ + +import type { Question } from "@/lib/questions"; + +const L = "advanced-algorithms"; + +export const questions: Question[] = [ + /* ---------------- Foundations of Analysis ---------------- */ + { + id: `${L}/analysis-of-algorithms/insertion-worst`, + lesson: "analysis-of-algorithms", + section: "insertion-sort-analysis", + prompt: "What is insertion sort's worst-case running time, and which input causes it?", + options: [ + { id: "a", text: "Θ(n lg n), on random input" }, + { id: "b", text: "Θ(n²), on input sorted in reverse order" }, + { id: "c", text: "Θ(n²), on input that is already sorted" }, + { id: "d", text: "Θ(n), on input sorted in reverse order" }, + ], + correctOptionId: "b", + explanation: "On reverse-sorted input, each new element shifts past every element before it: 1 + 2 + … + (n − 1) = Θ(n²). Already-sorted input is the best case, Θ(n).", + }, + { + id: `${L}/analysis-of-algorithms/three-kinds`, + lesson: "analysis-of-algorithms", + section: "three-kinds-of-analysis", + prompt: "Why is worst-case analysis usually preferred over best-case analysis?", + options: [ + { id: "a", text: "Best case is always the same as worst case" }, + { id: "b", text: "Worst case is easier to measure with a stopwatch" }, + { id: "c", text: "Worst case is a guarantee for every input; best case can make a slow algorithm look fast on one lucky input" }, + { id: "d", text: "Best case needs a probability distribution over inputs" }, + ], + correctOptionId: "c", + explanation: "An upper bound over all inputs is a promise. Best case can be faked: almost any algorithm is fast on some input. Average case is the one that needs assumptions about input distribution.", + }, + { + id: `${L}/analysis-of-algorithms/merge-recurrence`, + lesson: "analysis-of-algorithms", + section: "analyzing-merge-sort", + prompt: "Which recurrence describes merge sort?", + options: [ + { id: "a", text: "T(n) = 2T(n/2) + Θ(n)" }, + { id: "b", text: "T(n) = T(n/2) + Θ(1)" }, + { id: "c", text: "T(n) = T(n − 1) + Θ(n)" }, + { id: "d", text: "T(n) = 2T(n/2) + Θ(1)" }, + ], + correctOptionId: "a", + explanation: "Two halves sorted recursively, plus a linear merge. T(n) = T(n/2) + Θ(1) is binary search; T(n − 1) + Θ(n) is insertion sort's worst case.", + }, + { + id: `${L}/analysis-of-algorithms/machine-independent`, + lesson: "analysis-of-algorithms", + section: "machine-independent-time", + prompt: "Why do we compare algorithms by their growth rate instead of measured seconds?", + options: [ + { id: "a", text: "Because seconds can't be measured precisely" }, + { id: "b", text: "Because growth rate ignores machine-dependent constants and shows what happens as n gets large" }, + { id: "c", text: "Because all algorithms take the same number of seconds" }, + { id: "d", text: "Because growth rate is always smaller than the time in seconds" }, + ], + correctOptionId: "b", + explanation: "A faster computer changes the constant factor, not the shape. For large enough n, a Θ(n lg n) algorithm beats a Θ(n²) one on any pair of machines.", + }, + { + id: `${L}/counting-the-basic-operation/every-case`, + lesson: "counting-the-basic-operation", + section: "every-case-time-complexity", + prompt: "Adding up the n elements of an array has an every-case time complexity. Why?", + options: [ + { id: "a", text: "Because addition is a fast operation" }, + { id: "b", text: "Because the array might be sorted" }, + { id: "c", text: "Because the basic operation runs exactly n times whatever the values are" }, + { id: "d", text: "Because it has no basic operation" }, + ], + correctOptionId: "c", + explanation: "Every-case means the count depends only on n, not on the contents. Sequential search isn't every-case: it can stop early.", + }, + { + id: `${L}/counting-the-basic-operation/sequential-worst`, + lesson: "counting-the-basic-operation", + section: "when-there-is-no-every-case", + prompt: "Sequential search on n items: what is W(n), the worst-case number of comparisons, and when does it happen?", + options: [ + { id: "a", text: "n, when x is last or not in the array" }, + { id: "b", text: "n/2, on average" }, + { id: "c", text: "1, when x is first" }, + { id: "d", text: "lg n, always" }, + ], + correctOptionId: "a", + explanation: "Worst case checks every element. n/2 is the average when x is present and equally likely to be anywhere; 1 is the best case.", + }, + { + id: `${L}/counting-the-basic-operation/fibonacci`, + lesson: "counting-the-basic-operation", + section: "fibonacci-the-same-problem-two-very-different-algorithms", + prompt: "Why is the plain recursive Fibonacci algorithm so much slower than the iterative one?", + options: [ + { id: "a", text: "Recursion is always slower than loops by a constant factor" }, + { id: "b", text: "The recursive version uses bigger numbers" }, + { id: "c", text: "The iterative version uses a faster formula" }, + { id: "d", text: "It recomputes the same values over and over, so the number of calls grows exponentially; the iterative version computes each value once" }, + ], + correctOptionId: "d", + explanation: "fib(n) calls fib(n − 1) and fib(n − 2), which overlap massively. Storing each value once (iteratively or in a table) brings it down to linear time.", + }, + { + id: `${L}/asymptotic-notation/big-o-proof`, + lesson: "asymptotic-notation", + section: "big-o", + prompt: "To prove 3n + 2 ∈ O(n), which constants work?", + options: [ + { id: "a", text: "c = 3, n₀ = 1" }, + { id: "b", text: "c = 5, n₀ = 1" }, + { id: "c", text: "c = 1, n₀ = 100" }, + { id: "d", text: "No constants work; 3n + 2 isn't O(n)" }, + ], + correctOptionId: "b", + explanation: "For n ≥ 1, 3n + 2 ≤ 3n + 2n = 5n. With c = 3 you'd need 3n + 2 ≤ 3n, which is never true; with c = 1, 3n + 2 ≤ n is never true either.", + }, + { + id: `${L}/asymptotic-notation/rules`, + lesson: "asymptotic-notation", + section: "big-o-rules", + prompt: "What is the tightest Big-O of 4n³ + 100n² lg n + 7?", + options: [ + { id: "a", text: "O(n³)" }, + { id: "b", text: "O(n² lg n)" }, + { id: "c", text: "O(n⁵ lg n)" }, + { id: "d", text: "O(4n³)" }, + ], + correctOptionId: "a", + explanation: "Keep the fastest-growing term and drop its constant: n³ outgrows n² lg n. O(4n³) is the same set as O(n³), but the constant is never written.", + }, + { + id: `${L}/asymptotic-notation/growth-order`, + lesson: "asymptotic-notation", + section: "the-functions-youll-meet", + prompt: "Which list is in increasing order of growth?", + options: [ + { id: "a", text: "n, lg n, n lg n, n², 2ⁿ" }, + { id: "b", text: "lg n, n lg n, n, n², 2ⁿ" }, + { id: "c", text: "lg n, n, n lg n, 2ⁿ, n²" }, + { id: "d", text: "lg n, n, n lg n, n², 2ⁿ" }, + ], + correctOptionId: "d", + explanation: "Logarithmic < linear < linearithmic < quadratic < exponential. 2ⁿ overtakes every polynomial for large n.", + }, + { + id: `${L}/asymptotic-notation/theta`, + lesson: "asymptotic-notation", + section: "rate-of-growth", + prompt: "f(n) = Θ(g(n)) means…", + options: [ + { id: "a", text: "f grows strictly slower than g" }, + { id: "b", text: "f is bounded above by g only" }, + { id: "c", text: "f is bounded both above and below by constant multiples of g, for large n" }, + { id: "d", text: "f(n) = g(n) for every n" }, + ], + correctOptionId: "c", + explanation: "Θ is a tight bound: f ∈ O(g) and f ∈ Ω(g). Big-O alone is only an upper bound.", + }, + { + id: `${L}/solving-recurrences/master-case1`, + lesson: "solving-recurrences", + section: "4-the-master-method", + prompt: "Solve T(n) = 4T(n/2) + n with the master method.", + options: [ + { id: "a", text: "Θ(n lg n)" }, + { id: "b", text: "Θ(n²)" }, + { id: "c", text: "Θ(n)" }, + { id: "d", text: "Θ(n² lg n)" }, + ], + correctOptionId: "b", + explanation: "n^(log₂ 4) = n². f(n) = n is polynomially smaller (Case 1), so the leaves win: Θ(n²).", + }, + { + id: `${L}/solving-recurrences/master-case2`, + lesson: "solving-recurrences", + section: "4-the-master-method", + prompt: "Solve T(n) = 9T(n/3) + n² with the master method.", + options: [ + { id: "a", text: "Θ(n²)" }, + { id: "b", text: "Θ(n³)" }, + { id: "c", text: "Θ(n² lg n)" }, + { id: "d", text: "Θ(n lg n)" }, + ], + correctOptionId: "c", + explanation: "n^(log₃ 9) = n², the same as f(n): Case 2, a tie, so every level costs the same and there are Θ(lg n) levels: Θ(n² lg n).", + }, + { + id: `${L}/solving-recurrences/master-case3`, + lesson: "solving-recurrences", + section: "4-the-master-method", + prompt: "Solve T(n) = 2T(n/2) + n² with the master method.", + options: [ + { id: "a", text: "Θ(n lg n)" }, + { id: "b", text: "Θ(n² lg n)" }, + { id: "c", text: "Θ(n)" }, + { id: "d", text: "Θ(n²)" }, + ], + correctOptionId: "d", + explanation: "n^(log₂ 2) = n, and f(n) = n² is polynomially bigger. Regularity holds: 2(n/2)² = n²/2 ≤ ½·n². Case 3: the root wins, Θ(n²).", + }, + { + id: `${L}/solving-recurrences/binary-search`, + lesson: "solving-recurrences", + section: "where-a-recurrence-comes-from-binary-search", + prompt: "Binary search's recurrence T(n) = T(n/2) + 1 solves to…", + options: [ + { id: "a", text: "Θ(lg n)" }, + { id: "b", text: "Θ(n)" }, + { id: "c", text: "Θ(1)" }, + { id: "d", text: "Θ(n lg n)" }, + ], + correctOptionId: "a", + explanation: "Each level does constant work and halves n, so there are about lg n levels. With the master method: a = 1, b = 2, n^0 = 1 = f(n), Case 2, Θ(lg n).", + }, + { + id: `${L}/lab-bit-manipulation/power-of-two`, + lesson: "lab-bit-manipulation", + section: "1-is-x-a-power-of-2", + prompt: "For a positive integer x, x & (x − 1) == 0 tells you that…", + options: [ + { id: "a", text: "x is even" }, + { id: "b", text: "x is a power of 2" }, + { id: "c", text: "x is odd" }, + { id: "d", text: "x is zero" }, + ], + correctOptionId: "b", + explanation: "Subtracting 1 flips the lowest 1-bit and everything below it, so the AND clears exactly that bit. The result is 0 only if x had a single 1-bit. (Zero has to be ruled out separately.)", + }, + { + id: `${L}/lab-bit-manipulation/ith-bit`, + lesson: "lab-bit-manipulation", + section: "3-is-the-i-th-bit-set", + prompt: "Which expression is nonzero exactly when bit i of x is set?", + options: [ + { id: "a", text: "x | (1 << i)" }, + { id: "b", text: "x ^ i" }, + { id: "c", text: "x & (1 << i)" }, + { id: "d", text: "x << i" }, + ], + correctOptionId: "c", + explanation: "1 << i is a mask with only bit i set; AND keeps that bit of x and clears the rest. OR would always be nonzero.", + }, + { + id: `${L}/lab-algorithm-analysis/three-way-merge`, + lesson: "lab-algorithm-analysis", + section: "problem-2-3-way-merge-sort", + prompt: "A 3-way merge sort splits into three thirds and merges in linear time. Its running time is…", + options: [ + { id: "a", text: "Θ(n log₃ n), which is still Θ(n lg n)" }, + { id: "b", text: "Θ(n)" }, + { id: "c", text: "Θ(n^1.58)" }, + { id: "d", text: "Θ(n²)" }, + ], + correctOptionId: "a", + explanation: "T(n) = 3T(n/3) + Θ(n) is master Case 2: Θ(n log₃ n). Logarithms in different bases differ only by a constant factor, so it's the same class as Θ(n lg n).", + }, + { + id: `${L}/lab-algorithm-analysis/power`, + lesson: "lab-algorithm-analysis", + section: "problem-1-computing-ab", + prompt: "Computing aᵇ by multiplying a by itself b − 1 times takes how many multiplications, as a function of b?", + options: [ + { id: "a", text: "Θ(lg b)" }, + { id: "b", text: "Θ(1)" }, + { id: "c", text: "Θ(b²)" }, + { id: "d", text: "Θ(b)" }, + ], + correctOptionId: "d", + explanation: "One multiplication per step, b − 1 steps: linear in b. Squaring (a^(b/2) · a^(b/2)) gets it down to Θ(lg b).", + }, + + /* ---------------- Divide and Conquer ---------------- */ + { + id: `${L}/divide-and-conquer/powering`, + lesson: "divide-and-conquer", + section: "powering-a-number", + prompt: "Computing aⁿ by squaring a^(n/2) (and multiplying by a once more when n is odd) takes…", + options: [ + { id: "a", text: "Θ(n)" }, + { id: "b", text: "Θ(lg n)" }, + { id: "c", text: "Θ(n lg n)" }, + { id: "d", text: "Θ(√n)" }, + ], + correctOptionId: "b", + explanation: "T(n) = T(n/2) + Θ(1): one recursive call on half the exponent, plus at most two multiplications.", + }, + { + id: `${L}/divide-and-conquer/strassen`, + lesson: "divide-and-conquer", + section: "strassens-algorithm", + prompt: "Strassen's algorithm multiplies two n × n matrices using how many recursive half-size multiplications, and what running time?", + options: [ + { id: "a", text: "8 multiplications, Θ(n³)" }, + { id: "b", text: "4 multiplications, Θ(n²)" }, + { id: "c", text: "7 multiplications, Θ(n^lg 7) ≈ Θ(n^2.81)" }, + { id: "d", text: "7 multiplications, Θ(n² lg n)" }, + ], + correctOptionId: "c", + explanation: "T(n) = 7T(n/2) + Θ(n²). n^(log₂ 7) ≈ n^2.81 beats n² (Case 1). The plain divide-and-conquer version uses 8 and stays Θ(n³).", + }, + { + id: `${L}/divide-and-conquer/binary-search`, + lesson: "divide-and-conquer", + section: "binary-search", + prompt: "In divide-and-conquer terms, how many subproblems does binary search solve at each step?", + options: [ + { id: "a", text: "One, of half the size" }, + { id: "b", text: "Two, each of half the size" }, + { id: "c", text: "n, each of size 1" }, + { id: "d", text: "None; it isn't divide and conquer" }, + ], + correctOptionId: "a", + explanation: "Divide: compare with the middle. Conquer: recurse into one half. Combine: nothing to do. That's T(n) = T(n/2) + Θ(1).", + }, + { + id: `${L}/lab-divide-and-conquer/gauss`, + lesson: "lab-divide-and-conquer", + section: "example-multiplying-large-numbers-the-gauss-method", + prompt: "Gauss's trick multiplies two n-digit numbers with 3 half-size multiplications instead of 4. What does that do to the running time?", + options: [ + { id: "a", text: "Nothing; it's still Θ(n²)" }, + { id: "b", text: "Θ(n lg n)" }, + { id: "c", text: "Θ(n)" }, + { id: "d", text: "Θ(n^lg 3) ≈ Θ(n^1.585)" }, + ], + correctOptionId: "d", + explanation: "T(n) = 3T(n/2) + Θ(n), master Case 1: Θ(n^(log₂ 3)). With 4 multiplications, T(n) = 4T(n/2) + Θ(n) = Θ(n²).", + }, + { + id: `${L}/lab-divide-and-conquer/uneven-tree`, + lesson: "lab-divide-and-conquer", + section: "problem-2-tn--tn3--t2n3--cn", + prompt: "In the recursion tree for T(n) = T(n/3) + T(2n/3) + cn, how much work does each full level do, and how deep is the tree?", + options: [ + { id: "a", text: "cn per level, depth log₃ n, so Θ(n)" }, + { id: "b", text: "About cn per level, depth log_(3/2) n, so Θ(n lg n)" }, + { id: "c", text: "Doubling per level, so Θ(n²)" }, + { id: "d", text: "Halving per level, so Θ(n)" }, + ], + correctOptionId: "b", + explanation: "The pieces on each level add up to n, so each full level costs cn. The longest path follows the 2/3 branch, giving depth log_(3/2) n = Θ(lg n).", + }, + { + id: `${L}/quicksort/worst-case`, + lesson: "quicksort", + section: "worst-case", + prompt: "When does quicksort with a first-element pivot take Θ(n²) time?", + options: [ + { id: "a", text: "On random input" }, + { id: "b", text: "Never; quicksort is always Θ(n lg n)" }, + { id: "c", text: "When the input is already sorted or reverse sorted, so one side of every partition is empty" }, + { id: "d", text: "When all keys are distinct" }, + ], + correctOptionId: "c", + explanation: "Then T(n) = T(0) + T(n − 1) + Θ(n), which sums to Θ(n²).", + }, + { + id: `${L}/quicksort/randomized`, + lesson: "quicksort", + section: "randomized-quicksort", + prompt: "What does picking the pivot at random achieve?", + options: [ + { id: "a", text: "Expected Θ(n lg n) on every input: no particular input is bad, only unlucky random choices" }, + { id: "b", text: "Worst case Θ(n lg n) on every run" }, + { id: "c", text: "It makes quicksort stable" }, + { id: "d", text: "It removes the need to partition" }, + ], + correctOptionId: "a", + explanation: "The running time now depends on the random choices, not the input order, so its expectation is Θ(n lg n) for any input. A Θ(n²) run is possible but very unlikely.", + }, + { + id: `${L}/quicksort/partition-time`, + lesson: "quicksort", + section: "partitioning", + prompt: "How long does PARTITION take on a subarray of n elements?", + options: [ + { id: "a", text: "Θ(lg n)" }, + { id: "b", text: "Θ(n)" }, + { id: "c", text: "Θ(n lg n)" }, + { id: "d", text: "Θ(1)" }, + ], + correctOptionId: "b", + explanation: "It scans the subarray once, comparing each element with the pivot.", + }, + { + id: `${L}/quicksort/almost-best`, + lesson: "quicksort", + section: "the-almost-best-case", + prompt: "If every partition splits 1/10 to 9/10, what is quicksort's running time?", + options: [ + { id: "a", text: "Θ(n²)" }, + { id: "b", text: "Θ(n^1.1)" }, + { id: "c", text: "Θ(n lg n), the same as an even split, just with a bigger constant" }, + { id: "d", text: "Θ(n)" }, + ], + correctOptionId: "c", + explanation: "Each level still costs at most cn, and the depth is log_(10/9) n = Θ(lg n). Any constant-proportion split gives Θ(n lg n).", + }, + { + id: `${L}/sorting-lower-bounds/lower-bound`, + lesson: "sorting-lower-bounds", + section: "the-lower-bound", + prompt: "Why must any comparison sort take Ω(n lg n) comparisons in the worst case?", + options: [ + { id: "a", text: "Because merge sort does" }, + { id: "b", text: "Because comparisons are slow" }, + { id: "c", text: "Because n elements need n² pairs compared" }, + { id: "d", text: "Its decision tree needs at least n! leaves, one per ordering, so its height is at least lg(n!) = Ω(n lg n)" }, + ], + correctOptionId: "d", + explanation: "A binary tree of height h has at most 2ʰ leaves. 2ʰ ≥ n! gives h ≥ lg(n!), which is Θ(n lg n) by Stirling's approximation.", + }, + { + id: `${L}/sorting-lower-bounds/counting-sort`, + lesson: "sorting-lower-bounds", + section: "counting-sort", + prompt: "Counting sort on n integers in the range 1…k takes Θ(n + k). How can it beat Ω(n lg n)?", + options: [ + { id: "a", text: "It doesn't compare elements, so the comparison-sort lower bound doesn't apply" }, + { id: "b", text: "It only works when n is small" }, + { id: "c", text: "The lower bound is wrong" }, + { id: "d", text: "It uses a faster computer model" }, + ], + correctOptionId: "a", + explanation: "Counting sort uses the keys as array indexes. The Ω(n lg n) bound is only for algorithms that learn the order by comparing pairs.", + }, + { + id: `${L}/sorting-lower-bounds/stable`, + lesson: "sorting-lower-bounds", + section: "stable-sorting", + prompt: "Why does radix sort need its per-digit sort to be stable?", + options: [ + { id: "a", text: "To save memory" }, + { id: "b", text: "So the order from earlier, less significant digits survives when later digits tie" }, + { id: "c", text: "To make it run in place" }, + { id: "d", text: "It doesn't; any sort works" }, + ], + correctOptionId: "b", + explanation: "Sorting least significant digit first, ties on the current digit must keep the order the earlier passes set up, or that work is lost.", + }, + { + id: `${L}/sorting-lower-bounds/radix-time`, + lesson: "sorting-lower-bounds", + section: "analysis-of-radix-sort", + prompt: "Radix sort on n numbers of d digits, each digit in 0…k−1, using counting sort per digit, takes…", + options: [ + { id: "a", text: "Θ(n lg n)" }, + { id: "b", text: "Θ(n + k)" }, + { id: "c", text: "Θ(d(n + k))" }, + { id: "d", text: "Θ(dⁿ)" }, + ], + correctOptionId: "c", + explanation: "d passes of counting sort, each Θ(n + k).", + }, + { + id: `${L}/order-statistics/randomized-select`, + lesson: "order-statistics", + section: "summary-of-randomized-selection", + prompt: "Randomized SELECT finds the i-th smallest element. What are its expected and worst-case running times?", + options: [ + { id: "a", text: "Expected Θ(n lg n), worst Θ(n²)" }, + { id: "b", text: "Expected Θ(n), worst Θ(n)" }, + { id: "c", text: "Expected Θ(lg n), worst Θ(n)" }, + { id: "d", text: "Expected Θ(n), worst Θ(n²)" }, + ], + correctOptionId: "d", + explanation: "Unlike quicksort, it recurses into only one side, so the expected work is linear. Consistently unlucky pivots still give Θ(n²).", + }, + { + id: `${L}/order-statistics/median-of-medians`, + lesson: "order-statistics", + section: "developing-the-recurrence", + prompt: "The worst-case linear SELECT gives T(n) ≤ T(n/5) + T(7n/10) + Θ(n). Why is that linear?", + options: [ + { id: "a", text: "Because n/5 + 7n/10 = 9n/10 < n, so the work shrinks geometrically down the levels" }, + { id: "b", text: "Because both recursive calls are on constant-size inputs" }, + { id: "c", text: "Because the master method says so directly" }, + { id: "d", text: "It isn't; it's Θ(n lg n)" }, + ], + correctOptionId: "a", + explanation: "Each level costs at most 9/10 of the one above, a decreasing geometric series dominated by the root: Θ(n). The master method doesn't apply because the two subproblems have different sizes.", + }, + { + id: `${L}/order-statistics/groups-of-five`, + lesson: "order-statistics", + section: "choosing-the-pivot", + prompt: "In the worst-case linear SELECT, the pivot is…", + options: [ + { id: "a", text: "A random element" }, + { id: "b", text: "The first element" }, + { id: "c", text: "The median of the medians of groups of 5" }, + { id: "d", text: "The mean of all elements" }, + ], + correctOptionId: "c", + explanation: "Taking the median of the group medians guarantees a good split: about 3n/10 elements are on each side of the pivot at least.", + }, + + /* ---------------- Hashing and Search Trees ---------------- */ + { + id: `${L}/hashing/division`, + lesson: "hashing", + section: "the-division-method", + prompt: "With the division method and a table of size 13, which slot does key 100 go to?", + options: [ + { id: "a", text: "7" }, + { id: "b", text: "9" }, + { id: "c", text: "13" }, + { id: "d", text: "0" }, + ], + correctOptionId: "b", + explanation: "h(k) = k mod 13 = 100 − 7 × 13 = 9.", + }, + { + id: `${L}/hashing/why-prime`, + lesson: "hashing", + section: "why-prime-collisions", + prompt: "Why is a power of 2 a bad table size for the division method?", + options: [ + { id: "a", text: "Powers of 2 are too large" }, + { id: "b", text: "k mod 2ᵖ is just the lowest p bits of k, so keys that differ only in their high bits collide" }, + { id: "c", text: "Division by a power of 2 is slow" }, + { id: "d", text: "It isn't; powers of 2 are ideal" }, + ], + correctOptionId: "b", + explanation: "The hash ignores most of the key. A prime not close to a power of 2 makes every bit of the key matter.", + }, + { + id: `${L}/hashing/amortized-doubling`, + lesson: "hashing", + section: "the-aggregate-method", + prompt: "A table doubles when full, copying everything. What is the amortized cost of each of n insertions?", + options: [ + { id: "a", text: "O(1): the total over n insertions is at most 3n" }, + { id: "b", text: "O(n), because a single insertion can cost n" }, + { id: "c", text: "O(lg n)" }, + { id: "d", text: "O(n²) in total" }, + ], + correctOptionId: "a", + explanation: "The copies cost 1 + 2 + 4 + … < 2n in total, plus n for the insertions: at most 3n, so O(1) each on average, even though one insertion can be expensive.", + }, + { + id: `${L}/hashing/accounting`, + lesson: "hashing", + section: "the-accounting-method", + prompt: "In the accounting method, what must always be true of the bank balance?", + options: [ + { id: "a", text: "It must be exactly zero after every operation" }, + { id: "b", text: "It must grow with every operation" }, + { id: "c", text: "It must never drop below zero, so the amortized costs bound the real costs" }, + { id: "d", text: "It must equal the number of elements" }, + ], + correctOptionId: "c", + explanation: "Cheap operations overcharge and save credit; expensive ones spend it. As long as the balance never goes negative, the total amortized cost is an upper bound on the true total.", + }, + { + id: `${L}/collision-resolution/chaining`, + lesson: "collision-resolution", + section: "analysis-the-load-factor", + prompt: "A chained hash table holds 200 keys in 50 slots, with good hashing. What is the expected cost of a search?", + options: [ + { id: "a", text: "About 1 + α = 5 steps" }, + { id: "b", text: "About 200 steps" }, + { id: "c", text: "About 50 steps" }, + { id: "d", text: "Exactly 1 step" }, + ], + correctOptionId: "a", + explanation: "The load factor α = n/m = 200/50 = 4. One step to hash, then about α steps along the chain.", + }, + { + id: `${L}/collision-resolution/linear-probing`, + lesson: "collision-resolution", + section: "linear-probing", + prompt: "Linear probing, m = 11, h′(k) = k mod 11, empty table. Insert 22, 33, 44 in that order. Where does 44 go?", + options: [ + { id: "a", text: "Slot 0" }, + { id: "b", text: "Slot 1" }, + { id: "c", text: "Slot 4" }, + { id: "d", text: "Slot 2" }, + ], + correctOptionId: "d", + explanation: "All three hash to 0. 22 takes 0, 33 probes 1, and 44 tries 0 and 1, then takes 2. Runs of filled slots like this are primary clustering.", + }, + { + id: `${L}/collision-resolution/quadratic`, + lesson: "collision-resolution", + section: "quadratic-probing", + prompt: "Same keys with quadratic probing, h(k, i) = (k mod 11 + i²) mod 11: insert 22, 33, 44. Where does 44 go?", + options: [ + { id: "a", text: "Slot 2" }, + { id: "b", text: "Slot 4" }, + { id: "c", text: "Slot 3" }, + { id: "d", text: "Slot 9" }, + ], + correctOptionId: "b", + explanation: "44 tries i = 0 (slot 0, taken), i = 1 (slot 1, taken by 33), i = 2 (slot 0 + 4 = 4, free). The probes jump further apart instead of walking along a cluster.", + }, + { + id: `${L}/collision-resolution/double`, + lesson: "collision-resolution", + section: "double-hashing", + prompt: "Double hashing with m = 13, h₁(k) = k mod 13, h₂(k) = 1 + (k mod 11). Key 27 finds slot h₁ taken. Which slot does it probe next?", + options: [ + { id: "a", text: "2" }, + { id: "b", text: "6" }, + { id: "c", text: "7" }, + { id: "d", text: "1" }, + ], + correctOptionId: "c", + explanation: "h₁(27) = 1, h₂(27) = 1 + 5 = 6, so the next probe is (1 + 1 × 6) mod 13 = 7. Each key gets its own step size, which avoids clustering.", + }, + { + id: `${L}/collision-resolution/open-addressing-probes`, + lesson: "collision-resolution", + section: "analysis-of-open-addressing", + prompt: "With open addressing and uniform hashing, how many probes does an unsuccessful search expect when the table is 75% full?", + options: [ + { id: "a", text: "0.75" }, + { id: "b", text: "1.75" }, + { id: "c", text: "4" }, + { id: "d", text: "75" }, + ], + correctOptionId: "c", + explanation: "Expected probes ≤ 1/(1 − α) = 1/(1 − 0.75) = 4. At 90% full it's 10, which is why open-addressed tables are resized well before they fill up.", + }, + { + id: `${L}/lab-hashing/direct-address`, + lesson: "lab-hashing", + section: "direct-address-tables", + prompt: "Why not use a direct-address table (one slot per possible key) for student ID numbers up to 10 digits?", + options: [ + { id: "a", text: "Lookups would be slow" }, + { id: "b", text: "It would need about 10¹⁰ slots, almost all empty, for a few thousand students" }, + { id: "c", text: "Collisions would be frequent" }, + { id: "d", text: "IDs can't be used as indexes" }, + ], + correctOptionId: "b", + explanation: "Direct addressing is O(1) with no collisions, but its size is the size of the key universe. A hash table sized to the number of keys fixes that, at the price of collisions.", + }, + { + id: `${L}/lab-hashing/good-hash`, + lesson: "lab-hashing", + section: "good-hash-functions", + prompt: "What makes a hash function good?", + options: [ + { id: "a", text: "It spreads keys evenly over the slots and is fast to compute" }, + { id: "b", text: "It always gives different values for different keys" }, + { id: "c", text: "It maps every key to slot 0" }, + { id: "d", text: "It's slow, so it's hard to reverse" }, + ], + correctOptionId: "a", + explanation: "Collisions are unavoidable when there are more possible keys than slots (the pigeonhole principle), so the goal is to spread keys evenly and cheaply.", + }, + { + id: `${L}/balanced-search-trees/properties`, + lesson: "balanced-search-trees", + section: "red-black-trees", + prompt: "Which is NOT a red-black tree property?", + options: [ + { id: "a", text: "Every node is red or black" }, + { id: "b", text: "A red node never has a red child" }, + { id: "c", text: "Every path from a node down to a leaf has the same number of black nodes" }, + { id: "d", text: "Every path from the root to a leaf has the same number of nodes" }, + ], + correctOptionId: "d", + explanation: "Paths can differ in length because of red nodes: the longest can be up to twice the shortest. Only the black count must match.", + }, + { + id: `${L}/balanced-search-trees/height`, + lesson: "balanced-search-trees", + section: "the-height-of-a-red-black-tree", + prompt: "A red-black tree holds 1,023 keys. What's the most its height can be, by the bound h ≤ 2 lg(n + 1)?", + options: [ + { id: "a", text: "10" }, + { id: "b", text: "1,023" }, + { id: "c", text: "20" }, + { id: "d", text: "512" }, + ], + correctOptionId: "c", + explanation: "2 lg(1,024) = 2 × 10 = 20. A perfectly balanced tree would have height about 10; red-black trees allow up to twice that.", + }, + { + id: `${L}/balanced-search-trees/rotation`, + lesson: "balanced-search-trees", + section: "rotations", + prompt: "What does a rotation preserve?", + options: [ + { id: "a", text: "The colors of every node" }, + { id: "b", text: "The binary-search-tree order of the keys" }, + { id: "c", text: "The height of the tree" }, + { id: "d", text: "Nothing; it rebuilds the tree" }, + ], + correctOptionId: "b", + explanation: "A rotation changes a few pointers in O(1) time and keeps the in-order sequence of keys, which is what makes it safe for rebalancing.", + }, + { + id: `${L}/red-black-insert-delete/new-node-red`, + lesson: "red-black-insert-delete", + section: "insertion-the-idea", + prompt: "Why is a newly inserted node colored red?", + options: [ + { id: "a", text: "Red nodes are faster to search" }, + { id: "b", text: "Red is the default color in code" }, + { id: "c", text: "A red node can't break any property" }, + { id: "d", text: "A red node keeps every black count the same, so only the \"no red parent with a red child\" rule can break" }, + ], + correctOptionId: "d", + explanation: "A black node would add one to the black count on some paths, a violation that's hard to fix. A red node only risks a red-red pair, which recoloring and rotations can push up and fix.", + }, + { + id: `${L}/red-black-insert-delete/insert-cost`, + lesson: "red-black-insert-delete", + section: "analysis-of-insertion", + prompt: "How long does red-black insertion take, and how many rotations does it need?", + options: [ + { id: "a", text: "O(lg n) time, with O(1) rotations" }, + { id: "b", text: "O(n) time, with O(n) rotations" }, + { id: "c", text: "O(lg n) time, with O(lg n) rotations" }, + { id: "d", text: "O(1) time, with no rotations" }, + ], + correctOptionId: "a", + explanation: "Recoloring may climb the tree in O(lg n) steps, but the rotations happen only at the end, at most two of them.", + }, + { + id: `${L}/augmenting-data-structures/rank`, + lesson: "augmenting-data-structures", + section: "dynamic-order-statistics", + prompt: "In an order-statistic tree, node x has size[left[x]] = 6. What is x's rank within its own subtree?", + options: [ + { id: "a", text: "6" }, + { id: "b", text: "7" }, + { id: "c", text: "size[x]" }, + { id: "d", text: "5" }, + ], + correctOptionId: "b", + explanation: "Everything in the left subtree is smaller, so x is number size[left[x]] + 1 = 7 in its subtree.", + }, + { + id: `${L}/augmenting-data-structures/why-size`, + lesson: "augmenting-data-structures", + section: "data-structure-maintenance", + prompt: "Why store subtree sizes rather than each node's rank?", + options: [ + { id: "a", text: "Sizes take less memory" }, + { id: "b", text: "Ranks can't be computed at all" }, + { id: "c", text: "Inserting one key could change the rank of every node, but only the sizes along one path" }, + { id: "d", text: "Sizes are needed for red-black colors" }, + ], + correctOptionId: "c", + explanation: "A new minimum shifts every rank by one: O(n) updates. Sizes change only on the path from the root to the new node, O(lg n), and rotations fix theirs in O(1).", + }, + { + id: `${L}/augmenting-data-structures/interval`, + lesson: "augmenting-data-structures", + section: "following-the-methodology", + prompt: "In an interval tree, each node stores m[x]. What is it?", + options: [ + { id: "a", text: "The number of intervals in its subtree" }, + { id: "b", text: "The largest high endpoint of any interval in its subtree" }, + { id: "c", text: "The smallest low endpoint in its subtree" }, + { id: "d", text: "The length of its interval" }, + ], + correctOptionId: "b", + explanation: "m[x] = max(high[int[x]], m[left[x]], m[right[x]]). It lets a search skip a whole subtree whose intervals all end before the query starts.", + }, + { + id: `${L}/skip-lists/search-time`, + lesson: "skip-lists", + section: "search-time", + prompt: "What search time does a randomized skip list give?", + options: [ + { id: "a", text: "O(n) on average" }, + { id: "b", text: "O(1)" }, + { id: "c", text: "O(√n) always" }, + { id: "d", text: "O(lg n) with high probability" }, + ], + correctOptionId: "d", + explanation: "About lg n levels, with a constant expected number of steps on each. The same expected performance as a balanced tree, with much simpler code.", + }, + { + id: `${L}/skip-lists/promote`, + lesson: "skip-lists", + section: "randomization", + prompt: "On insertion, how does a skip list decide how many levels a new node goes up?", + options: [ + { id: "a", text: "By the key's value" }, + { id: "b", text: "By flipping a fair coin: keep promoting it a level while the coin comes up heads" }, + { id: "c", text: "By rebalancing like a red-black tree" }, + { id: "d", text: "Every node goes to every level" }, + ], + correctOptionId: "b", + explanation: "Each level holds about half the nodes of the one below, so there are about lg n levels, and no key order can make it degenerate.", + }, + { + id: `${L}/skip-lists/no-bad-sequences`, + lesson: "skip-lists", + section: "there-are-no-bad-sequences", + prompt: "Inserting keys in sorted order makes a plain BST degenerate. Why doesn't it hurt a skip list?", + options: [ + { id: "a", text: "Skip lists sort the keys first" }, + { id: "b", text: "It does; sorted input is the worst case" }, + { id: "c", text: "Skip lists reject sorted input" }, + { id: "d", text: "The levels come from coin flips, independent of the keys, so no input order is bad" }, + ], + correctOptionId: "d", + explanation: "Only very unlucky coin flips make a bad skip list, and that's extremely unlikely. The input can't cause it.", + }, + + /* ---------------- Design Strategies ---------------- */ + { + id: `${L}/dynamic-programming/coin-row`, + lesson: "dynamic-programming", + section: "the-coin-row-problem", + prompt: "Coin row 3, 8, 4, 1, 9, 2: pick coins, no two adjacent, for the largest total. What is it?", + options: [ + { id: "a", text: "16" }, + { id: "b", text: "17" }, + { id: "c", text: "18" }, + { id: "d", text: "27" }, + ], + correctOptionId: "b", + explanation: "F = 3, 8, 8, 9, 17, 17 using F(n) = max(cₙ + F(n − 2), F(n − 1)). The best is 8 + 9. 3 + 4 + 9 = 16 is the tempting alternative.", + }, + { + id: `${L}/dynamic-programming/lcs`, + lesson: "dynamic-programming", + section: "longest-common-subsequence-lcs", + prompt: "What is the length of a longest common subsequence of AGGTAB and GXTXAYB?", + options: [ + { id: "a", text: "3" }, + { id: "b", text: "5" }, + { id: "c", text: "2" }, + { id: "d", text: "4" }, + ], + correctOptionId: "d", + explanation: "GTAB appears in both, in order (not necessarily side by side). No common subsequence of length 5 exists.", + }, + { + id: `${L}/dynamic-programming/knapsack`, + lesson: "dynamic-programming", + section: "the-0-1-knapsack-problem", + prompt: "Capacity 6. Items (weight, value): (3, 25), (2, 20), (1, 15), (4, 40), (5, 50). What is the best total value?", + options: [ + { id: "a", text: "60" }, + { id: "b", text: "65" }, + { id: "c", text: "75" }, + { id: "d", text: "55" }, + ], + correctOptionId: "b", + explanation: "Weight 1 + weight 5 gives 15 + 50 = 65. 2 + 4 gives 60, and 3 + 2 + 1 also gives 60. 75 would need weight 7.", + }, + { + id: `${L}/dynamic-programming/overlapping`, + lesson: "dynamic-programming", + section: "example-fibonacci-numbers", + prompt: "Dynamic programming pays off when a problem has…", + options: [ + { id: "a", text: "Overlapping subproblems, solved once and stored, and optimal substructure" }, + { id: "b", text: "Independent subproblems that never repeat" }, + { id: "c", text: "No subproblems at all" }, + { id: "d", text: "A greedy choice that's always safe" }, + ], + correctOptionId: "a", + explanation: "If subproblems don't repeat, plain divide and conquer is enough; if a greedy choice is always safe, greedy is simpler.", + }, + { + id: `${L}/greedy-algorithms/coin-failure`, + lesson: "greedy-algorithms", + section: "a-failure-of-the-greedy-algorithm", + prompt: "Coins are 1, 6 and 9. Greedy change for 12 gives 9 + 1 + 1 + 1. What's the optimal?", + options: [ + { id: "a", text: "Greedy is optimal: 4 coins" }, + { id: "b", text: "9 + 1 + 1 + 1, but in a different order" }, + { id: "c", text: "6 + 6: 2 coins" }, + { id: "d", text: "12 ones" }, + ], + correctOptionId: "c", + explanation: "Grabbing the 9 looked best but leaves 3, which only 1s can make. Greedy is optimal only for coin systems designed for it.", + }, + { + id: `${L}/greedy-algorithms/definition`, + lesson: "greedy-algorithms", + section: "optimization-problems", + prompt: "What does a greedy algorithm do at each phase?", + options: [ + { id: "a", text: "Tries every possible choice and keeps the best" }, + { id: "b", text: "Takes the best choice available right now, without thinking about the future, and never undoes it" }, + { id: "c", text: "Splits the problem into equal halves" }, + { id: "d", text: "Fills a table of subproblem answers" }, + ], + correctOptionId: "b", + explanation: "Local, immediate and irrevocable. It's fast, but it's only optimal when you can prove the local choices add up to a global optimum.", + }, + { + id: `${L}/greedy-algorithms/tsp`, + lesson: "greedy-algorithms", + section: "traveling-salesman", + prompt: "\"Always go to the nearest unvisited city\" for the traveling salesman problem is…", + options: [ + { id: "a", text: "Optimal for every map" }, + { id: "b", text: "Never better than a random tour" }, + { id: "c", text: "Exponential time" }, + { id: "d", text: "Fast and often reasonable, but not guaranteed to find the shortest tour" }, + ], + correctOptionId: "d", + explanation: "Nearest-neighbor is a greedy heuristic. TSP is NP-hard, so no known fast algorithm guarantees the optimum.", + }, + { + id: `${L}/minimum-spanning-trees/cut`, + lesson: "minimum-spanning-trees", + section: "the-same-idea-in-terms-of-cuts-and-safe-edges", + prompt: "Split the vertices into two groups. Which edge across the split is guaranteed to be safe to add to some MST?", + options: [ + { id: "a", text: "Any edge crossing the cut" }, + { id: "b", text: "The heaviest edge crossing the cut" }, + { id: "c", text: "A lightest edge crossing the cut" }, + { id: "d", text: "An edge inside one of the groups" }, + ], + correctOptionId: "c", + explanation: "This cut property justifies both Prim (cut = tree vs. the rest) and Kruskal (cut = the edge's tree vs. the rest).", + }, + { + id: `${L}/minimum-spanning-trees/kruskal`, + lesson: "minimum-spanning-trees", + section: "analysis-of-kruskal", + prompt: "What does Kruskal's algorithm use to tell whether an edge would close a cycle, and what's its running time?", + options: [ + { id: "a", text: "A priority queue; O(V²)" }, + { id: "b", text: "Depth-first search for every edge; O(E · V)" }, + { id: "c", text: "An adjacency matrix; O(V³)" }, + { id: "d", text: "A disjoint-set (union-find) structure; O(E lg V)" }, + ], + correctOptionId: "d", + explanation: "Sorting the edges dominates. Union-find answers \"same tree?\" and merges trees in nearly constant time.", + }, + { + id: `${L}/minimum-spanning-trees/prim-heap`, + lesson: "minimum-spanning-trees", + section: "analysis-of-prim", + prompt: "Prim's algorithm with a binary heap runs in…", + options: [ + { id: "a", text: "O(E lg V)" }, + { id: "b", text: "O(V)" }, + { id: "c", text: "O(E²)" }, + { id: "d", text: "O(V + E)" }, + ], + correctOptionId: "a", + explanation: "V extract-mins and up to E decrease-keys, each O(lg V) with a binary heap.", + }, + { + id: `${L}/huffman-and-activity-selection/huffman-cost`, + lesson: "huffman-and-activity-selection", + section: "creating-a-huffman-code", + prompt: "Characters with frequencies 1, 1, 2 and 4. How many bits does a Huffman code need for all 8 characters of the text?", + options: [ + { id: "a", text: "16" }, + { id: "b", text: "12" }, + { id: "c", text: "14" }, + { id: "d", text: "8" }, + ], + correctOptionId: "c", + explanation: "Merge 1 + 1 = 2, then 2 + 2 = 4, then 4 + 4 = 8. The total cost is the sum of the merges: 2 + 4 + 8 = 14. A fixed 2-bit code would need 16.", + }, + { + id: `${L}/huffman-and-activity-selection/prefix`, + lesson: "huffman-and-activity-selection", + section: "prefix-codes", + prompt: "Why must a variable-length code be a prefix code?", + options: [ + { id: "a", text: "So it uses fewer bits" }, + { id: "b", text: "So no codeword is the start of another, and a bit string can be decoded without separators" }, + { id: "c", text: "So every codeword has the same length" }, + { id: "d", text: "So the code can be encrypted" }, + ], + correctOptionId: "b", + explanation: "With a = 0 and b = 01, \"01\" could be b, or a followed by something else. A prefix code removes that ambiguity.", + }, + { + id: `${L}/huffman-and-activity-selection/activity`, + lesson: "huffman-and-activity-selection", + section: "the-greedy-choice", + prompt: "Activities (start, finish): (1,3), (2,5), (4,6), (6,8), (5,9), (8,10). How many can greedy-by-earliest-finish schedule?", + options: [ + { id: "a", text: "3" }, + { id: "b", text: "5" }, + { id: "c", text: "4" }, + { id: "d", text: "6" }, + ], + correctOptionId: "c", + explanation: "It picks (1,3), then (4,6), (6,8) and (8,10): 4. Choosing the activity that finishes earliest leaves the most room for the rest.", + }, + { + id: `${L}/huffman-and-activity-selection/greedy-vs-dp`, + lesson: "huffman-and-activity-selection", + section: "greedy-algorithms-vs-dynamic-programming", + prompt: "What does a greedy algorithm need that dynamic programming doesn't?", + options: [ + { id: "a", text: "The greedy-choice property: a locally best choice that some optimal solution also makes" }, + { id: "b", text: "Optimal substructure" }, + { id: "c", text: "A table of subproblems" }, + { id: "d", text: "Overlapping subproblems" }, + ], + correctOptionId: "a", + explanation: "Both rely on optimal substructure. DP considers all choices; greedy commits to one, which is only correct if that choice is provably safe.", + }, + { + id: `${L}/shortest-paths/dijkstra-run`, + lesson: "shortest-paths", + section: "dijkstras-algorithm", + prompt: "Edges: s→a (4), s→b (1), b→a (2), a→c (1), b→c (5). What does Dijkstra give as the distance from s to c?", + options: [ + { id: "a", text: "5" }, + { id: "b", text: "6" }, + { id: "c", text: "4" }, + { id: "d", text: "3" }, + ], + correctOptionId: "c", + explanation: "s→b costs 1, b→a makes a cost 3 (better than 4), then a→c makes c cost 4 (better than 1 + 5 = 6).", + }, + { + id: `${L}/shortest-paths/negative`, + lesson: "shortest-paths", + section: "well-definedness-of-shortest-paths", + prompt: "Why can't a graph with a negative-weight cycle have well-defined shortest paths through it?", + options: [ + { id: "a", text: "Dijkstra crashes on negative numbers" }, + { id: "b", text: "Negative weights aren't allowed in graphs" }, + { id: "c", text: "The cycle disconnects the graph" }, + { id: "d", text: "Going round the cycle again always makes the path cheaper, so there's no minimum" }, + ], + correctOptionId: "d", + explanation: "Every extra lap lowers the weight without end. With all weights non-negative, shortest-path weights always exist.", + }, + { + id: `${L}/shortest-paths/heap-time`, + lesson: "shortest-paths", + section: "analysis-of-dijkstra", + prompt: "Dijkstra's algorithm with a binary heap runs in…", + options: [ + { id: "a", text: "O(V²)" }, + { id: "b", text: "O(E lg V)" }, + { id: "c", text: "O(V + E)" }, + { id: "d", text: "O(E · V)" }, + ], + correctOptionId: "b", + explanation: "Θ(V) extract-mins and Θ(E) decrease-keys, each O(lg V). With a plain array it's O(V²), which is better for very dense graphs.", + }, + { + id: `${L}/shortest-paths/unweighted`, + lesson: "shortest-paths", + section: "unweighted-graphs", + prompt: "If every edge has weight 1, what can replace Dijkstra's priority queue?", + options: [ + { id: "a", text: "A plain FIFO queue, which turns it into breadth-first search, O(V + E)" }, + { id: "b", text: "A stack, which turns it into DFS" }, + { id: "c", text: "Nothing; the heap is still needed" }, + { id: "d", text: "A sorted array" }, + ], + correctOptionId: "a", + explanation: "Vertices come off a FIFO queue in order of distance anyway when every edge costs the same, so no heap is needed.", + }, + + /* ---------------- Hard Problems ---------------- */ + { + id: `${L}/p-and-np/np-definition`, + lesson: "p-and-np", + section: "the-class-np", + prompt: "What puts a decision problem in NP?", + options: [ + { id: "a", text: "It can't be solved in polynomial time" }, + { id: "b", text: "It takes exponential time to solve" }, + { id: "c", text: "A proposed solution (certificate) can be checked in polynomial time" }, + { id: "d", text: "It's about graphs" }, + ], + correctOptionId: "c", + explanation: "NP means verifiable in polynomial time. Every problem in P is also in NP, so being in NP says nothing about being hard.", + }, + { + id: `${L}/p-and-np/np-complete`, + lesson: "p-and-np", + section: "np-complete", + prompt: "A problem is NP-complete if…", + options: [ + { id: "a", text: "It's NP-hard" }, + { id: "b", text: "It's in NP and NP-hard" }, + { id: "c", text: "It's in P" }, + { id: "d", text: "No algorithm can solve it" }, + ], + correctOptionId: "b", + explanation: "NP-hard alone allows problems harder than NP, even undecidable ones. NP-complete problems are the hardest problems inside NP.", + }, + { + id: `${L}/p-and-np/reduction-direction`, + lesson: "p-and-np", + section: "reduction", + prompt: "To show a new problem B is NP-hard, which reduction do you build?", + options: [ + { id: "a", text: "From B to a known easy problem" }, + { id: "b", text: "From B to a known NP-complete problem A" }, + { id: "c", text: "From a known NP-complete problem A to B, in polynomial time" }, + { id: "d", text: "From any problem in P to B" }, + ], + correctOptionId: "c", + explanation: "If A ≤ₚ B, a fast algorithm for B would solve A fast too, so B is at least as hard as A. Reducing B to A only shows B is no harder than A.", + }, + { + id: `${L}/p-and-np/sat-first`, + lesson: "p-and-np", + section: "the-sat-problem", + prompt: "Which problem was the first to be proved NP-complete?", + options: [ + { id: "a", text: "Boolean satisfiability (SAT), by the Cook–Levin theorem" }, + { id: "b", text: "The traveling salesman problem" }, + { id: "c", text: "Sorting" }, + { id: "d", text: "The clique problem" }, + ], + correctOptionId: "a", + explanation: "Cook–Levin (1971) showed every NP problem reduces to SAT. Others, like CLIQUE, were then shown NP-complete by reducing SAT (or 3-CNF SAT) to them.", + }, + { + id: `${L}/p-and-np/p-vs-np`, + lesson: "p-and-np", + section: "p-vs-np", + prompt: "What would follow if someone found a polynomial-time algorithm for one NP-complete problem?", + options: [ + { id: "a", text: "Only that problem would become easy" }, + { id: "b", text: "Nothing; NP-complete problems are unrelated" }, + { id: "c", text: "Every problem would become solvable" }, + { id: "d", text: "Every problem in NP could be solved in polynomial time: P = NP" }, + ], + correctOptionId: "d", + explanation: "Every NP problem reduces to each NP-complete one in polynomial time, so a fast algorithm for one is a fast algorithm for all of NP.", + }, +]; diff --git a/src/test/exam-page.test.tsx b/src/test/exam-page.test.tsx index e29447b..629a869 100644 --- a/src/test/exam-page.test.tsx +++ b/src/test/exam-page.test.tsx @@ -91,7 +91,7 @@ describe("course exam page", () => { }); it("says so when a course has no questions yet", async () => { - renderExam("advanced-algorithms"); + renderExam("computer-literacy"); expect(await screen.findByText(/no exam questions yet/)).toBeTruthy(); }); });