Read the constraints, compute the real operation count, and that number tells you the intended solution.
Asymptotic notation
| Notation | Meaning | Reads as |
|---|---|---|
| for | grows no faster than (upper bound) | |
| for | grows no slower than (lower bound) | |
| and | and grow at the same rate | |
| is strictly smaller order than | ||
| is strictly larger order than |
In competitive programming "" is almost always used loosely to mean (a tight bound, not just an upper one) — stating a true-but-weak bound like “bubble sort is ” is correct but useless, so the convention is to always give the tightest bound you can prove. and matter mainly for lower-bound arguments (e.g. “no comparison sort can be ”).
Which guarantee do you actually have, since they give different promises for the same stated bound:
| Guarantee | Promise | Example |
|---|---|---|
| Worst case | true for every input of size | quicksort is worst case |
| Average case | true in expectation over a fixed distribution of inputs (usually uniform random) | quicksort is on random input |
| Expected (randomized algorithm) | true in expectation over the algorithm’s own random choices, for every input | randomized quicksort is expected, on any input |
| Amortized | true for the total cost of operations, divided by — individual operations may be expensive | vector::push_back is amortized, worst case for one call |
Only worst case is safe against an adversary who sees your algorithm (adaptive interaction, anti-hash tests, a judge with hacked tests); average case needs the input to actually be drawn from the assumed distribution (an adversarial judge input breaks it, which is exactly why plain quicksort on adversarial data degrades to and randomized pivoting fixes it); expected and amortized both hold unconditionally over all inputs, but expected is a per-call promise (randomized over the algorithm) while amortized is a total-cost promise (divided evenly, no randomness required).
What fits in one second
About - simple operations per second: for vectorisable array loops, for cache-missing or branchy code, for map, modulo-heavy or long double code.
| Target | Typical tools | |
|---|---|---|
| - | permutations, brute force | |
| - | , | bitmask DP, SOS |
| submask enumeration | ||
| meet in the middle | ||
| interval DP with extra dimension | ||
| Floyd, Hungarian, interval DP | ||
| quadratic DP, Dijkstra | ||
| , | Mo, sqrt decomposition, CDQ, HLD | |
| sorting, segment tree, FFT | ||
| - | , | linear sieve, prefix sums, two pointers |
| trial division, harmonic lemma | ||
| , | Du’s sieve, Min-25, Lucy | |
| , | binary exponentiation, digit DP, matrix power |
Other constraint clues:
- across tests: per-test cost must depend on that test’s , never on
MAXN. - and both with “online” or forced decoding (
x ^= lastAns): you need a persistent or online structure, not Mo or offline sorting. - An oddly small parameter (alphabet , , values , at most 3 special vertices) is the exponential or DP dimension.
- A time limit of 5+ s with suggests or a bitset .
- Memory limits below 64 MB rule out persistent structures on .
Memory
| Type | Elements in 256 MB |
|---|---|
char / bool | |
int | |
long long / double | |
bits in a bitset |
vectoroverhead is 24 bytes, sovector<vector<int>>with rows costs 24 MB plus heap headers before any data.set/mapnodes cost about 40-48 bytes plus the key.- A persistent segment tree with updates on needs about nodes. At 12 bytes each that’s 53 MB.
- Recursion: each frame is roughly 50-200 bytes.
Useful magnitudes
| Quantity | Value |
|---|---|
double exact integers | up to |
(fits in ll); does not | |
| , | , |
| , , | 78498, 664579, 50847534 |
| max divisors of / / | 240 / 1344 / 103680 |
| max distinct prime factors, | 15 |
| prime gap below | under 1500 |
| partitions | |
| Catalan , | , |
| Bell | |
| sieve work is | |
| at most 4 for any practical |
Asymptotics in practice
- With several parameters, keep them all: , , .
- Common sums: , in practice, has distinct values, a loop where
j *= 2gives . - , so comparison sorting is . Any algorithm that could sort by comparisons inherits the bound (for example element distinctness, convex hull via the lift to a parabola).
- Expected, amortized and worst case are different promises. All three are fine for a total time limit; only worst case is safe against adaptive interaction or persistence.
Recurrences
| Recurrence | Solution | Where |
|---|---|---|
| binary search | ||
| quickselect, median of medians | ||
| merge sort, D&C | ||
| CDQ with a sort or BIT | ||
| Karatsuba | ||
| Strassen | ||
| median of medians | ||
| Hanoi | ||
| van Emde Boas | ||
| outside the master theorem |
Master theorem for : compare with . If is polynomially smaller, the leaves dominate and . If they are equal up to , . If is polynomially larger (and regular), .
Proof: the three cases, via the recursion tree
Unrolling gives a tree of depth : level has nodes, each on a subproblem of size , contributing of non-recursive work; the leaves (level ) number (writing ) and contribute each, i.e. total. So . Write the level sum, for , as — a geometric series in with ratio , and , , (since ). Case 1 (, ): the series grows with , so it is dominated by its last term , which is — matching the leaf cost, giving . Case 3 (, , with the regularity condition for some ensuring the recursive work doesn’t blow up): the series shrinks with , dominated by its first term , namely itself, and the leaf term is asymptotically smaller since — giving . Case 2 (, ): each level sum is exactly (the factor is ), and summing over gives (a standard power-sum bound), so . The sharper case is checked by brute-force simulation (counting operations against the closed form) in Divide and Conquer.
Akra-Bazzi for uneven splits: find with ; then . Example: gives and .
Recursion tree intuition: if level costs shrink geometrically the root dominates, if they’re equal multiply by the depth, if they grow the leaves dominate.
A linear recurrence has a closed form via the characteristic polynomial’s roots; compute the -th term in with matrix power or with Bostan–Mori (Linear Recurrences).
Amortized analysis
The total cost of a sequence is bounded even if single operations are expensive.
- Aggregate. Bound the total directly. A monotonic stack pushes each element once and pops it at most once, so a
whileloop that sometimes pops items is still overall. - Accounting. Overcharge cheap operations and save the credit.
push_backwith doubling: charge 3 per insert, and each element pays for its own future copy and one copy of an older element. - Potential. Pick with . The amortized cost is , and the sum telescopes. Binary counter: = number of ones, so an increment flipping ones has amortized cost .
The workhorse arguments:
| Argument | Used by |
|---|---|
| each element is pushed and popped once | monotonic stack/queue, two pointers, two-stack queue |
| a pointer never decreases more than it increases | prefix function, Z-function, Kasai’s LCP, Ukkonen |
| the smaller side moves, so each element moves times | small-to-large, union by size, DSU on tree, HLD light edges |
| each merge destroys a distinct value or a node | segment tree beats ( = number of distinct values), segment tree merging ( = number of nodes), Chtholly tree with random assigns |
| splay trees, link-cut trees: amortized |
To prove a new bound: find what makes an operation expensive, find a non-negative quantity that the expensive step decreases, and bound how much cheap steps increase it.
When amortization breaks
- Rollback undoes the progress that paid for the work. Rollback DSU must use union by size without path compression ( worst case).
- Persistence lets an expensive old version be reused many times. Splay trees and doubling vectors lose their bounds.
- Adversarial interaction with a per-query time limit can hit the worst case of every operation.
Complexity trade-offs in practice
Big-O hides constants that decide accepted vs TLE. Rough throughput on a judge (simple operations per second): additions in a tight loop ; random memory accesses ; map/set operations ; hash map operations .
| Choice | Prefer when |
|---|---|
| (Mo, sqrt decomposition) vs | : both fine; sqrt has better constants with simple arrays |
| segment tree vs Fenwick | Fenwick when the operation is invertible: 2–3 faster, less code |
set vs sorted vector | static data: sorted vector + lower_bound (cache-friendly) |
| online vs offline | offline lets you sort queries, use CDQ, or reverse time |
| recursion vs iteration | depth : iterate (stack limit) |
exact long double vs integer | integers whenever possible (exact and faster) |
| sort vs counting sort | values up to : counting / radix |
Choosing block sizes
For a sqrt decomposition with update cost and query cost , the balanced choice is ; if updates are times more expensive per element than queries, take . For Mo’s algorithm with queries, minimizes .
Proof: optimal block size
The total cost is convex in with , which vanishes at , giving . For Mo, the right pointer moves per block ( blocks, in total) and the left pointer per query (), so and the total is .
Profiling
Generate a max test (and adversarial ones: sorted, all equal, star graph, long path), run with time, and look at which loop dominates before optimizing anything.
Related: Complexity and Analysis, Sqrt Decomposition, Mo’s Algorithm.