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

ডিভাইড অ্যান্ড কনকার প্যারাডাইম

The Divide-and-Conquer Paradigm
৭ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • ডিভাইড, কনকার ও কমবাইন — তিনটি ধাপ সুনির্দিষ্টভাবে সংজ্ঞায়িত করতে পারা
  • পরিচিত অ্যালগরিদমে (মার্জ সর্ট, কুইক সর্ট, বাইনারি সার্চ) এই তিনটি ধাপ শনাক্ত করতে পারা
  • একটি D&C অ্যালগরিদমের রানটাইমকে রিকারেন্স রিলেশন হিসেবে লিখতে পারা
  • রিকার্সিভ স্প্লিটিং প্যাটার্নের একটি জেনুইন কম্পিউটেশনাল ডেমো দেখা — মোট রিকার্সিভ কল ও ডেপথ প্রকৃতপক্ষে গুনে

১ · তিনটি ধাপ — ডিভাইড, কনকার, কমবাইন

ডিভাইড অ্যান্ড কনকারDivide and Conquerএকটি ডিজাইন প্যারাডাইম যেখানে একটি সমস্যাকে ছোট, একই ধরনের সাব-প্রবলেমে ভাগ করা হয়, প্রতিটি রিকার্সিভভাবে সমাধান করা হয়, এবং সাব-সমাধানগুলো জোড়া দিয়ে মূল সমাধান তৈরি করা হয়। হলো একটি সাধারণ, বহু অ্যালগরিদমে পুনরাবৃত্ত হওয়া টেমপ্লেট। এর তিনটি ধাপ:

ডিভাইড (Divide)
মূল সমস্যাকে একই ধরনের কিন্তু ছোট আকারের একাধিক সাব-প্রবলেমে ভাগ করা — সাধারণত আকার প্রায় সমান ভাগে (যেমন $n/2$)।
কনকার (Conquer)
প্রতিটি সাব-প্রবলেম রিকার্সিভভাবে সমাধান করা — যদি সাব-প্রবলেমের আকার যথেষ্ট ছোট হয় (বেস কেস), তবে সরাসরি সমাধান করা।
কমবাইন (Combine)
সাব-প্রবলেমগুলোর সমাধান জোড়া দিয়ে মূল সমস্যার সমাধান তৈরি করা।

সাধারণ সিউডোকোড আকারে লিখলে:

function solve(problem):
    if problem is small enough (base case):
        return direct_solution(problem)
    subproblems = divide(problem)          # ডিভাইড
    sub_solutions = [solve(sp) for sp in subproblems]   # কনকার (রিকার্সিভ কল)
    return combine(sub_solutions)          # কমবাইন
আকার n আকার n/2 আকার n/2 আকার n/4 আকার n/4 আকার n/4 আকার n/4 ⋮ বেস কেসে (আকার ১) পৌঁছানো পর্যন্ত বিভাজন চলতে থাকে ডিভাইড ↓ ↑ কমবাইন
ডিভাইড ধাপ ট্রি-তে নিচে নামতে থাকে (সমস্যা ছোট হতে থাকে) যতক্ষণ না বেস কেসে পৌঁছায়; কমবাইন ধাপ রিকার্সন থেকে ফেরার সময় নিচ থেকে উপরে ঘটে (সাব-সমাধান জোড়া দিতে দিতে)।
D&C কখন কাজে লাগে

ডিভাইড অ্যান্ড কনকার তখনই কার্যকর যখন দুটি শর্ত পূরণ হয়: (১) সাব-প্রবলেমগুলো মূল সমস্যার সাথে একই ধরনের (তাই একই রিকার্সিভ ফাংশন দিয়ে সমাধান করা যায়), এবং (২) সাব-প্রবলেমগুলো একে অপরের থেকে স্বাধীন — একটির সমাধান আরেকটির উপর নির্ভর করে না। এই স্বাধীনতার কারণেই কনকার ধাপে সাব-প্রবলেমগুলো (নীতিগতভাবে) যেকোনো ক্রমে বা এমনকি সমান্তরালে সমাধান করা যায়। M6-এ আমরা দেখব, যখন সাব-প্রবলেমগুলো ওভারল্যাপ করে (একই সাব-প্রবলেম বারবার আসে), তখন প্লেইন রিকার্সন অপচয়মূলক হয়ে যায় এবং ডাইনামিক প্রোগ্রামিং প্রয়োজন হয়।

২ · পরিচিত D&C অ্যালগরিদম — ../dsa/ কোর্সে যা দেখেছেন

নিচের তিনটি অ্যালগরিদমের ইমপ্লিমেন্টেশন ও মেকানিক্স ইতিমধ্যে DSA কোর্সে কভার করা হয়েছে — এখানে শুধু D&C টেমপ্লেটের সাথে কীভাবে মেলে তা দেখা হচ্ছে; L15–L18-এ প্রতিটির রিগোরাস রানটাইম-অ্যানালাইসিস আসবে।

মার্জ সর্ট
ডিভাইড: অ্যারেকে মাঝখান থেকে দুই ভাগে ভাগ করা। কনকার: প্রতিটি অর্ধেক রিকার্সিভভাবে সর্ট করা। কমবাইন: দুটি সর্টেড অর্ধেক $O(n)$ সময়ে মার্জ করা। বিস্তারিত L15।
কুইক সর্ট
ডিভাইড: একটি পিভট বেছে অ্যারেকে পিভটের চেয়ে ছোট/বড় দুই ভাগে পার্টিশন করা (এই ধাপেই আসল কাজ হয়)। কনকার: দুই ভাগ রিকার্সিভভাবে সর্ট করা। কমবাইন: তুচ্ছ — কিছুই করতে হয় না, পার্টিশন হওয়া অ্যারেই চূড়ান্ত। বিস্তারিত L16।
বাইনারি সার্চ
ডিভাইড: মাঝের এলিমেন্টের সাথে তুলনা করে অ্যারেকে দুই ভাগে ভাগ করা। কনকার: শুধু একটি ভাগেই রিকার্সিভভাবে খোঁজা (অন্যটি বাতিল)। কমবাইন: তুচ্ছ — সাব-প্রবলেমের উত্তরই চূড়ান্ত উত্তর। বিস্তারিত L17।

লক্ষ্য করুন — কমবাইন ধাপের খরচ প্রতিটিতে ভিন্ন: মার্জ সর্টে $O(n)$ (মার্জ করতে পুরো অ্যারে ঘুরতে হয়), কুইক সর্ট ও বাইনারি সার্চে $O(1)$ (কিছুই করার নেই)। এই পার্থক্যই মূলত তাদের রানটাইম রিকারেন্সের $f(n)$ অংশ নির্ধারণ করে।

৩ · রানটাইম একটি রিকারেন্স হিসেবে

যেকোনো D&C অ্যালগরিদমের রানটাইম সাধারণভাবে এভাবে লেখা যায়:

$$T(n) = a \cdot T(n/b) + f(n)$$

এখানে $a$ = কতগুলো সাব-প্রবলেম তৈরি হয় (কনকার ধাপে কতবার রিকার্সিভ কল হয়), $b$ = প্রতিটি সাব-প্রবলেমের আকার মূল সমস্যার তুলনায় কত ভাগের এক ভাগ (ডিভাইড ফ্যাক্টর), এবং $f(n)$ = ডিভাইড ও কমবাইন ধাপের নিজস্ব খরচ (রিকার্সিভ কলগুলো বাদে)। এটি ঠিক সেই ফর্ম যা M3-এ (L10–L13) সাবস্টিটিউশন মেথড, রিকার্সন ট্রি মেথড এবং মাস্টার থিওরেম দিয়ে সমাধান করার কৌশল শেখা হয়েছে — এই মডিউল সেই টুলগুলো সরাসরি প্রয়োগ করবে:

মার্জ সর্ট
$T(n) = 2T(n/2) + O(n)$ — L15
কুইক সর্ট
ওয়ার্স্ট: $T(n) = T(n-1) + O(n)$; অ্যাভারেজ: $T(n) \approx 2T(n/2) + O(n)$ — L16
বাইনারি সার্চ
$T(n) = T(n/2) + O(1)$ — L17
স্ট্রাসেনের গুণন
$T(n) = 7T(n/2) + O(n^2)$ — L18

৪ · কম্পিউটেশনাল ডেমো — রিকার্সিভ স্প্লিটিং প্যাটার্নের আকার

L15–L18-এ আমরা প্রতিটি অ্যালগরিদমের তুলনার সংখ্যা গুনব। এখানে, তার আগে, শুধু D&C-এর রিকার্সিভ কল-স্ট্রাকচারটাই কেমন দেখতে — সেটা সরাসরি গুনে দেখি। নিচের কোডে একটি অ্যারের সর্বোচ্চ মান বের করার একটি (ইচ্ছাকৃতভাবে সরল, অপটিমাইজড নয়) D&C ফাংশন লেখা হয়েছে — এটি প্রতিটি রিকার্সিভ কল এবং সর্বোচ্চ ডেপথ গুনে রাখে।

Python
import random
import math

def max_divide_and_conquer(arr, counter, depth=0):
    counter['calls'] += 1
    counter['max_depth'] = max(counter['max_depth'], depth)
    if len(arr) == 1:
        return arr[0]
    mid = len(arr) // 2
    left_max = max_divide_and_conquer(arr[:mid], counter, depth + 1)
    right_max = max_divide_and_conquer(arr[mid:], counter, depth + 1)
    return left_max if left_max > right_max else right_max

random.seed(42)
sizes = [8, 16, 32, 64, 128]

print(f"{'n':>5} | {'সঠিক?':>7} | {'কল':>6} | {'2n-1':>6} | {'ডেপথ':>6} | {'log2(n)':>8}")
for n in sizes:
    arr = list(range(n))
    random.shuffle(arr)
    counter = {'calls': 0, 'max_depth': 0}
    result = max_divide_and_conquer(arr, counter)
    predicted_calls = 2 * n - 1
    predicted_depth = int(math.log2(n))
    correct = (result == max(arr))
    print(f"{n:>5} | {str(correct):>7} | {counter['calls']:>6} | {predicted_calls:>6} | {counter['max_depth']:>6} | {predicted_depth:>8}")

    
প্রতিটি $n$-এ কল কলামটি ঠিক 2n-1 এর সমান হয় এবং ডেপথ কলামটি ঠিক $\log_2 n$-এর সমান হয় (কারণ প্রতিটি $n$ এখানে $2$-এর ঘাত)। এটাই ঠিক পূর্ণ বাইনারি রিকার্সন ট্রি-র গঠন — $n$টি লিফ (বেস কেস) এবং $n-1$টি ইন্টারনাল নোড মিলে মোট $2n-1$টি নোড/কল, এবং রুট থেকে লিফ পর্যন্ত দূরত্ব (ডেপথ) $\log_2 n$। M3/L12-এ এই একই "রিকার্সন ট্রি" ধারণা ব্যবহার করেই রিকারেন্স সমাধান করা শেখা হয়েছে — এখানে শুধু ট্রি-র আকৃতিটা সরাসরি কোড দিয়ে গুনে দেখানো হলো। লক্ষণীয়, এই নির্দিষ্ট সমস্যার (max বের করা) জন্য D&C ব্যবহার করাটা রানটাইমে কোনো লাভ দেয় না (এখনও $O(n)$ তুলনা লাগে, একটি সরল লিনিয়ার স্ক্যানের মতোই) — কিন্তু এটি রিকার্সিভ স্প্লিটিং-এর আকৃতি স্পষ্টভাবে দেখানোর জন্য উপযোগী একটি সরল উদাহরণ।
মূল কথা · Key takeaway

ডিভাইড অ্যান্ড কনকার একটি টেমপ্লেট, একটি নির্দিষ্ট অ্যালগরিদম নয়। এর শক্তি নির্ভর করে নির্দিষ্ট সমস্যায় ডিভাইড ও কমবাইন ধাপ কতটা সস্তায় করা যায় তার উপর — এবং সেই খরচ ($f(n)$) ও শাখা-সংখ্যা ($a$, $b$) নির্ণয় করার পরই রিকারেন্স রিলেশন সমাধান করে প্রকৃত জটিলতা বের করা সম্ভব হয়। পরবর্তী চারটি পাঠ ঠিক এই কাজটিই করবে — মার্জ সর্ট, কুইক সর্ট, বাইনারি সার্চ ও স্ট্রাসেনের গুণনের জন্য রিগোরাসভাবে।

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

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

প্র ০১ D&C কার্যকর হতে সাব-প্রবলেমগুলো "স্বাধীন" হতে হবে কেন? স্বাধীন না হলে কী সমস্যা হয়?

সাব-প্রবলেমগুলো স্বাধীন হলে প্রতিটি সাব-প্রবলেম একবার সমাধান করলেই যথেষ্ট — কমবাইন ধাপে শুধু ফলাফলগুলো জোড়া দিলেই চলে। কিন্তু সাব-প্রবলেমগুলো যদি ওভারল্যাপ করে (একই সাব-প্রবলেম বহুবার বিভিন্ন রিকার্সিভ শাখা থেকে আসে), তাহলে প্লেইন D&C রিকার্সন সেই একই সাব-প্রবলেম বারবার সমাধান করবে — যেমন নেইভ ফিবোনাচি রিকার্সন এক্সপোনেনশিয়াল সময় নেয় (L10-এ দেখা হয়েছে)। এই ওভারল্যাপিং সাব-প্রবলেম পরিস্থিতিতেই M6-এর ডাইনামিক প্রোগ্রামিং (মেমোয়াইজেশন/ট্যাবুলেশন দিয়ে প্রতিটি সাব-প্রবলেম একবারই সমাধান করা) কাজে লাগে।

প্র ০২ মার্জ সর্টের কমবাইন ধাপ $O(n)$ কিন্তু কুইক সর্ট ও বাইনারি সার্চের কমবাইন ধাপ প্রায় $O(1)$ — এই পার্থক্য কেন হয়?

মার্জ সর্টে ডিভাইড ধাপ তুচ্ছ (শুধু মাঝখান থেকে ভাগ করা) কিন্তু "আসল কাজ" — দুটি সর্টেড অর্ধেককে একত্রে একটি সর্টেড ক্রমে সাজানো — কমবাইন ধাপে ঘটে, যা পুরো অ্যারে ঘুরে করতে হয় ($O(n)$)। কুইক সর্টে উল্টো — "আসল কাজ" (পার্টিশনিং, অর্থাৎ পিভটের চেয়ে ছোট/বড় আলাদা করা) ডিভাইড ধাপেই ঘটে যায়, ফলে কমবাইন ধাপে আর কিছুই করার থাকে না। বাইনারি সার্চেও একই রকম — ডিভাইড ধাপেই (মাঝের এলিমেন্টের সাথে তুলনা করে) সিদ্ধান্ত নেওয়া হয়ে যায় কোন অর্ধেকে উত্তর আছে, তাই শুধু একটি সাব-প্রবলেম সমাধান করলেই চলে এবং কমবাইন করার কিছু নেই। কোন ধাপে "আসল কাজ" হচ্ছে তা লক্ষ্য করাই একটি D&C অ্যালগরিদম বোঝার চাবিকাঠি।

প্র ০৩ উপরের কোডে সর্বোচ্চ মান বের করার D&C পদ্ধতিটি একটি সাধারণ লিনিয়ার স্ক্যানের চেয়ে দ্রুত নয় (উভয়ই $O(n)$) — তাহলে এটি দেখানোর উদ্দেশ্য কী?

এই ডেমোর উদ্দেশ্য গতি দেখানো নয় — উদ্দেশ্য হলো D&C-এর রিকার্সিভ কল-কাঠামো (কতগুলো কল হয়, কত গভীরে যায়) প্রকৃতপক্ষে গুনে দেখানো, কারণ পরবর্তী পাঠগুলোতে (L15–L18) ঠিক এই একই কাঠামোর উপর ভিত্তি করে রিকারেন্স রিলেশন লেখা ও মাস্টার থিওরেম প্রয়োগ করা হবে। কিছু D&C অ্যালগরিদম (যেমন মার্জ সর্ট, স্ট্রাসেনের গুণন) নেইভ পদ্ধতির চেয়ে দ্রুত হয়; কিছু (যেমন এই max উদাহরণ) হয় না — কারণটা নির্ভর করে কমবাইন ধাপে কতটা "অতিরিক্ত কাজ" বাঁচানো যাচ্ছে তার উপর, যা L15/L18-এ স্পষ্ট হবে।

অনুশীলন

  1. চিন্তা করুন: উপরের প্যাটার্ন অনুযায়ী $n = 256$ হলে মোট রিকার্সিভ কল সংখ্যা ও সর্বোচ্চ ডেপথ কত হবে বলে আপনার ধারণা?

    কল সংখ্যা $2 \times 256 - 1 = 511$টি হবে, এবং ডেপথ $\log_2 256 = 8$ হবে — প্যাটার্ন অনুসরণ করে।

  2. পরীক্ষা করুন: উপরের কোড সেলে sizes লিস্টে 256 যোগ করে (sizes = [8, 16, 32, 64, 128, 256]) Run চেপে আপনার অনুমান যাচাই করুন।

    আউটপুটে নতুন লাইনে দেখা যাবে n=256-এর জন্য কল 511 এবং ডেপথ 8 — দুটোই আপনার হিসাবের সাথে মিলে যাবে, কারণ $256 = 2^8$।

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

আগের পাঠ
মাস্টার থিওরেম