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 38ticked
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
Variables, selection, loops and functionsIn your course
Write short, correct programs that read input, loop, branch and use functions, as in 0478 and 9618.
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 fits
Typical 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.
Simulation and ad hoc reasoningGoes further
Trace a process by hand like a trace table, spot the pattern, and replace a slow simulation with a direct formula.
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$?
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:
Move right one step and add the new item.
While the block is bad, remove the item at left and move left on.
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.
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:
Try to break it with tiny examples (two or three items). If you find a counterexample, drop the rule.
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.
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.
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.
A brute force earns the first subtask and gives you a trusted checker for your fast solution.
Linear search in school checks every item. Complete search checks every candidate answer: every pair, every subset or every order. It is always correct, so the only question is whether it is fast enough.
$n$ items have $2^n$ subsets: fine up to about $n = 20$.
$n$ items have $n!$ orders: fine up to about $n = 9$ or $10$.
All pairs: $\frac{n(n-1)}{2}$, fine up to a few thousand.
A neat way to list subsets is a bitmask: the number mask from $0$ to $2^n - 1$ stands for a subset, and bit $i$ says whether item $i$ is in it. Python's itertools module also gives combinations, permutations and product, which save you writing nested loops.
Two habits make complete search powerful. First, write it before the clever solution, even if you think the clever one is easy. Second, keep it: run both on many small random inputs and compare the answers (see the skill on testing your own code). In IOI-style tasks the brute force often scores 10 to 30 points on its own.
Worked example
Five parcels weigh 3, 7, 4, 9 and 6 kg. How many different sets of parcels weigh exactly 16 kg in total?
Show the solution
There are $2^5 = 32$ subsets, so just try them all.
w = [3, 7, 4, 9, 6]
n = len(w)
count = 0
for mask in range(1 << n): # every subset
total = 0
for i in range(n):
if mask >> i & 1: # is item i in the subset?
total += w[i]
if total == 16:
count += 1
print(count)
It prints 3: the sets {7, 9}, {3, 4, 9} and {3, 7, 6}. A quick check: all five weigh 29 kg, so a 16 kg set leaves 13 kg behind, and 13 can be made as 4 + 9, 7 + 6 or 3 + 4 + 6.
Try it
A task has a subtask with $n \le 8$ and a full task with $n \le 10^5$. Is trying every order of the items fine for the subtask?
Show the answer
Yes: $8! = 40\,320$ orders, each checked in about 8 steps, is about $3 \times 10^5$ steps. It is hopeless for the full task.
Going deeper: The complete search chapter of a competitive programming handbook, and the Python documentation for the itertools module.
Backtracking and pruningNew
Build a candidate step by step with recursion and abandon any branch that cannot lead to a valid or better answer.
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.
Divide and conquerGoes further
Beyond recursion in 9618: split a problem in half, solve both halves and merge, as in merge sort and counting inversions in O(n log n).
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:
State: what $dp_i$ means, in one exact sentence, such as "the best total using the first $i$ items".
Recurrence: how $dp_i$ follows from smaller states, usually by asking what happens to the last item.
Base cases: the smallest states, filled in directly.
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?
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.
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:
Do a BFS or stack walk from the root, recording each vertex's parent and the order you visit them.
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.
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
Graphs and adjacency listsGoes further
Beyond the graphs in 9618: model a task as vertices and edges and store it as an adjacency list, including grids and state graphs.
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.
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.
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.
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.
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:
Sort the edges from cheapest to dearest.
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.
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.
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:
Count each vertex's in-degree (the number of edges coming in).
Put every vertex with in-degree 0 in a queue.
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
Stacks and queuesIn your course
Use a stack for last in, first out and a queue for first in, first out, with Python lists and collections.deque.
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.
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
DIV, MOD and number basesIn your course
Use integer division and remainders, and convert between binary, denary and hexadecimal.
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.
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.
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 →