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

অ্যাকাউন্টিং মেথড

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

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

  • অ্যাকাউন্টিং মেথডের মূল ধারণা — amortized charge, actual cost, এবং credit-এর মধ্যে সম্পর্ক
  • কেন charge per push $= 3$ ইউনিট বেছে নেওয়া হলো, এবং কীভাবে সেটি বীজগাণিতিকভাবে ন্যায্যতা পায়
  • credit balance সবসময় $\ge 0$ থাকে তার প্রমাণ — এবং একটি প্রকৃত কোড দিয়ে সেই দাবি যাচাই
  • একই ডাইনামিক অ্যারে, একই সংখ্যা — L43-এর অ্যাগ্রিগেট মেথড ও এই পাঠের অ্যাকাউন্টিং মেথড কীভাবে একই সিদ্ধান্তে পৌঁছায় তা সংযুক্ত করে দেখা

১ · অ্যাকাউন্টিং মেথডের ধারণা

L43-এ আমরা অ্যাগ্রিগেট মেথড দিয়ে দেখিয়েছি $n$-টি push-এর মোট cost $< 3n$, তাই amortized cost per push $O(1)$। অ্যাকাউন্টিং মেথড একই সিদ্ধান্তে পৌঁছায়, কিন্তু ভিন্নভাবে — প্রতিটি অপারেশনের জন্য একটি নির্দিষ্ট amortized chargeAmortized Chargeপ্রতিটি অপারেশনে "ধার্যকৃত" একটি কৃত্রিম, ধ্রুবক cost — বাস্তব cost-এর সমান হতে হবে না, শুধু এটা প্রমাণ করতে হবে যে সমস্ত অপারেশনের মোট charge সবসময় মোট প্রকৃত cost-এর চেয়ে বেশি বা সমান থাকে। নির্ধারণ করে। যদি কোনো অপারেশনের প্রকৃত cost তার charge-এর চেয়ে কম হয়, উদ্বৃত্ত অংশ credit হিসেবে জমা থাকে। যখন কোনো অপারেশনের প্রকৃত cost তার charge-এর চেয়ে বেশি হয় (যেমন রিয়েলোকেশন), জমা করা credit থেকে সেই ঘাটতি মেটানো হয়।

$$\text{credit balance after operation } i = \sum_{j=1}^{i} \left(\text{charge}_j - \text{actual\_cost}_j\right)$$

পুরো প্রমাণ দাঁড়িয়ে থাকে একটি শর্তের উপর: credit balance কখনও ঋণাত্মক হতে পারবে না — কারণ ঋণাত্মক credit মানে আমরা এমন cost মেটাচ্ছি যা এখনও charge করাই হয়নি, যা বৈধ নয়। যদি এই ইনভেরিয়েন্ট সবসময় বজায় থাকে, তাহলে মোট charge $\ge$ মোট actual cost — এবং যেহেতু charge একটি ধ্রুবক ($3$), তাই মোট actual cost $\le 3n = O(n)$, অর্থাৎ amortized cost per push $O(1)$।

২ · একই ডাইনামিক অ্যারে, charge $= 3$ কেন

L43-এর সেই একই ক্যাপাসিটি-ডাবলিং ডাইনামিক অ্যারে নেওয়া যাক। প্রতিটি push-কে charge করা হবে $3$ ইউনিট: $1$ ইউনিট আসল অ্যাসাইনমেন্টের জন্য ব্যয় হয়, বাকি $2$ ইউনিট credit হিসেবে জমা রাখা হয়। দাবি: যখনই অ্যারে পূর্ণ হয়ে রিয়েলোকেশন ঘটে (আকার $s$-এ, যেখানে $s$ বর্তমান ক্যাপাসিটির সমান), জমাকৃত credit ঠিক সেই মুহূর্তে $s$টি এলিমেন্ট কপি করার cost মেটাতে যথেষ্ট।

কেন? শেষ রিয়েলোকেশনের পর থেকে (যখন ক্যাপাসিটি $s/2$ থেকে $s$-এ দ্বিগুণ হয়েছিল) ঠিক $s/2$টি push হয়েছে (আকার $s/2$ থেকে $s$ পর্যন্ত পৌঁছাতে)। প্রতিটি push $2$ ইউনিট credit জমা করেছে, তাই মোট জমাকৃত credit $= 2 \times (s/2) = s$ — যা ঠিক পরবর্তী রিয়েলোকেশনের কপি-cost ($s$টি এলিমেন্ট)-এর সমান। credit ঠিক পুরোটাই খরচ হয়ে যায়, কখনও ঋণাত্মক হয় না:

$$\underbrace{2 \times \frac{s}{2}}_{\text{জমাকৃত credit}} = \underbrace{s}_{\text{রিয়েলোকেশন কপি cost}}$$

লক্ষ্য করুন এই বীজগণিতটাই L43-এর অ্যাগ্রিগেট মেথডের জ্যামিতিক সিরিজের যুক্তির সাথে সরাসরি সংযুক্ত — সেখানে মোট কপি cost $< 2n$ ছিল বলেই এখানে "$2$ ইউনিট credit প্রতি push" নির্বাচন করাটা ঠিক কাজ করে। যদি charge $3$-এর বদলে $2$ ধরা হতো (অর্থাৎ push প্রতি মাত্র $1$ ইউনিট credit), credit যথেষ্ট হতো না — নিচের কোড সেলে এটি সত্যিই ভেঙে পড়তে দেখা যাবে।

Python
class DynamicArray:
    """L43-এর হুবহু একই ক্যাপাসিটি-ডাবলিং ডাইনামিক অ্যারে -- আসল (actual) cost
    ফেরত দেয় প্রতিটি push()-এ, যাতে amortized charge-এর সাথে তুলনা করা যায়।"""

    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 run_with_charge(n, charge):
    """charge ইউনিট প্রতি push ধার্য করে, credit balance ট্র্যাক করে।
    যদি কোনো মুহূর্তে balance ঋণাত্মক হয়ে যায় -- AssertionError তোলে।"""
    da = DynamicArray()
    credit = 0
    min_credit = None
    for i in range(1, n + 1):
        actual_cost = da.push(i)
        credit += charge - actual_cost
        if min_credit is None or credit < min_credit:
            min_credit = credit
        assert credit >= 0, f"push #{i}-এ credit ঋণাত্মক হয়ে গেছে: {credit}"
    return credit, min_credit


n = 2000

final_credit, min_credit = run_with_charge(n, charge=3)
print(f"charge = 3 ইউনিট/push দিয়ে {n}টি push চালানো হলো")
print(f"শেষে জমাকৃত credit: {final_credit}")
print(f"পুরো সিকোয়েন্সে সর্বনিম্ন credit balance: {min_credit}")
print("--> কখনও ঋণাত্মক হয়নি, তাই charge = 3 বৈধ।\n")

print("এবার charge = 2 দিয়ে চেষ্টা করা যাক (L43-এর অ্যাগ্রিগেট প্রমাণ অনুযায়ী এটি যথেষ্ট নয়):")
try:
    run_with_charge(n, charge=2)
    print("charge = 2 ব্যর্থ হয়নি -- এটা প্রত্যাশিত ছিল না!")
except AssertionError as e:
    print(f"প্রত্যাশিতভাবেই ব্যর্থ: {e}")

    
$n = 2000$-এ charge $= 3$ দিয়ে রান করলে দেখা যায় credit balance কখনও ঋণাত্মক হয় না — সর্বনিম্ন মান থাকে $2$ (একদম শূন্যের কাছাকাছি, কিন্তু কখনও নিচে নামে না), এবং শেষে জমাকৃত credit থাকে $1953$ ইউনিট। এটি ঠিক সেই দাবিরই প্রকৃত যাচাই যা প্রমাণে বলা হয়েছিল। কিন্তু charge $= 2$ ধরলে push #৫-এই (প্রথম কয়েকটি রিয়েলোকেশনের ভেতরেই) credit ঋণাত্মক হয়ে যায় — প্রমাণ করে যে $3$ কোনো নির্বিচারে বাছাই করা সংখ্যা নয়, বরং L43-এর অ্যাগ্রিগেট বাউন্ড ($< 3n$)-এর সাথে বীজগাণিতিকভাবে সামঞ্জস্যপূর্ণ ন্যূনতম-কাছাকাছি মান।
মূল কথা · Key takeaway

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

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

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

প্র ০১ charge $= 4$ বা $5$ ধরলেও কি credit balance $\ge 0$ থাকবে?

হ্যাঁ — যেকোনো charge $\ge 3$ কাজ করবে, কারণ প্রতিটি push-এ তখন আরও বেশি credit জমা হবে, যা প্রয়োজনের চেয়ে বেশি সঞ্চয় তৈরি করবে (balance আরও বেশি ধনাত্মক থাকবে, কখনও ঋণাত্মক হবে না)। তবে amortized cost বাউন্ড হিসেবে charge যত ছোট রাখা যায় (এখানে $3$ হলো ন্যূনতম পূর্ণসংখ্যা যা কাজ করে) ততই বাউন্ডটা টাইট (নিখুঁত) হয় — বড় charge বৈধ কিন্তু কম তথ্যপূর্ণ, কারণ এটি প্রকৃত amortized cost-কে overestimate করে।

প্র ০২ credit balance যদি কোনো এক মুহূর্তে ঋণাত্মক হয়ে যেত, তার মানে ঠিক কী ভুল প্রমাণিত হতো?

এর মানে হতো আমরা এমন একটি অপারেশনের জন্য অর্থ পরিশোধ করছি যা এখনও charge করাই হয়নি — অর্থাৎ আমাদের বেছে নেওয়া charge মান ($2$, যেমন উপরের কোডে দেখানো হয়েছে) সেই ডেটা স্ট্রাকচারের প্রকৃত আচরণের জন্য যথেষ্ট নয়। এর মানে এই না যে ডেটা স্ট্রাকচারটির amortized cost $O(1)$ নয় — শুধু এই যে আমাদের বাছাই করা নির্দিষ্ট charge-টি ভুল (খুব কম) ছিল। সঠিক charge (এখানে $3$) খুঁজে বের করাই অ্যাকাউন্টিং মেথডের প্রধান কাজ।

প্র ০৩ অ্যাকাউন্টিং মেথড ও অ্যাগ্রিগেট মেথড (L43) কি সবসময় একই amortized cost বাউন্ড দেয়?

এই উদাহরণে হ্যাঁ ($3$), কারণ দুটো মেথডই একই অন্তর্নিহিত জ্যামিতিক সিরিজের যুক্তির উপর নির্ভর করে। কিন্তু সাধারণভাবে দুটো মেথড ভিন্ন (কখনও কম টাইট) বাউন্ড দিতে পারে, বিশেষ করে যদি ডেটা স্ট্রাকচারে একাধিক ধরনের অপারেশন থাকে (যেমন push এবং pop উভয়ই, ভিন্ন charge প্রয়োজন) — তখন charge নির্বাচন করা একটি সৃজনশীল পদক্ষেপ হয়ে ওঠে, এবং একটি ভুল (খুব কম) charge বাছাই করলে প্রমাণ ব্যর্থ হবে যদিও প্রকৃত amortized cost আসলে $O(1)$-ই থাকে সঠিক charge দিয়ে।

অনুশীলন

  1. চিন্তা করুন: উপরের কোডে charge=2 চেষ্টা করলে ব্যর্থ হয় push #৫-এ। এই push নাম্বারটা কি কাকতালীয়, নাকি ডিজাইনের সাথে সরাসরি সম্পর্কিত? (ইঙ্গিত: L43-এর রিয়েলোকেশন-ট্রিগার push নাম্বারগুলো মনে করুন — ১, ২, ৩, ৫, ৯, ১৭ ...)

    কাকতালীয় নয়। push #১ থেকে #৪ পর্যন্ত (charge $= 2$ ইউনিট প্রতি push হিসেবে) cumulative credit দাঁড়ায় যথাক্রমে $1, 1, 0, 1$ (প্রতিটি push-এ change $= 2 - \text{actual\_cost}$, আর push #১-৪-এর actual cost ছিল যথাক্রমে $1, 2, 3, 1$)। push #৫-এ অ্যারের আকার $4$ থেকে ক্যাপাসিটি পূর্ণ হয়ে রিয়েলোকেশন ঘটে — কপি cost $4$ + অ্যাসাইনমেন্ট $1$ = actual cost $5$। এই push-এর change হয় $2 - 5 = -3$, যা push #৪-এর পরের cumulative credit ($1$)-কে $1 - 3 = -2$-এ নামিয়ে দেয় — ঠিক এই push-এই প্রথমবার ঋণাত্মক হয়, কোডের আউটপুটের সাথে হুবহু মিলে যায়।

  2. পরীক্ষা করুন: উপরের কোডে run_with_charge(n, charge=3)-এর বদলে run_with_charge(200, charge=3) চালিয়ে (ছোট $n$) min_credit-এর মান কী আসে দেখুন, তারপর n=2000-এর সাথে তুলনা করুন — সর্বনিম্ন credit কি $n$-এর সাথে বদলায়?

    না — সর্বনিম্ন credit balance (এই ডিজাইনে $2$) $n$-নির্বিশেষে একই থাকে, কারণ এটি একটি স্থানীয় (local) ঘটনা — প্রতিটি রিয়েলোকেশনের ঠিক পরপরই ন্যূনতম credit ঘটে, এবং সেই প্যাটার্নটি প্রতিটি রিয়েলোকেশনেই পুনরাবৃত্তি হয় (স্কেল-ইনভেরিয়েন্ট)। এটাই অ্যাকাউন্টিং মেথডের একটি সুন্দর বৈশিষ্ট্য — ইনভেরিয়েন্টটি $n$ যত বড়ই হোক না কেন সমানভাবে ধরে রাখে।

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

  • পরবর্তী পাঠ — পটেনশিয়াল মেথড L45 একই ডাইনামিক অ্যারে, তৃতীয়বার — এবার একটি পটেনশিয়াল ফাংশন $\Phi$ দিয়ে amortized cost প্রমাণ করা হবে, যা আরও জেনারেল ও শক্তিশালী একটি টুল।
  • আগের পাঠ — অ্যাগ্রিগেট মেথড L43 এই একই ডাইনামিক অ্যারে উদাহরণের প্রথম প্রমাণ-কৌশল — মোট cost সরাসরি গণনা করে।
  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ অ্যাসিম্পটোটিক অ্যানালাইসিস, রিকারেন্স, ডিভাইড অ্যান্ড কনকার, গ্রিডি, DP, গ্রাফ অ্যালগরিদম, ব্যাকট্র্যাকিং, স্ট্রিং অ্যালগরিদম, অ্যামর্টাইজড অ্যানালাইসিস, NP-কমপ্লিটনেস ও অ্যাপ্রক্সিমেশন — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
আগের পাঠ
অ্যামর্টাইজড অ্যানালাইসিস — অ্যাগ্রিগেট মেথড