Foundations to FrontierPart XIV: The Algorithms LaneBezhig

CP01: Arrays, loops, and the frequency map

15 exercises, each with a complete solution. 3 figures. 18 code listings.

On this page
  1. Prerequisites and observable goals
  2. Warm-up: spaced review (10 minutes, closed book)
  3. 1. Quick summary
  4. 2. Deep explanation
  5. 2.1. Read the types first
  6. 2.2. Loops that keep a promise
  7. 2.3. The frequency map
  8. 2.4. Remember what you have seen: two-sum
  9. 2.5. Counting the work
  10. 2.6. The C++ this module uses
  11. 2.7. Beginner bugs: a field guide
  12. 2.8. Design trade-offs
  13. 3. The lab
  14. 3.0. Setup and ground rules
  15. 3.1. Step 1: count the positives (worked example)
  16. 3.2. Step 2: finish countEach (partly filled)
  17. 3.3. Step 3: mostFrequent (partly filled)
  18. 3.4. Step 4: firstUnique (on your own)
  19. 3.5. Step 5: containsDuplicate (on your own)
  20. 3.6. Step 6: isAnagram (on your own)
  21. 3.7. Step 7: twoSum, from a blank page (on your own)
  22. 3.8. Step 8: exactSum (on your own)
  23. 3.9. Step 9: countPairsWithSum (on your own)
  24. 3.10. Step 10: measure it
  25. 4. Practice
  26. Hint ladders for the lab
  27. 5. Code-reading drill: a game server counts its players
  28. 6. Mastery gate
  29. 7. Development log
  30. 8. Stretch (optional)
  31. Complete solutions: CP01

Prerequisites and observable goals#

Before

CP00, and a first program or two in C++: a for loop, std::vector<int>, and a function that returns a value. Nothing else is assumed; section 2.6 lists every piece of C++ this module uses.

After

Read any declaration or signature and say what it holds and what you may do with it; write accumulator loops whose promise (invariant) holds from the first pass; count anything with a frequency map; trade memory for time by remembering what you have seen; estimate and measure the work an algorithm does; solve three unseen counting problems in 40 minutes, closed book.

Level: Algorithms Lane, Module 1 of 14. About 8 hours: six 75-minute sessions and a shorter seventh for the gate. The clang++ compiler (Visual Studio’s on Windows), C++20, standard library only.

Warm-up: spaced review (10 minutes, closed book)#

These three target mistakes beginners commonly make before this module. Answer from memory, then check at the end of this chapter’s solutions. From CP02 on, the warm-up comes from due entries in your own error log (CP00, section E).

  1. In std::unordered_map<std::string, PlayerProfile> m;, what is the key type, what is the value type, and what goes inside m[...]?
  2. A first largest started with int Largest = 0;. On {-3, -1, -2} it returned 00. What should it start from, and why?
  3. Which line compiles, and why: int v = 1; v.size(); or std::vector<int> v = {4, 1, 7}; v.size();?

Number 1 returns in section 2.1 and the code-reading drill, number 2 in section 2.2, and number 3 is the first habit of this module: read the type before you use the name.

1. Quick summary#

Picture CP01.1. A loop is a cursor with a promise. The cursor visits one value per pass, and best keeps a promise: it is the largest value visited so far. Starting from v[0] (aqua), the promise is true before the first pass and stays true after every one. Starting from 00 (orange), it is false before the loop begins, and with all-negative values no pass ever repairs it. That is warm-up question 2’s bug, drawn.

Five ideas carry this module.

  1. Read the types first. A function’s signature is its plan: what comes in, and what type must come out. The return type tells you what to build.
  2. A loop keeps a promise. Write one sentence that is true after every pass (the invariant), and make sure it is already true before the first pass.
  3. A frequency map counts anything. counts[x]++ works even the first time x appears, because a missing key starts at zero.
  4. Remember what you have seen. Storing past values in a map turns “check every pair” into “look up the partner”: memory traded for time.
  5. Count the work, then measure it. Checking every pair grows like n2n^2; one pass grows like nn. At 100,000100{,}000 values that is about five billion pair checks against a hundred thousand look-ups.

What you will build. About 75 lines of C++ in drills.h, in the companion code’s cp/cp01 folder: nine functions, from counting positives to counting pairs that sum to a target, tested by the 64 checks in check.cpp; a failing check prints what your code returned. A benchmark (.\run.ps1 -Bench) then times your one-pass code against the brute force, and the mastery gate (.\run.ps1 -Gate) checks three problems you have not seen. The Counting Explorer, on the companion website at bezhig.com/book/explorers/cp01-counting.html, lets you step through a loop’s promise, the tally board, a tie, and two-sum’s check-then-store, and race every-pair against one pass in your browser.

2. Deep explanation#

2.1. Read the types first#

Every name in C++ has a type, and the type decides what you may do with the name. Before using any name, say its type card aloud: what it holds, and what it can do.

DeclarationHoldsCan do
int n = 5;one whole numberarithmetic, comparison. No members: n.size() does not exist
std::vector<int> v = {4, 1, 7};a row of numbers, positions 00 to 22v.size(), v[i], v.push_back(x), for (int x : v)
std::unordered_map<int, int> m;pairs key→\tovalue, no two with the same keym[k], m.count(k), m.find(k), m.size(), loop over the pairs
const std::vector<int>& w = v;a read-only view of v, not a copyeverything that reads; nothing that changes it

A map’s type names two types, in order: std::unordered_map<Key, Value>. The key is what goes inside the brackets of m[...]; the value is what comes back. In warm-up question 1, the key is a std::string, such as a player’s ID, and the value a PlayerProfile record. In code someone else wrote, the way to know is to open the declaration, never to guess from a nearby name.

The signature is the plan. Read

C++
std::unordered_map<int, int> countEach(const std::vector<int>& v)

from right to left: it takes a read-only view of a vector of ints (const&, so nothing is copied) and returns a map from int to int. So the body must create a map, fill it, and return it. A common first attempt reaches for one int counter, but the return type has already said that a single number cannot be the answer.

2.2. Loops that keep a promise#

Most loops in this module are accumulators: a variable that collects an answer as a cursor visits each value. The trick to writing them correctly is to write their promise first, one sentence that is true after every pass:

  • total: “total is the sum of the values visited so far.” Before any visit that sum is 00, so start at 00.
  • count: “count is the number of visited values that pass the test.” Start at 00.
  • best: “best is the largest value visited so far.” Before any visit there is no largest value. Make the promise true by starting from a real value, v[0], which needs v to be non-empty (the contract for largest guarantees it).

Each pass keeps the promise: if it was true before the pass, it is true after. When the loop ends, every value has been visited, so the promise is the answer. That is the whole proof of correctness, and Picture CP01.1 shows what happens when the starting value breaks it.

Two ways to walk a vector. The range loop for (int x : v) hands you each value; use it when you only need values. The index loop for (int i = 0; i < static_cast<int>(v.size()); ++i) hands you positions; use it when the answer is a position, as in firstUnique and twoSum. v.size() has the type std::size_t, an unsigned whole number, one that can never be negative. Comparing it with a signed int makes the compiler warn (-Wsign-compare), because a negative int would be read as a huge unsigned number. The cure is static_cast<int>(v.size()): static_cast<T>(x) means “convert x to the type T, on purpose and in plain sight”. Make it a habit.

2.3. The frequency map#

Picture CP01.2. A frequency map is a tally board. Four visits over {3, 1, 3, 3}. When the key is new (yellow), counts[x] first creates it with count 00 and the ++ makes it 11; when the key exists (aqua), ++ adds one to its tally. After the last visit the board answers “how many of each?” for every value at once.

A frequency map is an std::unordered_map<Value, int> from each value to how many times it has appeared. One line fills it:

C++
std::unordered_map<int, int> counts;
for (int x : v) counts[x]++;   // promise: counts holds the tally of every value visited

Why does counts[x]++ work the first time? Because operator[] on a map that lacks the key inserts it with a default value, which for int is 00, and then the ++ adds one. That same behaviour is a trap when you only want to look: counts[4] on a map without 44 quietly adds the key 44 with count 00. To ask “is it there?” without changing the map, use counts.count(4) (which returns 00 or 11) or counts.find(4).

Order is not a property of the map. Looping over an unordered_map visits its pairs in an order the standard leaves unspecified. It can change between compilers, between runs of a program that inserts differently, and as the map grows. So a rule like “break ties by the smaller value” must be written into your code; it never comes for free from the map. If you need keys in sorted order, std::map keeps them sorted, at the cost of log⁡n\log n work per operation instead of the hash map’s typical constant.

When a plain array beats a map. If the values are small and known in advance, like the 26 lowercase letters, an array of counts does the same job faster: int counts[26] = {}; then counts[c - 'a']++. That relies on 'a' to 'z' being consecutive codes, which holds in ASCII and UTF-8, the encodings you will use.

2.4. Remember what you have seen: two-sum#

Picture CP01.3. Two ways to find a pair that sums to 99 in {2, 7, 11, 15}. Left: try every pair i<ji<j (the shaded upper triangle); the work grows like n2n^2. Right: walk once, and for each value look up the partner it needs among the values already seen; the work grows like nn.

The two-sum problem asks for two positions whose values add to a target. Checking every pair is correct but slow. The fast idea is a promise about memory: “indexOf maps every value visited so far to a position where it appeared.” At position jj, the partner value v[j]v[j] needs is target−v[j]\text{target}-v[j]; one look-up says whether it has been seen. Then store v[j]v[j] and move on.

Check before you store. The order of those two moves is the whole subtlety. If you store v[j]v[j] first and then look for its partner, a value can find itself: with {3, 2, 4} and target 66, position 00 would pair 33 with its own copy and return {0, 0}. Checking first means the memory only ever holds earlier positions, so a found partner is always at an index i<ji<j. Section 3.7 asks you to break this on purpose and watch which check catches it.

Counting pairs instead of finding one. Step 9 asks how many pairs sum to the target. The same memory now holds how many times each value has been seen. At position jj, every earlier copy of the partner makes one new pair, so add that count, then record v[j]v[j]. With {3, 3, 3, 3} and target 66, the running total goes 0,1,3,60, 1, 3, 6: each new 33 pairs with every 33 before it.

2.5. Counting the work#

Before timing anything, estimate it. Checking every pair of nn values does about n(n−1)/2≈n2/2n(n-1)/2\approx n^2/2 steps; one pass does about nn steps, each a map operation costing more than a single addition but a fixed amount on average. A rough rule for planning: a modern core does on the order of 10810^8 to 10910^9 simple steps per second.

nnpairs n(n−1)/2n(n-1)/2one passat 10910^9 pair checks per second
1,0001{,}000499,500499{,}5001,0001{,}000under a millisecond
100,000100{,}0004,999,950,0004{,}999{,}950{,}000100,000100{,}000about 55 seconds
1,000,0001{,}000{,}000about 5×10115\times10^{11}1,000,0001{,}000{,}000about 88 minutes

We describe this growth with big-O notation: the brute force is O(n2)O(n^2) (multiply nn by 1010 and the work grows about 100100 times), the one-pass version O(n)O(n) on average (the work grows about 1010 times). A hash map’s look-up is constant time on average; with an adversarial input that makes many keys collide it can degrade, which competitive programmers occasionally defend against, but you can rely on the average in this lane. Lab step 10 measures all of this on your own machine.

When the machine beats the rule. The 10810^8 to 10910^9 rule is for planning, not a law. An optimizing compiler can turn a simple pair loop into vector instructions that test several pairs at once. On a recent desktop PC, the one this lab’s timing check was calibrated on, a brute-force pair count built by run.ps1 checked roughly 8×1098\times10^9 pairs per second, so 100,000100{,}000 values took about half a second to a second. That is why step 9’s last check uses 300,000300{,}000 values: there the brute force takes five to eight seconds, while one pass still takes a few milliseconds. Estimate first, then measure.

2.6. The C++ this module uses#

2.7. Beginner bugs: a field guide#

Each of these is something a careful beginner really writes. For each, the symptom you see, how to spot it in the code, and the check in check.cpp that catches it.

2.8. Design trade-offs#

  • Hash map, sorted map, or array? An unordered_map is the general tool: any key, constant time on average, no order. A std::map keeps keys sorted for O(log⁡n)O(\log n) per operation. A plain array is fastest of all when keys are small integers known in advance.
  • Memory for time. Every fast solution here spends memory proportional to nn to avoid n2n^2 work. When memory is the scarce resource, sorting first (CP03) often gives O(nlog⁡n)O(n\log n) time with no extra map.
  • Two passes are still linear. firstUnique walks the vector twice: once to count, once to find the first count of 11. Two passes of nn are 2n2n steps, still O(n)O(n). No method can stop early, though: whether a value is unique is not known until the last value has been read.

3. The lab#

3.0. Setup and ground rules#

Open the companion code’s cp/cp01 folder in your editor, and a PowerShell terminal in that folder. .\run.ps1 1 builds check.cpp with your drills.h and runs step 1’s checks. Every function starts as a throw that reports “not written yet”; replace it when you write the step. A failing check prints WRONG, what it expected, and, after got, what your code returned.

  • Predict before every run, in writing. The orange boxes tell you what to predict.
  • Read only the first error. Compiler errors cascade; fix the first one, then rebuild.
  • No AI and no searching during an attempt. Use the hint ladders, one rung at a time. Open a step’s complete solution, at the end of the module, only after your own attempt has run against the checks.

3.1. Step 1: count the positives (worked example)#

Type this into drills.h, replacing the throw. Say the promise aloud as you type it.

C++
inline int countPositives(const std::vector<int>& v) {
    int count = 0;                 // promise: count = positive values visited so far
    for (int x : v) {
        if (x > 0) ++count;        // keep the promise for this one value
    }
    return count;                  // every value visited: the promise is the answer
}

3.2. Step 2: finish countEach (partly filled)#

The signature says to build a map; fill the three blanks.

C++
inline std::unordered_map<int, int> countEach(const std::vector<int>& v) {
    ________ counts;            // blank 1: the type the signature returns
    for (int x : v) {
        ________;               // blank 2: one tally for x
    }
    return ________;            // blank 3
}

3.3. Step 3: mostFrequent (partly filled)#

Reuse countEach, then scan the board. The tie rule must be in your code (section 2.3).

C++
inline int mostFrequent(const std::vector<int>& v) {
    std::unordered_map<int, int> counts = countEach(v);
    int best = v[0];               // promise: best = best value scanned so far
    int bestCount = counts[best];  // v[0] is real, so the promise holds now
    for (const auto& [value, count] : counts) {
        // replace best when more often, or equally often with a smaller value
        if (________ || (________ && ________)) {
            best = value;
            bestCount = count;
        }
    }
    return best;
}

3.4. Step 4: firstUnique (on your own)#

Return the index of the first value that appears exactly once, or −1-1. Plan it as two passes (section 2.8): count, then find. Write the promise of each loop as a comment. Hints: hints.

3.5. Step 5: containsDuplicate (on your own)#

Return true as soon as any value is seen a second time. One pass, one map, and you may return early. Hints: hints.

3.6. Step 6: isAnagram (on your own)#

Two strings are anagrams when they use the same characters the same number of times: "listen" and "silent". Count one string, then “spend” the counts with the other. Case and spaces count. Hints: hints.

3.7. Step 7: twoSum, from a blank page (on your own)#

Write it from section 2.4’s idea, without looking at the solutions. Then break it on purpose:

3.8. Step 8: exactSum (on your own)#

3.9. Step 9: countPairsWithSum (on your own)#

Count index pairs i<ji<j that add to the target, in one pass (section 2.4). Hints: hints.

3.10. Step 10: measure it#

4. Practice#

F = foundation; C = standard; T = transfer; S = optional stretch; R = code reading. Work on paper first.

CP01.01FSolution

Type cards. For each declaration, decide whether each expression after it compiles; if it does, give its value or effect. (a) int n = 5; then n.size() and n + 1. (b) std::vector<int> v = {4, 1, 7}; then v.size(), v[3], and v[1] + 1. (c) std::unordered_map<std::string, int> m; then m["fire"]++, m[3], and m.count("ice"). (d) const std::vector<int>& w = v; then w.push_back(2).

CP01.02FSolution

A trace table. Trace int best = v[0]; for (int x : v) if (x > best) best = x; on v = {-3, -1, -2, -7}, writing best after each pass. Then trace the same loop starting from int best = 0;. Which line is the first wrong one, and why?

CP01.03CSolution

Predict the board. After std::unordered_map<int, int> counts; for (int x : {5, 2, 5, 5, 2, 9}) counts[x]++;, what does counts hold? What do counts[4] and then counts.size() give? What would counts.count(4) have given instead, and what would counts.size() be after it?

CP01.04CSolution

Spot the bug. This mostFrequent builds the right board, then scans it with int best = 0, bestCount = 0; and if (c > bestCount) { best = value; bestCount = c; }. Which part of the contract can it break, and on which input? Fix it by changing one line. Is starting best at 00 a bug here too?

CP01.05CSolution

Order matters. Trace the one-pass two-sum on {3, 3} with target 66: (a) checking before storing; (b) storing before checking. What does each return, and which field-guide bug is (b)?

CP01.06CSolution

Counting the work. How many pair checks does the brute-force two-sum make, at most, on n=1000n=1000 values? On n=100,000n=100{,}000? At 10910^9 checks per second, how long does each take? Assuming about 2020 nanoseconds per map operation, how long does the one-pass version take on each?

CP01.07TSolution

Overflow. An int holds values up to 2,147,483,6472{,}147{,}483{,}647. What does C++ say happens when int total = 0; adds 2,000,000,0002{,}000{,}000{,}000 twice? What do you typically see on your machine, and why exactly that number? What is the fix?

CP01.08TSolution

Array or map? To count the letters of a lowercase word, write the declaration of a counting array and the line that counts one character c. When would you still choose a map?

CP01.09TSolution

Why two passes? A student finds the first unique value in one pass by returning the first value “not seen before”. Give the shortest input on which that is wrong, and explain why no method can return the answer before it has read the last value.

CP01.10SSolution

Pairs at a distance. Count the index pairs i<ji<j with ∣v[i]−v[j]∣=k|v[i]-v[j]|=k for a given k≥1k\ge1, in one pass with a map. What must change when k=0k=0?

Hint ladders for the lab#

Climb one rung at a time, and try again after each one. Rung 1 is a question, rung 2 points to the idea, and rung 3 is a skeleton with the key parts left for you. Step 1 is worked in full in section 3.1.

Step 2: countEach
Rung 1: a nudge

What type does the signature say this function returns?

Rung 2: the idea

The return type is the thing to build (section 2.1); operator[] starts a missing key at 00 (section 2.3).

Rung 3: a skeleton

std::unordered_map<___, ___> counts;, then inside the loop counts[___]__;, then return ___;.

Step 3: mostFrequent
Rung 1: a nudge

When should a pair from the board replace best? There are two situations.

Rung 2: the idea

Strictly more often, or equally often with a smaller value. The map’s order cannot decide ties (section 2.3).

Rung 3: a skeleton

if (count __ bestCount || (count __ bestCount && value __ best)).

Step 4: firstUnique
Rung 1: a nudge

Can you know that a value is unique before you have seen every value?

Rung 2: the idea

Two passes: fill a frequency map, then walk the positions in order (section 2.8).

Rung 3: a skeleton

Pass 1: auto counts = countEach(v);. Pass 2: for (int i = 0; i < ___; ++i) if (counts[___] == __) return __; then return -1;.

Step 5: containsDuplicate
Rung 1: a nudge

At the moment you visit a value, what do you need to know about the values before it?

Rung 2: the idea

A memory of values seen so far: check, then record (section 2.4).

Rung 3: a skeleton

Inside the loop: if (seen.count(__)) return ___; then record x. After the loop: return ___;.

Step 6: isAnagram
Rung 1: a nudge

If the lengths differ, can they be anagrams?

Rung 2: the idea

Count every character of a; then each character of b spends one count. A count below zero means b used that character too often.

Rung 3: a skeleton

if (a.size() __ b.size()) return ___; then for (char c : a) counts[c]__; then for (char c : b) if (__counts[c] < __) return false; and finally return ___;.

Step 7: twoSum
Rung 1: a nudge

At position jj, which value would complete the pair?

Rung 2: the idea

Store value →\to index, and look up the partner before storing (section 2.4).

Rung 3: a skeleton

int need = ___; auto it = indexOf.find(___); if (it != indexOf.end()) return {___, ___}; and only then indexOf[___] = j;.

Step 8: exactSum
Rung 1: a nudge

What is the largest value an int can hold?

Rung 2: the idea

Accumulate in a type that can hold the true total (chord chart).

Rung 3: a skeleton

___ total = 0; for (int x : v) total += x; return total; Which type goes in the blank?

Step 9: countPairsWithSum
Rung 1: a nudge

At position jj, how many earlier positions could pair with it?

Rung 2: the idea

Keep how many times each value has been seen; add the partner’s count, then record x (section 2.4).

Rung 3: a skeleton

Before the loop, long long pairs = 0; and std::unordered_map<int, long long> seen;. Inside it, auto it = seen.find(___); and if (it != seen.end()) pairs += ___;, then seen[___]++;.

5. Code-reading drill: a game server counts its players#

A real-time game server regularly reports its own health: how many players are in each scene (one area of the game world) and how long its recent ticks (passes of its main loop) took. The four excerpts below are adapted from the health-metrics code of a real-time game engine’s server: unrelated lines are left out (marked // ...), and some names are simplified. Read all four, then answer in writing before opening the solutions.

(A) The report’s fields. One ServerMetricsSnapshot, called snap in the other excerpts, holds one report.

C++
struct ServerMetricsSnapshot {
    // ...
    std::unordered_map<std::string, int> sceneCounts;

    // Tick performance (rolling 60s window)
    float avgTickMs = 0.0f;
    float maxTickMs = 0.0f;
    int slowTickCount = 0;
    // ...
    struct TopScene {
        std::string scene;
        int playerCount = 0;
    };
    std::vector<TopScene> topScenes;
    // ...
};

(B) Counting players per scene. forEach calls the function written in place after it (a lambda) once for each player character in the world. cs points to that character’s stats, so its members are reached with ->, and cs->stats.currentScene is the name of the scene the character is in.

C++
if (world_) {
    world_->forEach<CharacterStatsComponent>([&](Entity*, CharacterStatsComponent* cs) {
        if (!cs->stats.currentScene.empty()) {
            snap.sceneCounts[cs->stats.currentScene]++;
        }
    });
}

(C) The last minute of ticks. tickSamples_ holds one sample for each tick of the last 60 seconds; s.totalMs is how long that tick took, in milliseconds.

C++
if (!tickSamples_.empty()) {
    float sum = 0.0f;
    float maxMs = 0.0f;
    int slowCount = 0;
    for (const auto& s : tickSamples_) {
        sum += s.totalMs;
        if (s.totalMs > maxMs) maxMs = s.totalMs;
        if (s.totalMs > 50.0f) slowCount++;
    }
    snap.avgTickMs = sum / static_cast<float>(tickSamples_.size());
    snap.maxTickMs = maxMs;
    snap.slowTickCount = slowCount;
}

(D) The five busiest scenes, and the JSON a dashboard stores. std::sort’s third argument returns true when a belongs before b. nlohmann::json is a widely used JSON library for C++.

C++
// Top scenes (scenes with most players, sorted desc)
for (const auto& [scene, count] : snap.sceneCounts) {
    snap.topScenes.push_back({scene, count});
}
std::sort(snap.topScenes.begin(), snap.topScenes.end(),
          [](const auto& a, const auto& b) { return a.playerCount > b.playerCount; });
if (snap.topScenes.size() > 5) snap.topScenes.resize(5);

// ... later, when the report is saved for the dashboard:
nlohmann::json sceneJson = nlohmann::json::object();
for (const auto& [scene, count] : snap.sceneCounts)
    sceneJson[scene] = count;
CP01.11RSolution

What are the key type and the value type of sceneCounts? Where did you find them, and what does one entry mean in the game?

CP01.12RSolution

In excerpt (B), the counting line runs snap.sceneCounts[cs->stats.currentScene]++;. What happens the first time a scene name is seen, and the second time? Why does the if just above it skip characters whose scene is empty?

CP01.13RSolution

Excerpt (C) runs three accumulators in one loop. Name each one and write its promise. maxMs starts at 0.0f; in largest (warm-up question 2), starting at 00 was a bug. Why is it safe here? What does the if around the loop protect against?

CP01.14RSolution

The first part of excerpt (D) builds the top five scenes by player count. If two scenes tie for fifth place, which one makes the list? Could the answer differ between two runs with the same players? How would you make it predictable?

CP01.15RSolution

The end of excerpt (D) writes the same counts to JSON with sceneJson[scene] = count;. In what order do the scenes appear in that JSON, and does it depend on the map’s iteration order? (Look up how nlohmann::json stores an object’s keys.)

6. Mastery gate#

7. Development log#

Add an entry to your development log: what you built (nine functions, about how many lines); your benchmark table and the ratio you predicted next to the ratio you measured; your costliest bug, its symptom, and the check that caught it; and one picture, for example Picture CP01.1 redrawn by hand with your own example.

8. Stretch (optional)#

Frontier connections: where this module’s ideas show up at scale#

  • Tokenizers. Byte-pair encoding, used to split text into tokens for language models, repeatedly counts adjacent symbol pairs with a frequency map and merges the most frequent: mostFrequent over pairs (Sennrich, Haddow, and Birch, “Neural machine translation of rare words with subword units”, 2016, arxiv.org/abs/1508.07909). AI Lab Module 2 builds one (section 2E).
  • Game servers. A real-time game server counts players per scene, messages per tick, and pending actions per player with exactly this ++counts[key] pattern; the code-reading drill above reads one of those counters.
  • Counting at internet scale. When the stream is too large to keep every key, a count–min sketch keeps a few small arrays of counters and answers “about how many?” with a guaranteed error bound (Cormode and Muthukrishnan, Journal of Algorithms 55(1), 2005).

Complete solutions: CP01#

Open a solution only after you have written your own attempt. Compare at the first step where your work differs. A corrected answer is not yet an independent success.

Lab step solutions#

Step 1 is worked in full in section 3.1.

L2. countEach.

C++
inline std::unordered_map<int, int> countEach(const std::vector<int>& v) {
    std::unordered_map<int, int> counts;    // the signature says: build a map
    for (int x : v) {
        counts[x]++;                        // a new key starts at 0, then becomes 1
    }
    return counts;                          // promise: the tally of every value
}

Writing counts[x] = 1; would make every count 11: the step 2 checks for {3, 1, 3, 3}, {-2, -2}, and the large input fail.

L3. mostFrequent.

C++
inline int mostFrequent(const std::vector<int>& v) {
    std::unordered_map<int, int> counts = countEach(v);
    int best = v[0];
    int bestCount = counts[best];
    for (const auto& [value, count] : counts) {
        if (count > bestCount || (count == bestCount && value < best)) {
            best = value;
            bestCount = count;
        }
    }
    return best;
}

For {9, 8, 8, 9, 7, 7} all three values appear twice and the answer is 77. With only count > bestCount, a tie never replaces best. So {9, 8, 8, 9, 7, 7} returns 99, the starting value v[0], which breaks the tie rule. And {1, 2, 2, 3, 3} returns whichever of 22 and 33 the loop meets first; the standard leaves that order unspecified, so the code alone cannot tell you which.

L4. firstUnique.

C++
inline int firstUnique(const std::vector<int>& v) {
    std::unordered_map<int, int> counts = countEach(v);    // pass 1: count
    for (int i = 0; i < static_cast<int>(v.size()); ++i) {  // pass 2: in order
        if (counts[v[i]] == 1) return i;                    // first count of 1
    }
    return -1;
}

The predictions: firstUnique({7, 7, 8, 9, 8}) is the index 33, and a version that returns v[i] prints got 9. In {1, 9} both values are unique and the first position, 00, wins. A second pass that walks the map instead of v can return 11 there, because the map’s order is not v’s order.

L5. containsDuplicate.

C++
inline bool containsDuplicate(const std::vector<int>& v) {
    std::unordered_map<int, int> seen;   // promise: holds every value visited so far
    for (int x : v) {
        if (seen.count(x)) return true;  // x was visited before: a duplicate
        seen[x] = 1;
    }
    return false;
}

(std::unordered_set<int> from <unordered_set> says the same thing more directly; a map works with the headers you have.) The predictions: {0, 0} gives true and {1} gives false. Recording x before checking makes {1} come out true: the loop finds the very x it just recorded.

L6. isAnagram.

C++
inline 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 c : a) counts[c]++;
    for (char c : b) {
        if (--counts[c] < 0) return false;   // b uses c more often than a does
    }
    return true;
}

Why this is enough: the lengths are equal, and no count went below zero, so every count ends at exactly zero. Without the length line, ("aa", "a") and ("listen", "silen") fail: b is shorter, so no count ever goes below zero, and the loop never notices the leftover letters of a. ("a", "aa") still comes out right, because the second 'a' of b drives its count to −1-1.

L7. twoSum.

C++
inline std::vector<int> twoSum(const std::vector<int>& v, int target) {
    std::unordered_map<int, int> indexOf;   // promise: value -> a position visited so far
    for (int j = 0; j < static_cast<int>(v.size()); ++j) {
        int need = target - v[j];
        auto it = indexOf.find(need);
        if (it != indexOf.end()) return {it->second, j};   // look up BEFORE storing v[j]
        indexOf[v[j]] = j;
    }
    return {};
}

Storing first returns {0, 0} for {3, 2, 4}, which fails the validity check (it needs i<ji<j). Three step 7 checks fail that way: {3, 2, 4}, {3, 3}, and {5}, where a lone 55 pairs with itself. The contract keeps every value and the target between −109-10^9 and 10910^9, so target - v[j] stays within ±2×109\pm2\times10^9, inside an int. Without that promise the subtraction itself could overflow, and you would compute it in long long.

L8. exactSum.

C++
inline long long exactSum(const std::vector<int>& v) {
    long long total = 0;          // holds about +-9.2e18
    for (int x : v) total += x;   // x is widened to long long before the addition
    return total;
}

With int total, 2×109+2×1092\times10^9+2\times10^9 overflows; CP01.07 explains the number the check printed after got.

L9. countPairsWithSum.

C++
inline long long countPairsWithSum(const std::vector<int>& v, int target) {
    std::unordered_map<int, long long> seen;  // value -> times seen so far
    long long pairs = 0;                      // promise: pairs among values visited
    for (int x : v) {
        auto it = seen.find(target - x);
        if (it != seen.end()) pairs += it->second;  // one pair per earlier partner
        seen[x]++;                                  // only now count x itself
    }
    return pairs;
}

For nn ones and target 22, every pair counts: n(n−1)/2=300,000⋅299,999/2=44,999,850,000n(n-1)/2=300{,}000\cdot299{,}999/2=44{,}999{,}850{,}000, about 2121 times what an int holds. Two nested loops would make all 44,999,850,00044{,}999{,}850{,}000 checks; even at the roughly 8×1098\times10^9 checks per second of section 2.5’s compiler-vectorized brute force, that is five seconds or more.

L10. The benchmark. The brute force should grow about 1616 times for every 44 times more data (424^2); your code about 44 times, with some noise at small sizes, where fixed costs dominate.

Practice solutions#

CP01.01Show solution

The type decides

(a) n.size() does not compile: an int has no members. n + 1 is 66. (b) v.size() is 33. v[3] compiles but reads past the end, since positions run from 00 to 22: undefined behaviour, which might print garbage or crash (v.at(3) would throw an exception instead). v[1] + 1 is 22. (c) m["fire"]++ compiles: it creates the key "fire" at 00 and makes it 11. m[3] does not compile, because the key type is std::string and 33 is an int. m.count("ice") is 00, and it inserts nothing. (d) does not compile: w is a read-only view.

Back to CP01.01
CP01.02Show solution

The first wrong line is the first line

From v[0]: −3-3, then −3-3, −1-1, −1-1, −1-1, returning −1-1, which is right. From 00: 00, 00, 00, 00, 00, returning 00, which is not even in the list. The first wrong line is the initialization: the promise “best is the largest value visited” is false before the loop, and no pass repairs it.

Back to CP01.02
CP01.03Show solution

Looking can change the board

counts holds 5→35\to3, 2→22\to2, 9→19\to1. counts[4] returns 00 and inserts the key 44, so counts.size() becomes 44. counts.count(4) would have returned 00 without inserting, leaving the size at 33.

Back to CP01.03
CP01.04Show solution

Ties are a contract, not a default

It breaks the tie rule. On {1, 2, 2, 1} both values appear twice, and whichever the map visits first wins, so it may return 22. Fix: if (c > bestCount || (c == bestCount && value < best)). Starting at 00 is harmless here, unlike in largest: with bestCount = 0, the first pair always replaces it, because every count is at least 11. The promise “best has the highest count among pairs scanned” is still true from the first pass. (Starting from v[0], as in L3, is clearer.)

Back to CP01.04
CP01.05Show solution

Check, then store

(a) j=0j=0: need 33, memory empty; store 3→03\to0. j=1j=1: need 33, found at 00: return {0,1}\{0,1\}. Correct. (b) j=0j=0: store 3→03\to0, then need 33, found at 00: return {0,0}\{0,0\}, the same position twice. That is B6.

Back to CP01.05
CP01.06Show solution

Seconds against milliseconds

At most n(n−1)/2n(n-1)/2 checks: 499,500499{,}500 for n=1000n=1000, about half a millisecond at 10910^9 per second; 4,999,950,0004{,}999{,}950{,}000 for n=100,000n=100{,}000, about 55 seconds. One pass: 1000×201000\times20 ns =20=20 microseconds, and 100,000×20100{,}000\times20 ns =2=2 milliseconds. The ratio between the two methods itself grows with nn.

Back to CP01.06
CP01.07Show solution

Undefined, and why you see that number

The true sum, 4,000,000,0004{,}000{,}000{,}000, does not fit. In C++, overflowing a signed integer is undefined behaviour: the language promises nothing, and optimizers may assume it never happens. On your machine you will typically see −294,967,296-294{,}967{,}296, because the hardware keeps the low 32 bits: 4,000,000,000−232=4,000,000,000−4,294,967,296=−294,967,2964{,}000{,}000{,}000-2^{32}=4{,}000{,}000{,}000-4{,}294{,}967{,}296=-294{,}967{,}296. Fix: accumulate in long long.

Back to CP01.07
CP01.08Show solution

Small, known keys: use an array

int counts[26] = {}; and counts[c - 'a']++;. The subtraction turns 'a' into 00 through 'z' into 2525. Use a map when the keys are not small integers in a known range: arbitrary characters, words, IDs, or values up to a billion.

Back to CP01.08
CP01.09Show solution

Uniqueness is only known at the end

{7, 8, 7}: at position 00 the value 77 has not been seen before, so the one-pass rule returns 00, but 77 repeats; the right answer is 11. Whatever the method, the last value can still change the answer: append a copy of the current answer and that value is no longer unique. So every correct method must read all nn values first, which is exactly what the counting pass does.

Back to CP01.09
CP01.10Show solution

Look both ways

At each xx, every earlier x−kx-k and every earlier x+kx+k makes a pair: pairs += seen[x - k] + seen[x + k]; seen[x]++; (use find to avoid inserting zeros, as in L9). For k=0k=0 the two partners are the same value, so the line would count each earlier copy twice; use pairs += seen[x]; alone.

Back to CP01.10

Code-reading solutions#

CP01.11Show solution

Open the declaration

In the report’s declaration, excerpt (A): std::unordered_map<std::string, int> sceneCounts;. The key is a scene’s name (std::string); the value is how many characters are in that scene right now (int). One entry reads “this scene has this many players”.

Back to CP01.11
CP01.12Show solution

A tally board for scenes

The first time a scene name appears, operator[] creates the key with count 00 and ++ makes it 11; the second time, the existing count becomes 22. The if skips an empty scene name, presumably characters not yet placed in any scene, so that the board does not gain a meaningless "" key.

Back to CP01.12
CP01.13Show solution

Three promises in one loop

sum: the total of the tick times visited. maxMs: the largest tick time visited. slowCount: how many visited ticks took over 5050 ms. Starting maxMs at 00 is safe because a tick’s duration is never negative: no sample is below the starting value, so the true maximum always replaces it or equals it (if every sample were 00, then 00 would be the right answer). The largest of warm-up question 2 was different, because its values could all be negative. The if around the loop protects the average, computed just after the loop, from dividing by zero when no samples exist. With floats that would not crash: 0/00/0 gives NaN (“not a number”), which would quietly reach the dashboard.

Back to CP01.13
CP01.14Show solution

Ties at the cut-off are arbitrary

The scenes are copied out of the map in its unspecified iteration order, then sorted by player count only, and std::sort is not stable, so tied scenes end up in an unspecified order and either may survive the cut to five. Two runs with the same players could disagree. To make it predictable, break ties explicitly, for example by scene name: a.playerCount > b.playerCount || (a.playerCount == b.playerCount && a.scene < b.scene).

Back to CP01.14
CP01.15Show solution

The JSON is sorted, the map is not

By default nlohmann::json stores an object’s members in a std::map, so its keys come out sorted by name, whatever order the loop inserted them in. The JSON is predictable even though the map’s iteration order is not. (The library’s ordered_json keeps insertion order instead.)

Back to CP01.15

Mastery gate solutions#

C++
inline int countDistinct(const std::vector<int>& v) {
    std::unordered_map<int, int> counts;
    for (int x : v) counts[x]++;
    return static_cast<int>(counts.size());     // one key per different value
}

inline char mostCommonLetter(const std::string& s) {
    int counts[26] = {};
    for (char c : s) counts[c - 'a']++;
    int best = 0;                                // promise: best letter among those scanned
    for (int i = 1; i < 26; ++i) {
        if (counts[i] > counts[best]) best = i;  // strictly greater keeps the earlier letter
    }
    return static_cast<char>('a' + best);
}

inline bool hasPairWithSum(const std::vector<int>& v, int target) {
    std::unordered_map<int, int> seen;
    for (int x : v) {
        if (seen.count(target - x)) return true;  // check before storing
        seen[x] = 1;
    }
    return false;
}

Faded example answer#

int count = 0; (true before any visit), if (x < 0) ++count;, return count;.

Warm-up answers#

  1. The key type is std::string (a player’s ID, for example), the value type is PlayerProfile (that player’s record), and a key goes inside the brackets.
  2. Start from v[0]: the promise “the largest value visited” must be true before the first pass, and 00 is not a value in {-3, -1, -2}.
  3. Only the second: .size() is a member of std::vector<int>, and an int has no members.