Wednesday, October 28, 2026

Dijkstra's Algorithm in C++ with a Priority Queue

Breadth-first search finds shortest paths when every edge has the same length. As soon as edges have different weights, such as road lengths or travel times, BFS gives wrong answers. Dijkstra's algorithm is the standard fix for graphs whose edge weights are non-negative.

The problem

There are n cities and m one-way flights, each with a cost. Find the cheapest cost from city 1 to every city. This is CSES 1671 – Shortest Routes I, with n up to 105 and m up to 2 × 105.

The idea

Keep a tentative distance for every node, starting at 0 for the source and infinity for the rest. Then repeat:

  1. Take the unfinished node u with the smallest tentative distance. That distance is now final.
  2. For every edge u → v with weight w, check whether going through u is cheaper: if dist[u] + w < dist[v], update dist[v]. This is called relaxing the edge.

Why the smallest node is final

Suppose u has the smallest tentative distance d. Any other route to u would have to leave the set of finished nodes through some unfinished node, whose distance is already at least d. Adding more edges can only make that route longer, because weights are non-negative. So no route can beat d.

That argument is exactly where negative edges break things: a negative edge could make a longer-looking path cheaper later. With negative weights, use Bellman–Ford instead.

Finding the minimum quickly

Scanning all nodes for the minimum each time is O(n2). A min-heap (priority_queue with greater<>) gives the smallest entry in O(log n). Whenever we improve a distance, we push the new (distance, node) pair.

A node can end up in the heap several times with older, larger distances. Instead of deleting those, we simply skip any entry whose distance does not match the current dist[u]. These are called stale entries.

Code (C++)

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m;
    cin >> n >> m;
    vector<vector<pair<int, long long>>> adj(n + 1);
    for (int i = 0; i < m; i++) {
        int a, b;
        long long w;
        cin >> a >> b >> w;
        adj[a].push_back({b, w});   // directed edge a -> b with weight w
    }

    const long long INF = LLONG_MAX;
    vector<long long> dist(n + 1, INF);
    // min-heap of (distance, node)
    priority_queue<pair<long long, int>, vector<pair<long long, int>>, greater<>> pq;

    dist[1] = 0;
    pq.push({0, 1});
    while (!pq.empty()) {
        auto [d, u] = pq.top();
        pq.pop();
        if (d != dist[u]) continue;          // stale entry, skip it
        for (auto [v, w] : adj[u]) {
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                pq.push({dist[v], v});
            }
        }
    }

    for (int i = 1; i <= n; i++)
        cout << dist[i] << (i == n ? '\n' : ' ');
    return 0;
}

Example

3 4
1 2 6
1 3 2
3 2 3
1 3 4

The direct flight 1 → 2 costs 6, but 1 → 3 → 2 costs 2 + 3 = 5. Output: 0 5 2.

Step by step: pop city 1 (distance 0) and relax its edges, so city 2 gets 6 and city 3 gets 2. Pop city 3 (distance 2) and relax 3 → 2, improving city 2 to 5. Pop city 2 (distance 5). The old entry (6, city 2) is popped later and skipped as stale.

Complexity

Each edge can push at most one entry, so the heap holds O(m) entries. Total time is O((n + m) log m), and memory is O(n + m).

Common mistakes

  • Using a max-heap. C++'s priority_queue returns the largest element by default. Use greater<>, or push negative distances.
  • Forgetting the stale check. Without if (d != dist[u]) continue; the answer is still correct, but on some graphs the algorithm becomes much slower.
  • Using int distances. A path of 105 edges with weight 109 each sums to 1014. Use long long.
  • Undirected graphs. If roads go both ways, add each edge in both directions.

Practice problems

If BFS is still new to you, start with the BFS deep dive. Dijkstra is the same idea with a priority queue in place of a plain queue.

Disjoint Set Union (Union-Find) Explained with C++ Code

Disjoint Set Union (DSU), also called Union-Find, answers one question extremely quickly: are these two things in the same group? It also lets you merge groups as you go. That makes it perfect for problems where connections are added over time, such as roads being built between cities.

The problem

There are n cities and no roads. Roads are built one at a time. After each new road, print how many separate connected groups of cities there are, and the size of the largest group. This is CSES 1676 – Road Construction, with up to 105 cities and 2 × 105 roads.

Running a BFS or DFS after every road would cost O(n + m) each time, which is far too slow. DSU handles each road in almost constant time.

How DSU represents groups

Each group is a tree. Every element points to a parent, and the root of the tree points to itself. The root is the “leader” that represents the whole group. Two elements are in the same group exactly when they have the same leader.

  • find(x): follow parent pointers from x until you reach the root.
  • unite(a, b): find both leaders. If they differ, make one leader point to the other. The two groups are now one.

Two optimisations that make it fast

Without care, the trees can become long chains, and find becomes O(n). Two tricks prevent that:

  1. Union by size. When merging, attach the smaller tree under the larger one. A node's depth only increases when its group at least doubles in size, so no tree gets deeper than log2 n.
  2. Path compression. During find, make nodes point closer to the root. The code below uses “path halving”, which points each visited node at its grandparent. Later lookups on the same path become almost instant.

With both, each operation takes amortised O(α(n)) time, where α is the inverse Ackermann function. It grows so slowly that it is at most 4 for any input that fits in the universe, so in practice it is constant.

Code (C++)

#include <bits/stdc++.h>
using namespace std;

struct DSU {
    vector<int> parent, sz;
    DSU(int n) : parent(n + 1), sz(n + 1, 1) {
        iota(parent.begin(), parent.end(), 0);   // everyone is their own leader
    }
    int find(int x) {
        while (parent[x] != x) {
            parent[x] = parent[parent[x]];       // path halving
            x = parent[x];
        }
        return x;
    }
    // Returns true if a and b were in different sets.
    bool unite(int a, int b) {
        a = find(a);
        b = find(b);
        if (a == b) return false;
        if (sz[a] < sz[b]) swap(a, b);           // union by size
        parent[b] = a;
        sz[a] += sz[b];
        return true;
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m;
    cin >> n >> m;
    DSU dsu(n);
    int components = n, largest = 1;

    while (m--) {
        int a, b;
        cin >> a >> b;
        if (dsu.unite(a, b)) {
            components--;
            largest = max(largest, dsu.sz[dsu.find(a)]);
        }
        cout << components << ' ' << largest << '\n';
    }
    return 0;
}

Each successful unite merges two groups, so the number of components drops by one. The size of the new group is stored at its leader, which makes the “largest group” easy to track.

Example

5 3
1 2
1 3
4 5

Output:

4 2
3 3
2 3

After road 1–2 there are 4 groups and the biggest has 2 cities. After 1–3, that group grows to 3. After 4–5, there are 2 groups: {1, 2, 3} and {4, 5}.

Complexity

O((n + m) · α(n)) time and O(n) memory.

Where DSU shows up

  • Kruskal's minimum spanning tree. Sort edges by weight and add each one if DSU says its endpoints are not yet connected.
  • Cycle detection in an undirected graph: an edge whose endpoints already share a leader closes a cycle.
  • Connectivity while edges are added: answer “are u and v connected?” at any moment as the graph grows.

DSU cannot easily remove edges. If a problem deletes connections, a common trick is to process everything in reverse, turning deletions into additions.

Practice problems

Bitmasks in Competitive Programming: Trying Every Subset

When a problem has n ≤ 20 and asks you to choose some of the items, the intended solution is often to try every possible choice. There are 2n of them, about a million for n = 20, which is fast enough. Bitmasks are the cleanest way to list them all.

An integer as a set

Think of the bits of an integer as yes/no switches. With n items, bit i of a number says whether item i is in the set. For 4 items:

  • 0000 (0) is the empty set.
  • 0101 (5) is the set {item 0, item 2}.
  • 1111 (15) is all four items.

Every integer from 0 to 2n − 1 is a different subset, and every subset appears exactly once. So a single for loop over those integers visits all subsets.

The bit operations you need

GoalExpression
Is item i in the set?mask & (1 << i)
Add item imask | (1 << i)
Remove item imask & ~(1 << i)
Number of items in the set__builtin_popcount(mask)
Number of subsets of n items1 << n

Example problem: dividing apples

There are n ≤ 20 apples with weights p1, …, pn. Split them into two groups so that the difference between the group weights is as small as possible. This is CSES 1623 – Apple Division.

Each mask chooses which apples go into group A. Group B gets the rest, so its weight is the total minus A. The difference is |total − 2A|. Try every mask and keep the smallest difference.

Code (C++)

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;
    vector<long long> p(n);
    long long total = 0;
    for (auto &x : p) { cin >> x; total += x; }

    long long best = LLONG_MAX;
    // Each mask from 0 to 2^n - 1 describes one way to choose group A:
    // bit i set means apple i goes to group A.
    for (int mask = 0; mask < (1 << n); mask++) {
        long long a = 0;
        for (int i = 0; i < n; i++)
            if (mask & (1 << i)) a += p[i];
        best = min(best, llabs(total - 2 * a));   // |A - B| where B = total - A
    }
    cout << best << '\n';
    return 0;
}

Example

Weights 3 2 7 4 1, total 17. Putting {3, 4, 1} in one group (8) and {2, 7} in the other (9) gives a difference of 1. A difference of 0 is impossible because the total is odd, so 1 is optimal.

Complexity

2n masks, and O(n) work for each, gives O(2n · n). For n = 20 that is about 2 × 107 operations. For n = 30 it would be 3 × 1010, which is why this technique is tied to small limits. My post on reading constraints has more on matching limits to techniques.

Common mistakes

  • Writing 1 << n when n can be 31 or more. That overflows a 32-bit int; use 1LL << n.
  • Operator precedence. mask & 1 << i == 0 does not mean what it looks like, because == binds tighter than &. Add parentheses: (mask & (1 << i)) == 0.
  • Recomputing sums from scratch for each mask when n is near the limit. It works here, but for tighter problems you can build the sum of a mask from a smaller mask in O(1).

Where to go next

Bitmasks are also used as DP states. In “bitmask DP”, dp[mask] stores the best answer for a particular subset of items already used. That is how the travelling salesman problem is solved for n ≤ 20 in O(2n · n2).

Practice problems

Dynamic Programming for Beginners: From Recursion to a DP Table

Dynamic programming (DP) has a reputation for being the hardest topic for beginners. A lot of that comes from the name, which says nothing about what it does. The real idea is simple: if a problem breaks into smaller versions of itself, and those smaller versions repeat, solve each one once and store the answer.

This post builds up DP with two classic problems, the frog and coin change, and gives a checklist you can reuse.

Problem 1: the frog

There are n stones in a row with heights h1, …, hn. A frog starts on stone 1 and wants to reach stone n. From stone i it can jump to i + 1 or i + 2, and a jump costs the absolute difference in height. What is the minimum total cost? This is AtCoder DP Contest A – Frog 1.

Step 1: define the state

Let dp[i] = the minimum cost to reach stone i. Choosing what dp[i] means is the most important decision in any DP. A good state is small and captures everything the future depends on. Here, once the frog is on stone i, it does not matter how it got there.

Step 2: write the transition

The frog reaches stone i either from stone i − 1 or from stone i − 2. So:

dp[i] = min( dp[i-1] + |h[i] - h[i-1]|,
             dp[i-2] + |h[i] - h[i-2]| )

Step 3: base case and order

The cost to reach the first stone is 0, since the frog starts there. Every dp[i] only uses smaller indices, so we fill the table from left to right. (The code below numbers stones from 0, as C++ arrays do.)

Why not plain recursion?

You could write the formula as a recursive function. But computing dp[i] calls dp[i − 1] and dp[i − 2], each of which calls two more, and the number of calls grows like the Fibonacci numbers, which is exponential. The same values get recomputed again and again. Storing each answer the first time (either with memoization or with a table like below) makes it linear.

Code (C++)

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;
    vector<int> h(n);
    for (auto &x : h) cin >> x;

    // dp[i] = minimum cost to reach stone i
    vector<long long> dp(n, LLONG_MAX);
    dp[0] = 0;
    for (int i = 1; i < n; i++) {
        dp[i] = dp[i - 1] + abs(h[i] - h[i - 1]);
        if (i >= 2)
            dp[i] = min(dp[i], dp[i - 2] + abs(h[i] - h[i - 2]));
    }
    cout << dp[n - 1] << '\n';
    return 0;
}

For heights 10 30 40 20, the best path is 1 → 2 → 4 with cost 20 + 10 = 30. The table is dp = [0, 20, 30, 30].

Complexity: O(n) time and memory.

Problem 2: fewest coins

You have coin values c1, …, cn, with unlimited coins of each. What is the fewest number of coins that add up to exactly x? Print −1 if it is impossible. This is CSES 1634 – Minimizing Coins.

Greedy (always take the largest coin) fails here. With coins 1, 5 and 7 and x = 10, greedy takes 7 + 1 + 1 + 1 = 4 coins, but 5 + 5 uses only 2.

  • State: dp[s] = fewest coins that sum to exactly s.
  • Transition: the last coin used is some c, so dp[s] = min over coins of dp[s − c] + 1.
  • Base case: dp[0] = 0, and every other value starts as “impossible”.
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n, x;
    cin >> n >> x;
    vector<int> c(n);
    for (auto &v : c) cin >> v;

    const int INF = 1e9;
    // dp[s] = fewest coins that add up to exactly s
    vector<int> dp(x + 1, INF);
    dp[0] = 0;
    for (int s = 1; s <= x; s++)
        for (int coin : c)
            if (coin <= s && dp[s - coin] != INF)
                dp[s] = min(dp[s], dp[s - coin] + 1);

    cout << (dp[x] == INF ? -1 : dp[x]) << '\n';
    return 0;
}

Complexity: O(n · x) time and O(x) memory.

A checklist for any DP problem

  1. State: what does dp[…] mean, in one sentence?
  2. Transition: what was the last decision, and how does it connect to smaller states?
  3. Base cases: which states can you answer directly?
  4. Order: in which order must states be filled so that dependencies are ready?
  5. Answer: which state, or combination of states, is the final answer?

If you can answer all five in plain words before coding, the code is usually short.

Practice problems

Greedy Interval Scheduling: Why You Sort by End Time

Greedy algorithms make the choice that looks best right now and never go back. Sometimes that works perfectly and sometimes it fails badly. Interval scheduling is the classic case where it works, and it is a great example for learning how to prove that a greedy choice is correct rather than just hoping.

The problem

There are n movies at a festival. Movie i starts at time si and ends at time ei. You can only watch one movie at a time, and you must watch each one from start to finish. A movie may start at the exact moment another one ends. What is the largest number of movies you can watch? This is CSES 1629 – Movie Festival, with n up to 2 × 105.

Greedy ideas that do not work

  • Earliest start first. A very long movie that starts first would block everything else.
  • Shortest movie first. A short movie can sit right across the boundary of two longer movies that would otherwise both fit, so choosing it costs you two.

Trying to break your own greedy idea with small examples like these is the most useful habit for greedy problems.

The rule that works: earliest end first

Sort movies by end time. Go through them in that order, and watch a movie whenever it starts after (or exactly when) the last chosen movie ended.

The intuition is that finishing early leaves as much of the day free as possible for everything that comes after.

Why it is correct (exchange argument)

Take any optimal schedule and look at its first movie. Our greedy picks the movie with the earliest end time overall. That greedy movie ends no later than the optimal schedule's first movie, so we can swap it in. The rest of the optimal schedule still fits, because everything in it starts after the old first movie ended, which is no earlier than when the greedy movie ends.

So there is an optimal schedule that begins with the greedy choice. Remove that movie and everything that overlaps it, and the same argument applies to the rest. By repeating this, the greedy schedule is as long as an optimal one.

Code (C++)

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;
    vector<pair<int, int>> movies(n);   // stored as (end, start)
    for (auto &[e, s] : movies) cin >> s >> e;

    sort(movies.begin(), movies.end());  // earliest end time first

    int watched = 0;
    int free_at = 0;                     // when we are next free
    for (auto [e, s] : movies) {
        if (s >= free_at) {
            watched++;
            free_at = e;
        }
    }
    cout << watched << '\n';
    return 0;
}

Storing each pair as (end, start) means the default sort orders by end time, with no custom comparator needed.

Example

3
3 5
4 9
5 8

Sorted by end time: (3, 5), (5, 8), (4, 9). We watch 3–5, then 5–8 (it starts right as the first ends), and skip 4–9 because it starts before 8. Answer: 2.

Complexity

Sorting is O(n log n) and the scan is O(n).

Be careful with the boundary

Whether a movie may start exactly when the previous one ends decides between >= and >. Read the statement for that detail every time. Getting it wrong passes the samples surprisingly often and then fails on hidden tests.

Related problems

  • Minimum number of points to hit every interval. Same sorting, and the same proof idea: place a point at the end of the earliest-ending interval not yet hit.
  • Minimum rooms so that every meeting fits. Different greedy: sort by start time and track when rooms become free, usually with a priority queue.

Practice problems

If you are newer to proofs like this, Domino Piling (50A) uses the same “upper bound plus construction” style of reasoning in a simpler setting.

GCD and LCM in C++: Euclid's Algorithm Explained

The greatest common divisor (GCD) is one of the oldest algorithms in mathematics, and it still shows up constantly in contest problems: simplifying fractions, synchronising cycles, and in clever tricks that turn a hard-looking problem into one line. This post covers how Euclid's algorithm works, why it is fast, and the LCM overflow trap.

Definitions

gcd(a, b) is the largest number that divides both a and b. lcm(a, b) is the smallest positive number that both a and b divide. For example gcd(12, 18) = 6 and lcm(12, 18) = 36. They are linked by a simple identity:

gcd(a, b) × lcm(a, b) = a × b

Euclid's algorithm

The key fact is gcd(a, b) = gcd(b, a mod b). Any number that divides both a and b also divides a − kb for any k, and a mod b is exactly such a number. The reverse also holds, so both pairs have the same common divisors and therefore the same greatest one.

Keep replacing the pair until the second number is 0. Then gcd(g, 0) = g.

Example for gcd(252, 105):

gcd(252, 105) -> 252 mod 105 = 42
gcd(105, 42)  -> 105 mod 42  = 21
gcd(42, 21)   -> 42 mod 21   = 0
gcd(21, 0)    = 21

Why it is fast

After two steps, the larger number always drops to less than half of what it was. So the number of steps is O(log min(a, b)), at most about 90 steps for numbers up to 1018. The slowest inputs are consecutive Fibonacci numbers, which is a fun fact worth remembering.

Code (C++)

This program reads n numbers and prints the GCD and LCM of all of them.

#include <bits/stdc++.h>
using namespace std;

long long gcd_ll(long long a, long long b) {
    while (b != 0) {
        long long r = a % b;
        a = b;
        b = r;
    }
    return a;
}

int main() {
    int n;
    cin >> n;
    vector<long long> a(n);
    for (auto &x : a) cin >> x;

    long long g = 0, l = 1;
    for (long long x : a) {
        g = gcd_ll(g, x);            // gcd(0, x) = x, so 0 is a safe start
        l = l / gcd_ll(l, x) * x;    // divide first to keep numbers small
    }
    cout << g << ' ' << l << '\n';
    return 0;
}

Since C++17 you can also use std::gcd and std::lcm from <numeric>. Writing it yourself once is still worth doing so you know what is inside.

The LCM overflow trap

The obvious formula a * b / gcd(a, b) multiplies first. For two numbers near 1010 the product is 1020, which overflows long long even though the LCM itself might be small. Dividing first, a / gcd(a, b) * b, avoids the giant intermediate value. The final LCM can still be too large to store if the numbers share few factors, so check the problem's limits.

Handy properties

  • gcd(a, 0) = a, which makes 0 a safe starting value when taking the GCD of a list.
  • The GCD of a list can be computed one element at a time: gcd(a, b, c) = gcd(gcd(a, b), c).
  • gcd(a, b) = gcd(a, b − a). This lets you rewrite expressions like gcd(a1 + x, a2 + x, …) in terms of differences that do not depend on x, which is the whole trick behind Codeforces 1458A – Row GCD.
  • For factorials, gcd(A!, B!) is just min(A, B)!, because the smaller factorial divides the larger one. That is all Codeforces 822A – I'm bored with life asks for.

Practice problems

For more number theory, see the Lucky Division walkthrough.

Binary Exponentiation: Compute a^b mod m in O(log b)

Many problems ask for an answer “modulo 109 + 7”, and many of those need a large power such as 2n or ab with b up to 1018. Multiplying a by itself b times is hopeless. Binary exponentiation does it in about 60 multiplications.

The idea: square instead of multiply

Notice that a8 = ((a2)2)2. That is only three multiplications instead of seven. Repeated squaring gives us a, a2, a4, a8, a16, … very cheaply.

Any exponent can be written as a sum of powers of two, which is exactly its binary representation. For example 13 = 8 + 4 + 1, which is 1101 in binary. So

a13 = a8 × a4 × a1

We walk through the bits of b from the lowest. At each step, the current a holds a2k. If the bit is 1, multiply it into the result. Then square a and shift b right.

Tracing a = 3, b = 13

b (binary)lowest bitcurrent aresult
1101131 × 3 = 3
110093
111813 × 81 = 243
116561243 × 6561 = 1 594 323

And 313 is indeed 1 594 323, found with a handful of multiplications.

Code (C++)

#include <bits/stdc++.h>
using namespace std;

const long long MOD = 1'000'000'007;

// Computes (a^b) mod m in O(log b) multiplications.
long long power(long long a, long long b, long long m) {
    long long result = 1 % m;
    a %= m;
    while (b > 0) {
        if (b & 1) result = result * a % m;   // this bit of b is set
        a = a * a % m;                        // a, a^2, a^4, a^8, ...
        b >>= 1;
    }
    return result;
}

int main() {
    int q;
    cin >> q;
    while (q--) {
        long long a, b;
        cin >> a >> b;
        cout << power(a, b, MOD) << '\n';
    }
    return 0;
}

The input format matches CSES 1095 – Exponentiation: a number of queries, then a and b on each line.

Complexity

The loop runs once per bit of b, so O(log b) multiplications. For b up to 1018 that is at most 60 iterations.

Details that cause Wrong Answer

  • Overflow. Both factors are below 109 + 7, so their product is below about 1018, which fits in long long. With int it overflows. If the modulus is close to 1018, even long long is not enough and you need __int128.
  • Reduce a first. If a itself is larger than the modulus, take a %= m before the loop.
  • b = 0. Anything to the power 0 is 1, including 00 by the usual convention in these problems. The code starts result at 1 and handles it without a special case.

Where it shows up

  • Modular inverse. When the modulus p is prime, Fermat's little theorem gives a−1 ≡ ap−2 (mod p). That is how you “divide” under a modulus, for example when computing binomial coefficients.
  • Huge exponents. To compute abc mod p, reduce the exponent bc modulo p − 1 first (again by Fermat), then use binary exponentiation twice.
  • Matrix exponentiation. The same loop works with matrices instead of numbers, which computes the n-th Fibonacci number in O(log n).

Practice problems

Sieve of Eratosthenes: Find All Primes up to N Fast (C++)

Checking whether one number is prime is easy: try dividing it by everything up to its square root. But when a problem needs to know about every number up to 106 or 107, repeating that check for each number is too slow. The Sieve of Eratosthenes, which is more than two thousand years old, finds all of them at once.

The idea

Write down all numbers from 2 to n, and assume they are all prime. Then:

  1. Take the smallest number that is still marked prime. Call it p. It really is prime, because no smaller number divided it.
  2. Cross out every multiple of p: they have p as a factor, so they are composite.
  3. Move on to the next number that is still marked and repeat.

Whatever survives is prime.

Example with n = 30

  • p = 2: cross out 4, 6, 8, …, 30.
  • p = 3: cross out 9, 15, 21, 27 (12, 18, 24 and 30 were already gone).
  • p = 5: cross out 25 (30 was already gone).
  • p = 7: 7 × 7 = 49 > 30, so we can stop.

The survivors are 2, 3, 5, 7, 11, 13, 17, 19, 23, 29: ten primes.

Two small optimisations that matter

Start crossing at p × p. Any smaller multiple k × p with k < p has a prime factor smaller than p, so it was already crossed out earlier. For p = 5 above, 10, 15 and 20 were already gone, so we start at 25.

Stop the outer loop at √n. Once p × p > n, there is nothing left to cross out. Every composite number up to n has a prime factor at most √n, so it has already been handled.

Code (C++)

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;

    // is_prime[x] is true if x is prime, for 0 <= x <= n
    vector<bool> is_prime(n + 1, true);
    is_prime[0] = false;
    if (n >= 1) is_prime[1] = false;

    for (long long p = 2; p * p <= n; p++) {
        if (!is_prime[p]) continue;
        for (long long m = p * p; m <= n; m += p)
            is_prime[m] = false;
    }

    int count = 0;
    for (int x = 2; x <= n; x++)
        if (is_prime[x]) count++;
    cout << count << '\n';
    return 0;
}

The program prints how many primes there are up to n. For n = 106 it prints 78 498, and for n = 107 it prints 664 579. Once is_prime is built, each “is x prime?” question is a single array lookup.

Complexity

For each prime p, the inner loop does about n / p steps. Summing n/2 + n/3 + n/5 + … over primes gives O(n log log n), which is almost linear in practice. Memory is O(n). vector<bool> stores one bit per entry, so a sieve up to 107 needs only about 1.25 MB.

Common mistakes

  • Forgetting that 0 and 1 are not prime.
  • Writing p * p <= n with int when n is close to the int limit. The product can overflow, so use long long for p, as in the code above.
  • Re-running the sieve for every test case. Build it once, up to the largest value you will need.

A useful variation: smallest prime factor

Instead of a true/false flag, store for every number its smallest prime factor. Then you can factorise any number up to n in O(log n) by repeatedly dividing by its smallest prime factor. This is the standard way to factorise many numbers quickly.

Practice problems

For a gentler number theory warm-up, see Lucky Division (122A).

The Two Pointers Technique: Sliding Windows in O(n)

Many array problems ask about contiguous segments: the longest one with some property, the number of segments below a limit, and so on. Checking every segment takes O(n2) time. The two pointers technique often brings that down to O(n) by moving a window across the array instead of rebuilding it from scratch.

The problem

Valera has t free minutes and a row of n books. Book i takes ai minutes to read. He picks a starting book and reads books in order until he runs out of time (he never stops halfway through a book). What is the largest number of books he can read? This is Codeforces 279B – Books, with n up to 105.

In other words: find the longest contiguous segment whose sum is at most t.

Brute force first

Try every start l, extend r while the sum stays within t, and remember the best length. That is correct but O(n2) in the worst case, about 1010 steps here.

The key observation

Suppose the best window that ends at position r starts at position l. When we move to r + 1, the best window ending there cannot start before l: if it did, it would contain the window [l − 1, r], whose sum is already too large (all times are positive). So the left end only ever moves right.

That gives the algorithm:

  1. Move the right pointer one step and add the new book to the running sum.
  2. While the sum is more than t, remove the leftmost book and move the left pointer right.
  3. The window [left, right] is now the longest valid one ending at right. Update the answer.

Code (C++)

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    long long t;
    cin >> n >> t;
    vector<long long> a(n);
    for (auto &x : a) cin >> x;

    int best = 0;
    long long sum = 0;   // sum of a[left..right]
    int left = 0;
    for (int right = 0; right < n; right++) {
        sum += a[right];
        while (sum > t) {        // window too expensive: shrink from the left
            sum -= a[left];
            left++;
        }
        best = max(best, right - left + 1);
    }
    cout << best << '\n';
    return 0;
}

Walking through an example

t = 5, books 3 1 2 1.

  • right = 0: window [3], sum 3. Best = 1.
  • right = 1: window [3, 1], sum 4. Best = 2.
  • right = 2: sum 6 > 5, so drop the 3. Window [1, 2], sum 3. Best stays 2.
  • right = 3: window [1, 2, 1], sum 4. Best = 3.

Why it is O(n)

There is a while loop inside a for loop, which looks quadratic. But each pointer only moves forward, and each moves at most n times in total. Counting all the steps of both pointers gives at most 2n, so the whole algorithm is O(n) time and O(1) extra memory.

When two pointers works, and when it does not

The technique relies on the window property being monotonic: shrinking a valid window keeps it valid, and growing an invalid one keeps it invalid. With positive numbers and “sum ≤ t”, that holds.

It breaks if the array can contain negative numbers. Then adding an element can make the sum smaller, and the left pointer might need to move back. Those problems usually need prefix sums with sorting or a data structure instead.

Another common form: two pointers on a sorted array

To find two numbers in a sorted array that add up to x, start one pointer at each end. If their sum is too small, move the left pointer right; if too large, move the right pointer left. Each step rules out one element for good, so it is also O(n) after sorting. CSES 1640 – Sum of Two Values is a good exercise for this version.

Practice problems

Two pointers pairs naturally with run-counting problems like 266A, where you also compare each element with what came before.

Binary Search on the Answer: A Step-by-Step Guide with C++ Code

Most people learn binary search as “find a number in a sorted array”. In contests, the more powerful version is binary search on the answer: instead of searching an array, you search over possible answers and test each guess with a yes/no function. Once you see the pattern, a whole family of “minimum time” and “maximum size” problems becomes routine.

A motivating problem

A factory has n machines. Machine i takes ti seconds to make one product, and all machines work at the same time. What is the shortest time needed to make k products? Here n can be 2 × 105 and k and the ti up to 109. This is CSES 1620 – Factory Machines.

Computing the answer directly is awkward. But a different question is easy: “Is T seconds enough?” In T seconds, machine i makes ⌊T / ti⌋ products. Add those up and compare with k. That takes O(n).

The key property: monotonicity

If T seconds are enough, then T + 1 seconds are also enough. If T is not enough, nothing smaller is either. So if you write the answers to “is T enough?” for T = 0, 1, 2, … you get:

NO NO NO ... NO YES YES YES ...

We want the position of the first YES. A sequence that switches only once can be binary searched, exactly like a sorted array. Each check halves the range of possible answers, so even a range up to 1018 takes only about 60 checks.

A template that avoids off-by-one bugs

I use two variables with a fixed meaning:

  • lo is a value that is definitely NO.
  • hi is a value that is definitely YES.

While they are more than one apart, test the middle and move whichever end it belongs to. When the loop ends, hi is the first YES. Because the meaning of each variable never changes, there is no guessing about mid + 1 or mid - 1.

For this problem, lo = 0 works because zero seconds produce nothing. For hi, the fastest machine alone makes k products in min(t) × k seconds, so that is always enough.

Code (C++)

#include <bits/stdc++.h>
using namespace std;

int n;
long long k;
vector<long long> t;

// Can all machines together make at least k products in `time` seconds?
bool enough(long long time) {
    long long made = 0;
    for (long long ti : t) {
        made += time / ti;
        if (made >= k) return true;   // stop early to avoid overflow
    }
    return false;
}

int main() {
    cin >> n >> k;
    t.resize(n);
    for (auto &x : t) cin >> x;

    long long lo = 0;                                   // not enough
    long long hi = *min_element(t.begin(), t.end()) * k; // surely enough
    while (hi - lo > 1) {
        long long mid = lo + (hi - lo) / 2;
        if (enough(mid)) hi = mid;
        else lo = mid;
    }
    cout << hi << '\n';
    return 0;
}

Walking through an example

Machines take 3, 2 and 5 seconds, and we need k = 7 products.

  • T = 7: 2 + 3 + 1 = 6 products. Not enough.
  • T = 8: 2 + 4 + 1 = 7 products. Enough.

So the answer is 8. The binary search finds this in a handful of checks, starting from lo = 0 and hi = 2 × 7 = 14.

Complexity

Each check is O(n), and we do about log2(hi) checks, so O(n log V) where V is the upper bound. Here that is roughly 2 × 105 × 60 = 1.2 × 107 operations.

The overflow trap

hi can be 109 × 109 = 1018, so everything must be long long. Inside the check, adding up T / ti over 2 × 105 machines could exceed even that, which is why the function returns as soon as the count reaches k. Also compute the middle as lo + (hi - lo) / 2 instead of (lo + hi) / 2, so the sum does not overflow.

How to recognise these problems

  • The statement asks for a minimum value that makes something possible, or a maximum value that keeps something possible.
  • Checking one specific candidate is much easier than finding the best one directly.
  • Making the candidate “more generous” never turns a YES into a NO.

If all three hold, write the check function first and wrap it in the template above.

Practice problems