সরাসরি পর্বে যান
পর্ব ৬৮ · ৭২-এর মধ্যে · মডিউল ৯

Complexity আর dry-run — হাতে চালিয়ে উত্তর বের করার অনুশীলন

Complexity and Dry-Run Drills — Counting, Recurrences, Master Theorem, Amortized Cost and Hand-Tracing Every Major Algorithm

Taught in Bangla, this page drills complexity analysis and hand tracing. It gives five methods for finding complexity, exercises on loops, sums, two pointers, recursion, the master theorem and amortized cost, and trace drills on recursion, stacks, prefix sums, sorting, heaps, BSTs, AVL trees, hashing, DSU, segment and Fenwick trees, BFS, DFS, Dijkstra, topological sort, MST, Floyd–Warshall, greedy, DP tables, strings and number theory.

এই পর্বে যা পাবেন

  • লুপ গুনে, যোগফল দিয়ে, recurrence আর master theorem দিয়ে, আর amortized যুক্তিতে complexity বের করার একটি নির্ভরযোগ্য পদ্ধতি
  • নির্ভরশীল সীমার nested loop, i *= 2 লুপ, harmonic যোগফল আর two pointer-এর ফাঁদ চেনা
  • Sorting, partition, heap, BST, AVL, hash, DSU, segment tree, Fenwick, গ্রাফ, DP আর KMP হাতে নির্ভুলভাবে trace করার অভ্যাস
  • খাতায় trace প্রশ্নের এমন উত্তর লেখা, যাতে প্রতিটি ধাপ পরীক্ষক মিলিয়ে দেখতে পারেন
  • নিজের trace কোড চালিয়ে যাচাই করা, আর AI-এর দেওয়া complexity ও trace সন্দেহের চোখে দেখা

১ · মডিউল ৯ শুরু — হাতে চালানো কেন এখনো জরুরি

কোর্সের শেষ মডিউলে স্বাগতম। প্রথম ৬৭টি পর্বে আপনি ডেটা স্ট্রাকচার আর অ্যালগরিদম শিখেছেন; এই পাঁচটি পর্বে সেগুলো পরীক্ষার টেবিলে, ইন্টারভিউর হোয়াইটবোর্ডে আর কনটেস্টের ঘড়ির সামনে কাজে লাগানোর অনুশীলন। শুরু করছি দুটি সবচেয়ে মৌলিক দক্ষতা দিয়ে: একটি কোডের খরচ বের করা, আর একটি অ্যালগরিদম কম্পিউটার ছাড়াই হাতে চালানো — যাকে বলে dry run.

বিশ্ববিদ্যালয়ের DSA বা অ্যালগরিদম কোর্সের লিখিত পরীক্ষা, চাকরির লিখিত পরীক্ষা (সরকারি-বেসরকারি ব্যাংকের IT পদ, সফটওয়্যার কোম্পানির প্রাথমিক পরীক্ষা) আর ইন্টারভিউ — সবখানে এ ধরনের প্রশ্ন প্রায়ই আসে: “এই কোডের time complexity কত?”, “[6, 3, 8, 2, 5, 1]-এ bubble sort-এর দ্বিতীয় পাসের পর অ্যারে কী?”, “এই গ্রাফে Dijkstra-র টেবিল ধাপে ধাপে দেখান।” কারণটি সরল: এই প্রশ্নগুলো মুখস্থ দিয়ে পার হওয়া যায় না। ঠিক উত্তর দিতে হলে অ্যালগরিদমের প্রতিটি নিয়ম — কোন সীমায় লুপ থামে, সমান হলে কী হয়, কোনটি আগে — সত্যিই জানতে হয়।

AI-এর যুগে এই দক্ষতার দাম আরও বেড়েছে। AI কোড লিখে দেয়, complexity বলে দেয় — কিন্তু প্রায়ই ভুল বলে (ভাগ ১০ দেখুন)। ভুল ধরার একমাত্র উপায় হলো নিজে গুনতে পারা আর নিজে চালাতে পারা। একজন অভিজ্ঞ ডেভেলপার কোড রিভিউতে ঠিক এটাই করেন: চোখ বুলিয়ে বলেন “এখানে লুপের ভেতরে একটি লুপ আছে, কিন্তু ভেতরেরটি কখনো পেছায় না — তাই আসলে রৈখিক।”

পাতার মানচিত্র — কোন ভাগে কী, কোন পর্ব ঝালাবেন

ভাগকী আছেমূল পর্ব
২Complexity বের করার পাঁচটি পদ্ধতি০৪, ০৬, ০৮, ০৯, ৬০
৩ক-০১…ক-১৪: লুপ, যোগফল, two pointer, sieve০৪–০৬, ১৬, ৬২
৪ক-১৫…ক-২৬: রিকার্শন, recurrence, master theorem, amortized০৭–০৯, ১০, ৬০
৫ট-০১…ট-১৩: রিকার্শন, stack, prefix sum, sorting, search, heap০৭, ১৩, ১৬–২৪
৬ট-১৪…ট-২০: BST, AVL, hash, DSU, segment tree, Fenwick২৬–২৮, ৩২–৩৬
৭ট-২১…ট-২৫: BFS, DFS, Dijkstra, topological sort, Kruskal, Floyd–Warshall৪০–৪৫
৮ট-২৬…ট-৩৫: Huffman, DP-র টেবিল, KMP, GCD, sieve৫১–৫৪, ৫৮, ৬২
৯খাতায় trace প্রশ্নের উত্তর লেখার কৌশল—

খাতায় trace table — একটি নমুনা

যেকোনো dry run-এর মূল যন্ত্র একটি ছক: প্রতিটি সারি একটি ধাপ (একটি পাস, একটি iteration, একটি ফাংশন কল), প্রতিটি কলাম একটি অবস্থা (ভ্যারিয়েবল, অ্যারে, কিউ)। ছকের নিয়ম তিনটি: ধাপের নাম স্পষ্ট লিখুন (যেমন “i = 3, key = 2”), প্রতিটি সারিতে পুরো অ্যারে লিখুন (শুধু বদলানো ঘর নয়), আর একটি কলাম রাখুন গণনার জন্য (তুলনা, swap, shift). নিচে insertion sort (০-based, পর্ব ২০) দিয়ে নমুনাটি দেখুন — ভাগ ৫-এর ট-০৭ এই একই ইনপুট ব্যবহার করে।

খাতায় যেভাবে লিখবেন — প্রতি সারি একটি i
সারি
কী সরল
অ্যারে
এই সারির shift
মোট shift
  1. শুরু. ছকের মাথায় নিয়মগুলো লিখুন: ০-based index, ছোট থেকে বড়, বাঁয়ের উপাদান কঠোরভাবে বড় হলেই সরে। প্রথম সারিতে মূল অ্যারে।
  2. i = 1. key = 3; বাঁয়ে 6 > 3, তাই 6 এক ঘর ডানে, 3 বসল index 0-তে। পুরো অ্যারে লিখুন: 3 6 8 2 5 1. এই সারিতে ১টি shift.
  3. i = 2. key = 8; বাঁয়ে 6 ≤ 8, কিছুই সরে না। অ্যারে বদলায়নি — তবু সারিটি লিখুন, কারণ পরীক্ষক “i = 2-এর পর” অবস্থা খুঁজবেন।
  4. i = 3. key = 2; 8, 6, 3 তিনটিই বড়, তিনটি shift, 2 বসল সামনে: 2 3 6 8 5 1. প্রশ্নে “তিনটি insertion-এর পর” চাইলে এটিই উত্তর।
  5. i = 4. key = 5; 8 আর 6 সরল, 3 ≤ 5-এ থামল: 2 3 5 6 8 1. মোট shift এখন ৬।
  6. i = 5. key = 1 পাঁচটিকেই টপকে সামনে: 1 2 3 5 6 8. মোট shift ১১ — আর ইনপুটের inversion-ও ঠিক ১১টি (পর্ব ০৫)। শেষ কলামের এই মিল হলো আপনার নিজের trace যাচাইয়ের একটি সস্তা পরীক্ষা।

নিজেকে যাচাইয়ের অভ্যাস: প্রতিটি trace-এর শেষে একটি “চেক-যোগফল” খুঁজুন — insertion sort-এ মোট shift = inversion, bubble sort-এ মোট swap = inversion, heap-এ শেষে heap property, BST-তে inorder সাজানো কি না, Dijkstra-য় প্রতিটি দূরত্ব কোনো একটি আসল পথের দৈর্ঘ্য কি না, DP টেবিলে মান কোনো দিকে কমছে কি না। মিললে উত্তর প্রায় নিশ্চিত; না মিললে কোথাও ভুল আছে।

২ · Complexity বের করার পাঁচটি পদ্ধতি

প্রতিটি complexity প্রশ্নের উত্তর আসে পাঁচটি যন্ত্রের কোনো একটি দিয়ে। কোনটি লাগবে, তা কোডের চেহারা দেখেই বোঝা যায়। নিচের বাক্সটি পরীক্ষার আগের রাতে একবার পড়ে নিন।

পদ্ধতির বাক্স

১. গোনা (count): সবচেয়ে ভেতরের কাজটি মোট কতবার চলে, সেটাই প্রশ্ন। প্রতিটি লুপের জন্য জিজ্ঞেস করুন: ভ্যারিয়েবলটি কীভাবে বদলায় (+1, ×2, ÷2, বর্গ), আর সীমা কি বাইরের ভ্যারিয়েবলের ওপর নির্ভর করে? i += 1 → n বার; i *= 2 বা i /= 2 → log n বার; i * i <= n → √n বার; i = i * i → log log n বার।

২. যোগফল (sum): ভেতরের লুপের সীমা বাইরের ভ্যারিয়েবলের ওপর নির্ভর করলে প্রতিটি i-এর কাজ লিখে যোগ করুন। চারটি যোগফল মুখস্থ রাখুন (নিচের টেবিল)।

৩. Recurrence: রিকার্সিভ ফাংশনের জন্য T(n) = (কয়টি ডাক) × T(ছোট আকার) + (ডাকের বাইরের কাজ) লিখুন, তারপর recursion tree এঁকে প্রতিটি স্তরের খরচ যোগ করুন (পর্ব ০৮)।

৪. Master theorem: T(n) = a·T(n/b) + Θ(nd) আকারে হলে nlogb a-কে nd-এর সাথে তুলনা করুন — a > bd হলে Θ(nlogb a), a = bd হলে Θ(nd log n), a < bd হলে Θ(nd) (পর্ব ০৯)। মনে রাখুন: “n − 1” ধরনের (বিয়োগের) recurrence-এ এটি খাটে না।

৫. Amortized: একটি কাজ কখনো কখনো দামি, কিন্তু দামি কাজ ঘন ঘন হতে পারে না — তখন পুরো ধারার মোট খরচ গুনে ভাগ করুন (aggregate), অথবা কোন ভ্যারিয়েবল কখনো পেছায় না তা খুঁজুন (পর্ব ৬০)। Two pointer, dynamic array আর binary counter এর প্রধান উদাহরণ।

যোগফলমানকোথায় আসে
1 + 2 + … + n (arithmetic series)n(n + 1)/2 = Θ(n²)নির্ভরশীল nested loop, insertion sort-এর worst case
1 + 2 + 4 + … + 2k (geometric series)2k+1 − 1 < 2 · (শেষ পদ)i *= 2 বাইরে আর ভেতরে i পর্যন্ত লুপ, dynamic array-র কপি
n/1 + n/2 + … + n/n (harmonic series)n · Hn ≈ n ln n = Θ(n log n)j += i লুপ, sieve-এর আত্মীয়, গুণিতক গোনা
log 1 + log 2 + … + log nlog(n!) = Θ(n log n)প্রতিটি i-এর জন্য log i কাজ
Σp ≤ n, p মৌলিক n/pΘ(n log log n)Sieve of Eratosthenes

যোগফলের পদ্ধতি কাজে দেখুন। নিচের লুপে ভেতরের j প্রতিবার i করে লাফায়, তাই i-এর জন্য ⌊n/i⌋ বার চলে। n = 8 নিয়ে খাতায় গোনার ছকটি এরকম (ক-০৬-এর সমাধানের ভিত্তি):

for (int i = 1; i <= n; i++)
    for (int j = i; j <= n; j += i) cnt++;
প্রতি i-এ j-এর মান আর গণনা
i
j-এর মান
এই i-এ কাজ = ⌊8/i⌋
মোট cnt
  1. i = 1. j লাফায় 1 করে — পুরো 1..8, ৮টি কাজ। এটি সবচেয়ে বড় পদ, n/1.
  2. i = 2. j = 2, 4, 6, 8 — ৪টি, অর্থাৎ n/2. “ভেতরের লুপ n পর্যন্ত চলে, তাই n²” — এই ভুল এখানেই ধরা পড়ে: সীমা n হলেও লাফ বড়।
  3. i = 3. j = 3, 6 — ⌊8/3⌋ = 2টি।
  4. i = 4. j = 4, 8 — ২টি। প্রতিটি পদ n/i, ক্রমশ ছোট হচ্ছে।
  5. i = 5 থেকে 8. প্রতিটিতে শুধু j = i, একবার করে — আরও ৪টি। মোট ২০.
  6. সাধারণ সূত্র. মোট = Σ ⌊n/i⌋ ≤ n(1 + 1/2 + … + 1/n) = n · Hn, আর Hn ≈ ln n. n = 8-এ সীমা প্রায় ২১.৭, আসল ২০. তাই Θ(n log n), n² নয়। নিচের ভাগ ৪-এর প্রোগ্রামে n = 216-এও এটি মেপে দেখবেন।

Θ, O আর Ω মিলিয়ে ফেলবেন না (পর্ব ০৪)। প্রশ্নে “time complexity” চাইলে সবচেয়ে টাইট উত্তর দিন — সম্ভব হলে Θ. Recursive Fibonacci-কে “O(2n)” বলা ভুল নয় (ওপরের সীমা হিসেবে সত্য), কিন্তু টাইটও নয়; Θ(φn) পূর্ণ উত্তর। আর কোনো অ্যালগরিদমের best আর worst case আলাদা হলে (যেমন insertion sort) কোনটি বলছেন, তা লিখে দিন।

৩ · Complexity ড্রিল ১ — লুপ, যোগফল আর two pointer

প্রতিটি কোডে n হলো ইনপুটের আকার, cnt++ বা লুপের শরীর O(1). প্রশ্ন সবসময় একই: মোট কাজ n-এর কোন Θ(·)? সম্ভব হলে ঠিক কতবার চলে, সেটিও বের করুন — উত্তরে সেই সংখ্যাও দেওয়া আছে, সব কোড চালিয়ে গোনা।

ক-০১ (সহজ)

for (int i = 0; i < n; i++)
    for (int j = i; j < n; j++) cnt++;
ক-০১উত্তর ও যুক্তি

Θ(n²). i-এর জন্য ভেতরের লুপ n − i বার: n + (n − 1) + … + 1 = n(n + 1)/2. n = 10-এ ঠিক ৫৫. “ভেতরের লুপ ছোট হচ্ছে, তাই n log n” — ভুল; অর্ধেক i-এর জন্যও ভেতরে অন্তত n/2 কাজ, তাই অন্তত n²/4.

ক-০২ (সহজ)

for (int i = 1; i < n; i *= 2) cnt++;
ক-০২উত্তর ও যুক্তি

Θ(log n). k-তম iteration-এ i = 2k; লুপ চলে যতক্ষণ 2k < n, অর্থাৎ ⌈log2 n⌉ বার (n ≥ 2). n = 1000-এ ১০ বার (1, 2, 4, …, 512). ভিত্তি 2 না হয়ে 3 হলেও (i *= 3) Θ(log n) — log-এর ভিত্তি শুধু ধ্রুবক গুণক বদলায়।

ক-০৩ (সহজ)

for (int i = n; i > 0; i /= 2)
    for (int j = 0; j < i; j++) cnt++;
ক-০৩উত্তর ও যুক্তি

Θ(n). বাইরের লুপ log n বার, কিন্তু “log n × n = n log n” ভুল — ভেতরের কাজ প্রতিবার অর্ধেক: n + n/2 + n/4 + … < 2n (geometric series). n = 16-এ ঠিক ৩১. বড় পদটিই (প্রথমটি) যোগফলকে নিয়ন্ত্রণ করে।

ক-০৪ (মাঝারি) — দুটি কোড পাশাপাশি; পার্থক্য শুধু ভেতরের সীমায়।

// (a)
for (int i = 1; i <= n; i *= 2)
    for (int j = 1; j <= i; j++) cnt++;
// (b)
for (int i = 1; i <= n; i *= 2)
    for (int j = 1; j <= n; j++) cnt++;
ক-০৪উত্তর ও যুক্তি

(a) Θ(n), (b) Θ(n log n). (a)-তে কাজ 1 + 2 + 4 + … + 2⌊log n⌋ < 2n; n = 16-এ ৩১. (b)-তে বাইরের লুপের ⌊log2 n⌋ + 1 বারের প্রতিটিতে পুরো n, তাই n(⌊log2 n⌋ + 1); n = 16-এ ৮০. শিক্ষা: বাইরের লুপের সংখ্যা × ভেতরের সর্বোচ্চ কাজ দিয়ে শুধু ওপরের সীমা পাওয়া যায় — টাইট উত্তরের জন্য যোগ করুন।

ক-০৫ (মাঝারি)

for (int i = 1; i <= n; i++)
    for (int j = 1; j < i; j *= 2) cnt++;
ক-০৫উত্তর ও যুক্তি

Θ(n log n). i-এর জন্য ভেতরে ⌈log2 i⌉ কাজ, মোট ≈ Σ log i = log(n!). ওপরের সীমা: প্রতিটি পদ ≤ log n, তাই ≤ n log n. নিচের সীমা: শেষ অর্ধেক (i ≥ n/2) প্রতিটির কাজ ≥ log(n/2), তাই ≥ (n/2) log(n/2) = Ω(n log n). n = 1024-এ ৯২১৭ (n log2 n = 10240-এর কাছাকাছি)।

ক-০৬ (মাঝারি)

for (int i = 1; i <= n; i++)
    for (int j = i; j <= n; j += i) cnt++;
ক-০৬উত্তর ও যুক্তি

Θ(n log n) — ভাগ ২-এর stepper. মোট Σ ⌊n/i⌋ ≈ n · Hn ≈ n ln n. n = 8-এ ২০, n = 1024-এ ৭২৬২ (n ln n ≈ 7098). এই ছাঁচে “প্রতিটি সংখ্যার সব গুণিতক ঘোরা” কাজ হয় — যেমন 1 থেকে n পর্যন্ত প্রতিটি সংখ্যার divisor-এর সংখ্যা একসাথে বের করা।

ক-০৭ (মাঝারি)

for (int i = 0; i < n; i++)
    for (int j = 0; j < i; j++)
        for (int k = 0; k < j; k++) cnt++;
ক-০৭উত্তর ও যুক্তি

Θ(n³). প্রতিটি কাজ একটি ত্রয়ী k < j < i, যেখানে তিনটিই 0..n−1 থেকে আলাদা — এমন ত্রয়ী ঠিক C(n, 3) = n(n − 1)(n − 2)/6টি। ধ্রুবক 1/6 ছোট, কিন্তু বৃদ্ধির হার ঘন। (n = 1024-এ ১৭,৮৪,৩৩,০২৪ বার — গুনে মেলানো।)

ক-০৮ (সহজ)

for (int i = 1; i * i <= n; i++) cnt++;
ক-০৮উত্তর ও যুক্তি

Θ(√n). শর্ত i ≤ √n, তাই ⌊√n⌋ বার; n = 1024-এ ৩২. Trial division দিয়ে মৌলিক যাচাই (পর্ব ৬২) এই ছাঁচের। সাবধান: n-এর bit সংখ্যা b হলে √n = 2b/2 — ইনপুটের দৈর্ঘ্যের তুলনায় exponential.

ক-০৯ (কঠিন)

for (long long i = 2; i <= n; i = i * i) cnt++;
ক-০৯উত্তর ও যুক্তি

Θ(log log n). k-তম iteration-এ i = 22k. লুপ চলে যতক্ষণ 22k ≤ n, অর্থাৎ 2k ≤ log2 n, k ≤ log2 log2 n. n = 106-এ মাত্র ৫ বার: 2, 4, 16, 256, 65536. int নিলে 65536 × 65536-এ overflow হতো — তাই long long.

ক-১০ (মাঝারি) — a সাজানো অ্যারে; K একটি ধনাত্মক সংখ্যা।

int j = 0, best = 0;
for (int i = 0; i < n; i++) {
    while (j < n && a[j] - a[i] < K) j++;
    best = max(best, j - i);
}
ক-১০উত্তর ও যুক্তি

Θ(n) — two pointer-এর amortized যুক্তি। একটি নির্দিষ্ট i-এর জন্য while অনেকবার চলতে পারে, কিন্তু j কখনো কমে না আর n ছাড়ায় না — তাই পুরো প্রোগ্রামে while-এর শরীর সব মিলিয়ে সর্বোচ্চ n বার। মোট ≤ n (বাইরের) + n (ভেতরের) = 2n. লুপের ভেতরে লুপ মানেই n² নয়; প্রশ্ন হলো ভেতরের ভ্যারিয়েবল প্রতিবার শূন্য থেকে শুরু হয় কি না।

ক-১১ (মাঝারি) — ক-১০-এর সাথে তুলনা করুন; একটি লাইনই আলাদা।

for (int i = 0; i < n; i++) {
    int j = 0;
    while (j < i && a[j] < a[i]) j++;
}
ক-১১উত্তর ও যুক্তি

Worst case Θ(n²), best case Θ(n). j প্রতিটি i-এ আবার 0 থেকে শুরু হয়, তাই amortized যুক্তি খাটে না। a ছোট থেকে বড় সাজানো হলে while i বার চলে: 0 + 1 + … + (n − 1) = n(n − 1)/2. a বড় থেকে ছোট সাজানো হলে প্রথম তুলনাতেই থামে — Θ(n). উত্তরে কোন case বলছেন, লিখুন।

ক-১২ (সহজ) — a সাজানো, T একটি লক্ষ্য যোগফল।

int l = 0, r = n - 1;
while (l < r) {
    if (a[l] + a[r] == T) break;
    if (a[l] + a[r] < T) l++; else r--;
}
ক-১২উত্তর ও যুক্তি

Θ(n) worst case. প্রতিটি iteration-এ r − l ঠিক ১ কমে (বা লুপ থামে); শুরুতে r − l = n − 1, তাই সর্বোচ্চ n − 1 বার। এটিই সাজানো অ্যারেতে two-sum-এর রৈখিক সমাধান (পর্ব ১৬); ট-০৫-এ এটি হাতে চালাবেন।

ক-১৩ (মাঝারি)

string s = "";
for (int i = 0; i < n; i++) s = s + 'a';   // compare: s += 'a';
ক-১৩উত্তর ও যুক্তি

s = s + 'a': Θ(n²); s += 'a': Θ(n) মোট (প্রতি কাজে amortized O(1)). s + 'a' একটি নতুন string বানায় আর পুরোনো i টি অক্ষর কপি করে: 1 + 2 + … + n = n(n + 1)/2 অক্ষর কপি। += জায়গায় যোগ করে, দরকারে capacity দ্বিগুণ করে — ঠিক dynamic array-র মতো (ক-২৪). Java আর Python-এ string immutable (পর্ব ১১), তাই লুপে জোড়া লাগাতে StringBuilder বা "".join(...). CPython কখনো কখনো ভেতরের একটি অপ্টিমাইজেশনে s = s + 'a' দ্রুত করে ফেলে, কিন্তু ভাষা সেই নিশ্চয়তা দেয় না।

ক-১৪ (কঠিন) — sieve of Eratosthenes; comp শুরুতে সব false.

for (int i = 2; i * i <= n; i++)
    if (!comp[i])
        for (int j = i * i; j <= n; j += i) comp[j] = true;
ক-১৪উত্তর ও যুক্তি

Θ(n log log n). ভেতরের লুপ শুধু মৌলিক i-এর জন্য চলে, প্রায় n/i বার। মোট ≈ n · Σp ≤ √n 1/p, আর মৌলিকের উল্টোগুলোর যোগফল ≈ ln ln n (Mertens-এর উপপাদ্য)। তাই n log log n — বাস্তবে প্রায় রৈখিক। n = 106-এ কাটাকাটি ২১,২২,০৪৮ বার, আর n ln ln n প্রায় ২৬ লাখ. if (!comp[i]) বাদ দিলে প্রতিটি i-এর জন্য চলত — তখন harmonic-এর মতো Θ(n log n)-এর কাছাকাছি।

৪ · Complexity ড্রিল ২ — রিকার্শন, master theorem আর amortized

রিকার্সিভ কোডের খরচ বের করার প্রথম ধাপ সবসময় একই: কোড থেকে recurrence লেখা। নিজেকে তিনটি প্রশ্ন করুন — ফাংশনটি নিজেকে কয়বার ডাকে (a)? প্রতিটি ডাকে আকার কত হয় (n/b, নাকি n − 1)? ডাকগুলোর বাইরে কত কাজ (f(n))? তারপর recursion tree আঁকুন। নিচের stepper-এ n = 8-এ তিনটি recurrence-এর গাছ পাশাপাশি — T(1) = 1 ধরে, প্রতিটি স্তরের মোট খরচ:

স্তর অনুযায়ী খরচ (node সংখ্যা × প্রতি node-এর কাজ)
স্তর
2T(n/2) + n
4T(n/2) + n
T(n/2) + n
  1. Root. তিনটিতেই root-এর কাজ f(8) = 8. পার্থক্য আসবে নিচের স্তরে — কয়টি সন্তান (a) আর প্রতিটির আকার (n/b) থেকে।
  2. স্তর ১. 2T(n/2)+n-এ দুটি node, প্রতিটি 4 — মোট আবার 8. 4T(n/2)+n-এ চারটি node, মোট 16 — দ্বিগুণ। T(n/2)+n-এ একটি node, মোট 4 — অর্ধেক।
  3. স্তর ২. একই ধারা চলছে: প্রথমটিতে 8, 8, 8 (ধ্রুবক), দ্বিতীয়টিতে 8, 16, 32 (বাড়ছে), তৃতীয়টিতে 8, 4, 2 (কমছে)। অনুপাতটি a/bd: 2/2 = 1, 4/2 = 2, 1/2.
  4. Leaf. Leaf-এর সংখ্যা alogb n = nlogb a: যথাক্রমে 8, 64 আর 1. দ্বিতীয়টিতে leaf-রাই সবচেয়ে দামি স্তর।
  5. মোট. ধ্রুবক ধারা → স্তর × প্রতি স্তর = n log n (+ n). বাড়ন্ত geometric ধারা → শেষ পদ (leaf) জেতে, nlog2 4 = n². কমন্ত ধারা → প্রথম পদ (root) জেতে, n. এটাই master theorem-এর তিন case — সূত্র মুখস্থ না থাকলেও গাছ এঁকে উত্তর বের করা যায়।

ক-১৫ (সহজ) — দুটি ফাংশনের সময় আর call stack-এর জায়গা।

int f(int n) { if (n <= 1) return 1; return f(n - 1) + 1; }
int bs(const vector<int>& a, int lo, int hi, int x) {      // a is sorted
    if (lo > hi) return -1;
    int mid = (lo + hi) / 2;
    if (a[mid] == x) return mid;
    return a[mid] < x ? bs(a, mid + 1, hi, x) : bs(a, lo, mid - 1, x);
}
ক-১৫উত্তর ও যুক্তি

f: সময় Θ(n), জায়গা Θ(n). bs: সময় Θ(log n), জায়গা Θ(log n). f-এর recurrence T(n) = T(n − 1) + O(1), আর একসাথে n টি frame stack-এ থাকে। bs-এর T(n) = T(n/2) + O(1). bs হলো tail recursion (রিকার্সিভ ডাকটিই শেষ কাজ), কিন্তু f নয় — ডাক ফেরার পর + 1 করতে হয়। C++ compiler অপ্টিমাইজেশনে bs-কে loop বানাতে পারে, কিন্তু ভাষা তার নিশ্চয়তা দেয় না — পরীক্ষায় stack-এর জায়গা গুনে লিখুন। খেয়াল করুন, vector reference-এ পাঠানো হয়েছে; মান হিসেবে পাঠালে প্রতিটি ডাকে Θ(n) কপি হতো, আর bs হয়ে যেত Θ(n log n).

ক-১৬ (মাঝারি)

long long fib(int n) { return n < 2 ? n : fib(n - 1) + fib(n - 2); }
ক-১৬উত্তর ও যুক্তি

সময় Θ(φn), φ = (1 + √5)/2 ≈ 1.618; জায়গা Θ(n). কলের সংখ্যা C(n) = 1 + C(n − 1) + C(n − 2), C(0) = C(1) = 1 — সমাধান C(n) = 2F(n + 1) − 1, যেখানে F হলো Fibonacci সংখ্যা। fib(5)-এ ১৫টি, fib(10)-এ ১৭৭টি কল (গুনে মেলানো)। O(2n) সত্য কিন্তু টাইট নয়। Stack-এ একসাথে সর্বোচ্চ n টি frame (fib(n) → fib(n−1) → … → fib(1)), তাই জায়গা Θ(n). Memoization দিলে সময় Θ(n) — পর্ব ৫২.

ক-১৭ (মাঝারি) — অর্ধেক-খোলা পরিসর [lo, hi).

void g(int lo, int hi) {
    if (hi - lo <= 1) return;
    int mid = (lo + hi) / 2;
    g(lo, mid); g(mid, hi);
    for (int i = lo; i < hi; i++) cnt++;     // linear work, like merge
}
ক-১৭উত্তর ও যুক্তি

Θ(n log n). T(n) = 2T(n/2) + Θ(n): a = 2, b = 2, d = 1, a = bd → master theorem-এর case 2. গাছে log2 n স্তর, প্রতিটিতে মোট n — merge sort-এর হুবহু ছাঁচ (পর্ব ২১)। জায়গা: stack-এ Θ(log n) frame.

ক-১৮ (মাঝারি) — ক-১৭ থেকে for লুপটি বাদ দিয়ে, তার জায়গায় শুধু cnt++;

ক-১৮উত্তর ও যুক্তি

Θ(n). T(n) = 2T(n/2) + Θ(1): a = 2 > bd = 20 = 1 → case 1, Θ(nlog2 2) = Θ(n). গাছে মোট node ≈ 2n − 1, প্রতিটিতে O(1). অনেকে “ভাগ করে জয় = n log n” মুখস্থ বলেন — ভুল; প্রতি স্তরের কাজই ঠিক করে। Binary tree-র সব node ঘোরা (পর্ব ২৬) এই ছাঁচ।

ক-১৯ (মাঝারি) — master theorem দ্রুত-রাউন্ড: পাঁচটি recurrence.

(a) T(n) = 4T(n/2) + n        (b) T(n) = 3T(n/2) + n
(c) T(n) = 7T(n/2) + n^2      (d) T(n) = 2T(n/4) + sqrt(n)
(e) T(n) = T(n/2) + n
ক-১৯উত্তর ও যুক্তি

প্রতিটিতে a আর bd তুলনা করুন:

  • (a) Θ(n²): 4 > 21, nlog2 4 = n². (n = 220-এ T(n)/n² → 2, গুনে মেলানো।)
  • (b) Θ(nlog2 3) ≈ Θ(n1.585): 3 > 2 — Karatsuba-র গুণ (পর্ব ৪৯)।
  • (c) Θ(nlog2 7) ≈ Θ(n2.807): 7 > 2² = 4 — Strassen-এর matrix গুণ।
  • (d) Θ(√n log n): a = 2, bd = 41/2 = 2, সমান → case 2.
  • (e) Θ(n): 1 < 21 → case 3 (regularity শর্ত: f(n/2) = n/2 ≤ c · n, c = 1/2 — খাটে)। গড় হিসেবে quickselect-এর ছাঁচ (পর্ব ২২)।

ক-২০ (কঠিন)

T(n) = 2T(n/2) + n log n
ক-২০উত্তর ও যুক্তি

Θ(n log² n). ফাঁদ: n log n “n-এর চেয়ে বড়”, তাই অনেকে case 3 ধরে Θ(n log n) বলেন। কিন্তু case 3-এ f(n)-কে nlogb a = n-এর চেয়ে polynomially বড় হতে হয় (n1+ε); log n কোনো nε-এর চেয়ে ধীরে বাড়ে। তিন-case-এর মূল রূপ এখানে খাটে না। Recursion tree: স্তর i-তে 2i টি node, প্রতিটির কাজ (n/2i) log(n/2i), স্তরের মোট n(log n − i). যোগ করলে n · (log n + (log n − 1) + … + 1) = Θ(n log² n). সম্প্রসারিত case 2 (f(n) = Θ(nlogb a logk n) হলে Θ(nlogb a logk+1 n)) একই উত্তর দেয়।

ক-২১ (মাঝারি) — বিয়োগের recurrence.

(a) T(n) = T(n - 1) + n        (b) T(n) = 2T(n - 1) + 1,   T(0) = 0
ক-২১উত্তর ও যুক্তি

(a) Θ(n²), (b) Θ(2n) (ঠিক 2n − 1). Master theorem এখানে খাটে না — আকার ভাগ হয় না, এক কমে। (a) খুলে লিখুন: n + (n − 1) + … + 1 = n(n + 1)/2 — সাজানো ইনপুটে শেষ উপাদানকে pivot ধরা quick sort-এর worst case (পর্ব ২২)। (b) হলো Tower of Hanoi-র চাল (পর্ব ০৭): প্রতিটি স্তরে কল দ্বিগুণ।

ক-২২ (কঠিন) — অসমান ভাগ।

T(n) = T(n/3) + T(2n/3) + n
ক-২২উত্তর ও যুক্তি

Θ(n log n). Master theorem-এর আকারে নেই (দুটি আলাদা আকার)। Recursion tree: প্রতিটি পূর্ণ স্তরের মোট আকার n/3 + 2n/3 = n, তাই প্রতি স্তরে ≤ n কাজ। সবচেয়ে লম্বা পথ (প্রতিবার 2/3) log3/2 n স্তর, সবচেয়ে ছোট পথ (প্রতিবার 1/3) log3 n স্তর — প্রথম log3 n স্তর পুরো ভরা। তাই n log3 n ≤ T(n) ≤ n log3/2 n (ধ্রুবক বাদে) — দুই দিকেই Θ(n log n). শিক্ষা: quick sort-এ pivot সবসময় ১:২ অনুপাতে ভাগ করলেও n log n থাকে।

ক-২৩ (কঠিন)

T(n) = 2T(sqrt(n)) + log n
ক-২৩উত্তর ও যুক্তি

Θ(log n · log log n). Change of variables (পর্ব ০৮): m = log2 n ধরুন, তাহলে √n = 2m/2. S(m) = T(2m) লিখলে S(m) = 2S(m/2) + m — ক-১৭-এর মতো, Θ(m log m). m = log n বসিয়ে Θ(log n log log n).

ক-২৪ (মাঝারি) — খালি dynamic array-তে n বার push_back. ভরে গেলে নতুন অ্যারে নিয়ে সব উপাদান কপি করা হয়।

(a) new capacity = 2 * old capacity      (b) new capacity = old capacity + 10
ক-২৪উত্তর ও যুক্তি

(a) মোট Θ(n), প্রতি push amortized O(1); (b) মোট Θ(n²), প্রতি push amortized Θ(n). (a)-তে কপি হয় 1 + 2 + 4 + … (n-এর চেয়ে ছোট সবচেয়ে বড় ২-এর ঘাত পর্যন্ত) < 2n — capacity 1 থেকে শুরু করলে n = 216-এ ঠিক ৬৫,৫৩৫টি কপি। (b)-তে কপি 10 + 20 + 30 + … ≈ n²/20 — গুনে অনুপাত 0.05-এ স্থির। একটি একক push-এর worst case দুটিতেই Θ(n); amortized মানে “ধারার মোট ÷ n”, গড়-case (সম্ভাবনা) নয়। C++-এর std::vector আর Python-এর list গুণ করে বাড়ে, যোগ করে নয়।

ক-২৫ (মাঝারি) — শূন্য থেকে শুরু করা একটি binary counter-কে n বার increment; খরচ = উল্টানো bit-এর সংখ্যা।

void increment(vector<int>& bit) {
    int i = 0;
    while (bit[i] == 1) { bit[i] = 0; i++; }   // carry
    bit[i] = 1;
}
ক-২৫উত্তর ও যুক্তি

মোট Θ(n), প্রতি increment amortized O(1) — যদিও একটি increment (যেমন 0111 → 1000) Θ(log n) bit উল্টাতে পারে। Aggregate যুক্তি: bit 0 প্রতিবার উল্টায় (n বার), bit 1 প্রতি দুবারে একবার (n/2), bit 2 প্রতি চারবারে (n/4)… মোট < 2n. n = 216-এ উল্টানো bit ঠিক 2n − 1 = ১,৩১,০৭১.

ক-২৬ (মাঝারি) — V টি vertex আর E টি edge-এর গ্রাফে।

(a) BFS, adjacency matrix         (b) BFS, adjacency list
(c) Dijkstra, binary heap          (d) Floyd-Warshall
ক-২৬উত্তর ও যুক্তি

(a) Θ(V²), (b) Θ(V + E), (c) O((V + E) log V), (d) Θ(V³). (a)-তে প্রতিটি বের করা vertex-এর জন্য matrix-এর পুরো সারি (V টি ঘর) দেখতে হয়, edge কম হলেও। (b)-তে প্রতিটি vertex একবার কিউতে, প্রতিটি edge (undirected হলে দুই দিক থেকে) একবার দেখা। (c)-তে প্রতিটি relaxation একটি O(log V) heap কাজ (পর্ব ৪৩)। (d)-তে তিনটি nested loop, প্রতিটি V বার (পর্ব ৪৪)। গ্রাফের প্রশ্নে সবসময় V আর E আলাদা করে লিখুন — শুধু “n” লিখলে উত্তর অস্পষ্ট।

নিজের গণনা মিলিয়ে নিন — প্রোগ্রাম ১

নিচের প্রোগ্রাম ক-০৫, ক-০৪(a), ক-০৬, ক-০৯, ক-১৪ আর ক-২৪(a)-এর লুপগুলো আক্ষরিক অর্থে চালিয়ে গোনে, তিনটি n-এ। শেষ কলামটি doubling test: n দ্বিগুণ করলে কাজ কত গুণ বাড়ে। Θ(n)-এ প্রায় 2, Θ(n log n)-এ 2-এর একটু বেশি (n = 216-এ 2 · 16/15 ≈ 2.13), Θ(n²)-এ 4, আর Θ(log log n)-এ প্রায় 1.

#include <cstdio>
#include <vector>
#include <initializer_list>
using namespace std;
using ll = long long;
ll logLoop(ll n)   { ll c = 0; for (ll i = 1; i <= n; ++i) for (ll j = 1; j < i; j *= 2) ++c; return c; }
ll geometric(ll n) { ll c = 0; for (ll i = 1; i <= n; i *= 2) for (ll j = 1; j <= i; ++j) ++c; return c; }
ll harmonic(ll n)  { ll c = 0; for (ll i = 1; i <= n; ++i) for (ll j = i; j <= n; j += i) ++c; return c; }
ll logLog(ll n)    { ll c = 0; for (ll i = 2; i <= n; i = i * i) ++c; return c; }
ll sieve(ll n) {                                   // count the crossing-out steps
    vector<bool> comp(n + 1); ll c = 0;
    for (ll i = 2; i * i <= n; ++i)
        if (!comp[i]) for (ll j = i * i; j <= n; j += i) { comp[j] = true; ++c; }
    return c;
}
ll amortized(ll n) {                               // copies made by a doubling array
    ll cap = 1, size = 0, copies = 0;
    for (ll i = 0; i < n; ++i) { if (size == cap) { copies += size; cap *= 2; } ++size; }
    return copies;
}
int main() {
    const char* name[] = {"log-loop", "geometric", "harmonic", "loglog", "sieve", "push-copy"};
    ll (*f[])(ll) = {logLoop, geometric, harmonic, logLog, sieve, amortized};
    printf("%-10s %9s %9s %9s  %s\n", "drill", "n=2^10", "n=2^13", "n=2^16", "f(2^16)/f(2^15)");
    for (int k = 0; k < 6; ++k) {
        printf("%-10s", name[k]);
        for (ll n : {1 << 10, 1 << 13, 1 << 16}) printf(" %9lld", f[k](n));
        printf("  %.3f\n", (double)f[k](1 << 16) / f[k](1 << 15));
    }
}

আউটপুট:

drill         n=2^10    n=2^13    n=2^16  f(2^16)/f(2^15)
log-loop        9217     98305    983041  2.143
geometric       2047     16383    131071  2.000
harmonic        7262     75108    736974  2.131
loglog             4         4         5  1.250
sieve           1450     13735    123951  2.074
push-copy       1023      8191     65535  2.000

প্রতিটি সারি একটি ড্রিলের উত্তর মেলায়। log-loop (ক-০৫) আর harmonic (ক-০৬): অনুপাত 2.14 আর 2.13 — n log n-এর ভবিষ্যদ্বাণী 2 · 16/15 ≈ 2.13-এর সাথে মেলে। geometric (ক-০৪a): ঠিক 2n − 1 (2047 = 2 · 1024 − 1), রৈখিক। loglog (ক-০৯): n ৬৪ গুণ বাড়লেও গণনা 4 থেকে মাত্র 5. sieve (ক-১৪): অনুপাত 2.07 — রৈখিকের খুব কাছে, log log n এত ধীরে বাড়ে। push-copy (ক-২৪a): মোট কপি ঠিক n − 1, প্রতি push-এ একটিরও কম। লক্ষ করুন, doubling test একটি প্রমাণ নয় — n log n আর n1.1-কে এই আকারে আলাদা করা কঠিন। পরীক্ষায় যুক্তি লিখবেন; প্রোগ্রামটি শুধু নিজের যুক্তির ভুল ধরার জন্য।

def log_loop(n):
    c = 0
    for i in range(1, n + 1):
        j = 1
        while j < i: c += 1; j *= 2
    return c
def geometric(n):
    c, i = 0, 1
    while i <= n: c += i; i *= 2              # the inner loop runs i times
    return c
def harmonic(n):
    return sum(len(range(i, n + 1, i)) for i in range(1, n + 1))
def log_log(n):
    c, i = 0, 2
    while i <= n: c += 1; i = i * i
    return c
def sieve(n):                                  # count the crossing-out steps
    comp, c = [False] * (n + 1), 0
    for i in range(2, int(n ** 0.5) + 1):
        if not comp[i]:
            for j in range(i * i, n + 1, i): comp[j] = True; c += 1
    return c
def amortized(n):                              # copies made by a doubling array
    cap, size, copies = 1, 0, 0
    for _ in range(n):
        if size == cap: copies += size; cap *= 2
        size += 1
    return copies
print(f"{'drill':10} {'n=2^10':>9} {'n=2^13':>9} {'n=2^16':>9}  f(2^16)/f(2^15)")
for name, f in [("log-loop", log_loop), ("geometric", geometric), ("harmonic", harmonic),
                ("loglog", log_log), ("sieve", sieve), ("push-copy", amortized)]:
    print(f"{name:10}" + "".join(f" {f(n):9}" for n in (1 << 10, 1 << 13, 1 << 16)),
          f" {f(1 << 16) / f(1 << 15):.3f}")

আউটপুট C++-এর সাথে হুবহু এক:

drill         n=2^10    n=2^13    n=2^16  f(2^16)/f(2^15)
log-loop        9217     98305    983041  2.143
geometric       2047     16383    131071  2.000
harmonic        7262     75108    736974  2.131
loglog             4         4         5  1.250
sieve           1450     13735    123951  2.074
push-copy       1023      8191     65535  2.000

Python-এ দুটি জায়গায় গোনা সংক্ষিপ্ত করা হয়েছে, ফল না বদলে: geometric-এ ভেতরের লুপ না চালিয়ে সরাসরি i যোগ (লুপটি ঠিক i বার চলত), আর harmonic-এ len(range(i, n + 1, i)) — range অবজেক্ট নিজের দৈর্ঘ্য O(1)-এ জানে। তবু log_loop n = 216-এ প্রায় দশ লাখ বার সত্যিই ঘোরে, তাই C++-এর চেয়ে বেশ ধীর। int(n ** 0.5) এখানে নিরাপদ কারণ n ছোট; খুব বড় n-এ floating-point ভুল এড়াতে math.isqrt নিন।

৫ · Trace ড্রিল ১ — রিকার্শন, stack, prefix sum, সর্টিং, সার্চ আর heap

এবার হাতে চালানোর পালা। প্রতিটি প্রশ্নে নিয়ম (index, pivot, ক্রম) দেওয়া আছে; খাতায় trace table এঁকে উত্তর লিখুন, তারপর খুলে মেলান। মডিউল ১–৩-এর অ্যালগরিদম এখানে।

ট-০১ (সহজ) — fib(n) = n < 2 ? n : fib(n-1) + fib(n-2). fib(5) ডাকলে মোট কয়টি কল হয়? fib(2) কয়বার ডাকা হয়? Call stack-এ একসাথে সর্বোচ্চ কয়টি frame থাকে?

ট-০১উত্তর

মোট ১৫টি কল; fib(2) ৩ বার; সর্বোচ্চ গভীরতা ৫টি frame. গণনা: fib(5) ১, fib(4) ১, fib(3) ২, fib(2) ৩, fib(1) ৫, fib(0) ৩ — যোগ ১৫ = 2F(6) − 1. সবচেয়ে গভীর পথ fib(5) → fib(4) → fib(3) → fib(2) → fib(1). খাতায় call tree আঁকুন: প্রতিটি node-এ n লিখে বাঁয়ে n − 1, ডানে n − 2.

ট-০২ (মাঝারি) — Binary exponentiation (ডান থেকে bit পড়া): r = 1; যতক্ষণ e > 0: e বিজোড় হলে r = r · b mod m; b = b² mod m; e = e / 2. 313 mod 7 trace করুন।

ট-০২উত্তর

উত্তর 3. 13 = 11012. সারিগুলো (e, সেই সময়ের b, কাজের পর r):

e = 13 (odd):  r = 1*3 mod 7 = 3     b -> 9 mod 7 = 2
e = 6  (even): r = 3                 b -> 4
e = 3  (odd):  r = 3*4 mod 7 = 5     b -> 16 mod 7 = 2
e = 1  (odd):  r = 5*2 mod 7 = 3     b -> 4,  e -> 0, stop

চেক: 36 = 729 = 7 · 104 + 1, তাই 312 ≡ 1 আর 313 ≡ 3 (mod 7) ✓. গুণ মাত্র ৪ ধাপে — Θ(log e).

ট-০৩ (সহজ) — Stack দিয়ে postfix রাশি 5 3 2 * + 4 --এর মান বের করুন; প্রতিটি token-এর পর stack লিখুন (নিচ → ওপর)।

ট-০৩উত্তর

উত্তর 7. 5 → [5]; 3 → [5, 3]; 2 → [5, 3, 2]; * → 3·2: [5, 6]; + → 5+6: [11]; 4 → [11, 4]; − → 11−4: [7]. মনে রাখুন বিয়োগ আর ভাগে ক্রম: প্রথমে pop করা হয় ডান operand (b), তারপর বাঁ (a), ফল a − b. উল্টালে −7 আসে — প্রচলিত ভুল।

ট-০৪ (সহজ) — (a) a = [3, 1, 4, 1, 5, 9, 2]-এর prefix array P (P[0] = 0, P[i] = a[0] + … + a[i−1]) লিখুন; a[2..5]-এর যোগফল (০-based, দুই প্রান্তসহ) P দিয়ে বের করুন। (b) ছয় ঘরের শূন্য অ্যারেতে difference array দিয়ে দুটি কাজ: [1..3]-এ +5, [2..5]-এ +2. শেষ অ্যারে কী?

ট-০৪উত্তর

(a) P = [0, 3, 4, 8, 9, 14, 23, 25]; যোগফল = P[6] − P[2] = 23 − 4 = 19 (4 + 1 + 5 + 9 ✓). সীমা মনে রাখুন: a[l..r] = P[r+1] − P[l]. (b) d[1] += 5, d[4] −= 5, d[2] += 2, d[6] −= 2 → d = [0, 5, 2, 0, −5, 0] (d[6] অ্যারের বাইরে, বাদ). Prefix যোগ করে [0, 5, 7, 7, 2, 2] (পর্ব ১৭)।

ট-০৫ (সহজ) — সাজানো a = [1, 2, 4, 7, 11, 15], লক্ষ্য 15; ক-১২-এর two pointer চালান। প্রতিটি ধাপে (l, r, যোগফল) লিখুন।

ট-০৫উত্তর

(0, 5, 16) বেশি → r−−; (0, 4, 12) কম → l++; (1, 4, 13) কম → l++; (2, 4, 15) পাওয়া গেল. জোড়া 4 + 11, index 2 আর 4; চার ধাপে।

ট-০৬ (সহজ) — Bubble sort (early exit, ছোট থেকে বড়) [6, 3, 8, 2, 5, 1]-এ: প্রথম আর দ্বিতীয় পাসের পর অ্যারে? মোট কয়টি পাস আর কয়টি swap?

ট-০৬উত্তর

পাস ১: [3, 6, 2, 5, 1, 8] (৪ swap); পাস ২: [3, 2, 5, 1, 6, 8] (৩ swap); পাস ৩: [2, 3, 1, 5, 6, 8]; পাস ৪: [2, 1, 3, 5, 6, 8]; পাস ৫: [1, 2, 3, 5, 6, 8]. মোট ৫টি পাস (n − 1 = ৫, শেষ পাসেও swap হয়েছে বলে আগে থামেনি), ১১টি swap = inversion-এর সংখ্যা ✓.

ট-০৭ (সহজ) — Insertion sort, একই ইনপুট [6, 3, 8, 2, 5, 1]: “তিনটি insertion-এর পর” (i = 1, 2, 3 শেষে) অ্যারে কী?

ট-০৭উত্তর

[2, 3, 6, 8, 5, 1] — ভাগ ১-এর stepper-এর চতুর্থ সারি। বাঁয়ের চারটি ঘর সাজানো, কিন্তু চূড়ান্ত নয় (1 পরে এসে সবাইকে ঠেলবে)। “পাস” শব্দটির মানে নিয়ে সন্দেহ হলে উত্তরে লিখুন “i = 1 থেকে গুনে”.

ট-০৮ (সহজ) — Selection sort (প্রতিবার সবচেয়ে ছোটটি সামনে), একই ইনপুট: দুটি পাসের পর অ্যারে?

ট-০৮উত্তর

পাস ১: 1 আর 6 বদল → [1, 3, 8, 2, 5, 6]; পাস ২: বাকিদের সবচেয়ে ছোট 2, 3-এর সাথে বদল → [1, 2, 8, 3, 5, 6]. তিন সর্টের মাঝপথের অবস্থা আলাদা চেনা যায়: bubble-এ ডানে বড়গুলো চূড়ান্ত, selection-এ বাঁয়ে ছোটগুলো চূড়ান্ত, insertion-এ বাঁ অংশ সাজানো কিন্তু চূড়ান্ত নয়।

ট-০৯ (মাঝারি) — Binary search (০-based, lo = 0, hi = n − 1, mid = (lo + hi)/2 নিচে গোল) B = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]-এ: (a) 23 খুঁজুন, (b) 40 খুঁজুন। প্রতিটি ধাপে (lo, hi, mid) লিখুন।

ট-০৯উত্তর

(a) (0, 9, 4) → B[4] = 16 < 23, lo = 5; (5, 9, 7) → 56 > 23, hi = 6; (5, 6, 5) → 23 পাওয়া গেল, index 5, ৩টি তুলনা-ধাপ. (b) একই প্রথম তিন ধাপ (23 < 40, lo = 6); (6, 6, 6) → 38 < 40, lo = 7; এখন lo = 7 > hi = 6, থামুন — পাওয়া যায়নি, ৪ ধাপ. থামার সময় lo = 7 হলো 40 ঢোকানোর জায়গা (প্রথম ≥ 40 উপাদান 56-এর index) — lower_bound-এর উত্তর।

ট-১০ (মাঝারি) — Top-down merge sort [38, 27, 43, 3, 9, 82, 10]; অর্ধেক-খোলা [lo, hi), mid = (lo + hi)/2, বাঁ অর্ধেক আগে। Merge-গুলো কোন ক্রমে হয়? শেষ merge-এ কয়টি তুলনা?

ট-১০উত্তর

প্রথম ভাগ [0, 3) আর [3, 7) — বাঁয়ে ৩টি, ডানে ৪টি। Merge-এর ক্রম:

1. [27] + [43]          -> [27, 43]                    1 comparison
2. [38] + [27, 43]      -> [27, 38, 43]                2
3. [3] + [9]            -> [3, 9]                      1
4. [82] + [10]          -> [10, 82]                    1
5. [3, 9] + [10, 82]    -> [3, 9, 10, 82]              2
6. [27, 38, 43] + [3, 9, 10, 82] -> [3, 9, 10, 27, 38, 43, 82]   6

শেষ merge-এ ৬টি তুলনা (বাঁয়ের তিনটি শেষ হওয়ার পর 82 তুলনা ছাড়াই যায়)। মোট ১৩টি। “বাঁ অর্ধেকে বেশি” নিয়ম নিলে ক্রম বদলাত — তাই mid-এর সূত্র উত্তরে লিখুন।

ট-১১ (মাঝারি) — (a) Lomuto partition (pivot = শেষ উপাদান) [7, 2, 9, 4, 3, 8, 6]-এ। (b) Hoare partition (pivot = প্রথম উপাদান, পর্ব ২২-এর pseudocode) [5, 3, 8, 4, 2, 7, 1, 6]-এ। প্রতিটির শেষ অ্যারে আর ফেরত দেওয়া index?

ট-১১উত্তর

(a) pivot 6. j চলার সময় ≤ 6 পাওয়া যায় 2, 4, 3 — তারা i = 0, 1, 2-তে আসে: [2, 7, 9, 4, 3, 8, 6] → [2, 4, 9, 7, 3, 8, 6] → [2, 4, 3, 7, 9, 8, 6]; শেষে a[3] ↔ pivot: [2, 4, 3, 6, 9, 8, 7], index 3 — pivot নিজের চূড়ান্ত জায়গায়।

(b) pivot 5. প্রথম জোড়া: i থামে index 0-তে (5 ≥ 5), j থামে index 6-এ (1 ≤ 5) → swap: [1, 3, 8, 4, 2, 7, 5, 6]. পরের জোড়া: i = 2 (8), j = 4 (2) → swap: [1, 3, 2, 4, 8, 7, 5, 6]. তারপর j = 3, i = 4, i ≥ j — ফেরত j = 3, অ্যারে [1, 3, 2, 4, 8, 7, 5, 6]. সাবধান: Hoare-এ pivot (5) index 3-এ বসে না! শুধু নিশ্চয়তা: a[0..3] ≤ 5 ≤ a[4..7]. পরের রিকার্শন [0..3] আর [4..7]-এ — Lomuto-র মতো p − 1 আর p + 1 নয়।

ট-১২ (মাঝারি) — (a) খালি min-heap-এ (০-based অ্যারে, sift-up) 10, 4, 15, 2, 8, 20, 1 ক্রমে insert করুন; তারপর দুবার extract-min (শেষ উপাদান root-এ এনে sift-down, সমান হলে বাঁ সন্তান)। প্রতিটি ধাপের পর অ্যারে? (b) [3, 9, 2, 1, 4, 5, 8, 7]-এ Floyd-এর bottom-up build-heap (max-heap) চালালে ফল?

ট-১২উত্তর

(a) [10] → [4, 10] → [4, 10, 15] → [2, 4, 15, 10] → [2, 4, 15, 10, 8] → [2, 4, 15, 10, 8, 20] → [1, 4, 2, 10, 8, 20, 15] (1 index 6 থেকে 2-এ, তারপর 0-তে)। Extract 1: 15 root-এ, ছোট সন্তান 2-এর সাথে বদল → [2, 4, 15, 10, 8, 20]. Extract 2: 20 root-এ → 4-এর সাথে → 8-এর সাথে → [4, 8, 15, 10, 20].

(b) i = 3 থেকে 0 পর্যন্ত sift-down: i = 3 (1 ↔ 7) → [3, 9, 2, 7, 4, 5, 8, 1]; i = 2 (2 ↔ 8) → [3, 9, 8, 7, 4, 5, 2, 1]; i = 1 (9 ঠিক আছে); i = 0 (3 ↔ 9, তারপর 3 ↔ 7) → [9, 7, 8, 3, 4, 5, 2, 1]. চেক: প্রতিটি a[i] ≥ a[2i+1], a[2i+2] ✓. একে একে insert করলে অন্য heap আসতে পারে — প্রশ্ন কোন পদ্ধতি চাইছে, দেখে নিন।

ট-১৩ (মাঝারি) — LSD radix sort (ভিত্তি 10, প্রতিটি অঙ্কে stable counting sort) [170, 45, 75, 90, 802, 24, 2, 66]-এ। প্রতিটি পাসের পর তালিকা?

ট-১৩উত্তর

একক অঙ্ক: [170, 90, 802, 2, 24, 45, 75, 66]. দশক: [802, 2, 24, 45, 66, 170, 75, 90]. শতক: [2, 24, 45, 66, 75, 90, 170, 802]. দ্বিতীয় পাসে 802 আর 2 দুটির দশকই 0 — stable বলে আগের ক্রম (802 আগে) থাকে; একই পাসে 170 আর 75 (দুটির দশকই 7)-ও তাই। Stable না হলে আগের পাসের কাজ নষ্ট হয় (পর্ব ২৪)।

নিজের trace মিলিয়ে নিন — প্রোগ্রাম ২

ট-০৬ থেকে ট-১২-এর সর্টিং, partition আর heap-এর উত্তর একসাথে ছাপায়। প্রতিটি অংশ ঠিক প্রশ্নের নিয়মে লেখা: bubble দুই পাস, insertion i = 1..3, selection দুই পাস, Lomuto (শেষ pivot), Hoare (প্রথম pivot), min-heap-এ sift-up আর sift-down.

#include <cstdio>
#include <vector>
#include <utility>
#include <initializer_list>
using namespace std;
using V = vector<int>;
void show(const char* label, const V& a) { printf("%-14s", label); for (int x : a) printf(" %d", x); printf("\n"); }
int main() {
    const V start = {6, 3, 8, 2, 5, 1}; const int n = start.size();
    V a = start;                                                      // bubble: 2 passes
    for (int i = 0; i < 2; ++i) for (int j = 0; j + 1 < n - i; ++j) if (a[j] > a[j + 1]) swap(a[j], a[j + 1]);
    show("bubble p2", a);
    a = start;                                                        // insertion: i = 1..3
    for (int i = 1; i <= 3; ++i) { int key = a[i], j = i; for (; j > 0 && a[j - 1] > key; --j) a[j] = a[j - 1]; a[j] = key; }
    show("insertion i=3", a);
    a = start;                                                        // selection: 2 passes
    for (int i = 0; i < 2; ++i) { int m = i; for (int j = i + 1; j < n; ++j) if (a[j] < a[m]) m = j; swap(a[i], a[m]); }
    show("selection p2", a);
    a = {7, 2, 9, 4, 3, 8, 6}; int i = -1;                            // Lomuto, pivot = last
    for (int j = 0; j < 6; ++j) if (a[j] <= a[6]) swap(a[++i], a[j]);
    swap(a[i + 1], a[6]); show("lomuto", a); printf("  pivot index %d\n", i + 1);
    a = {5, 3, 8, 4, 2, 7, 1, 6}; int p = a[0], l = -1, r = 8;        // Hoare, pivot = first
    while (true) { do --r; while (a[r] > p); do ++l; while (a[l] < p); if (l >= r) break; swap(a[l], a[r]); }
    show("hoare", a); printf("  returned j = %d\n", r);
    V h;                                                              // min-heap, 0-based
    for (int x : {10, 4, 15, 2, 8, 20, 1}) {
        h.push_back(x);                                               // sift-up
        for (int k = h.size() - 1; k > 0 && h[(k - 1) / 2] > h[k]; k = (k - 1) / 2) swap(h[k], h[(k - 1) / 2]);
    }
    show("heap built", h);
    for (int t = 0; t < 2; ++t) {                                     // extract-min twice
        h[0] = h.back(); h.pop_back(); int k = 0, m = h.size();
        for (int c; (c = 2 * k + 1) < m; k = c) {                     // sift-down
            if (c + 1 < m && h[c + 1] < h[c]) ++c;
            if (h[k] <= h[c]) break;
            swap(h[k], h[c]);
        }
    }
    show("heap -2 mins", h);
}

আউটপুট:

bubble p2      3 2 5 1 6 8
insertion i=3  2 3 6 8 5 1
selection p2   1 2 8 3 5 6
lomuto         2 4 3 6 9 8 7
  pivot index 3
hoare          1 3 2 4 8 7 5 6
  returned j = 3
heap built     1 4 2 10 8 20 15
heap -2 mins   4 8 15 10 20

প্রতিটি লাইন ট-০৬, ট-০৭, ট-০৮, ট-১১ আর ট-১২-এর উত্তরের সাথে হুবহু মেলে। লক্ষ করুন, extract-min-এর জন্য লাইব্রেরির std::pop_heap নেওয়া হয়নি: libstdc++-এর pop_heap খালি ঘরটিকে আগে একেবারে নিচ পর্যন্ত নামিয়ে তারপর শেষ উপাদানকে ওপরে তোলে। ফল সবসময় বৈধ heap, আর এই ইনপুটে বইয়ের অ্যারের সাথে মেলেও — কিন্তু C++ standard ভেতরের সাজানোর কোনো নিশ্চয়তা দেয় না, আর সমান মান থাকলে ফল বইয়ের trace থেকে আলাদা হতে পারে। তাই trace মেলাতে নিজের হাতে লেখা sift-down. Hoare-এর do … while ঠিক pseudocode-এর “repeat … until”. প্রশ্নের ইনপুট বদলে চালান — নিজেই নতুন trace প্রশ্ন বানাতে পারবেন।

৬ · Trace ড্রিল ২ — BST, AVL, hash, DSU, segment tree আর Fenwick

Tree-র প্রশ্নে অ্যারের বদলে ছবি আঁকতে হয়। খাতায় প্রতিটি insert বা rotation-এর পর পুরো tree আবার আঁকুন — ছোট করে হলেও। তারপর উত্তর দিন একটি traversal দিয়ে (preorder বা level order), কারণ একটি BST-কে তার preorder দিয়েই পুরো চেনা যায়।

ট-১৪ (মাঝারি) — খালি BST-তে 50, 30, 70, 20, 40, 60, 80, 35, 45, 65 ক্রমে insert করুন। (a) Preorder, inorder, postorder আর level order লিখুন; উচ্চতা কত (শুধু root-এর tree-র উচ্চতা 0)? (b) তারপর 30 delete, তারপর 50 delete (দুই সন্তান হলে inorder successor দিয়ে বদল) — শেষ tree-র preorder?

ট-১৪উত্তর
            50
         /      \
       30        70
      /  \      /  \
    20    40   60   80
         /  \    \
       35    45   65

(a) Preorder: 50 30 20 40 35 45 70 60 65 80. Inorder: 20 30 35 40 45 50 60 65 70 80 (সাজানো — চেক ✓). Postorder: 20 35 45 40 30 65 60 80 70 50. Level order: 50 30 70 20 40 60 80 35 45 65. উচ্চতা 3 (যেমন 50 → 30 → 40 → 35).

(b) 30-এর দুই সন্তান; successor = ডান subtree-র সবচেয়ে ছোট = 35 (leaf). 35 ওপরে আসে: preorder 50 35 20 40 45 70 60 65 80. এবার 50: successor = 60 (70-এর বাঁয়ে, নিজের বাঁ সন্তান নেই)। 60 root-এ কপি, আর পুরোনো 60-এর জায়গায় তার একমাত্র সন্তান 65. শেষ preorder: 60 35 20 40 45 70 65 80. Predecessor নিয়ম নিলে অন্য উত্তর আসত — প্রশ্নে নিয়ম না থাকলে নিজে লিখে দিন।

ট-১৫ (মাঝারি) — একটি binary tree-র (আলাদা key) preorder 8 4 2 6 5 12 10 14 আর inorder 2 4 5 6 8 10 12 14. Postorder কী?

ট-১৫উত্তর

2 5 6 4 10 14 12 8. Preorder-এর প্রথমটি root (8); inorder-এ 8-এর বাঁয়ে {2, 4, 5, 6} বাঁ subtree, ডানে {10, 12, 14}. বাঁ অংশের preorder 4 2 6 5 → root 4, বাঁয়ে 2, ডানে {5, 6} যার root 6 আর 5 তার বাঁ সন্তান। ডান অংশ 12 (10, 14). শুধু preorder আর postorder দিলে tree অনন্য হয় না (যেমন একটি সন্তান বাঁয়ে না ডানে, বোঝা যায় না) — inorder লাগে।

ট-১৬ (কঠিন) — খালি AVL tree-তে insert: (a) 10, 20, 30, 40, 50, 25; (b) 40, 20, 10, 25, 30, 22, 50. প্রতিটি rotation কোথায়, কোন ধরনের (LL, RR, LR, RL)? শেষ tree-র preorder?

ট-১৬উত্তর

(a) 30 ঢোকার পর 10-এ RR (বাঁয়ে ঘোরানো) → 20(10, 30). 50 ঢোকার পর 30-এ RR → 20(10, 40(30, 50)). 25 যায় 20 → 40 → 30-এর বাঁয়ে; এখন 20-এর balance factor −2, আর ডান সন্তান 40-এর +1 — RL: আগে 40-এ ডানে ঘোরানো, তারপর 20-এ বাঁয়ে। শেষ preorder 30 20 10 25 40 50.

        30
       /  \
     20    40
    /  \     \
  10    25    50

(b) 10-এর পর 40-এ LL → 20(10, 40). 30-এর পর 40-এ LR (25 বাঁ সন্তান, তার ডানে 30) → 20(10, 30(25, 40)). 22-এর পর 20-এ RL → 25(20(10, 22), 30(—, 40)). 50-এর পর 30-এ RR → 40(30, 50). শেষ preorder 25 20 10 22 40 30 50. নিয়ম: নতুন node থেকে ওপরে উঠে প্রথম ভারসাম্যহীন node-এ rotation, আর সেখান থেকে নতুন node কোন দিকে-কোন দিকে গেছে, তা-ই নাম (পর্ব ২৮)।

ট-১৭ (মাঝারি) — m = 11 ঘরের hash table, h(k) = k mod 11, linear probing (ভরা হলে পরের ঘর, শেষে গেলে 0-তে ফেরা)। 25, 36, 14, 47, 3, 58, 9, 20 insert করুন। (a) শেষ table? (b) প্রতিটির জন্য কয়টি ঘর দেখতে হলো (probe), মোট কত? (c) 69 খুঁজলে (নেই) কয়টি probe?

ট-১৭উত্তর

25, 36, 14, 47, 3, 58 — ছয়টিরই h = 3! তারা 3, 4, 5, 6, 7, 8 দখল করে (probe ১, ২, ৩, ৪, ৫, ৬)। 9 → ঘর 9 (১ probe), 20 → h = 9, ভরা, ঘর 10 (২ probe).

index: 0  1  2  3   4   5   6   7  8   9  10
key  : -  -  -  25  36  14  47  3  58  9  20

(b) মোট probe ১ + ২ + ৩ + ৪ + ৫ + ৬ + ১ + ২ = ২৪. (c) 69 mod 11 = 3; ঘর 3 থেকে 10 পর্যন্ত আটটি ভরা, তারপর ঘর 0 খালি — ৯টি probe. এটিই primary clustering: একটি লম্বা ভরা ব্লক, যার যেকোনো জায়গায় পড়া key-কে পুরো ব্লক হাঁটতে হয়।

ট-১৮ (কঠিন) — 0..7 আটটি উপাদানের DSU, union by rank আর recursive path compression. union(a, b): ra = find(a), rb = find(b); rank কম যার, সে অন্যটির নিচে; সমান হলে rb যায় ra-র নিচে আর ra-র rank এক বাড়ে। ক্রম: union(0,1), union(2,3), union(4,5), union(6,7), union(1,3), union(5,7), union(3,7). শেষে parent আর rank অ্যারে? তারপর find(7) চালালে parent কী বদলায়?

ট-১৮উত্তর

প্রথম চারটি union-এ চারটি জোড়া: 1 → 0, 3 → 2, 5 → 4, 7 → 6, প্রতিটি root-এর rank 1. union(1,3): root 0 আর 2, rank সমান → 2 → 0, rank[0] = 2. union(5,7): 6 → 4, rank[4] = 2. union(3,7): find(3) হাঁটে 3 → 2 → 0 আর compress করে p[3] = 0; find(7) হাঁটে 7 → 6 → 4, p[7] = 4. rank সমান (2, 2) → 4 → 0, rank[0] = 3.

i     : 0  1  2  3  4  5  6  7
parent: 0  0  0  0  0  4  4  4
rank  : 3  0  1  0  2  0  1  0

find(7): 7 → 4 → 0; compression-এ p[7] = 0 (p[4] আগেই 0). অন্য কারও parent বদলায় না — compression শুধু হাঁটা পথের node ছোঁয়। লক্ষ করুন, rank[2] = 1 রয়ে গেছে যদিও 2 এখন root নয়; compression rank কমায় না।

ট-১৯ (মাঝারি) — a = [2, 5, 1, 4, 9, 3] (০-based) থেকে sum segment tree (node 1 = root, সন্তান 2v আর 2v + 1, mid = (l + r)/2). (a) t[1..13] লিখুন (খালি node বাদ)। (b) a[2] = 6 করলে কোন node-গুলো বদলায়, নতুন মান? (c) তারপর sum(1, 4) কোন node-গুলো থেকে আসে?

ট-১৯উত্তর
node : 1[0,5]=24  2[0,2]=8  3[3,5]=16  4[0,1]=7  5[2,2]=1  6[3,4]=13
       7[5,5]=3   8[0,0]=2  9[1,1]=5   12[3,3]=4  13[4,4]=9   (10, 11 unused)

(b) leaf থেকে root পর্যন্ত একটি পথ: t[5] = 6, t[2] = 13, t[1] = 29 — log n-এর মতো তিনটি node. (c) [1, 4] ভাঙে node 9 ([1,1] = 5), node 5 ([2,2] = 6), node 6 ([3,4] = 13) — যোগফল 24. n ২-এর ঘাত না হলে কিছু index (এখানে 10, 11) ফাঁকা থাকে — তাই অ্যারের আকার 4n নেওয়া হয় (পর্ব ৩৪)।

ট-২০ (মাঝারি) — a = [3, 2, −1, 6, 5, 4, −3, 3] (১-based) থেকে Fenwick tree. (a) t[1..8]? (b) add(3, +2) কোন ঘরগুলো ছোঁয়, নতুন t? (c) তারপর prefix(7) কোন ঘর পড়ে, মান কত?

ট-২০উত্তর

(a) t[i] = a[i − lowbit(i) + 1..i]-এর যোগফল: [3, 5, −1, 10, 5, 9, −3, 19] (যেমন t[4] = 3 + 2 − 1 + 6 = 10, t[6] = 5 + 4 = 9, t[8] = সব = 19). (b) i = 3 → 4 → 8 (প্রতিবার lowbit যোগ): t = [3, 5, 1, 12, 5, 9, −3, 21]. (c) 7 → 6 → 4 → 0 (প্রতিবার lowbit বাদ): t[7] + t[6] + t[4] = −3 + 9 + 12 = 18. চেক: নতুন a[1..7] = 3 + 2 + 1 + 6 + 5 + 4 − 3 = 18 ✓.

৭ · Trace ড্রিল ৩ — গ্রাফ: BFS, DFS, Dijkstra, topological sort, MST আর Floyd–Warshall

গ্রাফের trace-এর প্রথম নিয়ম: প্রতিবেশীদের ক্রম ঠিক করুন। একই গ্রাফে প্রতিবেশী ছোট থেকে বড় ক্রমে না দেখে বড় থেকে ছোট দেখলে BFS আর DFS-এর ক্রম বদলে যায় — দুটিই “সঠিক”, কিন্তু পরীক্ষকের উত্তরের সাথে মিলবে না। প্রশ্নে ক্রম বলা না থাকলে উত্তরের শুরুতে লিখুন: “প্রতিবেশী ছোট থেকে বড় ক্রমে ধরা হলো।”

ট-২১ (মাঝারি) — Undirected গ্রাফ, vertex 0..6, edge: 0–1, 0–2, 1–3, 2–3, 2–4, 3–5, 4–5, 5–6; প্রতিবেশী ছোট থেকে বড়। (a) 0 থেকে BFS: ক্রম, dist আর parent অ্যারে, প্রতিটি vertex বের করার পর কিউ। (b) 0 থেকে recursive DFS: discovery ক্রম, finish ক্রম, discovery/finish time (১ থেকে গুনে)। (c) একটি stack নিয়ে “vertex তুলে, না-দেখা সব প্রতিবেশী ছোট থেকে বড় ক্রমে push” করলে ক্রম কী?

ট-২১উত্তর

(a) ক্রম 0 1 2 3 4 5 6. কিউ: 0 বের করার পর [1, 2]; 1 → [2, 3]; 2 → [3, 4]; 3 → [4, 5]; 4 → [5] (5 আগেই দেখা); 5 → [6]; 6 → []. dist = [0, 1, 1, 2, 2, 3, 4], parent = [−, 0, 0, 1, 2, 3, 5]. 3-এর parent 1, 2 নয় — কারণ 1 আগে কিউ থেকে বের হয়ে 3-কে আবিষ্কার করে।

(b) discovery: 0 1 3 2 4 5 6; finish: 6 5 4 2 3 1 0. সময় (d/f): 0: 1/14, 1: 2/13, 2: 4/11, 3: 3/12, 4: 5/10, 5: 6/9, 6: 7/8. চেক: প্রতিটি vertex-এর [d, f] হয় অন্যটির ভেতরে, নয়তো আলাদা (parenthesis theorem, পর্ব ৪১)।

(c) 0 2 4 5 6 3 1 — recursive DFS-এর মতো নয়! stack-এর ওপরে থাকে শেষে push করা, অর্থাৎ সবচেয়ে বড় প্রতিবেশী। recursive DFS-এর ক্রম পেতে প্রতিবেশী উল্টো ক্রমে push করুন। পরীক্ষায় “DFS” বললে সাধারণত recursive রূপ বোঝায়; stack রূপে লিখলে নিয়মটি স্পষ্ট লিখুন।

ট-২২ (মাঝারি) — Directed weighted গ্রাফ, vertex 0..5, edge (u → v, ওজন): 0→1 (7), 0→2 (2), 2→1 (3), 1→3 (1), 2→3 (8), 2→4 (5), 4→3 (2), 3→5 (4), 4→5 (6). উৎস 0 থেকে Dijkstra (binary heap, lazy deletion: পুরোনো এন্ট্রি বের হলে বাতিল)। কোন ক্রমে vertex চূড়ান্ত (settled) হয়, প্রতিটি ধাপের পর dist আর heap কী? নিচের stepper-এ পুরো টেবিল — আগে নিজে করুন।

প্রতিটি সারি একটি heap থেকে বের করা
ধাপ
settled
dist[0..5]
parent[0..5]
heap-এ বাকি
  1. শুরু. dist[0] = 0, বাকি সব ∞; heap-এ শুধু (0, 0). টেবিলের কলাম আগেই ঠিক করুন: settled, dist, parent, heap.
  2. 0 settled. 0→1: 0 + 7 = 7, 0→2: 0 + 2 = 2 — দুটিই ∞-এর চেয়ে ভালো, তাই আপডেট আর heap-এ push.
  3. 2 settled (dist 2). 2→1: 2 + 3 = 5 < 7 — উন্নতি! dist[1] = 5, parent 2. পুরোনো (7, 1) heap-এ রয়ে গেল। 2→3: 10, 2→4: 7.
  4. 1 settled (dist 5). 1→3: 5 + 1 = 6 < 10 — আবার উন্নতি, parent[3] = 1. heap-এ এখন 3-এর দুটি এন্ট্রি: (6, 3) আর পুরোনো (10, 3).
  5. 3 settled (dist 6). 3→5: 6 + 4 = 10. লক্ষ করুন, 4 (dist 7)-এর আগে 3 (dist 6) বের হলো — heap সবসময় সবচেয়ে ছোট dist দেয়।
  6. পুরোনো এন্ট্রি বাতিল, 4 settled. (7, 1) বের হয়, কিন্তু 1 আগেই settled (7 > dist[1] = 5) — বাতিল। তারপর (7, 4): 4→3 = 9 > 6, 4→5 = 13 > 10 — কোনো উন্নতি নেই।
  7. শেষ. (10, 3) বাতিল, (10, 5) settled. চূড়ান্ত dist = [0, 5, 2, 6, 7, 10], settle ক্রম 0, 2, 1, 3, 4, 5. 5-এর পথ parent ধরে উল্টো দিকে: 5 ← 3 ← 1 ← 2 ← 0, দৈর্ঘ্য 2 + 3 + 1 + 4 = 10 ✓.
ট-২২উত্তর সংক্ষেপে আর একটি প্রশ্ন: 2→3-এর ওজন 8 থেকে −3 হলে?

Settle ক্রম 0, 2, 1, 3, 4, 5; dist [0, 5, 2, 6, 7, 10]. ওজন −3 হলে আসল সবচেয়ে ছোট দূরত্ব dist[3] = 2 + (−3) = −1, dist[5] = 3. এই গ্রাফে Dijkstra-ও (lazy রূপে) ঠিক উত্তর দেয়, কারণ 2 settle হওয়ার সময়েই 3 আপডেট হয়ে যায় — কিন্তু এটা কাকতালীয়। Negative edge থাকলে “বের হওয়া মানেই চূড়ান্ত” invariant ভেঙে যায়, আর অন্য গ্রাফে ভুল আসে; তখন নিয়ম হলো Bellman-Ford (পর্ব ৪৪)। পরীক্ষায় “Dijkstra negative edge-এ কেন ব্যর্থ” প্রশ্নে এমন একটি ছোট পাল্টা-উদাহরণ নিজে বানিয়ে দেখানোই সেরা উত্তর।

ট-২৩ (মাঝারি) — DAG, vertex 0..6, edge: 0→3, 1→3, 1→4, 2→4, 3→5, 4→5, 2→6, 6→5, 4→0. Kahn-এর অ্যালগরিদম, কিউর বদলে min-heap (প্রস্তুতদের মধ্যে সবচেয়ে ছোট নম্বর আগে)। ক্রম কী?

ট-২৩উত্তর

In-degree: 0:1, 1:0, 2:0, 3:2, 4:2, 5:3, 6:1. প্রস্তুত {1, 2}. বের 1 → 3:1, 4:1 → {2}. বের 2 → 4:0, 6:0 → {4, 6}. বের 4 → 5:2, 0:0 → {0, 6}. বের 0 → 3:0 → {3, 6}. বের 3 → 5:1 → {6}. বের 6 → 5:0 → {5}. বের 5. ক্রম 1 2 4 0 3 6 5 — এটি lexicographically সবচেয়ে ছোট topological order. চেক: প্রতিটি edge u→v-তে u আগে ✓ (4→0-তে 4 আগে). সাধারণ কিউ দিলে ক্রম 1 2 4 6 0 3 5 হতো।

ট-২৪ (মাঝারি) — Undirected weighted গ্রাফ, vertex 0..5, edge (ওজন): 0–1 (4), 0–2 (3), 1–2 (1), 1–3 (2), 2–3 (6), 3–4 (5), 2–4 (9), 4–5 (7), 3–5 (8). Kruskal: কোন edge কোন ক্রমে নেওয়া বা বাদ দেওয়া হয়, MST-র ওজন কত?

ট-২৪উত্তর
1-2 (1) take     1-3 (2) take     0-2 (3) take
0-1 (4) skip: 0 and 1 already joined (cycle 0-2-1)
3-4 (5) take     2-3 (6) skip     4-5 (7) take   -> 5 edges, stop
3-5 (8), 2-4 (9): not needed

MST-র ওজন 1 + 2 + 3 + 5 + 7 = 18. V − 1 = ৫টি edge হলেই থামা যায়। সব ওজন আলাদা বলে MST অনন্য; সমান ওজন থাকলে একাধিক সঠিক MST হতে পারে, কিন্তু মোট ওজন একই (পর্ব ৪৫)।

ট-২৫ (কঠিন) — Directed গ্রাফ, vertex 1..4 (১-based), প্রাথমিক দূরত্বের matrix (∞ = edge নেই):

      1   2   3   4
 1    0   3   ∞   7
 2    8   0   2   ∞
 3    5   ∞   0   1
 4    2   ∞   ∞   0

Floyd–Warshall-এ k = 1 (vertex 1-কে মধ্যবর্তী হিসেবে অনুমতি) শেষে matrix কী? শেষ (k = 4) matrix কী?

ট-২৫উত্তর

k = 1-এ শুধু সারি 1 আর কলাম 1 দিয়ে ছোট পথ: d[i][j] = min(d[i][j], d[i][1] + d[1][j]). বদলায় তিনটি ঘর: d[2][4] = 8 + 7 = 15, d[3][2] = 5 + 3 = 8, d[4][2] = 2 + 3 = 5.

after k = 1           final (k = 4)
   0   3   ∞   7         0   3   5   6
   8   0   2  15         5   0   2   3
   5   8   0   1         3   6   0   1
   2   5   ∞   0         2   5   7   0

কৌশল: k-তম ধাপে সারি k আর কলাম k কখনো বদলায় না (d[k][k] = 0), তাই খাতায় সেগুলো আগে কপি করে বাকি ঘরগুলো মেলান। d[4][3] k = 1-এ ∞ থাকে (d[1][3] = ∞), k = 2-এ 4 → 1 → 2 → 3 = 7 হয়।

নিজের trace মিলিয়ে নিন — প্রোগ্রাম ৩

ট-২১ থেকে ট-২৪-এর পাঁচটি অ্যালগরিদম একই প্রোগ্রামে, প্রশ্নের নিয়মে: প্রতিবেশী সাজানো, Dijkstra-য় lazy deletion, Kahn-এ min-heap, Kruskal-এ DSU.

#include <cstdio>
#include <vector>
#include <queue>
#include <tuple>
#include <algorithm>
#include <functional>
using namespace std;
using G = vector<vector<int>>;
G g(7); vector<int> seen(7);
void dfs(int u) { seen[u] = 1; printf(" %d", u); for (int v : g[u]) if (!seen[v]) dfs(v); }
int main() {
    for (auto [a, b] : vector<pair<int, int>>{{0,1},{0,2},{1,3},{2,3},{2,4},{3,5},{4,5},{5,6}}) { g[a].push_back(b); g[b].push_back(a); }
    for (auto& adj : g) sort(adj.begin(), adj.end());                 // smaller neighbour first
    vector<int> d(7, -1); queue<int> q; q.push(0); d[0] = 0; printf("BFS order:");
    while (!q.empty()) { int u = q.front(); q.pop(); printf(" %d", u); for (int v : g[u]) if (d[v] < 0) { d[v] = d[u] + 1; q.push(v); } }
    printf("\nBFS dist: "); for (int x : d) printf(" %d", x);
    printf("\nDFS order:"); dfs(0);
    vector<vector<pair<int, int>>> w(6);                               // Dijkstra, directed
    for (auto [a, b, c] : vector<tuple<int, int, int>>{{0,1,7},{0,2,2},{2,1,3},{1,3,1},{2,3,8},{2,4,5},{4,3,2},{3,5,4},{4,5,6}}) w[a].push_back({b, c});
    vector<int> dist(6, 1e9); priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq;
    dist[0] = 0; pq.push({0, 0}); printf("\nDijkstra settles:");
    while (!pq.empty()) {
        auto [du, u] = pq.top(); pq.pop(); if (du > dist[u]) continue;  // stale entry
        printf(" %d(%d)", u, du);
        for (auto [v, c] : w[u]) if (du + c < dist[v]) pq.push({dist[v] = du + c, v});
    }
    G dag(7); vector<int> indeg(7);                                    // Kahn with a min-heap
    for (auto [a, b] : vector<pair<int, int>>{{0,3},{1,3},{1,4},{2,4},{3,5},{4,5},{2,6},{6,5},{4,0}}) { dag[a].push_back(b); ++indeg[b]; }
    priority_queue<int, vector<int>, greater<>> ready; for (int v = 0; v < 7; ++v) if (!indeg[v]) ready.push(v);
    printf("\nKahn order:");
    while (!ready.empty()) { int u = ready.top(); ready.pop(); printf(" %d", u); for (int v : dag[u]) if (--indeg[v] == 0) ready.push(v); }
    vector<tuple<int, int, int>> e = {{4,0,1},{3,0,2},{1,1,2},{2,1,3},{6,2,3},{5,3,4},{9,2,4},{7,4,5},{8,3,5}};
    sort(e.begin(), e.end()); vector<int> par(6); for (int v = 0; v < 6; ++v) par[v] = v;
    function<int(int)> find = [&](int x) { return par[x] == x ? x : par[x] = find(par[x]); };
    int total = 0; printf("\nKruskal takes:");
    for (auto [c, a, b] : e) if (find(a) != find(b)) { par[find(a)] = find(b); total += c; printf(" %d-%d(%d)", a, b, c); }
    printf("  total %d\n", total);
}

আউটপুট:

BFS order: 0 1 2 3 4 5 6
BFS dist:  0 1 1 2 2 3 4
DFS order: 0 1 3 2 4 5 6
Dijkstra settles: 0(0) 2(2) 1(5) 3(6) 4(7) 5(10)
Kahn order: 1 2 4 0 3 6 5
Kruskal takes: 1-2(1) 1-3(2) 0-2(3) 3-4(5) 4-5(7)  total 18

প্রতিটি লাইন ট-২১ (a, b), ট-২২, ট-২৩ আর ট-২৪-এর উত্তর। sort(adj…) লাইনটি মুছে দিন, আর edge-গুলো উল্টো ক্রমে দিন — BFS আর DFS-এর ক্রম বদলে যাবে, অথচ dist একই থাকবে। এটাই প্রমাণ যে ক্রম একটি “নিয়ম”, অ্যালগরিদমের সত্য নয়; দূরত্ব হলো সত্য। Dijkstra-র if (du > dist[u]) continue; লাইনটিই stepper-এর “বাতিল” ধাপ। Kruskal-এর edge {ওজন, u, v} আকারে রাখা হয়েছে, যাতে sort সরাসরি ওজন দিয়ে সাজায়।

৮ · Trace ড্রিল ৪ — greedy, DP-র টেবিল, string আর number theory

DP-র trace প্রশ্নে পুরো টেবিল ভরতে হয়, আর এক ঘরের ভুল পরের সব ঘরে ছড়ায়। তাই টেবিল আঁকার আগে তিনটি জিনিস লিখে নিন: ঘরের মানে (যেমন “L[i][j] = X-এর প্রথম i আর Y-এর প্রথম j অক্ষরের LCS”), base case (সারি ০, কলাম ০), আর সূত্র. তারপর সারি ধরে বাঁ থেকে ডানে ভরুন।

ট-২৬ (মাঝারি) — অক্ষরের frequency A: 7, B: 3, C: 2, D: 5, E: 9. Huffman-এর merge-এর ক্রম, প্রতিটি অক্ষরের code-এর দৈর্ঘ্য আর মোট bit?

ট-২৬উত্তর

সবসময় সবচেয়ে ছোট দুটি জোড়া: 2 + 3 = 5 (C, B); 5 + 5 = 10 (CB, D); 7 + 9 = 16 (A, E); 10 + 16 = 26 (root). দৈর্ঘ্য: A, D, E = ২ bit; B, C = ৩ bit. মোট bit = 7·2 + 9·2 + 5·2 + 3·3 + 2·3 = ৫৭ — আর এটি merge-গুলোর যোগফলের সমান: 5 + 10 + 16 + 26 = 57 ✓ (সহজ চেক)। Tie-এর কারণে আসল 0/1 code আলাদা হতে পারে, কিন্তু মোট bit সবসময় একই।

ট-২৭ (সহজ) — Coin {1, 3, 4}, মোট 6. ন্যূনতম coin-এর DP অ্যারে dp[0..6] লিখুন। Greedy (সবচেয়ে বড় coin আগে) কত coin দেয়?

ট-২৭উত্তর

dp = [0, 1, 2, 1, 1, 2, 2] (dp[x] = 1 + min dp[x − c]). উত্তর ২টি coin (3 + 3). Greedy: 4 + 1 + 1 = ৩টি — ভুল। এই coin system “canonical” নয়, তাই greedy খাটে না (পর্ব ৫০, ৫৩)। এই ছোট পাল্টা-উদাহরণটি মনে রাখুন।

ট-২৮ (মাঝারি) — [3, 10, 2, 1, 20, 4, 6, 7]-এ O(n log n) LIS (tails অ্যারে; প্রতিটি x-এর জন্য প্রথম ≥ x ঘরটি বদলান, না থাকলে শেষে যোগ)। প্রতিটি ধাপের পর tails? LIS-এর দৈর্ঘ্য?

ট-২৮উত্তর

[3] → [3, 10] → [2, 10] → [1, 10] → [1, 10, 20] → [1, 4, 20] → [1, 4, 6] → [1, 4, 6, 7]. দৈর্ঘ্য ৪ (যেমন 3, 4, 6, 7). সাবধান: tails নিজে সাধারণত একটি আসল subsequence নয় — এখানে কাকতালীয়ভাবে 1, 4, 6, 7 বৈধ, কিন্তু মাঝপথের [1, 10] বৈধ নয় (1 আসে 10-এর পরে)। tails[k] মানে “দৈর্ঘ্য k + 1-এর বাড়ন্ত subsequence-এর সবচেয়ে ছোট সম্ভাব্য শেষ”.

ট-২৯ (মাঝারি) — 0/1 knapsack, ধারণক্ষমতা W = 7; জিনিস (ওজন, মূল্য): (1, 1), (3, 4), (4, 5), (5, 7). এক-মাত্রার dp[0..7] (capacity উল্টো দিকে ঘুরিয়ে) — প্রতিটি জিনিসের পর অ্যারে? সর্বোচ্চ মূল্য আর কোন জিনিসগুলো?

ট-২৯উত্তর
capacity       0  1  2  3  4  5  6  7
after (1,1)    0  1  1  1  1  1  1  1
after (3,4)    0  1  1  4  5  5  5  5
after (4,5)    0  1  1  4  5  6  6  9
after (5,7)    0  1  1  4  5  7  8  9

উত্তর 9 = (3, 4) + (4, 5), ওজন ঠিক 7. লোভে সবচেয়ে মূল্যবান (5, 7) নিলে বাকি 2-এ শুধু (1, 1) — মোট 8. Capacity উল্টো দিকে (7 থেকে নিচে) ঘোরানোর কারণ: সামনে ঘোরালে একই জিনিস একাধিকবার নেওয়া হয়ে যায় (তখন সেটি unbounded knapsack).

ট-৩০ (মাঝারি) — X = ABCBDA, Y = BDCABA. LCS-এর পুরো টেবিল L[0..6][0..6] ভরুন; দৈর্ঘ্য কত? পেছনে হেঁটে (মিললে কোনাকুনি, না মিললে ওপর ≥ বাঁ হলে ওপরে) একটি LCS বের করুন।

ট-৩০উত্তর
        -  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

দৈর্ঘ্য ৪, LCS BCBA. এই জোড়ায় দৈর্ঘ্য-৪-এর common subsequence একটিই (সব পথ খুঁজে যাচাই করা), তাই tie-নিয়ম বদলালেও একই উত্তর। অনেক ইনপুটে তা হয় না: X = ABC, Y = ACB-তে “ওপর আগে” নিয়ম দেয় AB, “বাঁ আগে” দেয় AC — দুটিই সঠিক, তাই উত্তরে নিজের tie-নিয়ম লিখুন। চেক: প্রতিটি সারি বাঁ থেকে ডানে কখনো কমে না, আর পাশের ঘরের পার্থক্য সর্বোচ্চ ১.

ট-৩১ (মাঝারি) — Edit distance (insert, delete, replace — প্রতিটির খরচ 1): DHAKA থেকে DACCA (ঢাকার পুরোনো ইংরেজি বানান). টেবিল আর উত্তর?

ট-৩১উত্তর
        -  D  A  C  C  A
   -    0  1  2  3  4  5
   D    1  0  1  2  3  4
   H    2  1  1  2  3  4
   A    3  2  1  2  3  3
   K    4  3  2  2  3  4
   A    5  4  3  3  3  3

উত্তর ৩: D ঠিক, H → A, A → C, K → C, শেষ A ঠিক — তিনটি replace. সূত্র: D[i][j] = min(D[i−1][j] + 1, D[i][j−1] + 1, D[i−1][j−1] + (অক্ষর আলাদা হলে 1, নইলে 0)). প্রথম সারি আর কলাম 0, 1, 2, … — খালি string থেকে বানাতে সব insert.

ট-৩২ (কঠিন) — চারটি matrix A1…A4-এর মাপ 5×10, 10×3, 3×12, 12×5 (d = [5, 10, 3, 12, 5]). Matrix chain-এর m[i][j] টেবিল আর সবচেয়ে কম scalar গুণ, বন্ধনীসহ?

ট-৩২উত্তর

দৈর্ঘ্য ২: m[1][2] = 5·10·3 = 150, m[2][3] = 10·3·12 = 360, m[3][4] = 3·12·5 = 180. দৈর্ঘ্য ৩: m[1][3] = min(0 + 360 + 600, 150 + 0 + 180) = 330; m[2][4] = min(0 + 180 + 150, 360 + 0 + 600) = 330. দৈর্ঘ্য ৪: m[1][4] = min(k=1: 330 + 250 = 580, k=2: 150 + 180 + 75 = 405, k=3: 330 + 300 = 630) = 405, বন্ধনী (A1 A2)(A3 A4). টেবিল দৈর্ঘ্য ধরে ভরতে হয় (কোনাকুনি), সারি ধরে নয় — প্রচলিত ভুল।

ট-৩৩ (মাঝারি) — KMP: (a) P = ABABCABAB-এর prefix function π (০-based) লিখুন। (b) T = ABABABCABAB-এ P′ = ABAB কোন কোন index-এ মেলে (overlap সহ)?

ট-৩৩উত্তর

(a) π = [0, 0, 1, 2, 0, 1, 2, 3, 4]. i = 4-এ C আগের কোনো border বাড়াতে পারে না — π নেমে 0; তারপর ABAB আবার গড়ে ওঠে, শেষে π[8] = 4 (ABAB হলো prefix আর suffix দুটোই)। (b) index 0, 2, 7. একটি মিলের পর k = π[k − 1] = 2 থেকে চলতে থাকে, তাই index 2-এর overlap-করা মিলও ধরা পড়ে।

ট-৩৪ (মাঝারি) — Extended Euclid: gcd(252, 198) আর x, y যাতে 252x + 198y = gcd.

ট-৩৪উত্তর

নিচে নামা: (252, 198) q = 1 → (198, 54) q = 3 → (54, 36) q = 1 → (36, 18) q = 2 → (18, 0): gcd = 18. ওপরে ওঠা (x, y): (18, 0): (1, 0); (36, 18): (0, 1); (54, 36): (1, −1); (198, 54): (−1, 4); (252, 198): (4, −5). নিয়ম: নিচের (x′, y′) থেকে x = y′, y = x′ − q·y′. চেক: 252·4 − 198·5 = 1008 − 990 = 18 ✓.

ট-৩৫ (সহজ) — 30 পর্যন্ত sieve of Eratosthenes (প্রতিটি মৌলিক p-এর জন্য p² থেকে কাটা শুরু)। কোন p-এর জন্য নতুন কোন সংখ্যাগুলো কাটা পড়ে? কোথায় থামবেন? মৌলিকগুলো কী?

ট-৩৫উত্তর

p = 2: 4, 6, 8, …, 30 (১৪টি). p = 3: শুরু 9 থেকে; নতুন কাটা 9, 15, 21, 27 (12, 18, 24, 30 আগেই কাটা). p = 5: শুরু 25; নতুন শুধু 25 (30 আগেই). p = 7: 49 > 30 — থামুন (√30 < 6). মৌলিক: 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 — ১০টি।

নিজের trace মিলিয়ে নিন — প্রোগ্রাম ৪

ট-২৯ থেকে ট-৩৩-এর পাঁচটি টেবিল-প্রশ্ন একসাথে: LCS (দৈর্ঘ্য আর পেছনে হেঁটে পাওয়া LCS), knapsack-এর প্রতিটি সারি, edit distance, π অ্যারে আর matrix chain. C++ আর Python দুটিতেই একই আউটপুট।

#include <cstdio>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
using VV = vector<vector<int>>;
int main() {
    string x = "ABCBDA", y = "BDCABA"; int m = x.size(), n = y.size();
    VV L(m + 1, vector<int>(n + 1));                                   // LCS table
    for (int i = 1; i <= m; ++i) for (int j = 1; j <= n; ++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 s; for (int i = m, j = n; i && j;)                          // walk back: up wins ties
        if (x[i - 1] == y[j - 1]) s = x[--i] + s, --j; else if (L[i - 1][j] >= L[i][j - 1]) --i; else --j;
    printf("LCS length %d, one LCS %s\n", L[m][n], s.c_str());
    vector<int> best(8);                                               // 0/1 knapsack, W = 7
    for (auto [w, v] : vector<pair<int, int>>{{1, 1}, {3, 4}, {4, 5}, {5, 7}}) {
        for (int c = 7; c >= w; --c) best[c] = max(best[c], best[c - w] + v);
        printf("knapsack after (w=%d,v=%d):", w, v); for (int b : best) printf(" %d", b); printf("\n");
    }
    string a = "DHAKA", b = "DACCA"; VV D(6, vector<int>(6));          // edit distance
    for (int i = 0; i <= 5; ++i) for (int j = 0; j <= 5; ++j)
        D[i][j] = !i ? j : !j ? i : min({D[i - 1][j] + 1, D[i][j - 1] + 1, D[i - 1][j - 1] + (a[i - 1] != b[j - 1])});
    printf("edit distance %s -> %s = %d\n", a.c_str(), b.c_str(), D[5][5]);
    string p = "ABABCABAB"; vector<int> pi(p.size());                  // KMP prefix function
    for (int i = 1, k = 0; i < (int)p.size(); pi[i++] = k) {
        while (k && p[i] != p[k]) k = pi[k - 1];
        if (p[i] == p[k]) ++k;
    }
    printf("pi(%s):", p.c_str()); for (int v : pi) printf(" %d", v);
    vector<int> d = {5, 10, 3, 12, 5}; int k = d.size() - 1; VV C(k, vector<int>(k));   // matrix chain
    for (int len = 2; len <= k; ++len) for (int i = 0, j = len - 1; j < k; ++i, ++j) {
        C[i][j] = 1 << 30;
        for (int t = i; t < j; ++t) C[i][j] = min(C[i][j], C[i][t] + C[t + 1][j] + d[i] * d[t + 1] * d[j + 1]);
    }
    printf("\nmatrix chain 5x10x3x12x5: %d multiplications\n", C[0][k - 1]);
}

আউটপুট:

LCS length 4, one LCS BCBA
knapsack after (w=1,v=1): 0 1 1 1 1 1 1 1
knapsack after (w=3,v=4): 0 1 1 4 5 5 5 5
knapsack after (w=4,v=5): 0 1 1 4 5 6 6 9
knapsack after (w=5,v=7): 0 1 1 4 5 7 8 9
edit distance DHAKA -> DACCA = 3
pi(ABABCABAB): 0 0 1 2 0 1 2 3 4
matrix chain 5x10x3x12x5: 405 multiplications

ট-২৯ থেকে ট-৩৩-এর প্রতিটি উত্তর মেলে। LCS-এর পেছনে হাঁটার লাইনে >= বদলে > করুন (বাঁ আগে), আর x, y বদলে ABC ও ACB দিন — দুই নিয়মে AB আর AC আসবে; সমান দৈর্ঘ্যের দুটিই সঠিক। Matrix chain-এর লুপ len (দৈর্ঘ্য) ধরে চলে, তাই C[i][t] আর C[t+1][j] সবসময় আগেই ভরা থাকে। x[--i] + s-এ একটি char আর একটি string জোড়া লাগে — নতুন অক্ষর সামনে বসে, কারণ পেছন থেকে হাঁটছি।

x, y = "ABCBDA", "BDCABA"; m, n = len(x), len(y)
L = [[0] * (n + 1) for _ in range(m + 1)]                 # LCS table
for i in range(1, m + 1):
    for j in range(1, n + 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])
s, i, j = "", m, n                                       # walk back: up wins ties
while i and j:
    if x[i-1] == y[j-1]: s = x[i-1] + s; i -= 1; j -= 1
    elif L[i-1][j] >= L[i][j-1]: i -= 1
    else: j -= 1
print(f"LCS length {L[m][n]}, one LCS {s}")
best = [0] * 8                                           # 0/1 knapsack, W = 7
for w, v in [(1, 1), (3, 4), (4, 5), (5, 7)]:
    for c in range(7, w - 1, -1): best[c] = max(best[c], best[c - w] + v)
    print(f"knapsack after (w={w},v={v}):", *best)
a, b = "DHAKA", "DACCA"                                  # edit distance
D = [[i + j if not i or not j else 0 for j in range(6)] for i in range(6)]
for i in range(1, 6):
    for j in range(1, 6):
        D[i][j] = min(D[i-1][j] + 1, D[i][j-1] + 1, D[i-1][j-1] + (a[i-1] != b[j-1]))
print(f"edit distance {a} -> {b} = {D[5][5]}")
p = "ABABCABAB"; pi = [0] * len(p); k = 0                # KMP prefix function
for i in range(1, len(p)):
    while k and p[i] != p[k]: k = pi[k - 1]
    if p[i] == p[k]: k += 1
    pi[i] = k
print(f"pi({p}):", *pi)
d = [5, 10, 3, 12, 5]; k = len(d) - 1; C = [[0] * k for _ in range(k)]   # matrix chain
for ln in range(2, k + 1):
    for i in range(k - ln + 1):
        j = i + ln - 1
        C[i][j] = min(C[i][t] + C[t+1][j] + d[i] * d[t+1] * d[j+1] for t in range(i, j))
print(f"matrix chain 5x10x3x12x5: {C[0][k-1]} multiplications")

আউটপুট C++-এর সাথে হুবহু এক:

LCS length 4, one LCS BCBA
knapsack after (w=1,v=1): 0 1 1 1 1 1 1 1
knapsack after (w=3,v=4): 0 1 1 4 5 5 5 5
knapsack after (w=4,v=5): 0 1 1 4 5 6 6 9
knapsack after (w=5,v=7): 0 1 1 4 5 7 8 9
edit distance DHAKA -> DACCA = 3
pi(ABABCABAB): 0 0 1 2 0 1 2 3 4
matrix chain 5x10x3x12x5: 405 multiplications

Edit distance-এর টেবিল এক লাইনে শুরু করা হয়েছে: প্রথম সারি বা কলামে (i = 0 বা j = 0) মান i + j, যা 0, 1, 2, … দেয় — বাকি ঘর 0, পরে ভরা হবে। a[i-1] != b[j-1] একটি bool, আর Python-এ True যোগে 1 হিসেবে গোনা হয় — C++-এর মতোই। print(..., *best) list-এর উপাদানগুলো ফাঁকা দিয়ে আলাদা করে ছাপে। Matrix chain-এ min(... for t in range(i, j)) একটি generator — প্রতিটি split t-এর খরচ থেকে সবচেয়ে ছোটটি।

৯ · খাতায় trace প্রশ্নের উত্তর লেখার কৌশল

সঠিক শেষ উত্তর লিখেও নম্বর কাটা যায়, যদি পরীক্ষক বুঝতে না পারেন আপনি সেখানে কীভাবে পৌঁছালেন। উল্টোভাবে, মাঝপথে একটি ভুল হলেও পরিষ্কার trace-এ বাকি ধাপের নম্বর পাওয়া যায়, কারণ পরীক্ষক দেখতে পান আপনার পদ্ধতি ঠিক। বোর্ড পরীক্ষার অঙ্কের মতোই: “ধাপে নম্বর”.

সাতটি নিয়ম

  1. শুরুতে অনুমান লিখুন: “০-based index”, “pivot = শেষ উপাদান (Lomuto)”, “প্রতিবেশী ছোট থেকে বড়”, “সমান হলে বাঁ সন্তান”, “successor দিয়ে delete”. প্রশ্নে বলা না থাকলে এটা লেখাই আপনার রক্ষাকবচ।
  2. প্রতিটি মধ্যবর্তী অবস্থা দেখান: শুধু শেষ অ্যারে নয় — প্রতিটি পাস, প্রতিটি insert, প্রতিটি heap থেকে বের করা। প্রশ্নে “দ্বিতীয় পাসের পর” চাইলে প্রথম পাসও লিখুন; তাতে বোঝা যায় আপনি সত্যিই চালিয়েছেন।
  3. পাস আর ধাপের নাম দিন: “পাস ১ (j = 0..4)”, “k = 2”, “i = 3, key = 2”. নাম ছাড়া সারির কোনো মানে নেই।
  4. বদলানো ঘর চিহ্নিত করুন: swap হওয়া দুটি সংখ্যার নিচে দাগ, নতুন dist গোল করে, rotation-এর pivot node-এ তারা। পরীক্ষক এক নজরে মেলাতে পারেন।
  5. Tree আর গ্রাফ বারবার আঁকুন: প্রতিটি rotation বা delete-এর পর নতুন ছবি। একই ছবিতে কাটাকাটি করলে নিজেই বিভ্রান্ত হবেন।
  6. শেষে একটি চেক লিখুন: “inorder সাজানো ✓”, “shift = inversion = ১১ ✓”, “252 · 4 − 198 · 5 = 18 ✓”. এক লাইনের চেক আত্মবিশ্বাস দেখায় আর নিজের ভুল ধরে।
  7. Complexity-র উত্তরে কারণ এক লাইনে: “ভেতরের লুপ n/i বার, Σ n/i = Θ(n log n)” — শুধু “Θ(n log n)” নয়। ভিন্ন case থাকলে কোনটি বলছেন, লিখুন।
প্রচলিত ভুলঠিক অভ্যাস
০-based প্রশ্নে ১-based উত্তর (heap-এ সন্তান 2i বনাম 2i + 1)প্রশ্নের index পদ্ধতি প্রথম লাইনে লিখুন, সূত্রও
Insertion sort-এর “পাস” আর bubble sort-এর “পাস” এক ভাবা“i = 1 থেকে গুনে k টি insertion” স্পষ্ট লিখুন
Hoare partition-এ pivot-কে চূড়ান্ত জায়গায় ধরাফেরত index j আর দুই ভাগের শর্ত লিখুন
Dijkstra-য় একটি vertex-কে দুবার settle করা“settled” কলাম রাখুন; পুরোনো heap এন্ট্রি বাতিল লিখুন
Postfix-এ a − b উল্টে b − aপ্রথম pop = ডান operand, লিখে রাখুন
DP টেবিলে base case বাদসারি ০ আর কলাম ০ আগে ভরুন
Complexity-তে শুধু লুপ গুনে গুণ করা (log n × n)ভেতরের কাজ বদলালে যোগ করুন
নমুনা উত্তর

প্রশ্ন: [7, 2, 9, 4, 3, 8, 6]-এ quick sort-এর প্রথম partition দেখান।

উত্তর: অনুমান: Lomuto partition, pivot = শেষ উপাদান = 6, ০-based. i = −1. j = 0: 7 > 6, কিছু না। j = 1: 2 ≤ 6, i = 0, swap(a[0], a[1]) → [2, 7, 9, 4, 3, 8, 6]. j = 2: 9, কিছু না। j = 3: 4 ≤ 6, i = 1, swap(a[1], a[3]) → [2, 4, 9, 7, 3, 8, 6]. j = 4: 3 ≤ 6, i = 2, swap(a[2], a[4]) → [2, 4, 3, 7, 9, 8, 6]. j = 5: 8, কিছু না। শেষে swap(a[3], a[6]) → [2, 4, 3, 6, 9, 8, 7], pivot index 3. চেক: বাঁয়ে সব ≤ 6, ডানে সব > 6 ✓. পরের রিকার্শন [0..2] আর [4..6]-এ।

সময়ের হিসাবও মাথায় রাখুন। একটি বড় DP টেবিল (৭ × ৭) বা Floyd–Warshall-এর চারটি matrix ভরতে ১০–১৫ মিনিট লাগতে পারে। পরীক্ষায় প্রথমে ছোট, নিশ্চিত প্রশ্নগুলো শেষ করুন; বড় trace-এ গেলে টেবিলের কাঠামো (সারি, কলাম, base case) আগে এঁকে নিন — সময় ফুরালেও কাঠামো আর প্রথম কয়েকটি সারিতে নম্বর থাকে।

১০ · AI-এর যুগে — complexity আর trace যাচাইয়ের দক্ষতা

AI-কে “এই কোডের time complexity কত?” জিজ্ঞেস করলে উত্তর আসে আত্মবিশ্বাসী গলায় — আর প্রায়ই ছাঁচ দেখে। লুপের ভেতরে লুপ দেখলেই “O(n²)”, ভাগ করে জয় দেখলেই “O(n log n)”. এই পাতার ক-০৬, ক-১০, ক-১৮ আর ক-২০ ঠিক এই ছাঁচ-মেলানো উত্তরের ফাঁদ। Trace-এও একই সমস্যা: AI প্রায়ই নিয়ম মিশিয়ে ফেলে (Hoare আর Lomuto, ০-based আর ১-based, recursive আর stack-DFS) বা মাঝপথে একটি সংখ্যা ভুল করে, আর বাকি সব ভুলের ওপর দাঁড়ায়।

AI সাধারণত যেখানে ভুল করে

AI-এর দাবিআসলেকীভাবে ধরবেন
j += i লুপ “O(n²)”Θ(n log n) (harmonic)ভেতরের লুপ প্রতিটি i-এ কতবার, লিখে যোগ করুন
Two pointer-এর while “O(n²)”Θ(n) (j কখনো পেছায় না)ভেতরের ভ্যারিয়েবল reset হয় কি না দেখুন
Sieve “O(n log n)” বা “O(n√n)”Θ(n log log n)প্রোগ্রাম ১-এর doubling test
2T(n/2) + n log n “case 3 → n log n”Θ(n log² n)polynomially বড় কি না জিজ্ঞেস করুন; গাছ আঁকুন
T(n) = T(n − 1) + n-এ master theorem প্রয়োগখাটে না; Θ(n²)আকার ভাগ হচ্ছে, না বিয়োগ — দেখুন
push_back “O(n) worst, তাই n টিতে O(n²)”মোট Θ(n), amortized O(1)ধারার মোট কপি গুনুন
Hoare partition-এর পর pivot “নিজের জায়গায়”নিশ্চয়তা নেইপ্রোগ্রাম ২ চালান
Heap trace-এ লাইব্রেরি ফাংশনের অ্যারে দিয়ে উত্তরবইয়ের sift-down-এর সাথে না-ও মিলতে পারেনিজের sift-down লিখে তুলনা করুন

AI-কে উত্তরদাতা নয়, প্রশ্ন বানানোর আর আপনার trace পরীক্ষার সহকারী বানান:

আমি DSA-র লিখিত পরীক্ষার জন্য hand-trace অনুশীলন করছি। নিচে একটি অ্যালগরিদম, একটি ইনপুট আর আমার হাতে করা trace দিলাম: [অ্যালগরিদমের নাম ও নিয়ম (0-based/1-based, pivot, tie-break) + ইনপুট + আমার trace পেস্ট করুন] সরাসরি পুরো উত্তর দিও না। শুধু এগুলো করো: ১. আমার trace-এর প্রথম যে সারিতে ভুল আছে, শুধু সেই সারির নম্বর বলো — কারণ বলো না। ২. আমি ঠিক করার পর, একই নিয়মে একটি ছোট কিন্তু ফাঁদওয়ালা নতুন ইনপুট দাও (সমান মান, খালি অংশ বা সীমার কাছের মান)। ৩. এই অ্যালগরিদমের time আর space complexity আমি নিজে লিখব; তুমি শুধু বলো আমার যুক্তির কোন লাইন দুর্বল। ৪. তুমি যে সংখ্যা বলবে, তা যাচাই করার জন্য একটি ছোট C++ বা Python প্রোগ্রাম দাও যা প্রতিটি ধাপ ছাপায় — আমি নিজে চালিয়ে মেলাব।

যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন · গোনা আর চালানোর পেছনের গল্প

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

প্র ০১হাতে trace করার অভ্যাস কি শুধু পরীক্ষার জন্য, নাকি প্রোগ্রাম ঠিক কি না প্রমাণের সাথেও এর সম্পর্ক আছে?

সম্পর্কটি পুরোনো আর গভীর। ১৯৪৯ সালে Alan Turing কেমব্রিজের একটি সম্মেলনে “Checking a Large Routine” নামে একটি ছোট লেখা উপস্থাপন করেন। সেখানে তিনি একটি factorial-জাতীয় প্রোগ্রামের প্রতিটি ধাপের পর ভ্যারিয়েবলগুলোর মধ্যে কোন সম্পর্ক সত্য থাকে, তা একটি টেবিলে লিখে দেখান — আর যুক্তি দেন, প্রোগ্রাম যাচাইকারী এই সম্পর্কগুলো একে একে মিলিয়েই পুরো প্রোগ্রামের শুদ্ধতা বিশ্বাস করতে পারেন। এটি প্রায় আমাদের trace table, শুধু নির্দিষ্ট সংখ্যার বদলে সাধারণ সম্পর্ক।

পরে ১৯৬৭ সালে Robert Floyd (“Assigning Meanings to Programs”) আর ১৯৬৯ সালে Tony Hoare (“An Axiomatic Basis for Computer Programming”) এই ধারণাকে একটি পূর্ণ পদ্ধতিতে রূপ দেন — প্রতিটি লুপে একটি invariant, প্রতিটি নির্দেশের আগে-পরে শর্ত। পর্ব ০৩-এর loop invariant সেই ধারারই অংশ।

একটি নির্দিষ্ট ইনপুটে trace প্রমাণ নয় — শুধু একটি উদাহরণ। কিন্তু ভালো trace-এ আপনি প্রতিটি সারিতে invariant-ও চোখে দেখেন: “বাঁ অংশ সাজানো”, “settled-দের dist চূড়ান্ত”. অনেক সময় প্রমাণের ধারণা আসে ঠিক এই ছকগুলো থেকে।

মূল উপলব্ধি: trace হলো প্রমাণের খসড়া। কয়েকটি ইনপুটে নিখুঁত trace আপনাকে বলে কোন সম্পর্ক সবসময় সত্য থাকে — আর সেটাই invariant.

প্র ০২“Master theorem” নামটি কোথা থেকে এল, আর এর সীমা জেনে রাখা কেন জরুরি?

Divide and conquer-এর recurrence অনেক আগে থেকে আলাদা আলাদা করে সমাধান করা হতো। ১৯৮০ সালে Jon Bentley, Dorothea Haken আর James Saxe SIGACT News-এ “A General Method for Solving Divide-and-Conquer Recurrences” প্রবন্ধে T(n) = aT(n/b) + f(n) আকারের সাধারণ সমাধান দেন। ১৯৯০ সালে Cormen, Leiserson আর Rivest-এর বিখ্যাত পাঠ্যবই Introduction to Algorithms এটিকে “master theorem” নামে তিনটি case-এ সাজিয়ে উপস্থাপন করে, আর নামটি সেখান থেকেই ছড়িয়ে পড়ে।

কিন্তু উপপাদ্যটির সীমা আছে, আর পরীক্ষায় ঠিক সেই সীমা নিয়েই ফাঁদ থাকে: ক-২০-এর মতো f(n) = n log n, যেখানে তিন case-এর মাঝে ফাঁক; ক-২১-এর মতো বিয়োগের recurrence; ক-২২-এর মতো অসমান ভাগ। ১৯৯৮ সালে Mohamad Akra আর Louay Bazzi একটি অনেক সাধারণ সূত্র প্রকাশ করেন, যা অসমান ভাগও সামলায় — পর্ব ০৯-এ এর পরিচয় আছে।

বাস্তবে সবচেয়ে নির্ভরযোগ্য যন্ত্র তবু recursion tree: প্রতিটি স্তরের খরচ লিখে ধারাটি বাড়ছে, কমছে না সমান — দেখুন। Master theorem সেই পর্যবেক্ষণেরই সংক্ষিপ্ত রূপ।

মূল উপলব্ধি: সূত্র প্রয়োগের আগে শর্ত মেলান — আকার কি ভাগ হচ্ছে, সব ভাগ কি সমান, f(n) কি বহুপদী তুলনায় পরিষ্কার? না মিললে গাছ আঁকুন।

প্র ০৩Amortized বিশ্লেষণ কেন আলাদা একটি পদ্ধতি হিসেবে স্বীকৃতি পেল, আর বাস্তবের লাইব্রেরিতে এর ছাপ কোথায়?

১৯৭০-এর দশকে union-find, splay tree-এর মতো স্ট্রাকচারের বিশ্লেষণে দেখা গেল, একটি একক কাজের worst case দিয়ে হিসাব করলে উত্তর অযথা খারাপ আসে। ১৯৮৫ সালে Robert Tarjan “Amortized Computational Complexity” প্রবন্ধে এই চিন্তাকে একটি সাধারণ পদ্ধতি হিসেবে গুছিয়ে উপস্থাপন করেন — banker’s (accounting) আর physicist’s (potential) দৃষ্টিভঙ্গিসহ। একই বছরে Daniel Sleator-এর সাথে তাঁর splay tree-র কাজ এই পদ্ধতির সবচেয়ে বিখ্যাত প্রয়োগ।

ক-২৪-এর dynamic array প্রতিদিনের উদাহরণ। কত গুণ বাড়বে, তা লাইব্রেরিভেদে আলাদা: GCC-র libstdc++ std::vector-এর capacity দ্বিগুণ করে, Microsoft-এর বাস্তবায়ন ১.৫ গুণ, আর CPython-এর list প্রায় এক-অষ্টমাংশ বাড়তি জায়গা নেয়। যতক্ষণ গুণ করে বাড়ে (যোগ করে নয়), মোট কপি Θ(n) — গুণকটি শুধু মেমরি অপচয় আর কপির ধ্রুবকের মধ্যে আপস।

সাবধান থাকার জায়গাও আছে। amortized O(1) মানে প্রতিটি কাজ দ্রুত নয় — মাঝে মাঝে একটি push পুরো অ্যারে কপি করে। রিয়েল-টাইম সিস্টেমে (যেমন খেলার প্রতি ফ্রেম বা নিয়ন্ত্রণ ব্যবস্থা) সেই একটি ধীর কাজই সমস্যা; তখন আগে থেকে reserve করা বা deamortized স্ট্রাকচার লাগে (পর্ব ৬০)।

মূল উপলব্ধি: amortized হলো পুরো ধারার হিসাব, গড় ইনপুটের নয়। কোন ভ্যারিয়েবল কখনো পেছায় না, বা কোন দামি কাজ ঘন ঘন হতে পারে না — এটা খুঁজে পাওয়াই কৌশল।

প্র ০৪Dijkstra-র টেবিল খাতায় করা এত সহজ কেন — আর এর নকশার গল্পে কী শেখার আছে?

Edsger Dijkstra নিজে একটি সাক্ষাৎকারে বলেছেন, ১৯৫৬ সালে আমস্টারডামে বাগদত্তার সাথে কেনাকাটার পর একটি ক্যাফের বারান্দায় বসে প্রায় বিশ মিনিটে তিনি সবচেয়ে ছোট পথের অ্যালগরিদমটি ভাবেন — কাগজ-কলম ছাড়াই। তাঁর মতে কাগজ না থাকায় তাঁকে জটিলতা এড়াতে হয়েছিল, আর তাতেই নকশাটি এত সরল হয়েছে। অ্যালগরিদমটি প্রকাশিত হয় ১৯৫৯ সালে, “A Note on Two Problems in Connexion with Graphs” নামে তিন পাতার একটি প্রবন্ধে।

এই সরলতাই হাতে trace সহজ করে: প্রতিটি ধাপে একটিই সিদ্ধান্ত — বাকিদের মধ্যে সবচেয়ে ছোট dist যার, সে চূড়ান্ত। টেবিলের একটি কলাম (settled) এই invariant-টি সরাসরি দেখায়। ট-২২-এর stepper-এর মতো heap-এর অবস্থাসহ লিখলে বাস্তবায়নের খুঁটিনাটিও (পুরোনো এন্ট্রি বাতিল) ধরা পড়ে, যা অনেক বই এড়িয়ে যায়।

একই সরলতা দেখায় অ্যালগরিদমটির সীমাও। “সবচেয়ে ছোটটি চূড়ান্ত” কথাটি সত্য কারণ সব ওজন অঋণাত্মক; একটি ঋণাত্মক edge থাকলে পরে আসা পথ আগের “চূড়ান্ত” মানকে ছোট করতে পারে। ছোট একটি পাল্টা-উদাহরণ হাতে trace করেই এটা দেখানো যায়।

মূল উপলব্ধি: যে অ্যালগরিদম খাতায় সহজে চালানো যায়, তার invariant সাধারণত স্পষ্ট। আর স্পষ্ট invariant-ই বলে দেয় অ্যালগরিদম কোথায় খাটে, কোথায় খাটে না।

প্র ০৫০-based আর ১-based index নিয়ে এত বিভ্রান্তি কেন — কোনটি “ঠিক”?

দুটিই ঠিক; প্রশ্ন হলো কোনটি কোন কাজে সুবিধাজনক। ১৯৮২ সালে Dijkstra একটি ছোট হাতে লেখা নোটে (EWD831, “Why numbering should start at zero”) যুক্তি দেন, পরিসর অর্ধেক-খোলা [lo, hi) আর গোনা ০ থেকে শুরু করলে দৈর্ঘ্য হয় hi − lo আর ফাঁকা পরিসর লেখা সহজ। C, C++, Python, Java — সবই ০-based, কারণ index আসলে শুরুর ঠিকানা থেকে দূরত্ব (পর্ব ১০)।

অন্যদিকে কিছু স্ট্রাকচারে ১-based সূত্র অনেক পরিষ্কার। Binary heap ১-based হলে i-এর সন্তান 2i আর 2i + 1, parent i/2; ০-based-এ 2i + 1, 2i + 2, (i − 1)/2 — তাই CLRS-সহ অনেক পাঠ্যবই heap ১-based-এ লেখে। Peter Fenwick ১৯৯৪ সালের প্রবন্ধে যে tree দেন, তার lowbit কৌশল ০ index-এ কাজই করে না (lowbit(0) = 0, লুপ থামে না)। Segment tree-তে root 1 রাখলে সন্তান 2v, 2v + 1. অনেক পাঠ্যবইয়ের pseudocode ১-based, অথচ পরীক্ষার্থী কোড লেখেন ০-based-এ — বিভ্রান্তির মূল এখানেই।

এই পাতার প্রতিটি প্রশ্নে তাই index পদ্ধতি লেখা আছে, আর ভাগ ৯-এর প্রথম নিয়মও সেটাই: উত্তরের শুরুতে লিখুন আপনি কোনটি ধরছেন।

মূল উপলব্ধি: index পদ্ধতি একটি চুক্তি। চুক্তি স্পষ্ট লিখলে দুটিই নিরাপদ; না লিখলে সঠিক trace-ও “ভুল” দেখাতে পারে।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি রিকার্সিভ ফাংশনের complexity master theorem দিয়ে বের করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. কোড থেকে ডাকের সংখ্যা a, ছোট আকার n/b আর বাইরের কাজ f(n) বের করা
  2. T(n) = aT(n/b) + f(n) আকারে লেখা, আকার ভাগ হচ্ছে কি না যাচাই করা
  3. nlogb a হিসাব করা
  4. f(n)-কে nlogb a-এর সাথে তুলনা করে case ঠিক করা
  5. Case 3 হলে regularity শর্ত যাচাই করে Θ(·) লেখা

(সহজ) for (i = 1; i <= n; i *= 3) cnt++; — লুপটি কতবার চলে, n-এর কোন ক্রমে?

  • Θ(log n)
  • Θ(n)
  • Θ(√n)
  • Θ(log log n)

(মাঝারি) for (i = 1; i <= n; i++) for (j = 1; j <= n; j += i) cnt++; — মোট কাজ কত?

  • Θ(n²)
  • Θ(n log n)
  • Θ(n)
  • Θ(n√n)

(মাঝারি) T(n) = 8T(n/2) + n² হলে T(n) কত?

  • Θ(n²)
  • Θ(n² log n)
  • Θ(n³)
  • Θ(2ⁿ)

(মাঝারি) একটি dynamic array ভরে গেলে capacity 100 করে বাড়ায় (দ্বিগুণ নয়)। খালি অবস্থা থেকে n বার push_back-এর মোট খরচ কত?

  • Θ(n)
  • Θ(n log n)
  • Θ(1) প্রতি push, মোট Θ(n)
  • Θ(n²)

(সহজ) [5, 1, 4, 2, 8]-এ bubble sort-এর (ছোট থেকে বড়) প্রথম পাসের পর অ্যারে কী?

  • [1, 4, 2, 5, 8]
  • [1, 2, 4, 5, 8]
  • [1, 5, 4, 2, 8]
  • [1, 5, 4, 8, 2]

(মাঝারি) ০-based min-heap [2, 5, 3, 9, 7, 8]-এ 1 insert (শেষে বসিয়ে sift-up) করলে অ্যারে কী হয়?

  • [2, 5, 1, 9, 7, 8, 3]
  • [1, 5, 2, 9, 7, 8, 3]
  • [1, 2, 3, 5, 7, 8, 9]
  • [1, 2, 3, 9, 7, 8, 5]

(মাঝারি) খালি BST-তে 40, 20, 60, 10, 30, 50, 70 ক্রমে insert করলে postorder কী?

  • 40 20 10 30 60 50 70
  • 10 20 30 40 50 60 70
  • 10 30 20 50 70 60 40
  • 10 30 50 70 20 60 40

(মাঝারি) m = 7 ঘরের hash table, h(k) = k mod 7, linear probing. 10, 17, 24, 3 ক্রমে insert করলে 3 কোন index-এ বসে?

  • 3
  • 4
  • 5
  • 6

(মাঝারি) KMP-এর prefix function (০-based) — AABAAB-এর π অ্যারে কী?

  • [0, 0, 0, 1, 2, 3]
  • [0, 1, 0, 1, 2, 3]
  • [0, 1, 0, 1, 2, 2]
  • [0, 1, 2, 1, 2, 3]

(কঠিন) DAG, vertex 1..4, edge 3→1, 2→1, 4→2, 4→3. Kahn-এর অ্যালগরিদমে কিউর বদলে min-heap নিলে topological ক্রম কী?

  • 1 2 3 4
  • 4 3 2 1
  • 4 2 3 1
  • 2 4 3 1

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বে আসা শব্দগুলো এখানে একসাথে:

মনে রাখুন

  1. গুনুন, তারপর যোগ করুন: ভেতরের লুপের কাজ বাইরের ভ্যারিয়েবলের ওপর নির্ভর করলে গুণ নয়, যোগফল — arithmetic (n²), geometric (শেষ পদ), harmonic (n log n), log n! (n log n).
  2. Recurrence: গাছ এঁকে স্তরের খরচ দেখুন — সমান হলে × log n, বাড়লে leaf জেতে, কমলে root. Master theorem শুধু ভাগের আকারে, আর তিন case-এর ফাঁক মনে রাখুন।
  3. Amortized: কোনো ভ্যারিয়েবল কখনো পেছায় না, বা দামি কাজ ঘন ঘন হয় না — তাহলে পুরো ধারা গুনুন। গুণ করে বাড়া dynamic array Θ(n), যোগ করে বাড়া Θ(n²).
  4. Trace: নিয়ম আগে লিখুন (index, pivot, tie, প্রতিবেশীর ক্রম), প্রতিটি মধ্যবর্তী অবস্থা দেখান, শেষে চেক-যোগফল মেলান।
  5. যাচাই: নিজের trace-কে একটি ছোট প্রোগ্রাম দিয়ে মেলান; AI-এর complexity আর trace দুটিই একই চোখে দেখুন।

পরের পর্বে: পুরো কোর্সের MCQ প্রশ্নব্যাংক — মডিউল ধরে সাজানো, প্রতিটি অপশনের ব্যাখ্যাসহ। এই পাতার গোনা আর trace-এর দক্ষতা সেখানে দ্রুত সিদ্ধান্তে কাজে লাগবে।

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান