এই পর্বে যা শিখবেন
- যেকোনো সমস্যায় divide, conquer আর combine ধাপ চিনে recurrence লেখা
- Maximum subarray, inversion গোনা আর দ্রুত ঘাত — combine ধাপের তিন রকম কৌশল
- Karatsuba আর Strassen কীভাবে একটি গুণ বাঁচিয়ে পুরো complexity বদলে দেয়
- Closest pair of points-এর O(n log n) অ্যালগরিদম আর strip-এর যুক্তির প্রমাণ
- কখন divide and conquer লাভ দেয় না, আর তখন কোন পথে যাবেন
১ · ভাগ করুন, সমাধান করুন, জোড়া দিন — কৌশলটি
মডিউল ৭-এ আমরা একটি নির্দিষ্ট ডেটা স্ট্রাকচার নয়, বরং অ্যালগরিদম ডিজাইনের কৌশল শিখব — নতুন সমস্যা দেখলে কোন দিক থেকে ভাবতে শুরু করবেন। প্রথম কৌশলটি আপনি আগেই ব্যবহার করেছেন: merge sort (পর্ব ২১), quick sort (পর্ব ২২) আর binary search (পর্ব ১৮) — তিনটিই divide and conquer. এই পর্বে কৌশলটিকে নাম ধরে আলাদা করে দেখব, তারপর এমন কয়েকটি সমস্যায় প্রয়োগ করব যেখানে এর শক্তি সত্যিই চমকে দেয়।
একটি বোর্ড পরীক্ষায় ১০,০০০ খাতা দেখতে হবে, আর জানতে হবে সর্বোচ্চ নম্বর কত। একজন শিক্ষক একা দেখলে কয়েক সপ্তাহ লাগবে। প্রধান পরীক্ষক খাতাগুলো ১০ জন শিক্ষকের মধ্যে ভাগ করে দেন (divide), প্রত্যেকে নিজের ভাগ দেখে নিজের ভাগের সর্বোচ্চ নম্বরটি জানান (conquer), আর প্রধান পরীক্ষক ১০টি নম্বরের মধ্যে বড়টি বেছে নেন (combine). প্রত্যেক শিক্ষক চাইলে নিজের ভাগ আবার সহকারীদের মধ্যে ভাগ করতে পারেন — একই নিয়মে। খেয়াল করুন, “সর্বোচ্চ” জোড়া দেওয়া সহজ। কিন্তু প্রশ্ন যদি হয় “মেধাতালিকা বানাও”, তখন ১০টি সাজানো তালিকা মেলাতে বেশি কাজ লাগে — merge sort-এর merge. আর প্রশ্ন যদি হয় “কোন দুজনের নম্বর সবচেয়ে কাছাকাছি”, তখন উত্তর দুই শিক্ষকের ভাগের মাঝখানেও থাকতে পারে। জোড়া দেওয়ার এই সূক্ষ্মতাই এই পর্বের মূল বিষয়।
প্রতিটি divide and conquer অ্যালগরিদমে তিনটি অংশ থাকে:
- Divide: ইনপুটকে a টি উপ-সমস্যায় ভাগ, প্রতিটি সাধারণত মূলের n/b আকারের। ভাগটা সস্তা হতে পারে (merge sort-এ শুধু মাঝের ইনডেক্স), আবার দামিও হতে পারে (quick sort-এর partition).
- Conquer: প্রতিটি উপ-সমস্যা রিকার্শনে একই অ্যালগরিদম দিয়ে সমাধান। আকার যথেষ্ট ছোট হলে (base case) সরাসরি উত্তর।
- Combine: ছোট উত্তরগুলো থেকে বড় উত্তর তৈরি। এর খরচই সাধারণত অ্যালগরিদমের গতি ঠিক করে।
SOLVE(P) // P: a problem of size n
if n ≤ n0: return DIRECT(P) // base case, Θ(1) for constant n0
P1, ..., Pa = DIVIDE(P) // a pieces, each of size about n/b
for i = 1 to a:
Ri = SOLVE(Pi) // a recursive calls: a · T(n/b)
return COMBINE(R1, ..., Ra) // divide + combine together cost f(n)এই কাঠামো থেকে সরাসরি recurrence লেখা যায়: T(n) = aT(n/b) + f(n), যেখানে f(n) = divide আর combine-এর মোট খরচ। তারপর পর্ব ৯-এর master theorem বা পর্ব ৮-এর recursion tree দিয়ে উত্তর। এই পর্বের অ্যালগরিদমগুলোর একটি অগ্রিম তালিকা:
| সমস্যা | সরল পদ্ধতি | Recurrence | Divide and conquer |
|---|---|---|---|
| Maximum subarray | Θ(n²) | 2T(n/2) + Θ(n) | Θ(n log n) |
| Inversion গোনা | Θ(n²) | 2T(n/2) + Θ(n) | Θ(n log n) |
| xe (দ্রুত ঘাত) | Θ(e) গুণ | T(e/2) + Θ(1) | Θ(log e) গুণ |
| দুটি n-অঙ্কের সংখ্যার গুণ | Θ(n²) | 3T(n/2) + Θ(n) | Θ(nlog2 3) ≈ Θ(n1.585) |
| দুটি n×n matrix-এর গুণ | Θ(n³) | 7T(n/2) + Θ(n²) | Θ(nlog2 7) ≈ Θ(n2.807) |
| Closest pair of points | Θ(n²) | 2T(n/2) + Θ(n) | Θ(n log n) |
| k-তম ছোট (worst case) | Θ(n log n) সর্ট করে | T(n/5) + T(7n/10) + Θ(n) | Θ(n) |
Divide and conquer তখনই জেতে, যখন (১) উপ-সমস্যাগুলো স্বাধীন — একটির সমাধান অন্যটির কাজ নয়, (২) আকার ভাগ হয়ে কমে (বিয়োগ হয়ে নয়), আর (৩) combine ধাপ সস্তা — সাধারণত O(n) বা তার কম। তিনটির যেকোনো একটি ভাঙলে লাভ কমে বা হারিয়ে যায়।
২ · Maximum subarray — মাঝখান পার হওয়া অংশই আসল কাজ
পর্ব ১০-এ দেখেছেন maximum subarray সমস্যা: একটি দোকানের দৈনিক লাভ-ক্ষতির তালিকায় টানা কোন কয়েক দিনে মোট লাভ সবচেয়ে বেশি? সব (l, r) জোড়া চেষ্টা করলে Θ(n²). Divide and conquer দিয়ে ভাবুন: অ্যারেটি মাঝখানে ভাগ করলে সেরা অংশটি তিন জায়গার একটিতে থাকবেই —
- পুরোটাই বাঁ অর্ধেকে → রিকার্শন খুঁজে দেবে;
- পুরোটাই ডান অর্ধেকে → রিকার্শন খুঁজে দেবে;
- মাঝের রেখা পার হয়ে — বাঁ অর্ধেকের শেষ ঘর a[mid−1] আর ডান অর্ধেকের প্রথম ঘর a[mid] দুটিই এর মধ্যে। এটি রিকার্শন দেখে না, combine ধাপকে খুঁজতে হবে।
তৃতীয় ক্ষেত্রের চাবি: পার হওয়া যেকোনো অংশ = বাঁ অর্ধেকের একটি suffix (শেষ দিকের টুকরো) + ডান অর্ধেকের একটি prefix (শুরুর দিকের টুকরো). দুটি স্বাধীনভাবে বাছা যায়, তাই সেরা suffix আর সেরা prefix আলাদা করে খুঁজে যোগ করলেই সেরা পার-হওয়া অংশ। মাঝ থেকে বাঁয়ে একটি লুপ, ডানে একটি লুপ — মোট Θ(n).
MAXSUB(a, lo, hi) // 0-based, half-open a[lo..hi), hi - lo ≥ 1
if hi - lo == 1: return a[lo]
mid = (lo + hi) / 2
best = max(MAXSUB(a, lo, mid), MAXSUB(a, mid, hi))
s = 0; bestL = -∞
for i = mid-1 down to lo: s = s + a[i]; bestL = max(bestL, s) // best suffix
s = 0; bestR = -∞
for j = mid to hi-1: s = s + a[j]; bestR = max(bestR, s) // best prefix
return max(best, bestL + bestR)৮ দিনের হিসাব (হাজার টাকায়) [−2, 1, −3, 4, −1, 2, 1, −5] নিয়ে ধাপে ধাপে দেখুন:
index : 0 1 2 3 │ 4 5 6 7
a : -2 1 -3 4 │ -1 2 1 -5
left half │ right half
[-2 1] │ [-3 4]
left 1 , right 4 , cross = 1 + 1 = 2
best of left half = 4 ( [4] )
[-1 2] │ [1 -5]
left 2 , right 1 , cross = 2 + 1 = 3
best of right half = 3 ( [2 1] )
i : 3 2 1 0
a[i] : 4 -3 1 -2
s : 4 1 2 0
bestL = 4 ( a[3..3] )
j : 4 5 6 7
a[j] : -1 2 1 -5
s : -1 1 2 -3
bestR = 2 ( a[4..6] )
left = 4
right = 3
cross = bestL + bestR = 4 + 2 = 6 -> a[3..6] = [4 -1 2 1]
answer = max(4, 3, 6) = 6
- ভাগ। ৮টি ঘর, mid = ৪. বাঁ অর্ধেক ইনডেক্স ০–৩, ডান অর্ধেক ৪–৭. সেরা অংশটি বাঁয়ে, ডানে, নয়তো রেখা পার হয়ে — চতুর্থ কোনো সম্ভাবনা নেই।
- বাঁ অর্ধেক। রিকার্শন আবার ভাগ করে: [−2 1]-এর সেরা ১, [−3 4]-এর সেরা ৪, আর পার-হওয়া সেরা ১ + ১ = ২. তাই বাঁ অর্ধেকের উত্তর ৪ — শুধু ঘর ৩.
- ডান অর্ধেক। [−1 2]-এর সেরা ২, [1 −5]-এর সেরা ১, পার-হওয়া ২ + ১ = ৩. ডান অর্ধেকের উত্তর ৩ — ঘর ৫ আর ৬.
- সেরা suffix. mid−1 = ৩ থেকে বাঁয়ে যোগ করতে করতে যাই: ৪, ১, ২, ০. সবচেয়ে বড় ৪. খেয়াল করুন, ঘর ৩ থেকেই শুরু করা বাধ্যতামূলক — পার-হওয়া অংশে a[mid−1] থাকতেই হবে।
- সেরা prefix. mid = ৪ থেকে ডানে: −১, ১, ২, −৩. সবচেয়ে বড় ২ (ঘর ৪ থেকে ৬). প্রথম ঘর −১ হলেও তাকে বাদ দেওয়া যায় না — পার হতে হলে a[mid]-ও লাগবে।
- জোড়া। পার-হওয়া সেরা = ৪ + ২ = ৬, যা বাঁয়ের ৪ আর ডানের ৩ দুটিকেই হারায়। উত্তর ৬, অংশ [4 −1 2 1]. এখানে উত্তরটি কোনো অর্ধেকের ভেতরে ছিল না — combine না থাকলে হারিয়ে যেত।
কেন সঠিক, আর কত দ্রুত
সঠিকতা (induction, আকারের ওপর): আকার ১-এ উত্তর একমাত্র ঘরটিই। ধরুন ছোট সব আকারে MAXSUB সঠিক। সেরা অংশ [l, r] হয় r < mid (পুরো বাঁয়ে — হাইপোথিসিসে বাঁয়ের রিকার্শন অন্তত এর সমান পায়), নয়তো l ≥ mid (একইভাবে ডানে), নয়তো l < mid ≤ r. শেষ ক্ষেত্রে এর যোগফল = sum(a[l..mid−1]) + sum(a[mid..r]) ≤ bestL + bestR, আর bestL + bestR নিজেও একটি বৈধ পার-হওয়া অংশের যোগফল। তাই তিন প্রার্থীর সর্বোচ্চই উত্তর।
সময়: দুটি অর্ধেক-আকারের ডাক আর দুটি লুপ মিলিয়ে Θ(n) — T(n) = 2T(n/2) + Θ(n), master theorem-এর case 2: Θ(n log n). জায়গা: কোনো বাড়তি অ্যারে নেই, শুধু রিকার্শনের stack — Θ(log n).
Θ(n log n) ভালো, কিন্তু সেরা নয়। পর্ব ১০-এর Kadane's algorithm একবার বাঁ থেকে ডানে হেঁটে Θ(n) সময়ে আর Θ(1) জায়গায় একই উত্তর দেয়: প্রতিটি ঘরে “এখানে শেষ হওয়া সেরা যোগফল” = max(a[i], আগেরটি + a[i]). এটি আসলে dynamic programming-এর ভাবনা — পর্ব ৫২-এ ফিরে আসব। তাহলে divide and conquer সংস্করণ শিখলাম কেন? কারণ এর “বাঁ + ডান + মাঝ-পার” কাঠামোটি অনেক জায়গায় কাজে লাগে — যেমন পর্ব ৩৪-এর segment tree-তে রেঞ্জের সেরা subarray, যেখানে প্রতিটি নোড ঠিক এই চারটি মান (মোট যোগফল, সেরা prefix, সেরা suffix, সেরা subarray) রাখে আর দুই সন্তানের মান থেকে নিজেরটি জোড়া দেয়।
৩ · Combine ধাপের কারিগরি — inversion গোনা আর দ্রুত ঘাত
আরও দুটি পরিচিত উদাহরণে combine ধাপকে আলাদা চোখে দেখি।
Inversion গোনা — merge-এর ভেতরে লুকানো হিসাব
অ্যারেতে i < j অথচ a[i] > a[j] — এমন জোড়াকে বলে inversion. পর্ব ২১-এর শেষ দিকে এটি merge sort দিয়ে Θ(n log n)-এ গুনেছেন। Divide and conquer-এর ভাষায় পুরো ব্যাপারটা এক লাইনে: মোট inversion = বাঁয়ের ভেতরের + ডানের ভেতরের + পার-হওয়া (একটি বাঁয়ে, একটি ডানে). প্রথম দুটি রিকার্শন গোনে। পার-হওয়াগুলো সরাসরি গুনলে Θ(n²) লাগত — কিন্তু দুই অর্ধেক যদি আগেই সাজানো থাকে, তাহলে merge করার সময় ডান থেকে a[j] নেওয়ার মুহূর্তে বাঁয়ে অপেক্ষায় থাকা সবাই (mid − i জন) তার চেয়ে বড় — এক যোগে গোনা শেষ। অর্থাৎ রিকার্শন শুধু উত্তর নয়, combine-কে সহজ করার মতো বাড়তি তথ্যও (সাজানো ক্রম) ফেরত দেয়। এই কৌশলটি — “সমস্যাকে একটু শক্তিশালী করে নিন, যাতে জোড়া দেওয়া সহজ হয়” — closest pair-এও আবার দেখবেন, যেখানে রিকার্শন বিন্দুগুলো y অনুযায়ী সাজিয়ে ফেরত দেয়।
দ্রুত ঘাত — একটি উপ-সমস্যাই যথেষ্ট
313 বের করতে সরলভাবে ১২টি গুণ লাগে। কিন্তু xe = (xe/2)² (e জোড় হলে) আর x · (x(e−1)/2)² (e বিজোড় হলে). এখানে a = 1: একটিমাত্র উপ-সমস্যা, আর তার উত্তরকে নিজের সাথে গুণ করাই combine. পর্ব ৭-এ এটি binary exponentiation নামে দেখেছেন। 313-এর হিসাব:
| ডাক | e | নিয়ম | ফেরত মান |
|---|---|---|---|
| pow(3, 0) | 0 | base case | 1 |
| pow(3, 1) | 1 (বিজোড়) | 3 · 1² | 3 |
| pow(3, 3) | 3 (বিজোড়) | 3 · 3² | 27 |
| pow(3, 6) | 6 (জোড়) | 27² | 729 |
| pow(3, 13) | 13 (বিজোড়) | 3 · 729² | 1594323 |
Recurrence T(e) = T(e/2) + Θ(1), তাই Θ(log e) গুণ — প্রতিটি ধাপে বড়জোর ২টি। e = 1018 হলেও মাত্র ৬০টির মতো ধাপ। প্রতিটি গুণের পর mod নিলে (modular arithmetic) সংখ্যা ছোট থাকে — কনটেস্টে “উত্তর 109 + 7 দিয়ে mod করুন” ধরনের সমস্যার এটি রোজকার হাতিয়ার, আর RSA-র মতো cryptography-র ভিত্তিও। একই কৌশল matrix-এ খাটে: 2×2 matrix-এর ঘাত দিয়ে Fibonacci-র n-তম সংখ্যা Θ(log n) গুণে।
সাধারণ ভুল: pow(x, e/2) * pow(x, e/2) লেখা — একই উপ-সমস্যা দুবার ডাকা। তখন a = 2, T(e) = 2T(e/2) + Θ(1) = Θ(e) — সব লাভ শেষ! ফল একবার ভ্যারিয়েবলে রেখে তারপর বর্গ করুন। “একই উপ-সমস্যা দুবার সমাধান” — এই ভুলটির বড় রূপ দেখবেন অংশ ৭-এ।
৪ · Karatsuba — চারটির বদলে তিনটি গুণ
স্কুলে শেখা গুণে দুটি n-অঙ্কের সংখ্যা গুণ করতে প্রতিটি অঙ্ককে প্রতিটি অঙ্কের সাথে গুণ করতে হয় — n²টি এক-অঙ্কের গুণ। একে বলে schoolbook multiplication. ১০০ অঙ্কে ১০,০০০টি গুণ, আর cryptography-তে ব্যবহৃত হাজার অঙ্কের সংখ্যায় ১০ লাখ। এর চেয়ে কম কি সম্ভব?
১৯৬০ সালে মস্কো বিশ্ববিদ্যালয়ে কিংবদন্তি গণিতবিদ Andrey Kolmogorov একটি সেমিনারে অনুমান করেন যে গুণে Θ(n²)-এর কম সম্ভব নয়। সেমিনারের ২৩ বছরের ছাত্র Anatoly Karatsuba এক সপ্তাহের মধ্যেই এর চেয়ে দ্রুত একটি পদ্ধতি বের করে অনুমানটি ভুল প্রমাণ করেন। ফলাফল ১৯৬২ সালে Karatsuba ও Yuri Ofman-এর নামে প্রকাশিত হয়। বড় সংখ্যার গুণে স্কুলের Θ(n²) পদ্ধতিকে asymptotically হারানো এটিই প্রথম অ্যালগরিদম — আর প্রমাণ যে “স্বাভাবিক” পদ্ধতিই সবসময় সেরা নয়।
কৌশলটি দেখুন। দুটি n-অঙ্কের সংখ্যাকে অর্ধেক করি, B = 10n/2:
তাহলে xy = x₁y₁·B² + (x₁y₀ + x₀y₁)·B + x₀y₀. সরাসরি করলে চারটি অর্ধেক-আকারের গুণ: T(n) = 4T(n/2) + Θ(n), যা master theorem-এ Θ(nlog2 4) = Θ(n²) — কোনো লাভ নেই। Karatsuba-র চালাকি: মাঝের পদটির জন্য আলাদা দুটি গুণ লাগে না।
কারণ (x₁ + x₀)(y₁ + y₀) = x₁y₁ + x₁y₀ + x₀y₁ + x₀y₀ — এখান থেকে z₂ আর z₀ বাদ দিলে ঠিক মাঝের পদটি থাকে। মোট গুণ তিনটি; বিনিময়ে কয়েকটি বাড়তি যোগ-বিয়োগ, যার খরচ Θ(n). শেষে xy = z₂·B² + z₁·B + z₀ — B-এর গুণ মানে শুধু ডানে শূন্য বসানো (shift), আসল গুণ নয়।
| ধাপ | |
| জানা মান | |
| অর্ধেক-আকারের গুণ | |
| এক-অঙ্কের গুণ |
- ভাগ। ৪ অঙ্কের সংখ্যাকে দুই অঙ্ক করে ভাঙি: x₁ = ১২, x₀ = ৩৪, y₁ = ৫৬, y₀ = ৭৮. B = ১০০. স্কুলের পদ্ধতিতে এখান থেকে চারটি দুই-অঙ্কের গুণ লাগত।
- প্রথম গুণ: z₂ = ১২ × ৫৬ = ৬৭২. এটিও রিকার্শনে Karatsuba দিয়ে হয়: ১×৫ = ৫, ২×৬ = ১২, আর (১+২)(৫+৬) = ৩৩, মাঝের পদ ৩৩ − ৫ − ১২ = ১৬. তাই ৫০০ + ১৬০ + ১২ = ৬৭২ — তিনটি এক-অঙ্কের গুণে।
- দ্বিতীয় গুণ: z₀ = ৩৪ × ৭৮ = ২৬৫২. একইভাবে তিনটি এক-অঙ্কের গুণে (মোট এখন ৬টি)।
- তৃতীয় গুণ: (১২ + ৩৪)(৫৬ + ৭৮) = ৪৬ × ১৩৪ = ৬১৬৪. যোগফলে একটি অঙ্ক বেড়ে যেতে পারে (১৩৪) — Θ-এর হিসাবে এতে কিছু বদলায় না। কোডে অঙ্কগুলো carry ছাড়াই ঘরে ঘরে যোগ হয়: ৫৬ + ৭৮ থাকে দুই ঘরে (১২, ১৪), অর্থাৎ ১২·১০ + ১৪ = ১৩৪ — তাই ঘরের সংখ্যা ঠিক অর্ধেকই থাকে।
- মাঝের পদ বিয়োগে। z₁ = ৬১৬৪ − ৬৭২ − ২৬৫২ = ২৮৪০. যাচাই: ১২×৭৮ + ৩৪×৫৬ = ৯৩৬ + ১৯০৪ = ২৮৪০. চতুর্থ গুণটি বিয়োগ দিয়েই পেয়ে গেলাম।
- জোড়া। ৬৭২·১০⁴ + ২৮৪০·১০² + ২৬৫২ = ৭০০৬৬৫২. মোট এক-অঙ্কের গুণ ৯টি (৩ × ৩), স্কুলের পদ্ধতিতে ১৬টি (৪ × ৪). n = 2k অঙ্কে অনুপাতটি 3k বনাম 4k — যত বড় সংখ্যা, তত বড় ফারাক।
বিশ্লেষণ
T(n) = 3T(n/2) + Θ(n). Watershed nlog2 3 ≈ n1.585, আর f(n) = n তার চেয়ে polynomially ছোট — case 1: T(n) = Θ(nlog2 3) ≈ Θ(n1.585). Recursion tree-তে দেখলে: তলা j-তে 3jটি গুণ, প্রতিটি n/2j আকারের, তলার মোট খরচ (3/2)j·n — প্রতি তলায় দেড় গুণ বাড়ে, তাই পাতাগুলো জেতে, আর পাতা ঠিক 3log2 n = nlog2 3টি। ১০২৪ অঙ্কে স্কুলের পদ্ধতি প্রায় ১০ লাখ এক-অঙ্কের গুণ করে, Karatsuba করে 310 = ৫৯,০৪৯টি। জায়গা: প্রতি তলায় অস্থায়ী অঙ্কের তালিকা, মূল থেকে পাতা পর্যন্ত পথে n + n/2 + n/4 + … = Θ(n).
নিচের প্রোগ্রাম দশমিক অঙ্কের স্ট্রিং হিসেবে দেওয়া দুটি বড় সংখ্যা Karatsuba দিয়ে গুণ করে, এক-অঙ্কের গুণ গোনে, আর ফল যাচাই করে — C++-এ স্কুলের O(n²) পদ্ধতির সাথে, Python-এ ভাষার নিজস্ব বড়-সংখ্যার int-এর সাথে। অঙ্কগুলো উল্টো ক্রমে (little-endian) রাখা, আর দৈর্ঘ্য ২-এর ঘাতে পূরণ করা হয়েছে, যাতে প্রতিবার ঠিক অর্ধেক হয়। Carry শুধু একেবারে শেষে।
#include <cstdio>
#include <string>
#include <vector>
using namespace std;
using V = vector<long long>;
long long calls = 0; // one-digit multiplications done
V kara(V x, V y) { // little-endian digits, size = 2^k
size_t n = x.size(), m = n / 2;
if (n == 1) { ++calls; return {x[0] * y[0]}; }
V xh(x.begin() + m, x.end()), yh(y.begin() + m, y.end());
x.resize(m); y.resize(m); // x, y now hold the low halves
V z0 = kara(x, y), z2 = kara(xh, yh); // low*low, high*high
for (size_t i = 0; i < m; ++i) x[i] += xh[i], y[i] += yh[i];
V s = kara(x, y), r(2 * n - 1, 0); // (low+high)(low+high)
for (size_t i = 0; i + 1 < 2 * m; ++i) {
r[i] += z0[i], r[i + 2 * m] += z2[i];
r[i + m] += s[i] - z0[i] - z2[i]; // middle = s - z0 - z2
}
return r;
}
string digits(V c) { // carry coefficients into a string
string out; long long carry = 0; c.resize(c.size() + 2, 0);
for (long long v : c) { carry += v; out += char('0' + carry % 10); carry /= 10; }
while (out.size() > 1 && out.back() == '0') out.pop_back();
return string(out.rbegin(), out.rend());
}
int main() {
for (string a : {"1234", "31415926535897932384626433832795"}) {
string b = a.size() == 4 ? "5678" : "27182818284590452353602874713527";
size_t n = 1; while (n < max(a.size(), b.size())) n *= 2;
V x(n, 0), y(n, 0), plain(2 * n, 0);
for (size_t i = 0; i < a.size(); ++i) x[i] = a[a.size() - 1 - i] - '0';
for (size_t i = 0; i < b.size(); ++i) y[i] = b[b.size() - 1 - i] - '0';
for (size_t i = 0; i < n; ++i) // school method, O(n^2), as a check
for (size_t j = 0; j < n; ++j) plain[i + j] += x[i] * y[j];
calls = 0; string p = digits(kara(x, y));
printf("%s x %s\n= %s\n", a.c_str(), b.c_str(), p.c_str());
printf("digit mults: karatsuba %lld, school %zu, check %s\n", calls,
a.size() * b.size(), p == digits(plain) ? "ok" : "FAIL");
}
}দ্বিতীয় পরীক্ষার শেষ লাইন: digit mults: karatsuba 243, school 1024, check ok. পুরো আউটপুট আর ব্যাখ্যা নিচে।
calls = 0 # one-digit multiplications done
def kara(x, y): # little-endian digits, len = 2^k
global calls
n = len(x)
if n == 1:
calls += 1
return [x[0] * y[0]]
m = n // 2
z0 = kara(x[:m], y[:m]) # low * low
z2 = kara(x[m:], y[m:]) # high * high
s = kara([a + b for a, b in zip(x[:m], x[m:])],
[a + b for a, b in zip(y[:m], y[m:])]) # (low+high)(low+high)
r = [0] * (2 * n - 1)
for i in range(2 * m - 1):
r[i] += z0[i]
r[i + 2 * m] += z2[i]
r[i + m] += s[i] - z0[i] - z2[i] # middle = s - z0 - z2
return r
def multiply(a, b): # a, b: decimal strings
n = 1 << (max(len(a), len(b)) - 1).bit_length() # next power of two
x, y = ([int(d) for d in reversed(s)] + [0] * (n - len(s)) for s in (a, b))
out, carry = [], 0
for v in kara(x, y) + [0, 0]: # carry coefficients into digits
carry += v
out.append(carry % 10)
carry //= 10
return ''.join(map(str, reversed(out))).lstrip('0') or '0'
for a, b in [("1234", "5678"), ("31415926535897932384626433832795",
"27182818284590452353602874713527")]:
calls = 0
p = multiply(a, b)
print(f"{a} x {b}\n= {p}")
print(f"digit mults: karatsuba {calls}, school {len(a) * len(b)},",
"check ok" if p == str(int(a) * int(b)) else "check FAIL")আউটপুট C++ সংস্করণের সাথে অক্ষরে অক্ষরে এক। এখানে যাচাই হয়েছে str(int(a) * int(b))-এর সাথে — CPython নিজেও বড় সংখ্যায় Karatsuba ব্যবহার করে, তাই দুটি স্বাধীন বাস্তবায়ন একই উত্তর দিল।
দুই ভাষাতেই আউটপুট:
1234 x 5678 = 7006652 digit mults: karatsuba 9, school 16, check ok 31415926535897932384626433832795 x 27182818284590452353602874713527 = 853973422267356706546355086954668447174445893592492906242717965 digit mults: karatsuba 243, school 1024, check ok
প্রথম জোড়া ঠিক ডায়াগ্রামের উদাহরণ: ৭০০৬৬৫২, ৯টি এক-অঙ্কের গুণে। দ্বিতীয় জোড়ায় π আর e-এর প্রথম ৩২টি অঙ্ক — 32 = 25, তাই Karatsuba করে 35 = ২৪৩টি গুণ, স্কুলের পদ্ধতি 32² = ১০২৪টি — চার গুণেরও বেশি কম। কোডের তিনটি লক্ষণীয় দিক: (১) kara সংখ্যা নয়, অঙ্কের সহগ (coefficient) ফেরত দেয় — যেন বহুপদী রাশির গুণ; মাঝের পদ s − z0 − z2 তাই কখনো ঋণাত্মক হয় না। (২) যোগফলের “অঙ্ক” ৯-এর বেশি হতে পারে (যেমন ১৮), কিন্তু এক-অঙ্কের গুণ গোনায় তা একটি গুণই — বাস্তব লাইব্রেরি এক অঙ্কের বদলে একটি পুরো machine word (যেমন 232 ভিত্তির “অঙ্ক”) ব্যবহার করে। (৩) সহগগুলো long long-এ নিরাপদ, কারণ ৩২ অঙ্কে কোনো সহগ এক লাখও ছাড়ায় না। অনেক বড় ইনপুটে বাস্তব বাস্তবায়ন একটি সীমার (cutoff) নিচে স্কুলের পদ্ধতিতে চলে যায়, কারণ ছোট সংখ্যায় রিকার্শনের বাড়তি খরচ লাভকে খেয়ে ফেলে।
৫ · Strassen — matrix গুণে আটটির বদলে সাতটি
দুটি n×n matrix গুণ করার সাধারণ পদ্ধতিতে তিনটি নেস্টেড লুপ: C[i][j] = Σk A[i][k]·B[k][j], মোট n³টি গুণ। Divide and conquer দিয়ে প্রতিটি matrix-কে চারটি n/2 × n/2 টুকরায় (block) ভাঙা যায়:
এখানে ৮টি অর্ধেক-আকারের matrix গুণ আর ৪টি matrix যোগ (প্রতিটি Θ(n²)): T(n) = 8T(n/2) + Θ(n²) = Θ(nlog2 8) = Θ(n³). আবারও কোনো লাভ নেই — Karatsuba-র মতোই, সরল ভাগে ঘাত বদলায় না। ১৯৬৯ সালে জার্মান গণিতবিদ Volker Strassen “Gaussian elimination is not optimal” প্রবন্ধে দেখান যে ৭টি গুণেই চলে — এটিই Strassen-এর অ্যালগরিদম. তাঁর সাতটি গুণফল:
M1 = (A11 + A22) · (B11 + B22)
M2 = (A21 + A22) · B11
M3 = A11 · (B12 - B22)
M4 = A22 · (B21 - B11)
M5 = (A11 + A12) · B22
M6 = (A21 - A11) · (B11 + B12)
M7 = (A12 - A22) · (B21 + B22)
C11 = M1 + M4 - M5 + M7 C12 = M3 + M5
C21 = M2 + M4 C22 = M1 - M2 + M3 + M6মোট ৭টি গুণ আর ১৮টি যোগ-বিয়োগ (M-এ ১০টি, C-তে ৮টি). Matrix গুণ commutative নয় (AB ≠ BA সাধারণত), আর সূত্রগুলোতে কোথাও গুণের ক্রম উল্টানো হয়নি — তাই Aij নিজেরাই matrix হলেও সূত্র খাটে, আর রিকার্শন চলে। ছোট সংখ্যায় হাতে মিলিয়ে দেখি:
A = | 1 2 | B = | 5 6 |
| 3 4 | | 7 8 |
C = A·B = | 1·5+2·7 1·6+2·8 | = | 19 22 |
| 3·5+4·7 3·6+4·8 | | 43 50 |
M1 = (1 + 4)(5 + 8) = 5 · 13 = 65 M2 = (3 + 4) · 5 = 7 · 5 = 35 M3 = 1 · (6 - 8) = 1 · -2 = -2 M4 = 4 · (7 - 5) = 4 · 2 = 8
M5 = (1 + 2) · 8 = 3 · 8 = 24 M6 = (3 - 1)(5 + 6) = 2 · 11 = 22 M7 = (2 - 4)(7 + 8) = -2 · 15 = -30
C11 = M1 + M4 - M5 + M7 = 65 + 8 - 24 - 30 = 19 C12 = M3 + M5 = -2 + 24 = 22 C21 = M2 + M4 = 35 + 8 = 43 C22 = M1 - M2 + M3 + M6 = 65 - 35 - 2 + 22 = 50
n = 2 : 7 multiplications (plain: 8)
n = 4 : 7^2 = 49 (plain: 64)
n = 1024 : 7^10 ≈ 2.8 · 10^8 (plain: 1024^3 ≈ 10.7 · 10^8)
general : 7^(log2 n) = n^(log2 7) ≈ n^2.807
- লক্ষ্য। সাধারণ নিয়মে প্রতিটি ঘরে দুটি গুণ, মোট ৮টি: ফল [[19 22] [43 50]]. Strassen একই ফল ৭টি গুণে দেবেন — বিনিময়ে বেশি যোগ-বিয়োগ।
- প্রথম চারটি গুণফল। প্রতিটিতে ঠিক একটি গুণ; বাকিটা যোগ-বিয়োগ। M1 = ৬৫, M2 = ৩৫, M3 = −২, M4 = ৮. ঋণাত্মক মান আসা স্বাভাবিক — পরে কেটে যাবে।
- বাকি তিনটি। M5 = ২৪, M6 = ২২, M7 = −৩০. মোট গুণ ৭টি — এখানেই শেষ, আর কোনো গুণ লাগবে না।
- শুধু যোগ-বিয়োগে চারটি ঘর। ১৯, ২২, ৪৩, ৫০ — সাধারণ পদ্ধতির সাথে হুবহু মিলল। সংখ্যার জায়গায় অর্ধেক-আকারের matrix বসালে প্রতিটি “গুণ” একটি রিকার্সিভ ডাক, আর প্রতিটি যোগ Θ(n²).
- লাভটা রিকার্শনে। একটি স্তরে ৮ থেকে ৭ সামান্য মনে হয়, কিন্তু প্রতিটি স্তরে লাভ গুণ হয়: ৪×৪-এ ৪৯ বনাম ৬৪, ১০২৪×১০২৪-এ প্রায় ২৮ কোটি বনাম প্রায় ১০৭ কোটি। Master theorem-এর case 1: Θ(nlog2 7) ≈ Θ(n2.807).
বাস্তবে Strassen — crossover আর সতর্কতা
তত্ত্বে Strassen জেতে, কিন্তু Θ লুকিয়ে রাখে ধ্রুবক। ১৮টি matrix যোগ-বিয়োগ, অস্থায়ী matrix-এর জন্য বাড়তি মেমরি আর cache-এর কম-বান্ধব অ্যাক্সেস — ছোট matrix-এ এগুলোই বেশি খরচ করে। তাই বাস্তব বাস্তবায়ন hybrid: আকার একটি crossover point-এর নিচে নামলেই সাধারণ পদ্ধতি। এই সীমা হার্ডওয়্যার আর বাস্তবায়নের ওপর এতটাই নির্ভর করে যে ভিন্ন গবেষণায় কয়েক ডজন থেকে কয়েক হাজার পর্যন্ত মান পাওয়া গেছে — মেপে ঠিক করতে হয়। দ্বিতীয় সমস্যা দশমিক সংখ্যায় (floating point): বেশি বিয়োগের কারণে rounding ভুল সাধারণ পদ্ধতির চেয়ে কিছুটা বেশি জমতে পারে, তাই বৈজ্ঞানিক গণনার অনেক লাইব্রেরি (BLAS ধাঁচের) মূলত খুব যত্নে অপ্টিমাইজ করা Θ(n³) পদ্ধতিই চালায়। পূর্ণসংখ্যা বা modular matrix-এ এই সমস্যা নেই — সেখানে Strassen বেশি স্বচ্ছন্দ।
Strassen-এর পর শুরু হয় matrix গুণের ঘাত ω কমানোর দীর্ঘ প্রতিযোগিতা — ω হলো সবচেয়ে ছোট সংখ্যা যাতে n×n matrix গুণ O(nω+ε) সময়ে করা যায়। জানা আছে 2 ≤ ω (প্রতিটি ঘর অন্তত একবার লিখতে হয়) আর ω ≤ log2 7 ≈ 2.807 (Strassen). ১৯৯০ সালে Coppersmith ও Winograd ঘাতটিকে প্রায় ২.৩৭৬-এ নামান; ২০২০-এর দশকের গবেষণাপত্রগুলোতে ওপরের সীমা প্রায় ২.৩৭১-এ এসেছে, আর সামান্য উন্নতি এখনো চলছে। ω আসলে ২ কি না — এটি অ্যালগরিদম তত্ত্বের বড় খোলা প্রশ্নগুলোর একটি। তবে এই নতুন অ্যালগরিদমগুলোর ধ্রুবক এত বিশাল যে কোনো বাস্তব আকারে কাজে লাগে না — এদের বলে galactic algorithm.
৬ · Closest pair of points — strip-এর যুক্তি
সমতলে n টি বিন্দু দেওয়া আছে — ধরুন শাহজালাল বিমানবন্দরের রাডারে n টি উড়োজাহাজের অবস্থান, বা একটি শহরের মোবাইল টাওয়ার। কোন দুটি সবচেয়ে কাছাকাছি? এটি closest pair of points সমস্যা। Brute force সব n(n−1)/2 জোড়া দেখে — Θ(n²). এক মাত্রায় (সব বিন্দু একটি সরলরেখায়) উত্তর সহজ: সাজিয়ে পাশাপাশি জোড়াগুলো দেখুন, Θ(n log n). দুই মাত্রায় “পাশাপাশি” বলে কিছু নেই — x-এ কাছের বিন্দু y-এ অনেক দূরে হতে পারে। Michael Shamos ও Dan Hoey ১৯৭৫ সালে divide and conquer দিয়ে Θ(n log n) সমাধান দেন।
ধারণা
- বিন্দুগুলো x অনুযায়ী সাজান। মাঝের বিন্দুর x-এ একটি খাড়া রেখা টেনে বাঁ আর ডান অর্ধেকে ভাগ করুন।
- রিকার্শনে বাঁয়ের সেরা দূরত্ব δL আর ডানের δR বের করুন। δ = min(δL, δR).
- বাকি থাকে পার-হওয়া জোড়া — একটি বাঁয়ে, একটি ডানে। এমন জোড়া δ-এর চেয়ে কাছে হতে হলে দুটি বিন্দুই রেখা থেকে δ-এর কম দূরে থাকবে। তাই শুধু রেখার দুই পাশে δ চওড়া একটি strip-এর বিন্দুগুলো দেখলেই চলে।
- Strip-এর বিন্দুগুলো y অনুযায়ী সাজিয়ে প্রতিটির জন্য শুধু ঠিক আগের যে কয়েকটির y-ব্যবধান δ-এর কম, তাদের সাথে দূরত্ব মাপুন।
কিন্তু সবগুলো বিন্দুই তো strip-এ পড়তে পারে! তাহলে strip-এর ভেতরেও কি Θ(n²)? না — আর এর কারণটিই এই অ্যালগরিদমের প্রাণ।
Strip-এর বিন্দুগুলো y-এর ক্রমে সাজানো থাকলে, যেকোনো বিন্দু q-এর নিচে যত বিন্দুর y-ব্যবধান δ-এর কম, তাদের সংখ্যা বড়জোর ৭ — n যত বড়ই হোক।
প্রমাণ (pigeonhole নীতি): q-এর নিচে y-ব্যবধান < δ এমন সব বিন্দু একটি δ উঁচু আর 2δ চওড়া আয়তক্ষেত্রে পড়ে (রেখার বাঁয়ে δ, ডানে δ). এটিকে ৮টি δ/2 × δ/2 বর্গে ভাগ করুন — রেখার প্রতিটি পাশে ৪টি। একটি বর্গের কর্ণ δ/√2 < δ, তাই একটি বর্গে দুটি বিন্দু থাকলে তাদের দূরত্ব δ-এর কম হতো। কিন্তু প্রতিটি বর্গ পুরোপুরি এক পাশে, আর এক পাশের ভেতরের যেকোনো দুটি বিন্দুর দূরত্ব অন্তত δ (δL, δR-এর সংজ্ঞায়). তাই প্রতিটি বর্গে বড়জোর একটি বিন্দু, আয়তক্ষেত্রে বড়জোর ৮টি — q নিজে তার একটি, বাকি বড়জোর ৭টি। (একই x বা y-এর বিন্দুকে কোন বর্গে ধরা হবে, সেটি সীমারেখায় একটু যত্ন নিয়ে ঠিক করলে যুক্তি একই থাকে। আরও সূক্ষ্ম হিসাবে সংখ্যাটি আরও কমানো যায়, কিন্তু ধ্রুবক হওয়াই আসল কথা।)
৮টি বিন্দু নিয়ে দেখি: A(1,2), B(2,7), C(3,4), D(6,6), E(7,5), F(9,1), G(10,8), H(12,4). নিচের ছবিতে x অক্ষে শেষ তিনটি ঘর ১০, ১১, ১২.
8 . . . . . . .│. . . G . .
7 . . B . . . .│. . . . . .
6 . . . . . . D│. . . . . .
5 . . . . . . .│E . . . . .
4 . . . C . . .│. . . . . H
3 . . . . . . .│. . . . . .
2 . A . . . . .│. . . . . .
1 . . . . . . .│. . F . . .
0 . . . . . . .│. . . . . .
1 1 1
0 1 2 3 4 5 6 7 8 9 0 1 2
A-C : 2^2 + 2^2 = 8 <- best, δL = √8 ≈ 2.83
B-C : 1^2 + 3^2 = 10
C-D : 3^2 + 2^2 = 13
B-D : 4^2 + 1^2 = 17
A-B : 26 , A-D : 41
E-G : 3^2 + 3^2 = 18 <- best, δR = √18 ≈ 4.24
F-H : 3^2 + 3^2 = 18
E-F : 2^2 + 4^2 = 20
G-H : 2^2 + 4^2 = 20
δ = min(δL, δR) = √8 ≈ 2.83
8 . . . . .┆. .│. . .┆o . .
7 . . o . .┆. .│. . .┆. . .
6 . . . . .┆. D│. . .┆. . .
5 . . . . .┆. .│E . .┆. . .
4 . . . o .┆. .│. . .┆. . o
3 . . . . .┆. .│. . .┆. . .
2 . o . . .┆. .│. . .┆. . .
1 . . . . .┆. .│. . F┆. . .
0 . . . . .┆. .│. . .┆. . .
1 1 1
0 1 2 3 4 5 6 7 8 9 0 1 2
strip, in increasing y : F(9,1) E(7,5) D(6,6)
F(9,1) : nothing below -> 0 checks E(7,5) : F dy = 4, 4^2 = 16 >= 8 stop -> 0 checks
D(6,6) : E dy = 1, d^2 = 1 + 1 = 2 < 8 -> new best
F dy = 5, 25 >= 2 stop
answer : D-E, d = √2 ≈ 1.41 (crosses the line)
- ভাগ। x অনুযায়ী সাজালে A B C D বাঁয়ে, E F G H ডানে। কোডে রেখার অবস্থান ডান অর্ধেকের প্রথম বিন্দু E-এর x = ৭ (ছবিতে │ চিহ্নটি E-এর ঠিক বাঁয়ে). প্রতিটি অর্ধেকে ৪টি বিন্দু।
- বাঁ অর্ধেক। রিকার্শন (যা নিজেও আবার ভাগ করে) জানায়: সবচেয়ে কাছের জোড়া A–C, দূরত্বের বর্গ ৮, অর্থাৎ δL = √৮ ≈ ২.৮৩. কোডে বর্গমূল নেওয়াই হয় না — দূরত্বের বর্গ তুলনা করলেই চলে, আর পূর্ণসংখ্যায় কোনো rounding ভুল ঢোকে না।
- ডান অর্ধেক। সেরা E–G বা F–H, দুটিরই বর্গ ১৮, δR ≈ ৪.২৪. তাই এখন পর্যন্ত δ = √৮ ≈ ২.৮৩ — বাঁয়ের A–C.
- Strip. রেখা x = ৭ থেকে δ-এর কম দূরে কারা? (x − ৭)² < ৮ মানে x = ৫ থেকে ৯. শুধু D, E, F — বাকি ৫টি বিন্দু (o চিহ্নিত) কোনো পার-হওয়া জোড়ায় δ-কে হারাতে পারবে না। y-এর ক্রমে: F, E, D.
- স্ক্যান শুরু। F-এর নিচে কেউ নেই। E-এর নিচে F, কিন্তু y-ব্যবধান ৪, যার বর্গ ১৬ ≥ ৮ — এত দূরে যে এর নিচের কাউকেও দেখার দরকার নেই, লুপ থামে। এখানেই strip lemma কাজ করছে: y-ব্যবধান δ পেরোলেই থামা।
- নতুন সেরা। D-এর নিচে E: y-ব্যবধান ১, দূরত্বের বর্গ ১ + ১ = ২ < ৮ — নতুন সেরা! এখন δ² = ২. পরের F-এর y-ব্যবধান ৫, ২৫ ≥ ২ — থামুন। উত্তর D–E, দূরত্ব √২ ≈ ১.৪১ — যা রেখা পার হয়, তাই কোনো অর্ধেকের রিকার্শন একে দেখেনি।
CLOSEST(P) // P sorted by x; returns δ and P re-sorted by y
if |P| ≤ 3: check all pairs; sort P by y; return
mid = |P| / 2; line = P[mid].x
δ = min(CLOSEST(P[0..mid)), CLOSEST(P[mid..|P|)))
merge the two halves by y // Θ(n), as in merge sort
S = points of P with |x - line| < δ, in y order
for each q in S:
for each earlier s in S while q.y - s.y < δ: // at most 7 of them
δ = min(δ, dist(q, s))
return δবিশ্লেষণ — কেন n log n, n log² n নয়
প্রথমে একবার x অনুযায়ী সাজানো: Θ(n log n). তারপর প্রতিটি ডাকে দুটি অর্ধেক-ডাক, Θ(n)-এ y-merge, Θ(n)-এ strip বানানো, আর strip-এ প্রতিটি বিন্দুর জন্য বড়জোর ৭টি তুলনা — মোট Θ(n). তাই T(n) = 2T(n/2) + Θ(n) = Θ(n log n), মোট সময়ও Θ(n log n). জায়গা: merge আর strip-এর জন্য Θ(n). লক্ষ করুন — প্রতিটি ডাকে strip-কে নতুন করে y অনুযায়ী সাজালে combine Θ(n log n) হয়ে যায়, আর T(n) = 2T(n/2) + Θ(n log n) = Θ(n log² n) (পর্ব ৯-এর বর্ধিত case 2). Merge sort-এর মতো রিকার্শনকে y-সাজানো তালিকা ফেরত দিতে বলা — অংশ ৩-এর সেই “সমস্যাকে একটু শক্তিশালী করা” — বাড়তি log-টা সরিয়ে দেয়।
প্রোগ্রামে D&C উত্তরকে brute force-এর সাথে মেলানো হয়েছে দুই সেট বিন্দুতে: ডায়াগ্রামের ৮টি বিন্দু, আর একটি সূত্র দিয়ে বানানো ১০০০টি ভিন্ন বিন্দু (x = i² mod 10007, y = i³ mod 10009) — যাতে দুই ভাষায় হুবহু এক ইনপুট থাকে। সাথে গোনা হয়েছে strip-এ একটি বিন্দুর জন্য সর্বোচ্চ কয়টি দূরত্ব মাপতে হয়েছে।
#include <algorithm>
#include <cstdio>
#include <initializer_list>
#include <utility>
#include <vector>
using namespace std;
using ll = long long;
struct P { ll x, y; };
ll d2(P a, P b) { return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y); }
bool byY(P a, P b) { return a.y < b.y; }
int most = 0; // most checks for one strip point
ll rec(vector<P>& p, int lo, int hi) { // [lo, hi): by x in, by y out
ll best = d2(p[lo], p[lo + 1]);
if (hi - lo <= 3) { // tiny: check every pair
for (int i = lo; i < hi; ++i)
for (int j = i + 1; j < hi; ++j) best = min(best, d2(p[i], p[j]));
stable_sort(p.begin() + lo, p.begin() + hi, byY); return best;
}
int mid = (lo + hi) / 2; ll mx = p[mid].x; // the dividing vertical line
best = min(rec(p, lo, mid), rec(p, mid, hi));
inplace_merge(p.begin() + lo, p.begin() + mid, p.begin() + hi, byY);
vector<P> strip;
for (int i = lo; i < hi; ++i) { // in increasing y
P q = p[i]; int k = 0;
if ((q.x - mx) * (q.x - mx) >= best) continue;
for (int j = int(strip.size()) - 1; j >= 0; --j, ++k) {
if ((q.y - strip[j].y) * (q.y - strip[j].y) >= best) break; // too far below
best = min(best, d2(q, strip[j]));
}
most = max(most, k); strip.push_back(q);
}
return best;
}
int main() {
vector<P> fixed = {{1, 2}, {2, 7}, {3, 4}, {6, 6}, {7, 5}, {9, 1}, {10, 8}, {12, 4}}, big;
for (ll i = 1; i <= 1000; ++i) big.push_back({i * i % 10007, i * i * i % 10009});
for (auto [name, pts] : {pair{"fixed 8", fixed}, pair{"formula 1000", big}}) {
ll brute = d2(pts[0], pts[1]);
for (size_t i = 0; i < pts.size(); ++i)
for (size_t j = i + 1; j < pts.size(); ++j) brute = min(brute, d2(pts[i], pts[j]));
sort(pts.begin(), pts.end(), [](P a, P b) { return a.x != b.x ? a.x < b.x : a.y < b.y; });
most = 0; ll dc = rec(pts, 0, int(pts.size()));
printf("%s: dc d^2 = %lld, brute d^2 = %lld, max strip checks = %d\n", name, dc, brute, most);
}
}শেষ লাইন: formula 1000: dc d^2 = 5, brute d^2 = 5, max strip checks = 2. ব্যাখ্যা নিচে।
from heapq import merge
def d2(p, q): # squared distance, exact integers
return (p[0] - q[0]) ** 2 + (p[1] - q[1]) ** 2
def rec(p, lo, hi, st): # p[lo:hi]: by x in, by y out
if hi - lo <= 3:
best = min(d2(p[i], p[j]) for i in range(lo, hi) for j in range(i + 1, hi))
p[lo:hi] = sorted(p[lo:hi], key=lambda q: q[1])
return best
mid = (lo + hi) // 2
mx = p[mid][0] # the dividing vertical line
best = min(rec(p, lo, mid, st), rec(p, mid, hi, st))
p[lo:hi] = list(merge(p[lo:mid], p[mid:hi], key=lambda q: q[1]))
strip = []
for q in p[lo:hi]: # in increasing y
if (q[0] - mx) ** 2 >= best:
continue
k = 0
for s in reversed(strip):
if (q[1] - s[1]) ** 2 >= best:
break # too far below
best, k = min(best, d2(q, s)), k + 1
st[0] = max(st[0], k) # most checks for one strip point
strip.append(q)
return best
fixed = [(1, 2), (2, 7), (3, 4), (6, 6), (7, 5), (9, 1), (10, 8), (12, 4)]
big = [(i * i % 10007, i * i * i % 10009) for i in range(1, 1001)]
for name, pts in [("fixed 8", fixed), ("formula 1000", big)]:
brute = min(d2(p, q) for i, p in enumerate(pts) for q in pts[i + 1:])
st = [0]
dc = rec(sorted(pts), 0, len(pts), st)
print(f"{name}: dc d^2 = {dc}, brute d^2 = {brute}, max strip checks = {st[0]}")আউটপুট C++-এর সাথে হুবহু এক। heapq.merge দুটি y-সাজানো তালিকা merge sort-এর মতো জোড়া দেয়, আর C++-এর inplace_merge-এর মতোই সমান y-তে বাঁ অংশকে আগে রাখে।
দুই ভাষাতেই আউটপুট:
fixed 8: dc d^2 = 2, brute d^2 = 2, max strip checks = 2 formula 1000: dc d^2 = 5, brute d^2 = 5, max strip checks = 2
৮টি বিন্দুতে উত্তর দূরত্বের বর্গ ২ — ডায়াগ্রামের D–E, আর brute force-ও একই বলছে। ১০০০টি বিন্দুতে দুটি পদ্ধতিই বর্গ ৫ পেয়েছে (দূরত্ব √৫ ≈ ২.২৪); brute force এখানে প্রায় ৫ লাখ জোড়া দেখেছে। সবচেয়ে শিক্ষণীয় সংখ্যাটি max strip checks = 2: পুরো রিকার্শনে কোনো strip-বিন্দুর জন্য ২টির বেশি দূরত্ব মাপতে হয়নি — প্রমাণের সীমা ৭, বাস্তবে আরও কম। কোডের খুঁটিনাটি: (১) রিকার্শনে ঢোকার সময় অংশটি x-এ সাজানো, বেরোনোর সময় y-এ — base case-ও তাই y-এ সাজিয়ে ফেরে। (২) রেখার x (mx) merge-এর আগেই পড়ে রাখা হয়, কারণ merge-এর পর p[mid] আর মাঝের বিন্দু নয় — এটি খুব প্রচলিত একটি bug. (৩) ভেতরের লুপ y-ব্যবধান δ পেরোলেই break করে; এটিই strip lemma-কে কোডে রূপ দেয়। এই break বাদ দিলে উত্তর ঠিকই থাকবে, কিন্তু worst case Θ(n²) হয়ে যাবে।
৭ · Linear-time selection, আর কখন divide and conquer কাজ করে না
k-তম ছোট — median of medians
পর্ব ২২-এ quickselect দেখেছেন: একটি pivot দিয়ে partition, তারপর শুধু যে দিকে k-তম আছে সেদিকে রিকার্শন — গড়ে Θ(n), কিন্তু pivot বারবার খারাপ হলে Θ(n²). ১৯৭৩ সালে Blum, Floyd, Pratt, Rivest ও Tarjan-এর median of medians pivot-কে নিশ্চিতভাবে মাঝামাঝি করে: উপাদানগুলোকে ৫টি করে দলে ভাগ, প্রতিটি দলের median, তারপর সেই ⌈n/5⌉টি median-এর median রিকার্শনে বের করে pivot. এমন pivot-এর চেয়ে ছোট অন্তত প্রায় ৩n/10টি আর বড় অন্তত প্রায় ৩n/10টি উপাদান থাকে, তাই পরের ধাপে বড়জোর প্রায় 7n/10. Recurrence:
এটি অসমান ভাগ, master theorem সরাসরি খাটে না। কিন্তু recursion tree-তে প্রতিটি তলার মোট আকার আগের তলার 1/5 + 7/10 = 9/10 গুণ — কমতে থাকা জ্যামিতিক ধারা, যোগফল n·(1 + 0.9 + 0.81 + …) = 10n. তাই Θ(n), worst case-এও। ৫-এর বদলে ৩ করে দল বানালে ভাগ হয় 1/3 + 2/3 = 1 — ধারা আর কমে না, ফল Θ(n log n). দলের আকার বাছাইয়ের পেছনে এই হিসাবটিই কাজ করে। বিস্তারিত প্রমাণ পর্ব ২২-এ আছে; বাস্তবে ধ্রুবক বড় বলে randomized quickselect-ই বেশি চলে।
যখন উপ-সমস্যাগুলো মিলে যায়
Fibonacci সংখ্যার সরাসরি রিকার্সিভ সংজ্ঞাটি দেখতে নিখুঁত divide and conquer: F(n) = F(n−1) + F(n−2). কিন্তু recursion tree আঁকুন — F(n−2) একবার সরাসরি ডাকা হয়, আবার F(n−1)-এর ভেতর থেকেও। F(n−3) তিনবার, F(n−4) পাঁচবার… মোট ডাক প্রায় φn (φ ≈ 1.618) — exponential. এখানে overlapping subproblems: উপ-সমস্যাগুলো স্বাধীন নয়, একই প্রশ্ন বারবার আসে। সমাধান সহজ — একবার সমাধান করে মনে রাখুন (memoization), বা ছোট থেকে বড় টেবিল ভরুন। এটিই dynamic programming, পর্ব ৫২-এর বিষয়; তখন F(n) লাগে Θ(n) সময়।
উপ-সমস্যা আলাদা
Merge sort-এর দুই অর্ধেক, Karatsuba-র তিন গুণ — কেউ কারও কাজ করে না। Divide and conquer নিখুঁত।
D&Cউপ-সমস্যা মিলে যায়
Fibonacci, সব জোড়া-ভাগের সেরা ক্রম (matrix chain) — একই প্রশ্ন বহুবার। মনে রাখলেই exponential থেকে polynomial.
DP · পর্ব ৫২ভাগ অসম বা জোড়া দামি
T(n−1)-ধাঁচে কমলে বা combine Θ(n²) হলে লাভ হারায়। আগে recurrence লিখে দেখুন।
হিসাব আগেআরও দুটি ফাঁদ। এক: ভাগ খুব অসম হলে — যেমন সবসময় ১ আর n−1 — গভীরতা n হয়ে যায়; quick sort-এর worst case ঠিক এটি (পর্ব ২২). দুই: combine যদি উপ-সমস্যার সংখ্যার চেয়ে দ্রুত বাড়ে, তাহলে ভাগ করে কিছু বাঁচে না — যেমন অংশ ৫-এর শুরুর 8T(n/2) + n², যা সাধারণ Θ(n³)-ই রয়ে গেল। কোনো divide and conquer লেখার আগে recurrence লিখুন আর master theorem দিয়ে দেখে নিন — লাভ আছে কি না, কাগজেই বোঝা যায়।
আরও গভীরে: Divide and conquer-এর সবচেয়ে প্রভাবশালী রূপ সম্ভবত Fast Fourier Transform (FFT). দুটি n-ডিগ্রির বহুপদী রাশি গুণ করতে সরলভাবে Θ(n²) লাগে; FFT রাশিগুলোকে বিশেষ কিছু বিন্দুতে (জটিল সংখ্যার roots of unity) মান বের করে, বিন্দুতে বিন্দুতে গুণ করে, তারপর আবার সহগে ফেরায় — জোড় আর বিজোড় ঘাতের পদে ভাগ করে T(n) = 2T(n/2) + Θ(n) = Θ(n log n). James Cooley ও John Tukey ১৯৬৫ সালে এটি জনপ্রিয় করেন (মূল ভাবটি Gauss-এর অপ্রকাশিত লেখাতেও ছিল). বড় সংখ্যার গুণ আসলে বহুপদী গুণই — তাই FFT-ভিত্তিক Schönhage–Strassen (১৯৭১) থেকে শুরু করে ২০১৯ সালে Harvey ও van der Hoeven-এর O(n log n) পূর্ণসংখ্যা-গুণ পর্যন্ত Karatsuba-র গল্পটি চলেছে। কনটেস্টে FFT বা তার modular রূপ NTT দিয়ে বড় convolution — যেমন দুটি তালিকা থেকে একটি করে সংখ্যা নিলে প্রতিটি যোগফল কতভাবে আসে, তা গোনা — Θ(n log n)-এ হয়।
পরীক্ষা ও ইন্টারভিউতে
- “Divide and conquer-এর তিনটি ধাপ কী? একটি উদাহরণ দিন।” — Divide, conquer, combine; merge sort-এ মাঝ বরাবর ভাগ, দুই অর্ধেক সাজানো, তারপর Θ(n)-এ merge.
- “Karatsuba-র recurrence আর complexity লিখুন।” — T(n) = 3T(n/2) + Θ(n) → Θ(nlog2 3) ≈ Θ(n1.585); মাঝের পদ (x₁+x₀)(y₁+y₀) − z₂ − z₀.
- “Strassen কয়টি গুণ করে, complexity কত?” — ৮-এর বদলে ৭টি block গুণ, ১৮টি যোগ-বিয়োগ; Θ(nlog2 7) ≈ Θ(n2.81).
- “Closest pair-এ strip-এর প্রতিটি বিন্দুর জন্য কয়টি বিন্দু দেখতে হয়, কেন?” — ধ্রুবক সংখ্যক (বড়জোর ৭); δ × 2δ আয়তক্ষেত্রকে ৮টি δ/2 বর্গে ভাগ করে pigeonhole যুক্তি।
- “Maximum subarray O(n log n) D&C-তে কোন ক্ষেত্রটি সবচেয়ে বেশি ভুল হয়?” — মাঝ-পার অংশ: বাঁয়ের সেরা suffix (a[mid−1] বাধ্যতামূলক) + ডানের সেরা prefix (a[mid] বাধ্যতামূলক).
৮ · AI-এর যুগে divide and conquer
Divide and conquer কোড AI খুব সহজে লিখে দেয় — দেখতেও সুন্দর লাগে। কিন্তু এই কোডের ভুলগুলো সাধারণত ছোট ইনপুটে ধরা পড়ে না: অসীম রিকার্শন বড় ইনপুটে, ভুল complexity শুধু বড় n-এ, আর মাঝ-পার ক্ষেত্রের ভুল শুধু বিশেষ বিন্যাসে। যেসব ভুল বারবার দেখা যায়:
| AI-এর ভুল | ফল | কীভাবে ধরবেন |
|---|---|---|
| Closest pair-এ প্রতিটি ডাকে strip আবার sort করে বলে “O(n log n)” | আসলে O(n log² n) | Combine-এর খরচ লিখে recurrence বানান; y-merge আছে কি না দেখুন |
Strip-এর ভেতরে y-ব্যবধানে break নেই | উত্তর ঠিক, কিন্তু worst case Θ(n²) | সব বিন্দু একই x-এ এমন ইনপুটে সময় মাপুন |
Merge-এর পরে p[mid].x পড়া | ভুল রেখা, মাঝে মাঝে ভুল উত্তর | Brute force-এর সাথে কয়েক হাজার এলোমেলো ইনপুটে stress test |
| Maximum subarray-এর মাঝ-পারে suffix/prefix খালি রাখার সুযোগ | সব ঋণাত্মক অ্যারেতে ভুল উত্তর (যেমন 0) | [−3, −1, −2]-এর মতো edge case চালান |
| Base case-এ hi − lo = 1 না ধরা, বা mid = lo হয়ে যাওয়া | অসীম রিকার্শন, stack overflow | আকার ০, ১, ২-এর ইনপুট হাতে trace করুন |
| “Karatsuba O(n log n)” বা “Strassen সবসময় দ্রুত” | ভুল দাবি | Recurrence আর master theorem নিজে মেলান; ছোট n-এ সময় মাপুন |
AI-কে কোডের সাথে বিশ্লেষণ আর পরীক্ষাও লিখতে বলুন — তাহলে নিজেই যাচাই করতে পারবেন:
AI-এর দেওয়া divide and conquer কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Kolmogorov-এর অনুমান ভুল প্রমাণ হওয়ার পর গুণের গল্প কোথায় গেল? আজকের কম্পিউটার আসলে কোন পদ্ধতিতে গুণ করে?
Karatsuba-র ফল একটি দরজা খুলে দেয়: যদি দুই ভাগে ভেঙে ৪টির বদলে ৩টি গুণ চলে, তাহলে তিন ভাগে ভেঙে কী হয়? Andrei Toom (১৯৬৩) আর পরে Stephen Cook (১৯৬৬) দেখান, সংখ্যাকে k ভাগে ভাঙলে 2k − 1টি গুণ যথেষ্ট — কৌশলটি হলো সংখ্যাকে একটি বহুপদী রাশি ভেবে কয়েকটি বিন্দুতে তার মান বের করা, বিন্দুতে বিন্দুতে গুণ করা, তারপর interpolation দিয়ে সহগ ফেরত আনা। এই Toom–Cook পদ্ধতিতে k = 3 হলে ৫টি গুণ, ঘাত log3 5 ≈ ১.৪৬৫. Karatsuba আসলে এর k = 2 রূপ।
k বাড়ালে ঘাত ১-এর দিকে যায়, কিন্তু interpolation-এর খরচ বাড়ে। চূড়ান্ত পদক্ষেপ আসে FFT থেকে: ১৯৭১ সালে Arnold Schönhage ও Volker Strassen (হ্যাঁ, সেই Strassen) O(n log n log log n) অ্যালগরিদম দেন, আর অনুমান করেন যে Θ(n log n)-ই আসল সীমা। প্রায় ৫০ বছর পর, ২০১৯ সালে David Harvey ও Joris van der Hoeven O(n log n) অ্যালগরিদম প্রকাশ করেন — যদিও সেটি এত বিশাল সংখ্যায় লাভজনক যে বাস্তবে কেউ ব্যবহার করে না। n log n-এর চেয়ে দ্রুত সম্ভব কি না, তা এখনো প্রমাণিত নয়।
বাস্তবের ছবি স্তরে স্তরে সাজানো। GMP-এর মতো বড়-সংখ্যার লাইব্রেরি আকার মেপে পদ্ধতি বদলায়: কয়েক word পর্যন্ত স্কুলের পদ্ধতি, তারপর Karatsuba, তারপর Toom-এর কয়েকটি রূপ, আর খুব বড় সংখ্যায় FFT-ভিত্তিক গুণ। CPython-এর int একটি নির্দিষ্ট আকারের ওপরে Karatsuba ব্যবহার করে। প্রতিটি সীমা (threshold) তত্ত্ব থেকে নয়, মেপে ঠিক করা — আর অনেক লাইব্রেরি প্রসেসর অনুযায়ী আলাদা মান রাখে। আপনার ফোনে HTTPS সংযোগ খোলার সময় RSA বা অনুরূপ গণনায় শত শত অঙ্কের সংখ্যা গুণ হয় — সেখানে এই কৌশলগুলোই কাজ করছে।
মূল উপলব্ধি: একটি “সবাই জানে” অনুমান ভাঙলে প্রায়ই একটি পুরো গবেষণা-ধারা জন্ম নেয়। আর বাস্তব প্রকৌশলে একটিমাত্র “সেরা” অ্যালগরিদম থাকে না — আকার অনুযায়ী সঠিকটি বেছে নেওয়াই দক্ষতা।
প্র ০২2×2 matrix গুণে ৭টির কম গুণ কি সম্ভব? আর বড় block-এ গুণ কমানোর প্রতিযোগিতায় AI কী ভূমিকা রেখেছে?
Strassen-এর ৭টি গুণ কি আরও কমানো যায়? ১৯৭১ সালে প্রমাণিত হয় যে 2×2 matrix গুণে (যেখানে গুণ commutative ধরা হয় না, তাই রিকার্শনে বসানো যায়) ৭টির কম গুণে হয় না — Hopcroft ও Kerr, আর স্বাধীনভাবে Winograd এই নিম্নসীমা দেখান। Winograd আরও একটি রূপ দেন যেখানে গুণ ৭টিই কিন্তু যোগ-বিয়োগ ১৮ থেকে কমে ১৫টি — বাস্তব বাস্তবায়নে সেটি জনপ্রিয়।
তাহলে ঘাত কমানোর পথ বড় block: 3×3 matrix k টি গুণে করা গেলে রিকার্শনে ঘাত log3 k. Strassen-কে হারাতে k < 32.807 ≈ ২১.৮ লাগবে। ১৯৭৬ সালে Julian Laderman ২৩টি গুণের একটি পদ্ধতি দেন (log3 23 ≈ ২.৮৫৪ — Strassen-এর চেয়ে খারাপ), আর 3×3-এ ২৩-এর কম পাওয়ার চেষ্টা এখনো সফল হয়নি বলে জানা যায়। ω-এর বড় অগ্রগতিগুলো তাই এসেছে আরও বিমূর্ত পথে — Pan, Bini, Schönhage, Coppersmith–Winograd থেকে শুরু করে ২০২০-এর দশকের laser method-এর উন্নত বিশ্লেষণ।
২০২২ সালে DeepMind-এর AlphaTensor নামের একটি reinforcement learning ব্যবস্থা ছোট matrix গুণের নতুন সূত্র খোঁজাকে একটি খেলার মতো সাজিয়ে কিছু নতুন সূত্র খুঁজে পায় — যেমন শুধু 0 আর 1-এর (mod 2) গাণিতিক জগতে 4×4 matrix গুণ ৪৭টি গুণে, যেখানে Strassen দুই স্তরে দিত ৪৯টি। এটি AI দিয়ে নতুন অ্যালগরিদম আবিষ্কারের একটি নজরকাড়া উদাহরণ। তবে লক্ষ করুন: AI প্রস্তাব করেছে, কিন্তু প্রতিটি সূত্র গাণিতিকভাবে যাচাই করা হয়েছে — সূত্রটি সঠিক কি না, তা সব সম্ভাব্য ইনপুটে বীজগাণিতিক পরিচয় (identity) মিলিয়ে নিশ্চিত করা যায়।
মূল উপলব্ধি: অ্যালগরিদমের উন্নতি দুই রকম — ছোট ভিত্তি-সূত্রে গুণ কমানো (Strassen, AlphaTensor), আর পুরো কাঠামোর নতুন বিশ্লেষণ (ω-এর তত্ত্ব). দুই ক্ষেত্রেই প্রমাণ আর যাচাই ছাড়া কোনো দাবির মূল্য নেই — AI-এর আবিষ্কারের ক্ষেত্রেও।
প্র ০৩Closest pair-এ n log n-এর চেয়ে দ্রুত কি সম্ভব? নিম্নসীমা আর randomized পদ্ধতি কী বলে?
Shamos ও Hoey-র ১৯৭৫ সালের কাজের একটি গুরুত্বপূর্ণ অংশ ছিল নিম্নসীমা: তুলনা আর বীজগাণিতিক হিসাবের একটি সাধারণ মডেলে (algebraic decision tree) closest pair-এ Ω(n log n) সময় লাগবেই। যুক্তিটি একটি reduction: “n টি সংখ্যার মধ্যে কোনো দুটি সমান কি না” (element uniqueness) সমস্যার জন্য Ω(n log n) নিম্নসীমা জানা। এখন সংখ্যাগুলোকে x-অক্ষের বিন্দু ধরুন — closest pair-এর দূরত্ব ০ হলেই কোনো দুটি সমান। তাই closest pair দ্রুততর হলে element uniqueness-ও দ্রুততর হতো। এই মডেলে divide and conquer অ্যালগরিদম তাই asymptotically সেরা।
কিন্তু মডেলের বাইরে গেলে? ১৯৭৬ সালে Michael Rabin একটি randomized অ্যালগরিদম দেন যা প্রত্যাশিত O(n) সময়ে চলে। এর মূল হাতিয়ার floor ফাংশন আর hashing — যা algebraic decision tree মডেলে নেই। সরল রূপে ভাবুন: সমতলকে δ মাপের grid-এ ভাগ করুন, প্রতিটি বিন্দুকে ⌊x/δ⌋, ⌊y/δ⌋ ঘরে hash table-এ রাখুন, আর শুধু পাশের ৯টি ঘর দেখুন। পরে Khuller ও Matias (১৯৯৫) আরও সরল একটি randomized পদ্ধতি দেন। এলোমেলো ক্রমে বিন্দু যোগ করে δ কমলে grid নতুন করে বানানোর কৌশলও প্রত্যাশিত O(n) দেয় — পর্ব ৬১-এ এ ধরনের randomized বিশ্লেষণের ভিত্তি দেখবেন।
বাস্তব ব্যবহারে: বিমান চলাচল নিয়ন্ত্রণে সংঘর্ষের ঝুঁকি, মানচিত্রে কাছাকাছি স্থাপনা খোঁজা, clustering-এ সবচেয়ে কাছের দুটি দল জোড়া দেওয়া, VLSI চিপ নকশায় দুটি তারের ন্যূনতম দূরত্ব যাচাই — সবখানে এই সমস্যার রূপ আছে। আর পুরো পরিবারটির আধুনিক রূপ হলো spatial data structure (k-d tree, grid, quadtree), যা পর্ব ৬৩-এর computational geometry-র সাথে সম্পর্কিত।
মূল উপলব্ধি: নিম্নসীমা সবসময় একটি নির্দিষ্ট গণনা-মডেলের ভেতরে সত্য। মডেলের অনুমতির বাইরে (hashing, floor, এলোমেলোতা) গেলে সীমাও বদলে যেতে পারে — তাই “এর চেয়ে দ্রুত অসম্ভব” কথাটি শুনলে জিজ্ঞেস করুন, কোন মডেলে?
প্র ০৪Divide and conquer আর dynamic programming — দুটোই তো সমস্যা ভাঙে। আসল পার্থক্য কোথায়, আর কখন একটি থেকে অন্যটিতে যাবেন?
দুটি কৌশলই বড় সমস্যাকে ছোট সমস্যার উত্তর দিয়ে লেখে। পার্থক্য উপ-সমস্যার গঠনে. Divide and conquer-এ উপ-সমস্যাগুলো সাধারণত disjoint — merge sort-এর দুই অর্ধেকের কোনো উপাদান মিলে না, তাই একটি উপ-সমস্যা একবারই সমাধান হয়। Dynamic programming-এ উপ-সমস্যাগুলো overlap করে: Fibonacci-তে F(n−1) আর F(n−2) দুটোরই ভেতরে F(n−3) লাগে। রিকার্শন গাছে n টি আলাদা প্রশ্ন, কিন্তু প্রায় φnটি ডাক।
১৯৫০-এর দশকে Richard Bellman এই পর্যবেক্ষণ থেকেই dynamic programming গড়ে তোলেন — সাথে optimal substructure-এর ধারণা: সেরা সমাধানের অংশগুলো নিজেরাও সেরা। Maximum subarray দুটিরই সুন্দর উদাহরণ: “বাঁ + ডান + মাঝ-পার” divide and conquer দেয় Θ(n log n), আর “এখানে শেষ হওয়া সেরা যোগফল” DP (Kadane) দেয় Θ(n). একই সমস্যা, দুই রকম ভাঙন।
একটি ব্যবহারিক নিয়ম: রিকার্সিভ সমাধান লিখুন, তারপর recursion tree আঁকুন। একই আর্গুমেন্টের ডাক বারবার এলে memoization যোগ করুন — top-down DP তৈরি। আলাদা আর্গুমেন্টের সংখ্যা যদি মোট ডাকের তুলনায় অনেক কম হয়, লাভ বিশাল। উল্টো দিকেও যাওয়া যায়: কিছু DP-কে divide and conquer দিয়ে দ্রুত করা যায় — যেমন কনটেস্টে পরিচিত “divide and conquer DP optimization”, যেখানে সেরা ভাগের বিন্দু একমুখীভাবে সরে (monotone) বলে একটি স্তরের সব মান রিকার্শনে ভাগ করে হিসাব হয়। আরেকটি পার্থক্য সমান্তরালতায়: স্বাধীন উপ-সমস্যা বলে divide and conquer স্বাভাবিকভাবেই একাধিক core-এ ভাগ করা যায় (fork-join), DP-র নির্ভরতা সেখানে বাধা হতে পারে।
মূল উপলব্ধি: প্রশ্নটি “ভাঙব কি না” নয়, “ভাঙা টুকরোগুলো কি একে অপরের সাথে মিলে যায়?” — মিললে মনে রাখুন (DP), না মিললে স্বাধীনভাবে সমাধান করে জোড়া দিন (D&C).
প্র ০৫আধুনিক হার্ডওয়্যারে divide and conquer কেন আরও গুরুত্বপূর্ণ — cache, একাধিক core আর বিশাল ডেটার দিক থেকে?
পর্ব ১০-এ দেখেছেন, আধুনিক CPU-তে গণনার চেয়ে মেমরি থেকে ডেটা আনা অনেক ধীর, আর cache ছোট কিন্তু দ্রুত। Divide and conquer এখানে একটি অপ্রত্যাশিত সুবিধা দেয়: সমস্যা ভাঙতে ভাঙতে একসময় উপ-সমস্যাটি cache-এ পুরোপুরি এঁটে যায়, আর তখন তার সব কাজ দ্রুত মেমরিতেই হয় — cache-এর আকার না জেনেই। ১৯৯৯ সালে Frigo, Leiserson, Prokop ও Ramachandran এই ধারণাকে cache-oblivious অ্যালগরিদম নামে তত্ত্বে দাঁড় করান; রিকার্সিভ matrix গুণ আর রিকার্সিভ matrix transpose এর ক্লাসিক উদাহরণ। সাধারণ তিন-লুপের গুণে বড় matrix-এ cache বারবার খালি হয়; block বা রিকার্সিভ রূপে একই Θ(n³) কাজ অনেক দ্রুত চলে।
দ্বিতীয় সুবিধা সমান্তরালতা। উপ-সমস্যাগুলো স্বাধীন, তাই প্রতিটি আলাদা core-এ চালানো যায় — MIT-এর Cilk ভাষা আর Java-র ForkJoin framework এই “fork করো, তারপর join করো” ধাঁচের ওপর গড়া। Merge sort-এর দুই অর্ধেক দুই core-এ, তারপর merge — অংশ ১-এর পরীক্ষার খাতার উদাহরণটাই, শুধু শিক্ষকের জায়গায় core.
তৃতীয়, বিশাল ডেটা। ২০০৪ সালে Google-এর Jeffrey Dean ও Sanjay Ghemawat MapReduce প্রকাশ করেন: হাজারো কম্পিউটারে ডেটা ভাগ করে প্রতিটিতে একই কাজ (map), তারপর ফলগুলো জোড়া (reduce). এটি অনেকটা এক স্তরের divide and conquer — আর reduce ধাপ সহজ রাখতে combine-কে associative হতে হয় (যেমন যোগ, সর্বোচ্চ), ঠিক যেমন অংশ ১-এর “সর্বোচ্চ নম্বর” জোড়া দেওয়া সহজ ছিল কিন্তু “সবচেয়ে কাছাকাছি দুজন” কঠিন। Hadoop, Spark-এর মতো ব্যবস্থাগুলো এই ধারা থেকেই এসেছে।
মূল উপলব্ধি: Divide and conquer শুধু operation গোনার কৌশল নয় — এটি ডেটাকে ছোট, স্বাধীন, কাছাকাছি টুকরায় সাজানোর কৌশল। Cache, core আর cluster — তিন জায়গাতেই ঠিক এই গুণটিই গতি আনে।
অনুশীলন
খাতা-কলম নিয়ে বসুন। প্রথম দুটি হাতে trace, শেষটি কোড লেখা।
-
হাতে trace করুন:
[3, −4, 5, −1, 2, −6, 4, 1]-এ divide and conquer maximum subarray চালান। শীর্ষ স্তরে বাঁয়ের উত্তর, ডানের উত্তর, সেরা suffix, সেরা prefix আর চূড়ান্ত উত্তর লিখুন।বাঁ অর্ধেক [3, −4, 5, −1]: ভেতরে [3, −4]-এর সেরা ৩, [5, −1]-এর সেরা ৫, মাঝ-পার = সেরা suffix (−৪, −১ থেকে −১) + সেরা prefix (৫, ৪ থেকে ৫) = ৪ → বাঁয়ের উত্তর ৫. ডান অর্ধেক [2, −6, 4, 1]: [2, −6]-এর সেরা ২, [4, 1]-এর সেরা ৫, মাঝ-পার = সেরা suffix (−৬, −৪ থেকে −৪) + সেরা prefix (৪, ৫ থেকে ৫) = ১ → ডানের উত্তর ৫. শীর্ষ স্তরের মাঝ-পার: ঘর ৩ থেকে বাঁয়ে suffix যোগফল −১, ৪, ০, ৩ → সেরা ৪; ঘর ৪ থেকে ডানে prefix যোগফল ২, −৪, ০, ১ → সেরা ২; মাঝ-পার ৬. উত্তর max(৫, ৫, ৬) = ৬, অংশ [5, −1, 2].
-
হাতে trace করুন: Karatsuba দিয়ে ৪৭ × ৮৩ এক স্তরে (B = ১০) হিসাব করুন — z₂, z₀, যোগফলের গুণ আর z₁ লিখুন। তারপর বলুন: ৮ অঙ্কের দুটি সংখ্যায় (পূর্ণ রিকার্শনে) কয়টি এক-অঙ্কের গুণ লাগবে, আর স্কুলের পদ্ধতিতে কয়টি?
x₁ = ৪, x₀ = ৭, y₁ = ৮, y₀ = ৩. z₂ = ৪ × ৮ = ৩২, z₀ = ৭ × ৩ = ২১, (৪ + ৭)(৮ + ৩) = ১১ × ১১ = ১২১, z₁ = ১২১ − ৩২ − ২১ = ৬৮. ফল ৩২ × ১০০ + ৬৮ × ১০ + ২১ = ৩২০০ + ৬৮০ + ২১ = ৩৯০১ (যাচাই: ৪৭ × ৮৩ = ৩৯০১). ৮ = 23 অঙ্কে Karatsuba 33 = ২৭টি, স্কুলের পদ্ধতি 8² = ৬৪টি এক-অঙ্কের গুণ।
-
বিশ্লেষণ: ধরুন কেউ 3×3 matrix গুণের এমন একটি সূত্র পেলেন যাতে k টি গুণ লাগে (গুণের ক্রম না উল্টিয়ে, তাই রিকার্শনে বসানো যায়). (ক) রিকার্শনে বসালে recurrence আর complexity কত? (খ) Strassen-কে হারাতে k সর্বোচ্চ কত হতে পারে? (গ) Karatsuba-র অনুরূপ প্রশ্ন: সংখ্যাকে ৩ ভাগে ভেঙে ৫টি গুণে করা গেলে complexity কত?
(ক) T(n) = kT(n/3) + Θ(n²). k > 9 হলে case 1: Θ(nlog3 k). (খ) log3 k < log2 7 ≈ ২.৮০৭ চাই, অর্থাৎ k < 32.807 ≈ ২১.৮ → k ≤ ২১. (জানা সেরা সূত্র Laderman-এর ২৩টি গুণ, যা Strassen-কে হারায় না।) (গ) T(n) = 5T(n/3) + Θ(n) → Θ(nlog3 5) ≈ Θ(n1.465) — এটি Toom-3, Karatsuba-র চেয়ে ভালো ঘাত।
-
কোড লিখুন: C++-এ divide and conquer maximum subarray লিখুন (অংশ ২-এর pseudocode অনুযায়ী) আর Kadane-এর সাথে ২০০টি এলোমেলো অ্যারেতে মিলিয়ে দেখুন — অ্যারেতে ঋণাত্মক সংখ্যাও থাকবে।
long long maxsub(const vector<long long>& a, int lo, int hi) { // a[lo..hi), hi - lo >= 1 if (hi - lo == 1) return a[lo]; int mid = (lo + hi) / 2; long long best = max(maxsub(a, lo, mid), maxsub(a, mid, hi)); long long s = 0, bl = LLONG_MIN, br = LLONG_MIN; for (int i = mid - 1; i >= lo; --i) s += a[i], bl = max(bl, s); // best suffix s = 0; for (int j = mid; j < hi; ++j) s += a[j], br = max(br, s); // best prefix return max(best, bl + br); } long long kadane(const vector<long long>& a) { long long cur = a[0], best = a[0]; for (size_t i = 1; i < a.size(); ++i) cur = max(a[i], cur + a[i]), best = max(best, cur); return best; } // test: for 200 rounds build a random vector (size 1..30, values -50..50), // assert(maxsub(a, 0, a.size()) == kadane(a));খেয়াল করুন:
blআরbrশুরু হয়LLONG_MINদিয়ে, ০ দিয়ে নয় — তাহলে সব-ঋণাত্মক অ্যারেতেও মাঝ-পার অংশে অন্তত একটি করে ঘর থাকে। লুপ অন্তত একবার চলে (দুই অর্ধেকই খালি নয়), তাইbl + br-এ overflow হয় না।<climits>আর<algorithm>include করতে ভুলবেন না। সময় Θ(n log n), Kadane Θ(n).
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমDivide and conquer closest pair অ্যালগরিদমের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- বিন্দুগুলো x অনুযায়ী একবার সাজানো
- মাঝের বিন্দুর x-এ খাড়া রেখা টেনে দুই অর্ধেকে ভাগ
- দুই অর্ধেকে রিকার্শন চালিয়ে δ = min(δL, δR)
- রেখা থেকে δ-এর কম দূরের বিন্দু নিয়ে y-ক্রমে strip বানানো
- Strip-এ প্রতিটি বিন্দুকে y-ব্যবধান δ-এর কম এমন আগের কয়েকটির সাথে মেলানো
দুটি n-অঙ্কের সংখ্যার Karatsuba গুণের সময়-জটিলতা কত?
- Θ(n²)
- Θ(n log n)
- Θ(nlog2 3) ≈ Θ(n1.585)
- Θ(n1.5)
Strassen-এর পদ্ধতিতে দুটি n×n matrix-কে চারটি করে block-এ ভাঙলে প্রতিটি স্তরে কয়টি অর্ধেক-আকারের matrix গুণ লাগে?
- ৮টি
- ৭টি
- ৬টি
- ৪টি
Divide and conquer closest pair-এ strip-এর বিন্দুগুলো y-ক্রমে সাজানো থাকলে প্রতিটি বিন্দুর জন্য কয়টি বিন্দুর সাথে দূরত্ব মাপতে হয়?
- Strip-এর সব বিন্দুর সাথে
- প্রায় log n টি
- প্রায় √n টি
- বড়জোর একটি ধ্রুবক সংখ্যা (৭টি)
মাঝ বরাবর ভাগ করে maximum subarray খোঁজার divide and conquer অ্যালগরিদমের সময়-জটিলতা কত?
- Θ(n log n)
- Θ(n)
- Θ(n²)
- Θ(log n)
নিচের কোনটিতে সাধারণ (memoization ছাড়া) divide and conquer exponential সময় নেয়, কারণ একই উপ-সমস্যা বারবার সমাধান হয়?
- Merge sort
- Binary search
- রিকার্সিভ সংজ্ঞা থেকে সরাসরি Fibonacci-র n-তম সংখ্যা
- Karatsuba গুণ
xe বের করতে pow(x, e) = pow(x, e/2)² (e জোড় হলে) পদ্ধতিতে কয়টি গুণ লাগে, যদি pow(x, e/2) একবার হিসাব করে ভ্যারিয়েবলে রাখা হয়?
- Θ(e)
- Θ(log e)
- Θ(√e)
- Θ(1)
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Divide, conquer, combine — আর recurrence T(n) = aT(n/b) + f(n) লিখে master theorem দিয়ে আগেই লাভ যাচাই।
- উত্তর প্রায়ই মাঝ-পার অংশে লুকিয়ে থাকে; combine ধাপকে সেটি খুঁজতে হয় — maximum subarray-এ suffix + prefix, closest pair-এ strip.
- Case 1-এ উপ-সমস্যার সংখ্যা কমালেই ঘাত কমে: Karatsuba ৪→৩ (n1.585), Strassen ৮→৭ (n2.807).
- রিকার্শনকে বাড়তি তথ্য (যেমন সাজানো ক্রম) ফেরত দিতে বললে combine সস্তা হয় — inversion গোনা, closest pair-এর y-merge.
- উপ-সমস্যা overlap করলে (Fibonacci) memoization বা DP; ভাগ অসম বা combine দামি হলে লাভ হারায়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫০ · Greedy অ্যালগরিদম আর exchange argumentপরবর্তী পর্বদ্বিতীয় ডিজাইন কৌশল: প্রতিটি ধাপে লোভী সিদ্ধান্ত, প্রমাণসহ।
- পর্ব ০৯ · Master theorem — divide and conquer-এর হিসাব এক লাইনেআগের ভিত্তিএই পর্বের সব recurrence এক লাইনে সমাধানের যন্ত্র।
- পর্ব ২১ · Merge sort — ভাগ করা, সাজানো, জোড়া দেওয়াআগের ভিত্তিDivide and conquer-এর আদর্শ উদাহরণ, inversion গোনাসহ।
- পর্ব ২২ · Quick sort, pivot আর quickselectআগের ভিত্তিMedian of medians-এর পূর্ণ প্রমাণ এখানে।
- ডেটা স্ট্রাকচার ও অ্যালগরিদম · কোর্স হোম৭২টি পর্বপুরো কোর্সের রোডম্যাপ — ৯টি মডিউল।
- C++ প্রোগ্রামিং · STL algorithmsপাশের ট্র্যাক
sort,inplace_merge,nth_element— divide and conquer-এর তৈরি হাতিয়ার। - Python প্রোগ্রামিংপাশের ট্র্যাকবড় সংখ্যা আর দ্রুত যাচাইয়ের জন্য Python.