Bound the total cost of a sequence of operations, even when individual operations are expensive. An operation may cost occasionally, yet the average over any sequence is .
The three methods
1. Aggregate
Bound the total cost of operations directly, then divide.
Example: a monotonic stack over elements. Each element is pushed once and popped at most once, so the total is regardless of how long any single while loop runs.
2. Accounting (the banker’s method)
Charge each operation a fixed “fee”; cheap operations bank credit that expensive ones spend.
Example: vector::push_back. Charge 3 units per insertion — 1 to insert, 2 banked. When the array doubles, every element in the old half has 2 banked units, exactly paying for the copy.
3. Potential
Define with . The amortized cost is
and since never goes negative, .
Example: segment tree beats, where counts the distinct values across all nodes. Each expensive recursion merges two values, decreasing .
Where it appears
| Structure / algorithm | Amortized bound | Argument |
|---|---|---|
vector::push_back | doubling; accounting | |
| DSU | path compression + union by size | |
| Monotonic stack/queue | each element pushed and popped once | |
| Two pointers | pointers only move forward | |
| Prefix function, Z-function | the pointer never decreases below its total increase | |
| Kasai | decreases at most times | |
| Splay tree / link-cut tree | potential = sum of subtree sizes | |
| Segment tree beats | potential = distinct values | |
| Hash table with rehashing | doubling | |
| Ukkonen’s algorithm | per character | the active length is bounded |
Fibonacci heap decrease-key | potential = trees + marked nodes | |
| ”Next free slot” DSU | each slot is consumed once | |
| Small-to-large merging | per element | each element moves times |
The two workhorse arguments
”Each element is touched times”
The monotonic stack, two pointers, and the prefix function all rest on this. The inner while loop can run long, but every iteration removes something that can only be added once.
for (int i = 0; i < n; i++) {
while (!st.empty() && cond(st.top(), a[i])) st.pop(); // total pops <= total pushes = n
st.push(a[i]);
}”Each element moves times”
Small-to-large merging: when merging two sets, always move the smaller into the larger. An element’s set at least doubles each time it moves, so it moves at most times. Total .
This one argument justifies DSU on tree, merging sets in tree DP, and union by size.
Amortized vs worst case — when it matters
| Setting | Amortized acceptable? |
|---|---|
| Total time limit (all contests) | yes |
| Real-time systems | no — a single slow operation may miss a deadline |
| Interactive problems with per-query limits | usually yes |
| Persistent structures | amortized bounds often break under persistence |
The last row is a genuine subtlety: amortized analysis assumes a single timeline. If an old version can be re-used repeatedly (as in persistence), the banked credit is spent many times and the bound fails. Splay trees, for instance, do not remain amortized when made persistent.
Proving your own bound
- Identify what makes an operation expensive.
- Find a quantity that the expensive operation reduces.
- Bound how much that quantity can increase per cheap operation.
- Since it starts bounded and never goes negative, the total expensive work is bounded.
That is the potential method, and it is usually the easiest of the three to apply to a new structure.
See also: Monotonic Stack · DSU · Complexity Theory