Skip to content

The Playbook: A Process for Problems You Have Not Seen

A checklist for unfamiliar problems, a table from problem signals to the techniques of this course, and four original problems solved out loud, dead ends included.

Knowing the algorithms is not the same as solving the problem

By now you can write BFS, Dijkstra, a segment tree, KMP and a bitmask DP, and test each against a brute force. Yet a new problem rarely says "use Dijkstra". It describes drones, ticket prices or warehouse logs, and the algorithm is hidden behind the story. The typical failure in a contest or an interview is not forgetting an algorithm. It is one of these:

  • Starting to code the first idea that comes to mind, then discovering twenty minutes later that it is O(n^2) with n = 2·10^5, or that it answers a slightly different question.
  • Matching a keyword to a technique ("subarray" means two pointers) without checking the condition that makes the technique work.
  • Trusting a solution because it passes the sample, then losing time to a wrong answer whose cause you cannot see.

This lesson turns the course into a process. First a checklist you can run in your head. Then a table from the signals in a problem statement to the techniques of lessons 1 to 11. Then four original problems, each solved the way you would solve it under time pressure: restate, brute force, dead ends, insight, code, stress test, complexity. The dead ends are part of the content: they show which checks prevented a wrong solution.

The process, as a checklist

  1. Restate the problem in one sentence, in terms of the data, not the story. "Count pairs of prefix sums with equal remainders, at least L apart" is something you can solve; "count good stretches of the conveyor log" is not yet. In an interview, say it out loud and ask whether you understood it.
  2. Read the constraints and set a budget. Use the table from Complexity in Practice: n up to 2·10^5 means about O(n log n), n up to 20 invites O(2^n · n). Note the value ranges: do sums or counts need long long? Can values be negative or zero? Is the input possibly empty?
  3. Write down a brute force and its complexity, even if it is hopeless at the limits. It proves you understand the question, and it becomes the reference for the stress test.
  4. Find the bottleneck. What does the brute force repeat? Which quantity is recomputed for every candidate? The fast solution usually removes exactly that repetition.
  5. Look up the signals in the table below. Each technique comes with a condition (monotone predicate, non-negative weights, values without negatives). Check the condition before you commit.
  6. Code the solution in the solve(std::istream&, std::ostream&) form, with the types decided in step 2.
  7. Test: the samples first, then a few cases by hand (empty, one element, all equal, the maximum values), then a stress test against the brute force from step 3, as in Fast I/O, Contest Templates and Stress Testing.
  8. State the complexity in time and memory, worst case or expected, and check it against step 2.

The order matters more than it seems. Steps 2 and 3 come before any clever idea because they make the clever idea checkable: you know what it must beat, and you have something to compare it with.

From signal to technique

The table maps phrases and constraint patterns to the technique that usually fits and the lesson that teaches it. It is a list of first guesses, not a decision procedure: the second column always has a condition you must verify.

Signal in the statementFirst technique to tryLesson
n up to 10, "every order"all permutations with std::ranges::next_permutation1, 3
n up to 20, "choose a subset", "visit all"bitmask DP, O(2^n · n) or O(2^n · n^2)7
n up to about 40, subsetsmeet in the middle8
fewest moves, unweighted graph or gridBFS, multi-source BFS3
prerequisites, "in an order that respects"topological sort, cycle check3
two groups, conflictsbipartite check by two-coloring3
cheapest route, weights >= 0Dijkstra4
weights only 0 and 10-1 BFS4
negative weights, "arbitrage", "infinite gain"Bellman-Ford, negative cycles4
all pairs, V up to a few hundredFloyd-Warshall4
merge groups, "are they connected now"union-find5
connect everything at least costKruskal or Prim5
route whose worst edge is as small as possibleMST path, or binary search on the answer5, 8
"count the ways", "modulo 10^9 + 7"DP with a modulus, or a binomial formula6, 11
items, capacity, "at most W"knapsack6
two sequences, "edit", "common", "longest increasing"LCS, edit distance, LIS6
merge neighbors, split a rangeinterval DP7
a tree, an answer per subtreeDP on trees7
count numbers in [A, B] with a digit propertydigit DP7
"minimize the maximum", "maximize the minimum"binary search on the answer8
contiguous window, values >= 0two pointers8
intervals in time, "at the busiest moment"sweep line over sorted events8
many range sums, no updatesprefix sums (1D or 2D)9
many range additions, one read at the enddifference array9
range sums with point updates, inversionsFenwick tree9
range updates and range queriessegment tree with lazy propagation9
range minimum, no updatessparse table9
occurrences of a pattern, periodsprefix function (KMP), Z-function10
compare many substringsrolling hash10
primes, divisors, factorize many numberssieve, smallest prime factors11
remainders, gcd, "every m seconds"modular arithmetic, CRT11
"the k-th smallest" of a huge implicit setbinary search on the value, count <= x8

Two rows share the same signal on purpose. "Route whose worst edge is as small as possible" points to two techniques, and the second worked problem below shows a small change to the statement that rules one of them out. That is the general lesson of the table: a signal narrows the search, the conditions decide.

Worked problem 1: conveyor stretches

A conveyor belt logs n weight changes a[0..n-1], each between -10^9 and 10^9. A stretch is a run of consecutive entries. Count the stretches of length at least L whose total is a multiple of k. Constraints: 1 <= n <= 2·10^5, 1 <= k <= 10^9, 1 <= L <= n.

Restate and budget. Count pairs (i, j) with j - i >= L and a[i] + ... + a[j-1] divisible by k. With n = 2·10^5 the budget is about O(n log n). The answer can reach n(n+1)/2, about 2·10^10, so it needs long long; so do the sums (up to 2·10^14 in absolute value).

Brute force. Fix the start, extend the end, keep a running sum: O(n^2), about 2·10^10 steps at the limit. Correct and short, so it becomes the reference.

A dead end. "Consecutive entries" and "length at least L" suggest a sliding window. But two pointers rely on the window's property changing monotonically as it grows, and with negative values a longer stretch can have a smaller or a larger total. Divisibility is not monotone even for positive values. The window idea fails its condition, so we drop it before writing it.

The insight. Write P[i] for the sum of the first i entries. The stretch [i, j) has total P[j] - P[i], and that is divisible by k exactly when P[i] and P[j] leave the same remainder modulo k. So the question becomes: for each j, how many earlier i with i <= j - L have the same remainder? Walk j upwards, add P[j - L] to a table of remainder counts as it becomes far enough away, and look up P[j].

The typical bug is the one from Number Theory and Bit Tricks: with negative entries, P[j] % k can be negative, and -1 and k - 1 land in different table entries although they are the same remainder. The stress test runs both versions.

Input: n k L, then the entries.

6 3 2
4 -1 2 5 -3 3
main.cppc++23
#include <cstdint>
#include <iostream>
#include <map>
#include <print>
#include <sstream>
#include <vector>

using ll = long long;

struct Lcg {
    std::uint64_t s;
    std::uint32_t next() {
        s = s * 6364136223846793005ULL + 1442695040888963407ULL;
        return std::uint32_t(s >> 33);
    }
    int range(int lo, int hi) { return lo + int(next() % std::uint32_t(hi - lo + 1)); }
};

// Stretches a[i..j) with j - i >= len whose total is a multiple of k.
// Two prefix sums with equal remainders bound such a stretch.
ll countFast(const std::vector<ll>& a, ll k, int len, bool buggyRemainder) {
    int n = int(a.size());
    std::vector<ll> rem(n + 1, 0);  // rem[i] = (a[0] + ... + a[i-1]) mod k
    ll prefix = 0;
    for (int i = 0; i < n; ++i) {
        prefix += a[i];
        rem[i + 1] = buggyRemainder ? prefix % k : ((prefix % k) + k) % k;
    }
    std::map<ll, ll> seen;  // remainder -> how many usable left ends have it
    ll count = 0;
    for (int j = len; j <= n; ++j) {
        ++seen[rem[j - len]];  // left end j - len just became far enough away
        auto it = seen.find(rem[j]);
        if (it != seen.end()) count += it->second;
    }
    return count;
}

ll countBrute(const std::vector<ll>& a, ll k, int len) {
    ll count = 0;
    for (std::size_t i = 0; i < a.size(); ++i) {
        ll sum = 0;
        for (std::size_t j = i; j < a.size(); ++j) {
            sum += a[j];
            if (int(j - i + 1) >= len && sum % k == 0) ++count;
        }
    }
    return count;
}

void solve(std::istream& in, std::ostream& out) {
    int n, len;
    ll k;
    in >> n >> k >> len;
    std::vector<ll> a(n);
    for (auto& x : a) in >> x;
    out << countFast(a, k, len, false) << '\n';
}

void stress(bool buggy) {
    Lcg rng{5};
    for (int t = 0; t < 3000; ++t) {
        int n = rng.range(0, 1 + t / 200), len = rng.range(1, 4);
        ll k = rng.range(1, 6);
        std::vector<ll> a(n);
        for (auto& x : a) x = rng.range(-5, 5);
        if (countFast(a, k, len, buggy) != countBrute(a, k, len)) {
            std::print("{}: mismatch, k = {}, len = {}, a =", buggy ? "buggy" : "fixed",
                       k, len);
            for (ll x : a) std::print(" {}", x);
            std::println(" (fast {}, brute {})", countFast(a, k, len, buggy),
                         countBrute(a, k, len));
            return;
        }
    }
    std::println("{}: 3000 random cases agree", buggy ? "buggy" : "fixed");
}

int main() {
    std::istringstream in("6 3 2\n4 -1 2 5 -3 3\n");
    solve(in, std::cout);
    stress(true);
    stress(false);
}
Output

Press Run to compile and run this example.

5
buggy: mismatch, k = 2, len = 1, a = -1 2 (fast 0, brute 1)
fixed: 3000 random cases agree

Check the sample by hand. The prefix sums are 0, 4, 3, 5, 10, 7, 10, with remainders 0, 1, 0, 2, 1, 1, 1 modulo 3. Pairs at least 2 apart with equal remainders: (0, 2) for remainder 0, and (1, 4), (1, 5), (1, 6), (4, 6) for remainder 1. That makes 5. The buggy version fails on a two-element input: the prefix sums 0, -1, 1 have remainders 0, -1, 1 modulo 2 instead of 0, 1, 1, so the stretch [2] is missed.

Complexity. O(n log n) time with std::map, O(n) memory. A std::unordered_map gives O(n) expected time, with the caveat about adversarial inputs from lesson 1. When k is small (say up to 10^6), a plain std::vector of counts indexed by remainder is simpler and faster still.

Worked problem 2: the drone's range

Charging pads 1..n are joined by m two-way flight corridors with lengths. A drone with range R can fly a corridor only if its length is at most R, and it recharges fully at every pad. Find the smallest R that lets it fly from pad 1 to pad n using at most K corridors, or print -1. Constraints: n up to 10^5, m up to 2·10^5, lengths up to 10^9.

Restate and budget. Minimize the largest corridor length on a route from 1 to n, subject to the route having at most K edges. The budget is about O((n + m) log m).

Dead ends first. Without the K limit this is the minimum bottleneck path from Union-Find and Minimum Spanning Trees: add corridors in order of length until 1 and n are connected. With the limit, that answer can be too small, because the route it finds may use many short corridors. A Dijkstra variant that minimizes the maximum edge has the same blind spot. Both techniques match the signal "minimize the worst edge" and both fail the condition, which here is "no other constraint on the route".

Brute force. The answer is always one of the m corridor lengths. For each length r, keep only corridors of length at most r and run BFS from pad 1 to get the fewest corridors to pad n; accept r if that number is at most K. That is O(m · (n + m)), about 6·10^10 at the limit.

The insight. The check "can the drone make it with range r?" is monotone: a larger range keeps every corridor a smaller range allowed, so the fewest-corridor count can only drop. Monotone checks over a sorted set of candidates are what binary search on the answer is for. Sort the lengths and find the first one that passes with std::ranges::partition_point.

The sample has a cheap route 1-2-3-4-5 with four corridors of length 3, a direct pair 1-3 (length 9) and 3-5 (length 7). The program answers the same network for K = 2, 3, 4.

5 6 K
1 2 3
2 3 3
3 4 3
4 5 3
1 3 9
3 5 7
main.cppc++23
#include <algorithm>
#include <cstdint>
#include <format>
#include <iostream>
#include <print>
#include <queue>
#include <sstream>
#include <vector>

struct Lcg {
    std::uint64_t s;
    std::uint32_t next() {
        s = s * 6364136223846793005ULL + 1442695040888963407ULL;
        return std::uint32_t(s >> 33);
    }
    int range(int lo, int hi) { return lo + int(next() % std::uint32_t(hi - lo + 1)); }
};

struct Edge { int u, v, w; };

// Can the drone get from pad 0 to pad n-1 in at most maxHops corridors,
// using only corridors of length <= range? BFS counts corridors.
bool reachable(int n, const std::vector<Edge>& edges, int maxHops, int range) {
    std::vector<std::vector<int>> adj(n);
    for (auto [u, v, w] : edges)
        if (w <= range) { adj[u].push_back(v); adj[v].push_back(u); }
    std::vector<int> hops(n, -1);
    std::queue<int> q;
    hops[0] = 0;
    q.push(0);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int v : adj[u])
            if (hops[v] < 0) { hops[v] = hops[u] + 1; q.push(v); }
    }
    return hops[n - 1] >= 0 && hops[n - 1] <= maxHops;
}

// The answer is one of the corridor lengths (or 0 if start == goal), and
// reachable() is monotone in range: binary search over the sorted lengths.
int minRange(int n, const std::vector<Edge>& edges, int maxHops) {
    if (n == 1) return 0;
    std::vector<int> lengths;
    for (const auto& e : edges) lengths.push_back(e.w);
    std::ranges::sort(lengths);
    auto it = std::ranges::partition_point(lengths, [&](int r) {
        return !reachable(n, edges, maxHops, r);
    });
    return it == lengths.end() ? -1 : *it;
}

int bruteRange(int n, const std::vector<Edge>& edges, int maxHops) {
    if (n == 1) return 0;
    int best = -1;
    for (const auto& e : edges)  // try every length; keep the smallest that works
        if (reachable(n, edges, maxHops, e.w) && (best < 0 || e.w < best)) best = e.w;
    return best;
}

void solve(std::istream& in, std::ostream& out) {
    int n, m, maxHops;
    in >> n >> m >> maxHops;
    std::vector<Edge> edges(m);
    for (auto& [u, v, w] : edges) { in >> u >> v >> w; --u; --v; }
    out << minRange(n, edges, maxHops) << '\n';
}

int main() {
    // Pads 1..5. The cheap route 1-2-3-4-5 needs 4 corridors.
    const char* corridors = "1 2 3\n2 3 3\n3 4 3\n4 5 3\n1 3 9\n3 5 7\n";
    for (int maxHops : {2, 3, 4}) {
        std::istringstream in(std::format("5 6 {}\n{}", maxHops, corridors));
        std::cout << "at most " << maxHops << " corridors: ";
        solve(in, std::cout);
    }

    Lcg rng{17};
    int agree = 0;
    for (int t = 0; t < 3000; ++t) {
        int n = rng.range(1, 7), m = rng.range(0, 12), maxHops = rng.range(1, 6);
        std::vector<Edge> edges(m);
        for (auto& e : edges)
            e = {rng.range(0, n - 1), rng.range(0, n - 1), rng.range(1, 9)};
        if (minRange(n, edges, maxHops) != bruteRange(n, edges, maxHops)) {
            std::println("mismatch on test {}", t);
            return 0;
        }
        ++agree;
    }
    std::println("{} random graphs: binary search agrees with brute force", agree);
}
Output

Press Run to compile and run this example.

at most 2 corridors: 9
at most 3 corridors: 7
at most 4 corridors: 3
3000 random graphs: binary search agrees with brute force

With four corridors allowed, the answer is 3, which is what the bottleneck method returns regardless of K. With three, the route 1-2-3-5 needs range 7; with two, only 1-3-5 is short enough, and it needs 9. The random graphs include self-loops, parallel corridors, a single pad and disconnected networks, none of which the sample covers.

Complexity. Sorting is O(m log m). The binary search makes about log2(m) checks, about 18 for m = 2·10^5, and each check is a BFS in O(n + m). That is a few million steps, well inside the budget.

Worked problem 3: badge codes

An access badge carries a code of n digits (leading zeros allowed). A code is valid if no two neighboring digits are equal and its digit sum is divisible by 3. How many valid codes are there, modulo 10^9 + 7? Constraint: n up to 10^5.

Restate and brute force. Count digit strings of length n with two local and global conditions. The brute force tries all 10^n codes: fine for n up to 6, hopeless beyond.

The signal. "How many, modulo 10^9 + 7" with n up to 10^5 points to a counting DP from Dynamic Programming I: Patterns. The question that defines the state: when we append the next digit, what do we need to know about the prefix so far? Only two things: its last digit (for the neighbor rule) and its digit sum modulo 3 (for the final check). In the five steps of that lesson: the state ways[d][r] is the number of valid prefixes of the current length that end in digit d and have digit sum r modulo 3; the transition appends any digit other than d; the order is by increasing length; the base case is length 1, one code per digit; the answer sums ways[d][0] over all d. That gives 10 · 3 = 30 states per length, and each state tries 10 next digits. The same "keep only what the future needs" idea drove the digit DP in Dynamic Programming II; here the length is fixed and neighbors interact.

A dead end. A closed formula is tempting: count codes without equal neighbors (10 · 9^(n-1)), then take about a third of them. The "about a third" is not exact, and making it exact requires exactly the bookkeeping the DP does. Under time pressure, the DP is the safer route to a correct answer.

The program prints the answers for n = 1, 2, 3 and 10^5, then compares the DP with the brute force for every n up to 6.

1 2 3 100000
main.cppc++23
#include <array>
#include <iostream>
#include <print>
#include <sstream>

using ll = long long;
constexpr ll kMod = 1'000'000'007;

// Codes of length n over digits 0-9, no two equal neighbors, digit sum % 3 == 0.
// State: (last digit, digit sum mod 3) -> number of codes, modulo kMod.
ll countCodes(int n) {
    using Table = std::array<std::array<ll, 3>, 10>;
    Table ways{};
    for (int d = 0; d < 10; ++d) ways[d][d % 3] = 1;  // codes of length 1
    for (int len = 2; len <= n; ++len) {
        Table next{};
        for (int last = 0; last < 10; ++last)
            for (int r = 0; r < 3; ++r) {
                if (ways[last][r] == 0) continue;
                for (int d = 0; d < 10; ++d) {
                    if (d == last) continue;  // no two equal neighbors
                    ll& cell = next[d][(r + d) % 3];
                    cell = (cell + ways[last][r]) % kMod;
                }
            }
        ways = next;
    }
    ll total = 0;
    for (int d = 0; d < 10; ++d) total = (total + ways[d][0]) % kMod;
    return total;
}

// Brute force: try all 10^n codes.
ll bruteCodes(int n) {
    ll limit = 1, count = 0;
    for (int i = 0; i < n; ++i) limit *= 10;
    for (ll code = 0; code < limit; ++code) {
        int sum = 0, prev = -1;
        bool ok = true;
        ll x = code;
        for (int i = 0; i < n; ++i, x /= 10) {
            int d = int(x % 10);
            ok = ok && d != prev;
            sum += d;
            prev = d;
        }
        count += ok && sum % 3 == 0;
    }
    return count;
}

void solve(std::istream& in, std::ostream& out) {
    int n;
    while (in >> n) out << "n = " << n << ": " << countCodes(n) << '\n';
}

int main() {
    std::istringstream in("1 2 3 100000\n");
    solve(in, std::cout);
    for (int n = 1; n <= 6; ++n)
        std::println("n = {}: dp {} brute {}", n, countCodes(n), bruteCodes(n));
}
Output

Press Run to compile and run this example.

n = 1: 4
n = 2: 30
n = 3: 276
n = 100000: 14555685
n = 1: dp 4 brute 4
n = 2: dp 30 brute 30
n = 3: dp 276 brute 276
n = 4: dp 2442 brute 2442
n = 5: dp 21876 brute 21876
n = 6: dp 196878 brute 196878

n = 2 can be checked by hand. The digits fall into three remainder classes: {0, 3, 6, 9}, {1, 4, 7} and {2, 5, 8}. A pair sums to a multiple of 3 if both are in the first class (4 · 4 - 4 = 12 pairs once equal neighbors are removed) or one is in each of the other two (3 · 3 · 2 = 18 pairs). Total 30.

Complexity. O(n · 10 · 3 · 10), about 3·10^7 simple steps for n = 10^5, and O(1) memory because only the previous length is kept. Every addition is reduced modulo kMod; without the reduction, the counts grow by a factor of about 9 per digit, and the total no longer fits in long long from n = 21 on. Grouping digits by remainder class would cut the state to 3 · 3 and the time by about a factor of 10, but the plain version already fits, and the plain version is the one you can write correctly in five minutes.

Worked problem 4: the k-th cheapest bundle

A shop sells bundles of one item from list A (n prices) and one from list B (m prices). There are n · m bundles. What is the price of the k-th cheapest bundle, counting bundles with equal prices separately? Constraints: n, m up to 2·10^5, prices from 1 to 10^9, 1 <= k <= n · m.

Restate and budget. Find the k-th smallest value of A[i] + B[j]. Note the types now: n · m reaches 4·10^10, so k overflows int, and the sums reach 2·10^9, just below the int limit of 2147483647. Use long long for both rather than rely on that margin.

Brute force. Generate all sums and sort them: O(nm log(nm)), with 4·10^10 sums at the limit. Not even the memory exists.

A dead end. A heap that pops the cheapest bundle k times, pushing its neighbors, is a well-known technique when k is small. Here k can be 4·10^10, so O(k log k) is out.

The insight. "The k-th smallest" can be turned into a yes/no question: is the answer at most x? It is exactly when at least k bundles cost at most x. That count is monotone in x, so we can binary search on x. Counting the bundles that cost at most x is a two-pointer pass over the sorted lists: as A[i] grows, the usable part of B only shrinks. The smallest x whose count reaches k is always the price of an actual bundle, because the count only changes at bundle prices.

3 4 5
30 10 20
7 1 1000000000 4
main.cppc++23
#include <algorithm>
#include <cstdint>
#include <iostream>
#include <print>
#include <sstream>
#include <vector>

using ll = long long;

struct Lcg {
    std::uint64_t s;
    std::uint32_t next() {
        s = s * 6364136223846793005ULL + 1442695040888963407ULL;
        return std::uint32_t(s >> 33);
    }
    int range(int lo, int hi) { return lo + int(next() % std::uint32_t(hi - lo + 1)); }
};

// How many pairs (i, j) have a[i] + b[j] <= x? Both arrays sorted ascending.
// As a[i] grows, the usable prefix of b only shrinks: two pointers, O(n + m).
ll countAtMost(const std::vector<ll>& a, const std::vector<ll>& b, ll x) {
    ll count = 0;
    std::size_t j = b.size();
    for (ll ai : a) {
        while (j > 0 && ai + b[j - 1] > x) --j;
        count += ll(j);
    }
    return count;
}

// The k-th smallest sum (1-based): the smallest x with countAtMost(x) >= k.
// That x is always an actual sum, because the count only jumps at sums.
ll kthBundle(std::vector<ll> a, std::vector<ll> b, ll k) {
    std::ranges::sort(a);
    std::ranges::sort(b);
    ll lo = a.front() + b.front(), hi = a.back() + b.back();  // answer in [lo, hi]
    while (lo < hi) {
        ll mid = lo + (hi - lo) / 2;
        if (countAtMost(a, b, mid) >= k) hi = mid; else lo = mid + 1;
    }
    return lo;
}

ll bruteBundle(const std::vector<ll>& a, const std::vector<ll>& b, ll k) {
    std::vector<ll> sums;
    for (ll x : a)
        for (ll y : b) sums.push_back(x + y);
    std::ranges::sort(sums);
    return sums[k - 1];
}

void solve(std::istream& in, std::ostream& out) {
    int n, m;
    ll k;
    in >> n >> m >> k;
    std::vector<ll> a(n), b(m);
    for (auto& x : a) in >> x;
    for (auto& x : b) in >> x;
    out << kthBundle(a, b, k) << '\n';
}

int main() {
    std::istringstream in("3 4 5\n30 10 20\n7 1 1000000000 4\n");
    solve(in, std::cout);

    Lcg rng{23};
    int agree = 0;
    for (int t = 0; t < 3000; ++t) {
        int n = rng.range(1, 8), m = rng.range(1, 8);
        std::vector<ll> a(n), b(m);
        for (auto& x : a) x = rng.range(-20, 20);  // duplicates and negatives on purpose
        for (auto& x : b) x = rng.range(-20, 20);
        ll k = rng.range(1, n * m);
        if (kthBundle(a, b, k) != bruteBundle(a, b, k)) {
            std::println("mismatch on test {}", t);
            return 0;
        }
        ++agree;
    }
    std::println("{} random cases: binary search agrees with sorting all sums", agree);
}
Output

Press Run to compile and run this example.

24
3000 random cases: binary search agrees with sorting all sums

The sorted bundle prices of the sample begin 11, 14, 17, 21, 24, so the fifth is 24. The stress test uses values from -20 to 20 although real prices are positive: a wider input space than the statement needs costs nothing and catches more, and many duplicates exercise the "count equal prices separately" rule.

Complexity. Sorting is O(n log n + m log m). The binary search runs over a value range of at most 2·10^9, so about 31 iterations, each O(n + m): roughly 1.2·10^7 steps at the limit.