এই পর্বে যা শিখবেন
- কোডিং ইন্টারভিউর সাত ধাপের পদ্ধতি, কথা বলতে বলতে কোড লেখা, আর আটকে গেলে কী করবেন
- ১৪টি প্যাটার্ন চেনার সংকেত — সমস্যার বিবরণের কোন শব্দ কোন কৌশলের দিকে ইঙ্গিত করে
- প্রতিটি প্যাটার্নের টেমপ্লেট আর একটি পূর্ণ সমাধান: brute force থেকে সেরা পদ্ধতি, সঠিক জটিলতা আর edge case
- ইনপুটের আকার দেখে কতটা দ্রুত সমাধান লাগবে, তা আগেই আন্দাজ করা
- নিজের সমাধান brute force আর stress test দিয়ে যাচাই করা — AI-এর লেখা কোডও
১ · ইন্টারভিউর কাঠামো — সাত ধাপ, কথা বলা আর আটকে গেলে
পর্ব ৬৮–৭০-এ আপনি হাতে চালানো, MCQ আর লিখিত-মৌখিক প্রশ্নের অনুশীলন করেছেন। কোডিং ইন্টারভিউ এর থেকে আলাদা। এখানে একটি অচেনা সমস্যা দেওয়া হয়, আর ৩০–৪৫ মিনিটে আপনাকে সেটি বুঝে, সমাধান বের করে, কোড লিখে, পরীক্ষা করে দেখাতে হয় — পুরো সময় কথা বলে। দেশি সফটওয়্যার প্রতিষ্ঠান, স্টার্টআপ, ব্যাংক বা টেলিকমের আইটি বিভাগ, বিদেশি প্রতিষ্ঠানের রিমোট পদ — প্রতিষ্ঠানভেদে ধাপের নাম আর ক্রম আলাদা হয়, কিন্তু অ্যালগরিদমের অংশটা মোটামুটি একই রকম।
| ধাপ | সাধারণত কী হয় | এই কোর্সে প্রস্তুতি |
|---|---|---|
| Online assessment | সময় বাঁধা অনলাইন পরীক্ষা: ২–৪টি সমস্যা, লুকানো test case দিয়ে যন্ত্র নম্বর দেয়। কখনো MCQ-ও থাকে। | এই পাতার প্যাটার্ন, MCQ-র জন্য পর্ব ৬৯ |
| Live coding | শেয়ার করা editor বা হোয়াইটবোর্ডে ১–২টি সমস্যা, ইন্টারভিউয়ারের সাথে আলোচনা করে। | সাত ধাপের পদ্ধতি — এই অংশ |
| ফলো-আপ প্রশ্ন | “n দশ গুণ হলে?”, “মেমরি কমাতে পারবেন?”, “এটা ঠিক কেন?” | জটিলতা (পর্ব ০৪), প্রমাণ (পর্ব ০৩, পর্ব ৭০) |
| Dry run | “এই ইনপুটে আপনার কোড লাইন ধরে চালিয়ে দেখান।” | পর্ব ৬৮ |
সাত ধাপের পদ্ধতি
চাপের মধ্যে মাথা ফাঁকা হয়ে যাওয়া খুব স্বাভাবিক। এর সবচেয়ে ভালো ওষুধ একটি বাঁধা রুটিন — যাতে “এখন কী করব” ভাবতে না হয়, শুধু পরের ধাপে যেতে হয়। ডাক্তার যেমন প্রতিটি রোগীর ক্ষেত্রে একই ক্রমে এগোন — লক্ষণ জিজ্ঞেস করা, পরীক্ষা, রোগ নির্ণয়, ওষুধ, তারপর ফলো-আপ — আপনিও তেমন।
পদ্ধতিটি একটি সমস্যায় প্রয়োগ করে দেখা যাক। সমস্যা · লাভ-ক্ষতির খাতা (মাঝারি): “একটি মুদি দোকানের n দিনের হিসাব দেওয়া আছে — প্রতিদিনের লাভ (ঋণাত্মক হলে ক্ষতি)। কতগুলো টানা দিনের অংশে মোট লাভ ঠিক k টাকা?” নিচে “পরে” চাপুন:
- ১ · বুঝুনপ্রশ্ন করে সীমা জানুন
- ২ · উদাহরণছোট ইনপুট ও edge case
- ৩ · Brute forceআগে একটি সঠিক সমাধান
- ৪ · উন্নতিবারবার হওয়া কাজ কমান
- ৫ · কোডপরিষ্কার, ছোট ফাংশন
- ৬ · পরীক্ষানিজের উদাহরণে dry run
- ৭ · জটিলতাBig-O আর ফলো-আপ
- ঋণাত্মক মান আছে? (হ্যাঁ, ক্ষতির দিন)
- n কত বড়? (২ লাখ পর্যন্ত)
- অংশগুলো টানা দিন হতে হবে? (হ্যাঁ)
- গুনতে হবে, নাকি অংশগুলো দেখাতে? (গুনতে)
- উত্তর কত বড় হতে পারে?
n(n+1)/2 ≈ 2·10^10 -> long long
[1, 1, 1], k = 2 -> 2 [3,4,7,2,-3,1,4,2], k = 7 -> 4 [3,4] [7] [7,2,-3,1] [1,4,2] [0, 0, 0], k = 0 -> 6 শূন্যের ফাঁদ [5], k = 5 -> 1 [], k = 0 -> 0 খালি
for i in 0..n-1:
s = 0
for j in i..n-1:
s += a[j] // চলমান যোগফল
if s == k: ans++
সময় O(n²), বাড়তি মেমরি O(1)
n = 2·10^5 -> প্রায় 2·10^10 ধাপ: ধীর
P[t] = প্রথম t দিনের যোগফল
sum(a[i..j]) = P[j+1] - P[i] = k
<=> P[i] = P[j+1] - k
-> "আগের কতগুলো prefix-এর মান
P[j+1] - k?" -> hash map-এ গুনে রাখি
sliding window চলবে না: ঋণাত্মক আছে
seen = {0: 1} // খালি prefix একবার দেখা
pre = 0, ans = 0
for x in a:
pre += x
ans += seen[pre - k] // আগে খুঁজি
seen[pre] += 1 // পরে রাখি
x=1: pre=1, খুঁজি -1: 0 seen {0:1, 1:1}
x=1: pre=2, খুঁজি 0: 1 seen {.., 2:1} ans=1
x=1: pre=3, খুঁজি 1: 1 seen {.., 3:1} ans=2
{0: 1} না দিলে প্রথম অংশ [1,1] হারাত -> ans=1
সময় গড়ে O(n), মেমরি O(n) (hash map-এর কাজ গড়ে O(1); map নিলে O(n log n)) - সব মান ধনাত্মক হলে? -> sliding window, O(1) মেমরি - k দিয়ে বিভাজ্য অংশ? -> pre % k গুনুন - সবচেয়ে লম্বা অংশ? -> প্রথম index রাখুন
- বুঝুন। সমস্যার বিবরণ প্রায়ই ইচ্ছা করে অসম্পূর্ণ রাখা হয় — আপনি প্রশ্ন করেন কি না, সেটাই দেখা হয়। এখানে “ঋণাত্মক আছে কি না” প্রশ্নটির উত্তরই ঠিক করে দেবে কোন প্যাটার্ন চলবে। n-এর সীমা বলে দেয় কতটা দ্রুত সমাধান লাগবে, আর উত্তরের আকার বলে দেয়
intযথেষ্ট কি না। - উদাহরণ। দেওয়া উদাহরণের বাইরে নিজে কয়েকটি লিখুন: সব শূন্য (উত্তর অনেক বড় হয়ে যায়), একটিমাত্র দিন, খালি খাতা। এগুলোই পরে আপনার test case. Edge case আগে লিখলে কোডে ভুলে যাওয়ার সম্ভাবনা কমে।
- Brute force. সবচেয়ে সরল brute force সমাধানটি মুখে বলুন, যদিও সেটি ধীর। এতে দেখান যে অন্তত একটি সঠিক উত্তর আপনার হাতে আছে। সময় ফুরিয়ে এলে এটাই কোড করুন — কিছু না থাকার চেয়ে সঠিক ধীর সমাধান অনেক ভালো।
- উন্নতি। নিজেকে জিজ্ঞেস করুন: “কোন কাজটি বারবার হচ্ছে?” প্রতিটি j-এর জন্য আমরা আসলে একটি সংখ্যা খুঁজছি — আগের কোনো prefix sum যার মান P[j+1] − k. খোঁজার কাজ দ্রুত করার যন্ত্র hash map. ঋণাত্মক থাকায় sliding window এখানে ভুল উত্তর দেবে — এটাও মুখে বলুন।
- কোড। ইন্টারভিউয়ার পদ্ধতিতে রাজি হলে তবেই কোড লিখুন। খুঁটিনাটি দুটি: শুরুতে
seen[0] = 1(খালি prefix), আর আগে খুঁজি, পরে রাখি — যাতে k = 0 হলে একটি prefix নিজের সাথে জোড়া না বাঁধে। - পরীক্ষা। “শেষ” বলার আগে নিজের ছোট উদাহরণে কোডটি লাইন ধরে চালান — এটাই dry run. এখানে দেখা যাচ্ছে,
{0: 1}বাদ দিলে শুরু থেকে শুরু হওয়া অংশ [1, 1] গোনা হতো না। - জটিলতা ও ফলো-আপ। সময় আর মেমরি আবার পরিষ্কার করে বলুন, আর “গড়ে” শব্দটি বাদ দেবেন না। এরপর ইন্টারভিউয়ার সাধারণত শর্ত বদলান। প্রতিটি ফলো-আপে আবার একই সাত ধাপ, শুধু দ্রুত।
ইনপুটের আকার থেকে লক্ষ্য-জটিলতা
ইন্টারভিউয়ার বা প্রশ্নপত্র n-এর সীমা বললে সেটি একটি বড় ইঙ্গিত। একটি সাধারণ হিসাব: সাধারণ কম্পিউটার এক সেকেন্ডে মোটামুটি 108-এর কাছাকাছি সরল কাজ করতে পারে (ভাষা আর কাজের ধরনে কয়েক গুণ কম-বেশি হয়)। তাই:
| n-এর আকার | যে জটিলতা সাধারণত চলে | যে প্যাটার্নের কথা ভাববেন |
|---|---|---|
| n ≤ ১০–১২ | O(n!), O(n · n!) | সব বিন্যাস — backtracking |
| n ≤ ২০–২৫ | O(2n), O(n · 2n) | সব উপসেট, bitmask DP |
| n ≤ ৫০০ | O(n3) | interval DP, Floyd-Warshall |
| n ≤ ৫,০০০ | O(n2) | 2D DP (LCS, edit distance), জোড়ার লুপ |
| n ≤ 105–106 | O(n log n) বা O(n) | sort, heap, binary search, hashing, two pointer, window, stack |
| n ≥ 109 (মান বা পরিসর) | O(log n) বা O(√n) | উত্তরের ওপর binary search, গণিত |
কথা বলতে বলতে কোড লেখা
ইন্টারভিউয়ার আপনার মাথার ভেতর দেখতে পান না। তাই think aloud — মানে ভাবনাটা জোরে বলা — এখানে সবচেয়ে বড় দক্ষতা। সব কিছু বলার দরকার নেই, সিদ্ধান্তগুলো বলুন। কয়েকটি কাজের বাক্য:
- শুরুতে (clarifying question): “ধরে নিচ্ছি array খালি হতে পারে, আর উত্তর না থাকলে −1 দেব — ঠিক আছে?”
- পদ্ধতি বাছাইয়ে: “দুটি লুপে O(n²); n দুই লাখ হলে সেটা প্রায় দুই হাজার কোটি ধাপ। তাই prefix sum আর hash map ভাবছি — মেমরি O(n) বাড়বে।”
- লেখার মাঝে: “এখানে আগে খুঁজছি, পরে রাখছি, যাতে k = 0-তে একই prefix নিজের সাথে না মেলে।”
- শেষে: “সময় গড়ে O(n), মেমরি O(n). উত্তর long long-এ রাখছি, কারণ সব শূন্য হলে অংশের সংখ্যা n²/2-এর কাছাকাছি।”
আটকে গেলে কী করবেন
আটকে যাওয়া ব্যর্থতা নয় — ইন্টারভিউয়ার প্রায়ই দেখতে চান না-জানা অবস্থায় আপনি কীভাবে এগোন। নীরব হয়ে যাওয়াই আসল বিপদ। এই ক্রমে চেষ্টা করুন:
- ছোট উদাহরণ হাতে করুন। ৪–৫টি উপাদানের একটি ইনপুটে নিজে উত্তর বের করুন, আর লক্ষ করুন আপনার মাথা কী করছে। সেটাই প্রায়ই অ্যালগরিদম।
- সমস্যা সহজ করুন। ঋণাত্মক বাদ দিন, ২D-কে ১D করুন, k = 1 ধরুন। সহজটি সমাধান করে মূলটিতে ফিরুন।
- প্যাটার্নের তালিকা ঘুরে দেখুন। “সাজালে কি সহজ হয়? hash map দিলে? দুই প্রান্ত থেকে? উত্তরের ওপর binary search?” অংশ ৯-এর চিটশিট মুখস্থ থাকলে এটা কয়েক সেকেন্ডের কাজ।
- Brute force-এর অপচয় খুঁজুন। কোন হিসাব বারবার হচ্ছে? সেটি মনে রাখা (memo, prefix, map) বা বাদ দেওয়া (pointer, pruning) যায় কি?
- বলে ফেলুন। “আমি এই জায়গায় আটকেছি, কারণ …” — ভালো ইন্টারভিউয়ার তখন ছোট একটি hint দেন। Hint নিয়ে দ্রুত এগোনোও মূল্যায়নের অংশ, লজ্জার কিছু নয়।
ইন্টারভিউ একটি কাজের নমুনা। চুপচাপ লেখা নিখুঁত কোডের চেয়ে কথা বলে ধাপে ধাপে এগোনো, edge case নিজে থেকে তোলা আর সৎভাবে জটিলতা বলা প্রায়ই বেশি মূল্য পায়।
এই পাতার বাকি অংশে ১৪টি প্যাটার্ন আছে। প্রতিটির শুরুতে রিভিশনের পর্ব দেওয়া আছে — সমাধান পড়ার আগে সেই পর্বের মূল ধারণা একবার ঝালিয়ে নিন।
২ · Hashing আর two pointer — খোঁজা দ্রুত করা
প্যাটার্ন ১ · Hashing — “এটা কি আগে দেখেছি?”
রিভিশন: পর্ব ৩২ · Hash table, পর্ব ১৫ · STL আর Python-এর কালেকশন
চেনার সংকেত: “জোড়া খুঁজুন যার যোগফল …”, “আগে এসেছে কি না”, “ডুপ্লিকেট আছে কি”, “কতবার এসেছে”, “anagram”, “প্রথম অপুনরাবৃত্ত”, অথবা brute force-এর ভেতরের লুপ শুধু একটি নির্দিষ্ট মান খুঁজছে। ইনপুট সাজানো নয়, আর মূল index দরকার — তাহলে sort না করে hash.
Pseudocode (0-based) — "seen" template
seen = empty hash map // key -> whatever you need (index, count)
for j = 0 .. n-1:
if NEED(a[j]) is in seen: // look first ...
use seen[NEED(a[j])]
seen[a[j]] = j // ... then storeসমাধান-করা সমস্যা · বাজেটের দুই পণ্য (সহজ): “একটি অনলাইন দোকানে n টি পণ্যের দাম দেওয়া আছে। আপনার bKash-এ ঠিক T টাকা আছে। এমন দুটি ভিন্ন পণ্যের index বের করুন, যাদের দামের যোগফল ঠিক T. না থাকলে −1 −1 দিন।” এটি বিখ্যাত two sum সমস্যার একটি রূপ।
- Brute force: সব জোড়া (i, j) দেখুন — n(n−1)/2 টি জোড়া, সময় O(n²), বাড়তি মেমরি O(1).
- অপচয় কোথায়: ভেতরের লুপ প্রতিবার একটাই প্রশ্ন করে — “T − a[j] কি আগে কোথাও আছে?” এই খোঁজাটা hash map-এ গড়ে O(1).
- সেরা: একবার বাঁ থেকে ডানে যান, প্রতিটি j-এ আগে খুঁজুন, তারপর a[j] রাখুন। সময় গড়ে O(n), মেমরি O(n). এটাই সময়-মেমরির বিনিময়।
pair<int, int> twoSum(const vector<int>& a, int target) {
unordered_map<int, int> seen; // value -> index
for (int j = 0; j < (int)a.size(); ++j) {
if (auto it = seen.find(target - a[j]); it != seen.end()) return {it->second, j};
seen.emplace(a[j], j); // look first, then store
}
return {-1, -1};
}এই ফাংশন চারটি ইনপুটে চালিয়ে যাচাই করা হয়েছে: দাম [450, 120, 300, 80], T = 380 → 2 3 (300 + 80); [200, 200], T = 400 → 0 1; [500], T = 1000 → -1 -1; [−3, 4, 3, 90], T = 0 → 0 2.
- Edge case: একই দাম দুবার ([200, 200]) — “আগে খুঁজি, পরে রাখি” ক্রম থাকায় ঠিক কাজ করে; ক্রম উল্টালে [500], 1000-এ একই পণ্য নিজের সাথে জোড়া বাঁধত।
emplaceআগে থেকে থাকা key বদলায় না, তাই ডুপ্লিকেটে প্রথম index থাকে। - সতর্কতা:
unordered_map-এর O(1) গড়ে; ইচ্ছা করে বানানো ইনপুটে (anti-hash test) এটি অনেক ধীর হতে পারে। কনটেস্টে তখনmap(O(log n)) বা custom hash নিন (পর্ব ৩২)। - ফলো-আপ: দাম সাজানো থাকলে? → two pointer, মেমরি O(1) (প্যাটার্ন ২)। সব জোড়া গুনতে হলে? → map-এ গণনা রাখুন,
ans += cnt[T − x].
Hashing-এর আরেক রূপ frequency count: প্রতিটি অক্ষর বা মান কতবার এসেছে, তা গুনে রাখা। দুটি শব্দ anagram কি না দেখতে দুটোর গণনা মেলান — ছোট বর্ণমালায় ২৬ ঘরের frequency array-ই যথেষ্ট, hash map-এর চেয়ে দ্রুত।
অনু ১.১একটি string-এর প্রথম যে অক্ষরটি পুরো string-এ ঠিক একবার আছে, তার index বের করুন; না থাকলে −1. "aabbcdc"-এ উত্তর কত? (সহজ)
Hint: দুবার ঘুরুন — প্রথমবার গুনুন, দ্বিতীয়বার প্রথম যার গণনা 1, সেটি নিন।
উত্তর: "aabbcdc"-এ a, b দুবার করে, c দুবার, d একবার — উত্তর index 5 ('d'). "dhakadhaka"-য় সব অক্ষর পুনরাবৃত্ত, তাই −1. সময় O(n), মেমরি O(Σ), যেখানে Σ বর্ণমালার আকার (ASCII-তে ২৫৬ ঘরের array). ভুল পদ্ধতি: প্রতিটি অক্ষরের জন্য পুরো string আবার গোনা — O(n²)।
অনু ১.২শব্দের তালিকা ["eat", "tea", "tan", "ate", "nat", "bat"]-কে anagram-দলে ভাগ করুন। কয়টি দল, আর জটিলতা কত? (মাঝারি)
Hint: anagram-দের একটি সাধারণ “পরিচয়” বানান, যা hash map-এর key হবে।
উত্তর: প্রতিটি শব্দের অক্ষর সাজিয়ে key বানান ("eat" → "aet"). দল ৩টি: [eat, tea, ate], [tan, nat], [bat]. n টি শব্দ, প্রতিটির দৈর্ঘ্য সর্বোচ্চ m হলে সময় O(n · m log m). key হিসেবে ২৬টি গণনার tuple নিলে O(n · m). মেমরি O(n · m).
প্যাটার্ন ২ · Two pointer — দুই প্রান্ত থেকে চেপে আনা
রিভিশন: পর্ব ১৬ · Two pointer আর sliding window, পর্ব ৫০ · Greedy আর exchange argument
চেনার সংকেত: “সাজানো array”, “জোড়া/ত্রয়ী যার যোগফল …”, “palindrome”, “in-place ডুপ্লিকেট সরান”, “দুটি সাজানো তালিকা মেলান”, “সর্বোচ্চ পানি ধরে এমন দুই দেয়াল”। মূল কথা: সাজানো ক্রম থেকে বোঝা যায় কোন pointer সরালে কোনো উত্তর হারায় না।
Pseudocode (0-based) — opposite ends
L = 0, R = n - 1
while L < R (or L <= R):
look at a[L], a[R]
move L right OR R left, whichever side can never be part of an answerসমাধান-করা সমস্যা · রিকশার যাত্রী (মাঝারি): “স্টেশনের সামনে n জন যাত্রীর ওজন দেওয়া আছে। একটি রিকশায় সর্বোচ্চ ২ জন বসতে পারেন, আর দুজনের মোট ওজন limit-এর বেশি হতে পারবে না। সবাইকে পৌঁছাতে কমপক্ষে কয়টি রিকশা লাগবে?” (কারও একার ওজন limit-এর বেশি নয়।)
- Brute force: সব রকম জোড়া বানানো পরখ করা — সম্ভাবনা সূচকীয় হারে বাড়ে। একটু ভালো: প্রতিবার সবচেয়ে ভারী জনের জন্য বাকিদের মধ্যে সবচেয়ে ভারী সম্ভাব্য সঙ্গী খোঁজা — O(n²)।
- মূল পর্যবেক্ষণ: সবচেয়ে ভারী জনকে কোনো না কোনো রিকশায় উঠতেই হবে। সবচেয়ে হালকা জনও যদি তাঁর সাথে না মেলেন, তাহলে কেউ মিলবেন না — তিনি একা যাবেন। আর মিললে, তাঁর সাথে সবচেয়ে হালকা জনকে বসানো নিরাপদ।
- সেরা: sort করে দুই প্রান্তে L আর R. প্রতি ধাপে R (ভারী জন) রওনা দেন; L-কে সাথে নেওয়া গেলে L-ও। সময় O(n log n) (sort) + O(n), মেমরি sort-এর ওপর নির্ভর করে, বাড়তি O(1).
int minRickshaws(vector<int> w, int limit) { // at most 2 riders, total <= limit
sort(w.begin(), w.end());
int L = 0, R = (int)w.size() - 1, trips = 0;
while (L <= R) {
if (w[L] + w[R] <= limit) ++L; // lightest rides with the heaviest
--R; ++trips; // heaviest always leaves now
}
return trips;
}চালিয়ে যাচাই: ওজন [70, 50, 80, 50], limit 120 → 3 (80 একা, 70 + 50 একসাথে, বাকি 50 একা); [60, 40, 90, 30, 80], limit 120 → 3 (90+30, 80+40, 60); খালি তালিকা → 0. ছোট ইনপুটে সব জোড়া-বিন্যাস পরখ করা একটি brute force-ও একই উত্তর দিয়েছে।
কেন greedy ঠিক (exchange argument, পর্ব ৫০): ধরুন সবচেয়ে ভারী জন H, সবচেয়ে হালকা জন h. ক্ষেত্র ১: H + h > limit হলে বাকি সবাই h-এর চেয়ে ভারী, তাই H কোনো সমাধানেই কারও সাথে বসতে পারেন না — তাঁকে একা পাঠানোই একমাত্র পথ। ক্ষেত্র ২: H + h ≤ limit. যেকোনো সেরা সমাধান নিন। সেখানে H বসেছেন X-এর সাথে, আর h বসেছেন Y-এর সাথে (কেউ একা থাকলে যুক্তি আরও সহজ)। h আর X-এর জায়গা বদলান: নতুন জোড়া (H, h) আর (X, Y). প্রথমটি বৈধ ধরে নেওয়া আছে; দ্বিতীয়টিতে Y ≤ H, তাই X + Y ≤ X + H ≤ limit — এটিও বৈধ। রিকশার সংখ্যা বদলায়নি। অর্থাৎ greedy-র প্রথম সিদ্ধান্ত মেনেও একটি সেরা সমাধান আছে; বাকি যাত্রীদের ওপর একই যুক্তি (induction) চালালেই প্রমাণ শেষ।
- Edge case: এক জন, সবাই খুব ভারী (সবাই একা), সবাই হালকা (সব জোড়া), L == R অবস্থা (শেষ জন একা) — তাই শর্ত
L <= R,L < Rনয়। - ফলো-আপ: রিকশায় ৩ জন বসতে পারলে? তখন এই সরল greedy আর প্রমাণসহ খাটে না — সমস্যাটি bin packing-এর কাছাকাছি চলে যায় (পর্ব ৬৫)।
অনু ২.১সাজানো array [1, 1, 2, 3, 3, 3, 4] থেকে in-place ডুপ্লিকেট সরান; কতগুলো আলাদা মান থাকল, আর array-এর শুরুটা কেমন হলো? (সহজ)
Hint: একই দিকে চলা দুই pointer — k লেখার জায়গা, i পড়ার জায়গা।
উত্তর: k = 1 থেকে শুরু; প্রতিটি i-তে a[i] != a[k-1] হলে a[k++] = a[i]. ফল 4টি মান, শুরু [1, 2, 3, 4]. সময় O(n), বাড়তি মেমরি O(1). খালি array-এ আলাদা করে 0 ফেরত দিন — নইলে a[k-1]-এ সীমার বাইরে পড়বেন।
অনু ২.২বাক্যটি শুধু অক্ষর আর অঙ্ক রেখে, বড়-ছোট হাতের পার্থক্য বাদ দিয়ে palindrome কি না বলুন: "Was it a car or a cat I saw?" (সহজ)
Hint: দুই প্রান্তে L, R; অক্ষর/অঙ্ক নয় এমন ঘর লাফিয়ে যান, তারপর tolower করে তুলনা।
উত্তর: পরিষ্কার করলে "wasitacaroracatisaw" — সামনে-পেছনে একই, তাই palindrome. সময় O(n), বাড়তি মেমরি O(1) (নতুন string না বানালে)। ফাঁদ: isalnum/tolower-এ char সরাসরি না দিয়ে unsigned char-এ রূপান্তর করে দিন — ঋণাত্মক char-এ এগুলোর আচরণ অনির্ধারিত।
৩ · Sliding window আর prefix sum — টানা অংশের সমস্যা
প্যাটার্ন ৩ · Sliding window — ডানে বাড়ান, বাঁয়ে ছাঁটুন
রিভিশন: পর্ব ১৬, পর্ব ৬০ · Amortized analysis
চেনার সংকেত: “টানা (contiguous) substring/subarray”, “সবচেয়ে লম্বা/ছোট অংশ যেখানে …”, “k দৈর্ঘ্যের প্রতিটি অংশ”, “সর্বোচ্চ k টি আলাদা অক্ষর”। আর একটি শর্ত জরুরি: শর্তটি একমুখী — বৈধ window-এর ছোট অংশও বৈধ (বা সব মান ধনাত্মক)। ঋণাত্মক সংখ্যা থাকলে window সাধারণত ভাঙে, তখন prefix sum.
Pseudocode (0-based) — variable window
L = 0
for R = 0 .. n-1:
add a[R] to the window
while window is invalid: // shrink from the left
remove a[L]; L = L + 1
update answer with window a[L..R]সমাধান-করা সমস্যা · পুনরাবৃত্তিহীন দীর্ঘতম অংশ (মাঝারি): “একটি string দেওয়া আছে। এমন সবচেয়ে লম্বা টানা অংশ বের করুন, যেখানে কোনো অক্ষর দুবার নেই।” Brute force: প্রতিটি শুরু i থেকে ডানে বাড়াতে থাকুন, প্রথম পুনরাবৃত্তিতে থামুন — O(n²) (প্রতিটি অংশ নতুন করে যাচাই করলে O(n³))। উন্নতি: window-এর ডান প্রান্ত R বাড়ান; নতুন অক্ষরটি window-এর ভেতরে আগে থাকলে L-কে সেই আগের অবস্থানের ঠিক পরে লাফিয়ে দিন। প্রতিটি অক্ষরের শেষ দেখা index একটি array-তে রাখলে লাফটা O(1).
নিচে s = "abcbad"-এ অ্যালগরিদমটি চালানো হলো। “last[c] আগে” মানে নতুন অক্ষরটি এর আগে শেষবার কোথায় দেখা গিয়েছিল:
| R (ঢুকল) | |
| last[c] আগে | |
| L | |
| window | |
| সেরা |
- R = 0, 'a' ঢুকল। 'a' আগে দেখা যায়নি (last = −1). window "a", দৈর্ঘ্য R − L + 1 = 1 — সেরা 1.
- R = 1, 'b'. নতুন অক্ষর, window "ab" — সেরা 2.
- R = 2, 'c'. নতুন অক্ষর, window "abc" — সেরা 3.
- R = 3, আবার 'b'! আগের 'b' ছিল index 1-এ, যা window-এর ভেতরে (1 ≥ L = 0). তাই L লাফিয়ে যায় 1 + 1 = 2-এ — window "cb". একটি ধাপেই L দুই ঘর সরল, কিন্তু কখনো পিছনে ফেরে না। সেরা 3-ই থাকল।
- R = 4, আবার 'a' — ফাঁদ! আগের 'a' index 0-এ, কিন্তু সেটি এখন window-এর বাইরে (0 < L = 2). তাই L নড়বে না। যে কোড শুধু
L = last[c] + 1লেখে (শর্ত ছাড়া), সে এখানে L-কে 1-এ পিছিয়ে আনবে, আর window "bcba"-য় দুটি 'b' ঢুকে যাবে — ভুল উত্তর। window "cba", দৈর্ঘ্য 3. - R = 5, 'd'. নতুন অক্ষর, window "cbad", দৈর্ঘ্য 4 — নতুন সেরা। শেষ উত্তর 4 ("cbad"). মোট ৬টি ধাপ, প্রতিটি O(1).
#include <iostream>
#include <string>
#include <vector>
#include <initializer_list>
using namespace std;
// length and start of the longest substring with no repeated character
pair<int, int> longestUnique(const string& s) {
vector<int> last(256, -1); // last index where each byte was seen
int L = 0, best = 0, start = 0;
for (int R = 0; R < (int)s.size(); ++R) {
unsigned char c = s[R];
if (last[c] >= L) L = last[c] + 1; // repeat inside the window: jump L
last[c] = R;
if (R - L + 1 > best) { best = R - L + 1; start = L; }
}
return {best, start};
}
int brute(const string& s) { // O(n^2): grow from every start
int best = 0;
for (size_t i = 0; i < s.size(); ++i) {
vector<bool> seen(256, false);
for (size_t j = i; j < s.size() && !seen[(unsigned char)s[j]]; ++j) {
seen[(unsigned char)s[j]] = true;
best = max(best, int(j - i + 1));
}
}
return best;
}
int main() {
for (string s : {"abcbad", "abcabcbb", "bbbbb", "", "abba", "tmmzuxt"}) {
auto [len, st] = longestUnique(s);
cout << '"' << s << "\" -> " << len << " \"" << s.substr(st, len)
<< "\" brute=" << brute(s) << '\n';
}
}আউটপুট:
"abcbad" -> 4 "cbad" brute=4
"abcabcbb" -> 3 "abc" brute=3
"bbbbb" -> 1 "b" brute=1
"" -> 0 "" brute=0
"abba" -> 2 "ab" brute=2
"tmmzuxt" -> 5 "mzuxt" brute=5
প্রথম লাইনটি ওপরের stepper-এর ফল। প্রতিটি ইনপুটে দ্রুত সমাধান আর O(n²) brute force একই উত্তর দিয়েছে। "abba" আর "tmmzuxt" হলো সেই ফাঁদের কেস — শেষ 'a' বা শেষ 't'-এর আগের অবস্থান window-এর বাইরে, তাই if (last[c] >= L) শর্তটি না থাকলে উত্তর ভুল আসত। খালি string-এ লুপ ঢোকেই না, উত্তর 0. unsigned char-এ রূপান্তর জরুরি: সাধারণ char ঋণাত্মক হতে পারে (যেমন UTF-8-এর বাংলা অক্ষরের byte), তখন last[c] সীমার বাইরে পড়ত।
def longest_unique(s):
"""Return (length, start) of the longest substring with no repeated character."""
last = {} # char -> last index where it was seen
L = best = start = 0
for R, c in enumerate(s):
if last.get(c, -1) >= L: # repeat inside the window: jump L
L = last[c] + 1
last[c] = R
if R - L + 1 > best:
best, start = R - L + 1, L
return best, start
def brute(s): # O(n^2): grow from every start
best = 0
for i in range(len(s)):
seen = set()
for j in range(i, len(s)):
if s[j] in seen:
break
seen.add(s[j])
best = max(best, j - i + 1)
return best
for s in ["abcbad", "abcabcbb", "bbbbb", "", "abba", "tmmzuxt"]:
n, st = longest_unique(s)
print(f'"{s}" -> {n} "{s[st:st + n]}" brute={brute(s)}')আউটপুট C++-এর সাথে হুবহু এক:
"abcbad" -> 4 "cbad" brute=4
"abcabcbb" -> 3 "abc" brute=3
"bbbbb" -> 1 "b" brute=1
"" -> 0 "" brute=0
"abba" -> 2 "ab" brute=2
"tmmzuxt" -> 5 "mzuxt" brute=5
Python-এ last একটি dict, তাই বর্ণমালার আকার নিয়ে ভাবতে হয় না — বাংলা অক্ষরসহ যেকোনো Unicode code point চলে। last.get(c, -1) না-দেখা অক্ষরে −1 দেয়। মেমরি O(min(n, Σ))।
জটিলতা: R প্রতিটি ধাপে ১ বাড়ে, আর L কখনো কমে না — মোট n টি ধাপ, প্রতিটি O(1): সময় Θ(n). মেমরি O(Σ) (C++-এ ২৫৬ ঘর)। “ভেতরে while থাকলে O(n²)” — এই সাধারণ ভুলটি এড়াতে মনে রাখুন: L-এর মোট সরণ n-এর বেশি হতে পারে না, এটাই amortized হিসাব।
অনু ৩.১ধনাত্মক সংখ্যার array [2, 3, 1, 2, 4, 3]-এ যোগফল ≥ 7 এমন সবচেয়ে ছোট টানা অংশের দৈর্ঘ্য কত? (মাঝারি)
Hint: R দিয়ে যোগ করুন; যোগফল ≥ 7 থাকা পর্যন্ত উত্তর হালনাগাদ করে L দিয়ে ছাঁটুন।
উত্তর: 2 — অংশ [4, 3]. সময় O(n), মেমরি O(1). কোনো অংশ না থাকলে (যেমন [1, 1, 1], লক্ষ্য 5) 0 দিন — শুরুতে উত্তর “অসীম” রেখে শেষে যাচাই করুন। ঋণাত্মক সংখ্যা থাকলে এই window ভুল, কারণ ছাঁটলে যোগফল বাড়তেও পারে; তখন prefix sum আর monotonic deque লাগে।
অনু ৩.২একটি চায়ের দোকানের ৭ দিনের বিক্রি [5, 1, 3, 7, 2, 6, 4]. টানা ৩ দিনের সর্বোচ্চ বিক্রি কত? (সহজ)
Hint: fixed window — নতুন দিন যোগ, তিন দিন আগের দিন বিয়োগ।
উত্তর: window-এর যোগফল ক্রমে 9, 11, 12, 15, 12 — সর্বোচ্চ 15 (7 + 2 + 6). সময় O(n), প্রতিটি window-এ নতুন করে যোগ করলে O(n · k).
প্যাটার্ন ৪ · Prefix sum + hash map — “আগের কোন যোগফল দরকার?”
রিভিশন: পর্ব ১৭ · Prefix sum আর difference array, পর্ব ৩২
চেনার সংকেত: “টানা অংশ যার যোগফল ঠিক k” (বিশেষ করে ঋণাত্মক থাকলে), “k দিয়ে বিভাজ্য”, “০ আর ১ সমান সংখ্যক”, “বারবার রেঞ্জের যোগফল”। সূত্রটি মনে রাখুন: sum(a[i..j]) = P[j+1] − P[i] — অংশের প্রশ্ন দুটি prefix-এর পার্থক্যের প্রশ্নে বদলে যায়।
সমাধান-করা সমস্যা হলো অংশ ১-এর লাভ-ক্ষতির খাতা — stepper-এ সাত ধাপ দেখেছেন। এখন পুরো কোড, ছয়টি ইনপুট আর ৫০০টি এলোমেলো ইনপুটে brute force-এর সাথে মিলিয়ে:
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
long long countFast(const vector<int>& a, long long k) { // O(n) on average
unordered_map<long long, long long> seen{{0, 1}}; // prefix sum -> count
long long pre = 0, ans = 0;
for (int x : a) {
pre += x;
if (auto it = seen.find(pre - k); it != seen.end()) ans += it->second;
++seen[pre]; // count AFTER looking up
}
return ans;
}
long long countBrute(const vector<int>& a, long long k) { // O(n^2)
long long ans = 0;
for (size_t i = 0; i < a.size(); ++i) {
long long s = 0;
for (size_t j = i; j < a.size(); ++j) ans += (s += a[j]) == k;
}
return ans;
}
int main() {
vector<pair<vector<int>, int>> tests = {
{{1, 1, 1}, 2}, {{3, 4, 7, 2, -3, 1, 4, 2}, 7}, {{0, 0, 0}, 0},
{{5}, 5}, {{-1, -1, 1}, 0}, {{}, 0}};
for (auto& [a, k] : tests)
cout << "k=" << k << ": fast=" << countFast(a, k) << " brute=" << countBrute(a, k) << '\n';
unsigned seed = 12345; int bad = 0; // deterministic stress test
for (int t = 0; t < 500; ++t) {
vector<int> a(t % 9);
for (int& x : a) { seed = seed * 1103515245u + 12345u; x = int(seed >> 16) % 7 - 3; }
bad += countFast(a, t % 5 - 2) != countBrute(a, t % 5 - 2);
}
cout << "stress: 500 random arrays, mismatches = " << bad << '\n';
}আউটপুট:
k=2: fast=2 brute=2
k=7: fast=4 brute=4
k=0: fast=6 brute=6
k=5: fast=1 brute=1
k=0: fast=1 brute=1
k=0: fast=0 brute=0
stress: 500 random arrays, mismatches = 0
[0, 0, 0]-এ k = 0 হলে উত্তর 6 — তিনটি এক-ঘরের, দুটি দুই-ঘরের আর একটি তিন-ঘরের অংশ। [−1, −1, 1]-এ শুধু [−1, 1] অংশটির যোগফল 0. শেষ লাইনটি একটি stress test: −3 থেকে 3-এর মধ্যে মান নিয়ে ৫০০টি ছোট এলোমেলো array (নিজস্ব সরল random generator, তাই প্রতিবার একই ইনপুট), প্রতিটিতে দ্রুত আর ধীর সমাধানের ফল মেলানো হয়েছে — একটিও অমিল নেই। if (auto it = ...; ...) হলো C++17-এর if-with-initializer, যাতে it শুধু ওই if-এর ভেতরে থাকে।
- জটিলতা: সময় গড়ে O(n), মেমরি O(n) (সব prefix আলাদা হলে map-এ n + 1টি key).
- সবচেয়ে সাধারণ বাগ:
seen{{0, 1}}ভুলে যাওয়া — তখন index 0 থেকে শুরু হওয়া অংশগুলো হারায়। দ্বিতীয় বাগ: আগে++seen[pre], পরে খোঁজা — k = 0-তে প্রতিটি prefix নিজের সাথে মিলে খালি অংশ গুনে ফেলে। - Overflow: যোগফল আর উত্তর দুটোই
long long— n দুই লাখ, প্রতিটি মান 109 পর্যন্ত হলে যোগফল 2 · 1014.
অনু ৪.১[4, 5, 0, −2, −3, 1]-এ কতগুলো টানা অংশের যোগফল 5 দিয়ে বিভাজ্য? C++-এ ঋণাত্মক সংখ্যার ফাঁদটি কী? (মাঝারি)
Hint: দুটি prefix-এর ভাগশেষ সমান হলে তাদের মাঝের অংশ বিভাজ্য। প্রতিটি ভাগশেষ কতবার এসেছে গুনুন।
উত্তর: 7টি (brute force দিয়েও যাচাই করা)। cnt[0] = 1 দিয়ে শুরু, প্রতিটি prefix-এর ভাগশেষ r-এ ans += cnt[r]; ++cnt[r]. ফাঁদ: C++-এ -2 % 5 হয় −2, 3 নয়। তাই r = ((pre % k) + k) % k লিখুন। Python-এর % নিজেই অঋণাত্মক ফল দেয়। সময় O(n), মেমরি O(k).
অনু ৪.২[1, 7, 3, 6, 5, 6]-এ এমন index বের করুন যার বাঁয়ের সব সংখ্যার যোগফল ডানের সব সংখ্যার যোগফলের সমান। (সহজ)
Hint: মোট যোগফল S আগে বের করুন; বাঁয়ের যোগফল left হলে ডানেরটা S − left − a[i].
উত্তর: index 3 (বাঁয়ে 1 + 7 + 3 = 11, ডানে 5 + 6 = 11). S = 28. সময় O(n), মেমরি O(1) — পুরো prefix array রাখার দরকার নেই।
৪ · উত্তরের ওপর binary search আর monotonic stack
প্যাটার্ন ৫ · Binary search on answer — “এই মানে কি সম্ভব?”
রিভিশন: পর্ব ১৮ · Binary search, পর্ব ১৯ · উত্তরের ওপর binary search
চেনার সংকেত: “সর্বোচ্চটির সর্বনিম্ন মান” (minimize the maximum), “সর্বনিম্নটির সর্বোচ্চ”, “কমপক্ষে কত গতি/ক্ষমতা লাগবে যাতে D দিনে শেষ হয়”, আর উত্তরের পরিসর বিশাল (109) কিন্তু একটি নির্দিষ্ট মান দিলে “হবে কি না” সহজে যাচাই করা যায়। শর্ত: সম্ভাব্যতা একমুখী — c-তে সম্ভব হলে c + 1-এও সম্ভব।
Pseudocode — first value where feasible() becomes true
lo = smallest possible answer, hi = an answer that surely works
while lo < hi:
mid = lo + (hi - lo) / 2
if feasible(mid): hi = mid // mid works: answer is mid or smaller
else: lo = mid + 1 // mid fails: answer is bigger
return loসমাধান-করা সমস্যা · বন্দরে কার্টন পাঠানো (মাঝারি): “একটি গার্মেন্টস কারখানা থেকে n টি কার্টন এই ক্রমেই চট্টগ্রাম বন্দরে পাঠাতে হবে; প্রতিটির ওজন দেওয়া আছে। একটি ট্রাক দিনে একবার যায় আর সর্বোচ্চ cap কেজি নেয়। D দিনের মধ্যে সব পাঠাতে ট্রাকের ক্ষমতা কমপক্ষে কত হতে হবে?”
- যাচাই সহজ: একটি নির্দিষ্ট cap দিলে greedy-ভাবে ভরে দেখা যায় কত দিন লাগে — যতক্ষণ জায়গা আছে আজকের ট্রাকে তুলুন, না হলে পরের দিন। O(n). (Greedy এখানে নিরাপদ: আজকে যত বেশি পাঠানো যায়, পরের দিনগুলোর কাজ তত কম।)
- Brute force: cap = সবচেয়ে ভারী কার্টন থেকে এক এক করে বাড়িয়ে প্রথম সম্ভব মান — O(n · (S − max)), যেখানে S মোট ওজন। ওজন লাখের ঘরে হলে অচল।
- সেরা: cap যত বাড়ে, দিন তত কমে বা সমান থাকে — একমুখী। তাই [max, S] পরিসরে binary search: O(n log S).
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;
// can all cartons (in order) be sent within `days` if a truck carries `cap` per day?
bool feasible(const vector<int>& w, long long cap, int days) {
int used = 1; long long load = 0;
for (int x : w) {
if (load + x > cap) { ++used; load = 0; } // start a new day
load += x;
}
return used <= days;
}
long long minCapacity(const vector<int>& w, int days) {
long long lo = *max_element(w.begin(), w.end()); // must fit the heaviest
long long hi = accumulate(w.begin(), w.end(), 0LL); // one day carries all
while (lo < hi) { // first cap where feasible() becomes true
long long mid = lo + (hi - lo) / 2;
if (feasible(w, mid, days)) hi = mid; else lo = mid + 1;
}
return lo;
}
int main() {
vector<pair<vector<int>, int>> tests = {{{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 5},
{{3, 2, 2, 4, 1, 4}, 3}, {{1, 2, 3, 1, 1}, 4}, {{7, 2, 5, 10, 8}, 1}, {{9}, 3}};
for (auto& [w, d] : tests) {
long long brute = *max_element(w.begin(), w.end()); // linear scan upward
while (!feasible(w, brute, d)) ++brute;
cout << "days=" << d << ": binary=" << minCapacity(w, d) << " linear=" << brute << '\n';
}
}আউটপুট:
days=5: binary=15 linear=15
days=3: binary=6 linear=6
days=4: binary=3 linear=3
days=1: binary=32 linear=32
days=3: binary=9 linear=9
১ থেকে ১০ পর্যন্ত ওজন ৫ দিনে পাঠাতে ক্ষমতা 15 লাগে ([1..5], [6, 7], [8], [9], [10]). D = 1 হলে উত্তর মোট ওজন 32, আর একটিমাত্র কার্টনে (9) উত্তর তার নিজের ওজন — দিন বেশি থাকলেও কম ক্ষমতায় চলে না, তাই lo শুরু হয় সবচেয়ে ভারী কার্টন থেকে। প্রতিটি ইনপুটে binary search আর ধীর রৈখিক খোঁজ একই ফল দিয়েছে। lo + (hi - lo) / 2 লিখলে বড় মানে lo + hi-এর overflow এড়ানো যায়।
- সবচেয়ে সাধারণ বাগ:
while (lo <= hi)আরhi = midএকসাথে — lo == hi হলে চিরকাল ঘোরে। টেমপ্লেট একটি বেছে নিয়ে সবসময় সেটাই লিখুন। আরেকটি:lo1 থেকে শুরু করা — তখন feasible()-এ সবচেয়ে ভারী কার্টন কোনো দিনেই আঁটে না, অথচ কোড দিন গুনতে থাকে। - প্রমাণের এক লাইন: feasible একমুখী, তাই মানগুলো দেখতে F F F … T T T; লুপের invariant “উত্তর [lo, hi]-এর মধ্যে” প্রতিটি ধাপে টেকে, আর পরিসর প্রতিবার অন্তত অর্ধেক হয়।
অনু ৫.১একজন পরীক্ষকের কাছে চারটি বান্ডিলে [3, 6, 7, 11]টি খাতা। প্রতি ঘণ্টায় তিনি একটি বান্ডিল থেকে সর্বোচ্চ k টি খাতা দেখেন (বান্ডিলে কম থাকলে সেই ঘণ্টার বাকিটা বসে থাকেন)। ৮ ঘণ্টায় শেষ করতে k কমপক্ষে কত? (মাঝারি)
Hint: k নির্দিষ্ট হলে মোট ঘণ্টা = Σ ⌈x / k⌉। k বাড়লে ঘণ্টা কমে — একমুখী।
উত্তর: k = 4 (1 + 2 + 2 + 3 = 8 ঘণ্টা; k = 3-এ 1 + 2 + 3 + 4 = 10). পরিসর [1, max] = [1, 11]-এ binary search, সময় O(n log max). ⌈x / k⌉ পূর্ণসংখ্যায় লিখুন (x + k - 1) / k. আরেকটি ইনপুট দেখুন: [30, 11, 23, 4, 20]-এ ৫ ঘণ্টা দিলে উত্তর সবচেয়ে বড় বান্ডিলটি (30) — ঘণ্টার সংখ্যা h বান্ডিলের সংখ্যা n-এর সমান হলে উত্তর সবসময় max.
অনু ৫.২একটি সোজা রাস্তায় সম্ভাব্য জায়গা [1, 2, 4, 8, 9] কিলোমিটারে। তিনটি ATM বুথ বসাতে হবে, যাতে যেকোনো দুটি বুথের সবচেয়ে কম দূরত্বটি যত বেশি সম্ভব হয়। সেই দূরত্ব কত? (কঠিন)
Hint: “সর্বনিম্নটির সর্বোচ্চ” — দূরত্ব d দিলে বাঁ থেকে greedy-ভাবে বসিয়ে দেখুন অন্তত ৩টি আঁটে কি না। d বাড়লে কঠিন হয়, তাই এবার খুঁজছেন শেষ সম্ভব মান।
উত্তর: 3 (বুথ 1, 4, 8 বা 1, 4, 9-এ)। d = 4-এ 1, 8 — মাত্র দুটি। সময় O(n log n + n log(range)): প্রথমে sort, তারপর প্রতিটি যাচাই O(n). টেমপ্লেট উল্টো দিকের: mid = lo + (hi - lo + 1) / 2, সম্ভব হলে lo = mid, না হলে hi = mid - 1 — ওপরের দিকে গোল না করলে lo = mid-এ লুপ আটকে যায়।
প্যাটার্ন ৬ · Monotonic stack — “ডানে প্রথম বড় কে?”
রিভিশন: পর্ব ১৩ · Stack
চেনার সংকেত: “প্রতিটি উপাদানের ডানে/বাঁয়ে প্রথম বড়/ছোট”, “কত দিন পরে বেশি গরম পড়বে”, “stock span”, “histogram-এ সবচেয়ে বড় আয়তক্ষেত্র”, “বৃষ্টির পানি জমা”। brute force-এ প্রতিটি উপাদান থেকে ডানে হাঁটতে হয় — O(n²); monotonic stack সেটাকে O(n) করে।
মূল সমস্যা · Next greater element: “প্রতিদিনের একটি শেয়ারের দাম দেওয়া আছে। প্রতিটি দিনের জন্য বলুন, তার পরে প্রথম যেদিন দাম বেশি, সেদিনের দাম কত; না থাকলে −1.” ধারণা: stack-এ সেই দিনগুলো রাখুন যাদের উত্তর এখনো জানা নেই। তাদের দাম ওপর থেকে নিচে বাড়তে থাকে (নিচে বড়)। নতুন দিনের দাম stack-এর মাথার চেয়ে বড় হলে মাথার উত্তর পাওয়া গেল — pop করুন, আবার দেখুন।
- i = 0, মান 4. stack খালি, কাউকে উত্তর দেওয়ার নেই। 4-কে stack-এ রাখি — তার উত্তর এখনো অজানা।
- i = 1, মান 2. 2 < 4, তাই 4-এর উত্তর 2 নয়। 2-কে ওপরে রাখি। stack ওপর থেকে নিচে 2, 4 — বাড়ছে, এটাই monotonic অবস্থা।
- i = 2, মান 5. 5 > 2 → ans[1] = 5, pop. আবার 5 > 4 → ans[0] = 5, pop. stack খালি, 5-কে রাখি। একটি ধাপে দুটি pop — কিন্তু প্রতিটি index জীবনে একবারই pop হয়।
- i = 3, মান 3. 3 < 5, কারও উত্তর মেলেনি। 3 ওপরে যায়: stack [5, 3].
- i = 4, মান 1. 1 < 3, আবার কিছু pop হয় না: stack [5, 3, 1]. তিনজন অপেক্ষায়।
- i = 5, মান 6. 6 একে একে 1, 3, 5 — তিনজনেরই উত্তর: ans[4] = ans[3] = ans[2] = 6. stack-এ শুধু 6.
- i = 6, মান 2, শেষ। 2 < 6, 2-কে রাখি। array শেষ, stack-এ যারা রয়ে গেল (6 আর 2) তাদের ডানে বড় কেউ নেই — উত্তর −1. ফল [5, 5, 6, 6, 6, −1, −1]. মোট push 7টি, pop 5টি।
vector<int> nextGreater(const vector<int>& a) {
vector<int> ans(a.size(), -1), st; // st: indices, values decreasing
for (int i = 0; i < (int)a.size(); ++i) {
while (!st.empty() && a[st.back()] < a[i]) { ans[st.back()] = a[i]; st.pop_back(); }
st.push_back(i);
}
return ans;
}এই ফাংশন [4, 2, 5, 3, 1, 6, 2]-এ চালিয়ে 5 5 6 6 6 -1 -1 পাওয়া গেছে — stepper-এর সাথে মেলে। জটিলতা: প্রতিটি index ঠিক একবার push আর সর্বোচ্চ একবার pop হয়, তাই ভেতরের while সব মিলিয়ে সর্বোচ্চ n বার চলে — সময় Θ(n), মেমরি O(n). stack-এ মান না রেখে index রাখুন: তাহলে “কত দিন পরে” (দূরত্ব) প্রশ্নেও একই কোড চলে।
সমাধান-করা সমস্যা · সবচেয়ে বড় বিলবোর্ড (কঠিন): “একটি রাস্তার পাশে পাশাপাশি n টি ভবন, প্রতিটির প্রস্থ ১, উচ্চতা দেওয়া আছে। ভবনগুলোর গায়ে লাগিয়ে একটি আয়তাকার বিলবোর্ড বসাতে হবে, যা কোনো ভবনের ছাদ ছাড়িয়ে যাবে না। সবচেয়ে বড় ক্ষেত্রফল কত?” এটি বিখ্যাত largest rectangle in histogram.
- Brute force: প্রতিটি [i..j] অংশে সবচেয়ে নিচু ভবন × প্রস্থ। চলমান minimum রাখলে O(n²)।
- মূল পর্যবেক্ষণ: সেরা আয়তক্ষেত্রের উচ্চতা কোনো একটি ভবন h[t]-এর সমান। সেই উচ্চতায় আয়তক্ষেত্রটি বাঁয়ে প্রথম নিচু ভবন পর্যন্ত আর ডানে প্রথম নিচু ভবন পর্যন্ত ছড়াতে পারে — অর্থাৎ next smaller element দুই দিকে।
- সেরা: উচ্চতা বাড়ন্ত ক্রমে index-এর stack. নতুন ভবন মাথার চেয়ে নিচু (বা সমান) হলে মাথার ডান সীমা পাওয়া গেল (i), আর stack-এ তার নিচেরটি বাঁ সীমা। শেষে উচ্চতা 0-এর একটি sentinel ভবন সবাইকে বের করে আনে। O(n).
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
long long largestRectangle(vector<int> h) {
h.push_back(0); // sentinel flushes the stack at the end
vector<int> st; long long best = 0; // indices, heights increasing
for (int i = 0; i < (int)h.size(); ++i) {
while (!st.empty() && h[st.back()] >= h[i]) {
long long height = h[st.back()]; st.pop_back();
int left = st.empty() ? -1 : st.back(); // first lower bar on the left
best = max(best, height * (i - left - 1));
}
st.push_back(i);
}
return best;
}
long long bruteRect(const vector<int>& h) { // O(n^2): every [i..j], running min
long long best = 0;
for (int i = 0; i < (int)h.size(); ++i) {
int mn = h[i];
for (int j = i; j < (int)h.size(); ++j) {
mn = min(mn, h[j]);
best = max(best, 1LL * mn * (j - i + 1));
}
}
return best;
}
int main() {
vector<vector<int>> tests = {{2, 1, 5, 6, 2, 3}, {2, 4}, {6, 2, 5, 4, 5, 1, 6}, {3, 3, 3}, {5}, {}};
for (auto& h : tests)
cout << "rect=" << largestRectangle(h) << " brute=" << bruteRect(h) << '\n';
}আউটপুট:
rect=10 brute=10
rect=4 brute=4
rect=12 brute=12
rect=9 brute=9
rect=5 brute=5
rect=0 brute=0
[2, 1, 5, 6, 2, 3]-এ সেরা হলো উচ্চতা 5, প্রস্থ 2 (ভবন 5 আর 6) = 10. [6, 2, 5, 4, 5, 1, 6]-এ উচ্চতা 4, প্রস্থ 3 = 12. সব সমান [3, 3, 3]-এ পুরো প্রস্থ: 9 — এখানে >= দিয়ে সমান উচ্চতাও pop করা হয়; তখন প্রথম 3-এর আয়তক্ষেত্র ভুলভাবে ছোট হিসাব হয়, কিন্তু শেষে pop হওয়া 3 পুরো প্রস্থ পায়, তাই সর্বোচ্চটি ঠিক থাকে। খালি তালিকায় 0. প্রতিটি কেসে brute force একই উত্তর দিয়েছে। ক্ষেত্রফল long long, কারণ উচ্চতা 109 আর প্রস্থ 105 হলে গুণফল int-এ আঁটে না।
অনু ৬.১ঢাকার ৭ দিনের তাপমাত্রা [30, 32, 31, 29, 33, 28, 35]. প্রতিটি দিনের জন্য বলুন, কত দিন পরে প্রথম আরও গরম দিন আসবে (না এলে 0). (মাঝারি)
Hint: next greater element, কিন্তু মানের বদলে দূরত্ব — stack-এ index রাখুন, pop-এর সময় ans[j] = i - j.
উত্তর: [1, 3, 2, 1, 2, 1, 0]. যেমন দ্বিতীয় দিন (32)-এর পরে প্রথম বেশি গরম পঞ্চম দিন (33) — ৩ দিন পরে। সময় O(n), মেমরি O(n).
অনু ৬.২শেয়ারের দাম [100, 80, 60, 70, 60, 75, 85]. প্রতিটি দিনের “span” — আজসহ টানা কত দিন ধরে দাম আজকের দামের সমান বা কম ছিল — বের করুন। (মাঝারি)
Hint: এবার বাঁয়ে প্রথম বড় দাম খুঁজছেন। stack-এ index রাখুন; আজকের দামের সমান বা কম যারা, তাদের pop করুন।
উত্তর: [1, 1, 1, 2, 1, 4, 6]. যেমন 75-এর দিন: আগের 60, 70, 60 সবাই ≤ 75, তার আগে 80 বড় — span 4. শর্ত <= দিয়ে pop না করলে সমান দামের দিনগুলো span-এ বাদ পড়বে। সময় O(n).
৫ · Heap আর interval — সেরা k টি আর সময়ের পরিসর
প্যাটার্ন ৭ · Heap / top-k — “সবচেয়ে বড়/ছোট k টি”
রিভিশন: পর্ব ২৩ · Heap আর priority queue, পর্ব ২২ · Quickselect
চেনার সংকেত: “k তম বড়”, “সবচেয়ে কাছের k টি”, “সবচেয়ে বেশি আসা k টি”, “k টি সাজানো তালিকা মেলান”, “চলমান median”, “সবসময় সবচেয়ে ছোটটি বের করুন” (Dijkstra, ঘটনা-নির্ভর simulation). সব সাজালে O(n log n); heap দিয়ে O(n log k) — k ছোট আর n বিশাল হলে বড় পার্থক্য।
সমাধান-করা সমস্যা ১ · সবচেয়ে কাছের k জন রাইডার (মাঝারি): “একটি খাবার ডেলিভারি অ্যাপে রেস্তোরাঁ (0, 0) বিন্দুতে। n জন রাইডারের অবস্থান (x, y) দেওয়া আছে। সবচেয়ে কাছের k জনকে বের করুন।” কৌশলটি উল্টো শোনায়: সবচেয়ে কাছের k জনের জন্য max-heap — heap-এ এ পর্যন্ত সেরা k জন, আর মাথায় তাদের মধ্যে সবচেয়ে দূরের জন। নতুন কেউ এলে ঢোকান; আকার k ছাড়ালে মাথা (সবচেয়ে দূরের) ফেলে দিন। দূরত্বের বর্গ তুলনা করুন — sqrt-এর দরকার নেই, আর floating-point-এর ভুলও থাকে না।
সমাধান-করা সমস্যা ২ · k টি শাখার সাজানো তালিকা মেলানো (মাঝারি): “একটি ব্যাংকের k টি শাখার প্রতিটি থেকে সময় অনুযায়ী সাজানো লেনদেনের তালিকা এসেছে। সব মিলিয়ে একটি সাজানো তালিকা বানান।” প্রতিটি তালিকার প্রথম উপাদান একটি min-heap-এ রাখুন; সবচেয়ে ছোটটি বের করে আউটপুটে দিন, আর যে তালিকা থেকে এসেছিল তার পরেরটি heap-এ ঢোকান — এটাই k-way merge.
#include <iostream>
#include <vector>
#include <queue>
#include <tuple>
using namespace std;
// k points closest to (0,0): keep a MAX-heap of size k, O(n log k)
vector<pair<int, int>> kClosest(const vector<pair<int, int>>& pts, int k) {
priority_queue<tuple<long long, int, int>> heap; // (dist^2, x, y), largest on top
for (auto [x, y] : pts) {
heap.emplace(1LL * x * x + 1LL * y * y, x, y);
if ((int)heap.size() > k) heap.pop(); // drop the farthest
}
vector<pair<int, int>> out;
for (; !heap.empty(); heap.pop()) out.push_back({get<1>(heap.top()), get<2>(heap.top())});
return out; // farthest of the k first
}
// merge k sorted lists with a MIN-heap of (value, list, index), O(N log k)
vector<int> mergeK(const vector<vector<int>>& lists) {
priority_queue<tuple<int, int, int>, vector<tuple<int, int, int>>, greater<>> heap;
for (int i = 0; i < (int)lists.size(); ++i)
if (!lists[i].empty()) heap.emplace(lists[i][0], i, 0);
vector<int> out;
while (!heap.empty()) {
auto [v, i, j] = heap.top(); heap.pop();
out.push_back(v);
if (j + 1 < (int)lists[i].size()) heap.emplace(lists[i][j + 1], i, j + 1);
}
return out;
}
int main() {
for (auto [x, y] : kClosest({{3, 3}, {5, -1}, {-2, 4}, {1, 1}, {0, -2}, {4, 0}}, 3))
cout << "(" << x << "," << y << ") ";
cout << '\n';
for (int v : mergeK({{1, 4, 9}, {2, 3, 10, 12}, {}, {5}})) cout << v << ' ';
cout << '\n';
}আউটপুট:
(4,0) (0,-2) (1,1)
1 2 3 4 5 9 10 12
দূরত্বের বর্গ: (3, 3) → 18, (5, −1) → 26, (−2, 4) → 20, (1, 1) → 2, (0, −2) → 4, (4, 0) → 16. সবচেয়ে কাছের তিনজন (1, 1), (0, −2), (4, 0); max-heap থেকে বের করার সময় দূরের জন আগে আসে, তাই আউটপুটের ক্রম উল্টো। দ্বিতীয় লাইনে চারটি তালিকা (একটি খালি) মিলে একটি সাজানো তালিকা। greater<> দিলে priority_queue min-heap হয়; tuple-এর তুলনা প্রথম উপাদান দিয়ে, সমান হলে পরেরটি দিয়ে।
- জটিলতা: k closest — সময় O(n log k), মেমরি O(k). Merge — মোট N টি উপাদান, heap-এ সর্বোচ্চ k টি, সময় O(N log k), heap-এর মেমরি O(k). জোড়ায় জোড়ায় মেলালে (প্রথম দুটি, তারপর ফলের সাথে তৃতীয়টি …) সবচেয়ে খারাপ ক্ষেত্রে O(N · k).
- বিকল্প: শুধু k টি চাই, ক্রম লাগবে না — quickselect /
nth_element, গড়ে O(n). - Edge case: k = 0, k ≥ n, খালি তালিকা, সমান দূরত্ব (কোনটি নেবেন? ইন্টারভিউয়ারকে জিজ্ঞেস করুন)।
অনু ৭.১[3, 2, 1, 5, 6, 4]-এ দ্বিতীয় বৃহত্তম সংখ্যা কত? k তম বৃহত্তমের জন্য কোন heap, কত আকারের? (সহজ)
Hint: আকার k-এর একটি min-heap রাখুন — মাথায় থাকবে সেরা k জনের মধ্যে সবচেয়ে ছোটটি, অর্থাৎ k তম বৃহত্তম।
উত্তর: 5. সময় O(n log k), মেমরি O(k). বিকল্প quickselect গড়ে O(n), কিন্তু সবচেয়ে খারাপ ক্ষেত্রে O(n²) (random pivot নিলে সেটা খুব বিরল)।
অনু ৭.২[1, 1, 1, 2, 2, 3]-এ সবচেয়ে বেশিবার আসা ২টি সংখ্যা কী? heap ছাড়া O(n)-এ করা যায় কি? (মাঝারি)
Hint: আগে hash map-এ গুনুন। কোনো গণনা n-এর বেশি হতে পারে না।
উত্তর: [1, 2]. গণনার পরে আকার k-এর min-heap — O(n log k). অথবা bucket: bucket[c]-এ যাদের গণনা c, তাদের রাখুন; বড় c থেকে নামতে নামতে k জন নিন — O(n). ফলো-আপ প্রশ্ন প্রায়ই আসে: “গণনা সমান হলে কোনটি আগে?” — ছোট সংখ্যা, বা আগে আসা সংখ্যা; শর্তটি জিজ্ঞেস করে নিন।
প্যাটার্ন ৮ · Interval — sort করে একবার হেঁটে যান
রিভিশন: পর্ব ২৫ · Comparator, পর্ব ৫০ · Interval scheduling
চেনার সংকেত: ইনপুটে [শুরু, শেষ] জোড়া — appointment, ক্লাসের সময়, বুকিং, ছুটির দিন। “Overlap জোড়া লাগান”, “নতুনটি ঢোকান”, “কমপক্ষে কয়টি ঘর লাগবে”, “কমপক্ষে কয়টি বাদ দিলে আর overlap থাকবে না”। প্রায় সবসময় প্রথম কাজ: শুরু (বা শেষ) অনুযায়ী sort. শুরুতেই জিজ্ঞেস করুন: [1, 4] আর [4, 5] কি overlap? (শেষ সময় অন্তর্ভুক্ত, নাকি নয়)
সমাধান-করা সমস্যা · ডাক্তারের চেম্বারের সময়সূচি (মাঝারি): “একজন ডাক্তার দিনে কয়েকটি সময়-খণ্ডে রোগী দেখেন, কিন্তু তালিকায় খণ্ডগুলো এলোমেলো আর কিছু একটার ওপর আরেকটা পড়েছে। (ক) overlap থাকা খণ্ডগুলো জোড়া লাগিয়ে পরিষ্কার তালিকা বানান। (খ) একটি পরিষ্কার, সাজানো তালিকায় নতুন একটি খণ্ড ঢোকান।” এটি interval merging.
- Brute force: প্রতিটি জোড়া overlap কি না দেখে বারবার জোড়া লাগানো, যতক্ষণ বদল হয় — O(n²) বা খারাপ।
- সেরা (ক): শুরু অনুযায়ী sort. এখন overlap হলে সেটা শুধু ফলাফলের শেষ খণ্ডের সাথেই হতে পারে — কারণ আগের খণ্ডগুলো আরও আগে শেষ হয়েছে। O(n log n).
- সেরা (খ): তালিকা আগে থেকেই সাজানো, তাই তিন ভাগ: নতুনটির পুরো বাঁয়ে, overlap করা (জোড়া লাগান), পুরো ডানে। O(n), sort লাগে না।
vector<pair<int, int>> mergeIntervals(vector<pair<int, int>> v) {
sort(v.begin(), v.end());
vector<pair<int, int>> out;
for (auto [s, e] : v) {
if (!out.empty() && s <= out.back().second) out.back().second = max(out.back().second, e);
else out.push_back({s, e});
}
return out;
}
vector<pair<int, int>> insertInterval(const vector<pair<int, int>>& v, pair<int, int> x) {
vector<pair<int, int>> out; size_t i = 0;
while (i < v.size() && v[i].second < x.first) out.push_back(v[i++]); // fully left
while (i < v.size() && v[i].first <= x.second) { // overlapping
x.first = min(x.first, v[i].first); x.second = max(x.second, v[i].second); ++i;
}
out.push_back(x);
while (i < v.size()) out.push_back(v[i++]); // fully right
return out;
}চালিয়ে যাচাই: [9, 11], [1, 3], [2, 6], [8, 10], [15, 18], [6, 7] জোড়া লাগালে [1,7] [8,11] [15,18]; [1, 4], [4, 5] → [1,5] (ছুঁয়ে থাকা খণ্ডও জোড়া লাগে, কারণ শর্ত s <= end). [1, 2], [3, 5], [6, 7], [8, 10], [12, 16]-এ [4, 8] ঢোকালে [1,2] [3,10] [12,16]; খালি তালিকায় [5, 7] → [5,7]; [1, 5]-এ [6, 8] → [1,5] [6,8].
- সাধারণ বাগ: জোড়া লাগানোর সময়
out.back().second = eলেখা,maxছাড়া — [1, 10]-এর পরে [2, 3] এলে শেষ সময় ছোট হয়ে যায়। - সাধারণ বাগ ২: sort না করা, বা শেষ সময় অনুযায়ী sort করা — merge-এর জন্য শুরু অনুযায়ী সাজাতে হয়।
অনু ৮.১ক্লাসগুলোর সময় [0, 30], [5, 10], [15, 20]. একই সময়ে একটি ঘরে একটির বেশি ক্লাস চলতে পারে না। কমপক্ষে কয়টি ক্লাসরুম লাগবে? (মাঝারি)
Hint: সব শুরুকে +1, সব শেষকে −1 ঘটনা বানিয়ে সময় অনুযায়ী sort করুন (sweep line); চলমান যোগফলের সর্বোচ্চই উত্তর। বিকল্প: শুরু অনুযায়ী sort করে শেষ সময়ের min-heap.
উত্তর: ২টি — [0, 30] পুরো সময় একটি ঘর নেয়, বাকি দুটি একে একে অন্য ঘরে। সময় O(n log n). সমান সময়ে একটি শেষ আর আরেকটি শুরু হলে ([9, 10], [10, 11]) শেষটিকে আগে ধরুন — তখন ১টি ঘরই যথেষ্ট। এটাই সেই “শেষ সময় অন্তর্ভুক্ত কি না” প্রশ্ন।
অনু ৮.২[1, 2], [2, 3], [3, 4], [1, 3] থেকে কমপক্ষে কয়টি খণ্ড বাদ দিলে বাকিগুলোর মধ্যে কোনো overlap থাকবে না? (ছুঁয়ে থাকা overlap নয়) (মাঝারি)
Hint: উল্টো করে ভাবুন — সর্বোচ্চ কয়টি রাখা যায়? এটাই activity selection: শেষ সময় অনুযায়ী sort করে greedy.
উত্তর: ১টি ([1, 3] বাদ)। [1, 2], [2, 3], [3, 4] রাখা যায়। সময় O(n log n). শুরু অনুযায়ী sort করলে greedy ভুল হয় — লম্বা একটি খণ্ড আগে শুরু হয়ে অনেককে আটকে দিতে পারে। প্রমাণ পর্ব ৫০-এর “greedy stays ahead”.
৬ · Linked list আর tree — pointer আর recursion
প্যাটার্ন ৯ · Linked list-এর কৌশল — উল্টানো, মাঝখান আর চক্র
রিভিশন: পর্ব ১২ · Linked list
চেনার সংকেত: ইনপুট একটি linked list-এর head, আর শর্ত “বাড়তি মেমরি O(1)”. “উল্টান”, “মাঝের node”, “শেষ থেকে n তম”, “চক্র আছে কি / কোথায় শুরু”, “দুটি সাজানো list মেলান”, “palindrome list”. প্রায় সব সমাধান তিনটি কৌশলের একটি: তিন pointer দিয়ে উল্টানো, ধীর-দ্রুত pointer, আর dummy head.
সমাধান-করা সমস্যা · ট্রেনের বগি (মাঝারি): “একটি ট্রেনের বগিগুলো linked list-এর মতো একটির পর একটি জোড়া। (ক) মাঝের বগিটি খুঁজুন। (খ) পুরো ট্রেনটি উল্টো দিকে সাজান। (গ) কোনো ভুলে শেষ বগি যদি আগের কোনো বগির সাথে জুড়ে যায় (চক্র), তা ধরুন আর বলুন চক্রটি কোন বগি থেকে শুরু।” সবগুলোই O(1) বাড়তি মেমরিতে।
- Brute force: (ক) একবার গুনে n, তারপর n/2 ধাপ — দুবার হাঁটা, তবু O(n). (গ) দেখা node-গুলো একটি
unordered_set-এ রাখা — সময় O(n), কিন্তু মেমরি O(n). - মাঝখান: slow এক ধাপ, fast দুই ধাপ; fast শেষে পৌঁছালে slow ঠিক মাঝে। জোড় দৈর্ঘ্যে এই কোড দ্বিতীয় মাঝেরটি দেয় — কোনটি চাই, জিজ্ঞেস করুন।
- চক্র (Floyd-এর পদ্ধতি): চক্র থাকলে fast একসময় slow-কে ধরে ফেলে — প্রতি ধাপে তাদের ব্যবধান চক্রের ভেতরে ১ করে কমে। দেখা হওয়ার পর একটিকে head-এ ফিরিয়ে দুজনকেই এক ধাপ করে চালালে তারা চক্রের শুরুতে মেলে।
#include <iostream>
#include <vector>
#include <string>
using namespace std;
struct Node { int val; Node* next; };
Node* reverseList(Node* head) { // three pointers: prev, cur, next
Node* prev = nullptr;
while (head) { Node* nxt = head->next; head->next = prev; prev = head; head = nxt; }
return prev;
}
Node* middle(Node* head) { // slow 1 step, fast 2 steps
Node *slow = head, *fast = head;
while (fast && fast->next) { slow = slow->next; fast = fast->next->next; }
return slow; // even length: the second middle
}
Node* cycleEntry(Node* head) { // Floyd: meet, then restart one pointer
Node *slow = head, *fast = head;
while (fast && fast->next) {
slow = slow->next; fast = fast->next->next;
if (slow == fast) {
for (slow = head; slow != fast; slow = slow->next, fast = fast->next) {}
return slow;
}
}
return nullptr;
}
int main() {
vector<Node> pool(6); // nodes live in a vector: no new/delete needed
for (int i = 0; i < 6; ++i) pool[i] = {i + 1, i + 1 < 6 ? &pool[i + 1] : nullptr};
Node* head = &pool[0];
cout << "middle of 1..6: " << middle(head)->val << '\n';
cout << "reversed:";
for (Node* p = head = reverseList(head); p; p = p->next) cout << ' ' << p->val;
Node* e = cycleEntry(head);
cout << "\ncycle? " << (e ? "yes" : "no") << '\n';
pool[0].next = &pool[3]; // tail (value 1) now points back to value 4
e = cycleEntry(head);
cout << "cycle? " << (e ? "yes, entry value " + to_string(e->val) : "no") << '\n';
}আউটপুট:
middle of 1..6: 4
reversed: 6 5 4 3 2 1
cycle? no
cycle? yes, entry value 4
৬টি node-এ দুটি মাঝ (3 আর 4) — কোডটি দ্বিতীয়টি দিয়েছে। উল্টানোর পর head হলো 6. এরপর শেষ node (মান 1)-কে মান 4-এর node-এর সাথে জুড়ে চক্র বানানো হয়েছে: 6 → 5 → 4 → 3 → 2 → 1 → 4 → …; Floyd সঠিকভাবে বলেছে চক্র শুরু 4-এ। node-গুলো একটি vector-এ রাখা, তাই new/delete বা memory leak-এর ঝামেলা নেই — ইন্টারভিউতে এটি বললে ভালো সংকেত।
চক্রের শুরু কেন মেলে (প্রমাণ): head থেকে চক্রের শুরু পর্যন্ত দূরত্ব μ, চক্রের দৈর্ঘ্য λ। দেখা হওয়ার সময় slow হেঁটেছে s ধাপ, fast 2s; fast বাড়তি s ধাপে চক্রটি পুরো কয়েকবার ঘুরেছে, তাই s হলো λ-এর গুণিতক। slow চক্রের ভেতরে s − μ ধাপ এগিয়ে আছে। এখন একটি pointer head থেকে আর অন্যটি দেখা হওয়ার জায়গা থেকে μ ধাপ হাঁটলে প্রথমটি চক্রের শুরুতে পৌঁছায়, আর দ্বিতীয়টি চক্রের ভেতরে মোট s ধাপ — λ-এর গুণিতক — এগিয়ে, অর্থাৎ সেও চক্রের শুরুতে। সময় O(μ + λ) = O(n), মেমরি O(1).
- Edge case: খালি list (
head == nullptr), এক node, এক node-এর নিজের সাথে চক্র।while (fast && fast->next)— দুটোই যাচাই না করলে null-এরnextপড়ে crash. - উল্টানোর বাগ:
head->next = prevলেখার আগেnxtসেভ না করা — তখন বাকি list হারিয়ে যায়।
অনু ৯.১1 → 2 → 3 → 4 → 5 থেকে শেষ দিক থেকে দ্বিতীয় node মুছুন, এক পাসে। কোন কৌশল, আর head নিজেই মুছতে হলে কী করবেন? (মাঝারি)
Hint: দুটি pointer, দূরত্ব n — প্রথমটিকে আগে n ধাপ এগিয়ে দিন, তারপর দুজনকে একসাথে চালান।
উত্তর: ফল 1 → 2 → 3 → 5. একটি dummy node head-এর আগে বসান, দুই pointer-ই dummy থেকে শুরু; fast-কে n + 1 ধাপ এগিয়ে দিন, তারপর fast null না হওয়া পর্যন্ত দুজনকে চালান — slow তখন মুছতে হবে এমন node-এর ঠিক আগে। dummy থাকায় head মোছার (n = দৈর্ঘ্য) আলাদা কেস লাগে না; ফেরত দিন dummy.next. সময় O(L), মেমরি O(1).
অনু ৯.২দুটি সাজানো list 1 → 3 → 5 আর 2 → 3 → 6 মিলিয়ে একটি সাজানো list বানান, নতুন node না বানিয়ে। (সহজ)
Hint: dummy head আর একটি tail pointer; প্রতিবার দুটির মধ্যে ছোট মাথাটি tail-এর পরে জুড়ুন।
উত্তর: 1 → 2 → 3 → 3 → 5 → 6. একটি list ফুরিয়ে গেলে অন্যটির বাকি অংশ একবারে জুড়ে দিন। সময় O(m + n), মেমরি O(1) (recursive সংস্করণে call stack O(m + n)). এটি merge sort-এর merge ধাপ (পর্ব ২১)।
প্যাটার্ন ১০ · Tree-তে DFS recursion — “বাচ্চাদের কাছ থেকে কী জানতে চাই?”
রিভিশন: পর্ব ২৬ · Tree, পর্ব ২৭ · BST, পর্ব ৪৮ · LCA আর diameter
চেনার সংকেত: ইনপুট একটি binary tree-এর root. “উচ্চতা/গভীরতা”, “দুই node-এর সবচেয়ে লম্বা পথ”, “সাধারণ পূর্বপুরুষ”, “বৈধ BST কি না”, “root থেকে leaf পর্যন্ত যোগফল”, “ভারসাম্যপূর্ণ কি না”। প্রায় সবগুলোর একই ছাঁচ: একটি recursive ফাংশন যা দুই সন্তানের কাছ থেকে একটি তথ্য নেয়, নিজেরটা হিসাব করে ওপরে পাঠায় (post-order). প্রশ্নটা শুধু: ওপরে কী ফেরত পাঠাব, আর পথে কী হালনাগাদ করব?
সমাধান-করা সমস্যা · পারিবারিক বংশতালিকা (মাঝারি): “একটি binary tree দেওয়া আছে। (ক) যেকোনো দুই node-এর মধ্যে সবচেয়ে লম্বা পথ কত edge (diameter)? (খ) দুটি node-এর lowest common ancestor কে — অর্থাৎ সবচেয়ে নিচের যে node দুজনেরই পূর্বপুরুষ? (গ) tree-টি কি বৈধ BST?”
- Diameter: brute force — প্রতিটি node থেকে উচ্চতা নতুন করে হিসাব: O(n²) (একদিকে হেলে থাকা tree-তে)। উন্নতি: একই DFS-এ উচ্চতা ফেরত দিন আর পথে
বাঁ উচ্চতা + ডান উচ্চতাদিয়ে সেরা হালনাগাদ করুন — O(n). সবচেয়ে লম্বা পথ root দিয়ে যেতেই হবে, এমন নয় — এটাই সাধারণ ভুল। - LCA: node-টি p বা q হলে নিজেকে ফেরত দিন; দুই দিক থেকেই কিছু ফিরলে আপনিই মিলনস্থল। O(n).
- Valid BST: শুধু “বাঁ সন্তান < আমি < ডান সন্তান” যাচাই করা ভুল — নাতি-নাতনিও শর্ত ভাঙতে পারে। প্রতিটি node-কে একটি অনুমোদিত পরিসর (lo, hi) দিন। (বিকল্প: inorder traversal কঠোরভাবে বাড়তে হবে।)
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
struct T { int val; T *l = nullptr, *r = nullptr; };
int height(T* n, int& diam) { // returns height in nodes; updates diameter
if (!n) return 0;
int a = height(n->l, diam), b = height(n->r, diam);
diam = max(diam, a + b); // longest path THROUGH n, counted in edges
return 1 + max(a, b);
}
bool validBST(T* n, long long lo, long long hi) { // every key strictly inside (lo, hi)
if (!n) return true;
if (n->val <= lo || n->val >= hi) return false;
return validBST(n->l, lo, n->val) && validBST(n->r, n->val, hi);
}
T* lca(T* n, int p, int q) { // both p and q are assumed to be in the tree
if (!n || n->val == p || n->val == q) return n;
T *a = lca(n->l, p, q), *b = lca(n->r, p, q);
return a && b ? n : (a ? a : b); // found on both sides -> n is the meeting point
}
int main() {
// tree: 8 -> (3, 10); 3 -> (1, 6); 10 -> right 14; 6 -> right 7
int vals[] = {8, 3, 10, 1, 6, 14, 7};
vector<T> t(7);
for (int i = 0; i < 7; ++i) t[i].val = vals[i];
t[0].l = &t[1]; t[0].r = &t[2]; t[1].l = &t[3]; t[1].r = &t[4];
t[2].r = &t[5]; t[4].r = &t[6];
int diam = 0; height(&t[0], diam);
cout << "diameter (edges) = " << diam << '\n';
cout << "valid BST? " << validBST(&t[0], LLONG_MIN, LLONG_MAX) << '\n';
cout << "lca(7,1) = " << lca(&t[0], 7, 1)->val << ", lca(7,14) = " << lca(&t[0], 7, 14)->val << '\n';
t[6].val = 9; // 9 > 6 is fine locally, but it is left of root 8
cout << "after 7 -> 9, valid BST? " << validBST(&t[0], LLONG_MIN, LLONG_MAX) << '\n';
}আউটপুট:
diameter (edges) = 5
valid BST? 1
lca(7,1) = 3, lca(7,14) = 8
after 7 -> 9, valid BST? 0
সবচেয়ে লম্বা পথ 7 → 6 → 3 → 8 → 10 → 14 — ৫টি edge, আর এটি root দিয়ে যায়। 7 আর 1 দুজনেই 3-এর নিচে, তাই LCA 3; 7 আর 14 root-এর দুই দিকে, তাই LCA 8. শেষে 7-কে 9 বানানো হয়েছে: 9 > 6, তাই শুধু বাবা-সন্তান দেখলে সব ঠিক মনে হয়, কিন্তু 9 আছে root 8-এর বাঁ দিকে — পরিসর (6, 8)-এর বাইরে, তাই বৈধ নয় (0). cout bool-কে 1/0 হিসেবে ছাপে। সীমা হিসেবে LLONG_MIN/LLONG_MAX নেওয়া হয়েছে, যাতে INT_MIN মানের node থাকলেও তুলনা ঠিক থাকে।
- জটিলতা: তিনটিই প্রতিটি node একবার দেখে — সময় O(n); মেমরি O(h) call stack, যেখানে h উচ্চতা (ভারসাম্যপূর্ণ tree-তে O(log n), একদিকে হেলে থাকলে O(n)).
- Recursion-এর ঝুঁকি: দশ লাখ node-এর একটি সরলরেখার মতো tree-তে recursion stack overflow ঘটাতে পারে। ইন্টারভিউতে ঝুঁকিটা বলুন, আর দরকার হলে নিজের stack দিয়ে iterative লিখুন (পর্ব ০৭)।
অনু ১০.১একটি binary tree “ভারসাম্যপূর্ণ” কি না — প্রতিটি node-এ বাঁ আর ডান উচ্চতার পার্থক্য সর্বোচ্চ ১ — O(n)-এ কীভাবে যাচাই করবেন? (মাঝারি)
Hint: প্রতিটি node-এ height() আলাদা করে ডাকলে O(n²) পর্যন্ত। একটি DFS-এ উচ্চতা ফেরত দিন, আর ভারসাম্য ভাঙলে একটি বিশেষ মান (−1).
উত্তর: h(n): null হলে 0; বাঁ বা ডান −1 হলে −1; |বাঁ − ডান| > 1 হলে −1; নইলে 1 + max. root-এ −1 না এলে ভারসাম্যপূর্ণ। সময় O(n), মেমরি O(h). এটাই AVL tree-র balance factor-এর ধারণা (পর্ব ২৮)।
অনু ১০.২একটি BST-তে k তম ছোট মান বের করুন। ওপরের tree (8, 3, 10, 1, 6, 14, 7)-এ k = 4 হলে উত্তর কত? (সহজ)
Hint: BST-এর inorder traversal মানগুলো সাজানো ক্রমে দেয়।
উত্তর: inorder: 1, 3, 6, 7, 8, 10, 14 — চতুর্থটি 7. k তম পেলেই থামুন: সময় O(h + k), মেমরি O(h). বারবার এ প্রশ্ন এলে প্রতিটি node-এ subtree-এর আকার রাখুন (augmented BST) — তখন প্রতিটি প্রশ্ন O(h).
৭ · Graph আর union-find — সংযোগ, ক্রম আর গ্রুপ
প্যাটার্ন ১১ · Graph — grid-এ BFS আর topological sort
রিভিশন: পর্ব ৩৯ · গ্রাফ, পর্ব ৪০ · BFS, পর্ব ৪১ · DFS, পর্ব ৪২ · Topological sort
চেনার সংকেত: একটি 2D grid (মানচিত্র, গোলকধাঁধা, পিক্সেল), “কয়টি দ্বীপ/অঞ্চল”, “সবচেয়ে কম ধাপ”, “ছড়িয়ে পড়তে কত মিনিট” — grid নিজেই একটি implicit graph, প্রতিটি ঘর একটি vertex. সমান খরচের সবচেয়ে ছোট পথ মানেই BFS. আর “কাজ A-র আগে B”, “prerequisite”, “build-এর ক্রম”, “চক্র আছে কি” — এগুলো topological sort.
সমাধান-করা সমস্যা ১ · নদীর চর গোনা (মাঝারি): “একটি নদীর মানচিত্র grid-এ দেওয়া — '#' মানে চর (মাটি), '.' মানে পানি। ওপর-নিচ-ডান-বাঁ জুড়ে থাকা মাটির ঘরগুলো একটি চর। কয়টি চর আছে?” প্রতিটি নতুন মাটির ঘর পেলে গণনা বাড়ান আর সেখান থেকে flood fill (BFS) চালিয়ে পুরো চরটি “দেখা হয়েছে” চিহ্ন দিন। প্রতিটি ঘর একবারই queue-তে ঢোকে।
সমাধান-করা সমস্যা ২ · কোর্সের ক্রম (মাঝারি): “একটি কোর্সে n টি বিষয়, আর কিছু শর্ত ‘a পড়ার আগে b পড়তে হবে’। সব বিষয় পড়ার একটি বৈধ ক্রম দিন, অথবা বলুন অসম্ভব।” Kahn-এর অ্যালগরিদম: যাদের কোনো বাকি শর্ত নেই (in-degree 0), তাদের queue-তে রাখুন; একটি বের করে পড়ুন, তার ওপর নির্ভরশীলদের in-degree কমান। শেষে সবাইকে পড়া না গেলে চক্র আছে — অসম্ভব।
#include <iostream>
#include <vector>
#include <string>
#include <queue>
using namespace std;
int countIslands(vector<string> g) { // '#' = land (char), '.' = water
int R = g.size(), C = R ? g[0].size() : 0, islands = 0;
queue<pair<int, int>> q;
auto visit = [&](int r, int c) { // mark when PUSHED, not when popped
if (r >= 0 && r < R && c >= 0 && c < C && g[r][c] == '#') { g[r][c] = '.'; q.push({r, c}); }
};
for (int r = 0; r < R; ++r)
for (int c = 0; c < C; ++c) {
if (g[r][c] != '#') continue;
++islands; visit(r, c); // a new island: flood it with BFS
while (!q.empty()) {
auto [x, y] = q.front(); q.pop();
visit(x + 1, y); visit(x - 1, y); visit(x, y + 1); visit(x, y - 1);
}
}
return islands;
}
vector<int> courseOrder(int n, const vector<pair<int, int>>& pre) { // {a, b}: b before a
vector<vector<int>> adj(n); vector<int> indeg(n), order; queue<int> q;
for (auto [a, b] : pre) { adj[b].push_back(a); ++indeg[a]; }
for (int v = 0; v < n; ++v) if (indeg[v] == 0) q.push(v);
while (!q.empty()) {
int v = q.front(); q.pop(); order.push_back(v);
for (int w : adj[v]) if (--indeg[w] == 0) q.push(w);
}
if ((int)order.size() < n) order.clear(); // someone never reached in-degree 0: cycle
return order;
}
int main() {
cout << "islands = " << countIslands({"##..#", "#...#", "..#..", "....#", "##.##"}) << '\n';
vector<vector<pair<int, int>>> tests{{{1, 0}, {2, 0}, {3, 1}, {3, 2}, {4, 3}, {5, 4}}, {{1, 0}, {2, 1}, {0, 2}}};
for (auto& pre : tests) {
auto order = courseOrder(6, pre);
if (order.empty()) cout << "impossible: cycle";
for (int v : order) cout << v << ' ';
cout << '\n';
}
}আউটপুট:
islands = 5
0 1 2 3 4 5
impossible: cycle
মানচিত্রে পাঁচটি চর: বাঁ-ওপরের তিন ঘর, ডান-ওপরের দুই ঘর, মাঝের একক ঘর, ডান-নিচের তিন ঘর, আর বাঁ-নিচের দুই ঘর (কোনাকুনি ছোঁয়া জোড়া ধরা হয় না — শুরুতে জিজ্ঞেস করুন ৪ দিক নাকি ৮ দিক)। প্রথম শর্ত-তালিকায় ক্রম 0, 1, 2, 3, 4, 5 পাওয়া গেছে। দ্বিতীয়টিতে 0 → 1 → 2 → 0 চক্র, তাই 0, 1, 2 কখনো in-degree 0-তে পৌঁছায় না; 3, 4, 5 পড়া গেলেও সবাইকে নয় — অসম্ভব। visit lambda ঘরটিকে queue-তে ঢোকানোর সময়েই চিহ্ন দেয়; বের করার সময় দিলে একই ঘর বহুবার queue-তে ঢুকতে পারত।
- জটিলতা: চর গোনা — R × C grid-এ প্রতিটি ঘর একবার ঢোকে, ৪টি প্রতিবেশী দেখা হয়: সময় O(R · C), মেমরি O(R · C) (queue সবচেয়ে খারাপ ক্ষেত্রে)। Kahn — সময় O(V + E), মেমরি O(V + E).
- DFS না BFS? শুধু গোনার জন্য দুটোই চলে। কিন্তু বিশাল grid-এ recursive DFS-এর গভীরতা R · C পর্যন্ত হতে পারে — stack overflow-এর ঝুঁকি। সবচেয়ে কম ধাপ চাইলে BFS ছাড়া উপায় নেই।
- ইনপুট বদলানো: কোডটি grid-এর একটি কপি নিয়ে সেটাতেই চিহ্ন দেয় (
vector<string> gমান হিসেবে)। কলকারীর ডেটা বদলানো চলবে কি না, জিজ্ঞেস করুন।
অনু ১১.১একটি ঝুড়িতে আম: 2 = পচা, 1 = ভালো, 0 = খালি। প্রতি মিনিটে পচা আম তার ওপর-নিচ-ডান-বাঁয়ের ভালো আমকে পচিয়ে দেয়। [[2,1,1],[1,1,0],[0,1,1]]-এ সব আম পচতে কত মিনিট? (মাঝারি)
Hint: সব পচা আম একসাথে queue-তে দিয়ে শুরু করুন — multi-source BFS. BFS-এর স্তরের সংখ্যাই মিনিট।
উত্তর: ৪ মিনিট। শেষে কোনো ভালো আম বাকি থাকলে −1 — যেমন [[2,1,1],[0,1,1],[1,0,1]]-এ বাঁ-নিচের আমটিতে কখনো পৌঁছানো যায় না। সময় O(R · C). প্রতিটি পচা আম থেকে আলাদা BFS চালালে O((R · C)²)।
অনু ১১.২Topological sort-এ Kahn-এর বদলে DFS ব্যবহার করলে ক্রম কীভাবে পাবেন, আর চক্র কীভাবে ধরবেন? (কঠিন)
Hint: DFS-এ একটি vertex-এর সব বংশধর শেষ হলে তবেই সেটি “শেষ” হয়।
উত্তর: প্রতিটি vertex শেষ হওয়ার (post-order) সময় একটি তালিকায় যোগ করুন, শেষে তালিকা উল্টান — সেটিই topological ক্রম। চক্র ধরতে তিন রং: সাদা (দেখা হয়নি), ধূসর (এখন recursion-এর পথে), কালো (শেষ)। ধূসর vertex-এ আবার পৌঁছালে back edge — চক্র। সময় O(V + E). শুধু “visited” bool রাখলে ভুল হয়: পাশের শাখা থেকে কালো vertex-এ পৌঁছানো চক্র নয়, কিন্তু bool দিয়ে দুটো আলাদা করা যায় না (পর্ব ৪১)।
প্যাটার্ন ১২ · Union-find — “এরা কি একই দলে?”
রিভিশন: পর্ব ৩৩ · Disjoint set union, পর্ব ৪৫ · Kruskal
চেনার সংকেত: সংযোগ একটি একটি করে যোগ হয়, আর বারবার প্রশ্ন “x আর y কি এখন জুড়ে আছে?”, “কয়টি দল/প্রদেশ/বন্ধুমহল”, “কোন edge যোগ করলে চক্র হয় (redundant)”, “কখন সবাই একসাথে জুড়ল”, “একই account-এর email একত্র করুন”। edge শুধু যোগ হয়, মোছে না — তখন DSU BFS-এর চেয়ে সহজ ও দ্রুত।
সমাধান-করা সমস্যা · গ্রামের রাস্তা (মাঝারি): “একটি উপজেলার ৫টি গ্রাম। একটি একটি করে রাস্তা তৈরি হচ্ছে। প্রতিটি নতুন রাস্তার পরে বলুন কয়টি আলাদা অঞ্চল আছে, আর রাস্তাটি অপ্রয়োজনীয় (redundant) কি না — অর্থাৎ দুই প্রান্তের গ্রাম আগে থেকেই কোনো পথে জোড়া ছিল কি না।” Brute force: প্রতিটি রাস্তার পরে পুরো graph-এ BFS — মোট O(m · (n + m)). DSU-তে প্রতিটি কাজ প্রায় O(1).
#include <iostream>
#include <vector>
#include <numeric>
#include <utility>
using namespace std;
struct DSU {
vector<int> parent, size;
int components;
DSU(int n) : parent(n), size(n, 1), components(n) { iota(parent.begin(), parent.end(), 0); }
int find(int x) { return parent[x] == x ? x : parent[x] = find(parent[x]); } // path compression
bool unite(int a, int b) { // false if already connected
a = find(a); b = find(b);
if (a == b) return false;
if (size[a] < size[b]) swap(a, b); // union by size
parent[b] = a; size[a] += size[b]; --components;
return true;
}
};
int main() {
// villages 1..5, roads added one by one; the road that closes a cycle is redundant
vector<pair<int, int>> roads{{1, 2}, {2, 3}, {4, 5}, {1, 3}, {3, 4}, {2, 5}};
DSU d(6); // index 0 unused
for (auto [a, b] : roads) {
bool merged = d.unite(a, b);
cout << "road " << a << "-" << b << (merged ? " joins two groups" : " is REDUNDANT")
<< ", groups now = " << d.components - 1 << '\n';
}
}আউটপুট:
road 1-2 joins two groups, groups now = 4
road 2-3 joins two groups, groups now = 3
road 4-5 joins two groups, groups now = 2
road 1-3 is REDUNDANT, groups now = 2
road 3-4 joins two groups, groups now = 1
road 2-5 is REDUNDANT, groups now = 1
শুরুতে ৫টি আলাদা গ্রাম। 1-3 রাস্তার সময় 1 আর 3 আগে থেকেই 1-2-3 পথে জোড়া, তাই unite false দেয় — রাস্তাটি চক্র বানায়, এটাই “redundant connection”. 3-4-এর পরে সব গ্রাম এক অঞ্চলে, তাই 2-5-ও অপ্রয়োজনীয়। index 0 ব্যবহার হয়নি বলে গণনা থেকে 1 বাদ দেওয়া হয়েছে (components - 1) — এই ধরনের off-by-one ঝামেলা এড়াতে অনেকে গ্রামগুলোকে 0 থেকে নম্বর দেন।
- জটিলতা: path compression আর union by size একসাথে থাকলে m টি কাজে মোট O(m · α(n)), যেখানে α হলো inverse Ackermann — বাস্তবের যেকোনো n-এ ৪-এর বেশি নয়। মেমরি O(n). দুটির একটিও না থাকলে tree লম্বা সরলরেখা হয়ে প্রতিটি find O(n) পর্যন্ত যেতে পারে।
- Recursion: path compression-এর recursive
findপ্রথমবার লম্বা শিকলে গভীর হতে পারে; union by size থাকলে উচ্চতা O(log n), তাই নিরাপদ।
অনু ১২.১৪ জন মানুষের বন্ধুত্বের matrix [[1,1,0,0],[1,1,0,0],[0,0,1,1],[0,0,1,1]] (M[i][j] = 1 মানে বন্ধু)। কয়টি বন্ধুমহল? DSU আর DFS — দুটোর জটিলতা তুলনা করুন। (সহজ)
Hint: প্রতিটি 1-এর জন্য unite(i, j); শেষে আলাদা root গুনুন।
উত্তর: ২টি ({0, 1} আর {2, 3})। matrix পুরোটা পড়তেই O(n²), তাই DSU (O(n² · α(n))) আর DFS (O(n²)) প্রায় সমান। DSU-র আসল সুবিধা যখন সংযোগ ধাপে ধাপে আসে আর প্রতিটি ধাপের পরে উত্তর চাই।
অনু ১২.২৫ জন (0–4) বন্ধু হওয়ার ঘটনা (সময়, a, b): (3, 0, 1), (5, 2, 3), (7, 1, 2), (9, 0, 3), (11, 3, 4). কোন মুহূর্তে প্রথম সবাই একে অপরের সাথে (সরাসরি বা পরোক্ষে) জুড়ে গেল? (মাঝারি)
Hint: ঘটনাগুলো সময় অনুযায়ী sort করে DSU-তে একে একে যোগ করুন; দলের সংখ্যা ১ হলেই থামুন।
উত্তর: সময় 11. 3-এ দল ৪, 5-এ ৩, 7-এ ২; 9-এর ঘটনা (0, 3) redundant — দুজনেই আগে থেকে একই দলে; 11-এ 4 যোগ হয়ে দল ১। সময় O(m log m + m · α(n)) — sort-ই প্রধান খরচ। কেউ কখনো না জুড়লে −1.
৮ · Backtracking আর DP — সব সম্ভাবনা, তারপর মনে রাখা
প্যাটার্ন ১৩ · Backtracking — বাছুন, এগোন, ফিরিয়ে নিন
রিভিশন: পর্ব ০৭ · রিকার্শন, পর্ব ৫৬ · Backtracking
চেনার সংকেত: “সব উপসেট/বিন্যাস/সমাবেশ দিন”, “সব বৈধ বন্ধনী-জোড়া”, “N-queens”, “Sudoku”, “শব্দটি grid-এ আছে কি”, আর n ছোট (≤ ১০–২০)। আউটপুটই সূচকীয় আকারের হলে এর চেয়ে দ্রুত সমাধান সম্ভবই নয়। কাজ শুধু একটি: অবৈধ শাখা যত আগে সম্ভব কেটে দেওয়া (pruning).
Pseudocode — the backtracking skeleton
SOLVE(state):
if state is a complete answer: record it; return
for each choice c allowed in state:
if c cannot lead to an answer: skip // prune
apply c // choose
SOLVE(state) // explore
undo c // un-chooseসমাধান-করা সমস্যা · ঠিক বাজেটে কেনাকাটা (মাঝারি): “একটি দোকানে কয়েক রকম চকলেট, দাম [2, 3, 6, 7] টাকা; একই চকলেট যতবার খুশি কেনা যায়। ঠিক 7 টাকা খরচ করার সব আলাদা উপায় দিন (ক্রম বদলালে নতুন উপায় নয়)।” সাথে একটি ছোট উষ্ণতা-অনুশীলন: [1, 2, 3]-এর সব উপসেট।
- ডুপ্লিকেট এড়ানো: [2, 2, 3] আর [3, 2, 2] একই উপায়। তাই প্রতিটি ধাপে শুধু বর্তমান index থেকে ডানের চকলেট বাছুন (
start) — তাহলে প্রতিটি উপায় একবারই, অ-কমা ক্রমে তৈরি হয়। - পুনর্ব্যবহার: একই চকলেট আবার নেওয়া যায়, তাই পরের ডাকে
i,i + 1নয়। - Pruning: দাম সাজানো থাকলে
c[i] > leftহওয়ামাত্র লুপ থামান — ডানের সবাই আরও দামি।
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void print(const vector<int>& v) {
cout << '[';
for (size_t i = 0; i < v.size(); ++i) cout << (i ? "," : "") << v[i];
cout << "] ";
}
void subsets(const vector<int>& a, size_t i, vector<int>& cur) { // 2^n leaves
if (i == a.size()) { print(cur); return; }
cur.push_back(a[i]); subsets(a, i + 1, cur); cur.pop_back(); // take a[i]
subsets(a, i + 1, cur); // skip a[i]
}
// choose -> explore -> un-choose; a candidate may be reused
void combo(const vector<int>& c, int start, int left, vector<int>& cur, int& calls) {
++calls;
if (left == 0) { print(cur); return; }
for (int i = start; i < (int)c.size() && c[i] <= left; ++i) { // sorted: prune
cur.push_back(c[i]); // choose
combo(c, i, left - c[i], cur, calls); // explore (i, not i+1: reuse allowed)
cur.pop_back(); // un-choose
}
}
int main() {
vector<int> cur;
subsets({1, 2, 3}, 0, cur);
cout << '\n';
for (auto [cand, target] : vector<pair<vector<int>, int>>{{{2, 3, 6, 7}, 7}, {{5, 3, 2}, 8}, {{4}, 3}}) {
sort(cand.begin(), cand.end());
int calls = 0;
combo(cand, 0, target, cur, calls);
cout << "calls=" << calls << '\n';
}
}আউটপুট:
[1,2,3] [1,2] [1,3] [1] [2,3] [2] [3] []
[2,2,3] [7] calls=10
[2,2,2,2] [2,3,3] [3,5] calls=13
calls=1
প্রথম লাইনে 23 = ৮টি উপসেট — প্রতিটি উপাদানে “নিই/নিই না” দুই শাখা। 7 টাকায় দুটি উপায়: [2, 2, 3] আর [7]; মোট ১০টি recursive ডাক লেগেছে। [5, 3, 2]-কে আগে sort করে নেওয়া হয়েছে, তাই 8 টাকার উপায়গুলো অ-কমা ক্রমে এসেছে। শেষ লাইনে শুধু 4 টাকার চকলেট দিয়ে 3 টাকা মেলানো অসম্ভব — কিছু ছাপা হয়নি, আর pruning-এর কারণে মাত্র ১টি ডাকেই শেষ। লক্ষ করুন, cur প্রতিবার pop_back দিয়ে আগের অবস্থায় ফেরে — তাই একই vector পুরো খোঁজে চলে, কপি লাগে না।
- জটিলতা: উপসেট — 2n টি, প্রতিটি ছাপাতে O(n): Θ(n · 2n). বিন্যাস — Θ(n · n!)। Combination sum — ডাকের সংখ্যা সর্বোচ্চ শাখা-সংখ্যাT / ক্ষুদ্রতম দাম-এর মতো বাড়ে; আউটপুট-সংবেদী, কোনো সরল বহুপদী সীমা নেই। মেমরি O(T / ক্ষুদ্রতম দাম) recursion গভীরতা।
- সাধারণ বাগ: “un-choose” ভুলে যাওয়া — তখন আগের শাখার উপাদান পরের শাখায় থেকে যায়। আর ফল রেকর্ড করার সময়
cur-এর রেফারেন্স রেখে দেওয়া — পরে বদলে যায়; কপি রাখুন।
অনু ১৩.১n = 3 জোড়া বন্ধনী দিয়ে সব বৈধ (ঠিকমতো খোলা-বন্ধ) string তৈরি করুন। কয়টি, আর কোন দুটি শর্তে শাখা কাটবেন? (মাঝারি)
Hint: খোলা বন্ধনী n-এর কম হলে '(' দিন; বন্ধ বন্ধনী খোলার চেয়ে কম হলে ')' দিন।
উত্তর: ৫টি: ((())), (()()), (())(), ()(()), ()()()। সংখ্যাটি Catalan সংখ্যা C3 = 5; সাধারণভাবে Cn = (2n)! / ((n+1)! · n!)। দুটি শর্ত থাকায় কোনো অবৈধ শাখায় ঢোকাই হয় না — সব 22n string বানিয়ে পরে যাচাই করার চেয়ে অনেক দ্রুত।
অনু ১৩.২6 × 6 দাবার বোর্ডে ৬টি রানি এমনভাবে বসানোর কয়টি উপায় আছে যাতে কেউ কাউকে আক্রমণ না করে? প্রতিটি “এখানে বসানো যায় কি” যাচাই O(1) করবেন কীভাবে? (কঠিন)
Hint: প্রতিটি সারিতে একটি রানি; কলাম, প্রধান কর্ণ (r − c) আর উল্টো কর্ণ (r + c)-এর জন্য তিনটি bool array.
উত্তর: ৪টি উপায় (৮ × ৮-এ ৯২টি, ৪ × ৪-এ ২টি — সব বিন্যাস পরখ করে যাচাই করা)। তিনটি array থাকায় প্রতিটি যাচাই O(1). r − c ঋণাত্মক হতে পারে, তাই index হিসেবে r − c + n − 1 নিন। খোঁজার আকার n!-এর চেয়ে অনেক ছোট, তবু সূচকীয় (পর্ব ৫৬)।
প্যাটার্ন ১৪ · Dynamic programming — একই ছোট প্রশ্ন বারবার
রিভিশন: পর্ব ৫২ · DP-র ভিত্তি, পর্ব ৫৩ · Knapsack, coin change, LIS, পর্ব ৫৪ · LCS, edit distance
চেনার সংকেত: “কত উপায়ে”, “সর্বনিম্ন খরচ/সর্বোচ্চ লাভ”, “সম্ভব কি না”, আর সিদ্ধান্তগুলো ধাপে ধাপে নেওয়া যায় (প্রতিটি বাড়ি/কয়েন/অক্ষর)। brute force recursion-এ একই উপ-প্রশ্ন বারবার আসে (overlapping subproblems), আর বড় সমস্যার সেরা উত্তর ছোটদের সেরা উত্তর দিয়ে গড়া যায় (optimal substructure). greedy কাজ করছে কি না সন্দেহ হলে প্রায়ই DP-ই নিরাপদ।
Recipe — five questions before writing any DP
1. State: dp[i] (or dp[i][j]) means exactly WHAT?
2. Transition: how is dp[i] built from smaller states?
3. Base case: the smallest states, written by hand
4. Order: fill so every state you read is already computed
5. Answer: which cell (or max over cells) is the final answer?সমাধান-করা সমস্যা — চারটি ক্লাসিক এক জায়গায়:
- ১D · পাশাপাশি নয় (মাঝারি): “একজন বিক্রয়কর্মী একটি গলির বাড়িগুলোতে যান, প্রতিটিতে সম্ভাব্য বিক্রি জানা। পাশাপাশি দুই বাড়িতে একই দিনে যাওয়া যাবে না। সর্বোচ্চ বিক্রি কত?” dp[i] = প্রথম i টি বাড়ি থেকে সর্বোচ্চ; dp[i] = max(dp[i−1], dp[i−2] + a[i]). শুধু শেষ দুটি মান লাগে, তাই মেমরি O(1).
- ১D · সবচেয়ে কম কয়েন (মাঝারি): “কয়েন {1, 7, 10}, যতবার খুশি। ঠিক 14 টাকা বানাতে কমপক্ষে কয়টি কয়েন?” Greedy (সবচেয়ে বড়টি আগে) দেয় 10 + 1 + 1 + 1 + 1 = ৫টি; আসল উত্তর 7 + 7 = ২টি। dp[v] = min(dp[v − c] + 1). এটাই coin change.
- ২D · LCS (মাঝারি): “একটি ফাইলের দুই সংস্করণের মধ্যে সবচেয়ে লম্বা অভিন্ন subsequence কত?” — diff টুলের ভিত্তি। dp[i][j] = a-র প্রথম i আর b-র প্রথম j অক্ষরের LCS.
- ২D · grid-এর পথ (সহজ): “একটি এলাকার রাস্তার grid-এ বাঁ-ওপর থেকে ডান-নিচে শুধু ডানে বা নিচে গিয়ে কত উপায়ে যাওয়া যায়, যদি কিছু মোড় পানিতে ডুবে বন্ধ থাকে?” dp[r][c] = ওপর থেকে + বাঁ থেকে; বন্ধ ঘরে 0. একটি সারি রাখলেই চলে (rolling array).
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
int robber(const vector<int>& a) { // dp[i] = max(dp[i-1], dp[i-2] + a[i])
int take = 0, skip = 0; // best ending with / without a[i-1]
for (int x : a) { int t = skip + x; skip = max(skip, take); take = t; }
return max(take, skip);
}
int coinChange(const vector<int>& coins, int amount) { // fewest coins, -1 if impossible
const int INF = 1e9;
vector<int> dp(amount + 1, INF); dp[0] = 0;
for (int v = 1; v <= amount; ++v)
for (int c : coins) if (c <= v && dp[v - c] + 1 < dp[v]) dp[v] = dp[v - c] + 1;
return dp[amount] >= INF ? -1 : dp[amount];
}
int lcs(const string& a, const string& b) { // dp[i][j] = LCS of a[0..i), b[0..j)
vector<vector<int>> dp(a.size() + 1, vector<int>(b.size() + 1, 0));
for (size_t i = 1; i <= a.size(); ++i)
for (size_t j = 1; j <= b.size(); ++j)
dp[i][j] = a[i - 1] == b[j - 1] ? dp[i - 1][j - 1] + 1 : max(dp[i - 1][j], dp[i][j - 1]);
return dp[a.size()][b.size()];
}
long long gridPaths(const vector<string>& g) { // right/down moves, '#' = blocked
vector<long long> dp(g[0].size(), 0); dp[0] = 1;
for (const string& row : g)
for (size_t c = 0; c < row.size(); ++c)
dp[c] = row[c] == '#' ? 0 : dp[c] + (c ? dp[c - 1] : 0); // from above + from left
return dp.back();
}
int main() {
cout << "robber [2,7,9,3,1] = " << robber({2, 7, 9, 3, 1}) << '\n';
cout << "coins {1,7,10} -> 14 = " << coinChange({1, 7, 10}, 14) << ", {4} -> 6 = " << coinChange({4}, 6) << '\n';
cout << "lcs(AGGTAB, GXTXAYB) = " << lcs("AGGTAB", "GXTXAYB") << '\n';
cout << "grid paths = " << gridPaths({"...", ".#.", "..."}) << '\n';
}আউটপুট:
robber [2,7,9,3,1] = 12
coins {1,7,10} -> 14 = 2, {4} -> 6 = -1
lcs(AGGTAB, GXTXAYB) = 4
grid paths = 2
বাড়ি [2, 7, 9, 3, 1]-এ সেরা 2 + 9 + 1 = 12 (7 + 3 = 10 কম)। {4} দিয়ে 6 বানানো অসম্ভব, তাই −1 — “অসীম” হিসেবে 109 রাখা হয়েছে, আর dp[v - c] + 1 যোগে overflow হয় না কারণ INT_MAX নেওয়া হয়নি। LCS = 4 ("GTAB"). 3 × 3 grid-এর মাঝের ঘর বন্ধ থাকলে শুধু চারপাশ দিয়ে দুটি পথ। gridPaths-এ একটিমাত্র সারি dp বারবার হালনাগাদ হয়: হালনাগাদের আগে dp[c] হলো ওপরের ঘর, আর dp[c - 1] এই সারির বাঁয়ের ঘর।
- জটিলতা: robber — সময় O(n), মেমরি O(1). Coin change — সময় O(A · k) (A = টাকার অঙ্ক, k = কয়েনের ধরন), মেমরি O(A); এটি pseudo-polynomial, কারণ A-র মান বড় হলে ধীর। LCS — সময় O(n · m), মেমরি O(n · m), শুধু দৈর্ঘ্য চাইলে দুটি সারিতে O(min(n, m)). Grid — সময় O(R · C), মেমরি O(C).
- Top-down না bottom-up? Memoization লেখা সহজ আর শুধু দরকারি state ছোঁয়; tabulation-এ recursion-এর গভীরতার ঝুঁকি নেই আর rolling array সহজ। ইন্টারভিউতে আগে recursion + memo বলুন, তারপর টেবিলে রূপান্তর দেখান — দুটোই জানা ভালো সংকেত।
অনু ১৪.১একটি সিঁড়িতে ৫টি ধাপ; একবারে ১ বা ২ ধাপ ওঠা যায়। মোট কত উপায়ে ওপরে ওঠা যায়? (সহজ)
Hint: শেষ পা-টি ১ ধাপের নাকি ২ ধাপের — dp[n] = dp[n−1] + dp[n−2].
উত্তর: ৮টি উপায় (n = 1, 2, 3, 4, 5-এ 1, 2, 3, 5, 8 — Fibonacci). সময় O(n), মেমরি O(1). Memo ছাড়া সরল recursion-এ ডাকের সংখ্যা সূচকীয় (প্রায় φn, φ ≈ 1.618).
অনু ১৪.২"horse" থেকে "ros" বানাতে কমপক্ষে কয়টি সম্পাদনা (একটি অক্ষর ঢোকানো, মোছা বা বদলানো) লাগে? dp-এর সংজ্ঞা আর ভিত্তি লিখুন। (মাঝারি)
Hint: dp[i][j] = a-র প্রথম i অক্ষরকে b-র প্রথম j অক্ষর বানানোর খরচ; dp[i][0] = i, dp[0][j] = j.
উত্তর: ৩টি (h → r বদল, r মোছা, e মোছা)। রূপান্তর: অক্ষর মিললে dp[i−1][j−1], না মিললে 1 + min(মোছা dp[i−1][j], ঢোকানো dp[i][j−1], বদল dp[i−1][j−1]). সময় ও মেমরি O(n · m). এটি Levenshtein distance — বানান-সংশোধনের ভিত্তি (পর্ব ৫৪)। "dhaka" → "daka" মাত্র ১টি (h মোছা)।
অনু ১৪.৩[10, 9, 2, 5, 3, 7, 101, 18]-এ সবচেয়ে লম্বা কঠোরভাবে বাড়ন্ত subsequence-এর দৈর্ঘ্য কত? O(n²) আর O(n log n) — দুটি পদ্ধতি বলুন। (কঠিন)
Hint: O(n²): dp[i] = i-তে শেষ হওয়া LIS. O(n log n): tails[k] = দৈর্ঘ্য k + 1-এর বাড়ন্ত subsequence-এর সবচেয়ে ছোট সম্ভাব্য শেষ মান; প্রতিটি নতুন মানের জন্য lower_bound.
উত্তর: দৈর্ঘ্য ৪ (যেমন 2, 3, 7, 18). সাবধান: tails array নিজে কোনো বৈধ LIS নয় — শুধু দৈর্ঘ্য ঠিক দেয়; আসল subsequence চাইলে predecessor index রাখতে হয় (পর্ব ৫৩)।
৯ · প্যাটার্ন চেনার চিটশিট আর ভাবনার প্রশ্ন
নিচের টেবিলটি ইন্টারভিউর আগের রাতের রিভিশনের জন্য। বাঁ কলামের শব্দ বা অবস্থা সমস্যার বিবরণে দেখলে মাঝের প্যাটার্নটি প্রথমে ভাবুন। মনে রাখবেন, এটি নিশ্চিত নিয়ম নয়, শুরুর ইঙ্গিত — একটি সমস্যায় প্রায়ই দুটি প্যাটার্ন একসাথে লাগে (যেমন sort + two pointer, BFS + bitmask, binary search + greedy).
| সংকেত (সমস্যায় যা দেখবেন) | প্যাটার্ন ও জটিলতা | এই পাতায় · রিভিশন |
|---|---|---|
| “জোড়া যার যোগফল”, “আগে দেখেছি কি”, ডুপ্লিকেট, গণনা, anagram | Hashing — গড়ে O(n) | প্যাটার্ন ১ · পর্ব ৩২ |
| সাজানো array, দুই প্রান্ত, palindrome, in-place সরানো | Two pointer — O(n) (+ sort O(n log n)) | প্যাটার্ন ২ · পর্ব ১৬ |
| সবচেয়ে লম্বা/ছোট টানা অংশ, একমুখী শর্ত, সব মান ধনাত্মক | Sliding window — O(n) | প্যাটার্ন ৩ · পর্ব ১৬ |
| অংশের যোগফল ঠিক k, ঋণাত্মক আছে, বিভাজ্য, বারবার রেঞ্জ-যোগফল | Prefix sum + hash map — O(n) | প্যাটার্ন ৪ · পর্ব ১৭ |
| “সর্বোচ্চটির সর্বনিম্ন”, “কমপক্ষে কত ক্ষমতা/গতি”, বিশাল উত্তর-পরিসর | Binary search on answer — O(n log range) | প্যাটার্ন ৫ · পর্ব ১৯ |
| “ডানে/বাঁয়ে প্রথম বড়/ছোট”, histogram, span, “কত দিন পরে” | Monotonic stack — O(n) | প্যাটার্ন ৬ · পর্ব ১৩ |
| k তম, সেরা k টি, k টি সাজানো তালিকা, চলমান median | Heap — O(n log k) | প্যাটার্ন ৭ · পর্ব ২৩ |
| [শুরু, শেষ] জোড়া, overlap, ঘর/মেশিন কয়টি | Sort + sweep — O(n log n) | প্যাটার্ন ৮ · পর্ব ৫০ |
| linked list, O(1) মেমরি, মাঝ, চক্র, শেষ থেকে n তম | Pointer কৌশল, Floyd — O(n), O(1) | প্যাটার্ন ৯ · পর্ব ১২ |
| tree-এর root, উচ্চতা, পথ, পূর্বপুরুষ, BST বৈধতা | DFS recursion — O(n), O(h) | প্যাটার্ন ১০ · পর্ব ২৬, ৪৮ |
| grid, “কয়টি অঞ্চল”, সমান খরচের সবচেয়ে কম ধাপ, ছড়িয়ে পড়া | BFS / flood fill — O(V + E) | প্যাটার্ন ১১ · পর্ব ৪০ |
| prerequisite, “A-র আগে B”, build-ক্রম, চক্র আছে কি | Topological sort — O(V + E) | প্যাটার্ন ১১ · পর্ব ৪২ |
| ভিন্ন খরচের সবচেয়ে ছোট পথ (ঋণাত্মক নেই) | Dijkstra — O((V + E) log V) | পর্ব ৪৩ |
| সংযোগ ধাপে ধাপে যোগ, “একই দলে কি”, redundant edge | Union-find — প্রায় O(1) প্রতি কাজে | প্যাটার্ন ১২ · পর্ব ৩৩ |
| “সব” উপসেট/বিন্যাস, n ≤ ২০, N-queens | Backtracking — সূচকীয় | প্যাটার্ন ১৩ · পর্ব ৫৬ |
| “কত উপায়ে”, “সর্বনিম্ন খরচ”, greedy-র পাল্টা উদাহরণ মেলে | DP — state-সংখ্যা × রূপান্তরের খরচ | প্যাটার্ন ১৪ · পর্ব ৫২ |
| prefix খোঁজা, autocomplete, অনেক শব্দ একসাথে | Trie — O(শব্দের দৈর্ঘ্য) | পর্ব ৩১ |
ভাবনার প্রশ্ন
কোড লেখার পরে ইন্টারভিউয়ার প্রায়ই জিজ্ঞেস করেন, “এটা কেন এভাবে করলেন?” — আর কিছু প্রশ্ন প্রস্তুতির ধরনটাই ঠিক করে দেয়। প্রতিটি আগে নিজে ভাবুন, তারপর খুলুন।
প্র ০১প্যাটার্ন শেখা কি মুখস্থ করা? একদম নতুন সমস্যায় এগুলো কীভাবে কাজে লাগাবেন? (মাঝারি)
সমাধান মুখস্থ করা আর প্যাটার্ন শেখা আলাদা জিনিস। “two sum-এর উত্তর hash map” মুখস্থ থাকলে শুধু two sum পারবেন। কিন্তু যদি বোঝেন কেন — ভেতরের লুপটা শুধু একটি মান খুঁজছিল, আর খোঁজা দ্রুত করার যন্ত্র hash — তাহলে “অংশের যোগফল k” সমস্যায়ও একই প্রশ্ন করতে পারবেন: “কী খুঁজছি বারবার?” উত্তর আসে: একটি আগের prefix. এভাবেই প্যাটার্ন নতুন সমস্যায় স্থানান্তরিত হয়।
গণিতবিদ George Pólya ১৯৪৫ সালে How to Solve It বইয়ে সমস্যা সমাধানের যে প্রশ্নগুলো লিখেছিলেন, সেগুলো আজও খাটে: “এমন কোনো সম্পর্কিত সমস্যা কি আগে দেখেছেন?”, “সমস্যাটির একটি অংশ সমাধান করতে পারেন?”, “শর্তগুলোর কোনোটি বাদ দিলে কী হয়?” ইন্টারভিউর ভাষায় এগুলোই — চেনা প্যাটার্ন খোঁজা, ছোট সংস্করণ সমাধান, শর্ত শিথিল করা।
অনুশীলনের একটি ব্যবহারিক উপায়: প্রতিটি সমস্যা শেষ করে এক লাইনে লিখুন “কোন সংকেত দেখে এই প্যাটার্ন চিনলাম”। কয়েক সপ্তাহে আপনার নিজের চিটশিট তৈরি হবে, যা অংশ ৯-এর টেবিলের চেয়েও কাজের — কারণ সেটি আপনার নিজের ভুল থেকে এসেছে। আর মাঝে মাঝে প্যাটার্নের নাম না দেখে এলোমেলো সমস্যা নিন; নামটা আগে জানলে চেনার পেশিটা অনুশীলন হয় না, অথচ ইন্টারভিউতে কেউ নাম বলে দেয় না।
শেষ কথা: প্যাটার্ন শুরু করার জায়গা, শেষ নয়। প্রতিটি সমাধানের সঠিকতার যুক্তি (invariant, exchange argument, induction) বলতে পারা চাই — এ কারণেই এই কোর্সের আগের পর্বগুলোতে প্রমাণে এত জোর।
মূল উপলব্ধি: প্যাটার্ন মানে “কোন প্রশ্ন করলে কোন কৌশল আসে” — এই প্রশ্নগুলো নিজের ভাষায় লিখে রাখলে নতুন সমস্যাতেও পথ খুঁজে পাবেন।
প্র ০২একটি সমস্যায় greedy চলবে নাকি DP লাগবে — ইন্টারভিউর চাপের মধ্যে কীভাবে সিদ্ধান্ত নেবেন? (কঠিন)
Greedy প্রলোভনময়: কোড ছোট, দ্রুত, আর ছোট উদাহরণে প্রায়ই ঠিক উত্তর দেয়। কিন্তু “প্রায়ই” যথেষ্ট নয়। প্রথম কাজ: একটি পাল্টা উদাহরণ খোঁজা। কয়েন {1, 7, 10}-এ 14 টাকা — greedy ৫টি কয়েন দেয়, উত্তর ২টি। এমন একটি উদাহরণ পেলেই greedy বাতিল, তখন DP. বাংলাদেশের প্রচলিত নোট-কয়েনের মতো কিছু ব্যবস্থায় greedy ঠিক কাজ করে (এদের বলে canonical coin system), কিন্তু যেকোনো এলোমেলো কয়েন-তালিকায় সেই নিশ্চয়তা নেই।
পাল্টা উদাহরণ না পেলে প্রমাণের চেষ্টা করুন। সবচেয়ে কাজের হাতিয়ার exchange argument: “যেকোনো সেরা সমাধানকে, উত্তর খারাপ না করে, greedy-র প্রথম সিদ্ধান্তের দিকে বদলানো যায়।” রিকশার যাত্রী আর interval scheduling-এ এই যুক্তি সহজে দাঁড়ায়। প্রমাণ দাঁড় করাতে না পারলে সেটাও তথ্য।
ব্যবহারিক কৌশল: ছোট ইনপুটে দুটিকে মেলান। একটি brute force বা DP লিখে এলোমেলো ছোট ইনপুটে greedy-র সাথে তুলনা করুন — কয়েকশো পরীক্ষায় অমিল না পেলে আত্মবিশ্বাস বাড়ে (তবে এটা প্রমাণ নয়)। ইন্টারভিউতে মুখে বলুন: “আমার মনে হচ্ছে greedy চলবে, কারণ …; একটি পাল্টা উদাহরণ খুঁজে দেখছি।” এতে ইন্টারভিউয়ার আপনার যুক্তির ধরন দেখেন।
আরেকটি সংকেত: সমস্যায় যদি “পরের সিদ্ধান্ত আগের সিদ্ধান্তের ওপর নির্ভর করে, আর একবার ভুল করলে পরে শোধরানো যায় না” — যেমন knapsack-এ একটি ভারী দামি জিনিস নিলে পরে অনেকগুলো ছোট জিনিসের জায়গা থাকে না — তাহলে সাধারণত DP লাগে। greedy-র প্রমাণ যেখানে কাজ করে, সেখানে সাধারণত “আগে কম নেওয়া কখনো পরের সুযোগ নষ্ট করে না” ধরনের একটি সরল যুক্তি থাকে।
মূল উপলব্ধি: আগে পাল্টা উদাহরণ খুঁজুন, তারপর exchange argument; দুটোর কোনোটি না মিললে ছোট ইনপুটে brute force দিয়ে মিলিয়ে নিন — আর সন্দেহ থাকলে DP নিরাপদ।
প্র ০৩Hash map-এর “O(1)” কতটা সত্যি? কখন সেটা ভেঙে পড়ে, আর ইন্টারভিউতে কীভাবে বলবেন? (কঠিন)
Hash table-এর খোঁজা, ঢোকানো আর মোছা গড়ে O(1) — শর্ত হলো hash function key-গুলোকে প্রায় সমানভাবে ছড়িয়ে দেয় আর load factor একটি সীমার মধ্যে থাকে (পর্ব ৩২)। সবচেয়ে খারাপ ক্ষেত্রে, সব key একই bucket-এ পড়লে, প্রতিটি কাজ O(n) — n টি কাজে O(n²)।
এটা শুধু তত্ত্ব নয়। কনটেস্টে কেউ কেউ ইচ্ছা করে এমন ইনপুট বানান যাতে C++-এর unordered_map-এর ডিফল্ট integer hash-এ সব key একই bucket-এ পড়ে; তখন সমাধান সময়সীমা পেরিয়ে যায়। প্রতিকার: random seed-সহ একটি custom hash, অথবা map (red-black tree, প্রতিটি কাজ সবচেয়ে খারাপ ক্ষেত্রেও O(log n)). ওয়েব সার্ভারেও একই ঝুঁকি — আক্রমণকারী একই hash-এর অনেক key পাঠিয়ে সার্ভার ধীর করতে পারে (hash flooding); এ কারণে Python ৩.৩ থেকে string-এর hash প্রতিবার প্রোগ্রাম চালু হলে এলোমেলো seed দিয়ে হিসাব হয়।
ইন্টারভিউতে নিখুঁত ভাষা: “সময় গড়ে O(n), কারণ hash map-এর কাজ গড়ে O(1); সবচেয়ে খারাপ ক্ষেত্রে O(n²)। নিশ্চয়তা চাইলে ordered map-এ O(n log n).” এই এক বাক্যই দেখায় আপনি Big-O-কে লেবেল হিসেবে নয়, শর্তসহ যুক্তি হিসেবে দেখেন। আরও খেয়াল রাখুন: unordered_map-এ rehash হলে একটি ঢোকানো O(n) সময় নিতে পারে — মোট হিসাবে amortized O(1); আগে থেকে আকার জানলে reserve দিয়ে এড়ানো যায়।
আরেকটি সূক্ষ্মতা: key যদি string হয়, hash হিসাব করতেই key-এর দৈর্ঘ্য L-এর সমান সময় লাগে। তাই “O(1)” আসলে O(L) — group anagrams-এর মতো সমস্যায় জটিলতা লিখতে হয় O(n · m).
মূল উপলব্ধি: hash map গড়ে O(1), নিশ্চিতভাবে নয় — “গড়ে” শব্দটি বলুন, বিপক্ষ ইনপুটের ঝুঁকি জানুন, আর key-এর দৈর্ঘ্যও খরচে ধরুন।
প্র ০৪Tree, graph আর backtracking-এ recursion স্বাভাবিক — কিন্তু C++ আর Python-এ এর সীমা কোথায়? (মাঝারি)
Recursion কোডকে সমস্যার সংজ্ঞার মতো পরিষ্কার করে: “diameter = সন্তানদের উচ্চতার যোগফলের সর্বোচ্চ”। প্রথম সংস্করণ হিসেবে ইন্টারভিউতে এটি প্রায়ই সেরা পছন্দ। কিন্তু প্রতিটি ডাক call stack-এ একটি frame নেয়, আর stack সীমিত।
C++-এ stack-এর আকার অপারেটিং সিস্টেম আর linker ঠিক করে — অনেক Linux ব্যবস্থায় ডিফল্ট ৮ MB, Windows-এ সাধারণত ১ MB. প্রতিটি frame-এ কয়েক ডজন থেকে কয়েকশো byte হলে কয়েক লাখ স্তরের recursion-এ stack overflow হয়ে প্রোগ্রাম কোনো সুন্দর বার্তা ছাড়াই crash করতে পারে। Python-এ সীমাটা আরও কাছে: ডিফল্ট recursion limit ১০০০ (sys.getrecursionlimit()), পেরোলে RecursionError. sys.setrecursionlimit দিয়ে বাড়ানো যায়, কিন্তু আসল C stack তাতে বাড়ে না, তাই খুব বড় মানে interpreter নিজেই crash করতে পারে।
ব্যবহারিক নিয়ম: গভীরতা যদি log n বা ছোট হয় (ভারসাম্যপূর্ণ tree, binary search, n ≤ ২০-এর backtracking) — recursion নিশ্চিন্তে। গভীরতা যদি ইনপুটের আকারের সমান হতে পারে (linked list-এর মতো সরল tree, বড় grid-এ DFS, লম্বা DP chain) — নিজের stack দিয়ে iterative লিখুন, BFS নিন, বা bottom-up DP করুন। অন্তত ঝুঁকিটা মুখে বলুন: “এই tree এক লাখ স্তর গভীর হলে recursion ভাঙবে; তখন আমি explicit stack নেব।”
এ কারণেই এই পাতার grid সমস্যায় BFS নেওয়া হয়েছে আর DP-গুলো টেবিলে লেখা — গভীরতার ঝুঁকি নেই। Tree-এর কোডে recursion রাখা হয়েছে, কারণ সেখানে স্বচ্ছতা বেশি দামি, আর ঝুঁকিটা আলাদা করে বলা আছে।
মূল উপলব্ধি: recursion-এর গভীরতা = ব্যবহৃত stack; গভীরতা ইনপুটের আকারে পৌঁছাতে পারলে iterative বিকল্প জানা চাই, আর Python-এ সীমা মাত্র ১০০০-এর কাছে।
প্র ০৫ইন্টারভিউর এই সমস্যাগুলো কি বাস্তব কাজে লাগে, নাকি শুধু ধাঁধা? (মাঝারি)
এটা সফটওয়্যার শিল্পের পুরোনো বিতর্ক। সমালোচকেরা ঠিকই বলেন: প্রতিদিনের কাজে খুব কম মানুষ নিজে হাতে Floyd-এর চক্র-নির্ণয় লেখেন; বেশি সময় যায় পুরোনো কোড পড়ায়, বাগ খোঁজায় আর দলের সাথে আলোচনায়। শুধু ধাঁধা দিয়ে মাপলে এগিয়ে যান তাঁরা, যাঁরা অনুশীলনের বেশি সময় পেয়েছেন।
তবু প্যাটার্নগুলোর পেছনের ধারণা বাস্তব সিস্টেমের সর্বত্র। Topological sort-এর মতো dependency-ক্রম বের করার কাজ করে build টুল আর package manager — কোন লাইব্রেরি আগে compile বা install হবে। Union-find কাজ করে Kruskal-এর minimum spanning tree-তে (পর্ব ৪৫) আর ছবির সংযুক্ত অঞ্চল চিহ্নিত করায়। Heap দিয়ে চলে অপারেটিং সিস্টেমের timer আর Dijkstra-ভিত্তিক রুট খোঁজা। Sliding window-এর ধারণা নেটওয়ার্কে প্রবাহ নিয়ন্ত্রণে আর rate limiting-এ (“গত ৬০ সেকেন্ডে কতগুলো request”). LCS-এর ধারণা থেকেই diff টুল আর version control-এর পরিবর্তন দেখানো।
আর সবচেয়ে বড় কথা — জটিলতার বোধ। কোনো API-তে লুপের ভেতরে লুপ লিখে প্রতিটি request-এ O(n²) কাজ করালে দশ হাজার ব্যবহারকারীতে সার্ভার ধীর হয়ে যায়; সেই মুহূর্তে “এখানে hash map দিলে O(n)” ভাবতে পারাটাই এই অনুশীলনের ফল। অনেক প্রতিষ্ঠান তাই অ্যালগরিদম ধাপের পাশাপাশি বাস্তবধর্মী ধাপও রাখে — পুরোনো কোডে বাগ খোঁজা, code review, ছোট সিস্টেম ডিজাইন।
মূল উপলব্ধি: ইন্টারভিউর ধাঁধা নিজে লক্ষ্য নয়; এর পেছনের ধারণা আর জটিলতার বোধ বাস্তব সিস্টেমে প্রতিদিন কাজে লাগে — তাই প্যাটার্নের সাথে নিজের প্রজেক্টও গুছিয়ে রাখুন।
১০ · AI দিয়ে প্রস্তুতি — সৎ ব্যবহার আর brute force দিয়ে যাচাই
ইন্টারভিউ চলাকালীন AI-এর সাহায্য অনেক প্রতিষ্ঠানেই নিষিদ্ধ — live coding-এ, online assessment-এ, অনেক সময় take-home-এও। কিছু প্রতিষ্ঠান আবার অনুমতি দিয়ে দেখতে চায় আপনি AI-এর কোড কীভাবে যাচাই করেন। নিয়ম জায়গাভেদে আলাদা আর সময়ের সাথে বদলায় — আগে জিজ্ঞেস করে নিন, আর অনুমতি না থাকলে ব্যবহার করবেন না। লুকিয়ে ব্যবহার অসততা, আর চাকরিতে গিয়ে সেই দক্ষতার ঘাটতি আপনাকেই সামলাতে হবে।
প্রস্তুতির সময় অবশ্য AI হতে পারে চমৎকার mock interview-এর সঙ্গী — শর্ত একটাই: উত্তর নয়, প্রশ্ন আর মূল্যায়ন চাইবেন। আর মনে রাখবেন, DSA-তে AI-এর ভুলগুলো প্রায়ই আত্মবিশ্বাসী শোনায়:
| AI-এর সাধারণ ভুল | কোথায় দেখা যায় | কীভাবে ধরবেন |
|---|---|---|
| ভুল জটিলতা | ভেতরে while দেখে sliding window-কে O(n²) বলা, বা hash map-কে “সবসময় O(1)” | প্রতিটি pointer মোট কতবার সরে, নিজে গুনুন |
| Off-by-one | binary search-এর lo <= hi আর hi = mid একসাথে; window-এ R - L বনাম R - L + 1 | ১, ২ আর ০ উপাদানের ইনপুটে হাতে চালান |
| প্রমাণহীন greedy | coin change, ৩ জনের রিকশা, interval-এ শুরু অনুযায়ী sort | পাল্টা উদাহরণ খুঁজুন, brute force-এর সাথে মেলান |
| বাদ পড়া edge case | prefix map-এ {0: 1} নেই, sliding window-এ last[c] >= L নেই, খালি ইনপুট | "abba", [0, 0, 0], [] — এমন ফাঁদ-ইনপুট দিন |
| Overflow | অংশের সংখ্যা, ক্ষেত্রফল, যোগফল int-এ | সীমার সবচেয়ে বড় মানে ফলের আকার হিসাব করুন |
নিচের prompt দিয়ে একটি পূর্ণ mock interview চালান — AI-কে স্পষ্ট বলা হয়েছে সমাধান না দিতে আর শেষে কঠোর মূল্যায়ন করতে:
নিজের লেখা বা AI-এর দেওয়া সমাধান যাচাই করতে এই পাতার কোডগুলোর কৌশলটাই ব্যবহার করুন: একটি ধীর কিন্তু নিশ্চিতভাবে সঠিক brute force লিখুন, তারপর ছোট এলোমেলো ইনপুটে দুটির ফল মেলান — প্যাটার্ন ৪-এর ৫০০টি ইনপুটের মতো। brute force সহজে ঠিক লেখা যায়, তাই সে-ই বিচারক। ছোট মান-পরিসর (−3 থেকে 3, বা "abc") ইচ্ছা করে নিন — এতে ডুপ্লিকেট, শূন্য আর ফাঁদের কেস নিজে থেকেই তৈরি হয়। নিজের seed দেওয়া generator রাখুন, যাতে ভুল পেলে একই ইনপুট আবার বানানো যায়। এই পদ্ধতির বিস্তারিত অনুশীলন পর্ব ৬৮-এ আর কনটেস্টের প্রসঙ্গে পর্ব ৭২-এ।
AI দিয়ে অনুশীলনের চেকলিস্ট
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমকোডিং ইন্টারভিউতে একটি সমস্যা পেলে ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- ❓ প্রশ্ন করে সমস্যা ও n-এর সীমা পরিষ্কার করা
- 📝 নিজে উদাহরণ ও edge case লেখা
- 🐢 একটি সঠিক brute force সমাধান মুখে বলা
- 🧭 বারবার হওয়া কাজ খুঁজে প্যাটার্ন দিয়ে উন্নতি
- ⌨️ ইন্টারভিউয়ারের সম্মতি নিয়ে কোড লেখা
- 🔍 নিজের উদাহরণে dry run, তারপর জটিলতা বলা
(সহজ) একটি সাজানো array-এ এমন দুটি সংখ্যা খুঁজতে হবে যাদের যোগফল T, আর বাড়তি মেমরি O(1) হতে হবে। কোন পদ্ধতি সবচেয়ে উপযুক্ত?
- Hash map-এ দেখা মান রেখে খোঁজা
- দুটি nested লুপে সব জোড়া
- দুই প্রান্তে two pointer
- প্রতিটি সংখ্যার জন্য T − x-এর binary search
(মাঝারি) ঋণাত্মক সংখ্যাসহ একটি array-এ যোগফল ঠিক k এমন টানা অংশ গুনতে হবে। কোন পদ্ধতি O(n) গড় সময়ে সঠিক উত্তর দেয়?
- Sliding window — যোগফল বেশি হলে বাঁ থেকে ছাঁটা
- Prefix sum আর hash map-এ prefix-এর গণনা
- আগে sort, তারপর two pointer
- Monotonic stack
(মাঝারি) Monotonic stack দিয়ে n টি উপাদানের next greater element বের করার কোডে একটি for-এর ভেতরে while আছে। মোট সময় কত, আর কেন?
- O(n²), কারণ লুপের ভেতরে লুপ
- O(n log n), কারণ stack সাজানো থাকে
- O(n√n)
- Θ(n), কারণ প্রতিটি index সর্বোচ্চ একবার push ও একবার pop হয়
(মাঝারি) “যোগফল k এমন অংশ গোনা”-র কোডে seen map খালি দিয়ে শুরু করা হয়েছে (seen[0] = 1 নেই)। [1, 1, 1], k = 2-এ কী ঘটবে?
- শুরু থেকে শুরু হওয়া অংশ বাদ পড়ে — উত্তর আসে 1, সঠিক 2
- প্রোগ্রাম crash করে
- উত্তর আসে 0
- উত্তর আসে 3
(মাঝারি) n টি বিন্দুর মধ্যে মূলবিন্দুর সবচেয়ে কাছের k টি বের করতে আকার k-এর একটি max-heap ব্যবহার করা হলো (আকার k ছাড়ালে মাথা ফেলে দেওয়া)। সময় ও মেমরি কত?
- সময় O(n log n), মেমরি O(n)
- সময় O(n log k), মেমরি O(k)
- সময় O(n), মেমরি O(1)
- সময় O(n · k), মেমরি O(k)
(মাঝারি) Grid-এ BFS করার সময় কেউ ঘরকে “দেখা হয়েছে” চিহ্ন দেন queue থেকে বের করার সময়, ঢোকানোর সময় নয় (আর বের করার পর আগে দেখা হলে বাদ দেন)। কী ঘটে?
- ভুল উত্তর আসে, কারণ কিছু ঘর বাদ পড়ে
- অসীম লুপ হয়
- কাজ কমে যায়, কারণ চিহ্ন দেওয়া পিছিয়ে যায়
- উত্তর ঠিক থাকে, কিন্তু একই ঘর বহুবার queue-তে ঢুকে বাড়তি সময় ও মেমরি খরচ হয়
(কঠিন) কয়েন {1, 7, 10} দিয়ে 14 টাকা বানাতে “সবচেয়ে বড় কয়েন আগে” greedy কয়টি কয়েন নেয়, আর সেরা উত্তর কত?
- Greedy ৫টি, সেরা ২টি
- Greedy ২টি, সেরা ২টি
- Greedy ৪টি, সেরা ২টি
- Greedy ৫টি, সেরা ৩টি
(কঠিন) Floyd-এর চক্র-নির্ণয়ে slow আর fast দেখা হওয়ার পর একটি pointer head-এ ফিরিয়ে দুটিকেই এক ধাপ করে চালানো হয়। তারা কোথায় মেলে?
- সবসময় যেখানে প্রথমবার দেখা হয়েছিল
- list-এর শেষ node-এ
- চক্রের প্রথম node-এ (যেখানে চক্রে ঢোকা হয়)
- চক্রের ঠিক মাঝের node-এ
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব শব্দ এখানে একসাথে:
মনে রাখুন
- প্রতিটি সমস্যায় সাত ধাপ: বুঝুন → উদাহরণ → brute force → উন্নতি → কোড → পরীক্ষা → জটিলতা, পুরো সময় জোরে ভেবে; আটকে গেলে ছোট উদাহরণ, সহজ সংস্করণ, প্যাটার্নের তালিকা।
- উন্নতির মূল প্রশ্ন: “কোন কাজ বারবার হচ্ছে?” — খোঁজা হলে hash, টানা অংশ হলে window বা prefix, “প্রথম বড়” হলে stack, “সেরা k” হলে heap, “সম্ভব কি” একমুখী হলে উত্তরের ওপর binary search.
- প্রতিটি প্যাটার্নের নিজের ফাঁদ: prefix map-এ {0: 1}, window-এ last[c] ≥ L, binary search-এ lo/hi-র মিল, BFS-এ ঢোকানোর সময় চিহ্ন, backtracking-এ un-choose.
- জটিলতা বলুন শর্তসহ: hash গড়ে O(1), amortized O(n) window আর stack, heap O(n log k), DSU প্রায় O(1), recursion-এর মেমরি O(গভীরতা)।
- greedy সন্দেহজনক হলে পাল্টা উদাহরণ খুঁজুন, না পেলে exchange argument; আর নিজের বা AI-এর প্রতিটি সমাধান brute force আর stress test দিয়ে মিলিয়ে নিন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৭২ · কনটেস্টের অনুশীলন — ICPC-ধাঁচের সমস্যা সেটপরের পর্বএকই প্যাটার্ন, সময়ের চাপ আর লুকানো test case-এর সাথে।
- পর্ব ৬৮ · Complexity আর dry-runরিভিশনহাতে চালানোর অভ্যাস — ইন্টারভিউর “পরীক্ষা” ধাপে সরাসরি কাজে লাগে।
- ডেটা স্ট্রাকচার ও অ্যালগরিদম · কোর্স হোম৭২টি পর্বপুরো রোডম্যাপ — দুর্বল প্যাটার্নের মূল পর্বটি খুঁজে আবার পড়ুন।
- C++ STL অ্যালগরিদমC++sort, lower_bound, nth_element, next_permutation — ইন্টারভিউতে সময় বাঁচানো লাইব্রেরি।
- Python কোডিং ইন্টারভিউPythonএকই ধরনের সমস্যা Python-এর dict, heapq, deque দিয়ে।
- JavaScript কোডিং ইন্টারভিউJavaScriptfront-end আর Node পদের ইন্টারভিউ, machine coding round-সহ।