Skip to the content

Module 1: What an algorithm is

Module 2: Counting steps: Big O

Module 3: Searching a list

Module 4: Binary search

Module 5: Simple sorts

Module 6: Recursion

Module 7: Merge sort

Module 8: Quicksort

Module 9: Two pointers and sliding windows

Module 10: Counting and hashing

Module 11: Walking a tree

Module 12: Searching a graph

Module 13: Shortest paths

Module 14: Greedy algorithms

Module 15: Dynamic programming

Module 16: Dynamic programming in two dimensions

Module 17: Backtracking

Module 18: Minimum spanning trees

Module 19: Solving problems you have never seen

Module 20: Mastery challenge

Answer key

  1. 1. What an algorithm is · You already run algorithms every day · QuizWhich instruction is precise enough for a computer to follow?B. Take the book with the most pages; if two tie, take the leftmost.“Good” and “roughly” need judgement, which a computer does not have. The second rule says exactly what to compare (pages) and even what to do when two books tie, so anyone following it gets the same book. Thinking about ties and other awkward cases is a large part of writing algorithms.
  2. 1. What an algorithm is · A good algorithm is correct and finishes · QuizWhy does “keep halving the number until it is 0” never stop, starting from 8 as a double (a number with a fractional part)?A. Halving 8 gives 4, 2, 1, 0.5, 0.25… which gets close to 0 but never reaches itEach half is smaller, but still above 0, for a very long time. (A double has limited precision, so after about a thousand halvings it does round to 0, but only by accident of how it is stored, which is no way to plan an ending.) A loop needs a condition that is sure to become true. “Below 1” works: 8, 4, 2, 1, 0.5, and it stops. When you write a loop, ask: what changes on every pass, and why does that make the end certain?
  3. 1. What an algorithm is · From steps in words to pseudocode · Put in orderPut the steps for finding the largest score in order.1. If the list is empty, there is no answer 2. Remember the first score as the best so far 3. Move on to the next score 4. If it is bigger than the best, remember it instead 5. Repeat until every score has been seen, then give back the bestThe empty check comes first, because there is no “first score” to remember otherwise. Then the loop: move to the next score, compare, maybe update, repeat. The answer is only ready once every score has been compared, so it is given at the very end.
  4. 1. What an algorithm is · From pseudocode to C++ · Fill the blanksTurn it around: find the smallest price.1: std::nullopt, 2: size(), 3: <Only the comparison changes: < keeps the lower price. The loop runs while i < prices.size(), so the last index it visits is size() - 1, the last element. With <= there, it would read one past the end: here the standard library is built with checks on, so the program stops with “vector[] index out of bounds” instead of quietly reading whatever memory comes next.
  5. 1. What an algorithm is · Trace it by hand on a small input · Find the bugThis works for {4, 9, 2} but gives the wrong answer for {-5, -2, -8}. Which line is wrong?Line 2: int best = scores[0];Starting at 0 assumes some score is above 0. With all negative scores, none beats 0, so it answers 0, a score that is not even in the list. Start from a real element (the first, once you know the list is not empty), or from INT_MIN, the smallest int there is, which every score ties or beats. Tracing on an unusual input, all negatives, is what finds this.
  6. 1. What an algorithm is · Try it: find the largest score · Code challengeFinish largest(scores): return the biggest number in the vector, or std::nullopt if it is empty.One solution:
    Solution
    #include <optional>#include <vector>
    std::optional<int> largest(const std::vector<int>& scores) {  if (scores.empty()) return std::nullopt;  int best = scores[0];  for (int s : scores) {    if (s > best) best = s;  }  return best;}
    Handle the empty list first, then start from a real score, so negatives work. The range-for for (int s : scores) visits every element, the first included; comparing it with itself is harmless, since it is never bigger than itself. The tests are chosen to break the common mistakes: starting at 0 fails the negatives, and forgetting the empty case reads scores[0] of an empty vector, which stops the program with “vector[] index out of bounds”.
  7. 1. What an algorithm is · The same idea works on structs · Code challengeFinish cheapest(books): return the book with the lowest price, or std::nullopt for an empty vector. If two books cost the same, return the one that comes first.One solution:
    Solution
    #include <optional>#include <string>#include <vector>
    struct Book {  std::string title;  int price;};
    std::optional<Book> cheapest(const std::vector<Book>& books) {  if (books.empty()) return std::nullopt;  Book best = books[0];  for (const Book& book : books) {    if (book.price < best.price) best = book;  }  return best;}
    < and not <= is what makes ties go to the first book: with <=, Kim, an equal price found later, would replace Dune. The loop variable is a const Book&, a reference to each book in the vector, so looking at a book does not copy its title; only a new best is copied into best. In the tests, ->title reads a field of the book inside the optional.
  8. 2. Counting steps: Big O · Count what runs for each element · QuizHow many times does the std::cout line run when n is 4?C. 16The inner loop runs all 4 of its passes for each of the outer loop’s 4 passes: 4 × 4 = 16. Loops one inside another multiply; loops one after another add, so two separate loops of 4 would be 8.
  9. 2. Counting steps: Big O · Big O keeps only what matters as n grows · QuizAn algorithm does 2n² + 50n + 1000 operations. What is its Big O?B. O(n²)Drop the slower terms, 50n and 1000, and the constant 2: O(n²). For small n the 1000 is the biggest part, but Big O describes what happens as n grows large, and there n² dominates everything.
  10. 2. Counting steps: Big O · The common growth rates · Put in orderOrder these from slowest-growing (best) to fastest-growing (worst).1. O(1) 2. O(log n) 3. O(n) 4. O(n log n) 5. O(n²) 6. O(2ⁿ)Constant never grows; log n barely does; n grows in step with the input; n log n a little faster; n² squares it; 2ⁿ doubles with every extra element. At a million elements, an O(n²) algorithm does a million times the work of an O(n) one.
  11. 2. Counting steps: Big O · Reading Big O from loops · Fill the blanksWrite the Big O of each piece, for a list of n elements (n² can be typed n^2).1: 1, 2: n, 3: n², 4: log nReading one element takes the same time however long the vector is: O(1), since its elements sit side by side in memory and the address of list[i] is one sum away. One pass is O(n). The nested loops do n passes of n, n². The last loop starts at n and halves k each time, so it runs about log₂ n times: 9 times for 1,000 elements (1000, 500, 250, 125, 62, 31, 15, 7, 3), and log₂ 1000 ≈ 10.
  12. 2. Counting steps: Big O · Try it: count the comparisons · Code challengeFix hasDuplicate(items) so it compares each pair of different positions once, using same(a, b) for every comparison. For 5 items with no duplicates that is exactly 10 calls.One solution:
    Solution
    #include <string>#include <vector>
    int comparisons = 0;bool same(const std::string& a, const std::string& b) {  comparisons++;  return a == b;}
    bool hasDuplicate(const std::vector<std::string>& items) {  for (size_t i = 0; i < items.size(); i++) {    for (size_t j = i + 1; j < items.size(); j++) {      if (same(items[i], items[j])) return true;    }  }  return false;}
    Starting j at 0 compares every item with itself, so the very first check, items[0] with items[0], says “duplicate”. Starting at i + 1 skips the item itself and every pair already done, halving the work. Half of n² is still O(n²), though: module 10 shows an O(n) way with a std::unordered_set.
  13. 2. Counting steps: Big O · Try it: count the comparisons · Code challengeFinish halvings(n): count how many times you replace n with n / 2 until it reaches 0. halvings(8) is 4 (8 → 4 → 2 → 1 → 0).One solution:
    Solution
    int halvings(int n) {  int steps = 0;  while (n > 0) {    n = n / 2;    steps++;  }  return steps;}
    This is log n made concrete: a million shrinks to 0 in 20 halvings. Any algorithm that throws away half of what is left at each step, like binary search in module 4, takes O(log n) steps. The type matters: n is an int, and dividing two ints throws away the fraction, so 1 / 2 is 0 and the loop ends. With a double, 1 / 2 is 0.5, the trap from module 1. (1'000'000 is a million: C++ lets you group digits with '.)
  14. 2. Counting steps: Big O · Memory counts too: space complexity · QuizBoth reverse a list in O(n) time. What is the difference?A. The left makes a new array of n elements; the right swaps inside the original with two variablesThe right one only ever adds i, j and t, whatever n is: O(1) extra space. It takes the vector by plain reference (std::vector<int>&, no const), so it changes the caller’s vector, called working in place. The left takes const&, cannot change the original, and pays for a second vector of n elements. (Both count down with care: size_t cannot go below 0, so i >= 0 would always be true.)
  15. 2. Counting steps: Big O · Best case, worst case · QuizhasDuplicate from before, on n items. Which input is its worst case?B. No two items are the sameWith a match it can stop early; the first two being equal is the best case, 1 comparison. With no duplicates it must check every pair, n(n − 1) / 2 comparisons, before it can say false. “Not found” is usually the worst case for a search.
  16. 3. Searching a list · Linear search in code · QuizA list holds 1,000 song titles, all different. How many comparisons does a linear search for a title that is not there make?D. 1,000To be sure something is missing you must look at everything: 1,000 comparisons. A title that is there takes 500 on average. Both are O(n): double the list, double the work. Module 4 shows how a sorted list gets this down to about 10.
  17. 3. Searching a list · The standard library’s searches are linear too · QuizWhat does this print?A. 1 3Not found is v.end(), so the comparison prints 1 (true). And end() is the position just after the last element, index 3, not -1: turning a failed search into an index gives the size. Always compare with end() before using the result; reading *it there is undefined behaviour, a mistake C++ promises nothing about.
  18. 3. Searching a list · Try it: write linear search · Code challengeFinish linearSearch(prices, target): return the index of the first price equal to target, or -1 if there is none. Don’t use std::find.One solution:
    Solution
    #include <vector>
    int linearSearch(const std::vector<int>& prices, int target) {  for (int i = 0; i < std::ssize(prices); i++) {    if (prices[i] == target) return i;  }  return -1;}
    The return inside the loop is what makes it the first match: the function ends there. The -1 after the loop is only reached when nothing matched, including the empty list, where the loop never runs. Returning 0 for “missing” would be a bug, because 0 is a real index.
  19. 3. Searching a list · Try it: write linear search · Code challengeFinish lastIndex(songs, title): return the index of the last song with that title, or -1. Walk from the end, so you can stop at the first match you meet.One solution:
    Solution
    #include <string>#include <vector>
    int lastIndex(const std::vector<std::string>& songs,              const std::string& title) {  for (int i = std::ssize(songs) - 1; i >= 0; i--) {    if (songs[i] == title) return i;  }  return -1;}
    Start at size - 1, the last index, and count down while i >= 0. The classic slips are starting at size, which reads one past the end, and i > 0, which never checks index 0. And the index must be signed: with size_t i, i >= 0 is always true, so after 0 comes i--, which wraps round to the biggest size_t there is, and songs[i] stops the program with “index out of bounds”.
  20. 3. Searching a list · Searching with a condition · Fill the blanksFind the first song longer than 300 seconds.1: >, 2: find_if, 3: 342find_if calls isLong on 215, then 187, then 342, which passes, and stops there: 401 is never looked at. *it reads the element at the position it found. std::ranges::count_if would have counted both long songs, but only after looking at every one.
  21. 3. Searching a list · Searching with a condition · Code challengeWrite findFirst(prices, test): return the first price for which test(price) is true, or std::nullopt. Call test as few times as possible: stop at the first match.One solution:
    Solution
    #include <optional>#include <vector>
    template <typename Test>std::optional<int> findFirst(const std::vector<int>& prices,                             Test test) {  for (int p : prices) {    if (test(p)) return p;  }  return std::nullopt;}
    template <typename Test> lets test be anything you can call, such as a lambda. The starter gives the right answers, but it tests all four prices before picking the first: the third test catches it with a lambda that counts its calls ([&calls] lets the lambda change calls, a variable outside it). Returning inside the loop stops after 12 and 8. On a list of a million where the match is near the front, that is two steps instead of a million.
  22. 3. Searching a list · Stop as soon as you know · Find the bugThis should give the first index of target, but firstIndex({"a", "b", "a"}, "a") gives 2. Which line is wrong?Line 5: if (list[i] == target) return i;The loop never stops, so each later match overwrites found, and the answer is the last match: index 2. Returning at the first match fixes both the answer and the wasted work. Tests with the target appearing twice are what catch this.
  23. 3. Searching a list · Edge cases: empty, missing, and index 0 · QuizWhat does this print?B. none freelinearSearch does find the 0, at index 0, but if (0) is false, so it says “none free”. Worse, a missing fine gives -1, which is not 0, so it would say “found”. Compare with the sentinel instead: if (spot != -1). An std::optional avoids the trap: if (result) on an optional asks whether it holds a value, so an optional holding 0 is still true.
  24. 3. Searching a list · Searching records by a field · Code challengeFinish firstAvailable(books, author): return the first book by author whose onLoan is false, or std::nullopt if there is none.One solution:
    Solution
    #include <optional>#include <string>#include <vector>
    struct Book {  std::string title;  std::string author;  bool onLoan;};
    std::optional<Book> firstAvailable(const std::vector<Book>& books,                                   const std::string& author) {  for (const Book& b : books) {    if (b.author == author && !b.onLoan) return b;  }  return std::nullopt;}
    Both conditions go in one if with &&, so the loop keeps going past Emma, which is by Austen but on loan. std::ranges::find_if with a lambda would be just as right. If you search the same list by ISBN thousands of times, build an std::unordered_map from ISBN to book once, and each search becomes about one step: module 10.
  25. 4. Binary search · A sorted list can be halved · QuizWhy can’t binary search work on a list that isn’t sorted?B. Looking at the middle tells you nothing about which half the target is inThe whole trick is “the middle is smaller than the target, so everything before it is too”. That is only true when the list is in order. On an unsorted list, binary search does not fail loudly: it quietly answers “not found” for values that are there.
  26. 4. Binary search · Tracing binary search · QuizNow search the same list for 20, which is missing. What are lo and hi when the search gives up?A. lo 4, hi 3Rounds 1 and 2 go as before (19 < 20, then 31 > 20). In round 3, mid is 4 and 24 > 20, so hi = 3. Now lo (4) is past hi (3): no index is left, so it stops with -1. Notice where lo ended: at 4, exactly where 20 would go to keep the list sorted. That comes back later.
  27. 4. Binary search · Binary search in code · Put in orderPut one round of binary search in order.1. Check that lo is not past hi 2. Work out mid, halfway between them 3. Compare the element at mid with the target 4. If equal, return mid 5. Otherwise move lo or hi past mid, and go round againThe check comes first, so an empty list (where hi is -1) never reads an element. Each round rules out mid and everything on its wrong side, so the range gets strictly smaller every time: that is why it always ends.
  28. 4. Binary search · Try it: write binary search · Code challengeFinish binarySearch(sorted, target): return the index of target in the sorted vector, or -1. Halve the range each round; don’t look at every element.One solution:
    Solution
    #include <vector>
    int binarySearch(const std::vector<int>& sorted, int target) {  int lo = 0;  int hi = std::ssize(sorted) - 1;  while (lo <= hi) {    int mid = lo + (hi - lo) / 2;    if (sorted[mid] == target) return mid;    if (sorted[mid] < target) lo = mid + 1;    else hi = mid - 1;  }  return -1;}
    The first test tries every position, which catches most off-by-one errors: the first and last elements are where they hide. If lo and hi are declared but never move, you have a linear scan or a loop that never ends, and the last test runs out of time. A million elements take at most 20 rounds, each reading sorted[mid] once or twice.
  29. 4. Binary search · The off-by-one traps · Find the bugSearching [10, 20] for 20 never finishes. Which line is wrong?Line 7: if (sorted[mid] < target) lo = mid + 1;lo 0, hi 1, so mid is 0, and 10 < 20 sets lo = 0 again: nothing changed, so the same round repeats forever. mid was just ruled out, so it must not stay in the range: mid + 1. Every round has to shrink the range, or the loop has no reason to end.
  30. 4. Binary search · The off-by-one traps · QuizWith while (lo < hi) instead of lo <= hi (and nothing else changed), which search goes wrong?A. Searching [7] for 7For [7], lo and hi are both 0, so lo < hi is false at once and it answers -1 without ever looking. Whenever the range shrinks to one index, that last candidate is skipped. Searching [3, 8, 12] for 8 still works: 8 is the first mid.
  31. 4. Binary search · Lower bound: where it would go · Fill the blanksInsert a price into a sorted list, keeping it sorted.1: 12, 2: insert, 3: 1212 is between 9 and 15, so its insertion point is 2. insert takes a position, an iterator, so prices.begin() + at turns the index into one, and puts 12 there. Finding the spot is O(log n), but insert moves every later element one place along to make room, O(n): for many inserts, a different data structure, such as the balanced tree inside std::set, does better.
  32. 4. Binary search · Try it: how many can I afford? · Code challengeFinish countAffordable(sorted, budget): how many prices in the sorted vector are at most budget. Use binary search, not a loop over every price.One solution:
    Solution
    #include <vector>
    int countAffordable(const std::vector<int>& sorted, int budget) {  int lo = 0;  int hi = std::ssize(sorted);  while (lo < hi) {    int mid = lo + (hi - lo) / 2;    if (sorted[mid] <= budget) lo = mid + 1;    else hi = mid;  }  return lo;}
    With <=, equal prices count as affordable and lo moves past them, so the answer lands after the last 5. With < you get the lower bound instead, which stops at the first 5 and undercounts: the third test catches that. This “first index past” search is known as the upper bound.
  33. 4. Binary search · Searching for an answer · Code challengeFinish smallestCapacity(weights, days): the smallest van capacity that delivers every parcel in at most days days. trips is written for you.One solution:
    Solution
    #include <algorithm>#include <vector>
    int trips(const std::vector<int>& weights, int cap) {  int count = 1, load = 0;  for (int w : weights) {    if (load + w > cap) {      count++;      load = 0;    }    load += w;  }  return count;}
    int smallestCapacity(const std::vector<int>& weights, int days) {  int lo = 0, hi = 0;  // the heaviest parcel, the total  for (int w : weights) lo = std::max(lo, w), hi += w;  while (lo < hi) {    int mid = lo + (hi - lo) / 2;    if (trips(weights, mid) <= days) hi = mid;    else lo = mid + 1;  }  return lo;}
    It is lowerBound again, with “does mid work?” in place of sorted[mid] < x. If it works, it might be the answer, so hi = mid; if not, the answer is bigger. Each guess costs one O(n) call to trips, and there are O(log(total)) guesses, far fewer than trying every capacity from the heaviest up.
  34. 5. Simple sorts · Sorted data is easier to use · QuizA list of 1,000 prices is sorted in ascending order. Which questions become easy? Pick all that apply.A. What is the cheapest price? (look at the first one); B. Are any two prices the same? (compare each with its neighbour); C. Is the price 12.50 in the list? (binary search)Sorting puts the smallest first, puts duplicates side by side, and allows binary search. It forgets the original order, though: once sorted, you can no longer tell which price came last. If you need both, sort a copy.
  35. 5. Simple sorts · std::sort puts things in order with < · QuizScores were read from a file as text. What order are they in after sorting?C. "100", "25", "3", "42"They are strings, so < compares them as text: “100” starts with 1, so it comes first; then “25”, “3”, “42”. The sort did its job; the data had the wrong type. Turn them into numbers first (std::stoi("25") gives 25), or give sort a comparator that compares them as numbers, next.
  36. 5. Simple sorts · A comparator tells sort which comes first · Fill the blanksSort the books by price, cheapest first, and books with the same price by title, A to Z.1: !=, 2: <, 3: <Compare by the main key first; only when it ties does the second key decide. a.price < b.price is true when a is cheaper, so a goes first; > there would put the most expensive first. A short form you will see: return std::tie(a.price, a.title) < std::tie(b.price, b.title); (from <tuple>), which compares the prices and, only if they are equal, the titles.
  37. 5. Simple sorts · Selection sort picks the smallest each time · Put in orderSelection sort on {4, 1, 3}. Put what happens in order.1. Look through all three: the smallest is 1 2. Swap 1 with 4: {1, 4, 3} 3. Look through the last two: the smallest is 3 4. Swap 3 with 4: {1, 3, 4} 5. Only one item is left, so it is already in placeEach round finds the smallest of the unsorted part, then makes one swap to put it at the front of that part. The sorted part grows by one each round, so after n - 1 rounds the last item has nowhere else to go.
  38. 5. Simple sorts · Insertion sort is how you sort a hand of cards · Find the buginsertionSort leaves {3, 1} as {3, 1}. Which line is wrong?Line 5: while (j >= 0 && nums[j] > card) {An off-by-one: j > 0 never compares with index 0, so no card can move to the very front. With {3, 1}, j starts at 0 and the loop does not run at all. Index 0 is a real element, so the condition must be j >= 0. Off-by-one mistakes hide at the ends of a range: check the first and last index by hand.
  39. 5. Simple sorts · Try it: write insertion sort, and use a comparator · Code challengeFinish insertionSort(nums): sort the numbers in ascending order in place, changing the vector it is given. Do not use std::sort.One solution:
    Solution
    #include <vector>
    void insertionSort(std::vector<int>& nums) {  for (int i = 1; i < std::ssize(nums); i++) {    int card = nums[i];    int j = i - 1;    while (j >= 0 && nums[j] > card) {      nums[j + 1] = nums[j];      j--;    }    nums[j + 1] = card;  }}
    Keep the card aside, shift every bigger item one place right, then write the card into the gap at j + 1. The usual slips are j > 0, which never reaches the front (the second test), and forgetting nums[j + 1] = card, which loses the card. An empty vector or a vector of one never enters the loop, so it comes back unchanged. (using Ints = … in the tests just gives the type a short name.)
  40. 5. Simple sorts · Try it: write insertion sort, and use a comparator · Code challengeFinish sortBooks(books): return a new vector sorted by price, cheapest first; books with the same price go in title order, A to Z. books is const, so it stays as it was.One solution:
    Solution
    #include <algorithm>#include <string>#include <vector>
    struct Book {  std::string title;  int price;};
    std::vector<Book> sortBooks(const std::vector<Book>& books) {  std::vector<Book> out = books;  std::ranges::sort(out, [](const Book& a, const Book& b) {    if (a.price != b.price) return a.price < b.price;    return a.title < b.title;  });  return out;}
    books comes as const&, so the compiler refuses to sort it: copy it into out and sort that. The comparator compares prices, and only when they tie compares titles. Another way is to take the parameter by value, std::vector<Book> books: the caller’s vector is copied on the way in, and you may sort the copy. Both cost one copy, O(n).
  41. 5. Simple sorts · A stable sort keeps ties in their original order · QuizSelection sort runs on cards {5♥, 5♠, 2♣}, comparing numbers only. What is the result after the first swap?A. {2♣, 5♠, 5♥}The smallest, 2♣, is swapped with the first card, 5♥, which jumps to the end: {2♣, 5♠, 5♥}. The two fives have changed order, so selection sort is not stable. Insertion sort would slide 2♣ left past both fives and leave them as they were.
  42. 5. Simple sorts · These sorts take n² steps, except on nearly sorted data · QuizA list of 10,000 scores is sorted, then 3 new scores are added at the end. Which simple sort re-sorts it fastest?A. Insertion sortInsertion sort walks the 9,999 sorted items once, moving nothing, then slides each of the 3 new scores left to its place: roughly n steps. Selection sort still scans the whole unsorted part every round, about 50 million comparisons, whatever the order.
  43. 6. Recursion · Every recursion needs a base case it is sure to reach · Fill the blanksWrite power(base, exp), base multiplied by itself exp times, recursively. Anything to the power 0 is 1.1: 0, 2: 1, 3: exp - 1The base case is the smallest exponent, 0, whose answer is 1 by definition. The recursive case uses base^exp = base × base^(exp - 1), and exp - 1 moves towards 0. Writing exp + 1 there would move away from the base case, and the calls would never stop.
  44. 6. Recursion · The call stack remembers every call that is waiting · Put in orderPut what happens during factorial(3) in order.1. factorial(3) calls factorial(2) and waits 2. factorial(2) calls factorial(1) and waits 3. factorial(1) returns 1 4. factorial(2) returns 2 × 1 = 2 5. factorial(3) returns 3 × 2 = 6The calls go down first, each pushing a frame, until the base case. Then the answers come back up in the opposite order, each frame finishing its multiplication and being removed. Last in, first out: that is why it is a stack.
  45. 6. Recursion · The call stack remembers every call that is waiting · QuizWhat does this print?B. 1, 2, 3The printing comes after the recursive call, so each call waits for the smaller ones to finish before printing. show(1) prints first, on the way back up. Swap the two lines and it prints 3, 2, 1, like countdown: work before the call happens on the way down.
  46. 6. Recursion · A list is its first item and the rest · Code challengeFinish sumFrom(nums, i) recursively: the sum of the numbers from index i to the end. When i is the size there is nothing left, so the sum is 0; otherwise it is nums[i] plus the sum from i + 1.One solution:
    Solution
    #include <vector>
    int sumFrom(const std::vector<int>& nums, int i) {  if (i == std::ssize(nums)) return 0;  return nums[i] + sumFrom(nums, i + 1);}
    Two lines: the base case answers “nothing left”, and the recursive case takes one item and trusts sumFrom to handle the rest. Trusting the smaller call, without tracing it all the way down, is the knack of writing recursion. The vector is passed by const& to every call, so all of them read the same one; only i changes.
  47. 6. Recursion · Recursion is natural for nested data · Code challengeFinish totalSize(folder): the sizes in its own files, plus the totalSize of each folder in folders, however deep.One solution:
    Solution
    #include <string>#include <vector>
    struct Folder {  std::string name;  std::vector<int> files;  std::vector<Folder> folders;};
    int totalSize(const Folder& folder) {  int total = 0;  for (int size : folder.files) total += size;  for (const Folder& sub : folder.folders) total += totalSize(sub);  return total;}
    Each call handles one folder’s own files and asks its subfolders for theirs. The base case is hidden in the data: a folder with an empty folders vector makes no calls, so the recursion stops by itself. Adding only sub.files would get one level down but miss Summer, which the last test checks. (A struct may hold a std::vector of its own type: the vector keeps its folders elsewhere in memory, so a Folder stays a fixed size.)
  48. 6. Recursion · Too deep, and the stack overflows · Find the bugsumFrom({1, 2}, 0) never finishes. Which line is wrong?Line 3: return nums[i] + sumFrom(nums, i + 1);The call passes the same i, so every call asks the very same question and the base case is never reached. Built without optimisation, it crashes with a stack overflow; here the optimiser turns the calls into a loop (the next slide says how), which runs until it is stopped. Each recursive call must make the problem strictly smaller. When you write one, check both: is there a base case, and does every call move towards it?
  49. 6. Recursion · Recursion and loops can do the same work · QuizWhich job is the best fit for recursion?B. Listing every file in a folder and all its subfoldersFolders nest to a depth you do not know in advance, and each subfolder is the same problem again: a perfect fit. The other two are flat, so a loop is simpler, and a million recursive calls would overflow the stack.
  50. 7. Merge sort · Merging two sorted piles is easy · QuizThis merge forgets something. What does merge({1, 4}, {2, 3}) return?B. {1, 2, 3}It takes 1, 2, then 3, and now b is used up, so the loop stops with 4 still in a. The leftovers of whichever vector did not run out must be added at the end: while (i < a.size()) out.push_back(a[i++]);, and the same for b. a[i++] reads a[i] and then adds 1 to i, a common short form.
  51. 7. Merge sort · Try it: merge two sorted lists · Code challengeFinish merge(a, b): both vectors are sorted, smallest first. Return one new sorted vector with every number from both, in about a.size() + b.size() steps (no std::sort).One solution:
    Solution
    #include <vector>
    std::vector<int> merge(const std::vector<int>& a,                       const std::vector<int>& b) {  std::vector<int> out;  size_t i = 0, j = 0;  while (i < a.size() && j < b.size()) {    if (a[i] <= b[j]) out.push_back(a[i++]);    else out.push_back(b[j++]);  }  while (i < a.size()) out.push_back(a[i++]);  while (j < b.size()) out.push_back(b[j++]);  return out;}
    The loop runs while both vectors have items left, and takes the smaller front item each time. When it stops, one is used up and the other’s leftovers are all bigger than everything taken, so they go on the end as they are. Only one of the two last loops ever does anything. Using <= takes from a on a tie, which keeps the sort stable, later in this module. The library has this as std::ranges::merge.
  52. 7. Merge sort · Merge sort: split, sort both halves, merge · Put in orderPut the steps of merge sort, for one call, in order.1. If the list has 0 or 1 items, return it 2. Find the middle index 3. Merge-sort the left half 4. Merge-sort the right half 5. Merge the two sorted halves and return the resultThe base case is checked first, or the calls would never stop: a list of one would be split into an empty list and itself, forever. The two halves can be sorted in either order, but both must be finished before the merge, which needs two sorted lists.
  53. 7. Merge sort · Why merge sort takes n log n steps · QuizMerge sort runs on 1,024 items. How many levels of splitting until every piece has one item?A. 101,024 halves to 512, 256, 128, 64, 32, 16, 8, 4, 2, 1: ten halvings, because 2¹⁰ = 1,024. Each level merges 1,024 items in total, so the sort does about 10 × 1,024 ≈ 10,000 steps, against about a quarter of a million for insertion sort on random data.
  54. 7. Merge sort · Try it: merge sort with a comparator · Code challengeFinish sortBy(items, less) as a merge sort: return a new sorted vector. A vector of 0 or 1 items comes back as it is; otherwise sort each half and merge them.One solution:
    Solution
    #include <vector>
    template <typename T, typename Less>std::vector<T> merge(const std::vector<T>& a,                     const std::vector<T>& b, Less less) {  std::vector<T> out;  size_t i = 0, j = 0;  while (i < a.size() && j < b.size()) {    if (less(b[j], a[i])) out.push_back(b[j++]);    else out.push_back(a[i++]);  }  while (i < a.size()) out.push_back(a[i++]);  while (j < b.size()) out.push_back(b[j++]);  return out;}
    template <typename T, typename Less>std::vector<T> sortBy(const std::vector<T>& items, Less less) {  if (items.size() <= 1) return items;  auto mid = items.begin() + items.size() / 2;  auto left = sortBy(std::vector<T>(items.begin(), mid), less);  auto right = sortBy(std::vector<T>(mid, items.end()), less);  return merge(left, right, less);}
    The base case returns a copy, so the result is always a new vector. Each half is sorted by the same function, then merged. The comparison count is at most n log₂ n (10 levels of at most 1,024 comparisons), while insertion sort on this reversed list would need over half a million. Stability comes from merge: it takes from the right half only when less(b[j], a[i]), the right item strictly smaller, so on a tie the left one goes first. Kim was before Dune, and stays there. std::ranges::merge makes the same choice.
  55. 7. Merge sort · Merge sort is stable, if the merge takes left on ties · Find the bugThis merge sorts by price correctly, but books with the same price come out in the wrong order. Which line?Line 6: if (a[i].price <= b[j].price) out.push_back(a[i++]);With <, a tie goes to the else and takes from b, the right half, so a later book jumps ahead of an earlier one with the same price. The result is still sorted by price, which is why this bug hides; only the order among equals is wrong. <= takes from the left on ties and keeps the sort stable.
  56. 7. Merge sort · The price is extra memory · QuizWhich statements about merge sort are true? Pick all that apply.A. It takes O(n log n) time even on a list that is already sorted; C. It is stable when ties are taken from the left halfMerge sort always splits and merges, so a sorted list costs the same n log n; insertion sort would do it in n. It needs O(n) extra memory for merging. On tiny lists its overhead (copies, calls) makes it slower than insertion sort, which is why std::stable_sort and std::sort both switch to insertion sort for short stretches.
  57. 8. Quicksort · Trace a partition by hand · Put in orderPut the steps of a Lomuto partition in order.1. Take the last element as the pivot 2. Put the wall at the start of the range 3. Scan each other element from left to right 4. If it is smaller than the pivot, swap it onto the wall and move the wall right 5. Swap the pivot onto the wall and return the wall’s indexThe wall can only be set once you know the pivot, and the pivot can only be placed once every other element has been compared with it. Placing it last is what guarantees everything on its left is smaller.
  58. 8. Quicksort · Trace a partition by hand · QuizWhat is in a afterwards, and what is p?A. {3, 1, 4, 8}, 2The pivot is 4. 3 is smaller, swapped with itself, wall at 1. 8 is not. 1 is smaller, swapped with 8: {3, 1, 8, 4}, wall at 2. Then the pivot swaps with 8: {3, 1, 4, 8}, and 2 is returned. The left part {3, 1} is not sorted yet; partitioning only promises it is all smaller.
  59. 8. Quicksort · Try it: write partition · Code challengeFinish partition(a, lo, hi) with the last element as pivot: move every element smaller than the pivot before it, everything else after it, and return the pivot’s new index.One solution:
    Solution
    #include <utility>#include <vector>
    int partition(std::vector<int>& a, int lo, int hi) {  int pivot = a[hi];  int i = lo;  for (int j = lo; j < hi; j++) {    if (a[j] < pivot) {      std::swap(a[i], a[j]);      i++;    }  }  std::swap(a[i], a[hi]);  return i;}
    The wall starts at lo, not 0, and j stops before hi, so only the given range is touched: the third test checks that, because quicksort calls partition on smaller and smaller pieces of one vector. Forgetting the final swap leaves the pivot at the end, and the returned index pointing at the wrong element. (std::swap lives in <utility>.)
  60. 8. Quicksort · Quicksort: partition, then sort each side · Fill the blanksFinish quicksort.1: >=, 2: p - 1, 3: p + 1The pivot at p is already where it belongs, so both recursive calls leave it out. The base case uses >= because an empty range, where lo is past hi, happens all the time: when the pivot lands at the very start, quickSort(a, lo, lo - 1) is called.
  61. 8. Quicksort · Quicksort: partition, then sort each side · Find the bugOn {1, 2} this crashes: “the recursion went too deep (a stack overflow)”. Which line is wrong?Line 4: quickSort(a, lo, p - 1);On {1, 2} the pivot 2 is already last, so p is 1 and line 4 calls quickSort(a, 0, 1): the very same range. It partitions, gets p = 1 again, and calls itself again, forever, until the call stack runs out. Each call must work on a strictly smaller range, which is why the pivot is left out.
  62. 8. Quicksort · Try it: sort in place · Code challengepartition is written for you. Finish quickSort(a, lo, hi) so it sorts a from lo to hi in place, smallest first. The one-argument quickSort(a) sorts the whole vector with it.One solution:
    Solution
    #include <utility>#include <vector>
    int partition(std::vector<int>& a, int lo, int hi) {  int pivot = a[hi];  int i = lo;  for (int j = lo; j < hi; j++) {    if (a[j] < pivot) std::swap(a[i++], a[j]);  }  std::swap(a[i], a[hi]);  return i;}
    void quickSort(std::vector<int>& a, int lo, int hi) {  if (lo >= hi) return;  int p = partition(a, lo, hi);  quickSort(a, lo, p - 1);  quickSort(a, p + 1, hi);}
    void quickSort(std::vector<int>& a) {  quickSort(a, 0, std::ssize(a) - 1);}
    Three lines after the base case: partition, then recurse on each side of p, leaving p out. An empty vector gives hi = -1, so lo >= hi stops it at once. Two functions may share a name when their parameters differ, called overloading: quickSort(a) and quickSort(a, lo, hi) are different functions. In partition, std::swap(a[i++], a[j]) swaps and then moves the wall, in one line.
  63. 8. Quicksort · A bad pivot makes quicksort slow · QuizWith the last element as pivot, which input is the slowest to sort?B. A list that is already sortedIn a sorted list the last element is the largest, so every partition puts everything on the left and nothing on the right. Each step removes only the pivot. The cruel part: “already sorted” or “nearly sorted” is extremely common in real data.
  64. 8. Quicksort · Quickselect finds the k-th smallest · Code challengepartition is given. Finish kthSmallest(a, k): return the k-th smallest number, where k = 1 is the smallest. a is taken by value, a copy, so you may rearrange it freely. Don’t sort the whole list.One solution:
    Solution
    #include <utility>#include <vector>
    int partition(std::vector<int>& a, int lo, int hi) {  int pivot = a[hi];  int i = lo;  for (int j = lo; j < hi; j++) {    if (a[j] < pivot) std::swap(a[i++], a[j]);  }  std::swap(a[i], a[hi]);  return i;}
    int kthSmallest(std::vector<int> a, int k) {  int lo = 0;  int hi = std::ssize(a) - 1;  while (true) {    int p = partition(a, lo, hi);    if (p == k - 1) return a[p];    if (p < k - 1) lo = p + 1;    else hi = p - 1;  }}
    The k-th smallest sits at index k - 1 once sorted, and a placed pivot is exactly at its sorted index. So compare p with k - 1 and narrow lo/hi to the side that holds it. A loop works as well as recursion here, since only one side is ever followed. The off-by-one to watch is k versus k - 1: the first test catches it. The last test passes for free, because the parameter is a copy: that is what taking a std::vector by value means.
  65. 9. Two pointers and sliding windows · Two fingers on a list beat trying every pair · QuizA list has 1000 prices. Roughly how many different pairs does “try every pair” check?C. About 500 000Each of the 1000 prices pairs with each of the 999 others, and each pair is counted twice that way, so 1000 × 999 / 2 ≈ 500 000. That is n²/2, which is O(n²). On a sorted list, two pointers find a pair with a given sum in at most 1000 steps.
  66. 9. Two pointers and sliding windows · Why moving one pointer never skips the answer · Find the bugpairWithSum({5}, 10) returns {0, 0}, pairing the only book with itself. Which line is wrong?Line 5: while (lo < hi) {With <=, the loop also runs when both pointers are on the same index, and 5 + 5 is 10. A pair needs two different books, so the loop must stop as soon as they meet: lo < hi. Off-by-one errors in two-pointer code nearly always live in this condition.
  67. 9. Two pointers and sliding windows · Why moving one pointer never skips the answer · Code challengeFinish pairWithSum(prices, target): prices is sorted from cheapest to dearest. Return the indexes {i, j} (with i < j) of two prices adding up to target, or std::nullopt if there are none. Use two pointers.One solution:
    Solution
    #include <optional>#include <utility>#include <vector>
    std::optional<std::pair<int, int>> pairWithSum(    const std::vector<int>& prices, int target) {  int lo = 0;  int hi = std::ssize(prices) - 1;  while (lo < hi) {    int sum = prices[lo] + prices[hi];    if (sum == target) return std::pair{lo, hi};    if (sum < target) lo++;    else hi--;  }  return std::nullopt;}
    The loop condition lo < hi keeps the two books different, which the third test checks. Moving the wrong pointer (say hi-- when the sum is too small) makes the sum smaller still and walks past the answer. An empty list sets hi to -1, so the loop never runs; that is one reason the indexes are signed ints.
  68. 9. Two pointers and sliding windows · Reversing in place: swap the ends, move inwards · Fill the blanksReverse the list in place.1: 1, 2: <, 3: swap, 4: ++, 5: --right starts at the last index, size - 1. The loop stops when the pointers meet or cross: in an odd-length list, the middle song stays where it is. With <= it would swap the middle with itself, harmless here; with left < std::ssize(songs), it would carry on past the middle and swap everything back. The library has it as std::ranges::reverse(songs).
  69. 9. Two pointers and sliding windows · Reversing in place: swap the ends, move inwards · Code challengeFinish isPalindrome(word): return true if the word reads the same forwards and backwards, ignoring upper and lower case. Use two pointers, not a reversed copy.One solution:
    Solution
    #include <cctype>#include <string>
    bool isPalindrome(const std::string& word) {  std::string w = word;  for (char& c : w) c = std::tolower(c);  int left = 0;  int right = std::ssize(w) - 1;  while (left < right) {    if (w[left] != w[right]) return false;    left++;    right--;  }  return true;}
    One mismatch is enough to answer false at once: that early exit is why this beats building a reversed copy and comparing, which always does all the work. Reaching the middle with no mismatch means true, including for the empty word, where the loop never runs. The starter lowercases a copy, w, one char at a time with std::tolower; that is right for plain English letters, which are one byte each.
  70. 9. Two pointers and sliding windows · A read pointer and a write pointer · QuizWhat does this print?A. 3 63 is kept, the second 3 skipped, 5 and the first 8 copied to indexes 1 and 2, the other 8s skipped, so scores is now {3, 5, 8}: size 3. But resize to a smaller size never gives memory back: the capacity, how many elements the vector has room for, stays 6, ready for the vector to grow again without moving. shrink_to_fit() asks for the spare room to be freed. The library’s version of this walk is a.erase(std::unique(a.begin(), a.end()), a.end()).
  71. 9. Two pointers and sliding windows · Merging two sorted lists · Put in orderPut the steps of merging two sorted lists in order.1. Point at the first element of each list 2. Compare the two elements pointed at 3. Add the smaller to the output and move its pointer on 4. Repeat until one list is used up 5. Copy whatever is left of the other listThe leftovers can be copied without comparing, because they are sorted and all at least as big as everything already taken. Forgetting that last step is the classic merging bug: the output silently loses the biggest elements.
  72. 9. Two pointers and sliding windows · Try it: the best k days · Code challengeFinish bestRun(sales, k): return the largest sum of k consecutive days, or std::nullopt if there are fewer than k days. Slide the window; don’t add up each stretch from scratch.One solution:
    Solution
    #include <algorithm>#include <optional>#include <vector>
    std::optional<int> bestRun(const std::vector<int>& sales, int k) {  if (k > std::ssize(sales)) return std::nullopt;  int sum = 0;  for (int i = 0; i < k; i++) sum += sales[i];  int best = sum;  for (int i = k; i < std::ssize(sales); i++) {    sum += sales[i] - sales[i - k];    best = std::max(best, sum);  }  return best;}
    The first window is added up once; after that, the day at i comes in and the day at i - k falls out. best starts at the first window’s sum, not 0, so all-negative sales still work: the same trap as finding the largest number. The window starting at the last possible day is covered because i runs to the last index. Checking k against the size first matters in C++: reading sales[i] past the end would stop the program.
  73. 9. Two pointers and sliding windows · A window that grows and shrinks · Code challengeFinish longestFresh(songs): return the length of the longest stretch of the playlist in which no song appears twice.One solution:
    Solution
    #include <algorithm>#include <string>#include <unordered_set>#include <vector>
    int longestFresh(const std::vector<std::string>& songs) {  std::unordered_set<std::string> inWindow;  int best = 0;  int left = 0;  for (int right = 0; right < std::ssize(songs); right++) {    while (inWindow.contains(songs[right])) {      inWindow.erase(songs[left]);      left++;    }    inWindow.insert(songs[right]);    best = std::max(best, right - left + 1);  }  return best;}
    In the second test the best window is B, C, A, D, E: when the second A arrives, left moves past the first A, but B and C stay. A common mistake is to empty the whole window on a repeat and start again at right, which loses B and C and answers 3. Shrink only as far as the copy.
  74. 10. Counting and hashing · Remembering what you have seen saves looking again · QuizFor each of n orders, you check whether its customer is on a vector of n members with std::ranges::find(members, id). How much work is that?C. O(n²)Each find can scan all n members, and it runs n times: n × n. Building std::unordered_set<int> ids(members.begin(), members.end()) once costs O(n), then each contains is O(1), so the whole job drops to O(n). A find inside a loop is the first thing to look for when code is slow.
  75. 10. Counting and hashing · Try it: two-sum · Code challengeFinish twoSum(nums, target): the list is not sorted. Return {i, j}, with i < j, the indexes of two different numbers adding up to target, or std::nullopt. Do it in one pass with an unordered_map.One solution:
    Solution
    #include <optional>#include <unordered_map>#include <utility>#include <vector>
    std::optional<std::pair<int, int>> twoSum(    const std::vector<int>& nums, int target) {  std::unordered_map<int, int> seen;  for (int i = 0; i < std::ssize(nums); i++) {    auto it = seen.find(target - nums[i]);    if (it != seen.end()) return std::pair{it->second, i};    seen[nums[i]] = i;  }  return std::nullopt;}
    Checking before storing is what stops 5 pairing with itself, while two separate 5s still pair. The last test catches a classic slip, if (seen[need]): reading a missing key with [] adds it, with the value 0, and index 0 is false in an if, so a real partner at index 0 looks missing too. Use find or contains to look without changing the map.
  76. 10. Counting and hashing · Counting how often each thing appears · Fill the blanksCount the books in each genre.1: unordered_map, 2: genre, 3: ++, 4: 2The first time a genre is seen, counts[genre] makes an entry holding 0, and ++ makes it 1; the next time, it finds the entry and adds 1 again. Here that is exactly what you want. When you only want to read a count, [] still adds missing keys, so use counts.find(k) or counts.contains(k) instead (and on a const map, [] does not even compile).
  77. 10. Counting and hashing · Counting how often each thing appears · Find the bugEvery count comes out as 0, however many times a word appears. Which line is wrong?Line 5: int& count = counts[w];int count = counts[w]; copies the value into a new variable, so count++ changes the copy, and the entry in the map stays 0. A reference, int& count, is another name for the entry itself, so ++ changes the map. Or skip the variable: counts[w]++;. Copy versus reference is one of the commonest C++ bugs, and the compiler will not stop you: both are legal.
  78. 10. Counting and hashing · Try it: the most borrowed book · Code challengeFinish mostCommon(titles): return the title that appears most often, or std::nullopt for an empty list. On a tie, return the title that was seen first.One solution:
    Solution
    #include <optional>#include <string>#include <unordered_map>#include <vector>
    std::optional<std::string> mostCommon(    const std::vector<std::string>& titles) {  std::unordered_map<std::string, int> counts;  for (const std::string& t : titles) counts[t]++;  std::optional<std::string> best;  int bestCount = 0;  for (const std::string& t : titles) {    if (counts[t] > bestCount) {      best = t;      bestCount = counts[t];    }  }  return best;}
    Two passes: count, then find the largest count, exactly like finding the largest number in the first module. The second pass walks titles, not the map, on purpose: an unordered_map keeps its entries in no particular order, so looping over it could meet Dune before Emma. Walking the list in its own order, with a strict >, keeps the earlier title on a tie. An std::optional starts empty, so best needs no special start value.
  79. 10. Counting and hashing · Same letters, different order: anagrams · Code challengeFinish isAnagram(a, b): return true if the two words use exactly the same letters, each the same number of times. Count with an unordered_map<char, int>.One solution:
    Solution
    #include <string>#include <unordered_map>
    bool isAnagram(const std::string& a, const std::string& b) {  if (a.size() != b.size()) return false;  std::unordered_map<char, int> counts;  for (char ch : a) counts[ch]++;  for (char ch : b) {    if (counts[ch] == 0) return false;    counts[ch]--;  }  return true;}
    The second test breaks the tempting shortcut of comparing sets of letters: “aab” and “abb” use the same two letters, but not the same number of times. With equal lengths, if no count ever goes below zero, every count ends at exactly zero, so there is nothing to check at the end. A char is one byte, so a plain std::array<int, 256> of counts, indexed by the byte, would do the same job with no hashing at all.
  80. 10. Counting and hashing · A set spots duplicates in one pass · Quizstd::set is the tree-based set, like std::map without values. What does this print?A. 1 3 5A set holds each value once, so the second 5 is dropped, and std::set keeps its values sorted, so the loop gives 1, 3, 5. An std::unordered_set would also drop the repeat, but print the three in an order that depends on the hash table, which you should never rely on. Neither keeps the order the values arrived in.
  81. 10. Counting and hashing · Grouping: a map from a key to a list · Code challengeFinish groupAnagrams(words): return the words grouped so each group holds words that are anagrams of each other. Groups come in the order their first word appears, and words keep their order inside a group. where maps a key to its group’s index in groups.One solution:
    Solution
    #include <algorithm>#include <string>#include <unordered_map>#include <vector>using Group = std::vector<std::string>;
    std::vector<Group> groupAnagrams(const Group& words) {  std::vector<Group> groups;  std::unordered_map<std::string, int> where;  for (const std::string& w : words) {    std::string key = w;    std::ranges::sort(key);    auto it = where.find(key);    if (it != where.end()) {      groups[it->second].push_back(w);    } else {      where[key] = std::ssize(groups);      groups.push_back({w});    }  }  return groups;}
    key is a copy of the word, so sorting it leaves w alone. The first word with a new key starts a group at the end of groups, and where remembers its index; later words with that key go into the same group. Keeping the groups in a vector is what keeps them in order of first appearance. using Group = … gives the long type a short name.
  82. 10. Counting and hashing · Counting sort: sorting without comparing · Code challengeFinish sortByStars(reviews): return a new vector of the reviews from 1 star up to 5 stars, without comparing. Put each review in the bucket for its stars, then add the buckets to out in order. Reviews with the same stars keep their order.One solution:
    Solution
    #include <string>#include <vector>
    struct Review {  std::string book;  int stars;  // 1 to 5};
    std::vector<Review> sortByStars(const std::vector<Review>& reviews) {  std::vector<std::vector<Review>> buckets(6);  for (const Review& r : reviews) buckets[r.stars].push_back(r);  std::vector<Review> out;  for (const auto& bucket : buckets)    for (const Review& r : bucket) out.push_back(r);  return out;}
    Six buckets, 0 to 5, so bucket stars holds that many stars without subtracting 1; bucket 0 stays empty. Adding to the buckets in input order and joining them in order is what makes it stable: Kim stays ahead of Emma because Kim was put into bucket 5 first. reviews is const&, so the input cannot change; the result is a new vector.
  83. 11. Walking a tree · Depth first: before, between or after the children · QuizA tree has root A, whose children are B (left) and C (right); B has one left child, D. What is its post-order?C. D B C APost-order puts each node after both of its subtrees. Left of A is the subtree B, whose post-order is D B; then the right subtree, C; then A last. A B D C is pre-order and D B A C is in-order. The root always comes last in post-order and first in pre-order.
  84. 11. Walking a tree · Recursion writes itself for trees · Fill the blanksMake this function walk the tree in post-order.1: ==, 2: left, 3: right, 4: push_back, 5: valueMoving one line is all it takes: in post-order, push_back comes after both recursive calls, so a node is recorded only once everything below it has been. Swapping the two walk lines instead would give a mirror image, right before left, which is a different order altogether.
  85. 11. Walking a tree · Try it: in-order, sorted for free · Code challengeFinish inOrder(root): return the values of the tree in in-order (left subtree, node, right subtree). An empty tree (nullptr) gives {}. A helper like walk above is a good start.One solution:
    Solution
    #include <memory>#include <vector>
    struct TreeNode {  int value;  std::unique_ptr<TreeNode> left;  std::unique_ptr<TreeNode> right;};using Tree = std::unique_ptr<TreeNode>;
    Tree node(int value, Tree left = nullptr, Tree right = nullptr) {  return std::make_unique<TreeNode>(    TreeNode{value, std::move(left), std::move(right)});}
    void walk(const TreeNode* n, std::vector<int>& out) {  if (n == nullptr) return;  walk(n->left.get(), out);  out.push_back(n->value);  walk(n->right.get(), out);}
    std::vector<int> inOrder(const TreeNode* root) {  std::vector<int> out;  walk(root, out);  return out;}
    Visiting between the two calls is what makes it in-order. Passing one vector by reference keeps it O(n). Returning a new vector from each call and joining left, value and right also works and reads well, but copies values at every level, which costs O(n²) on a tree shaped like a long chain.
  86. 11. Walking a tree · The same walk with your own stack · Find the bugThis should give pre-order, 8 3 1 6 10 14 on our tree, but gives 8 10 14 3 6 1. Which line?Line 7: if (n->right) stack.push(n->right.get()); // and left on line 8A stack hands back the last thing pushed. Pushing left then right means right is taken first, so the walk goes down the right side before the left. Push right first, then left. (Swap lines 7 and 8.)
  87. 11. Walking a tree · Level by level with a queue · Put in orderPut one round of the level-order loop in order.1. Check that the queue still has nodes 2. Take the node at the front of the queue 3. Record its value 4. Add its left child to the back, if it has one 5. Add its right child to the back, if it has oneTake from the front, add to the back: that is what makes it a queue, and what makes the walk go level by level. Left before right keeps each level in left-to-right order. The loop ends when the queue is empty, which happens once the last leaf has been taken.
  88. 11. Walking a tree · Try it: one vector per level · Code challengeFinish levels(root): return the values level by level, one vector per level, each left to right. Our tree gives {{8}, {3, 10}, {1, 6, 14}}; an empty tree gives {}.One solution:
    Solution
    #include <memory>#include <vector>
    struct TreeNode {  int value;  std::unique_ptr<TreeNode> left;  std::unique_ptr<TreeNode> right;};using Tree = std::unique_ptr<TreeNode>;
    Tree node(int value, Tree left = nullptr, Tree right = nullptr) {  return std::make_unique<TreeNode>(    TreeNode{value, std::move(left), std::move(right)});}
    std::vector<std::vector<int>> levels(const TreeNode* root) {  std::vector<std::vector<int>> out;  std::vector<const TreeNode*> level;  if (root) level.push_back(root);  while (!level.empty()) {    std::vector<int> values;    std::vector<const TreeNode*> next;    for (const TreeNode* n : level) {      values.push_back(n->value);      if (n->left) next.push_back(n->left.get());      if (n->right) next.push_back(n->right.get());    }    out.push_back(values);    level = next;  }  return out;}
    Here the queue is kept as two vectors, the level you are reading and the next one you are filling, which makes the end of each level obvious. A single std::queue with “take size() nodes, then stop” works just as well. Either way each node is handled once, O(n). Level order is the tree case of breadth-first search, which the next module runs on any graph.
  89. 11. Walking a tree · Height and path sums: answers built from the children · Code challengeFinish hasPathSum(root, target): return true if some root-to-leaf path’s values add up to target. An empty tree has no paths.One solution:
    Solution
    #include <memory>#include <vector>
    struct TreeNode {  int value;  std::unique_ptr<TreeNode> left;  std::unique_ptr<TreeNode> right;};using Tree = std::unique_ptr<TreeNode>;
    Tree node(int value, Tree left = nullptr, Tree right = nullptr) {  return std::make_unique<TreeNode>(    TreeNode{value, std::move(left), std::move(right)});}
    bool hasPathSum(const TreeNode* root, int target) {  if (root == nullptr) return false;  int rest = target - root->value;  if (!root->left && !root->right) return rest == 0;  return hasPathSum(root->left.get(), rest) ||         hasPathSum(root->right.get(), rest);}
    Each call subtracts its own value and hands the rest down. The check happens only at a leaf: testing rest == 0 at any node would wrongly accept 8 + 10 = 18, which stops halfway. || stops early once one side finds a path, so not every node is always visited.
  90. 11. Walking a tree · Lowest common ancestor in a search tree · QuizA binary tree has 7 nodes. What heights can it have?B. Anything from 3 to 7Packed as tightly as possible, levels of 1, 2 and 4 nodes hold 7, so height 3. Strung out as a chain, one node per level, height 7. That gap is why tree algorithms are quoted in terms of height h: O(h) is O(log n) for a balanced tree but O(n) for a chain.
  91. 11. Walking a tree · Lowest common ancestor in a search tree · Code challengeFinish lca(root, a, b) for a binary search tree that contains both a and b: return the value of their lowest common ancestor.One solution:
    Solution
    #include <memory>#include <vector>
    struct TreeNode {  int value;  std::unique_ptr<TreeNode> left;  std::unique_ptr<TreeNode> right;};using Tree = std::unique_ptr<TreeNode>;
    Tree node(int value, Tree left = nullptr, Tree right = nullptr) {  return std::make_unique<TreeNode>(    TreeNode{value, std::move(left), std::move(right)});}
    int lca(const TreeNode* root, int a, int b) {  const TreeNode* n = root;  while (true) {    if (a < n->value && b < n->value) n = n->left.get();    else if (a > n->value && b > n->value) n = n->right.get();    else return n->value;  }}
    The BST rule tells you which side each value is on without searching. While both are on the same side, the answer is deeper on that side (and since both values are in the tree, that side is never empty); the moment they part, or one of them is the current node, nothing deeper holds both. That is O(h) time and O(1) memory. In an ordinary tree with no ordering you would instead search both subtrees, O(n).
  92. 12. Searching a graph · A graph is places and the links between them · QuizIn town, which places can you reach from school, by any path?B. library, shop and poolschool leads directly to library and shop, and both lead on to pool. You cannot get back to home or to park: the edges are one-way, and none points at them from there. Searching a graph is answering questions like this one, for graphs too big to do by eye.
  93. 12. Searching a graph · Breadth-first search explores in rings · Put in orderPut one round of the BFS loop in order.1. Take the node at the front of the queue 2. Look at each of its neighbours in turn 3. Skip a neighbour that has already been found 4. Record its distance as one more than the current node 5. Add it to the back of the queueFront out, back in: that is what makes it a queue, and what keeps the rings in order. Skipping nodes already found is what stops BFS from going round a loop for ever, and it also keeps the first (shortest) distance found.
  94. 12. Searching a graph · BFS in C++ · Code challengeWrite hops(graph, start, goal): the fewest edges on a path from start to goal, or -1 if there is no path. start to itself is 0.One solution:
    Solution
    #include <queue>#include <vector>
    using Graph = std::vector<std::vector<int>>;
    int hops(const Graph& graph, int start, int goal) {  std::vector<int> dist(graph.size(), -1);  dist[start] = 0;  std::queue<int> queue;  queue.push(start);  while (!queue.empty()) {    int node = queue.front();    queue.pop();    if (node == goal) return dist[node];    for (int next : graph[node]) {      if (dist[next] != -1) continue;      dist[next] = dist[node] + 1;      queue.push(next);    }  }  return -1;}
    BFS reaches nodes in order of distance, so the moment goal comes out of the queue, its distance is the smallest. Returning early saves exploring the rest. Without the dist[next] != -1 check, the loop a → b → d → a would refill the queue for ever, until the test runs out of time or memory. DFS would also find a path, but not necessarily the shortest one.
  95. 12. Searching a graph · Depth-first search follows one path to its end · QuizIn what order does the recursive dfs visit this graph from A?B. A B D C EDFS goes deep first: A, then its first neighbour B, then B’s neighbour D. D is a dead end, so it backs up to A and tries C. D is already seen, so it goes on to E. A B C D E is the BFS order, ring by ring. The stack version pushes neighbours in reverse so that it takes them in the same order as the recursive one. ("ABCDE"[node] picks one letter of the string by its position.)
  96. 12. Searching a graph · A visited set stops you going round in circles · Find the bugThis should mark every place reachable from start, but it misses some. Which line is wrong?Line 10: if (seen[next]) continue;break leaves the whole for loop at the first neighbour already seen, so the neighbours after it are never looked at. continue skips only that one. With Graph{{0, 1}, {}} (node 0 has an edge to itself, then to 1) this marks only node 0, though node 1 is one step away. Small test graphs with a loop catch this kind of slip.
  97. 12. Searching a graph · Connected components are the separate groups · Code challengeWrite countGroups(people, friendships): the people are numbered 0 to people - 1; return how many separate friend groups there are. A friendship {a, b} links both ways; someone with no friends is a group of one.One solution:
    Solution
    #include <utility>#include <vector>
    using Pairs = std::vector<std::pair<int, int>>;using Graph = std::vector<std::vector<int>>;
    void visit(const Graph& friends, int p, std::vector<bool>& seen) {  seen[p] = true;  for (int f : friends[p])    if (!seen[f]) visit(friends, f, seen);}
    int countGroups(int people, const Pairs& friendships) {  Graph friends(people);  for (auto [a, b] : friendships) {    friends[a].push_back(b);    friends[b].push_back(a);  }  std::vector<bool> seen(people, false);  int groups = 0;  for (int p = 0; p < people; p++) {    if (seen[p]) continue;    groups++;    visit(friends, p, seen);  }  return groups;}
    Build the adjacency list with both directions of each friendship, then search from every unseen person: each search marks one whole group, so the count of searches is the answer. Forgetting the reverse edge is the usual slip: eli → ben alone would let a search from ben miss eli, and the count comes out too high. The whole thing is O(V + E). The Data structures course’s union-find answers the same question as friendships arrive one by one.
  98. 12. Searching a graph · A grid is a graph too · Code challengeWrite floodFill(grid, row, col, paint): the grid is rows of one-character cells. Return the grid with the cell at row, col, and every cell joined to it (up, down, left, right) with the same character, changed to paint.One solution:
    Solution
    #include <stack>#include <string>#include <utility>#include <vector>
    using Grid = std::vector<std::string>;
    Grid floodFill(Grid grid, int row, int col, char paint) {  char old = grid[row][col];  if (old == paint) return grid;  int rows = grid.size(), cols = grid[0].size();  std::stack<std::pair<int, int>> stack;  stack.push({row, col});  while (!stack.empty()) {    auto [r, c] = stack.top();    stack.pop();    if (r < 0 || r >= rows || c < 0 || c >= cols) continue;    if (grid[r][c] != old) continue;    grid[r][c] = paint;    stack.push({r + 1, c});    stack.push({r - 1, c});    stack.push({r, c + 1});    stack.push({r, c - 1});  }  return grid;}
    Here the cell’s own colour does the job of the visited set: once a cell is painted it no longer matches old, so it is never pushed twice. That is why painting with the same colour must return early, or the search would never end. The bounds check comes before grid[r][c], so the edges are safe. Taking grid by value (no &) gives the function its own copy to paint, so the caller’s grid is left as it was.
  99. 12. Searching a graph · Finding a cycle in a directed graph · Fill the blanksComplete the cycle check. A node is active while it is on the current path.1: active, 2: false, 3: true, 4: doneMeeting an active node means an edge points back into the path being explored: a cycle. A done node was fully explored with no cycle found, so it is safe to skip. Once all of a node’s neighbours are explored it leaves the path and becomes done. With a single “seen” flag instead of three states, Graph{{1, 2}, {2}, {}} would wrongly report a cycle at node 2, reached twice by different routes.
  100. 12. Searching a graph · Topological sort puts tasks in a safe order · Code challengeWrite buildOrder(tasks, deps) with Kahn’s algorithm. The tasks are numbered 0 to tasks - 1; a dependency {a, b} means a must come before b. Return every task in a valid order, or std::nullopt if there is a cycle.One solution:
    Solution
    #include <optional>#include <utility>#include <vector>
    using Deps = std::vector<std::pair<int, int>>;
    std::optional<std::vector<int>> buildOrder(int tasks,                                           const Deps& deps) {  std::vector<std::vector<int>> after(tasks);  std::vector<int> waits(tasks, 0);  for (auto [first, then] : deps) {    after[first].push_back(then);    waits[then]++;  }  std::vector<int> ready;  for (int t = 0; t < tasks; t++)    if (waits[t] == 0) ready.push_back(t);  for (size_t i = 0; i < ready.size(); i++)    for (int next : after[ready[i]])      if (--waits[next] == 0) ready.push_back(next);  if (int(ready.size()) != tasks) return std::nullopt;  return ready;}
    A task joins ready only when its count of unfinished dependencies drops to 0, so it is always placed after all of them. Here ready is both the queue and the answer, read with an index as it grows. In a cycle, every task in it waits for another, none ever reaches 0, so fewer tasks come out than went in: that is the cycle check for free. Tests check the rules, not one exact order, because several orders are right.
  101. 13. Shortest paths · When roads have lengths, fewest steps is not enough · QuizBFS from home reaches shop first through which route, and how long does it take?B. A two-road route, which takes 5 or 7 minutes depending on neighbour orderBFS only counts roads. shop is two roads away by mill (5 minutes) and by bridge (7 minutes), and which one BFS meets first depends on the order of home’s neighbours. Neither is the 4-minute route, which has three roads. Weights need a different algorithm.
  102. 13. Shortest paths · A priority queue hands out the nearest place · Put in orderPut one round of Dijkstra’s loop in order.1. Pop the place with the smallest time from the priority queue 2. Skip it if it was already settled 3. Mark it settled: its time is final 4. For each road out of it, work out the time through it 5. If that beats the neighbour’s best time, record it and push the neighbourA place can be pushed several times, once for each improvement, so an old, larger entry can come out after the place was settled: the skip throws those away (this is called lazy deletion). Only after the pop is a time final; a time recorded while relaxing may still improve.
  103. 13. Shortest paths · Dijkstra in C++ · Find the bugThis version says every place is 0 minutes from start, whatever the graph. Which line is wrong?Line 1: std::vector<long long> dist(graph.size(), INF);A place not reached yet should count as infinitely far, so that any route to it is an improvement. Starting every place at 0, no positive time beats it, nothing is ever recorded or pushed, and the search stops after start with every answer still 0. C++ has no built-in “infinity” for whole numbers, so the largest value, INF, plays that part.
  104. 13. Shortest paths · Try it: cheapest times from one place · Code challengeWrite cheapest(graph, start): for every place, the least total weight of a path to it from start (start itself is 0), or INF if it cannot be reached. Use a std::priority_queue.One solution:
    Solution
    #include <functional>#include <limits>#include <queue>#include <utility>#include <vector>
    struct Edge { int to; int weight; };using Graph = std::vector<std::vector<Edge>>;using Item = std::pair<long long, int>;   // {time, place}const long long INF = std::numeric_limits<long long>::max();
    std::vector<long long> cheapest(const Graph& graph, int start) {  std::vector<long long> dist(graph.size(), INF);  std::vector<bool> done(graph.size(), false);  std::priority_queue<Item, std::vector<Item>, std::greater<>> queue;  dist[start] = 0;  queue.push({0, start});  while (!queue.empty()) {    auto [d, node] = queue.top();    queue.pop();    if (done[node]) continue;    done[node] = true;    for (auto [to, weight] : graph[node])      if (d + weight < dist[to]) {        dist[to] = d + weight;        queue.push({dist[to], to});      }  }  return dist;}
    The loop settles places in order of their cheapest time, so shop is first pushed at 7 (through bridge), then improved to 4 once mill is settled at 3. The 7 entry comes out later and is skipped because shop is done. The road from mill back to home is relaxed too, but 3 + 1 never beats 0, so the loop ends. island has a road to home but none from it, so it stays at INF. Only popped times are added to, and they are never INF, so d + weight cannot overflow.
  105. 13. Shortest paths · Remember where each best time came from · Code challengeWrite buildPath(previous, start, goal): the route from start to goal as a list of places, read from previous (where -1 means “no step recorded”). If goal cannot be traced back to start, return {}.One solution:
    Solution
    #include <algorithm>#include <vector>
    std::vector<int> buildPath(const std::vector<int>& previous,                           int start, int goal) {  std::vector<int> path = {goal};  int node = goal;  while (node != start) {    node = previous[node];    if (node == -1) return {};    path.push_back(node);  }  std::reverse(path.begin(), path.end());  return path;}
    Each place keeps only its last step, so the walk back goes goal first and is reversed at the end with std::reverse. previous takes as much memory as dist and nothing more, which is why it beats storing a whole route per place. The -1 check is what stops the walk at a place that was never reached; without it, previous[-1] would read outside the vector, and the program would stop with “index out of bounds”.
  106. 13. Shortest paths · Negative weights break Dijkstra · QuizRun cheapest (Dijkstra, with the done check) from S. What does it print for T?B. 3Dijkstra settles A at 2 and T at 3 before it ever looks at B. Popping B at 5 improves A to 1, but A is already done, so the improvement never reaches T. The true cheapest is S → B → A → T: 5 − 4 + 1 = 2. Bellman–Ford finds it.
  107. 13. Shortest paths · Try it: Bellman–Ford · Code challengeWrite bellmanFord(places, edges, start). Each edge is {from, to, weight}. Return the cheapest total from start to every place (INF if unreachable), or std::nullopt if there is a negative cycle.One solution:
    Solution
    #include <limits>#include <optional>#include <vector>
    struct WEdge { int from; int to; int weight; };const long long INF = std::numeric_limits<long long>::max();
    std::optional<std::vector<long long>> bellmanFord(    int places, const std::vector<WEdge>& edges, int start) {  std::vector<long long> dist(places, INF);  dist[start] = 0;  for (int round = 1; round < places; round++)    for (auto [from, to, w] : edges)      if (dist[from] != INF && dist[from] + w < dist[to])        dist[to] = dist[from] + w;  for (auto [from, to, w] : edges)    if (dist[from] != INF && dist[from] + w < dist[to])      return std::nullopt;  return dist;}
    The edges in the test are listed in an unhelpful order on purpose: A → T comes first, before A has a time. One round is not enough, which is why there are V − 1 rounds. The dist[from] != INF check matters here: X is never reached, and without it INF + 1 for the road X → T would overflow into a huge negative “time” for T. The last loop is the negative-cycle check: after V − 1 rounds nothing should still improve, and A → B → A adds up to −3, so it does.
  108. 13. Shortest paths · A* aims at the goal · Fill the blanksTurn Dijkstra’s loop into A*: stop at the goal, and order the queue by cost plus the guess.1: goal, 2: guessThe key in the queue is now cost so far plus guess(to), so the true cost is read from dist, not from the key. Popping the goal ends the search: with an honest guess, its cost is final then. With guess always 0, this is plain Dijkstra stopping at the goal.
  109. 13. Shortest paths · A* aims at the goal · QuizWhat happens if the guess sometimes overestimates the distance left?B. A* may return a route that is not the cheapestA* is guaranteed the cheapest route only when the guess never overestimates (an admissible heuristic). Straight-line distance is admissible for driving, since no road is shorter than a straight line. A guess that is too big can push the true best route to the back of the queue, and the goal is popped by a worse one first. Some games accept that trade for speed.
  110. 14. Greedy algorithms · A greedy algorithm takes the best-looking step now · QuizWhich of these is a greedy strategy?B. At each junction, taking the road that points most directly at your destinationTaking whichever road looks best at each junction, and never turning back, is greedy: one local choice at a time. Trying every route is brute force; it is always right but slow. Halving a sorted list is binary search. Note that the greedy route can lead you into a dead end or a long detour: looking best is not the same as being best.
  111. 14. Greedy algorithms · Making change: biggest coin first · Fill the blanksCount the coins greedily. coins is sorted from largest to smallest.1: /, 2: %Dividing one int by another in C++ gives a whole number and drops the remainder: 38 / 25 is 1, how many times the coin fits whole, which is the same as taking it again and again in a loop. amount % coin (the remainder, 13) is what is left to give afterwards. With doubles, 38.0 / 25 would be 1.52, which is not a number of coins.
  112. 14. Greedy algorithms · Try it: give change greedily · Code challengeFinish greedyChange(amount, coins): return the coins the greedy rule gives, largest first. Assume there is always a coin of 1.One solution:
    Solution
    #include <algorithm>#include <vector>
    std::vector<int> greedyChange(int amount, std::vector<int> coins) {  std::vector<int> used;  std::sort(coins.begin(), coins.end(),            [](int a, int b) { return a > b; });  for (int coin : coins) {    while (amount >= coin) {      used.push_back(coin);      amount -= coin;    }  }  return used;}
    coins is taken by value (no &), so the function sorts its own copy and the caller’s vector is left alone. The lambda [](int a, int b) { return a > b; } is a small unnamed function that tells std::sort “a comes first if it is bigger”: largest first (std::greater<>() says the same). The inner while takes one coin as often as it fits before moving down. The last test is a reminder that this is the greedy answer, not always the best one.
  113. 14. Greedy algorithms · Choosing meetings: the earliest finish wins · Put in orderPut the earliest-finish rule in order.1. Sort the meetings by when they end 2. Take the first meeting in that order 3. Remember when the room becomes free 4. Skip each meeting that starts before then 5. Take the next one that starts after, and repeatSort once, then a single pass that takes or skips each meeting. Because the meetings come by end time, the first one that fits is always the one that finishes soonest, which leaves the most room for the rest.
  114. 14. Greedy algorithms · Why the earliest finish is always right · QuizTry the exchange argument on “shortest meeting first”. Where does it break, with meetings 1–5, 4–7 and 6–10?B. Swapping a best schedule’s meeting for the shortest one, 4–7, can create clashes: 4–7 overlaps both 1–5 and 6–10The best schedule is 1–5 and 6–10. Swapping either for 4–7 leaves a clash with the other, so the swap makes things worse, and the argument fails. That failure points at the counterexample. With earliest finish, the swap is always safe because the greedy meeting ends no later than the one it replaces.
  115. 14. Greedy algorithms · Try it: cancel as few meetings as possible · Code challengeFinish fewestToCancel(meetings): return how many meetings must go so that the rest never overlap. A meeting may start exactly when another ends.One solution:
    Solution
    #include <algorithm>#include <limits>#include <vector>
    struct Meeting { int start; int end; };
    int fewestToCancel(std::vector<Meeting> meetings) {  std::sort(meetings.begin(), meetings.end(),            [](const Meeting& a, const Meeting& b) {              return a.end < b.end;            });  int kept = 0;  int freeAt = std::numeric_limits<int>::min();  for (auto [start, end] : meetings) {    if (start >= freeAt) {      kept++;      freeAt = end;    }  }  return int(meetings.size()) - kept;}
    Keep the most meetings with the earliest-finish rule, then cancel everything else: meetings.size() - kept. Sorting by start time is the tempting mistake: it keeps 1–10 first and then has to cancel the three short meetings. Using >= rather than > lets back-to-back meetings both stay.
  116. 14. Greedy algorithms · Try it: cancel as few meetings as possible · Find the bugThis compiles and runs, but sometimes keeps too few meetings. Which line is wrong?Line 4: return a.end < b.end;Comparing start sorts by start time, so this is the earliest-start rule, which grabs a long early meeting and loses the short ones inside it. Sorting by end is the rule the exchange argument proves right. Everything else in the function is fine, which is what makes this bug hard to see.
  117. 14. Greedy algorithms · Packing a bag: greedy by value per kilo · QuizNow sacks cannot be split: take a whole sack or leave it (the 0/1 knapsack). Same bag of 50, same spices. What does greedy by value per kilo give, and what is the best?B. Greedy 160 (saffron and vanilla), best 220 (vanilla and pepper)Greedy takes saffron and vanilla (30 kilos, 160), and then pepper does not fit, so 20 kilos of space are wasted. Vanilla and pepper fill the bag exactly for 220. Being able to split was what made greedy safe: without it, the problem needs dynamic programming (module 16).
  118. 14. Greedy algorithms · Packing a bag: greedy by value per kilo · Code challengeFinish bestLoad(items, capacity): return the most value that fits in the bag, when any part of an item may be taken.One solution:
    Solution
    #include <algorithm>#include <vector>
    struct Item { int weight; int value; };
    double rate(const Item& item) {  return double(item.value) / item.weight;}
    double bestLoad(std::vector<Item> items, int capacity) {  std::sort(items.begin(), items.end(),            [](const Item& a, const Item& b) {              return rate(a) > rate(b);            });  int room = capacity;  double total = 0;  for (const Item& item : items) {    int take = std::min(item.weight, room);    total += take * rate(item);    room -= take;  }  return total;}
    Sort by value per kilo, highest first, then take std::min(item.weight, room) of each: the whole item while it fits, then the part that does, then 0 for the rest. Sorting by value alone takes pepper (120) first and wastes the bag on cheap spice. Watch the division: item.value / item.weight on two ints drops the fraction, so 15 / 2 would be 7, the same as 14 / 2, and the last test would fail. Turning one side into a double first keeps it. EXPECT_NEAR is used because decimals are not always exact in floating point.
  119. 15. Dynamic programming · Count the calls to see the waste · QuizHow many times is fib called, in total, to work out fib(5) with the code above?C. 15Count the calls with the same recursion: calls(0) and calls(1) are 1; calls(n) is 1 plus calls(n - 1) plus calls(n - 2). That gives 1, 1, 3, 5, 9, 15. The count grows like the Fibonacci numbers themselves, about 1.6 times for each step of n: exponential time.
  120. 15. Dynamic programming · Memoisation: remember every answer · Fill the blanksMemoise a function that counts paths. Look up first, store before returning.1: contains, 2: n, 3: resultmemo.contains(n) (C++20) asks whether the answer is known, and only then is memo[n] read. That order matters: memo[n] on a key that is not there quietly adds it with the value 0, so using it to look up would find a 0 for every new question. memo[n] = result stores the new answer. Forgetting to store is the classic slip: the code stays correct but is exactly as slow as before.
  121. 15. Dynamic programming · Bottom-up: fill a table from the smallest case · Find the bugThis keeps only two values instead of a table, but fib(3) gives 3, not 2. Which line is wrong?Line 9: return prev;The comments say what each variable holds: after the loop has run n times, prev is fib(n) and curr is fib(n + 1), one step ahead. Returning curr is off by one. Writing down what each variable means at every step (an invariant) is how you catch this without guessing.
  122. 15. Dynamic programming · Climbing stairs: count the ways · Code challengeFinish ways(n, steps): how many orders of moves climb exactly n stairs, when each move is one of the sizes in steps? Fill a table from 0 up.One solution:
    Solution
    #include <vector>
    long long ways(int n, const std::vector<int>& steps) {  std::vector<long long> table(n + 1, 0);  table[0] = 1;  for (int i = 1; i <= n; i++)    for (int step : steps)      if (step <= i) table[i] += table[i - step];  return table[n];}
    The last move was one of the sizes in steps, so table[i] sums table[i - step] over every size that fits. Without table[0] = 1 every entry stays 0: the base case is what the sums are built from. With sizes 1 and 2 the table is Fibonacci; with 1, 2 and 3, each entry sums the three before it. O(n × steps) time. The count for 80 stairs is about 3.8 × 10¹⁶, which only fits because the table holds long long.
  123. 15. Dynamic programming · Coin change: how many ways to pay? · QuizWhat do the two loop orders count, for paying 3 with coins 1 and 2?B. A gives 2 (sets of coins), B gives 3 (orders: 1+2 and 2+1 apart)In A, a coin joins only after the smaller ones are all counted, so each combination is built in one order: 1+1+1 and 1+2. In B, every amount tries every coin as the last one, which is exactly the stairs problem: 1+1+1, 1+2 and 2+1. Same recurrence, different loop order, different question answered.
  124. 15. Dynamic programming · Coin change: how many ways to pay? · Code challengeFinish countWays(amount, coins): the number of different sets of coins that pay amount exactly (order does not matter).One solution:
    Solution
    #include <vector>
    long long countWays(int amount, const std::vector<int>& coins) {  std::vector<long long> ways(amount + 1, 0);  ways[0] = 1;  for (int coin : coins)    for (int a = coin; a <= amount; a++)      ways[a] += ways[a - coin];  return ways[amount];}
    Coins outside, amounts inside: that order is what counts sets rather than orders. Starting a at coin skips amounts the coin cannot help with. Counting upwards inside one coin is right here, because ways[a - coin] may already include this coin, which is how a set gets two or more of it. O(amount × coins) time, O(amount) memory.
  125. 15. Dynamic programming · Coin change: the fewest coins, for any coins · Code challengeFinish fewestCoins(amount, coins): the fewest coins that pay amount exactly, or -1 if it cannot be done. INF is ready for “not payable yet”.One solution:
    Solution
    #include <algorithm>#include <vector>
    const int INF = 1000000000;   // more coins than any real answer
    int fewestCoins(int amount, const std::vector<int>& coins) {  std::vector<int> fewest(amount + 1, INF);  fewest[0] = 0;  for (int a = 1; a <= amount; a++)    for (int coin : coins)      if (coin <= a)        fewest[a] = std::min(fewest[a], fewest[a - coin] + 1);  return fewest[amount] == INF ? -1 : fewest[amount];}
    fewest[0] = 0 is the base case; every other amount starts at INF (“not payable yet”), so std::min replaces it with the first real answer. INF is chosen with care: bigger than any real count of coins, yet small enough that INF + 1 still fits in an int. std::numeric_limits<int>::max() would overflow on that + 1 (undefined behaviour, which in practice gives a large negative number), and std::min would then pick it. Here loop order does not matter: the minimum over every last coin is the same either way. O(amount × coins).
  126. 15. Dynamic programming · Top-down or bottom-up · Put in orderPut the steps of designing a DP solution in order.1. Say in words what one subproblem is: “fewest coins to pay a” 2. Write the recurrence: the answer from smaller answers 3. Fix the base cases: fewest[0] = 0 4. Pick an order (or memoise) so smaller answers are ready first 5. Read the answer from the table, and rebuild the choices if neededEverything follows from the first step: once a subproblem is named precisely, the recurrence is usually “think about the last choice”. Base cases stop it, the order makes the table fillable, and the final entry is the answer. Most DP bugs are a vague first step.
  127. 16. Dynamic programming in two dimensions · Counting paths through a grid · QuizAdd a fourth row (row 3) to the table above. What goes in its last cell, the bottom-right corner of a 4 by 4 grid?C. 20Row 3 starts at 1 and each cell adds the cell above and the one on its left: 1, 1 + 3 = 4, 4 + 6 = 10, 10 + 10 = 20. A shortcut for open grids: a route is 3 rights and 3 downs in some order, and there are 6!/(3!·3!) = 20 ways to arrange them. The table still wins as soon as some corners are blocked.
  128. 16. Dynamic programming in two dimensions · Filling the grid row by row · Code challengeFinish countPaths(grid): grid[r][c] is 1 for a wall and 0 for open ground. Count the routes from the top-left to the bottom-right moving only right or down, never through a wall. A wall on the start or the end means 0 routes.One solution:
    Solution
    #include <vector>
    using Grid = std::vector<std::vector<int>>;
    long long countPaths(const Grid& grid) {  int rows = grid.size(), cols = grid[0].size();  std::vector<std::vector<long long>> ways(      rows, std::vector<long long>(cols, 0));  for (int r = 0; r < rows; r++)    for (int c = 0; c < cols; c++) {      if (grid[r][c] == 1) continue;      if (r == 0 && c == 0) {        ways[r][c] = 1;        continue;      }      long long up = r > 0 ? ways[r - 1][c] : 0;      long long left = c > 0 ? ways[r][c - 1] : 0;      ways[r][c] = up + left;    }  return ways[rows - 1][cols - 1];}
    A wall keeps its 0, and every cell after it adds that 0, so routes through it simply vanish. Taking up and left as 0 when they are off the grid handles the top row and the left column without a separate rule, which also matters with walls: a top-row cell after a wall must get 0, not the 1 of the open-grid shortcut. The start cell is the only base case.
  129. 16. Dynamic programming in two dimensions · The longest common subsequence · Fill the blanksComplete the rule for one cell.1: ==, 2: 1, 3: maxA match extends the best answer for both prefixes without their last letters, by one. Otherwise one of the two last letters is not in the answer, so take the larger of “drop the last letter of a” (dp[i - 1][j]) and “drop the last letter of b” (dp[i][j - 1]). Note a[i - 1]: row i is about the first i letters, and the ith letter sits at index i - 1.
  130. 16. Dynamic programming in two dimensions · From exponential to a table · Code challengeFinish lcsLength(a, b) with the table: fill dp[i][j] for every i from 1 and j from 1, using the rule from the slide before, and return the bottom-right cell.One solution:
    Solution
    #include <algorithm>#include <string>#include <vector>
    int lcsLength(const std::string& a, const std::string& b) {  int n = a.size(), m = b.size();  std::vector<std::vector<int>> dp(n + 1, std::vector<int>(m + 1, 0));  for (int i = 1; i <= n; i++)    for (int j = 1; j <= m; j++) {      if (a[i - 1] == b[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;      else dp[i][j] = std::max(dp[i - 1][j], dp[i][j - 1]);    }  return dp[n][m];}
    Row 0 and column 0 stay 0 (an empty prefix shares nothing), which is why the loops start at 1. Each cell reads only the cell up-left, up, and left, all filled already in row-by-row order. A common slip is comparing a[i] with b[j], which is off by one, and quietly so: a[a.size()] on a std::string is its hidden end marker \0, not a crash, so the code just compares the wrong letters. Time and memory are O(n × m): four million cells for the last test.
  131. 16. Dynamic programming in two dimensions · Edit distance: how far apart two words are · Find the bugThis says the distance from abc to an empty word is 0. Which line is wrong?Line 4: for (int i = 0; i <= n; i++) dp[i][0] = i;Turning the first i letters into the empty word takes i deletions, so column 0 must count up 0, 1, 2… just as row 0 does. With zeros there, every word looks free to delete, and the error spreads to every cell built from that column. Base cases are where most table bugs hide: check them on the smallest inputs first. (std::min({x, y, z}), with braces, takes the smallest of a whole list.)
  132. 16. Dynamic programming in two dimensions · The 0/1 knapsack: packing a bag · Fill the blanksComplete the knapsack loop.1: capacity, 2: weight, 3: maxTaking the item uses weight of the room, so the rest comes from the row above at c - weight: the row above, because each item can be used only once. When it does not fit, take is 0, which never beats skip, and c - weight is never read, which matters: a negative index would be out of bounds. The answer is best[items.size()][capacity].
  133. 16. Dynamic programming in two dimensions · The 0/1 knapsack: packing a bag · QuizA greedy packer takes the most valuable item first, then the next that still fits. With the 4 kg bag above, how does it do?B. It takes the camera and stops at 30The camera (30) goes in first and fills the bag, so nothing else fits. The table tries both “take” and “skip” for every item and room, which is why it finds map plus water for 35. Greedy choices are right for some problems (the greedy module showed when), and the 0/1 knapsack is not one of them.
  134. 16. Dynamic programming in two dimensions · Reading the answer back from the table · Code challengeFinish lcs(a, b), which returns the longest common subsequence itself. table(a, b) already fills the LCS table. Walk back from dp[a.size()][b.size()]: on matching letters, collect the letter and go up-left; otherwise move towards the larger of the cell above and the cell on the left.One solution:
    Solution
    #include <algorithm>#include <string>#include <vector>
    using Table = std::vector<std::vector<int>>;
    Table table(const std::string& a, const std::string& b) {  Table dp(a.size() + 1, std::vector<int>(b.size() + 1, 0));  for (size_t i = 1; i <= a.size(); i++)    for (size_t j = 1; j <= b.size(); j++)      dp[i][j] = a[i - 1] == b[j - 1]                     ? dp[i - 1][j - 1] + 1                     : std::max(dp[i - 1][j], dp[i][j - 1]);  return dp;}
    std::string lcs(const std::string& a, const std::string& b) {  Table dp = table(a, b);  std::string out;  size_t i = a.size(), j = b.size();  while (i > 0 && j > 0) {    if (a[i - 1] == b[j - 1]) {      out.push_back(a[i - 1]);      i--;      j--;    } else if (dp[i - 1][j] >= dp[i][j - 1]) i--;    else j--;  }  std::reverse(out.begin(), out.end());  return out;}
    Each step undoes the choice that filled the cell: a match came from up-left plus that letter, anything else came from whichever neighbour was larger. Letters are found last to first, so collect them with push_back and reverse once at the end. Putting each one on the front with out.insert(0, …) also works, but moves every letter already there, O(n²) in all. When up and left tie, either way gives a longest answer, though possibly a different one. Reading back costs only O(n + m) steps on top of the table.
  135. 16. Dynamic programming in two dimensions · Saving memory with one row · QuizWith one row and only the map (weight 1, value 15), what does the bag of room 4 end up worth if the loop goes up, from c = 1 to 4?C. 60Going up, best[c - 1] already includes the map, so each step packs it again: 15, 30, 45, 60. That is the answer to a different problem, with unlimited copies of each item (like the coins in coin change). Going down from 4, best[c - 1] is still the old 0, and the bag is worth 15.
  136. 16. Dynamic programming in two dimensions · Saving memory with one row · Code challengeFinish bestValue(items, capacity) for the 0/1 knapsack using only the one row best, where best[c] is the most value with room c. Each item can be packed at most once.One solution:
    Solution
    #include <algorithm>#include <vector>
    struct Item { int weight; int value; };
    int bestValue(const std::vector<Item>& items, int capacity) {  std::vector<int> best(capacity + 1, 0);  for (const Item& item : items) {    for (int c = capacity; c >= item.weight; c--) {      int take = best[c - item.weight] + item.value;      best[c] = std::max(best[c], take);    }  }  return best[capacity];}
    The inner loop runs from capacity down to the item’s weight, so best[c - item.weight] is still the value without this item, and stopping at the weight also skips rooms it does not fit. Loop upwards instead and the map is packed again and again: the bag of 4 comes out at 60, not 35. The time is still O(n × capacity).
  137. 17. Backtracking · Subsets: take it or leave it · QuizHow many subsets does a list of 10 items have?D. 1024Each item doubles the count, because every subset of the rest comes once with it and once without: 2 × 2 × … ten times is 2¹⁰ = 1024, including the empty one and the whole list. Listing them all takes O(2ⁿ × n) time (each saved copy is up to n long), which is why backtracking over all subsets is only for small n, around 20 to 25 at most.
  138. 17. Backtracking · Choosing k of n · Code challengeFinish explore so that choose(items, k) returns every way to pick k of the letters in items, each in the order the letters come, with the picks in that order too. Use chosen with push_back, recurse and pop_back.One solution:
    Solution
    #include <string>#include <vector>
    using Lists = std::vector<std::string>;
    void explore(const std::string& items, size_t k, size_t start,             std::string& chosen, Lists& result) {  if (chosen.size() == k) {    result.push_back(chosen);    return;  }  for (size_t i = start; i < items.size(); i++) {    chosen.push_back(items[i]);    explore(items, k, i + 1, chosen, result);    chosen.pop_back();  }}
    Lists choose(const std::string& items, size_t k) {  std::string chosen;  Lists result;  explore(items, k, 0, chosen, result);  return result;}
    The loop starts at start, and the recursive call passes i + 1, so every pick goes forward through the items and no combination is listed twice in another order. Passing start + 1 instead is the classic slip: the next call may pick item i again, or one before it, so "bb" and "cb" turn up. Forgetting the & on chosen would still give right answers, but every call would copy it.
  139. 17. Backtracking · Permutations: every order · Find the bugFor "abc" this finds only {"abc"}, not all six orders. Which line is wrong?Line 13: used[i] = false;Lines 12 and 13 are the undo, and both must put things back as they were. Line 13 seats the guest again instead of un-seating them, so after the first full row every letter stays marked as used and no other branch can place anything. Line 4 is fine in C++: push_back copies current. (In languages where it would store a reference to the one shared partial solution, such as JavaScript or Python, that line is the classic bug: every saved answer ends up empty.)
  140. 17. Backtracking · Try it: every arrangement of a word · Code challengeFinish explore so that arrangements(word) returns every ordering of the word's letters, in the order the search tree finds them (first letter first). The letters are all different.One solution:
    Solution
    #include <string>#include <vector>
    using Lists = std::vector<std::string>;
    void explore(const std::string& word, std::vector<bool>& used,             std::string& current, Lists& result) {  if (current.size() == word.size()) {    result.push_back(current);    return;  }  for (size_t i = 0; i < word.size(); i++) {    if (used[i]) continue;    used[i] = true;    current.push_back(word[i]);    explore(word, used, current, result);    current.pop_back();    used[i] = false;  }}
    Lists arrangements(const std::string& word) {  std::vector<bool> used(word.size(), false);  std::string current;  Lists result;  explore(word, used, current, result);  return result;}
    The undo is current.pop_back() plus used[i] = false: forget the second and only "abc" is found, because every letter stays marked as used. The last test puts every answer into a std::set, which keeps one copy of each, to check none is repeated. There are n! arrangements, so 10 letters already give over 3.6 million.
  141. 17. Backtracking · Choices that add up to a target · Fill the blanksComplete the pruning line and the recursive call.1: left, 2: break, 3: i + 1break (not continue) ends the whole loop: the list is sorted, so nothing after a too-big number can fit. Passing i + 1 means each number is used at most once and later numbers only, so [2, 5] and [5, 2] are not both found. Passing i instead would allow the same number again, as in coin change.
  142. 17. Backtracking · Choices that add up to a target · Code challengeFinish explore so that sumsTo(nums, target) returns every list of numbers from nums (distinct and positive, each used at most once) that adds up to target, each list in ascending order. The order of the lists does not matter.One solution:
    Solution
    #include <algorithm>#include <vector>
    using Nums = std::vector<int>;
    void explore(const Nums& sorted, size_t start, int left,             Nums& chosen, std::vector<Nums>& result) {  if (left == 0) {    result.push_back(chosen);    return;  }  for (size_t i = start; i < sorted.size(); i++) {    if (sorted[i] > left) break;    chosen.push_back(sorted[i]);    explore(sorted, i + 1, left - sorted[i], chosen, result);    chosen.pop_back();  }}
    std::vector<Nums> sumsTo(Nums nums, int target) {  std::sort(nums.begin(), nums.end());  Nums chosen;  std::vector<Nums> result;  explore(nums, 0, target, chosen, result);  return result;}
    Counting down what is left makes both checks cheap: 0 is a success, and a number bigger than left ends the loop. The numbers come from the sorted vector, so each list is already ascending. sumsTo takes nums by value, so it sorts its own copy and the caller’s vector is left alone. The tests sort the lists before comparing (vectors compare item by item, like words in a dictionary), so any order of answers passes.
  143. 17. Backtracking · Pruning: the N-queens puzzle · QuizQueens at (row 0, column 1) and (row 2, column 3): std::abs(1 - 3) == 2 - 0 is true. Are they safe from each other?B. No: they are on the same diagonalTwo squares are on one diagonal when they are as many columns apart as rows apart: here 2 and 2. The check is true, so safe returns false and the square is rejected. A queen between them would block the attack in a real game, but it would itself share that diagonal with both, so a board where no two queens attack each other never has one there.
  144. 17. Backtracking · Try it: find a word in a letter grid · Code challengeFinish hasWord(grid, word): return true if word can be traced through neighbouring squares, using each square at most once. Mark a square by setting it to '#' while on the path, and put the letter back afterwards, so the grid is unchanged at the end. A helper like from(grid, word, r, c, i), “can the word from letter i on start at this square?”, is a good start.One solution:
    Solution
    #include <string>#include <vector>
    using Grid = std::vector<std::string>;
    bool from(Grid& g, const std::string& word, int r, int c, size_t i) {  if (i == word.size()) return true;  if (r < 0 || r >= int(g.size())) return false;  if (c < 0 || c >= int(g[r].size())) return false;  if (g[r][c] != word[i]) return false;  g[r][c] = '#';  bool found = from(g, word, r + 1, c, i + 1) ||               from(g, word, r - 1, c, i + 1) ||               from(g, word, r, c + 1, i + 1) ||               from(g, word, r, c - 1, i + 1);  g[r][c] = word[i];  return found;}
    bool hasWord(Grid& grid, const std::string& word) {  for (int r = 0; r < int(grid.size()); r++)    for (int c = 0; c < int(grid[r].size()); c++)      if (from(grid, word, r, c, 0)) return true;  return false;}
    The bounds checks come first, so a step off the edge returns false before g[r][c] is ever read. After that, one comparison covers wrong letters and squares already marked '#'. The grid is taken by reference without const, because marking changes it; restoring the letter before returning is what makes a square available again to other paths, and what leaves the caller's grid as it was. || stops at the first neighbour that works, so the search ends as soon as the word is found.
  145. 17. Backtracking · When to reach for backtracking · Put in orderPut one call of a backtracking search in order.1. If the partial solution is complete, save a copy and return 2. Loop over the choices still allowed here 3. Skip a choice that breaks a rule (prune) 4. Make the choice: add it to the partial solution 5. Recurse to make the next choices 6. Undo the choice before trying the nextThis is the whole pattern, whatever the puzzle. The complete check comes first, so a finished solution is not extended. Pruning goes before the choice, so no work is spent on a dead end. And the undo comes after the recursion returns, so every sibling choice starts from the same partial solution.
  146. 17. Backtracking · When to reach for backtracking · QuizWhich of these are best done with backtracking? Pick every one.A. Print every way to seat 6 guests around a table; C. Fill in a SudokuPrinting every seating means visiting every answer, so nothing is faster than generating them. Sudoku is “any answer that meets the rules”, with strong pruning. Counting stairs and edit distance have overlapping subproblems and need only a number: as a backtracking search they take exponential time, as a table only a few steps per cell.
  147. 18. Minimum spanning trees · Connect every town with the least cable · QuizA spanning tree connects 5 towns. How many edges does it have?B. 4Start with 5 separate towns. Each edge that does not make a cycle joins two separate groups into one, so it lowers the number of groups by exactly one. Going from 5 groups to 1 takes 4 edges. Any tree on n vertices has n - 1 edges; a fifth edge would always close a loop.
  148. 18. Minimum spanning trees · Trace it: a table of decisions · Put in orderPut the steps of Kruskal’s algorithm in order.1. Put every vertex in a group of its own 2. Sort the edges from cheapest to dearest 3. Take the next edge in the list 4. If its ends are in different groups, keep the edge and merge the groups 5. Stop once you have kept n - 1 edgesGroups start as single vertices, and the sorted list is walked once. The “different groups” test is what keeps loops out, and n - 1 kept edges means everything is one group, so the rest of the list can be ignored.
  149. 18. Minimum spanning trees · Union-find answers “same group?” fast · Fill the blanksComplete Kruskal’s loop using the UnionFind from above.1: <, 2: unite, 3: costx.cost < y.cost says “x comes first when it is cheaper”: cheapest first. unite merges and returns true only when the ends were in different groups, so the edge is kept and its cost counted; when it returns false the edge would make a loop and is skipped.
  150. 18. Minimum spanning trees · Kruskal in C++ · Find the bugThis runs, and always adds up the cost of a spanning tree, but not the cheapest one. Which line is wrong?Line 4: return x.cost < y.cost;x.cost > y.cost sorts dearest first, so the loop builds a maximum spanning tree: still connected and loop-free, which is why it looks right. The sort order is the whole of Kruskal’s greed. (A maximum spanning tree is a real tool too, for finding the most reliable links, but it is not what was asked.)
  151. 18. Minimum spanning trees · Try it: the cost of a minimum spanning tree · Code challengeFinish mstCost(n, edges): the total cost of a minimum spanning tree of vertices 0 to n - 1. The graph is connected.One solution:
    Solution
    #include <algorithm>#include <vector>
    struct Edge { int from; int to; int cost; };
    struct UnionFind {  std::vector<int> parent;   // parent[i] == i means i is a root
      UnionFind(int n) : parent(n) {    for (int i = 0; i < n; i++) parent[i] = i;  }  int find(int x) {    while (parent[x] != x) {      parent[x] = parent[parent[x]];   // skip a step next time      x = parent[x];    }    return x;  }};
    long long mstCost(int n, const std::vector<Edge>& edges) {  UnionFind groups(n);  std::vector<Edge> sorted = edges;  std::sort(sorted.begin(), sorted.end(),            [](const Edge& x, const Edge& y) {              return x.cost < y.cost;            });  long long total = 0;  for (auto [a, b, cost] : sorted) {    int ra = groups.find(a), rb = groups.find(b);    if (ra == rb) continue;    groups.parent[ra] = rb;    total += cost;  }  return total;}
    std::vector<Edge> sorted = edges; makes the copy to sort; trying std::sort(edges.begin(), …) on the const& does not compile, which is const doing its job. The test ra == rb is what stops a loop: in the triangle, the road of cost 3 finds both ends already under one root. Forgetting groups.parent[ra] = rb is the classic slip: every edge then looks like it joins two groups, and the triangle costs 6. The sort is the cost, O(E log E): about 200 000 roads here, done in a blink.
  152. 18. Minimum spanning trees · Try it: Prim on a table of costs · Code challengeFinish primCost(cost). Each round, pick the town not in the tree with the smallest best, add it (and its best to total), then lower best[v] to cost[picked][v] for every town outside the tree where that is cheaper.One solution:
    Solution
    #include <algorithm>#include <vector>
    const int INF = 1000000000;   // no roadusing Table = std::vector<std::vector<int>>;
    long long primCost(const Table& cost) {  int n = cost.size();  std::vector<bool> inTree(n, false);  std::vector<int> best(n, INF);  best[0] = 0;  long long total = 0;  for (int round = 0; round < n; round++) {    int pick = -1;    for (int v = 0; v < n; v++)      if (!inTree[v] && (pick == -1 || best[v] < best[pick])) pick = v;    inTree[pick] = true;    total += best[pick];    for (int v = 0; v < n; v++)      if (!inTree[v]) best[v] = std::min(best[v], cost[pick][v]);  }  return total;}
    Town 0 starts with best 0, so the first round picks it for free; every later pick adds exactly one edge. The inTree check in the picking loop is what stops a town being added twice; a common slip is to leave it out, and the cheapest town (town 0, at 0) is picked every round. Lowering best only outside the tree just saves work. Two nested loops of n make this O(V²), which beats a heap when nearly every pair is connected. total is a long long and best never has INF added to it, so nothing overflows.
  153. 18. Minimum spanning trees · What a spanning tree does and does not promise · QuizRoads: A–B costs 2, B–C costs 2, A–C costs 3. In the minimum spanning tree, how far is it from A to C?B. 4, through BThe MST is A–B and B–C (total 4); the road A–C (3) is left out because it is the dearest edge of the triangle. So travelling A to C inside the tree costs 4, though the shortest path in the full graph is 3. An MST minimises the total cable, not any one trip; for trips you want Dijkstra.
  154. 19. Solving problems you have never seen · A method beats a flash of insight · Put in orderYou are given a problem you have never seen. Put the steps in order.1. Restate it and note the input sizes 2. Solve small examples by hand 3. Write a brute-force solution 4. Find the step that repeats work 5. Replace it with a known technique 6. Compare the fast solution with the brute force on many inputsUnderstanding comes first because a fast solution to the wrong problem is worth nothing. Brute force comes before cleverness: it is quick to write, it shows you where the waste is, and it stays useful at the end as the oracle that checks the fast one.
  155. 19. Solving problems you have never seen · The input size tells you how fast to be · QuizYou must choose which of up to 18 parcels to pack in a van, and the brute force tries every choice. Is that fast enough?B. Yes: 2¹⁸ is about 262 000 choicesEach parcel is in or out, so there are 2¹⁸ = 262 144 choices; even at a few dozen steps each, that is a few million steps, well under a second. Brute force is a fine answer when the input is small. At 60 parcels, 2⁶⁰ would never finish, and you would need DP or greedy.
  156. 19. Solving problems you have never seen · From brute force to fast: the best run of days · QuizWhat does bestRun return?A. 6here goes 2, −3, 3 (starting afresh beats −3 + 3 = 0), 2, 6, 0; the largest is 6, from the run 3, −1, 4. The −1 is worth keeping because 4 follows it. Restarting at each new day whenever the past is a burden is the whole idea.
  157. 19. Solving problems you have never seen · Clues that point to a technique · QuizOn a chessboard with some squares blocked, find the fewest moves a knight needs from one square to another. Which technique fits?B. Breadth-first search“Fewest moves” where every move counts the same is the BFS clue: squares are vertices, legal moves are edges, and BFS reaches squares in order of moves. Greedy fails because a knight often has to move away to get closer. Dijkstra works but is overkill when every edge weighs 1. DP struggles because moves can go back and forth, so there is no order of smaller subproblems.
  158. 19. Solving problems you have never seen · Check the fast answer against the slow one · Code challengeWrite firstMismatch(fast, slow, inputs): return the first input on which the two functions give different results, or std::nullopt if they agree on all.One solution:
    Solution
    #include <optional>#include <vector>
    template <typename In, typename Fast, typename Slow>std::optional<In> firstMismatch(Fast fast, Slow slow,                                const std::vector<In>& inputs) {  for (const In& input : inputs)    if (fast(input) != slow(input)) return input;  return std::nullopt;}
    One loop and one comparison: C++’s != on two std::vectors compares their contents, item by item, so sorted lists compare as you would hope with no extra work. The template lets the same function check ints and whole lists. The last test is a real bug an oracle finds: putting values in a std::set sorts them, but also keeps only one of each, so any list with a repeated value comes out short. A thousand random lists with a fixed seed find one at once.
  159. 19. Solving problems you have never seen · Edge cases are where solutions break · Find the bugThis passes every test with a profitable day in it. On which line does it fail for a list of all losses?Line 2: int best = std::numeric_limits<int>::min();For {-3, -1} the right answer is −1 (the least bad day), but best starts at 0, a run of no days that the problem does not allow, and nothing negative ever beats it. Starting at the smallest int (or at xs[0]) fixes it. here starting at 0 is harmless, because std::max(x, 0 + x) is just x. An oracle with negative numbers finds this in a few rounds.
  160. 19. Solving problems you have never seen · Amortised cost: expensive now and then, cheap on average · Fill the blanksComplete the growth step so pushes are O(1) amortised.1: 2, 2: slotsDoubling (any constant factor above 1 works) makes the copies a geometric series that sums to less than 2n. Growing by a fixed amount instead, say 10 slots each time, copies 10 + 20 + 30 + … which is O(n²) over n pushes. (new int[capacity] asks the heap for a row of capacity ints and delete[] gives it back: the array forms of the new and delete from the Data structures course. std::vector does all of this for you.)
  161. 19. Solving problems you have never seen · Try it: days until a warmer day · Code challengeFinish daysUntilWarmer(temps) with a stack of day numbers, so that it handles 100 000 days in time. {30, 31, 29, 33} gives {1, 2, 1, 0}.One solution:
    Solution
    #include <vector>
    std::vector<int> daysUntilWarmer(const std::vector<int>& temps) {  std::vector<int> wait(temps.size(), 0);  std::vector<int> stack;   // days still waiting, as a stack  for (int day = 0; day < int(temps.size()); day++) {    while (!stack.empty() && temps[stack.back()] < temps[day]) {      int cold = stack.back();      stack.pop_back();      wait[cold] = day - cold;    }    stack.push_back(day);  }  return wait;}
    Each day is pushed once and popped at most once, so all the while loops together run at most n times: O(n) overall, though one day can pop many. The comparison must be strict (<): an equally warm day is not warmer, and the second test catches <=. A std::vector makes a fine stack: push_back, back and pop_back all work at its end, O(1). The brute force on the last test would look ahead about 5 billion times and run out of time; the stack does about 200 000 steps.
  162. 19. Solving problems you have never seen · Choosing between correct solutions · QuizCheck a list of up to 10 million IDs for duplicates. The list must not change, and memory is plentiful. Which is the best choice?B. Put them in a std::unordered_set and compare sizes: O(n) time, O(n) memoryTen million squared is far too many steps, so pairs are out. Sorting in place breaks “the list must not change” (sorting a copy works, at O(n log n)). Binary search needs a sorted list in the first place. With memory to spare, std::unordered_set<int>(ids.begin(), ids.end()).size() != ids.size() is linear (on average), short and obviously right.
  163. 20. Mastery challenge · How this challenge works · Put in orderPut the method for an unseen problem in order.1. Restate the problem, its inputs and its limits 2. Work two or three small examples by hand 3. Write a brute force you are sure is right 4. Find the step that makes it slow 5. Match a pattern that removes that step 6. Test the fast version against the brute forceUnderstanding comes before code, and a correct slow answer before a fast one: the brute force is both a fallback and the oracle that checks the clever version. The bottleneck tells you which pattern to reach for: a repeated search suggests hashing, repeated subproblems suggest dynamic programming. (Problem solving.)
  164. 20. Mastery challenge · How this challenge works · QuizHow many times does the loop body run?A. 9Dividing two ints drops the remainder, so i takes the values 1000, 500, 250, 125, 62, 31, 15, 7 and 3, then becomes 1 and the loop stops: 9 runs. Halving each time gives about log₂ n steps (log₂ 1000 is just under 10), the same count that makes binary search fast. (Counting steps; binary search.)
  165. 20. Mastery challenge · Name the technique · QuizFind the length of the longest stretch of a string with no letter repeated. Which approach is O(n)?B. A sliding window with a set of the letters inside it (std::unordered_set<char>)Grow the window on the right; when the new letter is already inside, shrink it from the left until it is not. Each letter enters and leaves the window at most once, so the work is O(n) overall. Trying every stretch is O(n²) stretches, each checked in O(n). Sorting destroys the order the answer depends on. (Two pointers and sliding windows; counting and hashing.)
  166. 20. Mastery challenge · Name the technique · QuizCoins are worth 1, 5, 21 and 25. What is the fewest coins for 63, and what finds it reliably?C. 3 coins, by dynamic programming over the amounts 0 to 63Greedy takes 25, 25, then 5, 5, 1, 1, 1: 7 coins. But 21 + 21 + 21 is 3. Greedy is only right for coin systems where an exchange argument holds, and this one is not such a system. Dynamic programming tries every last coin for every amount, so it cannot miss: best[a] = 1 + min(best[a - c]). (Greedy; dynamic programming.)
  167. 20. Mastery challenge · Write it: count the islands · Code challengeFinish islands(grid): return how many islands the map has. Each string in grid is one row. The grid is your own copy (it is taken by value), so you may change it.One solution:
    Solution
    #include <stack>#include <string>#include <utility>#include <vector>
    int islands(std::vector<std::string> grid) {  const int dy[] = {1, -1, 0, 0}, dx[] = {0, 0, 1, -1};  int rows = grid.size(), count = 0;  for (int r = 0; r < rows; r++)    for (int c = 0; c < int(grid[r].size()); c++) {      if (grid[r][c] != '#') continue;      count++;      grid[r][c] = '.';   // sink it: seen      std::stack<std::pair<int, int>> stack;      stack.push({r, c});      while (!stack.empty()) {        auto [y, x] = stack.top();        stack.pop();        for (int k = 0; k < 4; k++) {          int ny = y + dy[k], nx = x + dx[k];          if (ny < 0 || ny >= rows) continue;          if (nx < 0 || nx >= int(grid[ny].size())) continue;          if (grid[ny][nx] != '#') continue;          grid[ny][nx] = '.';          stack.push({ny, nx});        }      }    }  return count;}
    Scan every square; each land square not yet seen starts a new island, and a flood fill (depth first with a stack here; breadth first works too) marks the whole island so it is not counted again. Here the mark is to sink the land, turning # into .: the grid is a copy, so the caller never sees it. Every square is pushed at most once, so it is O(rows × columns). Marking a square when it is pushed, not when it is popped, keeps it from being pushed twice. The bounds checks come before grid[ny][nx] is read. (Graph search: grids and flood fill.)
  168. 20. Mastery challenge · Write it: the shortest word ladder · Code challengeFinish ladder(start, goal, words): return the number of words in the shortest ladder from start to goal, counting both ends, using only words in words after start. Return 0 if there is none. oneApart is written for you.One solution:
    Solution
    #include <string>#include <vector>
    bool oneApart(const std::string& a, const std::string& b) {  if (a.size() != b.size()) return false;  int diff = 0;  for (size_t i = 0; i < a.size(); i++)    if (a[i] != b[i]) diff++;  return diff == 1;}
    int ladder(const std::string& start, const std::string& goal,           const std::vector<std::string>& words) {  std::vector<bool> queued(words.size(), false);  std::vector<std::string> level = {start};  for (int length = 1; !level.empty(); length++) {    std::vector<std::string> next;    for (const std::string& word : level) {      if (word == goal) return length;      for (size_t k = 0; k < words.size(); k++)        if (!queued[k] && oneApart(word, words[k])) {          queued[k] = true;          next.push_back(words[k]);        }    }    level = next;  }  return 0;}
    Breadth first goes one level at a time, so the first level that contains the goal gives the shortest length. Marking a word queued as soon as it joins the next level is the visited set: without it, words bounce back and forth and the search never ends. Keeping the words in a std::unordered_set and erasing each as it is queued also works, but not inside a range-for over that same set: erasing the element the loop stands on breaks the loop’s iterator, which is undefined behaviour (use the iterator that erase returns instead). Each level compares every queued word with every unseen word, O(n² × L) for n words of length L; for big dictionaries, try each of the 25 other letters in each position instead and look the result up in a std::unordered_set of the words. (Graph search: BFS.)
  169. 20. Mastery challenge · Write it: merge overlapping bookings · Code challengeFinish merge(bookings): return the merged bookings sorted by start. Bookings that touch (one ends where the next starts) merge.One solution:
    Solution
    #include <algorithm>#include <utility>#include <vector>
    using Span = std::pair<int, int>;   // {start, end}
    std::vector<Span> merge(std::vector<Span> bookings) {  std::sort(bookings.begin(), bookings.end());  std::vector<Span> out;  for (auto [start, end] : bookings) {    if (!out.empty() && start <= out.back().second)      out.back().second = std::max(out.back().second, end);    else      out.push_back({start, end});  }  return out;}
    Once sorted by start, a booking can only overlap the one merged just before it, so a single sweep is enough: O(n log n) for the sort, O(n) for the sweep. std::sort needs no comparator here, because pairs compare by first and then by second: by start. std::max matters: {1, 10} then {2, 3} must stay {1, 10}, not shrink to {1, 3}. out.back() is a reference to the last booking kept, so changing its .second changes the booking inside out. Taking bookings by value means sorting never touches the caller’s vector. (Simple sorts; greedy: intervals.)
  170. 20. Mastery challenge · Write it: the longest rising run · Code challengeFinish longestRise(nums): return the length of the longest strictly increasing subsequence. An empty vector gives 0.One solution:
    Solution
    #include <algorithm>#include <vector>
    int longestRise(const std::vector<int>& nums) {  std::vector<int> best(nums.size(), 1);  int most = 0;  for (size_t i = 0; i < nums.size(); i++) {    for (size_t j = 0; j < i; j++)      if (nums[j] < nums[i]) best[i] = std::max(best[i], best[j] + 1);    most = std::max(most, best[i]);  }  return most;}
    Every increasing subsequence ending at i is one ending at some earlier, smaller j, plus nums[i]; so best[i] is 1 more than the best such best[j], or 1 alone. The answer is the largest best[i], since the longest run can end anywhere: returning best.back() is the common slip (and on an empty vector back() has no element to read: here the library’s checks stop the program). Two nested loops: O(n²) time, O(n) memory. Starting most at 0 covers the empty vector. (Dynamic programming.)
  171. 20. Mastery challenge · Write it: how many rooms are needed · Code challengeFinish rooms(meetings): return the fewest rooms needed. A meeting may start in a room at the moment another ends. Use a std::priority_queue that keeps the smallest end time on top.One solution:
    Solution
    #include <algorithm>#include <functional>#include <queue>#include <utility>#include <vector>
    using Meeting = std::pair<int, int>;   // {start, end}
    int rooms(std::vector<Meeting> meetings) {  std::sort(meetings.begin(), meetings.end());   // by start  std::priority_queue<int, std::vector<int>, std::greater<>> ends;  for (auto [start, end] : meetings) {    if (!ends.empty() && ends.top() <= start) ends.pop();    ends.push(end);  }  return ends.size();}
    Sorting by start replays the day in order. Each meeting either takes over the room that frees up soonest, if that room is already free, or needs a new one; reusing the soonest-free room is the greedy choice, and it is safe because any free room is as good as another. At most one room is freed per meeting, so the queue never shrinks, and its final size is the most rooms in use at once. std::priority_queue is a binary heap, so each step is O(log n), O(n log n) in all. (Greedy; shortest paths: priority queues.)
  172. 20. Mastery challenge · Which statements are true? · QuizPick every true statement.A. Breadth-first search finds a path with the fewest edges, whatever the edges weigh.; C. Memoising the naive recursive Fibonacci makes it O(n).; E. Kruskal’s algorithm adds edges cheapest first, skipping any that would close a cycle.BFS counts edges and ignores weights, so it does find the fewest edges, just not the cheapest path (graph search; shortest paths). Dijkstra finalises a node the first time it is taken from the queue, and a negative edge found later can make it cheaper: use Bellman–Ford (shortest paths). Memoising computes each fib(k) once (dynamic programming). A random pivot makes O(n log n) the expected time; an unlucky run can still be O(n²) (quicksort). The last one is Kruskal exactly (spanning trees).
  173. 20. Mastery challenge · Which statements are true? · Find the bugThis topological sort (Kahn’s algorithm) leaves some tasks out of the order even though there is no cycle. Which line is wrong?Line 9: if (--waiting[next] == 0) ready.push_back(next); // and drop line 10waiting[t] counts how many unfinished tasks task t still depends on. Finishing task removes one, and next is ready when that count reaches 0. Line 9 checks before the decrement, so a task whose last dependency just finished still reads 1 and is never queued. Decrement first, then check. If order ends up shorter than tasks for real, the graph has a cycle. (Graph search: topological sort.)
  174. 20. Mastery challenge · Read a recurrence: edit distance · Fill the blanksComplete the step that fills one cell of the table.1: ==, 2: 1, 3: minWhen the last letters match, they cost nothing and the answer is the distance without them. Otherwise one edit is unavoidable, and you pick whichever of the three leaves the cheapest problem behind: 1 + std::min({…}), where the braces let std::min take a list of three. Filling the table row by row is O(len(a) × len(b)), and only the previous row is needed, so memory can be one row. (Dynamic programming in two dimensions.)
  175. 20. Mastery challenge · Read a recurrence: edit distance · QuizWhat is the edit distance from kitten to sitting?B. 3Replace k with s (sitten), replace e with i (sittin), insert g at the end (sitting): 3 edits. Filling the table confirms that no shorter sequence exists, because it tries every alternative at every cell. The length difference alone only proves at least 1, so a lower bound by eye is easy to get wrong.