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.
Common Confusion / Misconceptions
"Credit is a real quantity stored in the data structure." It is not. "Credit" is a bookkeeping device, not a physical resource. No object in the code holds a counter labeled credit; it exists only in the proof. The operational code still does whatever it would do; only the cost accounting is re-interpreted.
"The amortized cost is uniquely determined." It is not. Many choices work as long as credit stays non-negative. Accounting proofs often pick charges that read as "one for the move, one for the pair, one for the store" to make the intuition clear, but any non-negative choice bounding actual is valid.
"If a specific operation's credit goes negative, the proof is broken." The invariant is the running balance (sum of credits across all elements), not any single element's balance. As long as the global balance never goes negative, the proof is intact. In practice, most accounting proofs pick charges where local balances stay non-negative too, but that is a convenience.
"Accounting is just a pedagogical device; always use potential in practice." For Fibonacci heaps and splay trees, yes -- potential is essential. For dynamic arrays, stacks, counters, and union-find, accounting is often shorter and more insightful. Pick based on the proof's clarity, not on fashion.
How To Use It
Proof pattern:
- identify operation types and their actual costs
- assign amortized charges
a_1, a_2, ...to each type - prove that the running credit balance is always non-negative
- read off
sum actual <= sum amortized
Where to place credit is the creative step:
- dynamic array: credit on each item, to pay for its own future move
- stack multipop: credit on each pushed item, to pay for its eventual pop
- binary counter increment: credit on each bit that is set to
1, to pay for its future reset - two-stack queue: credit on each enqueued item, to pay for the one move and one pop it will experience
Debugging a shaky accounting proof:
- list all operation types and their charges
- trace an adversarial sequence and compute running balance element-by-element
- find the step where balance would go negative; either increase charges there or change credit placement
- when stuck, convert to potential (concept 14) and verify
Phi >= 0
Transfer / Where This Shows Up Later
- S5 (OS): token-bucket rate limiters are physical accounting schemes -- tokens are credits paid in as time passes, consumed as requests are served. The proof of fairness follows the same pattern.
- S6 (databases): cost-based query optimization budgets "cost credits" across plan-choice decisions; the accounting framework applies directly.
- S7 (architecture): error budgets in SRE are an accounting scheme -- each "green" month accrues credit, each incident spends it. The abstraction is identical to amortized accounting.
- S8 (scale): API quota systems, priority lanes for paid users, and fairness schedulers all charge/credit per operation and enforce non-negative balance invariants.
Check Yourself
- What is the invariant that makes the accounting method correct?
- In the dynamic-array proof above, why is the charge
3and not2? - How do you know where to place credit in a new proof?
- Why is there freedom in choosing the amortized charges?
- Work through the binary-counter accounting proof: what is the charge per increment and where does credit live?
- When is it cleaner to use the potential method instead of accounting?
Mini Drill or Application
- Prove, using the accounting method, that
npush+pop+multipopoperations on a stack starting empty have total costO(n). State the charges per operation and the credit invariant. - Prove that a binary counter under
nincrements has amortized costO(1)per increment. Where does credit live? - Consider a queue implemented as two stacks (
enqueuepushes onto stack A;dequeuepops from stack B, moving all of A into B if B is empty). Use the accounting method to prove amortizedO(1)for both operations. - For a dynamic array with both
appendandpopand thecapacity / 4shrink rule, assign charges so that both operations areO(1)amortized. Identify where credit accumulates and when it is spent. - Implement a
DynamicArrayin Python with explicitcreditcounters (separate from the real array) and assertsum(credit) >= 0after every operation. Run10^6mixed operations; the assertion should never fire.
Read This Only If Stuck
- CLRS: The accounting method (16.2)
- CLRS: Aggregate analysis (16.1) -- contrast
- CLRS: The potential method (16.3) -- next step
- CLRS: Dynamic tables (Part 3)
- CLRS: Dynamic tables (Part 4)
- Skiena: 2 Algorithm analysis -- discussion of amortized cost and charge schemes
- CMU 15-451 lecture notes on amortized analysis and splay tree potential / accounting
- Pat Morin, Open Data Structures -- accounting and the potential method