এই পর্বে যা শিখবেন
- DSA-র লিখিত উত্তর কোন ক্রমে সাজালে পরীক্ষক প্রতিটি নম্বর-ভাগ খুঁজে পান
- পরীক্ষায় বারবার আসা প্রমাণগুলো — invariant, induction, exchange, contradiction, cut আর reduction — গুছিয়ে লেখা
- recurrence সমাধান, DP টেবিল ভরা আর trace হাতে করে নির্ভুল সংখ্যা বের করা
- ভাইভায় এক–তিন বাক্যে নির্ভুল উত্তর, জোরে ভেবে বলা, না জানলে সৎ থাকা আর ছবি এঁকে বোঝানো
- AI-এর দেওয়া প্রমাণ আর complexity-র দাবি পরীক্ষা করে ভুল ধরা
১ · পরীক্ষক কী খোঁজেন — DSA উত্তরের কাঠামো আর মার্ক-স্কিম
আগের দুই পর্বে আপনি হাতে চালিয়ে উত্তর বের করেছেন আর MCQ অনুশীলন করেছেন। সেখানে উত্তর হয় ঠিক, নয় ভুল। লিখিত পরীক্ষা আর ভাইভা আলাদা। দুজন প্রার্থী একই অ্যালগরিদম জানেন, কিন্তু একজন বেশি নম্বর পান — কারণ তিনি দাবিটি স্পষ্ট লিখেছেন, ছোট একটি উদাহরণে চালিয়ে দেখিয়েছেন, complexity-র হিসাব দিয়েছেন আর প্রমাণটি ধাপে ধাপে শেষ করেছেন। পরীক্ষার খাতা দেখার সময় পরীক্ষকের হাতে একটি মার্ক-স্কিম থাকে: কোন অংশে কত নম্বর। আপনার কাজ হলো সেই তালিকার প্রতিটি ঘর খাতায় খুঁজে পেতে সাহায্য করা।
পাঁচ অংশের কাঠামো
ভাবুন ঢাকার রাস্তায় কাউকে ঠিকানা বোঝাচ্ছেন। প্রথমে গন্তব্য (সংজ্ঞা), তারপর পথ (অ্যালগরিদম), তারপর একটি চেনা মোড় দেখিয়ে দেওয়া (উদাহরণ), তারপর কতক্ষণ লাগবে (complexity), আর শেষে কেন এই পথটিই সবচেয়ে ছোট (প্রমাণ)। ছোট প্রশ্নে প্রথম দুই–তিনটি অংশই যথেষ্ট; ১০ নম্বরের প্রশ্নে কোনো একটি বাদ পড়লে নম্বরের একটি ভাগ ফাঁকা থাকে। নিচের মডেল উত্তরগুলো এই ক্রম মেনেছে, আর প্রতিটির শেষে “নম্বর কোথায়” লাইনে বলা আছে পরীক্ষক কোন জিনিসগুলো খোঁজেন।
প্রশ্নের ধরন অনুযায়ী মার্ক-স্কিম
| ধরন | যাতে নম্বর আসে | যাতে নম্বর কাটে |
|---|---|---|
| সংজ্ঞা ও পার্থক্য | নির্ভুল এক–দুই বাক্য, একটি উদাহরণ; পার্থক্যে একই মাপকাঠির টেবিল (সময়, জায়গা, শর্ত, ব্যবহার) | উদাহরণ ছাড়া মুখস্থ বাক্য; দুই পাশে আলাদা মাপকাঠি |
| Pseudocode লিখুন | input ও output বলা, 0- না 1-based তা ঘোষণা, লুপের সীমা নির্ভুল, base case, খালি বা এক উপাদানের ইনপুট | অসম্পূর্ণ শর্ত, return ভুলে যাওয়া, অকারণে ভাষার খুঁটিনাটি |
| Trace বা simulate | প্রতিটি ধাপের অবস্থা টেবিলে (অ্যারে, pointer, queue, dist), চূড়ান্ত উত্তর আলাদা করে লেখা | শুধু শেষ উত্তর; মাঝের একটি ধাপ বাদ |
| Complexity বের করুন | লুপ গোনা বা recurrence লেখা, সমাধানের পদ্ধতির নাম, সঠিক চলক দিয়ে Θ/O, আর জায়গা আলাদা করে | শুধু “O(n log n)” লেখা; Θ আর O গুলিয়ে ফেলা; V আর E না বলে শুধু n |
| প্রমাণ করুন | পদ্ধতির নাম (invariant, induction, exchange, contradiction), দাবি স্পষ্ট, প্রতিটি ধাপে কোন শর্ত ব্যবহার হলো, শেষে উপসংহার | একটি উদাহরণ দেখিয়ে “প্রমাণ”; গোল যুক্তি; শর্তের ব্যবহার না দেখানো |
| DP সমস্যা | state-এর অর্থ, recurrence, base case, ভরার ক্রম, টেবিল, উত্তর কোন ঘরে, পুনর্গঠন, Θ(সময়) ও Θ(জায়গা) | শুধু টেবিল, state-এর অর্থ ছাড়া |
| Reduction | দিক ঠিক (জানা কঠিন সমস্যা → নতুন সমস্যা), রূপান্তর polynomial সময়ে, “হ্যাঁ ⇔ হ্যাঁ” দুই দিকেই প্রমাণ, আর NP-তে আছে তা দেখানো | উল্টো দিকে reduction; শুধু এক দিকের প্রমাণ |
চার রকম প্রমাণের ছাঁচ
Loop invariant
একটি দাবি, যা লুপের প্রতিটি ঘোরার শুরুতে সত্য। তিন ধাপ: শুরু (প্রথম ঘোরার আগে সত্য), রক্ষা (এক ঘোরায় সত্য থাকে), সমাপ্তি (লুপ থামলে invariant + থামার শর্ত = সঠিক উত্তর)।
লুপের জন্যInduction
Base (ছোট n-এ সত্য), hypothesis (ছোট সবকিছুর জন্য সত্য ধরা), step (তাহলে n-এর জন্যও সত্য)। recursion, recurrence আর tree-এর প্রমাণে।
recursion-এর জন্যExchange argument
যেকোনো সেরা সমাধান নিন; তার প্রথম পছন্দটি greedy-র পছন্দ দিয়ে বদলান; দেখান খরচ খারাপ হয়নি। greedy, MST আর Huffman-এ।
greedy-র জন্যContradiction
ধরুন দাবিটি মিথ্যা; প্রথম যে জায়গায় ভাঙে সেটি নিন; দেখান সেটি অসম্ভব। Dijkstra, নিম্নসীমা আর অস্তিত্বের প্রমাণে।
“প্রথম ভুল” ধরেসময়ের হিসাব সহজ: মোট মিনিটকে মোট নম্বর দিয়ে ভাগ করুন। তিন ঘণ্টায় ১০০ নম্বর হলে প্রতি নম্বরে মোটামুটি দুই মিনিট — তাই ১০ নম্বরের একটি প্রমাণে ১৮–২০ মিনিটের বেশি নয়। প্রমাণ আটকে গেলে যতটুকু পেরেছেন তা গুছিয়ে লিখে এগিয়ে যান; প্রমাণের কাঠামো (দাবি, পদ্ধতি, base case) লিখলেও আংশিক নম্বর আসে।
এই পাতার প্রশ্নগুলো কোর্সের মডিউল অনুযায়ী সাজানো। কোনো অংশে বারবার আটকালে সেই অংশের শুরুতে দেওয়া পর্বগুলো আবার পড়ুন:
| কোর্সের মডিউল | পর্ব | লিখিত প্রশ্ন | ভাইভা প্রশ্ন |
|---|---|---|---|
| ১ · শুরু ও বিশ্লেষণের ভিত্তি | ০১–০৯ | অংশ ২ (ক-০১–ক-০৮) | ভ-০১–ভ-০৬ |
| ২ · লিনিয়ার ডেটা স্ট্রাকচার | ১০–১৭ | অংশ ৩ (খ-০১–খ-০৭) | ভ-০৭–ভ-১২ |
| ৩ · সার্চিং ও সর্টিং | ১৮–২৫ | অংশ ৪ (গ-০১–গ-০৬) | ভ-১৩–ভ-১৮ |
| ৪–৫ · ট্রি, হ্যাশ, DSU আর রেঞ্জ কোয়েরি | ২৬–৩৮ | অংশ ৫ (ঘ-০১–ঘ-০৬) | ভ-১৯–ভ-২৪ |
| ৬ · গ্রাফ অ্যালগরিদম | ৩৯–৪৮ | অংশ ৬ (ঙ-০১–ঙ-০৬) | ভ-২৫–ভ-৩০ |
| ৭ · অ্যালগরিদম ডিজাইন কৌশল | ৪৯–৫৯ | অংশ ৭ (চ-০১–চ-০৭) | ভ-৩১–ভ-৩৫ |
| ৮ · উন্নত বিশ্লেষণ ও তত্ত্ব | ৬০–৬৭ | অংশ ৮ (ছ-০১–ছ-০৫) | ভ-৩৬–ভ-৪১ |
এই পাতার কোনো প্রশ্ন কোনো নির্দিষ্ট পরীক্ষার প্রশ্নপত্র বা কোনো প্রতিষ্ঠানের ইন্টারভিউ থেকে নেওয়া নয়। এগুলো এমন ধরনের প্রশ্ন, যা বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় প্রায়ই আসে। নম্বর বণ্টন আর সিলেবাস নিজের পরীক্ষার সর্বশেষ বিজ্ঞপ্তি থেকে মিলিয়ে নিন। এখানে log বলতে, আলাদা করে না বললে, log2 বোঝানো হয়েছে, আর pseudocode-এ অ্যারে 0-based.
২ · অংশ ক — বিশ্লেষণের ভিত্তি (মডিউল ১)
রিভিশনের জন্য: পর্ব ০১, পর্ব ০২, পর্ব ০৩, পর্ব ০৪, পর্ব ০৫, পর্ব ০৬, পর্ব ০৭, পর্ব ০৮, পর্ব ০৯.
এই মডিউল থেকে আসে বিশ্লেষণের ভাষা — সংজ্ঞা থেকে Big-O প্রমাণ, recurrence সমাধান আর loop invariant. বাকি সব মডিউলের প্রমাণ এই হাতিয়ারগুলো দিয়েই লেখা হয়।
ক-০১অ্যালগরিদম কী? একটি ভালো অ্যালগরিদমের বৈশিষ্ট্যগুলো লিখুন। RAM model কী, আর কেন লাগে? — ৪ নম্বর (সহজ)
সংজ্ঞা: অ্যালগরিদম হলো সুনির্দিষ্ট ধাপের একটি সসীম ক্রম, যা একটি সমস্যার প্রতিটি বৈধ ইনপুট (instance) থেকে সঠিক আউটপুট দেয়।
বৈশিষ্ট্য: (১) ইনপুট — শূন্য বা বেশি; (২) আউটপুট — অন্তত একটি; (৩) definiteness — প্রতিটি ধাপ দ্ব্যর্থহীন; (৪) finiteness — সসীম ধাপে থামে; (৫) effectiveness — প্রতিটি ধাপ কাগজ-কলমে করা যায় এমন সরল। সাথে বলুন: সঠিকতা (সব ইনপুটে) আর দক্ষতা (সময় ও জায়গা)।
RAM model: Random Access Machine একটি কল্পিত কম্পিউটার — যোগ, গুণ, তুলনা, মেমরির যেকোনো ঘর পড়া বা লেখা প্রতিটি এক ধাপ (ধ্রুবক সময়), আর নির্দেশ চলে একটির পর একটি। কেন লাগে: আসল মেশিন, compiler আর ভাষা বদলালে সেকেন্ডের হিসাব বদলায়; ধাপ গুনলে অ্যালগরিদমের তুলনা যন্ত্র-নিরপেক্ষ হয়।
নম্বর কোথায়: পাঁচটি বৈশিষ্ট্যের নাম ও এক লাইনের অর্থ, আর RAM model-এ “প্রতিটি মৌলিক কাজ এক ধাপ” কথাটি। পর্ব ০২
ক-০২Insertion sort-এর pseudocode লিখুন এবং loop invariant দিয়ে এর সঠিকতা প্রমাণ করুন। worst case-এ কয়টি তুলনা হয়? — ৮ নম্বর (মাঝারি)
Pseudocode: INSERTION-SORT(a[0..n-1]) // 0-based
for i = 1 to n-1:
key = a[i]
j = i
while j > 0 and a[j-1] > key:
a[j] = a[j-1] // shift bigger item right
j = j - 1
a[j] = keyInvariant: বাইরের লুপের প্রতিটি ঘোরার শুরুতে a[0..i-1] অংশে আছে মূল অ্যারের প্রথম iটি উপাদানই, এবং সেগুলো সাজানো।
- শুরু:
i = 1-এ অংশটি শুধুa[0]— একটি উপাদান নিজেই সাজানো। সত্য। - রক্ষা: ভেতরের লুপ
key-এর চেয়ে বড় উপাদানগুলোকে এক ঘর ডানে সরায়, ক্রম না ভেঙে। থামে যখনj = 0অথবাa[j-1] ≤ key. তখনa[0..j-1]-এর সবাই ≤ key আরa[j+1..i]-এর সবাই > key, তাইa[j] = keyবসালেa[0..i]সাজানো, আর এতে আছে প্রথমi+1টি উপাদানই। পরের ঘোরার জন্য invariant সত্য। - সমাপ্তি: লুপ থামে
i = n-এ। invariant বলছেa[0..n-1]— পুরো অ্যারে — মূল উপাদানগুলো নিয়েই সাজানো। ∎
উদাহরণ: [5, 2, 4, 6, 1, 3] → i=1: [2, 5, 4, 6, 1, 3] → i=2: [2, 4, 5, 6, 1, 3] → i=3: অপরিবর্তিত → i=4: [1, 2, 4, 5, 6, 3] → i=5: [1, 2, 3, 4, 5, 6].
Complexity: উল্টো সাজানো ইনপুটে i-তম ঘোরায় i টি তুলনা, মোট 1 + 2 + … + (n−1) = n(n−1)/2 = Θ(n²). সাজানো ইনপুটে প্রতি ঘোরায় ১টি, মোট n − 1 = Θ(n). জায়গা Θ(1) বাড়তি (in-place). গ-০৫-এর পরের প্রোগ্রামটি সব ইনপুট চালিয়ে n = 8-এ worst case 28 = 8·7/2 পেয়েছে।
নম্বর কোথায়: invariant-এর নির্ভুল বাক্য (“প্রথম i টি উপাদানই, সাজানো” — দুই অংশই), তিনটি ধাপের নাম, আর সমাপ্তিতে থামার শর্ত (i = n) ব্যবহার। পর্ব ০৩ · পর্ব ২০
ক-০৩Big-O-এর সংজ্ঞা লিখুন। সংজ্ঞা থেকে প্রমাণ করুন 3n² + 5n + 2 = O(n²), এবং n² ≠ O(n). — ৬ নম্বর (মাঝারি)
সংজ্ঞা: f(n) = O(g(n)) যদি এমন ধনাত্মক ধ্রুবক c আর n0 থাকে যে সব n ≥ n0-এর জন্য 0 ≤ f(n) ≤ c·g(n). অর্থাৎ একটা সময়ের পর f কখনো g-এর একটি ধ্রুবক গুণের ওপরে ওঠে না — upper bound.
প্রমাণ ১: n ≥ 1 হলে 5n ≤ 5n² আর 2 ≤ 2n². তাই 3n² + 5n + 2 ≤ 3n² + 5n² + 2n² = 10n². সুতরাং c = 10, n0 = 1 নিলে সংজ্ঞা পূরণ হয়। ∎ (অন্য জোড়াও চলে: c = 4 নিলে দরকার 5n + 2 ≤ n², যা n = 6-এ 32 ≤ 36 আর তার পরে সবসময় সত্য, কিন্তু n = 5-এ 27 > 25 — তাই n0 = 6. c আর n0 একক নয়, একটি জোড়া দেখালেই যথেষ্ট।)
প্রমাণ ২ (contradiction): ধরুন n² = O(n). তাহলে কোনো c, n0-এর জন্য সব n ≥ n0-এ n² ≤ cn, অর্থাৎ n ≤ c. কিন্তু n = max(n0, ⌊c⌋ + 1) নিলে n > c — বিরোধ। তাই n² ≠ O(n). ∎
নম্বর কোথায়: সংজ্ঞায় “কোনো c > 0 আর n0 আছে, সব n ≥ n0-এর জন্য” — তিনটি অংশই; প্রমাণে c আর n0-এর নির্দিষ্ট মান, আর অসমতার প্রতিটি ধাপ কোন n-এর জন্য সত্য তা লেখা। পর্ব ০৪
ক-০৪O, Ω আর Θ-এর পার্থক্য লিখুন। প্রমাণ করুন (1/2)n² − 3n = Θ(n²). — ৬ নম্বর (মাঝারি)
| চিহ্ন | সংজ্ঞা (সব n ≥ n0-এ) | মানে | উদাহরণ |
|---|---|---|---|
| O(g) | 0 ≤ f ≤ c·g | ওপরের সীমা | n = O(n²) সত্য |
| Ω(g) | 0 ≤ c·g ≤ f | নিচের সীমা | n² = Ω(n) সত্য |
| Θ(g) | c1·g ≤ f ≤ c2·g | আঁটসাঁট — দুই দিকেই | n = Θ(n²) মিথ্যা |
প্রমাণ: দরকার c1n² ≤ (1/2)n² − 3n ≤ c2n². n² দিয়ে ভাগ করে: c1 ≤ 1/2 − 3/n ≤ c2. ডান দিক n ≥ 1-এ সবসময় সত্য, যদি c2 = 1/2. বাঁ দিকে 1/2 − 3/n বাড়তে থাকে; n = 7-এ মান 1/2 − 3/7 = 1/14 > 0. তাই c1 = 1/14, c2 = 1/2, n0 = 7. (n = 6-এ মান 0, তাই সেখান থেকে শুরু করা যায় না।) ∎
যোগ করুন: Θ মানে “O এবং Ω দুটোই”. Big-O মানেই worst case নয় — যেকোনো ফাংশনের সীমা; “insertion sort-এর best case Θ(n)” একটি সঠিক বাক্য।
নম্বর কোথায়: তিন সংজ্ঞায় অসমতার দিক ঠিক; প্রমাণে তিনটি ধ্রুবকের নির্দিষ্ট মান আর n0 কেন ৭ (৬ নয়) — এই ছোট যাচাই। পর্ব ০৪
ক-০৫T(1) = 1, T(n) = 2T(n/2) + n (n হলো 2-এর ঘাত) — recursion tree এঁকে সমাধান করুন, তারপর substitution method দিয়ে প্রমাণ করুন T(n) = O(n log n). — ১০ নম্বর (কঠিন)
Recursion tree: মূলে কাজ n. তলা ১-এ দুটি node, প্রতিটি n/2 — মোট n. তলা i-তে 2iটি node, প্রতিটি n/2i — মোট n. আকার 1 হয় যখন n/2i = 1, অর্থাৎ i = log n. ভেতরের তলা log n টি (i = 0 থেকে log n − 1), প্রতিটিতে n; পাতার তলায় n টি পাতা, প্রতিটি T(1) = 1, মোট n. তাই
নিচের ডায়াগ্রামে n = 8 ধরে গাছটি ধাপে ধাপে আঁকা হয়েছে: 8 + 8 + 8 + 8 = 32 = 8·3 + 8.
Substitution (অনুমান ও induction): দাবি — সব n ≥ 2 (2-এর ঘাত)-এর জন্য T(n) ≤ c·n log n, যেখানে c = 2.
- Base: n = 2: T(2) = 2·1 + 2 = 4, আর c·2·log 2 = 2·2·1 = 4. 4 ≤ 4 ✓. (n = 1 base হিসেবে নেওয়া যায় না, কারণ 1·log 1 = 0 < T(1) — এটাই এই প্রমাণের সাধারণ ফাঁদ।)
- Hypothesis: ধরুন T(n/2) ≤ c·(n/2)·log(n/2), যেখানে n/2 ≥ 2.
- Step: T(n) = 2T(n/2) + n ≤ 2c(n/2)(log n − 1) + n = c·n log n − cn + n ≤ c·n log n, কারণ c ≥ 1 হলে −cn + n ≤ 0. ∎
নিচের Ω দিকটিও একইভাবে: T(n) ≥ n log n (base n = 2: 4 ≥ 2), তাই T(n) = Θ(n log n). এই recurrence-ই merge sort-এর।
নম্বর কোথায়: প্রতি তলার খরচ আর তলার সংখ্যা (log n + 1) আলাদা করে লেখা, পাতার খরচ ভুলে না যাওয়া; substitution-এ base case বেছে নেওয়ার যুক্তি, আর step-এ ঠিক কোথায় c ≥ 1 লাগল। পর্ব ০৮
8 ← তলা 0: খরচ 8
8 ← তলা 0: 8
/ \
4 4 ← তলা 1: 4 + 4 = 8
8 ← তলা 0: 8
/ \
4 4 ← তলা 1: 8
/ \ / \
2 2 2 2 ← তলা 2: 4 × 2 = 8
8 ← তলা 0: 8
/ \
4 4 ← তলা 1: 8
/ \ / \
2 2 2 2 ← তলা 2: 8
/ \ / \ / \ / \
1 1 1 1 1 1 1 1 ← পাতা: 8 × T(1) = 8
তলা i-তে: 2^i টি node × n/2^i = n
ভেতরের তলা: i = 0 … log n − 1 → log n টি
পাতা: n টি × T(1) = n
T(n) = n·log n + n
n = 8: 8·3 + 8 = 32 ✓
অনুমান: T(n) ≤ c·n·log n, c = 2, n ≥ 2
Base : T(2) = 4 ≤ 2·2·1 = 4 ✓
Step : T(n) ≤ 2·c·(n/2)·(log n − 1) + n
= c·n·log n − c·n + n
≤ c·n·log n (কারণ c ≥ 1) ∎
- তলা ০. মূলে সমস্যার আকার 8, আর ভাঙা-জোড়ার কাজ f(8) = 8. এখনো ছোট সমস্যাগুলোর খরচ গোনা হয়নি।
- তলা ১. a = 2টি ছোট সমস্যা, প্রতিটি আকার 8/2 = 4, প্রত্যেকে নিজের ভাগে 4 কাজ করে। তলার মোট 4 + 4 = 8 — মূলের সমান।
- তলা ২. চারটি node, প্রতিটি আকার 2, খরচ 2. আবারও মোট 8. নিয়মটি দেখা যাচ্ছে: node দ্বিগুণ হয়, প্রতিটির কাজ অর্ধেক হয়, তাই প্রতি তলায় মোট n.
- পাতা. আকার 1-এ পৌঁছে ৮টি পাতা, প্রতিটি T(1) = 1. পাতার তলাতেও মোট 8. এই তলাটি অনেকে ভুলে যান — তখন উত্তর n log n আসে, n log n + n নয়।
- যোগফল. সাধারণ n-এ log n টি ভেতরের তলা, প্রতিটিতে n, আর পাতায় n: T(n) = n log n + n. n = 8-এ 3 টি ভেতরের তলা: 8·3 + 8 = 32, যা সরাসরি হিসাবের সাথে মেলে।
- Substitution দিয়ে নিশ্চিত. গাছ থেকে পাওয়া অনুমানটিকে induction দিয়ে প্রমাণ: base n = 2-এ সমতা, আর step-এ −cn + n ≤ 0 কারণ c = 2 ≥ 1. গাছ উত্তর খোঁজে, substitution সেটি প্রমাণ করে — পরীক্ষায় দুটোই চাইলে এই ক্রমে লিখুন।
সংখ্যা দিয়েও যাচাই করা যায়। নিচের প্রোগ্রাম recurrence-টি সরাসরি হিসাব করে, পাশে বন্ধ রূপ n log n + n আর প্রমাণের সীমা 2n log n ছাপে:
#include <cstdio>
// T(1) = 1, T(n) = 2T(n/2) + n (n a power of 2)
long long T(long long n) { return n == 1 ? 1 : 2 * T(n / 2) + n; }
int main() {
std::printf("%6s %8s %12s %12s %s\n", "n", "T(n)", "n*lg(n)+n", "2*n*lg(n)", "T <= 2n lg n ?");
int lg = 0;
for (long long n = 1; n <= 1024; n *= 2, ++lg) {
long long t = T(n), closed = n * lg + n, bound = 2 * n * lg;
const char* verdict = n < 2 ? "(n = 1: base only)" : (t <= bound ? "yes" : "NO");
std::printf("%6lld %8lld %12lld %12lld %s\n", n, t, closed, bound, verdict);
}
}আউটপুট:
n T(n) n*lg(n)+n 2*n*lg(n) T <= 2n lg n ?
1 1 1 0 (n = 1: base only)
2 4 4 4 yes
4 12 12 16 yes
8 32 32 48 yes
16 80 80 128 yes
32 192 192 320 yes
64 448 448 768 yes
128 1024 1024 1792 yes
256 2304 2304 4096 yes
512 5120 5120 9216 yes
1024 11264 11264 20480 yes
প্রতিটি সারিতে T(n) আর n*lg(n)+n হুবহু এক — recursion tree-র বন্ধ রূপ নির্ভুল। শেষ কলামে n ≥ 2-এর প্রতিটিতে T(n) ≤ 2n log n, আর n = 2-এ সমতা (4 = 4) — ঠিক যেমন substitution-এর base case বলেছিল। n = 1-এ সীমাটি 0, অথচ T(1) = 1; তাই base হিসেবে n = 1 চলে না। মনে রাখবেন, কয়েকটি সংখ্যা মেলা প্রমাণ নয় — এটি শুধু হিসাবের ভুল ধরার উপায়।
ক-০৬Master theorem-এর তিনটি case লিখুন এবং প্রয়োগ করুন: (ক) 9T(n/3) + n, (খ) T(2n/3) + 1, (গ) 3T(n/4) + n log n, (ঘ) 2T(n/2) + n log n. — ৮ নম্বর (মাঝারি)
বিবৃতি: a ≥ 1, b > 1 ধ্রুবক, আর T(n) = aT(n/b) + f(n). f(n)-কে মেলান watershed ফাংশন nlogb a-এর সাথে:
- কোনো ε > 0-এর জন্য f(n) = O(nlogb a − ε) হলে T(n) = Θ(nlogb a).
- f(n) = Θ(nlogb a) হলে T(n) = Θ(nlogb a log n).
- কোনো ε > 0-এর জন্য f(n) = Ω(nlogb a + ε), এবং কোনো c < 1-এর জন্য বড় n-এ a·f(n/b) ≤ c·f(n) (regularity শর্ত) হলে T(n) = Θ(f(n)).
প্রয়োগ:
- (ক) a = 9, b = 3: nlog3 9 = n². f = n = O(n2−1) → case ১ → Θ(n²).
- (খ) a = 1, b = 3/2: n0 = 1. f = 1 = Θ(1) → case ২ → Θ(log n).
- (গ) a = 3, b = 4: nlog4 3 ≈ n0.793. f = n log n = Ω(n0.793 + 0.2); regularity: 3·(n/4)·log(n/4) ≤ (3/4)·n log n, c = 3/4 < 1 → case ৩ → Θ(n log n).
- (ঘ) a = 2, b = 2: n1. f = n log n বড়, কিন্তু কোনো nε গুণ বড় নয় (log n < nε সব ε > 0-এর জন্য, বড় n-এ) — তিন case-এর কোনোটিই খাটে না। recursion tree-তে প্রতি তলা i-তে n·log(n/2i), যোগ করলে Θ(n log² n) (বর্ধিত case ২: f = Θ(nlogb a logk n) হলে Θ(nlogb a logk+1 n)).
নম্বর কোথায়: প্রতিটিতে a, b, nlogb a লেখা; case ৩-এ regularity আলাদা করে যাচাই; আর (ঘ)-তে “খাটে না” বলার সাহস ও কারণ। পর্ব ০৯
ক-০৭Linear search-এর best, worst আর average case বিশ্লেষণ করুন। — ৪ নম্বর (সহজ)
অ্যালগরিদম: i = 0 থেকে n − 1 পর্যন্ত a[i] = x কি না দেখুন; পেলে i ফেরত দিন, না পেলে −1.
- Best case: x প্রথম ঘরে — ১টি তুলনা, Θ(1).
- Worst case: x শেষ ঘরে বা নেই — n টি তুলনা, Θ(n).
- Average case: ধরা যাক x আছে, আর n টি অবস্থানের প্রতিটি সমান সম্ভাব্য (1/n). অবস্থান k-তে (1-based) k টি তুলনা, তাই প্রত্যাশিত তুলনা = (1 + 2 + … + n)/n = (n + 1)/2 = Θ(n).
নম্বর কোথায়: average case-এ ইনপুটের অনুমান (“সমান সম্ভাব্য”) স্পষ্ট করা — অনুমান ছাড়া average-এর কোনো অর্থ নেই। জায়গা Θ(1). পর্ব ০৫
ক-০৮xn হিসাবের জন্য একটি O(log n) রিকার্সিভ pseudocode লিখুন, এর recurrence দিন এবং সঠিকতা যুক্তি দিন। — ৫ নম্বর (সহজ)
Pseudocode: POWER(x, n) // n ≥ 0 integer
if n == 0: return 1
h = POWER(x, n div 2) // one recursive call only
if n is even: return h * h
else: return h * h * xসঠিকতা (strong induction): n = 0-এ x0 = 1 ✓. ধরুন ⌊n/2⌋-এর জন্য h = x⌊n/2⌋. n জোড় হলে h·h = xn; বিজোড় হলে 2⌊n/2⌋ = n − 1, তাই h·h·x = xn ✓.
Complexity: T(n) = T(⌊n/2⌋) + Θ(1) → Θ(log n) (master theorem case ২, বা প্রতি ডাকে n অর্ধেক)। n ≥ 1-এ ডাকের সংখ্যা ⌊log n⌋ + 2 (শেষ ডাকটি n = 0), প্রতিটিতে সর্বোচ্চ ২টি গুণ। call stack-এর জন্য জায়গা Θ(log n). সাধারণ ভুল: POWER(x, n/2) * POWER(x, n/2) লিখলে দুটি ডাক — T(n) = 2T(n/2) + 1 = Θ(n), সুবিধাই নষ্ট।
নম্বর কোথায়: base case, “একটিই রিকার্সিভ ডাক” আর বিজোড় n-এর বাড়তি x. পর্ব ০৭
৩ · অংশ খ — লিনিয়ার ডেটা স্ট্রাকচার (মডিউল ২)
রিভিশনের জন্য: পর্ব ১০, পর্ব ১১, পর্ব ১২, পর্ব ১৩, পর্ব ১৪, পর্ব ১৫, পর্ব ১৬, পর্ব ১৭.
এই মডিউলের প্রশ্নে সাধারণত দুটি জিনিস চাওয়া হয়: একটি ছোট pseudocode আর তার complexity-র সঠিক যুক্তি। “কেন O(1)?” প্রশ্নটির উত্তরে প্রায়ই amortized analysis লাগে।
খ-০১Dynamic array-তে জায়গা ফুরোলে ধারণক্ষমতা দ্বিগুণ করা হয়। প্রমাণ করুন n টি push_back-এর মোট খরচ O(n), অর্থাৎ প্রতিটির amortized খরচ O(1). ধারণক্ষমতা প্রতিবার ১০ করে বাড়ালে কী হতো? — ৮ নম্বর (মাঝারি)
মডেল: dynamic array-তে প্রতিটি push-এ একটি লেখা (খরচ 1). ভরা থাকলে নতুন দ্বিগুণ ব্লক নিয়ে পুরোনো সব উপাদান কপি করতে হয় (size টি কপি)। একটি push-এর worst case তাই Θ(n).
Aggregate প্রমাণ: ধারণক্ষমতা 1 থেকে শুরু। কপি হয় যখন size = 1, 2, 4, …, 2k, যেখানে 2k < n. মোট কপি = 1 + 2 + 4 + … + 2k = 2k+1 − 1 < 2n. লেখা n টি। তাই n টি push-এর মোট খরচ < n + 2n = 3n = O(n), গড়ে প্রতিটি O(1). ∎
Accounting দিয়ে একই কথা: প্রতিটি push থেকে ৩ টাকা নিন — ১ টাকায় নিজের লেখা, ২ টাকা জমা। ধারণক্ষমতা m থেকে 2m হওয়ার আগে শেষ m/2 টি push মোট m টাকা জমিয়েছে — ঠিক m টি কপির দাম। জমা কখনো ঋণাত্মক হয় না।
প্রতিবার +10: কপি হয় size = 10, 20, 30, … তে; মোট ≈ 10(1 + 2 + … + n/10) ≈ n²/20 = Θ(n²), তাই প্রতি push-এর amortized খরচ Θ(n). ধ্রুবক যোগ নয়, ধ্রুবক গুণ (growth factor > 1) দরকার।
নম্বর কোথায়: একক push-এর worst case Θ(n) আর amortized O(1) — দুটোই বলা; geometric series-এর যোগফল; আর amortized মানে “পুরো ক্রমের worst case-এর গড়”, সম্ভাবনা নয়। পর্ব ১০ · পর্ব ৬০
দুই কৌশলের কপির সংখ্যা গুনে দেখা যাক — দ্বিগুণ করা (×2) আর ১০ করে বাড়ানো (+10):
#include <cstdio>
#include <initializer_list>
// Count element copies made by n push_backs on a growing array.
long long copies(long long n, bool doubling) {
long long cap = doubling ? 1 : 10, size = 0, moved = 0;
for (long long k = 0; k < n; ++k) {
if (size == cap) { // full: allocate new block, copy all items
moved += size;
cap = doubling ? cap * 2 : cap + 10;
}
++size; // the push itself
}
return moved;
}
int main() {
std::printf("%8s %10s %9s %12s %9s\n", "n", "x2 copies", "per push", "+10 copies", "per push");
for (long long n : {10LL, 100LL, 1000LL, 10000LL, 100000LL}) {
long long d = copies(n, true), a = copies(n, false);
std::printf("%8lld %10lld %9.2f %12lld %9.1f\n", n, d, double(d) / n, a, double(a) / n);
}
}আউটপুট:
n x2 copies per push +10 copies per push
10 15 1.50 0 0.0
100 127 1.27 450 4.5
1000 1023 1.02 49500 49.5
10000 16383 1.64 4995000 499.5
100000 131071 1.31 499950000 4999.5
×2 কলামে প্রতি push-এ কপি সবসময় 2-এর নিচে (1.02 থেকে 1.64) — প্রমাণের “মোট কপি < 2n” সীমা। n = 10-এ কপি 15 = 1 + 2 + 4 + 8. +10 কলামে প্রতি push-এর কপি n-এর সাথে সরলরেখায় বাড়ে: n দশগুণ হলে 4.5 → 49.5 → 499.5 — Θ(n) amortized, মোট Θ(n²). n = 100000-এ প্রায় ৫০ কোটি কপি, যেখানে ×2-তে মাত্র 131071.
খ-০২Array আর linked list-এর পার্থক্য লিখুন। — ৫ নম্বর (সহজ)
| মাপকাঠি | Array | Singly linked list |
|---|---|---|
| মেমরি | পাশাপাশি এক ব্লক | ছড়ানো node, প্রতিটিতে বাড়তি pointer |
| i-তম উপাদান | Θ(1) — ঠিকানা = base + i × আকার | Θ(i) — শুরু থেকে হাঁটতে হয় |
| শুরুতে যোগ বা মোছা | Θ(n) — সবাইকে সরাতে হয় | Θ(1) |
| জানা node-এর পরে যোগ | Θ(n) | Θ(1) |
| শেষে যোগ | amortized Θ(1) (dynamic array) | Θ(1) যদি tail pointer রাখা হয় |
| খোঁজা | Θ(n); সাজানো হলে binary search Θ(log n) | Θ(n), সাজানো হলেও |
| cache | ভালো — পাশাপাশি ঘর একসাথে আসে | খারাপ — প্রতি node-এ নতুন জায়গা |
উপসংহার: বারবার index দিয়ে পড়া আর শেষে যোগ — array; মাঝখানে জানা জায়গায় অনেক যোগ-মোছা (যেমন LRU cache-এ hash map-এর সাথে) — linked list. বাস্তবে cache locality-র কারণে ছোট-মাঝারি n-এ array প্রায়ই জেতে।
নম্বর কোথায়: একই মাপকাঠিতে অন্তত ৫টি সারি, প্রতিটিতে নির্দিষ্ট complexity. পর্ব ১২
খ-০৩একটি singly linked list উল্টে দেওয়ার pseudocode লিখুন এবং 1 → 2 → 3-এ trace করুন। — ৪ নম্বর (সহজ)
Pseudocode: REVERSE(head)
prev = NULL
cur = head
while cur != NULL:
nxt = cur.next // save the rest of the list
cur.next = prev // turn this arrow around
prev = cur
cur = nxt
return prev // new headTrace: শুরুতে prev = NULL, cur = 1. ঘোরা ১: 1.next = NULL, prev = 1, cur = 2. ঘোরা ২: 2.next = 1, prev = 2, cur = 3. ঘোরা ৩: 3.next = 2, prev = 3, cur = NULL. ফেরত 3: তালিকা 3 → 2 → 1.
Invariant: প্রতিটি ঘোরার শুরুতে prev থেকে শুরু হওয়া তালিকা হলো মূল তালিকার ইতিমধ্যে দেখা অংশ, উল্টো ক্রমে। Complexity: প্রতিটি node একবার — Θ(n) সময়, Θ(1) বাড়তি জায়গা। খালি তালিকায় (head = NULL) লুপ চলে না, NULL ফেরত — সঠিক।
নম্বর কোথায়: nxt আগে বাঁচিয়ে রাখা (নইলে বাকি তালিকা হারায়), নতুন head ফেরত দেওয়া, খালি তালিকার কথা। পর্ব ১২
খ-০৪Stack ব্যবহার করে বন্ধনী মিলেছে কি না যাচাইয়ের অ্যালগরিদম লিখুন। "([]{})" আর "([)]"-এ trace করুন। — ৫ নম্বর (সহজ)
Pseudocode: BALANCED(s)
S = empty stack
for each ch in s:
if ch is one of ( [ {: push(S, ch)
else if ch is one of ) ] }:
if S is empty or top(S) is not the match of ch: return false
pop(S)
return S is empty"([]{})": ( → [(]; [ → [( []; ] মেলে [ → [(]; { → [( {]; } মেলে { → [(]; ) মেলে ( → []. শেষে stack খালি → true.
"([)]": ( → [(]; [ → [( []; ) এলো, কিন্তু top = [ — মেলে না → false.
কেন stack: শেষে খোলা বন্ধনীটিকেই আগে বন্ধ করতে হয় — এটিই LIFO. Complexity: প্রতিটি অক্ষর একবার, প্রতিটি push/pop Θ(1) → Θ(n) সময়, worst case-এ Θ(n) জায়গা (সব খোলা বন্ধনী)।
নম্বর কোথায়: দুই রকম ব্যর্থতা — খালি stack-এ বন্ধ বন্ধনী, আর শেষে stack খালি না থাকা (যেমন "(("). শেষেরটি অনেকে ভুলে যান। পর্ব ১৩
খ-০৫Array দিয়ে circular queue-এর enqueue ও dequeue লিখুন। queue খালি আর ভরা কীভাবে চিনবেন? — ৫ নম্বর (মাঝারি)
Pseudocode: circular queue in Q[0..N-1], fields front, count
ENQUEUE(x):
if count == N: error "full"
Q[(front + count) mod N] = x
count = count + 1
DEQUEUE():
if count == 0: error "empty"
x = Q[front]
front = (front + 1) mod N
count = count - 1
return xখালি/ভরা: শুধু front আর rear রাখলে দুই অবস্থাতেই front = rear হতে পারে — দ্ব্যর্থতা। সমাধান দুটি: (১) ওপরের মতো count রাখা — খালি count = 0, ভরা count = N; (২) একটি ঘর সবসময় খালি রাখা — খালি front = rear, ভরা (rear + 1) mod N = front; তখন N টি ঘরে সর্বোচ্চ N − 1টি উপাদান।
কেন circular: সাধারণ array-queue-তে dequeue-র পর সামনের ঘরগুলো নষ্ট হয়; mod N দিয়ে শেষ থেকে আবার শুরুতে ফেরা (wrap-around) সেই জায়গা ব্যবহার করে। প্রতিটি কাজ Θ(1).
নম্বর কোথায়: mod N, আর খালি-ভরার দ্ব্যর্থতা ও তার সমাধান। পর্ব ১৪
খ-০৬দুটি stack দিয়ে একটি queue বানান। দেখান প্রতিটি কাজের amortized খরচ O(1). — ৬ নম্বর (মাঝারি)
অ্যালগরিদম: দুটি stack — in আর out. enqueue(x): push(in, x). dequeue(): যদি out খালি হয়, in থেকে সব pop করে out-এ push করুন; তারপর pop(out).
কেন ঠিক: in থেকে out-এ ঢালার সময় ক্রম উল্টে যায়, তাই সবচেয়ে পুরোনো উপাদান out-এর মাথায় আসে — FIFO. out খালি না হলে ঢালা হয় না, তাই নতুন উপাদান পুরোনোদের আগে আসতে পারে না।
Amortized: প্রতিটি উপাদান জীবনে সর্বোচ্চ চারটি stack-কাজের মধ্য দিয়ে যায় — in-এ push, in থেকে pop, out-এ push, out থেকে pop. তাই n টি কাজের মোট খরচ ≤ 4n = O(n), প্রতিটি amortized O(1). একটি dequeue-র worst case অবশ্য Θ(n) — যখন n টি উপাদান একসাথে ঢালতে হয়।
উদাহরণ: enqueue 1, 2, 3 → in = [1, 2, 3]. dequeue: out খালি, তাই ঢালা → out = [3, 2, 1] (মাথায় 1), pop → 1. enqueue 4 → in = [4]. dequeue → 2 (ঢালা লাগেনি)।
নম্বর কোথায়: “out খালি হলেই কেবল ঢালা” শর্ত, আর প্রতি উপাদানে চারটি কাজের হিসাব। পর্ব ১৪ · পর্ব ৬০
খ-০৭সাজানো অ্যারেতে যোগফল target এমন একটি জোড়া two pointer দিয়ে খুঁজুন, আর প্রমাণ করুন অ্যালগরিদমটি কোনো সমাধান বাদ দেয় না। — ৬ নম্বর (মাঝারি)
Pseudocode: PAIR-SUM(a[0..n-1] sorted ascending, target)
i = 0; j = n - 1
while i < j:
s = a[i] + a[j]
if s == target: return (i, j)
if s < target: i = i + 1
else: j = j - 1
return noneInvariant: যদি কোনো সমাধান জোড়া (p, q), p < q থাকে, তবে প্রতিটি ঘোরার শুরুতে i ≤ p আর q ≤ j — অর্থাৎ সমাধানটি এখনো জানালার ভেতরে।
রক্ষা: ধরুন s < target আর আমরা i বাড়াই। দেখাতে হবে i কোনো সমাধানের p নয়। i-এর যেকোনো সঙ্গী k থাকে i < k ≤ j-তে, আর অ্যারে সাজানো বলে a[k] ≤ a[j], তাই a[i] + a[k] ≤ a[i] + a[j] = s < target. সুতরাং i দিয়ে কোনো সমাধান নেই, ফেলে দেওয়া নিরাপদ। s > target-এ একই যুক্তিতে j ফেলা নিরাপদ। সমাপ্তি: প্রতি ঘোরায় j − i এক কমে, তাই সর্বোচ্চ n − 1 ঘোরায় থামে; সমাধান থাকলে জানালা তার আগে খালি হতে পারে না, তাই পাওয়া যাবেই। ∎
Complexity: Θ(n) সময়, Θ(1) জায়গা — দুই লুপের Θ(n²) brute force-এর তুলনায়। অসাজানো হলে আগে sort: O(n log n).
নম্বর কোথায়: “কেন একটি pointer সরানো নিরাপদ” — সাজানো ক্রম ব্যবহার করে অসমতাটি লেখা। শুধু “বড় হলে কমাই” লিখলে প্রমাণ হয় না। পর্ব ১৬
৪ · অংশ গ — সার্চিং ও সর্টিং (মডিউল ৩)
রিভিশনের জন্য: পর্ব ১৮, পর্ব ১৯, পর্ব ২০, পর্ব ২১, পর্ব ২২, পর্ব ২৩, পর্ব ২৪, পর্ব ২৫.
সর্টিং নিয়ে লিখিত প্রশ্নের প্রিয় তিনটি প্রমাণ: binary search-এর invariant, তুলনা-সর্টের Ω(n log n) নিম্নসীমা আর build-heap-এর O(n). সাথে আসে “কোনটি stable, কোনটি in-place” টেবিল।
গ-০১Binary search-এর iterative pseudocode লিখুন, loop invariant দিয়ে সঠিকতা দেখান আর তুলনার সংখ্যা বের করুন। — ৬ নম্বর (মাঝারি)
Pseudocode: BINARY-SEARCH(a[0..n-1] sorted, x)
lo = 0; hi = n - 1
while lo <= hi:
mid = lo + (hi - lo) div 2 // not (lo+hi)/2: avoids overflow
if a[mid] == x: return mid
if a[mid] < x: lo = mid + 1
else: hi = mid - 1
return -1Invariant: x যদি অ্যারেতে থাকে, তবে সেটি a[lo..hi]-তে আছে। শুরু: lo = 0, hi = n − 1 — পুরো অ্যারে ✓. রক্ষা: a[mid] < x হলে সাজানো ক্রমে a[0..mid]-এর সবাই < x, তাই x থাকলে a[mid+1..hi]-তে ✓; উল্টো দিকও একইভাবে। সমাপ্তি: lo > hi হলে জানালা খালি — invariant বলছে x নেই, −1 সঠিক। প্রতি ঘোরায় জানালার আকার hi − lo + 1 অন্তত অর্ধেক হয়, তাই থামে।
Complexity: জানালা n → ≤ n/2 → ≤ n/4 …, তাই ঘোরা সর্বোচ্চ ⌊log n⌋ + 1 → Θ(log n) worst case, Θ(1) best case, Θ(1) জায়গা।
নম্বর কোথায়: lo ≤ hi শর্ত (< লিখলে এক-উপাদানের জানালা বাদ পড়ে — off-by-one), mid ± 1 (না দিলে অসীম লুপ হতে পারে), আর পূর্বশর্ত “অ্যারে সাজানো”. পর্ব ১৮
গ-০২Merge sort-এর pseudocode লিখুন। এর recurrence কী? এটি কি stable আর in-place? — ৬ নম্বর (মাঝারি)
Pseudocode: MERGE-SORT(a, lo, hi) // sorts a[lo..hi-1]
if hi - lo < 2: return
mid = lo + (hi - lo) div 2
MERGE-SORT(a, lo, mid)
MERGE-SORT(a, mid, hi)
MERGE(a, lo, mid, hi) // two sorted halves -> one, using a buffer;
// on a tie take the LEFT item firstRecurrence: দুই অর্ধেক + merge-এ সর্বোচ্চ n − 1টি তুলনা: T(n) = 2T(n/2) + Θ(n) → ক-০৫-এর মতো Θ(n log n) — best, worst, average তিনটিতেই, কারণ ইনপুট যেমনই হোক ভাগ একই রকম।
Stable: হ্যাঁ, যদি সমান হলে বাঁ দিকেরটা আগে নেওয়া হয় (≤). In-place: না — merge-এর জন্য Θ(n) বাড়তি জায়গা, সাথে Θ(log n) call stack.
নম্বর কোথায়: base case (আকার < 2), merge-এর বর্ণনা, recurrence থেকে Θ(n log n), আর stability-র শর্ত “≤”. পর্ব ২১
গ-০৩Quick sort-এর worst আর average case বিশ্লেষণ করুন। randomized pivot-এ প্রত্যাশিত তুলনার সংখ্যা O(n log n) কেন? — ৮ নম্বর (মাঝারি)
Worst case: pivot প্রতিবার সবচেয়ে ছোট বা বড় হলে (যেমন সাজানো ইনপুটে শেষ উপাদান pivot, Lomuto) এক পাশে n − 1, অন্য পাশে 0: T(n) = T(n − 1) + Θ(n) → Θ(n²). Best: প্রতিবার মাঝামাঝি ভাগ: T(n) = 2T(n/2) + Θ(n) → Θ(n log n). এমনকি প্রতিবার 1:9 ভাগ হলেও গভীরতা log10/9 n = Θ(log n), মোট Θ(n log n).
Randomized — প্রত্যাশিত তুলনা: সাজানো ক্রমে উপাদানগুলো z1 < z2 < … < zn. zi আর zj (i < j) তুলনা হয় কেবল যদি {zi, …, zj}-এর মধ্যে প্রথম যে pivot বাছা হয়, সেটি zi বা zj — নইলে মাঝের pivot তাদের আলাদা দুই পাশে পাঠিয়ে দেয়। pivot এলোমেলো, তাই সম্ভাবনা 2/(j − i + 1). Linearity of expectation দিয়ে:
আরও নিখুঁতভাবে প্রায় 2n ln n ≈ 1.39 n log n. এটি সব ইনপুটের জন্য — খারাপ ইনপুট বলে কিছু নেই, শুধু দুর্ভাগ্য, যার সম্ভাবনা খুব কম।
নম্বর কোথায়: worst case কোন ইনপুটে আর কেন; randomized বিশ্লেষণে “প্রথম pivot” যুক্তি আর 2/(j − i + 1). জায়গা: in-place, গড়ে Θ(log n) stack; ছোট পাশে আগে রিকার্শন করলে worst case-এও O(log n). পর্ব ২২ · পর্ব ৬১
গ-০৪Build-heap (heapify) প্রতিটি অ-পাতা node-এ sift-down চালায়। প্রতিটি sift-down O(log n), তবু প্রমাণ করুন মোট খরচ O(n). — ৮ নম্বর (কঠিন)
Pseudocode: BUILD-MAX-HEAP(a[0..n-1])
for i = n div 2 - 1 down to 0:
SIFT-DOWN(a, i, n) // swap with larger child until heap property holdsমূল ধারণা: sift-down-এর খরচ node-এর উচ্চতার সমানুপাতিক, গভীরতার নয়। আর বেশিরভাগ node নিচের দিকে, যাদের উচ্চতা ছোট। n node-এর heap-এ উচ্চতা h-এর node সর্বোচ্চ ⌈n/2h+1⌉ টি।
কারণ Σh≥0 h/2h = 2 (একটি অভিসারী ধারা)। ∎ “n/2 টি node × O(log n) = O(n log n)” একটি সত্য কিন্তু ঢিলে সীমা — প্রশ্নে আঁটসাঁট সীমা চাওয়া হয়েছে।
উদাহরণ: n = 15 (পূর্ণ tree, উচ্চতা 3): ৮টি পাতা (উচ্চতা 0), ৪টি উচ্চতা 1, ২টি উচ্চতা 2, ১টি উচ্চতা 3. swap সর্বোচ্চ 4·1 + 2·2 + 1·3 = 11 < n. সাধারণভাবে পূর্ণ tree-তে সব উচ্চতার যোগফল n − (উচ্চতা + 1). ছোট্ট একটি পরীক্ষায় ঊর্ধ্বক্রমে সাজানো ইনপুট (max-heap-এর জন্য সবচেয়ে খারাপ) n = 1023-এ ঠিক 1013টি swap দিয়েছে = 1023 − 10.
নম্বর কোথায়: উচ্চতা অনুযায়ী node গোনা, ধারার যোগফল 2, আর “n log n ঢিলে” মন্তব্য। এর ফলেই heapsort-এর প্রথম ধাপ O(n), যদিও মোট heapsort Θ(n log n). পর্ব ২৩
গ-০৫প্রমাণ করুন যেকোনো তুলনা-ভিত্তিক সর্টিং অ্যালগরিদমের worst case-এ Ω(n log n) তুলনা লাগে। — ১০ নম্বর (কঠিন)
মডেল: তুলনা-ভিত্তিক সর্ট উপাদান সম্পর্কে শুধু “ai ≤ aj?” প্রশ্ন করে তথ্য পায়। নির্দিষ্ট n-এর জন্য এমন অ্যালগরিদমকে একটি decision tree হিসেবে আঁকা যায়: প্রতিটি ভেতরের node একটি তুলনা, দুই শাখা দুই উত্তর, প্রতিটি পাতা একটি চূড়ান্ত বিন্যাস (permutation). একটি ইনপুটে অ্যালগরিদম মূল থেকে একটি পাতা পর্যন্ত একটি পথ ধরে; worst case-এর তুলনার সংখ্যা = tree-র উচ্চতা h.
প্রমাণ:
- n টি ভিন্ন উপাদানের n! টি বিন্যাস সম্ভব, আর প্রতিটির জন্য আলাদা উত্তর দরকার — দুটি ভিন্ন ইনপুট একই পাতায় এলে অন্তত একটি ভুল সাজানো হবে। তাই পাতা ≥ n!.
- উচ্চতা h-এর binary tree-তে পাতা সর্বোচ্চ 2h. তাই 2h ≥ n!, অর্থাৎ h ≥ log(n!).
- n! ≥ (n/2)n/2 (শেষ n/2 টি গুণফলের প্রতিটি ≥ n/2), তাই log(n!) ≥ (n/2)·log(n/2) = Ω(n log n). ∎
উদাহরণ: n = 3: 3! = 6 টি পাতা, ⌈log 6⌉ = 3 — তিনটি উপাদান দুই তুলনায় সব সময় সাজানো অসম্ভব। উপসংহার: merge sort আর heapsort তাই asymptotically সেরা তুলনা-সর্ট। counting বা radix sort এই সীমা “ভাঙে” না — তারা তুলনা ছাড়াও কী-এর অঙ্ক দেখে, তাই এই মডেলের বাইরে।
নম্বর কোথায়: decision tree-র সংজ্ঞা, পাতা ≥ n! কেন, 2h ≥ পাতা, আর log(n!)-এর নিম্নসীমা (Stirling বা ওপরের সরল যুক্তি)। পর্ব ২৪
নিম্নসীমাটি সংখ্যায় দেখা যাক। প্রোগ্রামটি n = 1 থেকে 8 পর্যন্ত সব n! টি ইনপুটে merge sort আর insertion sort চালিয়ে সবচেয়ে বেশি তুলনার সংখ্যা খোঁজে, আর পাশে ⌈log n!⌉ ছাপে:
#include <algorithm>
#include <cstdio>
#include <numeric>
#include <vector>
using namespace std;
long long cmp; // global comparison counter
void msort(vector<int>& a, int lo, int hi) { // sorts a[lo..hi)
if (hi - lo < 2) return;
int mid = lo + (hi - lo) / 2;
msort(a, lo, mid); msort(a, mid, hi);
vector<int> out; int i = lo, j = mid;
while (i < mid && j < hi) { ++cmp; out.push_back(a[i] <= a[j] ? a[i++] : a[j++]); }
while (i < mid) out.push_back(a[i++]);
while (j < hi) out.push_back(a[j++]);
copy(out.begin(), out.end(), a.begin() + lo);
}
void isort(vector<int>& a) {
for (size_t i = 1; i < a.size(); ++i)
for (size_t j = i; j > 0 && (++cmp, a[j - 1] > a[j]); --j) swap(a[j - 1], a[j]);
}
int main() {
printf(" n n! ceil(lg n!) merge worst insertion worst\n");
for (int n = 1; n <= 8; ++n) {
vector<int> p(n); iota(p.begin(), p.end(), 0);
long long fact = 1, worstM = 0, worstI = 0; for (int k = 2; k <= n; ++k) fact *= k;
int lower = 0; while ((1LL << lower) < fact) ++lower;
do { // try every one of the n! inputs
vector<int> a = p; cmp = 0; msort(a, 0, n); worstM = max(worstM, cmp);
a = p; cmp = 0; isort(a); worstI = max(worstI, cmp);
} while (next_permutation(p.begin(), p.end()));
printf("%2d %6lld %12d %12lld %16lld\n", n, fact, lower, worstM, worstI);
}
}আউটপুট:
n n! ceil(lg n!) merge worst insertion worst
1 1 0 0 0
2 2 1 1 1
3 6 3 3 3
4 24 5 5 6
5 120 7 8 10
6 720 10 11 15
7 5040 13 14 21
8 40320 16 17 28
প্রতিটি সারিতে merge worst ≥ ⌈lg n!⌉ — নিম্নসীমা কখনো ভাঙে না। n ≤ 4-এ merge sort ঠিক সীমায়; n = 5-এ 8 বনাম 7 — সীমাটি 7, আর ৫টি উপাদান ৭টি তুলনায় সাজানো সত্যিই সম্ভব (merge insertion, Ford ও Johnson, ১৯৫৯), কিন্তু সাধারণ merge sort তা পারে না। insertion sort-এর কলাম ঠিক n(n−1)/2: 28 = 8·7/2 — ক-০২-এর হিসাব। দুই কলামের ফারাক n-এর সাথে দ্রুত বাড়ে: n log n বনাম n².
গ-০৬Counting sort বর্ণনা করুন। stable sort কী, আর LSD radix sort-এ stability কেন অপরিহার্য? — ৬ নম্বর (মাঝারি)
Counting sort: কী-গুলো 0..k-এর পূর্ণসংখ্যা। (১) cnt[v] = v কতবার আছে; (২) prefix sum: cnt[v] = ≤ v কী-এর সংখ্যা; (৩) ইনপুট ডান থেকে বাঁয়ে পড়ে প্রতিটি x-কে out[cnt[x] − 1]-এ বসান, cnt[x] এক কমান। সময় Θ(n + k), জায়গা Θ(n + k).
Stable: সমান কী-এর উপাদানগুলোর আগের আপেক্ষিক ক্রম অপরিবর্তিত থাকে। ধাপ (৩)-এ ডান থেকে পড়া এটিই নিশ্চিত করে।
LSD radix: d অঙ্কের সংখ্যা, প্রথমে একক অঙ্ক ধরে stable sort, তারপর দশক, … শেষে সবচেয়ে বড় অঙ্ক। Invariant: i টি পাস শেষে সংখ্যাগুলো শেষ i টি অঙ্ক ধরে সাজানো। পরের পাসে যাদের নতুন অঙ্ক সমান, stability তাদের আগের (নিচের অঙ্কের) ক্রম রাখে — stable না হলে আগের পাসের কাজ নষ্ট। উদাহরণ: 21, 12 → একক ধরে: 21, 12; দশক ধরে: 12, 21 ✓. সময় Θ(d(n + b)), b = ভিত্তি।
নম্বর কোথায়: তিন ধাপ, ডান থেকে পড়ার কারণ, invariant, আর “k বড় হলে (যেমন k = n²) counting sort অচল”. পর্ব ২৪ · পর্ব ২৫
৫ · অংশ ঘ — ট্রি, হ্যাশ, DSU আর রেঞ্জ কোয়েরি (মডিউল ৪–৫)
রিভিশনের জন্য: পর্ব ২৬, পর্ব ২৭, পর্ব ২৮, পর্ব ২৯, পর্ব ৩০, পর্ব ৩১, পর্ব ৩২, পর্ব ৩৩, পর্ব ৩৪, পর্ব ৩৫, পর্ব ৩৬, পর্ব ৩৭, পর্ব ৩৮.
ঘ-০১BST থেকে একটি key মোছার তিনটি case ব্যাখ্যা করুন। complexity কত? — ৬ নম্বর (মাঝারি)
BST-এর নিয়ম: প্রতিটি node-এর বাঁ subtree-র সব key ছোট, ডানের সব বড়। মোছার পরেও এটি রাখতে হবে।
- পাতা: সরাসরি মুছে দিন — মা-বাবার pointer NULL.
- একটি সন্তান: node-টিকে তার একমাত্র সন্তান দিয়ে প্রতিস্থাপন করুন (মা-বাবা এখন নাতিকে দেখায়)।
- দুটি সন্তান: ডান subtree-র সবচেয়ে ছোট node — inorder successor s — খুঁজুন। s-এর key এই node-এ কপি করুন, তারপর s-কে মুছুন। s-এর বাঁ সন্তান থাকতে পারে না (থাকলে সেটিই আরও ছোট হতো), তাই s মোছা case ১ বা ২.
কেন নিয়ম টেকে: successor বাঁ subtree-র সবার চেয়ে বড় আর ডানের বাকি সবার চেয়ে ছোট — তাই ওই জায়গায় বসার যোগ্য। Complexity: খোঁজা আর successor মিলিয়ে O(h), h = উচ্চতা: ভারসাম্যপূর্ণ হলে O(log n), একপাশে হেলে পড়লে O(n).
নম্বর কোথায়: তিন case, case ৩-এ successor-এর “বাঁ সন্তান নেই” যুক্তি, আর O(h) — O(log n) নয়, যদি না balance-এর কথা বলা হয়। পর্ব ২৭
ঘ-০২প্রমাণ করুন n node-এর AVL tree-র উচ্চতা O(log n). — ৮ নম্বর (কঠিন)
নিয়ম: AVL tree-তে প্রতিটি node-এর দুই subtree-র উচ্চতার পার্থক্য (balance factor) সর্বোচ্চ 1. উচ্চতা গোনা হচ্ছে edge-এ — একটি node-এর tree-র উচ্চতা 0.
ধাপ ১ — সবচেয়ে কম node: উচ্চতা h-এর AVL tree-তে সর্বনিম্ন node-সংখ্যা N(h). সবচেয়ে “পাতলা” tree-তে মূলের এক subtree উচ্চতা h − 1, অন্যটি h − 2 (পার্থক্য 1-এর বেশি হতে পারে না), আর দুটিও সর্বনিম্ন:
মানগুলো: 1, 2, 4, 7, 12, 20, 33, 54, …
ধাপ ২ — সূচকীয় বৃদ্ধি (সরল সংস্করণ): N(h) > 2·N(h − 2), কারণ N(h − 1) ≥ N(h − 2). বারবার প্রয়োগে N(h) > 2h/2 (h ≥ 2-এ; base N(0) = 1, N(1) = 2 মিলিয়ে নিন)। তাই n ≥ N(h) > 2h/2, অর্থাৎ h < 2 log n = O(log n). ∎
আঁটসাঁট সংস্করণ: N(h) + 1 = F(h + 3), Fibonacci সংখ্যা (2, 3, 5, 8, 13, …)। F(m) ≥ φm−2, φ ≈ 1.618, তাই n + 1 ≥ φh+1, আর h < 1.44 log(n + 2). অর্থাৎ AVL সবচেয়ে খারাপ অবস্থায়ও নিখুঁত tree-র চেয়ে মোটামুটি ৪৪% বেশি উঁচু।
নম্বর কোথায়: N(h)-এর recurrence আর কেন দুই subtree h − 1 ও h − 2, base case, আর অসমতা থেকে log-এ যাওয়া। পর্ব ২৮
ঘ-০৩Chaining-যুক্ত hash table-এ খোঁজার প্রত্যাশিত আর worst case সময় বিশ্লেষণ করুন। load factor কী, আর কেন rehash করতে হয়? — ৬ নম্বর (মাঝারি)
মডেল: m টি bucket, n টি key, প্রতিটি bucket একটি তালিকা (separate chaining). Load factor α = n/m — গড়ে একটি তালিকার দৈর্ঘ্য।
প্রত্যাশিত: simple uniform hashing-এর অনুমানে (প্রতিটি key স্বাধীনভাবে যেকোনো bucket-এ সমান সম্ভাবনায়) একটি তালিকার প্রত্যাশিত দৈর্ঘ্য α. ব্যর্থ খোঁজায় hash হিসাব + পুরো তালিকা: Θ(1 + α). সফল খোঁজাতেও Θ(1 + α).
Worst case: সব n key একই bucket-এ গেলে খোঁজা Θ(n). এটি ঘটে দুর্বল hash function-এ, বা কেউ ইচ্ছা করে সংঘর্ষ ঘটানো key পাঠালে (hash flooding).
Rehash: n বাড়লে α বাড়ে, খোঁজা ধীর হয়। α একটি সীমা (যেমন 1) পেরোলে m দ্বিগুণ করে সব key নতুন table-এ বসানো হয় — একবারে Θ(n), কিন্তু খ-০১-এর মতো amortized O(1). তাই α = O(1) রাখলে insert, search, delete প্রত্যাশিত O(1).
নম্বর কোথায়: অনুমানটি স্পষ্ট করা (প্রত্যাশিত O(1) কোনো শর্তহীন সত্য নয়), Θ(1 + α), আর worst case-এর কারণ। পর্ব ৩২
ঘ-০৪Union by rank ব্যবহার করলে DSU-তে find কেন O(log n)? path compression যোগ করলে কী হয়? — ৬ নম্বর (মাঝারি)
Union by rank: দুই মূলের মধ্যে ছোট rank-এর মূলকে বড়টির নিচে বসান; সমান হলে যেকোনো একটিকে নিচে দিয়ে অন্যটির rank এক বাড়ান।
দাবি: rank r-এর মূলের tree-তে অন্তত 2r টি node. Induction: r = 0-এ একটি node ✓. rank r + 1 হয় কেবল দুটি rank-r মূল জোড়া লাগলে — প্রতিটিতে ≥ 2r, মোট ≥ 2r+1 ✓. অসমান rank-এ জোড়া লাগলে rank বাড়ে না, node শুধু বাড়ে ✓. তাই 2r ≤ n, r ≤ log n. tree-র উচ্চতা ≤ rank, তাই find O(log n). ∎
Path compression: find-এর পথে পাওয়া প্রতিটি node-কে সরাসরি মূলের সাথে জুড়ে দেয়। দুটি একসাথে থাকলে m টি কাজের মোট খরচ O(m·α(n)) — inverse Ackermann α(n), যা বাস্তবের যেকোনো n-এ ৪-এর বেশি নয় (Tarjan, ১৯৭৫)। তাই প্রতি কাজ amortized “প্রায় ধ্রুবক”.
নম্বর কোথায়: 2r দাবির induction, আর α(n) amortized — worst case প্রতি কাজে নয়। পর্ব ৩৩
ঘ-০৫B-tree কী, আর ডিস্ক বা ডেটাবেসে binary search tree-র বদলে কেন B-tree ব্যবহার হয়? উচ্চতার সীমা লিখুন। — ৬ নম্বর (মাঝারি)
সংজ্ঞা: minimum degree t ≥ 2-এর B-tree-তে মূল ছাড়া প্রতিটি node-এ t − 1 থেকে 2t − 1 টি key সাজানো থাকে, k টি key-এর node-এর k + 1 টি সন্তান, আর সব পাতা একই গভীরতায়।
উচ্চতা: n ≥ 1 key-এ h ≤ logt((n + 1)/2). কারণ গভীরতা 1-এ অন্তত 2টি node, তারপর প্রতি স্তরে অন্তত t গুণ, প্রতিটিতে অন্তত t − 1 key: n ≥ 1 + 2(t − 1)(1 + t + … + th−1) = 2th − 1.
কেন ডিস্কে: ডিস্ক বা SSD থেকে একটি ব্লক আনা মেমরির একটি তুলনার চেয়ে বহু গুণ ধীর। B-tree-র একটি node = একটি ব্লক, তাই খরচ গোনা হয় ব্লক পড়ায়, আর তা উচ্চতার সমান। t = 1000 হলে 109 key-এ h ≤ log1000(5 × 108) ≈ 2.9, অর্থাৎ উচ্চতা ২ — মাত্র ৩ স্তর, মূল মেমরিতে রাখলে ২টি ব্লক পড়া। binary BST-তে উচ্চতা কমপক্ষে প্রায় 30.
নম্বর কোথায়: key-সংখ্যার সীমা, সব পাতা এক গভীরতায়, উচ্চতার সূত্র, আর ব্লক-পড়াকে খরচ ধরার যুক্তি। পর্ব ৩০
ঘ-০৬Segment tree-তে একটি range query কেন O(log n)? build আর point update-এর complexity লিখুন। — ৬ নম্বর (কঠিন)
গঠন: segment tree-র প্রতিটি node একটি অংশ [l, r]-এর উত্তর (যেমন যোগফল বা min) রাখে; সন্তানেরা অংশটি দুই ভাগ করে। উচ্চতা ⌈log n⌉.
Query [L, R]: একটি node-এর অংশ পুরো ভেতরে হলে তার মান নিন; পুরো বাইরে হলে বাদ দিন; আংশিক মিললে দুই সন্তানে যান।
দাবি: প্রতিটি স্তরে সর্বোচ্চ ২টি node “আংশিক” — যাদের থেকে নিচে যাওয়া হয়। যুক্তি: এক স্তরের অংশগুলো পাশাপাশি, পরস্পর আলাদা। [L, R]-এর সাথে আংশিক মিল হতে পারে কেবল যে node-এ L বা R সীমানাটি পড়েছে — বড়জোর দুটি; মাঝের সবাই পুরো ভেতরে। তাই প্রতি স্তরে সর্বোচ্চ ২টি node বিস্তারিত হয়, ঘোরা হয় ≤ 4টি node-এ; স্তর ⌈log n⌉ + 1, মোট O(log n). ∎
অন্যগুলো: build Θ(n) (প্রায় 2n − 1 টি node, প্রতিটি একবার); point update O(log n) (পাতা থেকে মূল পর্যন্ত এক পথ); জায়গা Θ(n) (array-তে সাধারণত 4n ঘর রাখা হয়)। range update-এর জন্য lazy propagation লাগে, তখনও O(log n).
নম্বর কোথায়: “প্রতি স্তরে ≤ 2টি আংশিক node” — এটিই প্রমাণের মূল; শুধু “উচ্চতা log n” বললে যথেষ্ট নয়। পর্ব ৩৪ · পর্ব ৩৫
৬ · অংশ ঙ — গ্রাফ অ্যালগরিদম (মডিউল ৬)
রিভিশনের জন্য: পর্ব ৩৯, পর্ব ৪০, পর্ব ৪১, পর্ব ৪২, পর্ব ৪৩, পর্ব ৪৪, পর্ব ৪৫, পর্ব ৪৬, পর্ব ৪৭, পর্ব ৪৮.
গ্রাফের প্রমাণগুলো প্রায় সবই একটি ছাঁচে পড়ে: “ধরুন প্রথম যে vertex-এ ভুল হলো…” (BFS, Dijkstra), অথবা “একটি সেরা সমাধান নিন আর একটি edge বদলান…” (MST). complexity সবসময় V আর E দুটো দিয়ে লিখুন।
ঙ-০১প্রমাণ করুন unweighted গ্রাফে BFS প্রতিটি vertex-এর সবচেয়ে ছোট দূরত্ব (edge-সংখ্যায়) সঠিকভাবে দেয়। — ৮ নম্বর (মাঝারি)
অ্যালগরিদম: BFS উৎস s-কে dist 0 দিয়ে queue-তে রাখে; queue থেকে u বের করে প্রতিটি অদেখা প্রতিবেশী v-কে dist[v] = dist[u] + 1 দিয়ে queue-তে ঢোকায়।
Lemma (queue-এর গঠন): যেকোনো মুহূর্তে queue-তে dist-এর মান সামনে থেকে পেছনে অ-হ্রাসমান, আর শেষের মান ≤ প্রথমের মান + 1. (u বের হয়ে dist[u] + 1-এর vertex পেছনে যোগ হয় — দুই শর্তই টিকে থাকে।) ফলে vertex-গুলো queue থেকে বের হয় dist-এর অ-হ্রাসমান ক্রমে।
প্রমাণ (δ(v)-এর ওপর induction, δ = আসল দূরত্ব): δ = 0: শুধু s, dist 0 ✓. ধরুন δ ≤ k-এর সব vertex সঠিক dist পেয়েছে। δ(v) = k + 1 হলে v-এর একটি প্রতিবেশী u আছে যার δ(u) = k. dist[v] ≥ k + 1, কারণ dist সবসময় একটি আসল পথের দৈর্ঘ্য। আবার lemma অনুযায়ী dist ≤ k-এর সব vertex dist = k + 1-এর কোনো vertex-এর আগে বের হয়; তাই u বের হওয়ার সময় v-কে কেউ আগে আবিষ্কার করে থাকলে তার dist ≤ k + 1, নইলে u নিজেই দেবে dist[u] + 1 = k + 1. দুই ক্ষেত্রেই dist[v] = k + 1 ✓. ∎
Complexity: adjacency list-এ প্রতিটি vertex একবার queue-তে, প্রতিটি edge সর্বোচ্চ দুবার দেখা: Θ(V + E) সময়, Θ(V) বাড়তি জায়গা। adjacency matrix-এ Θ(V²).
নম্বর কোথায়: queue-এর lemma, “dist একটি আসল পথ, তাই ≥ δ” আর induction-এর ধাপ। weighted গ্রাফে BFS কেন চলে না — এক লাইনে যোগ করুন। পর্ব ৪০
ঙ-০২প্রমাণ করুন একটি directed graph-এর topological order থাকবে যদি এবং কেবল যদি গ্রাফটি DAG হয়। Kahn-এর অ্যালগরিদম লিখুন। — ৮ নম্বর (মাঝারি)
সংজ্ঞা: Topological order হলো vertex-গুলোর এমন সারি, যেখানে প্রতিটি edge u → v-তে u আগে আসে। DAG = cycle নেই এমন directed graph.
(⇒) order আছে ⇒ cycle নেই: ধরুন cycle v1 → v2 → … → vk → v1 আছে। order-এ প্রতিটি edge সামনে যায়, তাই pos(v1) < pos(v2) < … < pos(vk) < pos(v1) — অসম্ভব।
(⇐) DAG ⇒ order আছে: প্রথমে দাবি: DAG-এ in-degree 0-এর একটি vertex আছে। নইলে যেকোনো vertex থেকে আসা-edge ধরে পেছনে হাঁটা যেত অনন্তকাল; V ধাপ হাঁটলে V + 1টি vertex পেরোতে হয়, অথচ vertex আছে V টি — তাই কোনো vertex দুবার আসে — একটি cycle, বিরোধ। সেই vertex-টি প্রথমে বসান, তাকে মুছুন; বাকিটা এখনো DAG (vertex মুছলে cycle তৈরি হয় না)। V-এর ওপর induction-এ বাকিটার order আছে; সামনে বসালে পুরো order পাওয়া যায়। ∎
Pseudocode: KAHN(G)
compute indeg[v] for all v
Q = all v with indeg[v] == 0
order = []
while Q not empty:
u = Q.pop(); order.append(u)
for each edge u -> v:
indeg[v] = indeg[v] - 1
if indeg[v] == 0: Q.push(v)
if len(order) < V: report "cycle" // not a DAG
return orderComplexity: Θ(V + E). প্রমাণের (⇐) অংশটিই এই অ্যালগরিদম — তাই order ছোট হলে নিশ্চিতভাবে cycle আছে।
নম্বর কোথায়: দুই দিক আলাদা করে, in-degree 0-এর অস্তিত্বের যুক্তি, আর cycle শনাক্তের শর্ত। পর্ব ৪২
ঙ-০৩অ-ঋণাত্মক weight-এ Dijkstra-র সঠিকতা প্রমাণ করুন। ঋণাত্মক edge থাকলে এটি কেন ব্যর্থ হতে পারে — একটি উদাহরণ দিন। — ১০ নম্বর (কঠিন)
অ্যালগরিদম: Dijkstra প্রতিবার অমীমাংসিত vertex-গুলোর মধ্যে সবচেয়ে ছোট d[u]-এর u-কে “settled” করে, তারপর u-এর বাইরের edge-গুলো relax করে: d[v] = min(d[v], d[u] + w(u, v)).
দাবি: u settled হওয়ার মুহূর্তে d[u] = δ(s, u). (d সবসময় কোনো আসল পথের দৈর্ঘ্য, তাই d[u] ≥ δ(u).)
প্রমাণ (contradiction): ধরুন u হলো প্রথম vertex যা settle হলো d[u] > δ(u) নিয়ে। s → u-এর একটি সবচেয়ে ছোট পথ P নিন। P-তে প্রথম অমীমাংসিত vertex y, আর তার আগেরটি x (settled; s নিজে settled বলে x আছে)। u প্রথম ভুল, তাই d[x] = δ(x); x settle হওয়ার সময় x → y relax হয়েছে, তাই d[y] ≤ δ(x) + w(x, y) = δ(y). এখন weight অ-ঋণাত্মক বলে P-তে y থেকে u পর্যন্ত বাকি অংশের দৈর্ঘ্য ≥ 0, তাই δ(y) ≤ δ(u). মিলিয়ে: d[y] ≤ δ(y) ≤ δ(u) < d[u]. কিন্তু Dijkstra u-কে বেছেছে সবচেয়ে ছোট d হিসেবে, অথচ y-ও অমীমাংসিত ছিল — তাই d[u] ≤ d[y]. বিরোধ। ∎
ঋণাত্মক edge: প্রমাণের যে লাইনে δ(y) ≤ δ(u) লেগেছে, সেটি ঋণাত্মক edge-এ ভাঙে — একটি settled vertex-এর দূরত্ব পরে আরও কমতে পারে। নিচের ডায়াগ্রাম আর প্রোগ্রামে উদাহরণ: S → A (2), S → B (5), A → C (1), B → A (−4). আসল দূরত্ব A = 1 (S → B → A), C = 2; Dijkstra দেয় A = 2, C = 3.
Complexity: binary heap-এ O((V + E) log V); array-তে min খুঁজলে Θ(V²) — ঘন গ্রাফে এটিই ভালো।
নম্বর কোথায়: “প্রথম ভুল vertex” কাঠামো, x আর y-এর সংজ্ঞা, ঠিক কোন লাইনে অ-ঋণাত্মকতা লাগল, আর একটি নির্দিষ্ট পাল্টা-উদাহরণ (counterexample). পর্ব ৪৩
2 1 S ──────► A ──────► C │ ▲ 5 │ │ −4 ▼ │ B ────────┘
| কাজ | |
| d[A] | |
| d[B] | |
| d[C] | |
| settled |
- শুরু. d[S] = 0, বাকি সব ∞. কোনো vertex এখনো settled নয়।
- S settle. সবচেয়ে ছোট d = 0. S → A দেয় d[A] = 2, S → B দেয় d[B] = 5.
- A settle. অমীমাংসিতদের মধ্যে সবচেয়ে ছোট A (2) — Dijkstra এখন ধরে নেয় A-র দূরত্ব চূড়ান্ত। A → C দেয় d[C] = 2 + 1 = 3.
- C settle. বাকি দুটির মধ্যে C (3) < B (5). C-র বাইরের কোনো edge নেই।
- B settle. B → A দিলে 5 + (−4) = 1 < 2, কিন্তু A আগেই settled, তাই আর বদলায় না। আর বদলালেও C-কে কেউ আবার relax করত না — C = 3 থেকেই যেত।
- আসল উত্তর. S → B → A = 1, S → B → A → C = 2. Dijkstra-র দুটি মানই ভুল। প্রমাণের δ(y) ≤ δ(u) ধাপটি এখানে ভেঙেছে: B-র δ = 5, অথচ তার পরের A-র δ = 1. ঋণাত্মক edge থাকলে Bellman-Ford লাগে।
#include <cstdio>
#include <vector>
using namespace std;
struct Edge { int u, v, w; };
const int INF = 1000000000;
const char* NAME = "SABC";
int main() {
int n = 4; // S=0, A=1, B=2, C=3
vector<Edge> E = {{0, 1, 2}, {0, 2, 5}, {1, 3, 1}, {2, 1, -4}};
// Dijkstra: settle the closest unsettled vertex, never revisit it
vector<int> d(n, INF); vector<bool> done(n, false); d[0] = 0;
printf("Dijkstra settles:");
for (int it = 0; it < n; ++it) {
int u = -1;
for (int x = 0; x < n; ++x) if (!done[x] && (u < 0 || d[x] < d[u])) u = x;
done[u] = true; printf(" %c(%d)", NAME[u], d[u]);
for (auto [a, b, w] : E) if (a == u && !done[b] && d[u] + w < d[b]) d[b] = d[u] + w;
}
printf("\nDijkstra A=%d B=%d C=%d\n", d[1], d[2], d[3]);
// Bellman-Ford: relax every edge, n-1 rounds, then one check round
vector<int> bf(n, INF); bf[0] = 0;
for (int r = 1; r < n; ++r)
for (auto [a, b, w] : E) if (bf[a] != INF && bf[a] + w < bf[b]) bf[b] = bf[a] + w;
bool negCycle = false;
for (auto [a, b, w] : E) if (bf[a] != INF && bf[a] + w < bf[b]) negCycle = true;
printf("Bellman-Ford A=%d B=%d C=%d negative cycle: %s\n", bf[1], bf[2], bf[3], negCycle ? "yes" : "no");
}আউটপুট:
Dijkstra settles: S(0) A(2) C(3) B(5)
Dijkstra A=2 B=5 C=3
Bellman-Ford A=1 B=5 C=2 negative cycle: no
settle-এর ক্রম S, A, C, B — ডায়াগ্রামের মতোই। Dijkstra দেয় A = 2, C = 3; Bellman-Ford দেয় A = 1, C = 2, আর কোনো negative cycle নেই (শেষ যাচাই-রাউন্ডে কিছুই কমেনি)। অর্থাৎ ঋণাত্মক edge আছে কিন্তু ঋণাত্মক cycle নেই — তবু Dijkstra ভুল। “ঋণাত্মক cycle না থাকলে Dijkstra চলে” — এটি একটি প্রচলিত ভুল ধারণা।
ঙ-০৪Bellman-Ford কেন V − 1 রাউন্ডেই যথেষ্ট? ঋণাত্মক cycle কীভাবে শনাক্ত করে? — ৮ নম্বর (মাঝারি)
অ্যালগরিদম: Bellman-Ford d[s] = 0, বাকি ∞; তারপর V − 1 বার সব E টি edge relax করে।
Lemma: i টি রাউন্ড শেষে প্রতিটি v-এর জন্য d[v] ≤ সর্বোচ্চ i টি edge-এর s → v পথগুলোর মধ্যে সবচেয়ে ছোটটির দৈর্ঘ্য। Induction: i = 0-এ শুধু s, 0 ✓. ধরুন i − 1-এ সত্য। ≤ i edge-এর সেরা পথের শেষ edge u → v; পথের বাকিটা ≤ i − 1 edge, তাই d[u] ইতিমধ্যে ততটা ছোট; রাউন্ড i-তে u → v relax হলে d[v] ≤ d[u] + w ✓.
কেন V − 1: ঋণাত্মক cycle না থাকলে একটি সবচেয়ে ছোট পথ simple ধরা যায় (cycle-এর weight ≥ 0, তাই বাদ দিলে পথ লম্বা হয় না), আর simple পথে সর্বোচ্চ V − 1 টি edge. lemma অনুযায়ী V − 1 রাউন্ড শেষে d[v] ≤ δ(v); আর d সবসময় একটি আসল পথ, তাই d[v] = δ(v). ∎
শনাক্তকরণ: আরও একটি রাউন্ড চালান। কোনো edge-এ তখনো d[u] + w < d[v] হলে s থেকে পৌঁছানো যায় এমন একটি negative cycle আছে — কারণ না থাকলে ওপরের প্রমাণ বলে সব d ইতিমধ্যে চূড়ান্ত।
Complexity: Θ(V·E) সময়, Θ(V) জায়গা। কোনো রাউন্ডে কিছু না বদলালে আগেই থামানো যায়। ওপরের প্রোগ্রামে ৪টি vertex, তাই ৩ রাউন্ড।
নম্বর কোথায়: lemma-র নির্ভুল বিবৃতি (“≤ i edge-এর পথ”), simple path-এ V − 1 edge, আর V-তম রাউন্ডের যুক্তি। পর্ব ৪৪
ঙ-০৫MST-এর cut property বিবৃত করে প্রমাণ করুন, এবং এ থেকে Kruskal-এর সঠিকতা দেখান। — ১০ নম্বর (কঠিন)
সংজ্ঞা: একটি cut (S, V − S) হলো vertex-গুলোর দুই ভাগ; যে edge-এর দুই প্রান্ত দুই ভাগে, সেটি cut-কে অতিক্রম করে (crossing edge).
Cut property: ধরুন A কোনো একটি MST-র edge-এর উপসেট, আর cut (S, V − S)-কে A-র কোনো edge অতিক্রম করে না। তাহলে cut অতিক্রমকারী সবচেয়ে হালকা edge e = (u, v) যোগ করলে A ∪ {e}-ও কোনো MST-র উপসেট।
প্রমাণ (exchange): T একটি MST, A ⊆ T. e ∈ T হলে শেষ। নইলে T ∪ {e}-তে ঠিক একটি cycle তৈরি হয়, যাতে আছে u থেকে v-এর T-পথ। u আর v দুই ভাগে, তাই এই পথ অন্তত আরেকবার cut অতিক্রম করে — একটি edge e′ দিয়ে। e′ ∉ A (A cut অতিক্রম করে না)। T′ = T − {e′} + {e} আবার একটি spanning tree (cycle ভাঙল, সংযোগ রইল)। e সবচেয়ে হালকা, তাই w(e) ≤ w(e′), আর w(T′) ≤ w(T). T নিজে minimum, তাই T′-ও MST, এবং A ∪ {e} ⊆ T′. ∎
Kruskal: edge-গুলো weight-এর ক্রমে দেখে; যে edge দুটি আলাদা component জোড়ে তা নেয়। নেওয়া edge (u, v)-এর জন্য S = u-এর বর্তমান component নিন: আগের কোনো নেওয়া edge এই cut অতিক্রম করে না, আর এর চেয়ে হালকা কোনো crossing edge থাকলে সেটি আগেই দেখা হতো এবং নেওয়া হতো। তাই এটি সবচেয়ে হালকা crossing edge — cut property অনুযায়ী নিরাপদ। V − 1টি নিরাপদ edge = একটি MST. complexity O(E log E) = O(E log V) (sort + DSU).
নম্বর কোথায়: cut property-র তিনটি শর্ত (A ⊆ MST, A cut মানে, e সবচেয়ে হালকা), cycle-এ দ্বিতীয় crossing edge e′-এর অস্তিত্ব, আর Kruskal-এ cut-টি কী তা স্পষ্ট বলা। পর্ব ৪৫
ঙ-০৬Max-flow min-cut theorem বিবৃত করুন। Ford-Fulkerson পদ্ধতি সংক্ষেপে লিখুন এবং একটি ছোট network-এ যাচাই করুন। — ৮ নম্বর (কঠিন)
বিবৃতি: যেকোনো flow network-এ উৎস s থেকে গন্তব্য t-এর সর্বোচ্চ flow-এর মান = সব s-t cut-এর মধ্যে সবচেয়ে ছোট capacity. সমতুল্যভাবে, একটি flow f-এর জন্য এই তিনটি একই কথা: (১) f সর্বোচ্চ; (২) residual graph-এ কোনো augmenting path নেই; (৩) কোনো cut (S, T)-এর জন্য |f| = c(S, T).
মূল যুক্তি: যেকোনো flow ≤ যেকোনো cut, কারণ s থেকে t-এ যাওয়া সব flow-কে cut পেরোতে হয় (weak duality). augmenting path না থাকলে S = residual graph-এ s থেকে পৌঁছানো যায় এমন vertex নিন: S থেকে বের হওয়া প্রতিটি edge পুরো ভরা, আর ঢোকা প্রতিটি edge-এ flow 0 — তাই |f| = c(S, T), আর f সর্বোচ্চ।
Pseudocode: FORD-FULKERSON(G, s, t)
f = 0 on every edge
while there is an s-t path p in the residual graph:
b = minimum residual capacity on p // bottleneck
push b units along p (update forward and reverse edges)
return fউদাহরণ: s → a (4), s → b (2), a → b (3), a → t (1), b → t (4). flow: s → a 4 (তার 1 যায় a → t, 3 যায় a → b), s → b 1, b → t 4. মান 1 + 4 = 5. cut S = {s, a, b}: বের হয় a → t (1) + b → t (4) = 5. flow = cut, তাই দুটিই সেরা — আর কোনো augmenting path নেই।
Complexity: পূর্ণসংখ্যার capacity-তে প্রতি augment-এ ≥ 1 বাড়ে: O(E·|f*|). BFS দিয়ে সবচেয়ে ছোট পথ নিলে (Edmonds-Karp) O(V·E²), capacity যা-ই হোক।
নম্বর কোথায়: তিন শর্তের সমতুল্যতা, residual graph-এ reverse edge-এর কথা, আর উদাহরণে flow ও cut দুটোই দেখানো। পর্ব ৪৭
৭ · অংশ চ — অ্যালগরিদম ডিজাইন কৌশল (মডিউল ৭)
রিভিশনের জন্য: পর্ব ৪৯, পর্ব ৫০, পর্ব ৫১, পর্ব ৫২, পর্ব ৫৩, পর্ব ৫৪, পর্ব ৫৫, পর্ব ৫৬, পর্ব ৫৭, পর্ব ৫৮, পর্ব ৫৯.
greedy-র প্রশ্নে প্রমাণ ছাড়া উত্তর অর্ধেক; DP-র প্রশ্নে state-এর অর্থ ছাড়া টেবিল অর্থহীন। দুই ক্ষেত্রেই শুরু করুন একটি বাক্যে: “greedy পছন্দ হলো …” বা “dp[i][j] মানে …”.
চ-০১Activity selection-এ “সবচেয়ে আগে শেষ হওয়া কাজটি নাও” greedy কেন সর্বোচ্চ সংখ্যক কাজ দেয়? exchange argument দিয়ে প্রমাণ করুন। — ৮ নম্বর (মাঝারি)
সমস্যা: n টি কাজ, প্রতিটির শুরু s আর শেষ f. পরস্পর না-মেলা (একটি শেষ হওয়ার পর অন্যটি শুরু) সবচেয়ে বেশি কাজ বাছতে হবে — যেমন একটি হলরুমে সবচেয়ে বেশি অনুষ্ঠান।
Greedy: f অনুযায়ী সাজান; প্রথমটি নিন; তারপর প্রতিটি কাজ নিন যার s ≥ শেষ নেওয়া কাজের f.
প্রমাণ (exchange argument): g1 = সবচেয়ে আগে শেষ হওয়া কাজ। যেকোনো সেরা সমাধান O নিন, শেষের ক্রমে o1, o2, …, ok. o1-এর জায়গায় g1 বসান: f(g1) ≤ f(o1) ≤ s(o2), তাই g1 বাকিদের সাথে মেলে না, আর সংখ্যা এখনো k. অর্থাৎ g1-সহ একটি সেরা সমাধান আছে (greedy-choice property). g1 নেওয়ার পর বাকি সমস্যা হলো “f(g1)-এর পর শুরু হওয়া কাজগুলো” — একই ধরনের ছোট সমস্যা, আর তার সেরা সমাধান + g1 = পুরোটার সেরা (optimal substructure). k-এর ওপর induction-এ greedy সেরা। ∎
উদাহরণ: (s, f) = (1,4), (3,5), (0,6), (5,7), (3,9), (5,9), (6,10), (8,11), (8,12), (2,14), (12,16). greedy নেয় (1,4), (5,7), (8,11), (12,16) — ৪টি।
Complexity: sort O(n log n) + এক পাস O(n). সতর্কতা: “সবচেয়ে ছোট কাজ আগে” বা “সবচেয়ে আগে শুরু” greedy ভুল — একটি করে পাল্টা-উদাহরণ খুঁজে দেখুন।
নম্বর কোথায়: বদলানোর পরও সমাধান বৈধ — f(g1) ≤ f(o1) ≤ s(o2) অসমতাটি; আর সংখ্যা কমেনি। পর্ব ৫০
চ-০২a:5, b:9, c:12, d:13, e:16, f:45 ফ্রিকোয়েন্সির জন্য Huffman code তৈরি করুন, মোট বিট বের করুন, আর অ্যালগরিদমটি কেন সেরা — সংক্ষেপে যুক্তি দিন। — ১০ নম্বর (কঠিন)
অ্যালগরিদম: প্রতিটি অক্ষর একটি পাতা, একটি min-heap-এ। বারবার সবচেয়ে কম ফ্রিকোয়েন্সির দুটি বের করে জোড়া লাগান (নতুন node-এর ফ্রিকোয়েন্সি = যোগফল), heap-এ ফেরত দিন — যতক্ষণ না একটি tree থাকে।
Trace: 5 + 9 = 14 → 12 + 13 = 25 → 14 + 16 = 30 → 25 + 30 = 55 → 45 + 55 = 100. কোডের দৈর্ঘ্য: f = 1, c = d = e = 3, a = b = 4 বিট; যেমন f = 0, c = 100, d = 101, a = 1100, b = 1101, e = 111 (বাঁ = 0, ডান = 1 ধরে)।
৬টি অক্ষরের জন্য নির্দিষ্ট দৈর্ঘ্যের কোডে লাগত ৩ বিট × ১০০ = ৩০০ বিট। কোনো কোড অন্যটির শুরু নয় (prefix-free), তাই পড়ার সময় দ্ব্যর্থতা নেই।
সেরা কেন (স্কেচ): (১) Greedy choice: সবচেয়ে কম ফ্রিকোয়েন্সির দুই অক্ষর x, y-কে সবচেয়ে গভীর স্তরে ভাই বানানো এমন একটি সেরা tree আছে — যেকোনো সেরা tree-র সবচেয়ে গভীর দুই ভাই-পাতার সাথে x, y অদলবদল করলে খরচ বাড়ে না (কম ফ্রিকোয়েন্সি গভীরে গেল)। (২) Optimal substructure: x, y-কে একটি নতুন অক্ষর z (f = fx + fy) দিয়ে বদলালে B(T) = B(T′) + fx + fy, তাই ছোট সমস্যার সেরা tree থেকে বড়টির সেরা tree পাওয়া যায়। অক্ষর-সংখ্যার ওপর induction. ∎
Complexity: n অক্ষরে O(n log n) (heap-এ n − 1 বার merge).
নম্বর কোথায়: প্রতিটি merge-এর ক্রম, tree-র ছবি, প্রতিটি কোড ও তার দৈর্ঘ্য, মোট বিট, আর দুই lemma-র নাম। পর্ব ৫১
চ-০৩LCS-এর recurrence লিখুন। X = ABCBDAB, Y = BDCABA-এর জন্য টেবিল ভরে LCS-এর দৈর্ঘ্য আর একটি LCS বের করুন। — ১০ নম্বর (মাঝারি)
State: L[i][j] = X-এর প্রথম i অক্ষর আর Y-এর প্রথম j অক্ষরের LCS-এর দৈর্ঘ্য।
কেন ঠিক: শেষ দুই অক্ষর মিললে কোনো একটি LCS সেটি দিয়েই শেষ হয় (exchange); না মিললে LCS-এ অন্তত একটি শেষ অক্ষর নেই, তাই দুই সম্ভাবনার বড়টি।
| i \ j | ε | B | D | C | A | B | A |
|---|---|---|---|---|---|---|---|
| ε | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| A | 0 | 0 | 0 | 0 | 1 | 1 | 1 |
| B | 0 | 1 | 1 | 1 | 1 | 2 | 2 |
| C | 0 | 1 | 1 | 2 | 2 | 2 | 2 |
| B | 0 | 1 | 1 | 2 | 2 | 3 | 3 |
| D | 0 | 1 | 2 | 2 | 2 | 3 | 3 |
| A | 0 | 1 | 2 | 2 | 3 | 3 | 4 |
| B | 0 | 1 | 2 | 2 | 3 | 4 | 4 |
উত্তর: L[7][6] = 4. কোণ থেকে পেছনে হাঁটলে (মিললে কোণাকুণি, না মিললে বড় দিকে, সমান হলে ওপরে) পাওয়া যায় BCBA. অন্য LCS-ও আছে (যেমন BDAB); “একটি LCS” চাইলে যেকোনো একটি চলে।
Complexity: (n + 1)(m + 1) ঘর, প্রতিটি Θ(1): Θ(nm) সময়, Θ(nm) জায়গা; শুধু দৈর্ঘ্য চাইলে দুই সারি — O(min(n, m)).
নম্বর কোথায়: state-এর অর্থ, base case, দুই case-এর যুক্তি, সম্পূর্ণ টেবিল, পুনর্গঠনের নিয়ম আর উত্তর। পর্ব ৫৪
চ-০৪0/1 knapsack-এর DP recurrence লিখুন। ধারণক্ষমতা W = 7, জিনিস (ওজন, মূল্য) = (1, 1), (3, 4), (4, 5), (5, 7)-এর টেবিল ভরুন। মূল্য/ওজন অনুপাতে greedy কেন ব্যর্থ? — ১০ নম্বর (মাঝারি)
State: K[i][c] = প্রথম i টি জিনিস থেকে, মোট ওজন ≤ c রেখে, সর্বোচ্চ মূল্য।
| i (w, v) \ c | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| 0 — | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 (1, 1) | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 2 (3, 4) | 0 | 1 | 1 | 4 | 5 | 5 | 5 | 5 |
| 3 (4, 5) | 0 | 1 | 1 | 4 | 5 | 6 | 6 | 9 |
| 4 (5, 7) | 0 | 1 | 1 | 4 | 5 | 7 | 8 | 9 |
উত্তর: K[4][7] = 9: জিনিস ২ আর ৩ (ওজন 3 + 4 = 7, মূল্য 4 + 5 = 9). পুনর্গঠন: K[4][7] = K[3][7] → জিনিস ৪ নেই; K[3][7] ≠ K[2][7] → জিনিস ৩ আছে, c = 3; K[2][3] ≠ K[1][3] → জিনিস ২ আছে, c = 0.
Greedy ব্যর্থ: অনুপাত 1, 1.33, 1.25, 1.4. greedy আগে নেয় (5, 7), বাকি জায়গা 2-এ শুধু (1, 1) — মোট 8 < 9. ভগ্নাংশ নেওয়া গেলে (fractional knapsack) অনুপাত-greedy সেরা, কিন্তু 0/1-এ নয়।
Complexity: Θ(nW) সময়, Θ(nW) জায়গা (এক সারিতে Θ(W), তখন c-কে উল্টো দিকে চালাতে হয়)। এটি pseudo-polynomial — W-এর মানে polynomial, W-এর বিট-সংখ্যায় নয়।
নম্বর কোথায়: state, recurrence-এর দুই শাখা ও শর্ত wi ≤ c, টেবিল, পুনর্গঠন, আর greedy-র পাল্টা-উদাহরণ। পর্ব ৫৩
দুটি টেবিলই প্রোগ্রাম দিয়ে যাচাই করা যাক। নিজের ভাষার ট্যাব বেছে নিন — দুটির আউটপুট হুবহু এক:
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int main() {
string x = "ABCBDAB", y = "BDCABA"; // LCS
size_t n = x.size(), m = y.size();
vector<vector<int>> L(n + 1, vector<int>(m + 1, 0));
for (size_t i = 1; i <= n; ++i)
for (size_t j = 1; j <= m; ++j)
L[i][j] = x[i-1] == y[j-1] ? L[i-1][j-1] + 1 : max(L[i-1][j], L[i][j-1]);
string lcs; // walk back; on a tie go up
for (size_t i = n, j = m; i > 0 && j > 0; ) {
if (x[i-1] == y[j-1]) { lcs += x[i-1]; --i; --j; }
else if (L[i-1][j] >= L[i][j-1]) --i; else --j;
}
reverse(lcs.begin(), lcs.end());
cout << "LCS length " << L[n][m] << ": " << lcs << '\n';
vector<int> w = {1, 3, 4, 5}, v = {1, 4, 5, 7}; int W = 7; // 0/1 knapsack
vector<vector<int>> K(w.size() + 1, vector<int>(W + 1, 0));
for (size_t i = 1; i <= w.size(); ++i)
for (int c = 0; c <= W; ++c) {
K[i][c] = K[i-1][c]; // skip item i
if (w[i-1] <= c) K[i][c] = max(K[i][c], K[i-1][c - w[i-1]] + v[i-1]);
}
for (auto& row : K) { for (int val : row) cout << val << ' '; cout << '\n'; }
cout << "take items:"; for (size_t i = w.size(), c = W; i > 0; --i)
if (K[i][c] != K[i-1][c]) { cout << ' ' << i; c -= w[i-1]; }
cout << " best value " << K[w.size()][W] << '\n';
}আউটপুট:
LCS length 4: BCBA
0 0 0 0 0 0 0 0
0 1 1 1 1 1 1 1
0 1 1 4 5 5 5 5
0 1 1 4 5 6 6 9
0 1 1 4 5 7 8 9
take items: 3 2 best value 9
প্রথম লাইন চ-০৩-এর উত্তর: দৈর্ঘ্য 4, আর সমান হলে ওপরে যাওয়ার নিয়মে LCS = BCBA. পরের পাঁচ লাইন চ-০৪-এর টেবিলের পাঁচটি সারি — হাতে ভরা টেবিলের সাথে প্রতিটি ঘর মেলে। শেষ লাইনে পুনর্গঠন: জিনিস ৩ আর ২, মূল্য 9.
x, y = "ABCBDAB", "BDCABA" # LCS
n, m = len(x), len(y)
L = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(1, m + 1):
L[i][j] = L[i-1][j-1] + 1 if x[i-1] == y[j-1] else max(L[i-1][j], L[i][j-1])
lcs, i, j = [], n, m # walk back; on a tie go up
while i > 0 and j > 0:
if x[i-1] == y[j-1]:
lcs.append(x[i-1]); i -= 1; j -= 1
elif L[i-1][j] >= L[i][j-1]:
i -= 1
else:
j -= 1
print(f"LCS length {L[n][m]}: {''.join(reversed(lcs))}")
w, v, W = [1, 3, 4, 5], [1, 4, 5, 7], 7 # 0/1 knapsack
K = [[0] * (W + 1) for _ in range(len(w) + 1)]
for i in range(1, len(w) + 1):
for c in range(W + 1):
K[i][c] = K[i-1][c] # skip item i
if w[i-1] <= c:
K[i][c] = max(K[i][c], K[i-1][c - w[i-1]] + v[i-1])
for row in K:
print(" ".join(map(str, row)) + " ")
picked, c = [], W
for i in range(len(w), 0, -1):
if K[i][c] != K[i-1][c]:
picked.append(str(i)); c -= w[i-1]
print("take items: " + " ".join(picked) + f" best value {K[len(w)][W]}")আউটপুট C++-এর সাথে হুবহু এক:
LCS length 4: BCBA
0 0 0 0 0 0 0 0
0 1 1 1 1 1 1 1
0 1 1 4 5 5 5 5
0 1 1 4 5 6 6 9
0 1 1 4 5 7 8 9
take items: 3 2 best value 9
একই recurrence, একই পুনর্গঠন। টেবিলের প্রতিটি সারির শেষে একটি ফাঁকা অক্ষর রাখা হয়েছে, যাতে C++-এর আউটপুটের সাথে অক্ষরে অক্ষরে মেলে।
চ-০৫Matrix chain multiplication-এর recurrence লিখুন। মাত্রা p = (10, 30, 5, 60, 10)-এর চারটি matrix-এর জন্য সবচেয়ে কম গুণের সংখ্যা ও বন্ধনী বের করুন। — ১০ নম্বর (কঠিন)
State: m[i][j] = Ai…Aj গুণ করার সবচেয়ে কম scalar গুণ; Ai-এর মাত্রা pi−1 × pi.
ভরার ক্রম: দৈর্ঘ্য অনুযায়ী (কর্ণ ধরে) — ছোট chain আগে।
- দৈর্ঘ্য ২: m[1][2] = 10·30·5 = 1500; m[2][3] = 30·5·60 = 9000; m[3][4] = 5·60·10 = 3000.
- m[1][3]: k = 1 → 0 + 9000 + 10·30·60 = 27000; k = 2 → 1500 + 0 + 10·5·60 = 4500. সেরা 4500 (k = 2).
- m[2][4]: k = 2 → 0 + 3000 + 30·5·10 = 4500; k = 3 → 9000 + 0 + 30·60·10 = 27000. সেরা 4500 (k = 2).
- m[1][4]: k = 1 → 0 + 4500 + 10·30·10 = 7500; k = 2 → 1500 + 3000 + 10·5·10 = 5000; k = 3 → 4500 + 0 + 10·60·10 = 10500. সেরা 5000 (k = 2).
উত্তর: 5000 গুণ, বন্ধনী ((A1A2)(A3A4)). পাঁচটি সম্ভাব্য বন্ধনীর মধ্যে সবচেয়ে খারাপটি (A1(A2A3))A4: 9000 + 18000 + 6000 = 33000 — সেরার ছয় গুণেরও বেশি।
Complexity: Θ(n²) টি ঘর, প্রতিটিতে O(n) টি k: Θ(n³) সময়, Θ(n²) জায়গা।
নম্বর কোথায়: মাত্রার নির্দেশ pi−1 × pi, প্রতিটি k-এর হিসাব আলাদা করে, s টেবিল (সেরা k) থেকে বন্ধনী। পর্ব ৫৪
চ-০৬KMP-এর prefix function কী? "ababaca"-র জন্য বের করুন, আর প্রমাণ করুন এর হিসাব O(m) সময়ে হয়। — ৮ নম্বর (কঠিন)
সংজ্ঞা: prefix function π[q] = P[0..q]-এর সবচেয়ে লম্বা এমন proper prefix-এর দৈর্ঘ্য, যা ওই অংশের suffix-ও (border). মিল ভাঙলে KMP pattern-কে কতটা সরাবে, তা এটিই বলে — text-এর pointer কখনো পেছনে যায় না।
Pseudocode: PREFIX-FUNCTION(P[0..m-1])
pi[0] = 0; k = 0
for q = 1 to m-1:
while k > 0 and P[k] != P[q]:
k = pi[k-1] // fall back to a shorter border
if P[k] == P[q]: k = k + 1
pi[q] = k"ababaca": π = 0, 0, 1, 2, 3, 0, 1. যেমন q = 4: "ababa"-র border "aba", দৈর্ঘ্য 3; q = 5-এ 'c' কোনো border বাড়াতে পারে না, k পড়ে যায় 3 → 1 → 0.
O(m) প্রমাণ (aggregate / potential): k-কে একটি সঞ্চয় হিসেবে ভাবুন। for-লুপের প্রতিটি ঘোরায় k সর্বোচ্চ 1 বাড়ে, তাই মোট বৃদ্ধি ≤ m − 1. while-লুপের প্রতিটি ধাপে k কঠোরভাবে কমে (π[k−1] < k), আর k কখনো ঋণাত্মক হয় না। তাই while-এর মোট ধাপ ≤ মোট বৃদ্ধি ≤ m − 1. মোট কাজ O(m). ∎ একই যুক্তিতে n দৈর্ঘ্যের text-এ খোঁজা O(n), সব মিলিয়ে O(n + m) — naive-এর O(nm)-এর বদলে।
নম্বর কোথায়: সংজ্ঞায় “proper” শব্দটি, প্রতিটি π মান, আর “k মোট যত বাড়ে তার বেশি কমতে পারে না” যুক্তি। পর্ব ৫৮
প্রমাণটি সংখ্যায় দেখতে, প্রোগ্রামটি প্রতিটি অক্ষর-তুলনা গোনে আর 2(m − 1)-এর সাথে মেলায়:
#include <cstdio>
#include <string>
#include <vector>
using namespace std;
// prefix function pi[q] = length of the longest proper border of P[0..q]
vector<int> prefixFunction(const string& P, long long& comps) {
vector<int> pi(P.size(), 0);
int k = 0; comps = 0;
for (size_t q = 1; q < P.size(); ++q) {
while (true) {
++comps;
if (P[k] == P[q]) { ++k; break; } // border grows by one
if (k == 0) break;
k = pi[k - 1]; // fall back: k strictly decreases
}
pi[q] = k;
}
return pi;
}
int main() {
for (string P : {string("ababaca"), string("aabaaab"), string(999, 'a') + "b"}) {
long long c;
vector<int> pi = prefixFunction(P, c);
if (P.size() <= 10) { printf("%-8s pi =", P.c_str()); for (int x : pi) printf(" %d", x); }
else printf("a^999 b pi[last] = %d", pi.back());
printf(" comparisons %lld <= 2(m-1) = %zu\n", c, 2 * (P.size() - 1));
}
}আউটপুট:
ababaca pi = 0 0 1 2 3 0 1 comparisons 8 <= 2(m-1) = 12
aabaaab pi = 0 1 0 1 2 2 3 comparisons 8 <= 2(m-1) = 12
a^999 b pi[last] = 0 comparisons 1997 <= 2(m-1) = 1998
"ababaca"-র π = 0 0 1 2 3 0 1 — হাতের উত্তরের সাথে মেলে। তুলনা কখনো 2(m − 1)-এর বেশি হয় না, কারণ প্রতিটি ঘোরায় একটি তুলনা “সফল বা শেষ” আর বাকিগুলো প্রতিটি k-কে কমায়, যা মোট বৃদ্ধির বেশি হতে পারে না। কঠিনতম ধাঁচ — ৯৯৯টি a-এর পর একটি b — তেও 1997 ≤ 1998: শেষ অক্ষর b-তে k একবারে নয়, 998 থেকে এক এক করে শূন্যে নামে — ৯৯৯টি তুলনা এক ঘোরাতেই — কিন্তু সেই নামার “দাম” আগের ৯৯৮টি বৃদ্ধিতেই জমা ছিল।
চ-০৭N-queens সমস্যার backtracking pseudocode লিখুন। pruning কীভাবে কাজ করে? — ৬ নম্বর (মাঝারি)
Pseudocode: PLACE(r) // queens already in rows 0..r-1
if r == n: record solution; return
for c = 0 to n-1:
if not col[c] and not d1[r+c] and not d2[r-c+n-1]:
put queen at (r, c); mark col[c], d1[r+c], d2[r-c+n-1]
PLACE(r + 1)
remove queen; unmark the three // undo = backtrackব্যাখ্যা: প্রতি সারিতে একটি রানি — তাই সারির সংঘর্ষ আপনা থেকেই নেই। কলাম আর দুই কর্ণের চিহ্ন দিয়ে প্রতিটি পরীক্ষা O(1). সংঘর্ষ হলে সেই শাখার পুরো subtree না দেখেই ফেরা — এটিই pruning.
Complexity: সরল সীমা O(n!) (সারি r-এ বড়জোর n − r টি কলাম খোলা), আর কর্ণের pruning বাস্তবে এর চেয়ে অনেক কম। n = 8-এ সমাধান ৯২টি। জায়গা Θ(n).
নম্বর কোথায়: base case, “চেষ্টা → recurse → undo” তিন অংশ, কর্ণের সূচক r + c আর r − c, আর pruning-এর ব্যাখ্যা। পর্ব ৫৬
৮ · অংশ ছ — উন্নত বিশ্লেষণ ও তত্ত্ব (মডিউল ৮)
রিভিশনের জন্য: পর্ব ৬০, পর্ব ৬১, পর্ব ৬২, পর্ব ৬৩, পর্ব ৬৪, পর্ব ৬৫, পর্ব ৬৬, পর্ব ৬৭.
ছ-০১Amortized analysis-এর তিনটি পদ্ধতি একটি binary counter-এর increment দিয়ে ব্যাখ্যা করুন। — ৮ নম্বর (মাঝারি)
সমস্যা: 0 থেকে শুরু করা একটি binary counter-এ n বার increment; খরচ = কয়টি বিট উল্টায়। একটি increment-এর worst case Θ(k) (যেমন 0111…1 → 1000…0). তাহলে n বারে কি O(nk)? না — O(n).
(১) Aggregate: বিট 0 প্রতিবার উল্টায়, বিট 1 প্রতি দুবারে একবার, বিট i প্রতি 2i বারে একবার। মোট ≤ Σ n/2i < 2n. প্রতি increment amortized < 2.
(২) Accounting: একটি বিট 0 → 1 করতে ২ টাকা নিন: ১ টাকা এখনকার উল্টানোয়, ১ টাকা সেই বিটের গায়ে জমা। পরে 1 → 0 উল্টানোর খরচ সেই জমা টাকাই দেয়। প্রতি increment-এ ঠিক একটি বিট 0 → 1 হয় (counter উপচে না পড়লে), তাই amortized খরচ 2.
(৩) Potential: Φ = counter-এ 1-এর সংখ্যা (≥ 0, শুরুতে 0). একটি increment t টি 1-কে 0 করে আর একটি 0-কে 1: আসল খরচ t + 1, ΔΦ = 1 − t. amortized = (t + 1) + (1 − t) = 2. ∎
যাচাই: 0 থেকে 16 বার increment-এ মোট ৩১টি বিট উল্টায় — 2·16 = 32-এর নিচে।
নম্বর কোথায়: তিন পদ্ধতির নাম ও প্রতিটির নিজস্ব হিসাব, potential-এ Φ ≥ 0 আর Φ0 = 0 শর্ত। পর্ব ৬০
ছ-০২P, NP, NP-hard আর NP-complete-এর সংজ্ঞা দিন। একটি নতুন সমস্যা NP-complete প্রমাণের ধাপগুলো লিখুন। — ৬ নম্বর (মাঝারি)
- P: যে decision problem polynomial সময়ে সমাধান করা যায়।
- NP: যেটির “হ্যাঁ” উত্তরের একটি certificate polynomial সময়ে যাচাই করা যায় (সমতুল্যভাবে, nondeterministic polynomial time). NP মানে “non-polynomial” নয়। P ⊆ NP.
- NP-hard: NP-র প্রতিটি সমস্যা polynomial সময়ে এতে reduce হয় — অন্তত NP-র কঠিনতম সমস্যার মতো কঠিন; নিজে NP-তে না-ও থাকতে পারে।
- NP-complete: NP-তে আছে এবং NP-hard.
প্রমাণের ধাপ: (১) নতুন সমস্যা B NP-তে আছে — certificate আর তার polynomial যাচাইকারী দেখান। (২) একটি জানা NP-complete সমস্যা A বাছুন (যেমন 3-SAT). (৩) A-র যেকোনো instance x থেকে B-র instance f(x) বানানোর polynomial-সময়ের রূপান্তর দিন। (৪) x “হ্যাঁ” ⇒ f(x) “হ্যাঁ”. (৫) f(x) “হ্যাঁ” ⇒ x “হ্যাঁ”. তাহলে A ≤p B, আর B NP-complete.
নম্বর কোথায়: reduction-এর দিক — জানা কঠিন থেকে নতুনে; উল্টো দিকে (B → A) দেখালে শুধু প্রমাণ হয় B সহজ-বা-সমান, কঠিন নয়। পর্ব ৬৪
ছ-০৩3-SAT থেকে reduction দিয়ে প্রমাণ করুন Independent Set NP-complete. — ১০ নম্বর (কঠিন)
সমস্যা: Independent Set — গ্রাফ G আর সংখ্যা k দেওয়া; এমন k টি vertex আছে কি, যাদের কোনো দুটি edge দিয়ে যুক্ত নয়?
NP-তে: certificate = k টি vertex-এর তালিকা; সব জোড়া দেখে edge নেই কি না যাচাই O(k²) বা O(V + E).
রূপান্তর: 3-CNF সূত্র φ-তে m টি clause. প্রতিটি clause-এর তিনটি literal-এর জন্য তিনটি vertex, পরস্পর যুক্ত (clause-ত্রিভুজ)। তারপর ভিন্ন clause-এর যেকোনো x আর ¬x vertex-এর মধ্যে একটি “বিরোধের” edge. k = m. গ্রাফে 3m টি vertex, edge O(m²) — polynomial সময়।
(⇒): φ সত্য করে এমন একটি মান-বণ্টন নিন। প্রতিটি clause থেকে একটি সত্য literal-এর vertex বাছুন — m টি। দুটি একই ত্রিভুজে নয় (প্রতি clause-এ একটি), আর বিরোধের edge শুধু x আর ¬x-এর মধ্যে, যারা একসাথে সত্য হতে পারে না। তাই এটি m আকারের independent set.
(⇐): m আকারের একটি independent set S নিন। প্রতিটি ত্রিভুজ থেকে সর্বোচ্চ একটি, আর ত্রিভুজ m টি — তাই প্রতিটি থেকে ঠিক একটি। S-এর literal-গুলো সত্য ধরুন; x আর ¬x দুটোই S-এ থাকতে পারে না (বিরোধের edge), তাই বণ্টনটি সংগত; বাকি চলক যা খুশি। প্রতিটি clause-এ একটি সত্য literal, তাই φ সত্য। ∎
উদাহরণ: φ = (x1 ∨ x2 ∨ x3) ∧ (¬x1 ∨ ¬x2 ∨ x3) ∧ (x1 ∨ ¬x2 ∨ ¬x3): ৯টি vertex, ৯টি ত্রিভুজ-edge, ৬টি বিরোধের edge, k = 3. x1 = 1, x2 = 0, x3 = 0 থেকে S = {C1.x1, C2.¬x2, C3.x1}.
নম্বর কোথায়: NP-তে থাকা, রূপান্তরের দুই রকম edge আর k = m, polynomial আকার, আর দুই দিকের প্রমাণ — বিশেষ করে (⇐)-তে “প্রতি ত্রিভুজে ঠিক একটি”. পর্ব ৬৪
ছ-০৪Vertex cover-এর একটি 2-approximation অ্যালগরিদম দিন এবং approximation ratio প্রমাণ করুন। — ৮ নম্বর (মাঝারি)
সমস্যা: vertex cover = এমন vertex-সেট, যাতে প্রতিটি edge-এর অন্তত এক প্রান্ত আছে; সবচেয়ে ছোটটি খোঁজা NP-hard.
Pseudocode: APPROX-VERTEX-COVER(G)
C = empty set
for each edge (u, v) in any order:
if u not in C and v not in C: // edge still uncovered
C = C + {u, v}
return Cপ্রমাণ: যে edge-গুলোর জন্য দুই প্রান্ত যোগ হলো, তাদের সেট M. এদের কোনো দুটি একই vertex ছোঁয় না — M একটি maximal matching, আর |C| = 2|M|. বৈধতা: প্রতিটি edge হয় M-এ, নয়তো দেখার সময় এক প্রান্ত C-তে ছিল — তাই সব edge ঢাকা। সীমা: যেকোনো cover-কে M-এর প্রতিটি edge ঢাকতে প্রতিটি থেকে অন্তত একটি প্রান্ত নিতে হয়, আর M-এর edge-গুলোর প্রান্ত আলাদা — তাই OPT ≥ |M|. সুতরাং |C| = 2|M| ≤ 2·OPT. ∎
Complexity: O(V + E). সীমাটি আঁটসাঁট: ৬টি vertex-এর একটি cycle-এ অ্যালগরিদম ৬টি নিতে পারে, অথচ OPT = 3 — নিচের প্রোগ্রাম দেখুন।
নম্বর কোথায়: বৈধতা আর OPT ≥ |M| — দুটি আলাদা দাবি; “approximation ratio = সব ইনপুটে ALG/OPT-এর সর্বোচ্চ” সংজ্ঞাটি। পর্ব ৬৬
#include <algorithm>
#include <cstdio>
#include <utility>
#include <vector>
using namespace std;
using Graph = vector<pair<int, int>>;
int approxCover(int n, const Graph& E) { // both ends of a maximal matching
vector<bool> in(n, false); int size = 0;
for (auto [u, v] : E)
if (!in[u] && !in[v]) { in[u] = in[v] = true; size += 2; }
return size;
}
int optimalCover(int n, const Graph& E) { // brute force over all 2^n subsets
int best = n;
for (int S = 0; S < (1 << n); ++S) {
bool ok = true;
for (auto [u, v] : E) if (!((S >> u) & 1) && !((S >> v) & 1)) { ok = false; break; }
if (ok) best = min(best, __builtin_popcount(S));
}
return best;
}
int main() {
Graph gs[] = {{{0, 1}, {0, 2}, {1, 3}, {2, 4}, {3, 5}, {4, 5}}, // 6-cycle
{{0, 1}, {0, 2}, {0, 3}, {0, 4}}, // star
{{1, 2}, {0, 1}, {3, 4}, {2, 3}}}; // path
const char* names[] = {"6-cycle", "star", "path"}; int ns[] = {6, 5, 5};
for (int g = 0; g < 3; ++g) {
int a = approxCover(ns[g], gs[g]), o = optimalCover(ns[g], gs[g]);
printf("%-8s approx %d optimal %d ratio %.2f\n", names[g], a, o, double(a) / o);
}
}আউটপুট:
6-cycle approx 6 optimal 3 ratio 2.00
star approx 2 optimal 1 ratio 2.00
path approx 4 optimal 2 ratio 2.00
তিনটি গ্রাফেই অনুপাত ঠিক 2.00 — প্রমাণের সীমা ছাড়ায়নি, আবার ঠিক সীমাতেই পৌঁছেছে, তাই 2-এর চেয়ে ভালো কোনো দাবি এই অ্যালগরিদমের জন্য করা যায় না। star-এ কেন্দ্র একাই সব ঢাকে (OPT = 1), কিন্তু অ্যালগরিদম প্রথম edge-এর দুই প্রান্তই নেয়। brute force 2n টি উপসেট দেখে — n = 6-এ মাত্র 64, কিন্তু n = 60-এ অসম্ভব; সেজন্যই approximation.
ছ-০৫Las Vegas আর Monte Carlo অ্যালগরিদমের পার্থক্য উদাহরণসহ লিখুন। — ৪ নম্বর (সহজ)
| মাপকাঠি | Las Vegas | Monte Carlo |
|---|---|---|
| উত্তর | সবসময় সঠিক | ছোট সম্ভাবনায় ভুল হতে পারে |
| সময় | এলোমেলো — প্রত্যাশিত সীমা | নির্দিষ্ট সীমা |
| উদাহরণ | randomized quicksort — প্রত্যাশিত O(n log n) | Miller–Rabin — k রাউন্ডে ভুলের সম্ভাবনা ≤ 4−k |
| ভুল কমানো | প্রযোজ্য নয় | স্বাধীনভাবে আবার চালানো |
নম্বর কোথায়: কোনটিতে সময় এলোমেলো আর কোনটিতে উত্তর — এই এক লাইন, আর প্রতিটির একটি উদাহরণ। পর্ব ৬১
৯ · ভাইভা — মুখে উত্তরের কৌশল আর মডিউলভিত্তিক দ্রুত প্রশ্ন
লিখিত পরীক্ষায় আপনি ভাবার সময় পান, কাটাকাটি করতে পারেন। ভাইভায় প্রশ্ন আসে মুখে, উত্তরও দিতে হয় তখনই — আর পরীক্ষক আপনার প্রথম উত্তর শুনেই পরের প্রশ্ন বানান। এমন প্রশ্নকে বলে follow-up প্রশ্ন. “stack কী?” থেকে শুরু হয়ে তিন প্রশ্নে পৌঁছাতে পারে “তাহলে recursion-এ stack overflow কেন হয়?” পর্যন্ত। চাকরির ভাইভা বোর্ডে বা কোডিং ইন্টারভিউর আলাপে একই দক্ষতা লাগে।
তিন বাক্যের কাঠামো
প্রশ্ন: “binary search কী?” দুর্বল উত্তর: “মানে… মাঝখান থেকে… দুই ভাগ করে…”. গোছানো উত্তর: “সাজানো অ্যারেতে মাঝের উপাদানের সাথে তুলনা করে প্রতিবার অর্ধেক অংশ বাদ দেওয়ার খোঁজ (কী)। যেমন ফোনবুকে নাম খুঁজতে মাঝের পাতা খুলে ঠিক করি আগে না পরে (কেন)। সময় O(log n), কিন্তু অ্যারে সাজানো না হলে বা linked list-এ এটি কাজে আসে না (কত, কখন নয়)।” পনেরো সেকেন্ড, তিন বাক্য — আর পরীক্ষকের হাতে পরের প্রশ্নের তিনটি সূত্র।
ভাইভাকে ট্রেনের টিকিট কাউন্টারের লাইনের মতো ভাবুন। কাউন্টারের লোকটি এক প্রশ্নে শুধু গন্তব্য আর তারিখ চান। আপনি যদি পুরো ভ্রমণের পরিকল্পনা বলতে শুরু করেন, পেছনের লাইন অধৈর্য হয়, আর আসল তথ্যটি হারিয়ে যায়। ছোট, সঠিক উত্তর দিন — দরকার হলে তিনিই পরের প্রশ্ন করবেন।
চারটি অভ্যাস যা নম্বর এনে দেয়
- জোরে ভাবুন (think aloud): সাথে সাথে উত্তর মাথায় না এলে ভাবনাটা বলুন — “recurrence হবে T(n) = 2T(n/2) + n… তলা প্রতি n… log n টি তলা… তাই n log n.” পরীক্ষক আপনার যুক্তির পথ দেখতে চান; পথ ঠিক থাকলে ছোট হিসাবের ভুলও তিনি ক্ষমা করেন।
- ছবি আঁকুন: কাগজ বা বোর্ড চাইতে দ্বিধা করবেন না। array-র বাক্স, linked list-এর তীর, ৫টি node-এর একটি tree, ৪টি vertex-এর একটি গ্রাফ — দুই সেকেন্ডের ছবি দশ বাক্যের কাজ করে, আর আপনার নিজের ভুলও ধরিয়ে দেয়।
- ছোট উদাহরণ আগে: “ধরুন অ্যারে [3, 1, 2]…” বলে শুরু করলে বিমূর্ত প্রশ্ন হাতে-ধরা যায়। প্রমাণের প্রশ্নেও আগে n = 3 বা ৪টি vertex-এ দেখান, তারপর সাধারণ যুক্তি।
- প্রশ্ন নিশ্চিত করুন: “heap বলতে কি data structure, না memory-র heap?”, “weight কি ঋণাত্মক হতে পারে?”, “গ্রাফ directed?” — একই শব্দের দুই মানে, বা একটি অনুমান উত্তর বদলে দিলে জিজ্ঞেস করাই পেশাদারি।
যে প্রশ্নের উত্তর জানেন না — সৎভাবে সামলানো
- স্বীকার করুন: “পুরো নিশ্চিত নই” বলা দুর্বলতা নয়। ভুল কথা জোর দিয়ে বললে পরীক্ষক আপনার বাকি ঠিক উত্তরগুলো নিয়েও সন্দেহ করেন।
- যা জানেন সেখান থেকে যুক্তি দিন: “Fibonacci heap ব্যবহার করিনি, তবে জানি Dijkstra-য় খরচ আসে decrease-key থেকে; সেটি দ্রুত হলে মোট সময় কমবে…”.
- আন্দাজকে আন্দাজ বলুন: “আমার ধারণা O(E + V log V), কারণ…”.
- যাচাইয়ের উপায় বলুন: “ছোট ইনপুটে brute force-এর সাথে মিলিয়ে দেখতাম” — এটি দেখায় আপনি কীভাবে কাজ করেন।
- এগিয়ে যান: একটি প্রশ্ন না পারা পুরো ভাইভা নয়। পরের প্রশ্নে মন দিন।
ভাইভা কুইকফায়ার — দ্রুত প্রশ্ন, ছোট উত্তর
জোরে জোরে উত্তর বলুন, তারপর প্রশ্নে চেপে মেলান — প্রতিটিতে ২০–৩০ সেকেন্ডের বেশি নয়। উত্তরগুলো ইচ্ছা করে এক–তিন বাক্যে রাখা হয়েছে; ভাইভায় এর বেশি সাধারণত চাওয়া হয় না।
বিশ্লেষণের ভিত্তি (পর্ব ০১–০৯)
ভ-০১অ্যালগরিদম আর প্রোগ্রামের পার্থক্য কী? (সহজ)
অ্যালগরিদম ভাষা-নিরপেক্ষ ধাপের পরিকল্পনা, যা সব ইনপুটে থামে আর সঠিক উত্তর দেয়; প্রোগ্রাম হলো কোনো নির্দিষ্ট ভাষায় তার রূপ — আর একটি প্রোগ্রাম চিরকাল চলতেও পারে (যেমন সার্ভার)। পর্ব ০২
ভ-০২Big-O মানেই কি worst case? (মাঝারি)
না। Big-O একটি ফাংশনের ওপরের সীমা; best, worst বা average — যেকোনো case-এর ফাংশনকে O দিয়ে বলা যায়। “insertion sort-এর best case O(n)” একটি সঠিক বাক্য। পর্ব ০৪
ভ-০৩O(n) অ্যালগরিদম কি সবসময় O(n²)-এর চেয়ে দ্রুত চলে? (মাঝারি)
না। Big-O ধ্রুবক আর ছোট n লুকায় — 1000n ধাপের অ্যালগরিদম n < 1000-এ n² ধাপের চেয়ে ধীর। এজন্যই বাস্তবের sort ছোট অংশে insertion sort ব্যবহার করে। পর্ব ০৪
ভ-০৪Complexity-তে log-এর base কেন লেখা হয় না? (সহজ)
loga n = logb n / logb a — base বদলালে শুধু একটি ধ্রুবক গুণ বদলায়, যা O, Ω, Θ উপেক্ষা করে। তবে সূচকে (যেমন nlog2 3) base জরুরি। পর্ব ০৬
ভ-০৫Recursion-এ base case না থাকলে কী হয়? (সহজ)
ফাংশন নিজেকে ডাকতেই থাকে; প্রতিটি ডাক call stack-এ একটি frame যোগ করে, আর stack-এর সীমা ফুরোলে প্রোগ্রাম stack overflow-এ ভেঙে পড়ে (Python-এ RecursionError). পর্ব ০৭
ভ-০৬Master theorem কখন খাটে না? (মাঝারি)
যখন recurrence aT(n/b) + f(n) ছাঁচে নয় — যেমন T(n − 1) + n বা T(n/3) + T(2n/3) + n — অথবা f আর nlogb a-এর ফারাক polynomial নয় (যেমন 2T(n/2) + n/log n). তখন recursion tree বা substitution ধরুন। পর্ব ০৯
লিনিয়ার ডেটা স্ট্রাকচার (পর্ব ১০–১৭)
ভ-০৭Stack আর queue-এর পার্থক্য কী? (সহজ)
ভ-০৮Array-তে a[i] পড়া O(1) কেন? (সহজ)
উপাদানগুলো মেমরিতে পাশাপাশি আর সমান আকারের, তাই ঠিকানা = শুরুর ঠিকানা + i × আকার — একটি গুণ আর একটি যোগ, i যত বড়ই হোক। পর্ব ১০
ভ-০৯vector-এর push_back-কে O(1) বলা হয়, অথচ কখনো পুরো অ্যারে কপি হয় — তাহলে? (মাঝারি)
এটি amortized O(1): ধারণক্ষমতা দ্বিগুণ হয় বলে n টি push-এ মোট কপি 2n-এর কম, তাই গড়ে প্রতি push ধ্রুবক; একটি একক push-এর worst case অবশ্য Θ(n). পর্ব ১০
ভ-১০সাজানো linked list-এ binary search কেন লাভজনক নয়? (মাঝারি)
মাঝের node-এ পৌঁছাতেই হাঁটতে হয় Θ(n) — random access নেই; মোট হাঁটা n/2 + n/4 + … = Θ(n), তুলনা কম হলেও সময় linear search-এর মতোই। দ্রুত খোঁজার জন্য BST বা skip list লাগে। পর্ব ১২
ভ-১১Prefix sum কী সুবিধা দেয়, আর কখন চলে না? (সহজ)
Θ(n) প্রস্তুতিতে যেকোনো পরিসরের যোগফল O(1)-এ: sum(l..r) = P[r + 1] − P[l]. কিন্তু অ্যারে বদলালে পুরো P আবার বানাতে হয় — তখন Fenwick বা segment tree. পর্ব ১৭
ভ-১২Sliding window কখন খাটে? (মাঝারি)
পাশাপাশি অংশ (subarray/substring)-এর প্রশ্নে, যখন জানালা বড় করলে শর্তটি এক দিকেই বদলায় — যেমন অ-ঋণাত্মক সংখ্যায় “যোগফল ≤ K”. ঋণাত্মক সংখ্যা থাকলে এই একমুখিতা ভাঙে, তখন prefix sum + hash map. পর্ব ১৬
সার্চিং ও সর্টিং (পর্ব ১৮–২৫)
ভ-১৩Binary search চালানোর শর্ত কী? (সহজ)
ভ-১৪Worst case O(n²) হলেও quick sort বাস্তবে এত দ্রুত কেন? (মাঝারি)
in-place, আর partition অ্যারের ওপর দিয়ে পরপর হাঁটে বলে cache খুব ভালো কাজে লাগে; ভেতরের লুপ ছোট, ধ্রুবক কম; এলোমেলো pivot-এ worst case প্রায় অসম্ভব। লাইব্রেরি (যেমন introsort) গভীরতা বেশি হলে heapsort-এ চলে গিয়ে O(n log n) নিশ্চিত করে। পর্ব ২২
ভ-১৫Stable sort কী, কোথায় দরকার? (সহজ)
সমান কী-এর উপাদানের আগের ক্রম রাখে। দরকার একাধিক কী-তে সাজাতে — যেমন আগে নাম ধরে, তারপর stable sort-এ বিভাগ ধরে সাজালে প্রতি বিভাগে নাম ক্রমে থাকে — আর LSD radix sort-এ। পর্ব ২৫
ভ-১৬Merge sort না quick sort — কখন কোনটি? (মাঝারি)
stability, linked list বা ডিস্কের বিশাল ফাইল (external sort) আর নিশ্চিত Θ(n log n) চাইলে merge sort; মেমরি কম আর অ্যারেতে গড়ে দ্রুততম চাইলে quick sort. পর্ব ২১
ভ-১৭Counting sort কি Ω(n log n) নিম্নসীমা ভাঙে? (মাঝারি)
না, কারণ সীমাটি শুধু তুলনা-ভিত্তিক সর্টের জন্য। counting sort কী-কে index হিসেবে ব্যবহার করে — পূর্ণসংখ্যা আর ছোট পরিসর k ধরে নেয় — তাই Θ(n + k). পর্ব ২৪
ভ-১৮Heap-এ একটি নির্দিষ্ট মান খুঁজতে O(n) লাগে কেন? (মাঝারি)
heap property শুধু মা-বাবা আর সন্তানের ক্রম বলে, বাঁ আর ডানের মধ্যে কোনো ক্রম নেই — তাই কোন দিকে যেতে হবে বোঝা যায় না, সব দেখতে হয়। heap শুধু min বা max দ্রুত দেয়। পর্ব ২৩
ট্রি, হ্যাশ, DSU আর রেঞ্জ কোয়েরি (পর্ব ২৬–৩৮)
ভ-১৯Hash table-এর worst case O(n) কেন? (মাঝারি)
সব key একই bucket-এ গেলে একটি তালিকায় n টি key — দুর্বল hash function বা ইচ্ছা করে সংঘর্ষ ঘটানো ইনপুটে এমন হয়। ভালো hash আর সীমিত load factor-এ প্রত্যাশিত সময় O(1), কিন্তু সেটি নিশ্চয়তা নয়। পর্ব ৩২
ভ-২০BST-এর inorder traversal কী দেয়? (সহজ)
সব key ছোট থেকে বড় ক্রমে — বাঁ subtree, মূল, ডান subtree; সময় Θ(n). এটি দিয়েই কোনো binary tree BST কি না যাচাই করা যায়। পর্ব ২৭
ভ-২১AVL আর red-black tree-র মধ্যে কোনটি কখন? (কঠিন)
AVL বেশি কড়া ভারসাম্য রাখে (উচ্চতা ≈ 1.44 log n বনাম red-black-এর ≤ 2 log(n + 1)), তাই খোঁজা একটু দ্রুত; red-black insert-এ সর্বোচ্চ ২টি আর delete-এ সর্বোচ্চ ৩টি rotation করে, তাই ঘন ঘন পরিবর্তনে সুবিধা — অনেক লাইব্রেরির ordered map এজন্য red-black ব্যবহার করে। পর্ব ২৯
ভ-২২Trie কখন hash table-এর চেয়ে ভালো? (মাঝারি)
prefix-এর প্রশ্নে — “এই prefix দিয়ে কয়টি শব্দ শুরু?”, autocomplete, অভিধান ক্রমে তালিকা; খরচ শব্দের দৈর্ঘ্য L-এ O(L), n-এ নির্ভর করে না। hash table prefix খুঁজতে পারে না। পর্ব ৩১
ভ-২৩Path compression কী করে? (সহজ)
find-এর সময় পথের প্রতিটি node-কে সরাসরি মূলের সাথে জুড়ে দেয়, তাই পরের find প্রায় এক ধাপে হয়; union by rank-এর সাথে প্রতি কাজ amortized O(α(n)). পর্ব ৩৩
ভ-২৪Segment tree আর Fenwick tree-র পার্থক্য? (মাঝারি)
Fenwick ছোট কোড আর n ঘরের জায়গায় prefix-ভিত্তিক কাজ (যোগফল, উল্টানো যায় এমন অপারেশন) করে; segment tree যেকোনো associative কাজ (min, max, gcd) আর lazy propagation-এ range update পারে — দুটিতেই query ও update O(log n). পর্ব ৩৬
গ্রাফ অ্যালগরিদম (পর্ব ৩৯–৪৮)
ভ-২৫BFS আর DFS-এর পার্থক্য? (সহজ)
ভ-২৬Adjacency matrix না adjacency list? (সহজ)
matrix-এ জায়গা Θ(V²), “u-v edge আছে?” O(1) — ঘন গ্রাফ বা Floyd-Warshall-এ; list-এ Θ(V + E), প্রতিবেশী ঘোরা দ্রুত — বেশিরভাগ বাস্তব গ্রাফ পাতলা, তাই সাধারণত list. পর্ব ৩৯
ভ-২৭Dijkstra আর Prim-এর মিল-অমিল কী? (কঠিন)
মিল: দুটিই greedy, priority queue থেকে প্রতিবার সবচেয়ে ছোট key-এর vertex নিয়ে একটি tree বড় করে, complexity একই — O((V + E) log V). অমিল: Dijkstra-র key উৎস থেকে মোট দূরত্ব d[u] + w, Prim-এর key শুধু tree-তে জোড়ার edge-এর weight w; Dijkstra দেয় shortest-path tree, Prim দেয় MST — আর Prim ঋণাত্মক weight-এও ঠিক থাকে, Dijkstra থাকে না। পর্ব ৪৩ · পর্ব ৪৫
ভ-২৮Dijkstra ঋণাত্মক edge-এ কেন ভাঙে? (মাঝারি)
Dijkstra ধরে নেয় একটি vertex settle হলে তার দূরত্ব আর কমবে না, কারণ অ-ঋণাত্মক weight-এ পথ বাড়ালে দৈর্ঘ্য কমে না; ঋণাত্মক edge থাকলে পরে আসা পথ ছোট হতে পারে — ঙ-০৩-এর S, A, B, C উদাহরণে A = 1 না হয়ে 2 আসে, negative cycle ছাড়াই। পর্ব ৪৩
ভ-২৯Bellman-Ford আর Floyd-Warshall কখন? (মাঝারি)
এক উৎস থেকে, ঋণাত্মক edge থাকলে বা negative cycle খুঁজতে Bellman-Ford — Θ(VE); সব জোড়ার দূরত্ব, ছোট বা ঘন গ্রাফে Floyd-Warshall — Θ(V³) সময়, Θ(V²) জায়গা। পর্ব ৪৪
ভ-৩০Topological sort বাস্তবে কোথায় লাগে? (সহজ)
যেখানে কাজের আগে-পরে নির্ভরতা আছে — কোর্সের পূর্বশর্ত, build system-এ কোন ফাইল আগে compile, package install-এর ক্রম, spreadsheet-এর সূত্রের হিসাব। cycle থাকলে কোনো বৈধ ক্রম নেই। পর্ব ৪২
অ্যালগরিদম ডিজাইন কৌশল (পর্ব ৪৯–৫৯)
ভ-৩১Greedy আর dynamic programming-এর পার্থক্য? (মাঝারি)
ভ-৩২Memoization আর tabulation-এর পার্থক্য? (সহজ)
memoization ওপর থেকে নিচে — রিকার্সিভ ফাংশন, প্রথমবার হিসাব করে মনে রাখে, শুধু দরকারি state ছোঁয়; tabulation নিচ থেকে ওপরে — লুপে ক্রম মেনে পুরো টেবিল ভরে, recursion-এর stack লাগে না আর জায়গা বাঁচানো সহজ। পর্ব ৫২
ভ-৩৩Knapsack-এর Θ(nW) DP থাকা সত্ত্বেও সমস্যাটি NP-hard কেন? (কঠিন)
ইনপুটের আকার W-এর বিট-সংখ্যা log W, মান W নয়; W = 2b হলে nW = n·2b — ইনপুট-আকারে সূচকীয়। এটিই pseudo-polynomial; W ছোট হলে DP কাজের, বড় হলে নয়। পর্ব ৫৩
ভ-৩৪Backtracking আর brute force-এর পার্থক্য? (সহজ)
brute force সব সম্পূর্ণ সমাধান তৈরি করে তারপর যাচাই করে; backtracking আংশিক সমাধানেই শর্ত ভাঙলে সেই শাখা ছেঁটে ফেলে (pruning) — worst case একই থাকতে পারে, কিন্তু বাস্তবে অনেক কম শাখা দেখে। পর্ব ৫৬
ভ-৩৫KMP naive string matching-এর চেয়ে কীভাবে ভালো? (মাঝারি)
naive মিল ভাঙলে text-এ পেছনে ফিরে আবার শুরু করে — O(nm); KMP prefix function দিয়ে জানে ইতিমধ্যে মেলা অংশের কতটুকু কাজে লাগবে, তাই text-এর pointer কখনো পেছনে যায় না — O(n + m). পর্ব ৫৮
উন্নত বিশ্লেষণ ও তত্ত্ব (পর্ব ৬০–৬৭)
ভ-৩৬Amortized আর average case-এর পার্থক্য কী? (মাঝারি)
amortized হলো যেকোনো কাজের ক্রমে worst case-এর গড় — কোনো সম্ভাবনা বা ইনপুট-বণ্টন লাগে না, নিশ্চয়তা; average case নির্ভর করে ইনপুটের সম্ভাব্যতা-বণ্টনের অনুমানের ওপর। পর্ব ৬০
ভ-৩৭NP মানে কি “non-polynomial”? (সহজ)
না — nondeterministic polynomial: যে সমস্যার “হ্যাঁ” উত্তর একটি প্রমাণ (certificate) দেখে polynomial সময়ে যাচাই করা যায়। P-র সব সমস্যাও NP-তে আছে। পর্ব ৬৪
ভ-৩৮NP-hard আর NP-complete-এর পার্থক্য? (মাঝারি)
NP-complete = NP-তে আছে এবং NP-hard; NP-hard সমস্যা NP-র বাইরেও হতে পারে — যেমন halting problem, বা TSP-র optimization রূপ (“সবচেয়ে ছোট tour কত?”), যার উত্তর যাচাই করা সহজ নয়। পর্ব ৬৪
ভ-৩৯নতুন সমস্যা B কঠিন প্রমাণ করতে reduction কোন দিকে করবেন? (মাঝারি)
জানা কঠিন সমস্যা A থেকে B-তে: A ≤p B — অর্থাৎ B সমাধান করতে পারলে A-ও পারা যেত। উল্টো দিকে করলে শুধু দেখায় B, A-র চেয়ে কঠিন নয়। পর্ব ৬৪
ভ-৪০Approximation ratio 2 মানে কী? (সহজ)
প্রতিটি ইনপুটে অ্যালগরিদমের সমাধানের খরচ সেরা সমাধানের সর্বোচ্চ দ্বিগুণ (minimization-এ) — গড়ে নয়, সবসময়; আর সেরা সমাধান না জেনেও এটি প্রমাণ করা হয় OPT-এর একটি নিম্নসীমা দিয়ে। পর্ব ৬৬
ভ-৪১একটি সমস্যা NP-hard জানার পর বাস্তবে কী করবেন? (মাঝারি)
ইনপুট ছোট হলে exact (backtracking, bitmask DP, branch and bound); বিশেষ গঠন থাকলে (tree, bipartite, ছোট প্যারামিটার) সেটির দ্রুত অ্যালগরিদম; নইলে প্রমাণিত approximation বা heuristic — আর সবসময় ইনপুটের সীমা দেখে সিদ্ধান্ত। পর্ব ৬৫
১০ · AI-কে কড়া পরীক্ষক বানান — আর তার প্রমাণ যাচাই করুন
ভাইভা অনুশীলনের সবচেয়ে বড় সমস্যা হলো প্রশ্ন করার মানুষ না থাকা। AI এখানে ভালো সঙ্গী — ক্লান্ত হয় না, আর আপনার উত্তর থেকে follow-up বানাতে পারে। কিন্তু DSA-তে AI-এর ভুলের একটি নিজস্ব ধরন আছে: আত্মবিশ্বাসী ভুল complexity, এক ঘর এদিক-ওদিক সীমা, প্রমাণ ছাড়া greedy, আর এমন “প্রমাণ” যা আসলে একটি উদাহরণ মাত্র। তাই তাকে স্পষ্ট নিয়ম দিন, আর প্রতিটি দাবি নিজে যাচাই করুন।
নিচের prompt-টি কপি করে যেকোনো AI চ্যাটে দিন। বিষয়ের জায়গায় যে মডিউল ঝালাতে চান সেটি লিখুন:
লিখিত উত্তরের জন্য: খাতার উত্তরটি টাইপ করে বলুন “এটি একটি ১০ নম্বরের প্রমাণ। পরীক্ষক হিসেবে নম্বর দাও আর কোন ধাপ দুর্বল বলো — কিন্তু পুরো উত্তর নতুন করে লিখে দিও না।” শেষ শর্তটি জরুরি; নইলে আপনি শিখবেন না, শুধু আরেকটি উত্তর মুখস্থ করবেন।
DSA নিয়ে AI-এর উত্তরে যেসব ভুল প্রায়ই থাকে
| AI যা বলে | আসল সত্য |
|---|---|
| “2T(n/2) + n log n — master theorem case ৩, উত্তর Θ(n log n)” | তিন case-এর কোনোটিই খাটে না; উত্তর Θ(n log² n) (ক-০৬) |
| “negative cycle না থাকলে Dijkstra ঋণাত্মক edge-এও চলে” | ভুল — ঙ-০৩-এর চার vertex-এর উদাহরণেই ভাঙে |
| “build-heap O(n log n)” | ঢিলে সীমা; আঁটসাঁট Θ(n) (গ-০৪) |
| “hash table-এ খোঁজা সবসময় O(1)” | প্রত্যাশিত O(1) অনুমানসহ; worst case Θ(n) (ঘ-০৩) |
| “কয়েন বদলে বড় কয়েন আগে — greedy সবসময় সেরা” | {1, 3, 4}-এ 6-এর জন্য ভুল (ভ-৩১); প্রমাণ ছাড়া greedy নয় |
| “X-কে 3-SAT-এ reduce করলাম, তাই X NP-complete” | দিক উল্টো — 3-SAT থেকে X-এ reduce করতে হয় (ছ-০২) |
| “knapsack-এর DP polynomial, তাই P = NP প্রমাণিত” | Θ(nW) pseudo-polynomial (ভ-৩৩) |
binary search-এ while (lo < hi) আর hi = mid − 1 একসাথে | এক-উপাদানের জানালা বাদ পড়ে বা অসীম লুপ — invariant মিলিয়ে দেখুন (গ-০১) |
AI-এর উত্তর যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
এগুলো মুখস্থের প্রশ্ন নয় — প্রমাণ, পরীক্ষা আর শেখা নিয়ে একটু থেমে ভাবার প্রশ্ন। নিজে কিছুক্ষণ ভাবুন, তারপর প্রশ্নে চেপে উত্তর দেখুন।
প্র ০১টেস্ট পাস করলেই তো কোড ঠিক — তবু পরীক্ষায় প্রমাণ কেন চাওয়া হয়?
টেস্ট দেখায় নির্দিষ্ট কয়েকটি ইনপুটে উত্তর ঠিক; প্রমাণ দেখায় সব ইনপুটে ঠিক। পার্থক্যটি বাস্তবে কতটা বড়, তার বিখ্যাত উদাহরণ binary search. ২০০৬ সালে Joshua Bloch (তখন Google-এ) একটি ব্লগ লেখায় দেখান, Java-র স্ট্যান্ডার্ড লাইব্রেরির binary search-এ বহু বছর ধরে একটি bug ছিল: mid = (low + high) / 2. অ্যারের আকার ২৩০-এর বেশি হলে low + high পূর্ণসংখ্যার সীমা পেরিয়ে ঋণাত্মক হয়ে যায়। কোডটি বছরের পর বছর অসংখ্য টেস্ট পাস করেছিল, কারণ কেউ এত বড় অ্যারে দিয়ে পরীক্ষা করেননি। আর এ ধরনের কোডের পাঠ্যবইয়ের প্রমাণে পূর্ণসংখ্যাকে সাধারণত গাণিতিক — সীমাহীন — ধরা হয়; প্রমাণে ভুল ছিল না, ভুল ছিল সেই অনুমানে।
এ থেকে দুটি শিক্ষা। প্রথমত, প্রমাণ সেই ভুলগুলো ধরে যা টেস্ট ধরে না — যেমন একটি বিরল case বা একটি বড় ইনপুট। দ্বিতীয়ত, প্রমাণও তার অনুমানের মতোই শক্ত: “int সীমাহীন” অনুমানটি ভুল হলে প্রমাণ সত্য, কোড ভুল। Donald Knuth-এর একটি বিখ্যাত রসিকতা আছে — তিনি একটি চিঠিতে লিখেছিলেন, “এই কোডে bug থাকতে পারে; আমি শুধু এটির সঠিকতা প্রমাণ করেছি, চালিয়ে দেখিনি।” বাক্যটি দুই দিকের সীমাই মনে করিয়ে দেয়।
তাই পরীক্ষায় প্রমাণ চাওয়ার আসল উদ্দেশ্য আপনাকে গণিতবিদ বানানো নয়, বরং অভ্যাস তৈরি করা: কোড লেখার সময় নিজেকে জিজ্ঞেস করা “কেন এটি সবসময় ঠিক?”, invariant মাথায় রাখা, আর অনুমানগুলো (সাজানো? অ-ঋণাত্মক? সীমার ভেতরে?) স্পষ্ট করা। এই অভ্যাসই AI-এর লেখা কোডে ভুল ধরার সবচেয়ে বড় হাতিয়ার।
মূল উপলব্ধি: টেস্ট আর প্রমাণ একে অপরের পরিপূরক — একটি দেখায় কোথায় ভুল আছে, অন্যটি বলে কেন আর কোথায় ভুল থাকতে পারে না, আর দুটিই অনুমানের ওপর দাঁড়িয়ে।
প্র ০২Dijkstra-র অ্যালগরিদমটি কাগজ-কলম ছাড়াই উদ্ভাবিত হয়েছিল বলে শোনা যায়। এতে লিখিত পরীক্ষার জন্য কী শেখার আছে?
Edsger W. Dijkstra পরে এক সাক্ষাৎকারে বলেছিলেন, ১৯৫৬ সালে আমস্টারডামে তিনি বাগদত্তার সাথে কেনাকাটায় বেরিয়ে একটি ক্যাফের বারান্দায় বসে প্রায় বিশ মিনিটে সবচেয়ে ছোট পথের অ্যালগরিদমটি ভেবে বের করেন — কাগজ-কলম ছাড়া। তাঁর উদ্দেশ্য ছিল নতুন একটি কম্পিউটার (ARMAC) দেখাতে এমন একটি সমস্যা বেছে নেওয়া, যা সাধারণ মানুষও বোঝেন: নেদারল্যান্ডসের শহরগুলোর মধ্যে সবচেয়ে ছোট পথ। অ্যালগরিদমটি ছাপা হয় ১৯৫৯ সালে, তিন পাতার একটি লেখায়, যেখানে minimum spanning tree-র সমস্যাও ছিল।
তিনি পরে মন্তব্য করেছিলেন, কাগজ-কলম না থাকায় তাঁকে সব জটিলতা এড়াতে হয়েছিল — আর সেটিই অ্যালগরিদমটিকে এত সরল করেছে। লিখিত উত্তরের জন্য এখানে একটি বড় শিক্ষা: সবচেয়ে ভালো প্রমাণগুলো ছোট, কারণ সেগুলো একটি মূল ধারণাকে কেন্দ্র করে দাঁড়ায়। Dijkstra-র প্রমাণের মূল ধারণা একটি বাক্য — “প্রথম যে vertex-এ ভুল হলো, তার আগের পথের অংশ ইতিমধ্যে ঠিক, আর অ-ঋণাত্মক weight পথকে ছোট হতে দেয় না।” এই বাক্যটি মাথায় থাকলে বাকি প্রমাণ নিজেই লেখা হয়ে যায়।
পরীক্ষার হলে তাই প্রমাণ মুখস্থ করার বদলে প্রতিটির “এক বাক্যের মূল ধারণা” মনে রাখুন। insertion sort: “বাঁ অংশ সবসময় সাজানো।” cut property: “cycle-এ আরেকটি crossing edge আছে, তাকে বদলাও।” নিম্নসীমা: “n! টি পাতা দরকার।” KMP: “k যত বাড়ে তার বেশি কমতে পারে না।” বাকি সব এই মূল থেকে গড়া যায়।
মূল উপলব্ধি: প্রতিটি প্রমাণের একটি কেন্দ্রীয় ধারণা আছে; সেটি এক বাক্যে বলতে পারলে বাকি ধাপগুলো পরীক্ষার হলেই সাজানো যায়।
প্র ০৩Ω(n log n) নিম্নসীমা প্রমাণিত, তবু radix sort আর counting sort রৈখিক সময়ে চলে। “নিম্নসীমা” কথাটির আসল অর্থ কী?
প্রতিটি নিম্নসীমা একটি গণনার মডেলের ভেতরে সত্য। decision tree-র প্রমাণ ধরে নেয় অ্যালগরিদম উপাদান সম্পর্কে তথ্য পায় শুধু দুটির তুলনা করে। এই মডেলে n log n-এর নিচে যাওয়া অসম্ভব — কোনো চতুর কৌশলেও নয়। কিন্তু radix sort তুলনা করে না; সে কী-এর অঙ্ক পড়ে আর সেটি দিয়ে সরাসরি একটি বালতি বেছে নেয় — একটি “তুলনা”-র চেয়ে অনেক বেশি তথ্য একবারে। তাই সে অন্য মডেলে খেলছে, আর সেখানে Θ(d(n + b)) সম্ভব।
মডেলটি আরও সূক্ষ্মও হতে পারে। তুলনা-মডেলের ভেতরেই “ঠিক কয়টি তুলনা লাগে” একটি পুরোনো প্রশ্ন। ১৯৫৯ সালে Lester Ford ও Selmer Johnson merge insertion নামে একটি পদ্ধতি দেন, যা ছোট n-এ ⌈log n!⌉-এর খুব কাছে পৌঁছায় — যেমন ৫টি উপাদান ৭টি তুলনায় (এই পর্বের প্রোগ্রামে সাধারণ merge sort-এর লেগেছে ৮টি)। বাস্তবে এটি কেউ ব্যবহার করেন না, কারণ তুলনা কম হলেও তথ্য সরানো জটিল। এখানেই আরেকটি শিক্ষা: “কম তুলনা” আর “কম সময়” এক কথা নয়; আধুনিক যন্ত্রে cache আর branch prediction অনেক সময় বেশি গুরুত্বপূর্ণ।
তাই পরীক্ষায় নিম্নসীমার প্রশ্নে মডেলটি প্রথম বাক্যেই বলুন: “তুলনা-ভিত্তিক সর্টিং-এর জন্য…”. আর “radix sort কি এই সীমা ভাঙে?” প্রশ্নের উত্তরও তখন সহজ — না, কারণ সে এই মডেলের বাইরে, আর তার নিজের অনুমান আছে: কী-গুলো সীমিত দৈর্ঘ্যের অঙ্কে লেখা যায়।
মূল উপলব্ধি: নিম্নসীমা “সব অ্যালগরিদমের” নয়, “একটি মডেলের সব অ্যালগরিদমের” — মডেলের অনুমান বদলালে সীমাও বদলায়।
প্র ০৪Huffman coding একজন ছাত্রের টার্ম পেপার থেকে এসেছিল। কঠিন প্রশ্নে নিজে প্রমাণ খোঁজার অভ্যাস কেন মূল্যবান?
১৯৫১ সালে MIT-তে Robert Fano তাঁর তথ্যতত্ত্বের ক্লাসে ছাত্রদের একটি বিকল্প দেন: “পরীক্ষা দাও, অথবা একটি টার্ম পেপার লেখো” — যার একটি বিষয় ছিল সবচেয়ে দক্ষ binary code খোঁজা। David Huffman টার্ম পেপার বেছে নেন। অনেক দিন চেষ্টা করেও প্রমাণযোগ্য কিছু না পেয়ে তিনি যখন হাল ছেড়ে পরীক্ষার জন্য পড়া শুরু করবেন ভাবছিলেন, তখন হঠাৎ মূল ধারণাটি আসে: tree ওপর থেকে ভাগ না করে, নিচ থেকে — সবচেয়ে কম ফ্রিকোয়েন্সির দুটি জোড়া লাগিয়ে — গড়লে সেটি প্রমাণসহ সেরা হয়। Fano আর Claude Shannon-এর আগের পদ্ধতি (Shannon–Fano coding) ওপর থেকে ভাগ করত, আর সবসময় সেরা হতো না। Huffman-এর লেখা ছাপা হয় ১৯৫২ সালে।
গল্পটির শিক্ষা হলো, “কেন এটি সেরা?” প্রশ্নটিই সমাধানের দিক ঠিক করে দেয়। Huffman-এর প্রমাণের দুটি lemma — সবচেয়ে কম ফ্রিকোয়েন্সির দুটি সবচেয়ে গভীরে ভাই, আর তাদের জোড়া লাগালে সমস্যা এক অক্ষর ছোট হয় — আসলে অ্যালগরিদমটিকেই বর্ণনা করে। অর্থাৎ প্রমাণের চেষ্টা থেকেই অ্যালগরিদম বেরিয়ে এসেছে। greedy আর DP-র অনেক প্রশ্নে একই কৌশল কাজ করে: আগে জিজ্ঞেস করুন, সেরা সমাধানের গঠন কেমন হতে বাধ্য; তারপর সেই গঠন থেকে অ্যালগরিদম গড়ুন।
লিখিত পরীক্ষায় এর ব্যবহারিক রূপ: অপরিচিত প্রশ্নে আটকালে একটি ছোট উদাহরণে সেরা উত্তরটি হাতে খুঁজুন, তারপর দেখুন তাতে কোন বৈশিষ্ট্য সবসময় থাকে। সেই বৈশিষ্ট্যই প্রায়ই exchange argument বা DP-র state হয়ে দাঁড়ায়।
মূল উপলব্ধি: “কেন সেরা?” প্রশ্নটি শুধু পরীক্ষার জন্য নয় — সেটিই প্রায়ই সমাধানের পথ দেখায়।
প্র ০৫NP-completeness-এর প্রমাণ প্রায় সবসময় অন্য একটি সমস্যা থেকে reduction. এই শিকলটি কোথা থেকে শুরু হলো, আর এর ব্যবহারিক মূল্য কী?
শিকলের প্রথম আংটা কোনো reduction দিয়ে আসেনি। ১৯৭১ সালে Stephen Cook দেখান, NP-র যেকোনো সমস্যাকে polynomial সময়ে boolean satisfiability (SAT)-এ রূপান্তর করা যায় — একটি nondeterministic যন্ত্রের পুরো হিসাবকে একটি বড় সূত্রে লিখে। প্রায় একই সময়ে সোভিয়েত ইউনিয়নে Leonid Levin স্বাধীনভাবে একই ধরনের ফল পান; তাই একে Cook–Levin theorem বলা হয়। পরের বছর, ১৯৭২ সালে, Richard Karp একটি লেখায় SAT থেকে reduction দিয়ে ২১টি পরিচিত সমস্যাকে NP-complete দেখান — তার মধ্যে clique, vertex cover আর Hamiltonian cycle-ও ছিল। এরপর থেকে নতুন সমস্যার জন্য আর পুরো যন্ত্র লিখতে হয় না; একটি জানা NP-complete সমস্যা থেকে একটি reduction-ই যথেষ্ট — যেমন ছ-০৩-এ 3-SAT থেকে independent set.
ব্যবহারিক মূল্যটি বড়। কাজের জায়গায় কোনো সমস্যা — রুটিন বানানো, ডেলিভারির পথ, গুদামে মাল সাজানো — যদি NP-complete প্রমাণ করা যায়, তবে বোঝা যায় দ্রুত exact সমাধান খোঁজার পেছনে মাসের পর মাস খরচ করা সম্ভবত বৃথা; কারণ পেলে P = NP প্রমাণ হয়ে যেত, যা কয়েক দশকের চেষ্টাতেও কেউ পারেননি। তখন মনোযোগ যায় ইনপুটের বিশেষ গঠন, approximation বা heuristic-এর দিকে।
পরীক্ষায় reduction লেখার সময় তাই দিকটি মনে রাখার সহজ উপায়: “আমি দেখাচ্ছি নতুন সমস্যাটি পুরোনো কঠিন সমস্যার মতোই কঠিন — অর্থাৎ পুরোনোটিকে নতুনটির ছদ্মবেশে লুকিয়ে ফেলা যায়।” তীর তাই পুরোনো থেকে নতুনে।
মূল উপলব্ধি: Cook–Levin একবার প্রথম আংটা গড়েছে; তারপর প্রতিটি নতুন NP-completeness প্রমাণ শুধু একটি সঠিক দিকের reduction — আর সেই জ্ঞান বাস্তবে ভুল পথে সময় নষ্ট হওয়া থেকে বাঁচায়।
নিজেকে যাচাই করুন — প্রমাণ আর বিশ্লেষণের মূল তথ্য
প্রথমে একটি ক্রম সাজানোর প্রশ্ন, তারপর প্রমাণের ধাপ আর ক্লাসিক complexity নিয়ে MCQ. ভুল হলে কারণসহ ব্যাখ্যা পাবেন।
ক্রমএকটি নতুন সমস্যা B NP-complete প্রমাণের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- B-এর certificate polynomial সময়ে যাচাই করা যায় — B ∈ NP
- একটি জানা NP-complete সমস্যা A বাছাই (যেমন 3-SAT)
- A-র instance থেকে B-র instance বানানোর polynomial-সময়ের রূপান্তর
- A-তে “হ্যাঁ” হলে রূপান্তরিত B-তেও “হ্যাঁ” — প্রমাণ
- B-তে “হ্যাঁ” হলে মূল A-তেও “হ্যাঁ” — প্রমাণ, তারপর উপসংহার
Loop invariant দিয়ে সঠিকতা প্রমাণের কোন ধাপে লুপ থামার শর্তটি ব্যবহার করা হয়? (সহজ)
- শুরু (initialization)
- সমাপ্তি (termination)
- রক্ষা (maintenance)
- কোনো ধাপেই নয়
T(n) = 3T(n/4) + n log n-এর সমাধান কী? (মাঝারি)
- Θ(nlog4 3)
- Θ(n log² n)
- Θ(n log n)
- Θ(n²)
৫টি ভিন্ন উপাদান সাজানোর যেকোনো তুলনা-ভিত্তিক অ্যালগরিদমের worst case-এ অন্তত কয়টি তুলনা লাগবে? (মাঝারি)
- ৭
- ৮
- ১২
- ১০
V টি vertex-এর গ্রাফে (negative cycle নেই) Bellman-Ford কেন V − 1 রাউন্ডেই সঠিক দূরত্ব দেয়? (মাঝারি)
- প্রতি রাউন্ডে ঠিক একটি vertex চূড়ান্ত হয়
- edge-গুলো সবসময় সঠিক ক্রমে relax হয়
- গ্রাফে মোট edge সর্বোচ্চ V − 1 টি
- সবচেয়ে ছোট পথ simple ধরা যায়, যাতে সর্বোচ্চ V − 1 টি edge
Maximal matching-এর দুই প্রান্ত নেওয়া vertex cover অ্যালগরিদমের 2-approximation প্রমাণে মূল ধাপ কোনটি? (মাঝারি)
- matching-টি সবচেয়ে বড় (maximum)
- যেকোনো cover-কে matching-এর প্রতিটি edge থেকে আলাদা একটি vertex নিতে হয়, তাই OPT ≥ |M|
- গ্রাফটি bipartite
- edge-গুলো weight-এর ক্রমে দেখা হয়
একটি dynamic array জায়গা ফুরোলে ধারণক্ষমতা দ্বিগুণ না করে প্রতিবার ১০০ ঘর বাড়ায়। n টি push-এ প্রতি push-এর amortized খরচ কত? (মাঝারি)
- O(1)
- O(log n)
- Θ(n)
- Θ(n²)
S → A (2), S → B (5), A → C (1), B → A (−4) গ্রাফে S থেকে Dijkstra চালালে C-র জন্য কোন দূরত্ব আসে, আর আসল দূরত্ব কত? (কঠিন)
- Dijkstra 3, আসল 2
- Dijkstra 3, আসল 3
- Dijkstra 2, আসল 3
- Dijkstra 6, আসল 2
n উপাদানের build-heap-এর আঁটসাঁট সময় Θ(n) হওয়ার মূল কারণ কোনটি? (কঠিন)
- প্রতিটি sift-down O(1) সময় নেয়
- শুধু মূলে sift-down চালানো হয়
- ইনপুট আগে থেকেই প্রায় সাজানো থাকে
- বেশিরভাগ node-এর উচ্চতা ছোট, আর Σ h/2h একটি ধ্রুবকে অভিসারী
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বে ব্যবহৃত সব শব্দ এখানে একসাথে:
মনে রাখুন
- বড় প্রশ্নে পাঁচ অংশ: সংজ্ঞা → অ্যালগরিদম → ছোট উদাহরণ → complexity → প্রমাণ; প্রতিটি অংশ মার্ক-স্কিমের একটি ঘর।
- প্রমাণের চার ছাঁচ চিনে নিন: invariant (লুপ), induction (recursion, recurrence), exchange (greedy, MST, Huffman), “প্রথম ভুল” contradiction (Dijkstra, BFS) — আর NP-completeness-এ সঠিক দিকের reduction.
- সংখ্যার প্রশ্নে প্রতিটি ধাপ দেখান — recursion tree-র প্রতি তলা, DP টেবিলের প্রতিটি ঘর, Dijkstra-র প্রতিটি settle — আর complexity সঠিক চলক ও Θ/O দিয়ে।
- ভাইভায় তিন বাক্য: কী, কেন, কত বা কখন নয়; জোরে ভাবুন, ছবি আঁকুন, না জানলে সৎভাবে যুক্তি দিয়ে এগোন।
- AI দিয়ে অনুশীলন করুন, কিন্তু তার প্রতিটি trace হাতে, প্রতিটি কোড brute force-এর সাথে, আর প্রতিটি greedy পাল্টা-উদাহরণ খুঁজে যাচাই করুন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৭১ · কোডিং ইন্টারভিউর সমস্যা — প্যাটার্ন ধরে সমাধানপরবর্তী পর্বইন্টারভিউর ধাঁচের সমস্যা আর কথা বলে সমাধানের কৌশল।
- পর্ব ৬৯ · MCQ প্রশ্নব্যাংক — ডেটা স্ট্রাকচার আর অ্যালগরিদমআগের পর্বযেকোনো পরীক্ষার MCQ অংশের জন্য বিষয়ভিত্তিক অনুশীলন।
- পর্ব ৬৮ · Complexity আর dry-run — হাতে চালিয়ে উত্তর বের করার অনুশীলনঅনুশীলনলিখিত উত্তরের trace আর লুপ গোনার হাত পোক্ত করে।
- পর্ব ০৩ · অ্যালগরিদম ঠিক কি না — loop invariant আর inductionফিরে দেখুনএই পাতার প্রায় সব প্রমাণের ভিত্তি।
- ডেটা স্ট্রাকচার ও অ্যালগরিদম · কোর্স হোম৭২টি পর্বযে পর্বে দুর্বলতা পেলেন, সেখানে ফিরে যান।
- C++ প্রোগ্রামিং · কোর্স হোমপাশের ট্র্যাকএই কোর্সের কোডের ভাষা — STL আর আধুনিক C++ ঝালিয়ে নিন।