Appearance
Amortized Analysis: Why It's Fast Even When It Looks Slow
Quick Revisit
- USE WHEN: inner loop looks O(n) but a global bound limits total work (two pointers, stack pops, DSU)
- INVARIANT: total work across all n operations ≤ cn, even if a single operation costs O(n)
- TIME: O(n) amortized total for n operations | SPACE: varies
- CLASSIC BUG: claiming amortized O(1) without a valid argument — nested loops aren't always amortized
- PRACTICE: 08-ladder-mixed
Your teammate says "this loop runs
Difficulty: [Advanced]CF Rating Range: 1600-2200 Prerequisites: Basic complexity analysis, familiarity with data structures from Chapters 2-3
Contents
- Why Amortized Analysis Matters in CP
- Method 1: Aggregate (Just Count Everything)
- Method 2: Banker's (Credits)
- Method 3: Physicist's (Potential Function)
- Five CP Examples
- How to Recognize Amortized O(n)
- Practice Problems
Why Amortized Analysis Matters in CP
Worst-case analysis per operation sometimes lies. A single operation might take
In CP, this matters because many correct solutions look
Method 1: Aggregate (Just Count Everything)
The simplest method. Instead of analyzing one operation, count the total work across all operations and divide.
Example -- Two pointers:
cpp
int j = 0;
for (int i = 0; i < n; i++) {
while (j < n && a[j] - a[i] <= k) j++;
// process window [i, j)
}The inner while looks like it runs up to for, suggesting j only moves forward. Across all iterations of the outer loop, j increments at most
Total:
Method 2: Banker's (Credits)
Each cheap operation deposits "credits" (pre-paid work). When an expensive operation occurs, it spends the saved credits.
Example -- Dynamic array (vector push_back with doubling):
When the array is full (capacity push_back allocates
Credit scheme: Each push_back pays 3 coins:
- 1 coin for the actual insertion.
- 2 coins saved for later (one for copying itself, one for copying an element that was already present when this element was added).
When doubling occurs at capacity
Amortized cost per push_back:
Method 3: Physicist's (Potential Function)
Define a potential function
If
Example — small-to-large merging:
Let
(Splay trees and link-cut trees are the classic textbook potential-function examples. They are legitimate but rarely on the contest path; if you need them, see CLRS or Tarjan's Data Structures and Network Algorithms.)
Five CP Examples
1. Two Pointers -- Despite Nested Loops
As shown above: both pointers traverse the array at most once each. Total pointer movement:
2. DSU with Path Compression -- Inverse Ackermann
Each find operation flattens a path, making future find operations faster. A single find can traverse
3. Monotonic Stack -- Each Element Pushed and Popped Once
cpp
stack<int> st;
for (int i = 0; i < n; i++) {
while (!st.empty() && a[st.top()] >= a[i])
st.pop(); // looks O(n) per iteration
st.push(i);
}Each of the while loop is
4. Heavy Path Decomposition Queries -- per Query
A path from
The amortization here is structural: the tree decomposition guarantees few chain transitions. It's the aggregate method applied to the tree structure rather than to a sequence of operations.
5. Euler Tour Construction -- Preprocessing for LCA
Building the Euler tour visits each edge twice: once going down, once going up. For a tree with
How to Recognize Amortized
When you see nested loops or expensive operations inside loops, ask:
"Is there a quantity that changes monotonically across the entire execution?"
- Two pointers: Each pointer only moves forward ->
total movement. - Monotonic stack: Each element enters and exits at most once ->
total. - Queue-based BFS: Each node is enqueued and dequeued once ->
total. - DSU path compression: Path length shrinks after each find -> amortized
.
The general pattern: each element is processed at most
If every element can be "charged" to a unique event that happens
When the amortized argument breaks
Three warning patterns. If any apply, double-check (or abandon) the amortized claim.
- A pointer can move backward. Two pointers depend on monotone movement. If
jever decreases — say, the inner condition allowsj--— thenjcan traverse the array Θ(n) times, and the inner loop is genuinely. Same trap with stacks: if you push the same element twice through different code paths, the "each element popped once" argument fails. - Items can be reinserted without growth. Small-to-large works because each move at least doubles the receiving container. If a problem allows inserting into a smaller container, or re-adding an element that was just removed, the doubling argument no longer applies. DSU has the same caveat: union without size/rank tracking can produce
chains. - Cleanup repeats over the same large set. Patterns like "after each query, walk the whole window and reset state" look amortized but aren't — every query pays for the whole window. The fix is usually local rollback (only undo what this query touched) or a true potential argument showing the cleanup cost is paid by prior insertions.
The diagnostic: count the total work done by the suspicious inner loop across all outer iterations. If you cannot bound that count by something linear (or near-linear) in the input size, the amortized claim is wrong.
Practice Problems
| Problem | Amortized Insight | CF Rating |
|---|---|---|
| CF 1944C - MEX Game 1 | Greedy with amortized counting | 1300 |
| CF 1407D - Discrete Centroid | Reroot with amortized transitions | 1800 |
| CF 1619E - MEX and Increments | Maintain stack of costs, amortized pushes/pops | 1700 |
| CF 1476D - Journey | DP with "looks O(n²)" but O(n) total transitions | 1700 |
| CF 1614D1 - Divan and Kostomuksha (easy) | DP over divisors, harmonic sum amortization | 1700 |
| CF 1799D2 - Hot Start Up (hard) | DP with amortized state transitions | 2100 |
Related Techniques
- Small-to-Large Merging -- the classic example of amortized O(n log n) from repeated merging
- Monotonic Stack -- each element is pushed and popped at most once, giving amortized O(n)
- Union-Find (DSU) -- path compression + union by rank gives amortized near-O(1) per operation