অ্যামর্টাইজড অ্যানালাইসিস — অ্যাগ্রিগেট মেথড
এই পাঠে যা শিখবেন
- অ্যামর্টাইজড cost কী, এবং কেন এটি সাধারণ worst-case-প্রতি-অপারেশন অ্যানালাইসিসের চেয়ে বেশি নির্ভুল
- ক্যাপাসিটি-ডাবলিং ডাইনামিক অ্যারে কীভাবে কাজ করে — কেন কখনও কখনও একটি
push()ব্যয়বহুল হয় - অ্যাগ্রিগেট মেথড দিয়ে জ্যামিতিক সিরিজ ব্যবহার করে $n$-টি push-এর মোট cost $O(n)$ প্রমাণ করা
- একটি প্রকৃত ইমপ্লিমেন্টেশনে ২,০০০টি push চালিয়ে মোট কপি অপারেশনের সংখ্যা গুনে বাউন্ডটি সত্যিই ধরে রাখে কিনা তা যাচাই করা
১ · সমস্যাটা কী — worst-case-প্রতি-অপারেশন কেন যথেষ্ট নয়
ধরুন একটি ডেটা স্ট্রাকচারে $n$-টি অপারেশন করা হবে, এবং প্রতিটি অপারেশনের worst-case cost $O(n)$। সরল যুক্তিতে মনে হতে পারে পুরো সিকোয়েন্সের cost $O(n) \times n = O(n^2)$। কিন্তু বাস্তবে অনেক ডেটা স্ট্রাকচারে এই "ব্যয়বহুল" ঘটনাটি প্রায়ই ঘটে না — এটি ঘটার পর পরবর্তী অনেকগুলো অপারেশন সস্তা হতে বাধ্য হয়। এই নির্ভরতা ধরতে না পারলে অ্যানালাইসিস অপ্রয়োজনীয়ভাবে লুজ (pessimistic) হয়ে যায়।
অ্যামর্টাইজড অ্যানালাইসিসAmortized Analysisএকটি ডেটা স্ট্রাকচারের উপর করা অপারেশনগুলোর একটি সম্পূর্ণ সিকোয়েন্সের মোট cost বিশ্লেষণ করে প্রতি-অপারেশন গড় cost বের করার কৌশল — এলোমেলো ইনপুটের উপর নির্ভরশীল average-case অ্যানালাইসিসের (L08) থেকে আলাদা, কারণ এটি guaranteed (worst-case সিকোয়েন্সেও সত্য), র্যান্ডম বা প্রায়োগিক ধারণার উপর নয়। সমাধান দেয় — এটি বলে না "গড় ইনপুটে" cost কম, বরং প্রমাণ করে যে যেকোনো সিকোয়েন্সে মোট cost সবসময় একটি নির্দিষ্ট বাউন্ডের মধ্যে থাকবেই। তিনটি প্রমাণ কৌশল আছে — অ্যাগ্রিগেট মেথড (এই পাঠ), অ্যাকাউন্টিং মেথড (L44), এবং পটেনশিয়াল মেথড (L45)। তিনটিই একই সিদ্ধান্তে পৌঁছায়, শুধু ভিন্ন উপায়ে প্রমাণ করে।
Python-এর নিজস্ব list ঠিক এই কৌশলেই (ক্যাপাসিটি ওভারগ্রোথ) ইমপ্লিমেন্ট করা — DSA কোর্সে অ্যারে/লিস্ট নিয়ে কাজ করার সময় append()-কে "amortized $O(1)$" বলা
হয়েছে বলে যদি মনে থাকে, এই পাঠেই সেই দাবিটার সম্পূর্ণ প্রমাণ পাবেন।
২ · ক্যাপাসিটি-ডাবলিং ডাইনামিক অ্যারে
একটি ডাইনামিক অ্যারে একটি ফিক্সড-সাইজ capacity নিয়ে শুরু হয়। যতক্ষণ জায়গা আছে, push()
শুধু পরবর্তী খালি স্লটে মান বসায় — cost $1$। কিন্তু যখন অ্যারে পূর্ণ হয়ে যায় (size == capacity),
তখন একটি নতুন, দ্বিগুণ ক্যাপাসিটির অ্যারে বরাদ্দ করে পুরনো সব এলিমেন্ট নতুন অ্যারেতে কপি করতে
হয় — cost $= \text{size} + 1$ (পুরনো এলিমেন্ট কপি + নতুন এলিমেন্ট বসানো)।
৩ · অ্যাগ্রিগেট মেথড — মোট cost সরাসরি গণনা
ধরা যাক ফাঁকা অ্যারে থেকে শুরু করে $n$-টি push() করা হলো। প্রতিটি push-এ নতুন এলিমেন্ট বসানোর
জন্য cost $1$ — মোট $n$। এছাড়া, প্রতিবার রিয়েলোকেশন ঘটলে অতিরিক্ত কপি cost লাগে। ক্যাপাসিটি যায় $1, 2, 4,
\dots, 2^{k-1}$ পর্যন্ত (যেখানে $2^{k-1} < n$), এবং প্রতিটি রিয়েলোকেশনে কপি cost পুরনো ক্যাপাসিটির সমান:
$$\text{মোট কপি cost} = \sum_{i=0}^{k-1} 2^i = 2^k - 1 < 2n$$
(কারণ $2^{k-1} < n$ মানে $2^k < 2n$)। সুতরাং:
$$\text{মোট cost} = \underbrace{n}_{\text{প্রতিটি push-এর অ্যাসাইনমেন্ট}} + \underbrace{(2^k - 1)}_{\text{সব রিয়েলোকেশনের কপি}} < n + 2n = 3n$$
$$\text{Amortized cost per push} = \frac{\text{মোট cost}}{n} < 3 = O(1)$$
এখানেই অ্যাগ্রিগেট মেথডের মূল কৌশল — প্রতিটি অপারেশন আলাদাভাবে না দেখে পুরো সিকোয়েন্সের মোট cost সরাসরি গণনা করে, তারপর $n$ দিয়ে ভাগ করে amortized cost বের করা হয়। কেন এটি কাজ করে তা বোঝার চাবিকাঠি হলো: ক্যাপাসিটি দ্বিগুণ (গুণিতক হারে) বাড়ে বলেই কপি cost-গুলো একটি জ্যামিতিক সিরিজ তৈরি করে, যার যোগফল রৈখিক ($O(n)$) — যদি ক্যাপাসিটি প্রতিবার মাত্র $1$ করে বাড়ানো হতো (গাণিতিক সিরিজ), যোগফল হতো $O(n^2)$, এবং amortized cost হতো $O(n)$, একদমই $O(1)$ নয়।
class DynamicArray:
"""ফাঁকা অবস্থা থেকে শুরু হয়ে, ভর্তি হয়ে গেলে ক্যাপাসিটি ডাবল করে
পুরনো এলিমেন্টগুলো নতুন অ্যারেতে কপি করে -- ক্লাসিক ডাইনামিক অ্যারে।"""
def __init__(self):
self.capacity = 0
self.size = 0
self.arr = []
def push(self, value):
cost = 0 # এই push()-এ মোট কতগুলো "বেসিক অপারেশন" (কপি + অ্যাসাইনমেন্ট) হলো
if self.size == self.capacity:
# অ্যারে পূর্ণ -- রিয়েলোকেট করে ক্যাপাসিটি ডাবল করা হচ্ছে
new_capacity = 1 if self.capacity == 0 else self.capacity * 2
new_arr = [None] * new_capacity
for i in range(self.size):
new_arr[i] = self.arr[i]
cost += 1 # প্রতিটি কপি একটি অপারেশন
self.arr = new_arr
self.capacity = new_capacity
self.arr[self.size] = value
self.size += 1
cost += 1 # নতুন এলিমেন্ট বসানো
return cost
n = 2000
da = DynamicArray()
total_copies = 0
total_cost = 0
per_push_cost = []
for i in range(n):
c = da.push(i)
per_push_cost.append(c)
total_cost += c
total_copies += (c - 1) # cost থেকে ১ (অ্যাসাইনমেন্ট) বাদ দিলে বাকিটাই কপি
print(f"মোট push সংখ্যা (n): {n}")
print(f"মোট কপি অপারেশন: {total_copies}")
print(f"মোট কপি / n: {total_copies / n:.4f}")
print(f"মোট cost (কপি + অ্যাসাইনমেন্ট): {total_cost}")
print(f"গড় (amortized) cost per push = মোট cost / n: {total_cost / n:.4f}")
print("\nসবচেয়ে ব্যয়বহুল ১০টি push (বাকি প্রায় সবগুলোরই cost = 1):")
top10 = sorted(range(1, n + 1), key=lambda i: -per_push_cost[i - 1])[:10]
for idx in sorted(top10):
print(f" push #{idx}: cost = {per_push_cost[idx - 1]}")
অ্যাগ্রিগেট মেথড প্রমাণ করে $n$-টি push-এর মোট cost $O(n)$, তাই amortized cost per push $O(1)$ — প্রকৃতপক্ষে পরিমাপ করা সংখ্যা ($\approx 2.02$) সেই প্রমাণের সাথে হুবহু মেলে। এই একই ডাইনামিক অ্যারে এবং একই ২,০০০-push সিকোয়েন্স L44-এ অ্যাকাউন্টিং মেথড দিয়ে এবং L45-এ পটেনশিয়াল মেথড দিয়ে আবার বিশ্লেষণ করা হবে — একই সিদ্ধান্তে পৌঁছাতে, শুধু ভিন্ন প্রমাণ-কৌশল দিয়ে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ যদি প্রতিটি push-এর worst-case cost ($O(n)$) ধরে সেটাকে $n$ দিয়ে গুণ করে বলা হয় পুরো সিকোয়েন্সের cost $O(n^2)$ — এটা কি "ভুল"?
গাণিতিকভাবে ভুল নয় — এটি একটি বৈধ upper bound, কিন্তু অত্যন্ত লুজ (pessimistic)। এটি ধরে নেয় প্রতিটি push-ই worst-case (রিয়েলোকেশন) ঘটাবে, যা বাস্তবে কখনও ঘটে না — রিয়েলোকেশনের পরে বহু push cost-$1$ হতে বাধ্য (নতুন ফাঁকা জায়গা ব্যবহার করে)। অ্যাগ্রিগেট মেথড এই নির্ভরতা (dependency) ধরে টাইট বাউন্ড ($O(n)$, $O(n^2)$ নয়) দেয় — এটাই অ্যামর্টাইজড অ্যানালাইসিসের পুরো পয়েন্ট।
প্র ০২
ক্যাপাসিটি প্রতিবার দ্বিগুণ না করে ঠিক $1$ করে বাড়ানো হলে (capacity += 1) কী হতো?
তাহলে প্রতিটি push-ই রিয়েলোকেশন ঘটাত (অ্যারে সবসময় ঠিক পূর্ণ থাকত), এবং $i$-তম push-এ কপি cost হতো $i - 1$। মোট কপি cost হতো $0 + 1 + 2 + \dots + (n-1) = \frac{n(n-1)}{2} = O(n^2)$ — একটি গাণিতিক সিরিজ, জ্যামিতিক সিরিজ নয়। তখন amortized cost per push হতো $O(n)$, মোটেও $O(1)$ নয়। এটাই দেখায় কেন গুণিতক (multiplicative) গ্রোথ — দ্বিগুণ, তিনগুণ, বা যেকোনো ধ্রুবক ফ্যাক্টরে — জরুরি, শুধু "ক্যাপাসিটি বাড়ানো" যথেষ্ট নয়।
প্র ০৩ অ্যাগ্রিগেট মেথডের সবচেয়ে বড় সীমাবদ্ধতা কী — কেন আমাদের অ্যাকাউন্টিং ও পটেনশিয়াল মেথড (L44, L45)ও শিখতে হবে?
অ্যাগ্রিগেট মেথড শুধু মোট/গড় cost দেয় — এটি বলে না নির্দিষ্ট একটি অপারেশনের "amortized cost" ঠিক কত হওয়া উচিত ধরে নিলে হিসাব মিলবে, এবং যদি ডেটা স্ট্রাকচারে একাধিক ধরনের অপারেশন থাকে (যেমন push এবং pop উভয়ই) যাদের ভিন্ন ভিন্ন amortized cost প্রয়োজন, অ্যাগ্রিগেট মেথড দিয়ে আলাদা করা কঠিন হয়ে পড়ে। অ্যাকাউন্টিং মেথড (L44) প্রতিটি অপারেশনের একটি নির্দিষ্ট amortized "charge" ধার্য করে এবং accounting-এর মতো credit ট্র্যাক করে; পটেনশিয়াল মেথড (L45) আরও general এবং প্রায়ই জটিল ডেটা স্ট্রাকচারে (যেমন splay tree) একমাত্র ব্যবহারযোগ্য পদ্ধতি।
অনুশীলন
-
চিন্তা করুন: যদি ক্যাপাসিটি দ্বিগুণের বদলে তিনগুণ ($capacity \times 3$)
করে বাড়ানো হয়, তাহলে কি amortized cost এখনও $O(1)$ থাকবে? কেন?
হ্যাঁ, এখনও $O(1)$ থাকবে — যেকোনো ধ্রুবক গুণিতক ফ্যাক্টর $b > 1$ (যেমন $2$, $1.5$, বা $3$) দিয়ে ক্যাপাসিটি বাড়ালে কপি cost-গুলো এখনও একটি জ্যামিতিক সিরিজ তৈরি করে, যার যোগফল $O(n)$ থাকে (শুধু ধ্রুবক ফ্যাক্টরটা বদলায় — $b$ বড় হলে amortized cost কম হয় কিন্তু মেমরি বেশি নষ্ট হয়, এটাই ট্রেড-অফ)। মূল শর্ত হলো growth factor $1$-এর চেয়ে বড় একটি ধ্রুবক হতে হবে — সংযোজনমূলক (additive) বৃদ্ধি নয়।
-
পরীক্ষা করুন: উপরের কোড সেলে
new_capacity = 1 if self.capacity == 0 else self.capacity * 2লাইনটি বদলেself.capacity * 3করে (আরn = 2000রেখে) Run চেপে দেখুন মোট কপি/n এবং cost/n কীভাবে বদলায়।রিয়েলোকেশন কম ঘটবে (ক্যাপাসিটি দ্রুত বাড়ে বলে), কিন্তু প্রতিটি রিয়েলোকেশনে গড়ে বেশি এলিমেন্ট কপি করতে হবে (কারণ প্রতিবার আরও বেশি নতুন খালি স্লট তৈরি হয়)। মোট কপি/n এবং cost/n-এর প্রকৃত মান বদলাবে, কিন্তু উভয়ই এখনও একটি ছোট ধ্রুবকের মধ্যে বাউন্ডেড থাকবে ($n$ বড় হলেও বাড়বে না) — অর্থাৎ amortized $O(1)$ সিদ্ধান্তটি একই থাকে, শুধু ধ্রুবকটা বদলায়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পরবর্তী পাঠ — অ্যাকাউন্টিং মেথড L44 একই ডাইনামিক অ্যারে উদাহরণ, কিন্তু এবার প্রতিটি push-কে একটি নির্দিষ্ট "amortized charge" ধার্য করে এবং credit balance ট্র্যাক করে প্রমাণ করা হবে।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ অ্যাসিম্পটোটিক অ্যানালাইসিস, রিকারেন্স, ডিভাইড অ্যান্ড কনকার, গ্রিডি, DP, গ্রাফ অ্যালগরিদম, ব্যাকট্র্যাকিং, স্ট্রিং অ্যালগরিদম, অ্যামর্টাইজড অ্যানালাইসিস, NP-কমপ্লিটনেস ও অ্যাপ্রক্সিমেশন — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
- Data Structures & Algorithms কোর্স সহোদর কোর্স সেই কোর্সে ডাইনামিক অ্যারে/লিস্টের ব্যবহারিক ইমপ্লিমেন্টেশন দেখা যাবে — এই কোর্স তার amortized cost দাবিটির রিগোরাস প্রমাণে যায়।