পাঠ ৪৫ · ৫৭-এর মধ্যে · মডিউল ১০
Home / Courses / Design and Analysis of Algorithms / পটেনশিয়াল মেথড

পটেনশিয়াল মেথড

The potential method
১১ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • পটেনশিয়াল ফাংশনের ধারণা এবং $\text{amortized cost} = \text{actual cost} + \Delta\Phi$ সূত্রটি কোথা থেকে আসে
  • কেন $\Phi = 2 \cdot \text{size} - \text{capacity}$ একটি বৈধ পটেনশিয়াল ফাংশন (সবসময় $\ge 0$, খালি অবস্থায় $0$)
  • বীজগাণিতিকভাবে দেখানো — সাধারণ push-এ (রিয়েলোকেশন ছাড়া) এবং রিয়েলোকেশন-push-এ amortized cost উভয়ক্ষেত্রেই বাউন্ডেড থাকে
  • L43 (অ্যাগ্রিগেট), L44 (অ্যাকাউন্টিং), এবং L45 (পটেনশিয়াল) — তিনটি ভিন্ন প্রমাণ কীভাবে একই ডেটা স্ট্রাকচার নিয়ে একই সিদ্ধান্তে পৌঁছায় তা সংক্ষিপ্ত করা

১ · পটেনশিয়াল ফাংশন — সঞ্চিত "শক্তি" পরিমাপ

পটেনশিয়াল মেথড পদার্থবিজ্ঞানের পটেনশিয়াল এনার্জির ধারণা থেকে অনুপ্রাণিত। ডেটা স্ট্রাকচারের প্রতিটি অবস্থার (state) জন্য একটি সংখ্যা $\Phi(\text{state})$ সংজ্ঞায়িত করা হয় — এটি বোঝায় সেই মুহূর্তে ডেটা স্ট্রাকচারে কতটা "সঞ্চিত potential" আছে, যা ভবিষ্যতের একটি ব্যয়বহুল অপারেশনের cost "প্রি-পেইড" (আগে থেকে পরিশোধ করা) হিসেবে কাজ করতে পারে। $i$-তম অপারেশনের amortized cost সংজ্ঞায়িত হয়:

$$\hat{c}_i = c_i + \Phi(D_i) - \Phi(D_{i-1}) = c_i + \Delta\Phi_i$$

এখানে $c_i$ হলো actual cost, $D_{i-1}$ ও $D_i$ হলো অপারেশনের আগে ও পরের ডেটা স্ট্রাকচারের অবস্থা। $n$-টি অপারেশনের মোট amortized cost যোগ করলে (টেলিস্কোপিং সাম — মাঝের সব $\Phi$ মান বাতিল হয়ে যায়):

$$\sum_{i=1}^{n} \hat{c}_i = \sum_{i=1}^{n} c_i + \Phi(D_n) - \Phi(D_0)$$

যদি $\Phi(D_0) = 0$ (শুরুতে) এবং $\Phi(D_i) \ge 0$ সবসময় (কোনো অবস্থাতেই ঋণাত্মক নয়), তাহলে $\sum c_i \le \sum \hat{c}_i$ — অর্থাৎ, amortized cost-এর যোগফল প্রকৃত cost-এর যোগফলের একটি বৈধ upper bound। এটাই মূল কৌশল: প্রতিটি $\hat{c}_i$ ধ্রুবক ($O(1)$) প্রমাণ করতে পারলেই মোট প্রকৃত cost $O(n)$।

২ · একই ডাইনামিক অ্যারে-তে $\Phi$ নির্বাচন

L43/L44-এর সেই একই ক্যাপাসিটি-ডাবলিং ডাইনামিক অ্যারের জন্য নেওয়া যাক:

$$\Phi(\text{size}, \text{capacity}) = 2 \cdot \text{size} - \text{capacity}$$

এটি একটি বৈধ পটেনশিয়াল ফাংশন হওয়ার জন্য দুটো শর্ত পূরণ করতে হবে: (ক) ফাঁকা অ্যারেতে ($\text{size}=0, \text{capacity}=0$) $\Phi = 0$ — সত্যি, কারণ $2 \times 0 - 0 = 0$। (খ) সবসময় $\Phi \ge 0$ — এটি সত্য কারণ এই ডিজাইনে ক্যাপাসিটি রিয়েলোকেশনের ঠিক পরপরই $\text{capacity} = 2 \times \text{size}_{\text{old}}$ এবং নতুন $\text{size} = \text{size}_{\text{old}} + 1$, তাই $\Phi = 2(\text{size}_{\text{old}}+1) - 2\,\text{size}_{\text{old}} = 2 > 0$; আর দুটো রিয়েলোকেশনের মাঝামাঝি সময়ে $\text{size}$ ক্যাপাসিটির অর্ধেকের বেশি থাকে (কারণ শেষ রিয়েলোকেশনে ঠিক অর্ধেক ভরাট অবস্থায় ছিল), তাই $2\,\text{size} \ge \text{capacity}$।

এবার দুই ধরনের push-এ amortized cost হিসাব করা যাক:

সাধারণ push (রিয়েলোকেশন ছাড়া): size $s \to s+1$, capacity $c$ অপরিবর্তিত। actual cost $c_i = 1$।

$$\Delta\Phi = \big(2(s+1) - c\big) - (2s - c) = 2 \quad\Rightarrow\quad \hat{c}_i = 1 + 2 = 3$$

রিয়েলোকেশন push: size $s = c$ (পূর্ণ) $\to$ নতুন capacity $c' = 2c$, নতুন size $s+1$। actual cost $c_i = s + 1$ (কপি $s$টি + অ্যাসাইনমেন্ট $1$)।

$$\Delta\Phi = \big(2(s+1) - 2c\big) - (2s - c) = 2 - c = 2 - s \quad\Rightarrow\quad \hat{c}_i = (s+1) + (2-s) = 3$$

দুই ক্ষেত্রেই amortized cost ঠিক $3$! (প্রথম push-টি একটি বিশেষ প্রান্তিক ঘটনা — $\text{capacity}=0$ থেকে $1$-এ যাওয়ার সময় সূত্রে সামান্য পার্থক্য আসে, amortized cost হয় $2$, তাও $3$-এর মধ্যেই বাউন্ডেড)। এটাই পটেনশিয়াল মেথডের শক্তি — এটি প্রতিটি push-এর amortized cost একটি নির্দিষ্ট সূত্র দিয়ে বের করে দেয়, L44-এর মতো charge "অনুমান করে বসিয়ে" পরীক্ষা করতে হয় না।

Python
class DynamicArray:
    """L43/L44-এর হুবহু একই ক্যাপাসিটি-ডাবলিং ডাইনামিক অ্যারে।"""

    def __init__(self):
        self.capacity = 0
        self.size = 0
        self.arr = []

    def push(self, value):
        cost = 0
        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


def phi(size, capacity):
    """পটেনশিয়াল ফাংশন: Phi = 2*size - capacity"""
    return 2 * size - capacity


n = 2000
da = DynamicArray()
amortized_costs = []
phi_prev = phi(da.size, da.capacity)  # শুরুতে size=0, capacity=0 -> Phi = 0

min_phi_seen = phi_prev

for i in range(n):
    actual_cost = da.push(i)
    phi_new = phi(da.size, da.capacity)
    delta_phi = phi_new - phi_prev
    amortized = actual_cost + delta_phi
    amortized_costs.append(amortized)

    assert phi_new >= 0, f"push #{i+1}-এ Phi ঋণাত্মক হয়ে গেছে: {phi_new}"
    assert amortized <= 3, f"push #{i+1}-এ amortized cost 3-এর বেশি: {amortized}"

    min_phi_seen = min(min_phi_seen, phi_new)
    phi_prev = phi_new

print(f"{n}টি push-এর উপর পটেনশিয়াল মেথড যাচাই")
print(f"amortized cost-এর সর্বনিম্ন মান: {min(amortized_costs)}")
print(f"amortized cost-এর সর্বোচ্চ মান: {max(amortized_costs)}")
print(f"সব amortized cost কি <= 3? {all(c <= 3 for c in amortized_costs)}")
print(f"পুরো রানে সর্বনিম্ন Phi মান (কখনও ঋণাত্মক হয়নি): {min_phi_seen}")
print(f"গড় amortized cost: {sum(amortized_costs) / n:.4f}")
print(f"\nপ্রথম ১২টি push-এর amortized cost: {amortized_costs[:12]}")

    
কোড রান করলে দেখা যায় $2000$টি push-এর প্রতিটির amortized cost $[2, 3]$-এর মধ্যেই থাকে (প্রথম push ছাড়া বাকি সবই ঠিক $3$) — উপরের বীজগাণিতিক ডেরিভেশনের সাথে নিখুঁতভাবে মিলে যায়। গড় amortized cost দাঁড়ায় $\approx 3.0$, যা L43-এ পরিমাপ করা প্রকৃত গড় cost ($\approx 2.02$)-এর চেয়ে সামান্য বেশি — এটাই প্রত্যাশিত, কারণ পটেনশিয়াল মেথড amortized cost-কে একটি upper bound হিসেবে দেয় (প্রকৃত মোট cost $\le$ amortized মোট cost, L43/L44-এর মতোই)। $\Phi$ কখনও ঋণাত্মক হয়নি — এটাই নিশ্চিত করেছে টেলিস্কোপিং যুক্তিটি বৈধ ছিল।
তিনটি প্রমাণ, একই সিদ্ধান্ত — M10-এর সারাংশ

L43, L44, এবং L45 — তিনটি লেসনই একই ক্যাপাসিটি-ডাবলিং ডাইনামিক অ্যারে এবং একই ২,০০০-push সিকোয়েন্স নিয়ে কাজ করেছে, এবং তিনটিই একই সিদ্ধান্তে পৌঁছেছে: push-এর amortized cost $O(1)$, নির্দিষ্টভাবে $\le 3$। অ্যাগ্রিগেট মেথড (L43) সরাসরি মোট cost গণনা করে সবচেয়ে সহজ প্রমাণ দেয়, কিন্তু শুধু গড় cost বলে। অ্যাকাউন্টিং মেথড (L44) প্রতিটি অপারেশনের জন্য একটি নির্দিষ্ট charge বসিয়ে credit balance দিয়ে প্রমাণ করে। পটেনশিয়াল মেথড (L45) সবচেয়ে general — এটি একটি ফাংশন থেকে প্রতিটি অপারেশনের amortized cost ডেরিভ করে, কোনো charge "অনুমান" করার দরকার হয় না, এবং জটিল ডেটা স্ট্রাকচারে (যেমন splay tree, Fibonacci heap) এটিই একমাত্র ব্যবহারযোগ্য কৌশল। পরবর্তী পাঠে (L46) আমরা একই প্যাটার্ন একটি সম্পূর্ণ ভিন্ন ডেটা স্ট্রাকচারে — ইউনিয়ন-ফাইন্ডে — প্রয়োগ করব।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ $\Phi = \text{size}$ (শুধু আকার) বেছে নিলে কি এটি একটি বৈধ পটেনশিয়াল ফাংশন হতো এই ডাইনামিক অ্যারের জন্য?

$\Phi = \text{size}$ শর্ত (ক) পূরণ করে ($\text{size}=0$ হলে $\Phi=0$) এবং শর্ত (খ)-ও পূরণ করে (সবসময় $\ge 0$) — কিন্তু এটি রিয়েলোকেশনের cost "প্রি-পেইড" করতে পারে না। রিয়েলোকেশনে $\Delta\Phi = 1$ (size $s \to s+1$), কিন্তু actual cost $= s+1$, তাই amortized cost $= (s+1)+1 = s+2$ — যা $n$-এর সাথে বাড়ে, $O(1)$ নয়! এই উদাহরণ দেখায় পটেনশিয়াল ফাংশনের পছন্দটাই সবকিছু — শুধু "শর্ত (ক) ও (খ) পূরণ করা" যথেষ্ট নয়, এটাকে অবশ্যই ব্যয়বহুল অপারেশনের ঠিক আগে "যথেষ্ট বড়" হয়ে থাকতে হবে যাতে সেই ব্যয় শোষণ করতে পারে।

প্র ০২ পটেনশিয়াল মেথডের $\Delta\Phi$ এবং অ্যাকাউন্টিং মেথডের "credit" ধারণার মধ্যে সম্পর্ক কী?

এরা মূলত একই ধারণার দুটো ভিন্ন প্রকাশ। $\Phi(D_i)$-কে "cumulative credit balance after operation $i$" হিসেবে ভাবা যায় — L44-এর কোডে ট্র্যাক করা credit ভেরিয়েবলটাই বস্তুত এই ডাইনামিক অ্যারের জন্য $\Phi$-এর একটি (রৈখিক রূপান্তরিত) সংস্করণ। পার্থক্য শুধু উপস্থাপনায়: অ্যাকাউন্টিং মেথড এটিকে "টাকা জমানো"-র রূপকে ভাবে, পটেনশিয়াল মেথড এটিকে একটি ফাংশন $\Phi(\text{state})$ হিসেবে ভাবে যা শুধু বর্তমান অবস্থার উপর নির্ভর করে (ইতিহাসের উপর নয়) — এই "শুধু বর্তমান অবস্থা" বৈশিষ্ট্যই পটেনশিয়াল মেথডকে জটিল ডেটা স্ট্রাকচারে বেশি নমনীয় করে তোলে।

প্র ০৩ উপরের কোডে assert amortized <= 3 লাইনটি যদি রিমুভ না করে বরং assert amortized <= 2 করা হতো, তাহলে কী ঘটত?

এটি ব্যর্থ হতো — কারণ বীজগণিতে দেখানো হয়েছে সাধারণ (নন-রিয়েলোকেশন) push-এর amortized cost ঠিক $3$ (প্রথম push বাদে)। তাই দ্বিতীয় push থেকেই assert amortized <= 2 ব্যর্থ হবে। এটি একটি গুরুত্বপূর্ণ শিক্ষা: পটেনশিয়াল মেথডের ডেরিভেশন থেকে বের হওয়া বাউন্ড ($3$) নির্বিচারে নয় — এটি সঠিক এবং টাইট (আসলে exact, কারণ প্রায় সব push-এর amortized cost হুবহু $3$)।

অনুশীলন

  1. চিন্তা করুন: $\Phi = 3 \cdot \text{size} - \text{capacity}$ (গুণাঙ্ক $2$-এর বদলে $3$) ব্যবহার করলে কী হতো — এটি কি এখনও একটি বৈধ পটেনশিয়াল ফাংশন হতো, এবং amortized cost বাউন্ড কীভাবে বদলাত?

    হ্যাঁ, এটিও বৈধ থাকত (খালি অবস্থায় $0$, এবং একই যুক্তিতে সবসময় $\ge 0$) — কিন্তু amortized cost ভিন্ন হতো। সাধারণ push-এ $\Delta\Phi = 3$, actual cost $1$, তাই amortized cost $= 4$। রিয়েলোকেশন push-এ হিসাব করলে amortized cost $= 2$ আসে। উভয়ই এখনও $O(1)$-এর মধ্যে বাউন্ডেড, কিন্তু সর্বোচ্চ মান ($4$) আগের চেয়ে (গুণাঙ্ক $2$-এ $3$ ছিল) বেশি — অর্থাৎ $\Phi$-এর সঠিক গুণাঙ্ক নির্বাচন প্রমাণের টাইটনেসকে প্রভাবিত করে, যদিও চূড়ান্ত $O(1)$ সিদ্ধান্ত বদলায় না।

  2. পরীক্ষা করুন: উপরের কোডে phi ফাংশনটি return 3 * size - capacity করে বদলে Run চাপুন, এবং assert amortized <= 3 লাইনটি সাময়িকভাবে <= 4 করে দেখুন আপনার আগের অনুমান মিলছে কিনা।

    আউটপুটে amortized cost-এর সর্বোচ্চ মান দেখাবে $4$, এবং সর্বনিম্ন মান হবে $2$ (রিয়েলোকেশন push-গুলোতে) — ঠিক যেমন আগের প্রশ্নে বীজগণিতে ডেরিভ করা হয়েছিল। এটি নিশ্চিত করে $\Phi$-এর সঠিক গুণাঙ্ক বাছাই প্রমাণকে "টাইট" করে (প্রতিটি push-এর amortized cost একই, $3$) বনাম "লুজ" করে (push অনুযায়ী $2$ থেকে $4$ পর্যন্ত ওঠানামা করে) — উভয়ই বৈধ $O(1)$ বাউন্ড, কিন্তু একটি বেশি তথ্যপূর্ণ।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
অ্যাকাউন্টিং মেথড