Accounting Method: Credits Stored on Operations
What This Concept Is
The accounting method assigns each operation a fixed amortized cost (called the charge) that may differ from its actual cost. The amortized cost is chosen so that:
- for every operation
i,amortized_i >= actual_ion its own, except that - "cheap" operations are overcharged, generating credit, and
- "expensive" operations are undercharged, and the missing cost is paid from credit stored by earlier cheap operations
As long as the credit balance is always non-negative, the sum of amortized costs upper-bounds the sum of actual costs:
sum actual_i <= sum amortized_i
so the amortized cost per operation is a valid amortized bound.
Concretely, credit is usually imagined as coins stored on specific elements of the data structure, to be spent later when that element is touched. The key intellectual move is choosing where to store credit -- usually on the object that is most directly responsible for triggering the expensive future work. Good credit placement makes the proof read like a receipt; bad placement makes it a blur of symbols.
The accounting method is stricter than aggregate (different ops can have different charges) and looser than potential (no explicit state-dependent function). It is the most intuitive of the three for most people because the bookkeeping language ("charge 3 per push, pay 1 for store, save 2 for future move") matches how engineers already reason about resource budgets.
Why It Matters Here
The accounting method makes some proofs dramatically more intuitive than aggregate. In particular:
- dynamic-array resize: charge
3per append (1for the store,1saved for moving this item later,1saved for moving the item already there that gets paired with it) - stack multipop: charge
2per push (1for the push,1saved for the eventual pop) and charge0per pop/multipop-step - binary-counter increment: charge
2per increment (1for the bit that becomes1,1saved to pay for its eventual reset to0) - union-find and heaps: charge per operation type in terms that precisely reflect the work each kind does
Once you can frame a proof in accounting terms, you usually also see the potential-function version almost immediately: the potential is just "total unspent credit across all elements."
Concrete Examples
Example 1 -- dynamic array with doubling, via accounting. Charge amortized cost 3 per append.
- actual cost of a non-resizing append:
1. Charge3. Net credit:+2, stored on the newly appended element. - actual cost of a resizing append at size
m:m + 1(copymold elements, store new one).- at the moment of resize, the array is full with
melements; each of themelements was stored with credit2on it - use
1credit on each of themitems to pay for its move, leaving credit1on each - use
1credit on each new "unpaired" half of the doubled capacity later
- at the moment of resize, the array is full with
The accounting balances: every resize is paid for by credit that was accumulated when the moved items were first inserted. The amortized cost is 3 = O(1) per append.
Example 2 -- two-stack queue. Implement a queue using two stacks A and B. enqueue(x) pushes onto A (actual cost 1). dequeue() pops from B; if B is empty, first move every element from A to B in reverse order, then pop (actual cost up to 2 * |A| + 1 in the worst case).
Charge 3 per enqueue and 1 per dequeue:
- enqueue: actual
1, charge3, leaves2credits on the enqueued element (one for the future move fromAtoB, one for the future pop fromB) - dequeue when
Bnon-empty: actual1, charge1, balance unchanged - dequeue when
Bempty: actual2k + 1forkelements inA; charge1; remaining2kis paid from the2credits on each of thekenqueued-but-never-moved elements. Balance stays non-negative.
Amortized cost: O(1) per enqueue, O(1) per dequeue. Identical to the aggregate argument, but with each cost attributed to a specific operation rather than averaged.