DSA DSA In Depth
0%
lesson amortized-analysispotential-methodproofs

Amortized Analysis: Aggregate, Banker's & Physicist's Potential (Φ)

7 min read Module 1.1: Asymptotic Analysis, Recurrences & Memory Hierarchy Updated Oct 23, 2026
📄 Cheat Sheet
MIT 6.046J Lecture 5: Amortized Analysis — Aggregate, Accounting & Potential Method
MIT OpenCourseWare (Erik Demaine) MIT 6.046J Lecture 5: Amortized Analysis — Aggregate, Accounting & Potential Method Watch on YouTube ↗

Erik Demaine rigorously derives Aggregate, Banker (Accounting), and Physicist Potential (Φ) amortized bounds across dynamic arrays, binary counters, and multiphase data structures.

0:00 / --:--

Three Frameworks: Aggregate, Accounting (Banker's) & Potential ($\Phi$)

Let a sequence of $m$ operations act on a data structure starting at state $D_0$, transitioning through states $D_1, D_2, \dots, D_m$ with actual operation costs $c_1, c_2, \dots, c_m$. All three amortized frameworks upper-bound the total actual cost $\sum_{i=1}^m c_i$:

  • 1. Aggregate Method: Bound the entire sequence cost $T(m) = \sum_{i=1}^m c_i$ directly (e.g., via geometric series or bit-flip frequency), then assign every operation the uniform amortized cost $T(m)/m$.
  • 2. Accounting (Banker's) Method: Charge each operation $i$ an amortized fee $\hat{c}_i$. When $\hat{c}_i > c_i$, store the surplus $\hat{c}_i - c_i$ as prepaid credit attached to specific elements in the data structure. When an expensive operation occurs ($c_i > \hat{c}_i$), spend the stored credits to pay the difference. As long as total credit never goes negative ($\sum_{i=1}^k \hat{c}_i \ge \sum_{i=1}^k c_i$ for all $k$), $\sum \hat{c}_i$ upper-bounds total actual work.
  • 3. Physicist's Potential Method ($\Phi$): Define a global state function $\Phi: \mathcal{D} \to \mathbb{R}$ mapping each data structure state $D_i$ to a scalar "stored potential energy". Define the amortized cost of operation $i$ as: $$\hat{c}_i = c_i + \Phi(D_i) - \Phi(D_{i-1}) = c_i + \Delta \Phi_i$$
Data Structure / Problem Worst-Case Single Op $c_i$ Potential Function $\Phi(D_i)$ Amortized Cost $\hat{c}_i = c_i + \Delta \Phi_i$ Key Telescoping Mechanism
Binary Counter increment() $O(k)$ bit flips $b_i =$ number of $1$-bits in counter $\hat{c}_i = 2 = O(1)$ Resetting $t_i$ trailing $1$s gives $\Delta \Phi = 1 - t_i$, canceling $c_i = t_i + 1$
Dynamic Array push_back() ($2\times$ growth) $O(n)$ reallocation $\Phi = 2n - m$ (size $n$, cap $m$) $\hat{c}_i = 3 = O(1)$ When $n = m$, resize drops $\Phi$ from $n$ to $2$, releasing $n-2$ energy
Two-Stack FIFO Queue (inStack, outStack) $O(n)$ transfer $\Phi = 2 \cdot |\text{inStack}|$ $\hat{c}_{\text{enq}} = 3, \hat{c}_{\text{deq}} = 1$ Pouring $k$ items empties inStack, dropping $\Phi$ by $2k$
Splay Tree splay(x) $O(n)$ rotations $\Phi(T) = \sum_{u \in T} \lfloor \log_2 |\text{subtree}(u)| \rfloor$ $\le 3\log_2 n + 1 = O(\log n)$ Zig-Zig / Zig-Zag steps compress deep spines (Access Lemma)

Formal Specification: The Telescoping Potential Theorem

Summing $\hat{c}_i = c_i + \Phi(D_i) - \Phi(D_{i-1})$ across $m$ operations causes all intermediate potential terms $\Phi(D_1), \dots, \Phi(D_{m-1})$ to telescope and cancel out identically:

$$\sum_{i=1}^{m} \hat{c}_i = \sum_{i=1}^{m} \left(c_i + \Phi(D_i) - \Phi(D_{i-1})\right) = \sum_{i=1}^{m} c_i + \Phi(D_m) - \Phi(D_0)$$

Rearranging for total actual cost yields the fundamental validity condition of the Potential Method: if $\Phi(D_0) = 0$ and $\Phi(D_i) \ge 0$ for all states $D_i$, then for every prefix length $m$:

$$\sum_{i=1}^{m} c_i = \sum_{i=1}^{m} \hat{c}_i - \Phi(D_m) \le \sum_{i=1}^{m} \hat{c}_i$$

Visualizing Dynamic Array Potential Sawtooth $\Phi(n, m) = 2n - m$

Notice how $\Phi(n, m) = 2n - m$ climbs by $+2$ on every normal insertion, peaks at $\Phi = m$ when the buffer is full ($n = m$), and immediately drops back to $\Phi = 2$ upon doubling capacity to $2m$—releasing the exact stored energy needed to copy all $n$ elements.

─── Potential Φ(D_i) = 2n - m ███ Actual Cost Spike c_i = n + 1 --- Amortized Cost ĉ_i = 3 (Constant!) Resize 2→4 Resize 4→8 Resize 8→16 (ΔΦ = -6)
flowchart LR subgraph TwoStackQueue["Amortized O(1) Two-Stack Queue (Φ = 2·|inStack|)"] E["enqueue(x): inStack.push(x) c_i = 1, ΔΦ = +2 → ĉ_i = 3"] --> D{"dequeue(): is outStack empty?"} D -- "No" --> Fast["outStack.pop() c_i = 1, ΔΦ = 0 → ĉ_i = 1"] D -- "Yes (k = |inStack|)" --> Pour["Transfer k items inStack → outStack + pop c_i = 2k + 1, ΔΦ = -2k → ĉ_i = 1"] end
Dynamic Array Geometric Doubling & Potential Φ = 2n − m
Amortized O(1)

Step through push(x) operations as capacity doubles (1 → 2 → 4 → 8 → 16). Watch Tarjan’s potential function Φ = 2n − m bank 2 credits on cheap inserts to pay for O(n) reallocation copies.

Step 1 / 10
Custom Input:
Timeline

Invariant & State

Φ(D_i) = 2n − m = 2(1) − 1 = 1 ≥ 0 | ĉ_i = c_i + ΔΦ = 1 + (1 − 0) = 2
🔗 Live Pointer & State Transition Graph — Dynamic Array Geometric Doubling & Potential Φ = 2n − m Active Pointer Jump/Transition Matched/Optimal
NEW (+1 10 [0]
Buffer Capacity m = 1 | Size n = 1 | Load Factor α = 100%
[0] 10 NEW (+1)
Size / Cap (n / m)
1 / 1
Actual Cost c_i
1 (1 write)
Potential Φ = 2n−m
1
Amortized Cost ĉ_i
2 (Avg 1.00)
[Step 1] Inserted 10 at index 0. Actual cost = 1, banked +2 potential (Φ = 1), Amortized cost = 2.

Hardware Reality: Amortized $O(1)$ vs Worst-Case Tail Latency (p99.99 SLA Violations)

While amortized $O(1)$ guarantees optimal throughput over a batch of $N$ operations, a single $O(N)$ resize spike is catastrophic in hard real-time systems (high-frequency trading, audio DSP callbacks, kernel interrupt handlers, or p99.99 RPC tail-latency SLAs). Copying a $64\text{ MB}$ table on a single push_back() stalls the calling thread for $\sim 2\text{ ms}$. Production real-time systems de-amortize table resizing via incremental dual-buffer migration (as used in Redis dictRehash): allocate the $2m$ table upon reaching load factor $1$, and on every subsequent insert/lookup, migrate a constant $c = 2$ elements from the old table to the new table, achieving strict worst-case $O(1)$ per operation.

Production Implementation: De-Amortized vs Amortized Two-Stack Queue

The C++20 implementation below shows the classic Two-Stack FIFO Queue alongside an explicit potential verifier that asserts $\hat{c}_i = c_i + \Delta \Phi_i \le 3$ on every single operation.

// C++20: Two-Stack FIFO Queue with Runtime Potential Verification (Phi = 2 * inStack.size())
#include <cassert>
#include <cstddef>
#include <vector>

template <typename T>
class AmortizedTwoStackQueue {
    std::vector<T> inStack_;
    std::vector<T> outStack_;

public:
    // Potential function Phi(D) = 2 * |inStack| >= 0
    size_t potential() const noexcept { return 2 * inStack_.size(); }

    // Actual cost c_i = 1, Delta Phi = +2 ==> Amortized cost c_hat = 3
    void enqueue(T value) {
        const size_t phiBefore = potential();
        inStack_.push_back(std::move(value));
        const size_t actualCost = 1;
        const ptrdiff_t amortizedCost = static_cast<ptrdiff_t>(actualCost + potential())
                                      - static_cast<ptrdiff_t>(phiBefore);
        assert(amortizedCost == 3);
    }

    // If outStack is empty, pours k = |inStack| items:
    // Actual cost c_i = 2k + 1, Delta Phi = -2k ==> Amortized cost c_hat = 1!
    T dequeue() {
        const size_t phiBefore = potential();
        size_t actualCost = 0;

        if (outStack_.empty()) {
            while (!inStack_.empty()) {
                outStack_.push_back(std::move(inStack_.back()));
                inStack_.pop_back();
                actualCost += 2; // 1 pop + 1 push
            }
        }
        T front = std::move(outStack_.back());
        outStack_.pop_back();
        actualCost += 1;

        const ptrdiff_t amortizedCost = static_cast<ptrdiff_t>(actualCost + potential())
                                      - static_cast<ptrdiff_t>(phiBefore);
        assert(amortizedCost == 1);
        return front;
    }
};

Failure Modes & Adversarial Pitfalls

  • The Half-Full Resize Thrashing Bug (Shrink at $n = m/2$): If a dynamic array doubles capacity when $n = m$ and halves capacity as soon as $n$ drops to $m/2$, an adversary alternating push_back() and pop_back() at the threshold $n = m/2$ triggers an $\Theta(n)$ reallocation on every single operation, degrading amortized complexity from $O(1)$ to $\Theta(n)$! To preserve $O(1)$ amortized push and pop, you must introduce hysteresis: double at $n = m$, but only halve when $n = m/4$ (using the piecewise potential $\Phi(n, m) = |2n - m|$), ensuring the load factor after any resize is $\alpha = 1/2$, far from both thresholds.
  • Amortized Bounds Collapse Under Persistence / Backtracking: If a data structure supports $O(1)$ state snapshotting or branching (persistence), an adversary can repeatedly branch from the exact state $D_{i-1}$ immediately preceding an expensive $O(n)$ operation, paying $\Theta(n)$ every time because the stored potential $\Phi(D_{i-1})$ cannot be spent more than once without lazy memoization (as shown by Chris Okasaki).
Check yourself Question 1 of 4
Concept Check Telescoping Validity Condition for Tarjan's Potential Function Φ recall

In the Physicist's Potential Method, the amortized cost of operation `i` is defined as `ĉ_i = c_i + Φ(D_i) - Φ(D_{i-1})`. What condition on the potential function `Φ` guarantees that the sum of amortized costs `Σ_{i=1..m} ĉ_i` is a valid upper bound on the sum of actual costs `Σ_{i=1..m} c_i` for every prefix of `m` operations?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Suppose a dynamic array doubles capacity (`m → 2m`) when `n = m` and halves capacity (`m → m/2`) as soon as `n` drops to `m/2`. Why does this policy fail in the worst case, and how does shrinking at `n = m/4` fix it?

Key Takeaways

Amortized analysis guarantees the worst-case average cost per operation over any sequence of m operations without any probability assumptions.
In Tarjan's Potential Method, setting ĉ_i = c_i + Φ(D_i) - Φ(D_{i-1}) causes intermediate potentials to telescope: Σ c_i = Σ ĉ_i - Φ(D_m) + Φ(D_0).
For a dynamic array supporting both push and pop, shrinking at n = m/2 causes Θ(n) thrashing; shrinking at n = m/4 (hysteresis) restores O(1) amortized cost.
Naive amortized bounds break in persistent (branching) settings and hard real-time systems where p99.99 tail latency matters.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue