Home / Olympiads / Informatics / Roadmap
IOI · where to start

Informatics olympiad roadmap.

The IOI expects you to design an algorithm and code it so that it runs fast on very large inputs. IGCSE 0478 and A Level 9618 give you the programming base, plus binary search, sorting, recursion, stacks, queues, big-O and a first look at graphs. Almost everything else on this roadmap, from greedy proofs and dynamic programming to union-find and segment trees, is new, so the bridge notes and the practice problems carry most of the load.

Go through the 38 topics below. Tick Got it when you are confident; topics marked New have a short bridge note to get you started.

0 of 38 ticked

Study plan

Stage 1: Secure the school core3-4 weeks
Get fluent at writing short Python programs that read input, loop and use arrays, functions and recursion. Revisit big-O and binary search from 9618 and learn to read a task's constraints.
Stage 2: Core contest techniques6-8 weeks
Learn complete search, sorting as a tool, greedy with exchange arguments, prefix sums, two pointers and binary search on the answer. Solve the feeder problems and test every solution against a brute force.
Stage 3: Dynamic programming and graphs8-10 weeks
Work through 1D and 2D DP, then BFS, DFS, Dijkstra, union-find, minimum spanning trees and topological sort, with gcd and modular arithmetic alongside. These cover most national round tasks.
Stage 4: International level10-12 weeks or more
Add tree DP, bitmask DP, Fenwick and segment trees and combinatorics modulo a prime. Practise full past papers under time limits, aiming for every subtask you can reach.

Programming and complexity

Big-O and reading the constraintsGoes further

Beyond 9618 big-O: read the limits to choose the complexity you need before coding, and know how fast Python really is.

Learn it Comparing algorithms: Big O notation (A Level 9618)

Practise The corridor of lamps Counting fizzy numbers

Read the bridge note

Olympiad tasks are marked on large hidden tests, so a correct but slow program scores only the small subtasks.

In 9618 you compare algorithms with big-O. In a contest you use it the other way round: read the limits first, then decide which complexity you need before you write any code.

A rough guide: C++ does about $10^8$ simple steps a second, Python about $10^7$. With a one or two second limit in Python this table is a safe start:

Largest $n$Complexity that fitsTypical idea
about 10$O(n!)$try every order
about 20$O(2^n \cdot n)$try every subset, bitmask DP
about 300$O(n^3)$three nested loops, interval DP
about 3000$O(n^2)$all pairs, 2D DP
$2 \times 10^5$ to $10^6$$O(n \log n)$ or $O(n)$sorting, heaps, prefix sums, BFS
$10^{12}$ or more$O(\sqrt{n})$, $O(\log n)$, $O(1)$a formula, binary search

Subtasks follow the same pattern: a task with subtasks $n \le 20$, $n \le 3000$ and $n \le 2 \times 10^5$ is telling you the brute force, the $O(n^2)$ idea and the full solution.

Python also has its own costs. Reading input line by line with input() is slow for $10^5$ lines, so read everything at once:

import sys
data = sys.stdin.buffer.read().split()
n = int(data[0])
a = list(map(int, data[1:1 + n]))

Python integers never overflow, which is a gift, but every loop step costs more than in C++. Prefer built-ins such as sum, sorted and max, which run at C speed.

Worked example

A task has $n \le 2 \times 10^5$. Your first idea checks every pair of positions. Is it fast enough in Python, and what should you aim for?

Show the solution

Pairs: about $\frac{n^2}{2} = 2 \times 10^{10}$ steps. At $10^7$ steps a second that is over half an hour, so it only passes small subtasks.

Aim for $O(n \log n)$: $2 \times 10^5 \times 18 \approx 3.6 \times 10^6$ steps, well under a second. The limit is a hint that the intended solution sorts, uses a heap or uses a clever single pass.

Try it

The limits say $n \le 3000$ and your idea checks every pair once. Roughly how many steps is that, and is it fine in Python?

Show the answer

About $\frac{3000^2}{2} = 4.5 \times 10^6$ steps, so yes, it should run in well under a second.

Going deeper: A first-year university algorithms textbook chapter on asymptotic analysis, and the complexity tables in competitive programming handbooks.

Sorting and searching

Binary search on the answerNew

Turn a best-value task into a yes or no check, then binary search for the boundary.

Learn it Binary search (A Level 9618)

Practise Weather stations on the ridge

Read the bridge note

Many optimisation tasks become easy once you only have to check whether a given answer is possible.

School binary search finds a value in a sorted list. Olympiads use the same halving idea on the answer. Suppose the task asks for the largest $d$ such that something is possible. Write a function ok(d) that checks one value. If ok is monotone, true for every $d$ up to the answer and false above it, you can binary search for the boundary.

This turns "find the best" into "check a guess", which is usually a simple greedy pass. The cost is $O(\log R)$ calls of ok, where $R$ is the range of possible answers: about 30 calls for $R = 10^9$, about 60 for $R = 10^{18}$.

A template for the largest true value, assuming ok(lo) is true:

def largest_true(lo, hi, ok):
    while lo < hi:
        mid = (lo + hi + 1) // 2    # round up, or the loop can stall
        if ok(mid):
            lo = mid
        else:
            hi = mid - 1
    return lo

Three things to check every time: why ok is monotone (say it in one sentence), what the starting lo and hi are, and whether you want the largest true or the smallest true value. For the smallest true value use mid = (lo + hi) // 2, set hi = mid when true and lo = mid + 1 when false.

Worked example

You have ribbons of length 9, 14 and 20 cm. You want at least 7 pieces, all of the same whole-number length $L$ (offcuts are thrown away). What is the largest possible $L$?

Show the solution

A ribbon of length $x$ gives $\lfloor x / L \rfloor$ pieces. Longer pieces mean fewer of them, so ok(L) is monotone.

def largest_true(lo, hi, ok):
    while lo < hi:
        mid = (lo + hi + 1) // 2
        if ok(mid):
            lo = mid
        else:
            hi = mid - 1
    return lo

ribbons = [9, 14, 20]
need = 7
def ok(L):
    return sum(x // L for x in ribbons) >= need
print(largest_true(1, max(ribbons), ok))

It prints 5: lengths 5 give $1 + 2 + 4 = 7$ pieces, but lengths 6 give only $1 + 2 + 3 = 6$.

Try it

Same ribbons, but now you need at least 10 pieces. What is the largest $L$?

Show the answer

$L = 4$: $2 + 3 + 5 = 10$ pieces. $L = 5$ gives only 7.

Going deeper: Search for practice sets titled "binary search on the answer" or "parametric search" in a competitive programming handbook.

Two pointers and sliding windowsNew

Scan blocks of consecutive items with two indices that only move forwards, for O(n) instead of O(n squared).

Read the bridge note

It replaces an $O(n^2)$ scan over all blocks with a single $O(n)$ pass.

Many tasks ask about blocks of consecutive items, such as the longest block with total at most $S$. There are about $\frac{n^2}{2}$ blocks, too many for $n = 10^5$. The two-pointer (sliding window) idea keeps a block from left to right and moves both ends only forwards.

It works when the condition is monotone in the block: if a block is good, every shorter block inside it is also good. With all values positive, a block with total at most $S$ stays good when you shrink it. So:

  1. Move right one step and add the new item.
  2. While the block is bad, remove the item at left and move left on.
  3. Now the block is the longest good block ending at right; record what you need.

Each pointer moves at most $n$ times, so the whole pass is $O(n)$ even though there is a loop inside a loop. Counting is just as easy: the number of good blocks ending at right is right - left + 1.

If values can be negative, shrinking a block can make its total larger, the condition is no longer monotone and the method breaks. Then use prefix sums with a dictionary or a sorted structure instead.

Worked example

The values are 2, 1, 3, 2, 4, 1 and $S = 6$. Find the length of the longest block with total at most 6.

Show the solution
a = [2, 1, 3, 2, 4, 1]
S = 6
left = 0
total = 0
best = 0
for right in range(len(a)):
    total += a[right]
    while total > S:
        total -= a[left]
        left += 1
    best = max(best, right - left + 1)
print(best)

It prints 3, for example from the block 2, 1, 3 or the block 1, 3, 2. No block of four has total at most 6: the smallest, 2, 1, 3, 2, already has total 8.

Try it

For the values 1, 2, 3, 1, how many blocks have total at most 5? Use the counting rule.

Show the answer

7. Ending at each position the windows are [1], [1, 2], [2, 3] and [3, 1], giving $1 + 2 + 2 + 2 = 7$.

Going deeper: The two pointers and sliding window sections of a competitive programming handbook.

Greedy methods, prefix sums and hashing

Greedy algorithms and exchange argumentsNew

Choose the locally best option, and prove with an exchange argument that it is globally best, or find a counterexample.

Practise Booking the school hall Weather stations on the ridge

Read the bridge note

Greedy algorithms are short and fast, but olympiads test whether you can tell when one is correct.

A greedy algorithm builds the answer one step at a time, always taking the choice that looks best now, and never goes back. It is usually a sort followed by one pass, so $O(n \log n)$.

The danger is that a natural rule can be wrong. Before you code a greedy rule:

  1. Try to break it with tiny examples (two or three items). If you find a counterexample, drop the rule.
  2. If it survives, give an exchange argument: take any best answer that disagrees with your greedy choice, swap in the greedy choice, and show the result is no worse. Then a best answer exists that agrees with greedy at every step.
  3. Still unsure? Compare it with a brute force on random small tests.

Classic correct rules: for the most non-overlapping bookings, take the one that ends first; to minimise total waiting time, serve the shortest job first; to cover points with fewest intervals, extend as far right as possible. Classic wrong rule: paying an amount with the fewest coins by always taking the largest coin works for usual currencies but not for every coin set, which is a sign the task wants dynamic programming.

Worked example

Four customers need 5, 2, 8 and 3 minutes of service. One server serves them one at a time. Each customer's waiting time ends when their own service finishes. In what order should they be served to make the total of these times smallest, and what is that total?

Show the solution

Serve the shortest job first: 2, 3, 5, 8. Finish times are 2, 5, 10, 18, total 35.

Exchange argument: if a longer job $x$ comes straight before a shorter job $y$, swapping them leaves everyone else unchanged, and the pair's finish times go from $t + x,\ t + x + y$ to $t + y,\ t + x + y$, which is smaller because $y \lt x$. So any order with a longer job before a shorter one can be improved.

jobs = [5, 2, 8, 3]
t = 0
total = 0
for d in sorted(jobs):
    t += d
    total += t
print(total)
Try it

Coins are worth 1, 3 and 4. To pay 6, the rule "always take the largest coin that fits" uses how many coins, and what is the true fewest?

Show the answer

Greedy takes 4 + 1 + 1, three coins. The best is 3 + 3, two coins, so the greedy rule is wrong for this coin set.

Going deeper: The greedy algorithms chapter of a university algorithms textbook, which proves several exchange arguments in full.

Prefix sums and difference arraysNew

Answer range sums in O(1), count blocks with a given sum, and apply many range updates at once.

Practise Fair shares of the takings

Read the bridge note

Many tasks ask for sums over many ranges, and prefix sums answer each one in $O(1)$.

For values $a_1, \dots, a_n$ define $P_0 = 0$ and $P_i = P_{i-1} + a_i$. Then the sum of a block is one subtraction:

$$a_l + a_{l+1} + \dots + a_r = P_r - P_{l-1}.$$

Building $P$ costs $O(n)$, then every query is $O(1)$, instead of $O(n)$ each.

Three extensions come up all the time:

  • Counting blocks. A block has sum 0 exactly when two prefix sums are equal. More generally, a block sum is divisible by $K$ when $P_r \equiv P_{l-1} \pmod K$. Count how often each prefix value appears in a dictionary; a value seen $c$ times gives $\frac{c(c-1)}{2}$ blocks.
  • 2D prefix sums. For a grid, $S_{i,j}$ is the sum of the rectangle from the top-left corner to $(i, j)$, with $S_{i,j} = a_{i,j} + S_{i-1,j} + S_{i,j-1} - S_{i-1,j-1}$. Any rectangle sum is then four lookups.
  • Difference arrays. To add $v$ to every item from $l$ to $r$, add $v$ at $l$ and subtract it at $r + 1$ in a difference array; one prefix sum at the end applies all the updates.
Worked example

The values are 3, $-1$, 4, 1, $-5$, 9. Find the sum of positions 2 to 5 using prefix sums.

Show the solution

$P = 0, 3, 2, 6, 7, 2, 11$. The sum is $P_5 - P_1 = 2 - 3 = -1$. Check: $-1 + 4 + 1 - 5 = -1$.

a = [3, -1, 4, 1, -5, 9]
P = [0]
for x in a:
    P.append(P[-1] + x)
l, r = 2, 5                  # positions counted from 1
print(P[r] - P[l - 1])
Try it

How many blocks of 1, $-1$, 2, $-2$ have sum 0? Use the equal prefix sums idea.

Show the answer

The prefix sums are 0, 1, 0, 2, 0. The value 0 appears 3 times, giving $\frac{3 \times 2}{2} = 3$ blocks: [1, $-1$], [2, $-2$] and the whole list.

Going deeper: The range queries chapter of a competitive programming handbook.

Recursion and search

Backtracking and pruningNew

Build a candidate step by step with recursion and abandon any branch that cannot lead to a valid or better answer.

Learn it Recursion and the call stack (A Level 9618)

Read the bridge note

When no clever method exists, a well-pruned search can still solve inputs far larger than plain brute force.

Backtracking builds a candidate one choice at a time with recursion. After each choice it checks whether the partial candidate can still be completed. If not, it abandons that branch at once (prunes) and undoes the last choice.

The shape is always the same: a recursive function place(k) that tries every option for step $k$, skips options that break a rule, makes the choice, calls place(k + 1) and then undoes the choice.

Pruning is where the speed comes from. Good pruning checks are cheap (keep sets or boolean arrays of what is used, rather than rescanning) and early (reject a bad choice as soon as it is made). For optimisation, also stop a branch when even the best possible completion cannot beat the best answer found so far.

Watch the cost of copying: pass indices and change shared lists in place, then undo, rather than building new lists at each call.

Worked example

In how many ways can 5 queens be placed on a $5 \times 5$ board so that no two share a row, a column or a diagonal?

Show the solution

Place one queen per row. A queen at $(r, c)$ blocks column $c$ and the diagonals $r - c$ and $r + c$; keep these in sets so each check is $O(1)$.

def queens(n):
    cols, d1, d2 = set(), set(), set()
    count = 0
    def place(r):
        nonlocal count
        if r == n:
            count += 1
            return
        for c in range(n):
            if c in cols or r - c in d1 or r + c in d2:
                continue                 # prune: this square is attacked
            cols.add(c); d1.add(r - c); d2.add(r + c)
            place(r + 1)
            cols.remove(c); d1.remove(r - c); d2.remove(r + c)
    place(0)
    return count
print(queens(5))

It prints 10. Without pruning you would test $5^5 = 3125$ full boards; with it, most branches die after two or three rows.

Try it

Use the same idea by hand for a $4 \times 4$ board. How many placements are there?

Show the answer

2: the queens in columns 2, 4, 1, 3 (row by row) and its mirror image 3, 1, 4, 2.

Going deeper: The backtracking section of a competitive programming handbook, and classic puzzles such as sudoku solvers to practise pruning.

Dynamic programming

Dynamic programming in one dimensionNew

Define a state, a recurrence, base cases and a filling order, then compute each subproblem once.

Practise The rising tower

Read the bridge note

Dynamic programming appears in almost every olympiad, usually where greedy fails and complete search is too slow.

Dynamic programming (DP) solves a problem by solving smaller versions of it, storing each answer so it is worked out once. You need four things, and it helps to write them down before coding:

  1. State: what $dp_i$ means, in one exact sentence, such as "the best total using the first $i$ items".
  2. Recurrence: how $dp_i$ follows from smaller states, usually by asking what happens to the last item.
  3. Base cases: the smallest states, filled in directly.
  4. Order: fill states so that everything a state needs is already known.

The cost is (number of states) times (work per state). If you see a recursive brute force that solves the same subproblem again and again, storing answers (memoisation) turns it into DP.

In Python, filling a list with a loop is usually faster and safer than a memoised recursive function, which can hit the recursion limit. Common 1D patterns: counting ways (add the options), best value (take the max of the options), and longest increasing subsequence, where $dp_i$ is the longest run ending at item $i$.

Worked example

Houses in a row hold 4, 1, 2, 7, 5, 3, 1 coins. You may visit any houses except two neighbours. What is the most coins you can collect?

Show the solution

State: $b_i$ = best total from the first $i + 1$ houses. Last house: either skip house $i$ (so $b_{i-1}$) or take it and skip house $i - 1$ (so $b_{i-2} + v_i$). So $b_i = \max(b_{i-1}, b_{i-2} + v_i)$.

v = [4, 1, 2, 7, 5, 3, 1]
b = [0] * len(v)
b[0] = v[0]
b[1] = max(v[0], v[1])
for i in range(2, len(v)):
    b[i] = max(b[i - 1], b[i - 2] + v[i])
print(b[-1])

The table is 4, 4, 6, 11, 11, 14, 14, so the answer is 14 (houses with 4, 7 and 3).

Try it

You climb a staircase taking 1 or 3 steps at a time. With $f(0) = 1$, how many ways are there to climb 7 steps?

Show the answer

$f(n) = f(n-1) + f(n-3)$, giving 1, 1, 1, 2, 3, 4, 6, 9, so $f(7) = 9$.

Going deeper: The dynamic programming chapter of a university algorithms textbook, then graded DP problem sets on online judges.

Two-dimensional DP: grids, knapsack, stringsNew

Use two indices for grid paths, 0/1 knapsack and longest common subsequence, and save memory by keeping one row.

Practise Paths through the orchard The rising tower

Read the bridge note

Knapsack, grid paths and string matching are standard olympiad building blocks, and all are two-index DP.

When one index is not enough to describe a subproblem, use two. The state $dp_{i,j}$ might mean "using the first $i$ items with capacity $j$", "the cell in row $i$, column $j$" or "the first $i$ letters of one word and the first $j$ of another".

Three classics to know:

  • Grid paths (moves right or down): $dp_{i,j} = dp_{i-1,j} + dp_{i,j-1}$, with blocked cells set to 0.
  • 0/1 knapsack: $dp_{i,c} = \max(dp_{i-1,c},\ dp_{i-1,c-w_i} + v_i)$. You can keep one row and loop the capacity downwards, so each item is used at most once.
  • Longest common subsequence of strings $s$ and $t$: if $s_i = t_j$ then $dp_{i,j} = dp_{i-1,j-1} + 1$, otherwise $\max(dp_{i-1,j},\ dp_{i,j-1})$.

The table has (rows) times (columns) entries, so check the memory as well as the time: $5000 \times 5000$ Python integers do not fit, but keeping only the previous row usually does. As in 1D, decide the order of filling first: row by row works for grids and strings, item by item for knapsack.

Worked example

A bag holds at most 7 kg. Items have (weight, value) (3, 4), (4, 5) and (2, 3). What is the greatest total value that fits?

Show the solution
items = [(3, 4), (4, 5), (2, 3)]
cap = 7
dp = [0] * (cap + 1)               # dp[c] = best value with capacity c
for w, v in items:
    for c in range(cap, w - 1, -1):  # downwards: each item once
        dp[c] = max(dp[c], dp[c - w] + v)
print(dp[cap])

It prints 9, from the 3 kg and 4 kg items. All three would weigh 9 kg, and the other pairs are worth only 7 and 8.

Try it

Find the length of the longest common subsequence of OLYMPIAD and POLYGON.

Show the answer

3, for OLY. After the Y, OLYMPIAD has M, P, I, A, D and POLYGON has G, O, N, with nothing in common; a subsequence starting with P cannot be extended either.

Going deeper: The dynamic programming chapter of a university algorithms textbook, which covers knapsack and edit distance in detail.

DP on treesNew

Combine children's DP values at each vertex in a rooted tree, processing vertices in an iterative bottom-up order.

Practise The committee and the guards

Read the bridge note

Tree DP is a favourite at national and international level because trees have no cycles, so each subtree is an independent subproblem.

Root the tree at any vertex. Each vertex $v$ gets one or more DP values that describe the best answer inside the subtree of $v$, often split by a small choice about $v$ itself (taken or not, coloured or not). A vertex's values are combined from its children's values, so children must be done before parents.

In Python, a recursive DFS on a tree with $2 \times 10^5$ vertices in a line will hit the recursion limit or crash. Use an iterative order instead:

  1. Do a BFS or stack walk from the root, recording each vertex's parent and the order you visit them.
  2. Go through that order backwards: every child then comes before its parent.

Each vertex is handled once and each edge twice, so the whole DP is $O(n)$. Typical states: maximum weight set with no two linked vertices (two values per vertex), smallest set covering every edge (two values), subtree sizes, and the longest path through a vertex.

Worked example

A tree has edges 1-2, 1-3, 2-4, 2-5, 3-6. Find the smallest set of vertices such that every edge has at least one end in the set.

Show the solution

For each $v$: $t_v$ = smallest cover of $v$'s subtree with $v$ taken, $s_v$ = the same with $v$ skipped. If $v$ is skipped every child must be taken: $s_v = \sum t_c$. If $v$ is taken children are free: $t_v = 1 + \sum \min(t_c, s_c)$.

from collections import deque
n = 6
edges = [(1, 2), (1, 3), (2, 4), (2, 5), (3, 6)]
adj = [[] for _ in range(n + 1)]
for u, v in edges:
    adj[u].append(v)
    adj[v].append(u)
parent = [0] * (n + 1)
order = []
seen = [False] * (n + 1)
q = deque([1]); seen[1] = True
while q:
    u = q.popleft(); order.append(u)
    for w in adj[u]:
        if not seen[w]:
            seen[w] = True; parent[w] = u; q.append(w)
take = [1] * (n + 1)
skip = [0] * (n + 1)
for u in reversed(order):           # children before parents
    p = parent[u]
    if p:
        take[p] += min(take[u], skip[u])
        skip[p] += take[u]
print(min(take[1], skip[1]))

It prints 2: the set {2, 3} touches all five edges.

Try it

For a path 1-2-3-4-5, what is the smallest set of vertices touching every edge?

Show the answer

2, for example {2, 4}. One vertex touches at most two of the four edges, so 2 is the least.

Going deeper: The tree algorithms chapter of a competitive programming handbook, including rerooting techniques.

Bitmask DPNew

Use a subset stored as an integer as the DP state when n is about 20, as in assignment and travelling salesman tasks.

Learn it Shifts, masks and bit manipulation (A Level 9618)

Read the bridge note

When $n$ is about 20 and the order or the choice of a subset matters, bitmask DP is often the intended solution.

A subset of $n$ items can be stored as one integer mask from $0$ to $2^n - 1$: bit $i$ is 1 when item $i$ is in the set. The key operations are:

  • mask >> i & 1: is item $i$ in the set?
  • mask | (1 << i): add item $i$.
  • bin(mask).count('1'): the size of the set.

Bitmask DP uses the subset as (part of) the state. In an assignment task, $dp[\text{mask}]$ is the cheapest way to give the jobs in mask to the first $k$ workers, where $k$ is the size of mask. In the travelling salesman problem, $dp[\text{mask}][v]$ is the shortest route that visits exactly the cities in mask and ends at $v$.

The cost is $2^n$ states times the work for each, so $O(2^n \cdot n)$ or $O(2^n \cdot n^2)$. That is why the limit is usually $n \le 20$ or less, much smaller than for other tasks. The limit itself is your clue: a small $n$ with an exponential feel means bitmask DP.

Worked example

Three workers and three jobs. Worker 0 charges 4, 2, 8 for jobs 0, 1, 2; worker 1 charges 4, 3, 7; worker 2 charges 3, 1, 6. Each worker does one job. Find the least total cost.

Show the solution
cost = [[4, 2, 8],
        [4, 3, 7],
        [3, 1, 6]]
n = len(cost)
INF = float('inf')
dp = [INF] * (1 << n)
dp[0] = 0
for mask in range(1 << n):
    k = bin(mask).count('1')        # next worker to assign
    if k == n or dp[mask] == INF:
        continue
    for j in range(n):
        if not mask >> j & 1:       # job j still free
            new = mask | (1 << j)
            dp[new] = min(dp[new], dp[mask] + cost[k][j])
print(dp[(1 << n) - 1])

It prints 12, for example worker 0 on job 1, worker 1 on job 0 and worker 2 on job 2: $2 + 4 + 6$. Of the six possible assignments, three cost 12, two cost 13 and one costs 14.

Try it

For a travelling salesman DP with $n = 15$ cities, roughly how many (mask, end city) states are there, and how many steps if each state tries every next city?

Show the answer

$2^{15} \times 15 \approx 4.9 \times 10^5$ states, times 15 next cities, about $7.4 \times 10^6$ steps. That is fine, even in Python.

Going deeper: The bit manipulation and bitmask DP sections of a competitive programming handbook.

Graphs

Breadth-first searchNew

Find fewest-move paths in mazes and networks in O(V + E), including on state graphs.

Practise Escape with one hammer Islands at high tide

Read the bridge note

Fewest moves in a maze or a network is one of the most common olympiad questions, and BFS answers it in linear time.

Breadth-first search (BFS) explores a graph in layers: first the start, then everything one edge away, then everything two edges away, and so on. Because it uses a queue (first in, first out), the first time it reaches a vertex is by a shortest path, when every edge counts as one move.

Store the graph as an adjacency list: adj[u] is the list of neighbours of $u$. A grid needs no list; the neighbours of a cell are the four cells next to it that are inside the grid and not walls.

BFS visits each vertex once and looks along each edge once or twice, so it is $O(V + E)$. In Python use collections.deque; popping from the front of an ordinary list is slow.

A powerful trick is the state graph. If the moves depend on more than your position (you carry a key, you have used a hammer, you have $k$ vouchers left), make each vertex a pair (position, extra information) and run BFS on those pairs. The graph is bigger, but the method is the same.

Worked example

A network has links 1-2, 1-3, 2-4, 3-4 and 4-5. Find the fewest links from vertex 1 to every vertex.

Show the solution
from collections import deque
n = 5
edges = [(1, 2), (1, 3), (2, 4), (3, 4), (4, 5)]
adj = [[] for _ in range(n + 1)]
for u, v in edges:
    adj[u].append(v)
    adj[v].append(u)
dist = [-1] * (n + 1)
dist[1] = 0
q = deque([1])
while q:
    u = q.popleft()
    for w in adj[u]:
        if dist[w] == -1:          # first visit is the shortest
            dist[w] = dist[u] + 1
            q.append(w)
print(dist[1:])

It prints [0, 1, 1, 2, 3]. A value still at $-1$ would mean that vertex cannot be reached.

Try it

A $3 \times 3$ grid has a wall only in the centre cell. Moving one cell up, down, left or right per move, how many moves from the top-left corner to the bottom-right corner?

Show the answer

4: go along the top row and down the right side. No route can be shorter, because the corners are 2 rows and 2 columns apart.

Going deeper: The graph traversal chapter of a university algorithms textbook.

Depth-first search and componentsGoes further

Beyond tree traversals in 9618: run DFS on any graph to find components, flood fill grids and detect cycles, without deep recursion.

Learn it Stacks, queues, linked lists and binary trees (A Level 9618) Recursion and the call stack (A Level 9618)

Practise Islands at high tide The committee and the guards

Read the bridge note

Many graph tasks start by finding which vertices are connected, counting the pieces or walking a tree, and DFS does all of these.

In 9618 you traverse binary trees (pre-order, in-order, post-order). Depth-first search (DFS) is the same idea on any graph: go as deep as you can along unvisited edges, then back up. Mark vertices as visited so you never enter one twice.

Uses you will meet early:

  • Connected components: start a DFS from each unvisited vertex; each start is a new component.
  • Flood fill on a grid: count islands or regions.
  • Trees: find parents, depths and subtree sizes, the starting point for tree DP.
  • Cycle detection: in an undirected graph, meeting a visited vertex that is not your parent means a cycle.

DFS is $O(V + E)$. Recursion is the natural way to write it, but Python's default recursion limit is about 1000 and even after sys.setrecursionlimit a deep recursion can crash the program. For large inputs, use an explicit stack, as below. Marking a vertex as seen when you push it, not when you pop it, keeps each vertex on the stack only once.

Worked example

Seven vertices with edges 1-2, 2-3 and 4-5. How many connected components are there?

Show the solution
n = 7
edges = [(1, 2), (2, 3), (4, 5)]
adj = [[] for _ in range(n + 1)]
for u, v in edges:
    adj[u].append(v)
    adj[v].append(u)
seen = [False] * (n + 1)
components = 0
for s in range(1, n + 1):
    if seen[s]:
        continue
    components += 1
    stack = [s]
    seen[s] = True
    while stack:
        u = stack.pop()
        for w in adj[u]:
            if not seen[w]:
                seen[w] = True
                stack.append(w)
print(components)

It prints 4: {1, 2, 3}, {4, 5}, {6} and {7}.

Try it

A graph has 8 vertices and 5 edges and contains no cycle. How many components does it have?

Show the answer

3. Each edge in a graph with no cycle joins two separate pieces, so 8 single vertices become $8 - 5 = 3$ pieces.

Going deeper: The graph traversal chapter of a university algorithms textbook.

Dijkstra's algorithm with a heapGoes further

Beyond the hand method in 9618: code Dijkstra with a heap in O((V + E) log V) and add extra state such as vouchers.

Learn it Graphs in AI: Dijkstra's and A* algorithms (A Level 9618)

Practise Toll-free vouchers

Read the bridge note

Cheapest routes with different edge costs are a standard olympiad task, and contest versions add twists such as vouchers or extra states.

You meet Dijkstra's algorithm in 9618 as a hand method. In a contest you must code it to run fast on $10^5$ edges, and that needs a priority queue (a heap).

The idea: keep the best known distance to each vertex. Repeatedly take the unfinished vertex with the smallest distance; its distance is now final, so relax its edges (if going through it gives a neighbour a shorter distance, update it). With Python's heapq you push (distance, vertex) pairs and skip any pair that is out of date when popped.

The cost is $O((V + E) \log V)$. Two facts to keep in mind:

  • Dijkstra needs edge costs that are not negative. With negative costs a finished vertex might later get cheaper, and the method gives wrong answers.
  • If every edge costs the same, BFS is enough. If costs are only 0 or 1, a deque version of BFS (0-1 BFS) is simpler and faster.

As with BFS, extra conditions become extra state: a vertex (city, vouchers used) turns a voucher task into an ordinary shortest path on a layered graph.

Worked example

Two-way roads: A-B costs 4, A-C costs 1, C-B costs 2, B-D costs 1, C-D costs 5. Find the cheapest cost from A to every town.

Show the solution
import heapq
roads = [('A', 'B', 4), ('A', 'C', 1), ('C', 'B', 2), ('B', 'D', 1), ('C', 'D', 5)]
adj = {}
for u, v, c in roads:
    adj.setdefault(u, []).append((v, c))
    adj.setdefault(v, []).append((u, c))
dist = {town: float('inf') for town in adj}
dist['A'] = 0
heap = [(0, 'A')]
while heap:
    d, u = heapq.heappop(heap)
    if d > dist[u]:
        continue                    # out-of-date entry
    for v, c in adj[u]:
        if d + c < dist[v]:
            dist[v] = d + c
            heapq.heappush(heap, (dist[v], v))
print(sorted(dist.items()))

B is cheaper through C ($1 + 2 = 3$) than directly (4), and D is reached through B for 4.

Try it

Roads: 1-2 costs 7, 1-3 costs 2, 3-2 costs 3, 2-4 costs 1, 3-4 costs 8. What is the cheapest cost from 1 to 4?

Show the answer

6, along 1-3-2-4 ($2 + 3 + 1$). The routes 1-2-4 and 1-3-4 cost 8 and 10.

Going deeper: The shortest paths chapter of a university algorithms textbook, which also covers Bellman-Ford for negative costs.

Union-find (disjoint set union)New

Join groups and test whether two items are connected in almost O(1), and process events offline in reverse.

Practise Islands at high tide

Read the bridge note

Union-find answers "are these two connected?" while edges keep being added, in almost constant time per step.

A disjoint set union (DSU, or union-find) keeps items in groups. Each group is a tree with a chosen root, and parent[x] points towards that root. Two operations:

  • find(x): follow parents to the root. Two items are in the same group exactly when they have the same root.
  • union(a, b): find both roots; if they differ, point one root at the other.

Two small tricks make it very fast: path compression (after a find, point every item on the path straight at the root) and union by size (hang the smaller tree under the larger). Together each operation costs almost $O(1)$ in practice.

Union-find can only join groups, never split them. When a task removes edges over time, look at it backwards: process the events in reverse, so removals become additions. This offline trick turns many hard-looking tasks into simple DSU tasks. DSU is also the heart of Kruskal's minimum spanning tree algorithm.

Worked example

Six people. Join 1 with 2, 3 with 4, then 2 with 4. Are 1 and 3 in the same group, and how many groups are there?

Show the solution
n = 6
parent = list(range(n + 1))
size = [1] * (n + 1)
def find(x):
    root = x
    while parent[root] != root:
        root = parent[root]
    while parent[x] != root:        # path compression
        parent[x], x = root, parent[x]
    return root
def union(a, b):
    a, b = find(a), find(b)
    if a == b:
        return False
    if size[a] < size[b]:
        a, b = b, a
    parent[b] = a                   # smaller tree under larger
    size[a] += size[b]
    return True
groups = n
for a, b in [(1, 2), (3, 4), (2, 4)]:
    if union(a, b):
        groups -= 1
print(find(1) == find(3), groups)

It prints True 3: the groups are {1, 2, 3, 4}, {5} and {6}.

Try it

Five vertices. You add edges 1-2, 2-3, 3-1, 4-5 in that order using union-find. Which edge joins two vertices that are already in the same group?

Show the answer

3-1: after 1-2 and 2-3, vertices 1 and 3 already share a root, so this edge would close a cycle.

Going deeper: The disjoint sets chapter of a university algorithms textbook.

Minimum spanning treesNew

Connect every vertex at least total cost with Kruskal's algorithm and know why the greedy choice is safe.

Practise Islands at high tide

Read the bridge note

Connecting every site at least cost is a classic olympiad model, and Kruskal's algorithm is short once you have union-find.

A spanning tree of a connected graph with $V$ vertices uses $V - 1$ edges to join every vertex with no cycle. A minimum spanning tree (MST) has the smallest total edge cost.

Kruskal's algorithm is a greedy method that is provably correct:

  1. Sort the edges from cheapest to dearest.
  2. Go through them in order. Take an edge if its two ends are in different groups (check with union-find), and join the groups. Otherwise skip it, because it would close a cycle.
  3. Stop after $V - 1$ edges.

The cost is $O(E \log E)$ for the sort; the union-find part is almost free. The reason it works is the cut property: for any split of the vertices into two sides, the cheapest edge crossing the split belongs to some MST.

Olympiad twists include the most expensive edge on the MST (to minimise the largest single cost), counting how many groups remain after adding edges up to some cost, and Prim's algorithm, a heap-based alternative that is better for dense graphs.

Worked example

Edges: A-B 3, A-C 1, B-C 2, B-D 4, C-D 5. Find the MST cost.

Show the solution

In order: A-C (1) take, B-C (2) take, A-B (3) skip as A and B are already joined, B-D (4) take. Three edges for four vertices, so stop. Total $1 + 2 + 4 = 7$.

edges = [(3, 'A', 'B'), (1, 'A', 'C'), (2, 'B', 'C'), (4, 'B', 'D'), (5, 'C', 'D')]
parent = {v: v for v in 'ABCD'}
def find(x):
    while parent[x] != x:
        parent[x] = parent[parent[x]]
        x = parent[x]
    return x
total = 0
for c, u, v in sorted(edges):
    ru, rv = find(u), find(v)
    if ru != rv:
        parent[ru] = rv
        total += c
print(total)
Try it

Four towns in a square: A-B 1, B-C 2, C-D 3, D-A 4, plus a diagonal A-C 5. What is the MST cost?

Show the answer

6: take A-B, B-C and C-D ($1 + 2 + 3$). Every vertex is then joined, so D-A and A-C are skipped.

Going deeper: The minimum spanning trees chapter of a university algorithms textbook.

Topological sort and DAGsNew

Order tasks with before-after rules using Kahn's algorithm, detect cycles and run DP along a DAG.

Read the bridge note

Tasks with "this must happen before that" rules need an order that respects them, or proof that none exists.

A directed graph with an edge $u \to v$ for "$u$ must come before $v$" can be put in an order that respects every edge exactly when it has no directed cycle. Such a graph is a DAG (directed acyclic graph) and the order is a topological order.

Kahn's algorithm builds the order:

  1. Count each vertex's in-degree (the number of edges coming in).
  2. Put every vertex with in-degree 0 in a queue.
  3. Repeatedly take a vertex from the queue, add it to the order, and remove its outgoing edges by lowering its neighbours' in-degrees; any neighbour that reaches 0 joins the queue.

If the order ends up with fewer than $V$ vertices, the rest lie on or behind a cycle, so no valid order exists. The cost is $O(V + E)$.

Topological order is also the order to fill a DP on a DAG: longest path, number of paths or the earliest finish time of a project all follow by going through the vertices in that order.

Worked example

Tasks 1 to 4 with rules $1 \to 2$, $1 \to 3$, $3 \to 2$, $2 \to 4$. Find an order that respects every rule.

Show the solution
from collections import deque
n = 4
rules = [(1, 2), (1, 3), (3, 2), (2, 4)]
out = [[] for _ in range(n + 1)]
indeg = [0] * (n + 1)
for u, v in rules:
    out[u].append(v)
    indeg[v] += 1
q = deque(v for v in range(1, n + 1) if indeg[v] == 0)
order = []
while q:
    u = q.popleft()
    order.append(u)
    for v in out[u]:
        indeg[v] -= 1
        if indeg[v] == 0:
            q.append(v)
print(order if len(order) == n else 'cycle')

It prints [1, 3, 2, 4], and here that is the only valid order.

Try it

Rules: $a \to b$, $b \to c$, $c \to a$, $c \to d$. What does Kahn's algorithm output?

Show the answer

Nothing: every vertex has in-degree 1, so the queue starts empty. The order has 0 of 4 vertices, which shows there is a cycle ($a \to b \to c \to a$).

Going deeper: The directed graphs chapter of a university algorithms textbook.

Data structures

Heaps and priority queuesNew

Add items and remove the smallest in O(log n) with Python's heapq, for scheduling, greedy merging and Dijkstra.

Practise Toll-free vouchers

Read the bridge note

Whenever a program must keep taking the smallest or largest item from a changing collection, a heap does it in $O(\log n)$ per step.

A priority queue supports three operations: add an item, look at the smallest, and remove the smallest. A sorted list makes adding slow; an unsorted list makes finding the smallest slow. A binary heap does both in $O(\log n)$.

A heap is a complete binary tree stored in a list, where every parent is no larger than its children, so the smallest item sits at index 0. In Python use the heapq module on an ordinary list:

  • heapq.heappush(h, x) adds $x$.
  • heapq.heappop(h) removes and returns the smallest.
  • h[0] is the smallest, without removing it.
  • heapq.heapify(h) turns a list into a heap in $O(n)$.

For the largest item instead, push $-x$ and negate when you pop. Push tuples such as (cost, vertex) to order by the first entry. Heaps appear inside Dijkstra and Prim, in scheduling simulations (which event happens next?), in keeping the $k$ largest values seen so far, and in greedy tasks that repeatedly combine the two smallest items.

Worked example

Four ropes have lengths 4, 3, 2 and 6. Joining two ropes costs the sum of their lengths. Always join the two shortest. What is the total cost of making one rope?

Show the solution
import heapq
h = [4, 3, 2, 6]
heapq.heapify(h)
total = 0
while len(h) > 1:
    a = heapq.heappop(h)
    b = heapq.heappop(h)
    total += a + b
    heapq.heappush(h, a + b)
print(total)

Joins: 2 + 3 = 5, then 4 + 5 = 9, then 6 + 9 = 15. Total $5 + 9 + 15 = 29$.

Try it

Same rule for ropes of length 1, 2, 3 and 4. What is the total cost?

Show the answer

19: join 1 + 2 = 3, then 3 + 3 = 6, then 4 + 6 = 10, and $3 + 6 + 10 = 19$.

Going deeper: The heaps and priority queues chapter of a university algorithms textbook.

Fenwick treesNew

Support point updates and prefix sums in O(log n), and use them to count inversions and smaller earlier values.

Learn it Shifts, masks and bit manipulation (A Level 9618)

Practise The rising tower

Read the bridge note

When values change between queries, prefix sums break, and a Fenwick tree keeps both updates and range sums at $O(\log n)$.

Prefix sums answer range sums in $O(1)$ but one change to the array forces an $O(n)$ rebuild. A Fenwick tree (binary indexed tree, BIT) is a list t where each entry stores the sum of a block of the array whose length is a power of 2, chosen from the binary form of its index.

The block length at index $i$ is the lowest set bit of $i$, written i & -i in Python. For example 12 is 1100 in binary, so t[12] holds 4 values: positions 9 to 12.

  • Add $v$ at position $i$: add $v$ to t[i], then move to i += i & -i, until past $n$.
  • Prefix sum of positions 1 to $i$: add t[i], then move to i -= i & -i, until 0.

Both loops run at most about $\log_2 n$ times. The range sum from $l$ to $r$ is prefix(r) - prefix(l - 1). Positions start at 1; index 0 is unused.

Fenwick trees are short to write and fast in Python. Beyond sums they count inversions, count how many earlier values are smaller (after coordinate compression), and give an $O(n \log n)$ longest increasing subsequence with a prefix maximum instead of a prefix sum.

Worked example

The array is 5, 2, 9, 1, 6. Find the sum of positions 1 to 4. Then add 3 to position 2 and find the sum of positions 2 to 4.

Show the solution
a = [5, 2, 9, 1, 6]
n = len(a)
t = [0] * (n + 1)
def add(i, v):
    while i <= n:
        t[i] += v
        i += i & -i
def prefix(i):
    s = 0
    while i > 0:
        s += t[i]
        i -= i & -i
    return s
for i, x in enumerate(a, 1):
    add(i, x)
print(prefix(4))
add(2, 3)
print(prefix(4) - prefix(1))

It prints 17 ($5 + 2 + 9 + 1$) and then 15 ($5 + 9 + 1$ after the 2 becomes 5).

Try it

What is 12 & -12, and which positions does t[12] cover?

Show the answer

4, because the lowest set bit of 1100 is 100. So t[12] covers positions 9, 10, 11 and 12.

Going deeper: The range queries chapter of a competitive programming handbook, which also covers 2D Fenwick trees.

Segment treesNew

Answer range minimum, maximum or sum queries with updates in O(log n), then learn lazy propagation for range updates.

Read the bridge note

Segment trees handle range queries that Fenwick trees cannot, such as range minimum with updates, and appear in many IOI tasks.

A segment tree is a binary tree over the array. Each leaf holds one value; each inner node holds the combined value (sum, minimum, maximum, gcd) of the range below it. A query for any range is split into at most about $2\log_2 n$ nodes, and an update changes one leaf and the $\log_2 n$ nodes above it.

The simplest version for contests is iterative and stores the tree in a list of size $2n$:

  • Leaves are at indices $n$ to $2n - 1$; node $i$ has children $2i$ and $2i + 1$.
  • Build: for $i$ from $n - 1$ down to 1, combine the two children.
  • Update: set the leaf, then walk up to the root, recombining.
  • Query $[l, r)$: start both ends at the leaves and move up, taking in a node whenever it lies at the edge of the range.

Any operation that is associative works, such as $+$, min, max or gcd. Harder versions add lazy propagation so that a whole range can be updated at once; learn that after the basic version feels easy.

Worked example

The array is 7, 3, 8, 2, 9, 4 (positions 1 to 6). Find the minimum of positions 2 to 5. Then set position 4 to 10 and ask again.

Show the solution
a = [7, 3, 8, 2, 9, 4]
n = len(a)
t = [0] * n + a                     # leaves at t[n] .. t[2n-1]
for i in range(n - 1, 0, -1):
    t[i] = min(t[2 * i], t[2 * i + 1])
def update(pos, value):             # pos counted from 0
    i = pos + n
    t[i] = value
    while i > 1:
        i //= 2
        t[i] = min(t[2 * i], t[2 * i + 1])
def query(l, r):                    # minimum of a[l..r-1], from 0
    res = float('inf')
    l += n; r += n
    while l < r:
        if l & 1:
            res = min(res, t[l]); l += 1
        if r & 1:
            r -= 1; res = min(res, t[r])
        l //= 2; r //= 2
    return res
print(query(1, 5))                  # positions 2 to 5
update(3, 10)                       # position 4
print(query(1, 5))

It prints 2 (from 3, 8, 2, 9) and then 3 (from 3, 8, 10, 9).

Try it

With $n = q = 10^5$ (array size and number of queries), roughly how many node visits does a segment tree need?

Show the answer

About $2 \log_2 n \approx 34$ per query, so about $3.4 \times 10^6$ in total. Rescanning the range each time could need $10^{10}$.

Going deeper: The segment tree sections of a competitive programming handbook, then lazy propagation.

Maths for programmers

GCD, primes and counting multiplesNew

Use Euclid's algorithm, the sieve of Eratosthenes, trial division and inclusion-exclusion to count multiples.

Practise Counting fizzy numbers The corridor of lamps

Read the bridge note

Divisibility facts turn many counting tasks into formulas, and Euclid's algorithm and the sieve are the standard tools.

Greatest common divisor. Euclid's algorithm uses $\gcd(a, b) = \gcd(b, a \bmod b)$ and $\gcd(a, 0) = a$. It takes $O(\log \min(a, b))$ steps, so it is instant even for $10^{18}$. Python has math.gcd. Then $\text{lcm}(a, b) = \frac{a}{\gcd(a, b)} \times b$; dividing first keeps numbers small in languages that overflow.

Counting multiples. The numbers from 1 to $x$ divisible by $p$ number $\lfloor x / p \rfloor$. For "divisible by $p$ or $q$" use inclusion-exclusion: $\lfloor x/p \rfloor + \lfloor x/q \rfloor - \lfloor x/\text{lcm}(p,q) \rfloor$. A range $a$ to $b$ is $f(b) - f(a - 1)$.

Primes. To test one number $n$, try divisors up to $\sqrt{n}$: $O(\sqrt{n})$. To find all primes up to $N$, use the sieve of Eratosthenes: cross out multiples of each prime, in about $O(N \log \log N)$. The same trial division up to $\sqrt{n}$ factorises $n$ and so counts its divisors.

Use math.isqrt(n) for whole-number square roots. int(n ** 0.5) goes through floating point and can be off by one for large $n$.

Worked example

Find $\gcd(84, 60)$ and $\text{lcm}(84, 60)$. Then count the numbers from 1 to 100 divisible by 6 or by 10.

Show the solution

$84 \bmod 60 = 24$, $60 \bmod 24 = 12$, $24 \bmod 12 = 0$, so the gcd is 12 and the lcm is $\frac{84}{12} \times 60 = 420$.

Multiples of 6: 16; of 10: 10; of $\text{lcm}(6, 10) = 30$: 3. So $16 + 10 - 3 = 23$.

from math import gcd
def f(x, p, q):
    l = p // gcd(p, q) * q
    return x // p + x // q - x // l
print(gcd(84, 60), 84 // gcd(84, 60) * 60, f(100, 6, 10))
Try it

How many numbers from 1 to 60 are divisible by 4 or by 6?

Show the answer

20: $15 + 10 - 5$, because $\text{lcm}(4, 6) = 12$, not 24.

Going deeper: An elementary number theory book, or the number theory chapter of a competitive programming handbook.

Modular arithmeticGoes further

Beyond MOD in school: reduce at every step, compute fast powers and inverses modulo a prime, and handle negative remainders.

Learn it Library routines: MOD, DIV, ROUND and RANDOM (IGCSE 0478)

Practise Fair shares of the takings Paths through the orchard

Read the bridge note

Answers to counting tasks are often astronomically large, so olympiads ask for them modulo a prime such as $10^9 + 7$.

You already use MOD in school. Modular arithmetic is the rule that you may reduce at every step: for any whole numbers,

$$(a + b) \bmod m = ((a \bmod m) + (b \bmod m)) \bmod m,$$

and the same for subtraction and multiplication. So you never need the full number; reduce after every operation and all values stay below $m$.

Points to know:

  • Negative values. In Python, -7 % 3 is 2, always in $0$ to $m - 1$. In C++ and Java it is $-1$, so contestants there write ((x % m) + m) % m. Remember this when reading other people's code.
  • Fast power. $a^e \bmod m$ by repeated squaring takes $O(\log e)$ steps. Python's pow(a, e, m) does it for you.
  • Division is different. You cannot just divide a reduced number. When $m$ is a prime $p$ and $a$ is not a multiple of $p$, Fermat's little theorem gives $a^{p-1} \equiv 1 \pmod p$, so the inverse of $a$ is $a^{p-2} \bmod p$, computed with pow(a, p - 2, p).

Even though Python integers do not overflow, huge numbers get slow, so reduce as you go anyway.

Worked example

Find $3^{100} \bmod 7$ by hand, then check it with Python.

Show the solution

By Fermat, $3^6 \equiv 1 \pmod 7$. Since $100 = 6 \times 16 + 4$, $3^{100} \equiv 3^4 = 81 \equiv 4 \pmod 7$.

print(pow(3, 100, 7))
Try it

What are the last two digits of $7^{20}$?

Show the answer

01. $7^4 = 2401 \equiv 1 \pmod{100}$, so $7^{20} = (7^4)^5 \equiv 1$.

Going deeper: An elementary number theory book, chapters on congruences and Fermat's little theorem.

Combinatorics modulo a primeNew

Compute binomial coefficients modulo a prime with Pascal's rule or factorial tables, and count grid routes.

Practise Paths through the orchard

Read the bridge note

Counting routes, arrangements and choices modulo a prime is a staple of national and international rounds.

The number of ways to choose $k$ items from $n$ is $\binom{n}{k} = \frac{n!}{k!\,(n-k)!}$. Two standard ways to get it modulo a prime $p$:

  • Pascal's rule $\binom{n}{k} = \binom{n-1}{k-1} + \binom{n-1}{k}$, as a DP table. Only additions, but $O(n^2)$, so for $n$ up to a few thousand.
  • Factorial tables: precompute $i! \bmod p$ for $i$ up to $N$, then the inverse of $N!$ with pow(fact[N], p - 2, p), then work down with $\frac{1}{(i-1)!} = \frac{i}{i!}$. After $O(N)$ set-up each $\binom{n}{k}$ is three multiplications.

A key model: on a grid, a route from the top-left corner to a cell $a$ rows down and $b$ columns right, moving only down or right, is a choice of which $a$ of the $a + b$ moves go down, so there are $\binom{a+b}{a}$ routes. With blocked cells, combine this with a DP over the blocks and subtract the routes that pass through one (inclusion-exclusion).

Other tools: stars and bars ($\binom{n+k-1}{k-1}$ ways to share $n$ identical items among $k$ people), and the complement trick (count all, subtract the bad ones).

Worked example

A robot moves only down or right on a grid of 4 rows and 5 columns. How many routes go from the top-left to the bottom-right cell? Compute it with factorial tables modulo $10^9 + 7$.

Show the solution

It needs 3 moves down and 4 right, so $\binom{7}{3} = 35$ routes.

p = 10**9 + 7
N = 20
fact = [1] * (N + 1)
for i in range(1, N + 1):
    fact[i] = fact[i - 1] * i % p
inv = [1] * (N + 1)
inv[N] = pow(fact[N], p - 2, p)      # Fermat inverse
for i in range(N, 0, -1):
    inv[i - 1] = inv[i] * i % p
def C(n, k):
    if k < 0 or k > n:
        return 0
    return fact[n] * inv[k] % p * inv[n - k] % p
print(C(7, 3))
Try it

How many down-or-right routes cross a grid of 3 rows and 6 columns, from corner to corner?

Show the answer

2 moves down and 5 right, so $\binom{7}{2} = 21$.

Going deeper: An introductory combinatorics book, and the combinatorics chapter of a competitive programming handbook.

Contest skills

Testing your own codeBefore you submit in the Code it panel, run the samples, then your own edge cases: the smallest input, all values equal, the largest values, already sorted and reverse sorted. Then write a slow brute force, generate many small random inputs and compare its output with your fast solution; the first input where they differ shows you the bug.
Reading constraints for the intended complexityLook at the limits before you think about the method. A value of n about 20 hints at subsets, a few thousand at O(n squared), and 200 000 at O(n log n). The subtask limits often spell out the steps from brute force to full solution.
Collecting subtask pointsIOI-style tasks award points for each subtask, so a partial solution is worth a lot. Code the brute force first, then the special cases (a line instead of a tree, all values small), and only then aim for the full solution.
Solving on paper firstWork the sample by hand and write down the state, the greedy rule or the graph before you type. Try to break your idea with a tiny counterexample; it is far cheaper to find a flaw on paper than after an hour of debugging.
Writing Python that is fast enoughRead all input at once with sys.stdin, use built-ins such as sorted and sum, and replace deep recursion with an explicit stack or loop. Remember that Python does about ten times fewer steps per second than C++, so a tight complexity matters even more.
Ready to practise? Problems with staged hints and a timed drill. IOI practice →