Amortized Analysis — মাঝে মাঝে দামি, গড়ে সস্তা
Amortized Analysis
Amortized analysis দামি অপারেশনের খরচ সস্তাদের ওপর ছড়িয়ে দেয় — dynamic array-র n append-এর মোট খরচ 3n প্রমাণ করে দেখায় যে প্রতি-append গড়ে constant, যদিও একক append মাঝে মাঝে linear।
আগে এটা বুঝি
গত লেসনে আমরা growth-এর ভাষা শিখলাম। সেই ভাষায় একটা বিখ্যাত বিরোধ
আছে, যেটা প্রায় প্রতিটা নতুন student-কে থামিয়ে দেয়। Python-এর
documentation বলে list.append-এর complexity O(1)। আপনি গত লেসনের
জ্ঞানে বই খুলে দেখলেন: append করতে গিয়ে যখন array ভর্তি, Python-কे
নতুন বড় block allocate করে পুরো পুরনো content copy করতে হয় —
অর্থাৎ ওই একটা append-এর খরচ তো current size-এর সমান, মানে O(n)!
Documentation কি মিথ্যা বলছে?
না। দুই বাক্যই সত্য — কারণ তারা দুই ভিন্ন প্রশ্নের উত্তর। “একটা append সবচেয়ে খারাপ কত খরচ করতে পারে?” — উত্তর O(n)। “append-গুলোর একটা long sequence-এ গড়ে প্রতি append কত?” — উত্তর O(1)। দ্বিতীয়টাই amortized analysis: দামি মুহূর্তগুলোর খরচ সস্তা মুহূর্তগুলোর ওপর ছড়িয়ে দেওয়া। কফি shop-এর loyalty card-এর মতো — নয়টা coffee সস্তা, দশমটা “ফ্রি” না, দশটার খরচই গড়ে ভাগ হয়ে গেছে।
আজকের প্রশ্ন তিনটা: (১) sequence-এর মোট খরচ ঠিকভাবে upper-bound করা যায় কীভাবে? (২) প্রমাণের জন্য তিনটা আলাদা হিসাব-রীতি — aggregate, accounting, potential — আসলে কী ভিন্ন চোখ? (৩) এই guarantee কোথায় ভাঙে, আর per-operation latency কখন amortized গড়কে চ্যাটার করে?
মূল ধারণা
Dynamic array — সমস্যার সেটআপ
আমাদের running example: capacity c-র একটা array, size s। Append-এর
নিয়ম: slot খালি থাকলে লিখে দাও — খরচ 1। Array ভর্তি হলে দ্বিগুণ
capacity-র নতুন array বানাও, সব পুরনো element copy করো, তারপর লেখো —
খরচ s + 1। এখন n টা append করলে মোট খরচ কত?
Copy হয় শুধু capacity 1, 2, 4, 8, …, অর্থাৎ সব power of two-তে, n-এর আগ পর্যন্ত। মোট copied elements:
Geometric series-এর ক্লাসিক identity — sum সবসময় last term-এর দ্বিগুণের কম। Slot-write ঠিক n বার। সুতরাং মোট খরচ ≤ n + (2n−1) + resizes-এর খুদ্র overhead ≈ 3n। Per-append amortized খরচ ≤ 3 — একটা constant! দামি O(n) append এখনও ঘটে সত্যি, কিন্তু তাদের সংখ্যা মাত্র log₂ n টা — গড়ের হিসাবে সব ডুবে যায়।
পদ্ধতি ১ — Aggregate: সবার মোট হিসাব
সবচেয়ে direct: সবচেয়ে খারাপ sequence-এর মোট খরচ T(n) bound করো, তারপর per-op গড় = T(n)/n। Dynamic array-তে T(n) ≤ 3n, তাই amortized ≤ 3। একটাও probability লাগে না — এটা প্রতিটা possible sequence-এর উপর absolute দাবি।
আরেকটা উদাহরণ — multipop stack: push খরচ 1, pop খরচ 1, আর multipop(k) স্ট্যাকের top-এর k টা element ফেলে — খরচ min(k, s)। একটা multipop একাই O(n) হতে পারে! কিন্তু n টা operation-এর যেকোনো sequence-এ মোট খরচ কত? প্রতিটা element স্ট্যাকে ঢুকেছে ঠিক একবার (push), আর বেরোবার আগে ঢুকতে হয় — তাই total pops ≤ total pushes ≤ n। মোট খরচ ≤ 2n → প্রতিটা অপারেশন amortized O(1)। দামি multipop আসলে আগের push-গুলোর ঋণ শোধ মাত্র।
পদ্ধতি ২ — Accounting: প্রতিটাকে বেশি টাকা দাও
প্রতিটা append-কে charge করো ৩ unit (আসল খরচ 1 হোক বা s+1 হোক):
- 1 unit যায় নিজের slot-write-এ
- বাকি 2 unit element-টার savings account-এ জমে
Resize-এর দিন প্রতিটা moved element-এর খরচ 1 — আর তার account-এ তখন 2 জমা (এই জীবনে দ্বিতীয়বার move হওয়ার আগে আবার 2 করে জমবে)। Account কখনো negative হয় না — এটাই accounting method-এর validity condition। যে খরচ আজকের charge-এ ঢেকে যায় না, তার জন্য আগের কারো জমানো টাকা আছেই। মজার observation: আমরা ঠিক 3 charge করলাম কারণ aggregate বলে গড় ≤ 3 — accounting method নিজে থেকে গড় আবিষ্কার করে না, সে aggregate-এর জ্ঞানকে per-element হিসাবে রূপ দেয়।
পদ্ধতি ৩ — Potential: গোটা structure-এর ঋণ-পত্র
Accounting প্রতি element-এ হিসাব রাখে; potential পুরো data structure-এর একটা scalar “ঋণ” function রাখে:
Amortized cost = actual cost + Φ(after) − Φ(before)। তিনটা কেস:
| কেস | Actual | ΔΦ | Amortized |
|---|---|---|---|
| Normal append (slot ছিল) | 1 | +2 | 3 |
| Resize append (s == c ছিল) | s + 1 | (2(s+1) − 2s) − (2s − s) = 2 − s | (s+1) + (2−s) = 3 |
| প্রথম append (capacity 0 → 1) | 1 | +1 | 2 |
সব কেসে amortized cost constant — potential নিজেই দামি মুহূর্তের খরচ absorb করে নিচ্ছে, কারণ resize-এর আগে Φ ছিল বিশাল (s), মানে গোটা structure ততক্ষণ ঋণ জমিয়ে বসে ছিল। Validity শর্ত: Φ ≥ 0 সবসময় — convention নিই শূন্য array-র capacity 0, চাইলেই grow (তাই শুরুতে Φ = 0); এরপর capacity কখনো size-এর দ্বিগুণের বেশি হয় না, অর্থাৎ 2s ≥ c বজায় থাকে। তাহলে মোট actual ≤ মোট amortized = 3n-এর নিচে।
Binary counter — আরেকটা ক্লাসিক
k-bit binary counter, increment অপারেশন: নিচের বিট থেকে carry propagate। 000111-কে increment করলে ৩টা bit flip (111→000 carry, 0→1)। Worst case একটা increment-এ k টা flip — O(k)। কিন্তু n টা increment-এ মোট? Bit 0 প্রতিটা increment-এ ফ্লিপ করে, bit 1 প্রতি দ্বিতীয়টায়, bit i প্রতি 2^i-তম increment-এ। মোট flip:
Per-increment amortized ≤ 2 flips — যদিও একক increment log n বিট ঘুরিয়ে দিতে পারে। একই geometric-series স্বাদ, নতুন costume-এ।
Average case বনাম amortized — সূক্ষ্ম কিন্তু অনড়
দুটোই “গড়”-এর কথা বলে, কিন্তু ভিন্ন জিনিসের গড়:
| Amortized | Average-case | |
|---|---|---|
| গড়টা কীসের উপর | operation-এর ক্রমের উপর (time) | input-এর distribution-এর উপর |
| Assumption | কোনোটাই না — সব sequence-এ সত্য | একটা specific probability model |
| Quicksort | — | O(n log n) average (random pivot assumption) |
| list.append | O(1) amortized, unconditional | — |
Adversary আপনার input ইচ্ছেমতো দুষ্ট করলেও amortized bound টিকে থাকে — quicksort-এর average bound ভেঙে পড়ে (sorted input + খারাপ pivot rule)। Production system-এর ভাষায়: amortized হলো SLA, average হলো hope।
ভেতরে কী ঘটছে
CPython আসলে কী করে
CPython-এর list pure doubling করে না। Growth pattern আনুমানিক
~1.125× (নতুন capacity ≈ old + old/8 + constant)। কেন এত লোভী না?
কারণ over-allocated slot-গুলো বাস্তব memory খায় — doubling গড়ে ~২৫%
slot খালি রাখে, 1.125 রাখে ~৬%। Copy-র মোট সংখ্যা বেড়ে যায় (factor
f হলে total copies ≈ n/(f−1)), কিন্তু C-স্তরের copy এত দ্রুত (memcpy)
যে সেই লস মেমরি-saving-এর কাছে মূল্যবান না। Java ArrayList বেছেছে 1.5,
C++ vector standard-এ 2 (implementation-defined)। তিনটা ecosystem,
একই theory, তিন রকম trade-off point — geometric constraint-এর ভেতরের
স্বাধীনতা এটুকুই।
Latency — যখন গড় যথেষ্ট না
Audio callback প্রতি ৫ ms-এ চলবেই, গড়ে না। Frame budget, trading system deadline, request tail-latency SLO — সবখানে প্রশ্নটা “worst single moment” — amortized গড় সেখানে প্রায় অর্থহীন। Dynamic array-র resize spike production system-এ বাস্তব সমস্যা: এক কোটি element-এর vector-এর একটা append হঠাৎ করে ১০⁷ element copy শুরু করলে latency graph-এ স্পাইক ধরা পড়বেই। প্রফেশনাল প্রতিকার: preallocation (reserve upfront), incremental flush/compaction strategies, বা chunked structure (deque — পরের লেসনগুলোর বিষয়)। Amortized analysis throughput-এর ভাষা; latency-র প্রশ্নে আপনাকে আলাদা করে ভাবতেই হবে।
- Call sitelist.append(v) — user দেখছে একটা অপারেশন, খরচ যা-ই হোক
- Capacity checksize == capacity ? এই তুলনাই fast/slow path-এর দারোয়ান
- Fast pathslot-write + size++ — খরচ 1, potential 2 unit জমে — এরাই resize-এর প্রিমিয়াম জমায়
- Slow pathrealloc + s টা copy — খরচ s+1, কিন্তু প্রতিটা element-এর জমানো credit এখানেই খরচ হয়
- LedgerΦ = 2s − c আপডেট — গোটা history মিলিয়ে প্রতি append গড়ে 3-এর বেশি কখনো না
উদাহরণ
হাতে-হাতে: ১০টা append-এর পূর্ণ লেজার
Capacity 1 থেকে শুরু, doubling। প্রতিটা append-এর actual খরচ, জমা credit (accounting view, প্রতি append চার্জ 3), আর balance:
| Append # | Capacity আগে/পরে | Copies | Actual | Charge | Bank after |
|---|---|---|---|---|---|
| 1 | 1 → 2 | 1 | 2 | 3 | 1 |
| 2 | 2 → 4 | 2 | 3 | 3 | 1 |
| 3 | 4 → 4 | 0 | 1 | 3 | 3 |
| 4 | 4 → 8 | 4 | 5 | 3 | 1 |
| 5 | 8 → 8 | 0 | 1 | 3 | 3 |
| 6 | 8 → 8 | 0 | 1 | 3 | 5 |
| 7 | 8 → 8 | 0 | 1 | 3 | 7 |
| 8 | 8 → 16 | 8 | 9 | 3 | 1 |
| 9 | 16 → 16 | 0 | 1 | 3 | 3 |
| 10 | 16 → 16 | 0 | 1 | 3 | 5 |
মোট actual = 27, মোট charge = 30, bank কখনোই negative হয়নি — minimum 1। Per-append actual গড় = 2.7 — এবং n বাড়ালে এই গড় 3-এর দিকে তাক করে, কখনো পার করে না। Bank-এর balance কখনো না কখনো resize-এর পর 1-এ নেমে আসে — ঠিক ততটাই credit জমেছিল যতটা দরকার ছিল। এটাই accounting method- এর সৌন্দর্য: abstract inequality-টা একটা দৃশ্যমান হিসাবের বই হয়ে যায়।
ছবিতে
Doubling timeline — খরচ কোথায় জমে কোথায় ফোটে
appends: 1 2 3 4 5 6 7 8 9 ... 16
capacity: 1→2 2→4 4 4 4 8→16 16
cost: 2 3 1 5 1 1 1 9 1 ... 17
▄▄ ▀▀ ▄ ██ ▄ ▄ ▄ ████ ▄ ████████
↑ ↑ ↑ ↑
resize resize resize resize
সস্তা append-গুলোর জমানো credit প্রতিটা spike-এই খরচ হয়নিজে চালিয়ে দেখুন
CPython-এর গোপন resize — sizeof trace আর per-append spike
# amortize_lab.py — list.append-এর hidden cost moments
# চালান: python3 amortize_lab.py
import sys, time
N = 100_000
xs = []
sizes_seen = {}
jump_at = []
spike_ns = []
for i in range(N):
t0 = time.perf_counter_ns()
xs.append(i)
dt = time.perf_counter_ns() - t0
if dt > 500: # গড়ের অনেক ওপরে — candidate spike
spike_ns.append((i, dt))
raw = sys.getsizeof(xs)
if raw not in sizes_seen:
sizes_seen[raw] = i # প্রথমবার এই capacity — এটাই resize moment
jump_at.append((i, raw))
print(f'{N:,} appends, distinct capacities seen: {len(jump_at)}')
print('first 10 capacity jumps:')
for idx, sz in jump_at[:10]:
print(f' append #{idx:>7,} -> capacity block {sz:>9,} bytes')
print('top 5 slowest appends:')
for idx, dt in sorted(spike_ns, key=lambda p: p[1], reverse=True)[:5]:
print(f' append #{idx:>7,}: {dt:>9,} ns')
mean = sum(d for _, d in spike_ns) / max(len(spike_ns), 1)
print(f'spike count: {len(spike_ns)}, typical append well below 500 ns, '
f'spikes reach {max(d for _, d in spike_ns):,} ns')python3 amortize_lab.pyTypical আকৃতি (সংখ্যা machine/version-ভেদে বদলাবে, pattern বদলাবে না):
100,000 appends, distinct capacities seen: 42
first 10 capacity jumps:
append # 0 -> capacity block 120 bytes
append # 4 -> capacity block 184 bytes
append # 8 -> capacity block 216 bytes
append # 16 -> capacity block 280 bytes
append # 24 -> capacity block 344 bytes
...
top 5 slowest appends:
append # 98304: 1,204,318 ns
append # 90112: 988,412 ns
append # 81920: 871,006 ns
append # 65536: 745,220 ns
append # 73728: 512,904 ns
spike count: 61, typical append well below 500 ns, spikes reach 1,204,318 nsতিনটা reading: (১) capacity blocks ধাপে ধাপে বাড়ছে — geometric, factor ~1.125, doubling না; (২) spike-গুলোর index বড় resize moment- গুলোর (৬৫৫৩৬, ৮১৯২০, ৯৮৩০৪ — প্রতিবার ~১.১২৫× গুণ) চারপাশে cluster করছে — একক append মাঝে মাঝে মাইক্রোসেকেন্ড খায় যখন গড় কয়েকশ nanosecond; (৩) মোট সময় এখনও N × constant — গড় ভাঙেনি।
যা প্রমাণিত: documentation-এর O(1) দাবি গড়ের দাবি — বাস্তব মেশিনে সেই গড়ের ভেতরে মাপা যায় এমন দামি মুহূর্ত থাকে, আর সেগুলোর অবস্থান theory (capacity schedule) থেকেই predict করা যায়।
Python list append-এর ভেতরে লুকানো capacity jump আছে — sizeof হঠাৎ লাফ দেয় নির্দিষ্ট ব্যবধানে, আর per-append timing-এ ঠিক সেই মুহূর্তগুলোতে latency spike ধরা পড়ে; গড় সময় constant থাকে, একক মুহূর্ত হাজারগুণ দামি হয় — amortized analysis-র দুই মুখ একসাথে।
নিজে বানান
DynArr — নিজের dynamic array, সঙ্গে সততার লেজার
- Fixed buffer দিয়ে শুরু করুন — size, capacity, আর দুইটা counter: writes ও copies
- append-এ capacity full হলে doubling করে নতুন buffer-এ সব copy করুন — copies counter বাড়ান
- Slot-write প্রতিবার writes counter বাড়ান — এই দুই counter-ই আমাদের লেজার
- n টা append চালিয়ে মোট elementary op (writes + copies) প্রতি append গড়ে ছাপুন — 3-এর কাছাকাছি আসা উচিত
- Growth factor প্যারামিটার করে 1.25 বনাম 2 তুলনা করুন — copy count আর final slack memory দুই-ই দেখান
লক্ষ্য: theory-র 3n bound-টা নিজের structure নিজের counter দিয়ে empirically verify করা — আর growth factor-এর trade-off হাতে অনুভব করা।
# dynarr.py — manual dynamic array with honest bookkeeping
# চালান: python3 dynarr.py
class DynArr:
__slots__ = ('_buf', '_size', 'writes', 'copies', 'resizes', 'growth')
def __init__(self, growth=2.0):
self._buf = [None] * 1
self._size = 0
self.writes = 0 # elementary slot-writes
self.copies = 0 # elements moved during resizes
self.resizes = 0
self.growth = growth
def __len__(self):
return self._size
def _capacity(self):
return len(self._buf)
def _resize(self):
new_cap = int(self._capacity() * self.growth) + 1
new_buf = [None] * new_cap
for i in range(self._size):
new_buf[i] = self._buf[i]
self.copies += 1
self._buf = new_buf
self.resizes += 1
def append(self, value):
if self._size == self._capacity():
self._resize()
self._buf[self._size] = value
self.writes += 1
self._size += 1
def get(self, i):
if i >= self._size:
raise IndexError('out of range') # negative indexing omitted — exercise!
return self._buf[i]
def report(name, arr, n):
total = arr.writes + arr.copies
slack = arr._capacity() - arr._size
print(f'{name:<14} appends={n:>8,} writes={arr.writes:>8,} '
f'copies={arr.copies:>8,} resizes={arr.resizes:>3} '
f'total={total:>8,} per-append={total/n:5.2f} slack-slots={slack:,}')
if __name__ == '__main__':
N = 200_000
for g, name in [(2.0, 'factor 2.0'), (1.5, 'factor 1.5'), (1.25, 'factor 1.25')]:
a = DynArr(growth=g)
for i in range(N):
a.append(i)
report(name, a, N)
# empirical amortized proof: per-append cost stays flat as n doubles
print('\ndoubling check (factor 2.0):')
prev = None
for n in [50_000, 100_000, 200_000, 400_000]:
a = DynArr()
for i in range(n):
a.append(i)
total = (a.writes + a.copies) / n
line = f'n={n:>7,}: total-ops-per-append = {total:4.2f}'
if prev is not None:
line += f' (delta from previous: {total - prev:+.2f})'
print(line)
prev = totalপ্রত্যাশিত output-এর আকৃতি:
factor 2.0 appends= 200,000 writes= 200,000 copies= 262,142 resizes= 18 total= 462,142 per-append= 2.31 slack-slots=161,056
factor 1.5 appends= 200,000 writes= 200,000 copies= 398,585 resizes= 27 total= 598,585 per-append= 2.99 slack-slots= 76,483
factor 1.25 appends= 200,000 writes= 200,000 copies= 800,004 resizes= 53 total=1,000,004 per-append= 5.00 slack-slots= 49,999
doubling check (factor 2.0):
n= 50,000: total-ops-per-append = 2.31 (delta from previous: +0.00)
n=100,000: total-ops-per-append = 2.31 (delta from previous: +0.00)
n=200,000: total-ops-per-append = 2.31 (delta from previous: +0.00)
n=400,000: total-ops-per-append = 2.32 (delta from previous: +0.01)Reading: factor 2.0-এ per-append ≈ 2.31 (theory বলছে ≤ 3 ✓), আর doubling n করলেও per-append cost flat — এটাই amortized O(1)-এর empirical fingerprint। Factor ছোট করলে copy খরচ বাড়ছে (1.25-এ প্রায় দ্বিগুণ), কিন্তু slack memory কমছে — CPython ঠিক এই axis-এ 1.125 বেছেছে। লক্ষ্য করুন copies ≈ n/(g−1): g=2 দিলে ≈ n, g=1.25 দিলে ≈ 4n — formula আর measurement মিলে যাচ্ছে।
নিজে বাড়ান:
pop()যোগ করুন — size quarter-এ নামলে capacity half করুন (hysteresis ছাড়া কেন খারাপ, oscillation বানিয়ে দেখুন)- Iterator (
__iter__) যোগ করুন buffer-এর active prefix-এর উপর - Accounting-view assertion যোগ করুন — প্রতি append-এ 3 credit জমান, প্রতি copy-তে 1 খরচ করুন, প্রতিটা মুহূর্তে bank ≥ 0 assert করুন — theory-র inequality runtime-এ enforce হচ্ছে এটাই প্রমাণ
বাস্তব সিস্টেমে
Amortized thinking বাস্তবে যেখানে দেখা যায়
প্রতিটা modern language-এর growable array। C++ vector, Java ArrayList, Go slice, Rust Vec, JavaScript array engine internals, CPython list — সবাই geometric growth, সবাই amortized O(1) append। Factor choice আলাদা (Rust 2×, Go ছোট slice-এ 2× বড়গুলোতে ~1.25×, CPython ~1.125×) কিন্তু principle এক।
Log-structured storage। LSM-tree (LevelDB, RocksDB, Cassandra) compaction মাঝে মাঝে পুরো dataset rewrite করে — দামি মুহূর্ত; write-এর গড় খরচ তবু constant — database world-এর বৃহত্তর dynamic array। Kafka segment rotation, git packfile rebuild — একই ছাঁচ।
Garbage collector-এর batch work। Generational GC minor collection মাঝে মাঝে চলে (দামি), mutation-এর মূল কাজ তবু per-object cheap — runtime designers স্পষ্টভাবে amortization কথা বলে design doc-এ। Pause-time problem-টা ঠিক এই লেসনের latency-vs-throughput tension-ই।
যে ভুলগুলো সবাই করে
“Amortized O(1) মানে average case O(1) — একই কথা।”
ভিন্ন জিনিস। Average case input-এর উপর একটা probability distribution ধরে — সেই distribution বাস্তবের সঙ্গে না মিললে দাবিটা অর্থহীন। Amortized কোনো assumption-ই করে না: প্রতিটা সম্ভাব্য অপারেশন-sequence-এর মোট খরচ bound করে, adversary-ও ভাঙতে পারে না। list.append-এ দুষ্ট user specific pattern দিয়ে গড় খরচ বাড়াতে পারবে না; quicksort-এ পারবে (sorted input, fixed pivot) — এটাই পার্থক্যের প্রমাণ। একটা deterministic sequence-guarantee, আরেকটা statistical input-assumption।
“Amortized O(1) মানে প্রতিটা অপারেশন আসলে দ্রুত হয়ে গেছে।”
না — দামি অপারেশন আজও সমান দামি। Amortized bound বলছে মোট হিসাব ভালো, একটা নির্দিষ্ট call-এর খরচ সম্পর্কে কিছুই বলছে না। Real-time audio, frame-budgeted game loop, বা tail-latency SLO-তে এই ভুল বোঝাবুঝি production incident: গড় benchmark pass করেছে, কিন্তু প্রতি লক্ষ request-এ একটা ৪০ ms spike — user-এর কাছে সেটাই প্রোডাক্ট। গড় throughput-এর প্রশ্নে amortized analysis রাজা; single-moment deadline-এর প্রশ্নে আপনাকে worst-case বা preallocation-এর পথ নিতে হবে।
“Growth factor যত বড়, array তত ভালো — doubling সবসময় best।”
Bigger factor কম resize দেয়, কিন্তু বেশি memory অপচয় করে — factor f-এ গড়ে (f−1)/f fraction খালি: doubling-এ ৫০%, 4× growth-এ ৭৫% বরবাদ। Cache-এর দৃষ্টিতেও বড় block jump locality নষ্ট করে। আবার factor খুব ছোট (1.125) হলে copy-র মোট পরিমাণ বাড়ে n/(f−1) সূত্রে। CPython 1.125 নেয় কারণ তার list অনেক সময় small থাকে আর memory pressure বড় খরচ; Rust 2.0 নেয় systems context-এ। সঠিক প্রশ্ন: আপনার workload-এ memory tax আর copy tax-এর বিনিময় হার কী — universal optimum নেই।
বুঝেছেন কি না দেখুন
1তিনটা amortized method-এর নাম আর এক-এক বাক্যে মূল ভাব লিখুন। কোনটা
probability ব্যবহার করে?
স্মরণ
Aggregate — পুরো sequence-এর মোট খরচ bound করে n দিয়ে ভাগ করা। Accounting — প্রতিটা অপারেশনকে বেশি charge দিয়ে surplus জমানো, ভবিষ্যতের দামি অপারেশন সেই জমা থেকে শোধ, bank কখনো negative না। Potential — structure-এর অবস্থার উপর একটা scalar ঋণ-function Φ, amortized cost = actual + ΔΦ। কোনোটাই probability ব্যবহার করে না — সব কটা deterministic; probability average-case analysis-এর জিনিস।
2Capacity 8-এ পৌঁছানো পর্যন্ত (৮ম append পর্যন্ত) মোট copy কত হয়
doubling scheme-এ? Formula n/(g−1) দিয়ে g=2 হলে estimate কত দেয়?
প্রয়োগ
Copy ঘটে capacity transition-গুলোতে: 1→2 (1 copy), 2→4 (2), 4→8 (4) — মোট 7 (৮ম append-এর নিজের কাজ শুধু slot-write)। General: 1+2+4+⋯+2^(k−1) = 2^k − 1 = n − 1 যখন n power of two। Formula n/(g−1) = 8/1 = 8 — approximation দেয় 8, আসল 7; বড় n-এ gap negligible (exact: n − 1)। এই formula-টাই factor-choice এর cost-side।
3একটা teammate বলছে: “আমরা vector-এ reserve() আগে ডাকি না, তবু amortized
O(1), তাই reserve অপ্রয়োজনীয়।” কোথায় ভুল — বিশেষত latency-sensitive
service-এ?
যুক্তি
Throughput-এর হিসাবে সে ঠিক — মোট খরচ তবু 3n-এর মধ্যে। কিন্তু reserve ছাড়া resize spike গুলো রানটাইমে ঘটে: এক কোটি element-এ একটা append হঠাৎ ১ কোটি copy শুরু করে — p99.9 latency-তে স্পষ্ট স্পাইক। Latency-sensitive service-এ প্রশ্ন গড় না, worst moment — reserve করলে সেই মুহূর্তগুলো startup-এ সরিয়ে predictable করা যায়। Plus over-allocation schedule জানা থাকলে memory planning-ও সহজ। Amortized guarantee আর latency engineering দুই আলাদা লেন — একটার সত্যি হওয়া আরেকটাকে বাতিল করে না।
4Binary counter-এর বদলে Gray code counter ব্যবহার করলে প্রতি increment-এ
ঠিক ১টা bit flip হয়। তাহলে binary counter কেন standard? Amortized
argument কী বলে?
ডিজাইন
Gray code increment-এ flip 1, কিন্তু পড়া দরকার হলে counter-এর মান decode করতে হয় O(k) — খরচ অপারেশন থেকে read-এ সরে গেল, মুछে যায়নি। Binary counter read O(1) (bits already are the value), increment amortized 2 flips। Total-work argument: workload এ কতবার read আর কতবার increment — সেই ratio সিদ্ধান্ত বদলায়। Amortized thinking শেখায়: একটা অপারেশনের খরচ একা না, পুরো operation-mix-এর মোট খরচ দেখে structure বাছতে হয়। (Hardware-এ বিশেষ ক্ষেত্রে Gray counter সত্যিই জেতে — async FIFO pointer-এ, যেখানে multi-bit simultaneous flip glitch তৈরি করে।)
5Multipop stack: একটা multipop(k) worst case Θ(n)। কেন তবু n টা
অপারেশনের যেকোনো sequence-এর মোট খরচ O(n)?
স্মরণ
কারণ প্রতিটা element বেরোতে পারে ঠিক একবার, আর বেরোনোর আগে ঢুকতেই হয়। Push সর্বোচ্চ n বার (প্রতি অপারেশন অন্তত একটা খরচ করে), সুতরাং pop+multipop মিলে সর্বোচ্চ n বার element remove করে — মোট removal work ≤ মোট insertion work ≤ n। দামি multipop-এর খরচ আসলে আগের push-গুলোর খরচের পুনঃপ্রদান — double counting এড়ালেই aggregate bound পরিষ্কার।
এরপর কী
এরপর কী
Amortized analysis আমাদের দিল sequence-এর সততার হিসাব — কিন্তু এখনো আমরা array-কে ভাবি abstract box হিসেবে। পরের লেসনে সেই box-টা খুলব: contiguous memory layout কীভাবে O(1) indexing-কে base + i×size arithmetic-এ নামায়, কেন spatial locality array-কে cache-এর প্রিয় বানায়, row-major traversal column-major-কে কেন বাস্তবে হারায় (নিজের মেশিনে মেপে), আর middle insertion-এর O(n) খরচ ঠিক কোথায় ঘটে। Theory আর silicon-এর প্রথম direct handshake — এটাই এই module-এর আসল স্বাদ।
পরের লেসন: Array ও Dynamic Array — memory layout থেকে performance পর্যন্ত।
আরও পড়ুন
- Introduction to Algorithms (3rd edition), Chapter 17 — Amortized Analysis — Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein · Aggregate, accounting, potential — তিন পদ্ধতির canonical treatment; dynamic table analysis এই অধ্যায়েই
- "Amortized Computational Complexity" (SIAM Journal on Algebraic and Discrete Methods, 1985) — Robert Endre Tarjan · Amortized analysis-কে unified framework হিসেবে দাঁড় করানো মূল পেপার — potential method-এর জনক
- Purely Functional Data Structures — Chris Okasaki · কোথায় amortized bound ভেঙে পড়ে (persistent structures, scheduling) — hood section-এর সতর্কতার উৎস