পাঠ ৫৪ · ৫৭-এর মধ্যে · মডিউল ১২
Home / Courses / Design and Analysis of Algorithms / র‍্যান্ডোমাইজড কুইকসর্ট

র‍্যান্ডোমাইজড কুইকসর্ট ও এক্সপেক্টেড টাইম অ্যানালাইসিস

Randomized quicksort & expected time analysis
১৪ মিনিট পড়া কঠিন · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

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

  • র‍্যান্ডোমাইজড কুইকসর্টের সম্পূর্ণ বাস্তবায়ন — র‍্যান্ডম পিভট নির্বাচনসহ
  • ইনডিকেটর-র‍্যান্ডম-ভ্যারিয়েবল কৌশল দিয়ে $E[\text{comparisons}] = O(n \log n)$-এর সম্পূর্ণ প্রমাণ
  • একটি genuine, কম্পিউট করা পেঅফ — L16-এর ঠিক একই ওয়ার্স্ট-কেস ইনপুটে randomized সংস্করণ কীভাবে $n^2$ এড়িয়ে যায়, বহু seed-এর গড়ে
  • কেন "এক্সপেক্টেড" দাবি একটি একক রান দিয়ে যাচাই করা যায় না — শুধু বহু-ট্রায়াল গড়ের মাধ্যমেই

১ · রিক্যাপ — L16-এর সমস্যা

L16-এ দেখা হয়েছিল একটি ফিক্সড-পিভট কুইকসর্ট (সবসময় শেষ এলিমেন্টকে পিভট নেওয়া) আগে-থেকে-সাজানো ইনপুটে কেন সবচেয়ে খারাপ পারফর্ম করে: পিভট সবসময় সাব-অ্যারের সবচেয়ে বড় এলিমেন্ট হওয়ায়, প্রতিটি পার্টিশন $(n-1, 0)$ আকারে ভাগ হয় — একটি দিকে প্রায় পুরো অ্যারে, অন্য দিকে কিছুই না। ফলে মোট তুলনা $\approx n(n-1)/2 = \Theta(n^2)$।

মূল সমস্যাটা পিভট নির্বাচনের নিয়মে, কুইকসর্টের মূল ধারণায় নয়। যদি পিভট নির্বাচনটাই র‍্যান্ডম করে দেওয়া হয়, তাহলে কোনো নির্দিষ্ট ইনপুট (আগে-থেকে-সাজানো সহ) অ্যালগরিদমকে সবসময় খারাপ বিভাজনে বাধ্য করতে পারে না — কারণ পিভট নিজেই প্রতিবার ভিন্ন হতে পারে।

২ · র‍্যান্ডোমাইজড কুইকসর্ট

একমাত্র পরিবর্তন: পিভট ইনডেক্স বেছে নেওয়া হয় rng.randint(lo, hi) দিয়ে, ফিক্সড ইনডেক্সের বদলে।

Python
import random

def randomized_quicksort(rng, arr):
    arr = list(arr)
    def qs(lo, hi):
        if lo >= hi:
            return
        pivot_idx = rng.randint(lo, hi)          # র‍্যান্ডম পিভট -- একমাত্র পরিবর্তন L16-এর নেইভ সংস্করণ থেকে
        arr[pivot_idx], arr[hi] = arr[hi], arr[pivot_idx]
        pivot = arr[hi]
        store = lo
        for i in range(lo, hi):
            if arr[i] < pivot:
                arr[i], arr[store] = arr[store], arr[i]
                store += 1
        arr[store], arr[hi] = arr[hi], arr[store]
        qs(lo, store - 1)
        qs(store + 1, hi)
    qs(0, len(arr) - 1)
    return arr

# প্রথমেই সঠিকতা যাচাই -- এটি এখনো একটি লাস ভেগাস অ্যালগরিদম (L53), তাই আউটপুট সবসময় সঠিক হতে হবে
master_rng = random.Random(2026)
all_correct = True
for trial in range(25):
    n = master_rng.randint(1, 60)
    test_arr = [master_rng.randint(-50, 50) for _ in range(n)]
    seed = master_rng.randint(0, 10**6)
    sort_rng = random.Random(seed)
    result = randomized_quicksort(sort_rng, test_arr)
    if result != sorted(test_arr):
        all_correct = False
        print(f"trial {trial}: MISMATCH!")

print(f"২৫টি র‍্যান্ডম টেস্টে (বিভিন্ন n, বিভিন্ন seed) randomized quicksort সবসময় সঠিক ফলাফল দিয়েছে: {all_correct}")
assert all_correct

    

৩ · প্রমাণ — এক্সপেক্টেড তুলনার সংখ্যা $O(n \log n)$

সাজানো ক্রমে এলিমেন্টগুলোকে র‍্যাংক $1, 2, \dots, n$ দিয়ে চিহ্নিত করি (অর্থাৎ $z_i$ হলো $i$-তম ক্ষুদ্রতম এলিমেন্ট)। ধরি $X_{ij}$ একটি ইনডিকেটর ভ্যারিয়েবল: অ্যালগরিদম চলাকালীন কখনো $z_i$ ও $z_j$ ($i < j$) সরাসরি তুলনা করা হলে $X_{ij}=1$, নাহলে $0$। তাহলে মোট তুলনার সংখ্যা:

$$C(n) = \sum_{i=1}^{n-1}\sum_{j=i+1}^{n} X_{ij}$$

মূল পর্যবেক্ষণ: $z_i$ ও $z_j$ কখনো তুলনা হয় শুধুমাত্র যদি $z_i$ থেকে $z_j$ পর্যন্ত ($j-i+1$টি এলিমেন্ট) সাব-অ্যারের মধ্যে প্রথম যে এলিমেন্টটি পিভট হিসেবে বাছা হয় তা $z_i$ অথবা $z_j$ নিজেই হয় (এই রেঞ্জের মাঝের কোনো এলিমেন্ট $z_i$ ও $z_j$-কে একে অপরের থেকে আলাদা করে ভিন্ন সাব-প্রবলেমে পাঠিয়ে দেয়, ফলে তারা আর কখনো তুলনা হয় না)। যেহেতু পিভট প্রতিবার সেই মুহূর্তের সাব-অ্যারে থেকে সমান সম্ভাবনায় (uniformly) বাছা হয়, এই $j-i+1$টি এলিমেন্টের যেকোনো একটি সমান সম্ভাবনায় প্রথম বাছা হতে পারে — তাই:

$$\Pr[X_{ij}=1] = \frac{2}{j-i+1}$$

এক্সপেক্টেশনের লিনিয়ারিটি (discrete math-এ পরিচিত একটি মৌলিক ফলাফল) ব্যবহার করে:

$$E[C(n)] = \sum_{i=1}^{n-1}\sum_{j=i+1}^{n} \frac{2}{j-i+1}$$

এই দ্বৈত-সমষ্টিটি harmonic number ($H_n = 1+\tfrac12+\dots+\tfrac1n \approx \ln n$) ব্যবহার করে সরলীকরণ করলে একটি সুপরিচিত বদ্ধ-রূপ (closed form) পাওয়া যায় (CLRS-এর মতো ক্লাসিক টেক্সটে সম্পূর্ণ বীজগণিতসহ দেখানো আছে):

$$E[C(n)] = 2(n+1)H_n - 4n = \Theta(n\log n)$$

লক্ষ্য করুন — এই পুরো যুক্তিতে ইনপুট অ্যারে কীভাবে সাজানো ছিল তার কোনো উল্লেখ নেই। $\Pr[X_{ij}=1] = 2/(j-i+1)$ শুধু র‍্যাংকের ($i, j$) উপর নির্ভর করে, ইনপুটের প্রাথমিক ক্রমের (সাজানো, উল্টো-সাজানো, র‍্যান্ডম — যাই হোক না কেন) উপর না। এটাই র‍্যান্ডোমাইজড কুইকসর্টের মূল শক্তি: এক্সপেক্টেড রানটাইম $\Theta(n \log n)$ সব ইনপুটে সমান, L16-এর ফিক্সড-পিভট সংস্করণের মতো কোনো "খারাপ ইনপুট" নেই।

৪ · সরাসরি পেঅফ — L16-এর ঠিক সেই adversarial ইনপুটে

তত্ত্ব যথেষ্ট নয় এই কোর্সে — চলুন সত্যিই যাচাই করা যাক। নিচে ঠিক একই আগে-থেকে-সাজানো ইনপুট ব্যবহার করা হচ্ছে যা L16-এ ফিক্সড-পিভট সংস্করণকে $\Theta(n^2)$-এ ফেলেছিল। প্রথমে সেই ফিক্সড-পিভট সংস্করণ (comparison-counting সহ) আবার চালিয়ে বেসলাইন সংখ্যাগুলো দেখানো হচ্ছে, তারপর সেই একই ইনপুটে randomized সংস্করণ ৪০টি ভিন্ন seed-এ চালিয়ে গড় তুলনার সংখ্যা গণনা করা হচ্ছে:

Python
import random
import math

def naive_quicksort_count(arr):
    # L16-এর ফিক্সড-পিভট সংস্করণ (সবসময় শেষ এলিমেন্ট) -- comparison counter সহ
    arr = list(arr)
    comparisons = [0]
    def qs(lo, hi):
        if lo >= hi:
            return
        pivot = arr[hi]
        store = lo
        for i in range(lo, hi):
            comparisons[0] += 1
            if arr[i] < pivot:
                arr[i], arr[store] = arr[store], arr[i]
                store += 1
        arr[store], arr[hi] = arr[hi], arr[store]
        qs(lo, store - 1)
        qs(store + 1, hi)
    qs(0, len(arr) - 1)
    return comparisons[0]

def randomized_quicksort_count(rng, arr):
    # ঠিক একই লজিক, শুধু পিভট এখন র‍্যান্ডম -- comparison counter সহ
    arr = list(arr)
    comparisons = [0]
    def qs(lo, hi):
        if lo >= hi:
            return
        pivot_idx = rng.randint(lo, hi)
        arr[pivot_idx], arr[hi] = arr[hi], arr[pivot_idx]
        pivot = arr[hi]
        store = lo
        for i in range(lo, hi):
            comparisons[0] += 1
            if arr[i] < pivot:
                arr[i], arr[store] = arr[store], arr[i]
                store += 1
        arr[store], arr[hi] = arr[hi], arr[store]
        qs(lo, store - 1)
        qs(store + 1, hi)
    qs(0, len(arr) - 1)
    return comparisons[0]

sizes = [50, 100, 200, 400]
num_seeds = 40

print(f"{'n':>5} | {'নেইভ (আগে-সাজানো, L16)':>24} | {'র‍্যান্ডোমাইজড গড়':>18} | {'গড়/(n·log2 n)':>15}")
naive_counts = []
rand_avgs = []
rand_samples_by_n = {}
for n in sizes:
    sorted_arr = list(range(n))     # L16-এর ঠিক সেই adversarial ইনপুট -- ইতিমধ্যে সম্পূর্ণ সাজানো

    naive_c = naive_quicksort_count(sorted_arr)
    naive_counts.append(naive_c)

    samples = []
    for seed in range(num_seeds):
        rng = random.Random(seed)   # প্রতিটি ট্রায়ালের জন্য একটি ভিন্ন, কিন্তু নির্দিষ্ট (reproducible) seed
        samples.append(randomized_quicksort_count(rng, sorted_arr))
    rand_samples_by_n[n] = samples
    avg = sum(samples) / len(samples)
    rand_avgs.append(avg)

    ratio = avg / (n * math.log2(n))
    print(f"{n:>5} | {naive_c:>24} | {avg:>18.1f} | {ratio:>15.3f}")

print("\nn দ্বিগুণ হলে বৃদ্ধির অনুপাত (nাইভ ~4x হওয়া উচিত -- Theta(n^2); randomized ~2x-এর কাছাকাছি -- Theta(n log n)):")
for i in range(1, len(sizes)):
    n_prev, n_cur = sizes[i - 1], sizes[i]
    print(f"n={n_prev}->{n_cur}: নেইভ {naive_counts[i]/naive_counts[i-1]:.2f}x, "
          f"র‍্যান্ডোমাইজড গড় {rand_avgs[i]/rand_avgs[i-1]:.2f}x")

biggest_n = sizes[-1]
biggest_samples = rand_samples_by_n[biggest_n]
print(f"\nn={biggest_n}-এ {num_seeds}টি ভিন্ন seed-এর মধ্যে তুলনার সংখ্যার তারতম্য:")
print(f"সর্বনিম্ন: {min(biggest_samples)}, সর্বোচ্চ: {max(biggest_samples)}, গড়: {sum(biggest_samples)/len(biggest_samples):.1f}")
print(f"(তুলনা করুন নেইভের নিশ্চিত {naive_counts[-1]} তুলনার সাথে -- randomized-এর যেকোনো একক ট্রায়ালও অনেক কম)")

assert rand_avgs[-1] < naive_counts[-1] / 5, "র‍্যান্ডোমাইজড গড় প্রত্যাশার চেয়ে অনেক বেশি বড়!"

    
টেবিলের শেষ কলাম (গড়/$(n\log_2 n)$) প্রতিটি $n$-এ প্রায় একই মানে স্থির থাকে (একটি ধ্রুবকের কাছাকাছি) — ঠিক যেমন $\Theta(n\log n)$ প্রেডিক্ট করে। বিপরীতে নেইভ সংস্করণের সংখ্যা প্রতিটি $n$ দ্বিগুণ হলে প্রায় চারগুণ হয় (কারণ $n(n-1)/2$ ঠিক এই আগে-সাজানো ইনপুটে প্রতিবার নিশ্চিতভাবে ঘটে — এটি কোনো র‍্যান্ডম প্রক্রিয়া নয়), অথচ randomized সংস্করণের গড় বাড়ে অনেক ধীরে, $n\log n$-এর স্বাক্ষর অনুযায়ী। $n=400$-এ সর্বনিম্ন-সর্বোচ্চ ব্যবধান দেখায় ব্যক্তিগত ট্রায়ালগুলো তারতম্য করে (কোনো একটি নির্দিষ্ট seed হয়তো খারাপ পিভট-ক্রম পেতে পারে) — কিন্তু এমনকি এই সর্বোচ্চটাও নেইভের নিশ্চিত সংখ্যার তুলনায় অনেক ছোট, আর গড়-টাই হলো "এক্সপেক্টেড টাইম" দাবির প্রকৃত পরিমাপ।
মূল কথা · Key takeaway

এটাই সম্পূর্ণ বৃত্ত সম্পন্ন করে: L16 দেখিয়েছিল একটি ফিক্সড পিভট আগে-থেকে-সাজানো ইনপুটে নিশ্চিতভাবে $\Theta(n^2)$-এ পড়ে। এই লেসনে দেখানো হলো — শুধু পিভট নির্বাচনকে র‍্যান্ডম করেই, ঠিক সেই একই ইনপুটে, এক্সপেক্টেড আচরণ $\Theta(n\log n)$-এ ফিরে আসে, কারণ এখন আর কোনো একক ইনপুট অ্যালগরিদমকে বারবার একই খারাপ সিদ্ধান্তে বাধ্য করতে পারে না। এটি র‍্যান্ডোমাইজেশনের ব্যবহারিক শক্তির সবচেয়ে ক্লাসিক উদাহরণ — এবং L53-এর "লাস ভেগাস" শ্রেণির একটি সম্পূর্ণ, সততার সাথে যাচাই করা কেস স্টাডি।

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

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

প্র ০১ যদি একটি নির্দিষ্ট, দুর্ভাগ্যজনক seed-এ randomized quicksort বারবার খারাপ পিভট বেছে নেয়, তাহলে কি সেই নির্দিষ্ট রানে $\Theta(n^2)$-এর কাছাকাছি সময় লাগা সম্ভব?

হ্যাঁ, তাত্ত্বিকভাবে সম্ভব — প্রতিটি ধাপে সবচেয়ে খারাপ পিভট বেছে নেওয়ার একটি ছোট (কিন্তু শূন্য নয়) সম্ভাবনা সবসময় থাকে। এটাই কেন র‍্যান্ডোমাইজড কুইকসর্ট একটি লাস ভেগাস অ্যালগরিদম, একটি গ্যারান্টিড-ফাস্ট অ্যালগরিদম নয় — ওয়ার্স্ট-কেস রানটাইম এখনো $O(n^2)$, শুধু তার সম্ভাবনা এত কম (exponentially small, $n$ বাড়ার সাথে সাথে দ্রুত কমে) যে ব্যবহারিকভাবে এটি কখনো ঘটে না। উপরের কোডে $n=400$-এ ৪০টি seed-এর মধ্যে সর্বোচ্চটাও গড়ের চেয়ে সামান্যই বেশি ছিল, কোনো কাছাকাছি $n^2$ মান আসেনি — এটাই এক্সপেক্টেড-কেস বিশ্লেষণের ব্যবহারিক নির্ভরযোগ্যতা।

প্র ০২ প্রমাণে $\Pr[X_{ij}=1] = 2/(j-i+1)$ বলা হয়েছে। $i$ ও $j$ পাশাপাশি র‍্যাংকের হলে ($j=i+1$) এই সম্ভাবনা কত হবে, এবং এর অর্থ কী?

$j=i+1$ হলে $j-i+1=2$, তাই $\Pr[X_{ij}=1] = 2/2 = 1$ — অর্থাৎ পাশাপাশি র‍্যাংকের দুটো এলিমেন্ট সবসময়ই কখনো-না-কখনো তুলনা হবে, নিশ্চিতভাবে। এটি স্বজ্ঞামূলকভাবেও ঠিক লাগে: দুটো পাশাপাশি এলিমেন্টকে আলাদা করার জন্য তাদের মাঝে কোনো তৃতীয় এলিমেন্ট থাকা সম্ভব না (কোনো "মধ্যবর্তী" র‍্যাংক নেই), তাই যেকোনো পার্টিশন ধাপে যেই একজন এদের একজনকে পিভট হিসেবে বাছুক না কেন, অন্যজনের সাথে তুলনা অবশ্যম্ভাবী।

প্র ০৩ এই লেসনের কোডে sizes = [50, 100, 200, 400] ব্যবহার করা হয়েছে, খুব বড় $n$ (যেমন ১০ লক্ষ) নয় কেন?

দুটো ব্যবহারিক কারণ। প্রথমত, randomized_quicksort_count রিকার্সিভভাবে লেখা — Python-এর ডিফল্ট রিকার্শন লিমিট (~1000) অতিক্রম করার ঝুঁকি এড়াতে $n$ ছোট রাখা নিরাপদ (যদিও randomized সংস্করণে গভীরতা প্রায় সবসময়ই $O(\log n)$, ফিক্সড-পিভটের বিপরীতে)। দ্বিতীয়ত, এই ডেমোতে প্রতিটি $n$-এ ৪০টি সম্পূর্ণ সর্ট চালানো হচ্ছে (মোট $4 \times 40 = 160$টি সর্ট) — Pyodide-এর ব্রাউজার-ভিত্তিক sandbox-এ যুক্তিসঙ্গত সময়ে শেষ হওয়ার জন্য এই আকারই যথেষ্ট স্পষ্ট $n\log n$ বনাম $n^2$ প্যাটার্ন দেখাতে পারে।

অনুশীলন

  1. চিন্তা করুন: যদি ইনপুট অ্যারে আগে-থেকে-সাজানো না হয়ে সম্পূর্ণ র‍্যান্ডম হতো, তাহলে কি নেইভ (ফিক্সড-পিভট) সংস্করণের তুলনার সংখ্যাও randomized সংস্করণের কাছাকাছি চলে আসত?

    হ্যাঁ — ফিক্সড-পিভট কুইকসর্টের average-case (র‍্যান্ডম ইনপুটে) আচরণও $\Theta(n\log n)$ (এটাই L16-এ উল্লেখ করা হয়েছিল)। সমস্যাটা ফিক্সড পিভটে নেই বরং এই ঘটনায় যে ইনপুট যদি adversarial হয় (যেমন আগে-সাজানো, বা এমনকি এমন কেউ ইচ্ছাকৃতভাবে তৈরি করলে যে জানে কোন এলিমেন্ট পিভট হবে), তাহলে ফিক্সড-পিভট সংস্করণ প্রতিবারই খারাপ বিভাজনে বাধ্য হয়। র‍্যান্ডোমাইজড সংস্করণের সৌন্দর্য এটাই যে এটি এই "ইনপুট বনাম পিভট-নির্বাচন" সম্পর্কটাকেই ভেঙে দেয় — কোনো ইনপুট (এমনকি ইচ্ছাকৃতভাবে তৈরি করা adversarial ইনপুটও) এখন র‍্যান্ডম পিভটের বিরুদ্ধে "জানতে" পারে না।

  2. পরীক্ষা করুন: উপরের দ্বিতীয় কোড সেলে num_seeds = 40-কে num_seeds = 5 করে Run চেপে দেখুন — গড়/$(n\log_2 n)$ কলামটি কি আগের মতোই স্থির থাকে, নাকি বেশি ওঠানামা করে?

    মাত্র ৫টি seed দিয়ে গড় নেওয়া হলে কলামের মানগুলো বেশি ওঠানামা করবে (কম নমুনার কারণে গড়ের নিজস্ব ভেরিয়েন্স বেশি) — কোনো কোনো $n$-এ হয়তো ধ্রুবক থেকে একটু দূরে চলে যাবে। এটাই দেখায় কেন M12 জুড়ে (এবং সাধারণভাবে র‍্যান্ডোমাইজড অ্যালগরিদম বিশ্লেষণে) অনেক ট্রায়ালের গড় নেওয়া জরুরি — কম ট্রায়ালের গড় নিজেই একটি noisy অনুমান, সত্যিকারের এক্সপেক্টেশনের নির্ভরযোগ্য প্রতিনিধি নয়।

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

আগের পাঠ
র‍্যান্ডোমাইজড অ্যালগরিদম — লাস ভেগাস বনাম মন্টি কার্লো