এই পর্বে যা শিখবেন
- C++ কোডিং ইন্টারভিউতে ইন্টারভিউয়ার আসলে কী কী দেখেন
- যেকোনো নতুন সমস্যায় প্রয়োগ করা যায় এমন সাত ধাপের পদ্ধতি, আর জোরে ভেবে সমাধান করা
- অ্যারে, স্ট্রিং, হ্যাশিং, স্ট্যাক, লিংকড লিস্ট, ট্রি, হিপ, বাইনারি সার্চ ও DP-র ১৩টি ক্লাসিক সমস্যা STL দিয়ে
- LRU cache, Rule of Five টাইপ আর thread-safe queue-এর মতো C++ ডিজাইন সমস্যা
- ক্যাপস্টোন প্রজেক্ট সৎভাবে ব্যাখ্যা করা, আর AI দিয়ে নিজের সমাধান যাচাই করা
১ · C++ কোডিং ইন্টারভিউ কী যাচাই করে
পর্ব ৫৪-এ দেখেছেন লিখিত ও মৌখিক পরীক্ষায় কীভাবে উত্তর গোছাতে হয়। সফটওয়্যার কোম্পানির technical interview একটু আলাদা। এখানে শুধু “উত্তর জানেন কি না” দেখা হয় না। দেখা হয়, অচেনা একটি সমস্যা পেলে আপনি কীভাবে ভাবেন, কীভাবে কথা বলেন, আর কতটা নির্ভরযোগ্য কোড লেখেন।
দেশি স্টার্টআপ, সফটওয়্যার ফার্ম, গেম স্টুডিও, ব্যাংকের আইটি বিভাগ বা বহুজাতিক কোম্পানি — প্রতিষ্ঠানভেদে ধাপের নাম ও সংখ্যা আলাদা। তবে কাঠামোটা সাধারণত এমন:
| ধাপ | কী হয় | প্রস্তুতি |
|---|---|---|
| Phone / HR screen | ১৫–৩০ মিনিটের কথা: পড়াশোনা, প্রজেক্ট, আগ্রহ। কখনো একটি সহজ টেকনিক্যাল প্রশ্ন। | নিজের প্রজেক্ট দুই মিনিটে গুছিয়ে বলা |
| Online assessment | সময় বাঁধা অনলাইন পরীক্ষা: ২–৪টি কোডিং সমস্যা, কখনো C++-এর MCQ. লুকানো test case দিয়ে যন্ত্র নম্বর দেয়। | Online judge-এ সময় ধরে সমাধান (পর্ব ৫০) |
| Live coding | ৪৫–৬০ মিনিট, একজন ইন্টারভিউয়ার, শেয়ার করা editor বা হোয়াইটবোর্ড। ১–২টি সমস্যা আর আলোচনা। | জোরে ভেবে সমাধান — এই পাতার মূল অনুশীলন |
| C++ গভীরতা | ভাষার প্রশ্ন: RAII, move, const, smart pointer, virtual, UB. কখনো ছোট একটি class ডিজাইন। | অংশ ৬–৭, আর পর্ব ৫২–৫৪ |
| Behavioural | দলে কাজ, মতবিরোধ, ব্যর্থতা থেকে শেখা, নিজের প্রজেক্টের সিদ্ধান্ত। | STAR কাঠামোয় ৪–৫টি সত্যি গল্প (অংশ ৮) |
ইন্টারভিউয়ারের মূল্যায়ন-পত্রে কী থাকে
অনেকে ভাবেন, “সঠিক কোড লিখলেই পাস।” বাস্তবে মূল্যায়নে সাধারণত কয়েকটি আলাদা ঘর থাকে। ঠিক কোড লিখেও কেউ পিছিয়ে পড়েন, আবার কোড শেষ না করেও কেউ ভালো করেন — যদি বাকি ঘরগুলো শক্ত হয়।
সমস্যা বোঝা ও যোগাযোগ
লেখার আগে প্রশ্ন করে অস্পষ্টতা দূর করেছেন কি? ভাবনা জোরে বলেছেন কি? ইন্টারভিউয়ার আপনার মাথার ভেতর দেখতে পান না।
সঠিকতা, জটিলতা, edge case
সব ইনপুটে ঠিক কি? খালি ইনপুট, এক উপাদান, ডুপ্লিকেট, overflow ভেবেছেন কি? সময় ও মেমরি Big-O-তে নিজে বলেছেন কি?
কোডের মান
পরিষ্কার নাম, ছোট ফাংশন, const& দিয়ে প্যারামিটার, ঠিক container বাছাই — প্রোডাকশনের মতো কোড, কনটেস্টের শর্টকাট নয়।
C++-এ সাবলীলতা
STL চেনেন কি? reference আর copy-র পার্থক্য, const, RAII, মালিকানা, move — এগুলো স্বাভাবিকভাবে ব্যবহার করেন কি?
C++-নির্দিষ্ট প্রশ্ন দেখতে কেমন
সাধারণ অ্যালগরিদমের প্রশ্ন যেকোনো ভাষায় করা যায়। কিন্তু C++ পদের ইন্টারভিউতে প্রায়ই এমন প্রশ্নও আসে, যেখানে ভাষাটাই আসল বিষয়:
- “RAII দিয়ে একটি class লিখুন” — যেমন একটি ফাইল হ্যান্ডেল, যা destructor-এ নিজেই বন্ধ হয় (পর্ব ১৭, ২১)।
- “একটি Rule-of-Five টাইপ লিখুন” — নিজে মেমরি রাখা একটি Buffer, যার copy গভীর আর move সস্তা (পর্ব ২২, ৩৬)।
- “একটি LRU cache ডিজাইন করুন” — get ও put দুটোই O(1); এখানে container বাছাইটাই পরীক্ষা (পর্ব ৩০–৩২)।
- “একটি thread-safe counter বা queue লিখুন” — mutex, condition variable আর atomic-এর সঠিক ব্যবহার (পর্ব ৪৫–৪৬)।
ইন্টারভিউ একটি কাজের নমুনা। ইন্টারভিউয়ার মনে মনে ভাবেন: “এই মানুষটির সাথে একটি কঠিন বাগ নিয়ে এক ঘণ্টা বসলে কেমন লাগবে?” তাই চুপচাপ নিখুঁত কোডের চেয়ে কথা বলে ধাপে ধাপে এগোনো ভালো কোড প্রায়ই বেশি মূল্য পায়।
এই পাতার ১৬টি সমস্যা
| বিষয় | সমস্যা | রিভিশনের পর্ব |
|---|---|---|
| পদ্ধতি (অংশ ২) | ১. Two sum | ৩১ |
| অ্যারে, স্ট্রিং, হ্যাশিং (অংশ ৩) | ২. শব্দের ক্রম উল্টানো · ৩. Anagram ও group anagrams · ৪. পুনরাবৃত্তিহীন দীর্ঘতম substring · ৫. প্রথম অপুনরাবৃত্ত অক্ষর | ১৩, ১৪, ৩১, ৩৩ |
| স্ট্যাক, লিংকড লিস্ট (অংশ ৪) | ৬. বন্ধনী মেলানো · ৭. Min-stack · ৮. unique_ptr দিয়ে লিস্ট উল্টানো | ১৭, ৩০, ৩৬ |
| ট্রি, হিপ, সার্চ, DP (অংশ ৫) | ৯. BST insert ও inorder · ১০. Level-order BFS · ১১. ঘোরানো অ্যারেতে সার্চ · ১২. k-তম বৃহত্তম · ১৩. সিঁড়ি ও কয়েন চেঞ্জ | ৩০, ৩৩, ৫০ |
| C++ ডিজাইন (অংশ ৬) | ১৪. LRU cache · ১৫. Rule-of-Five Buffer · ১৬. Thread-safe queue ও counter | ২২, ৩২, ৪৫, ৪৬ |
২ · সমস্যা থেকে সমাধান — সাত ধাপের পদ্ধতি
ইন্টারভিউর চাপে মাথা ফাঁকা হয়ে যাওয়া খুব স্বাভাবিক। এর সবচেয়ে ভালো ওষুধ একটি বাঁধা পদ্ধতি — যাতে “এখন কী করব” ভাবতে না হয়, শুধু পরের ধাপে যেতে হয়। ক্রিকেটে ব্যাটসম্যান যেমন প্রতিটি বলের আগে একই রুটিন মানেন — গার্ড নেওয়া, ফিল্ডিং দেখা, তারপর স্টান্স — এটাও তেমন।
এবার পদ্ধতিটি একটি বিখ্যাত সমস্যায় প্রয়োগ করি — সমস্যা ১ · Two sum (সহজ): “একটি std::vector<int> আর একটি target দেওয়া আছে। এমন দুটি ভিন্ন index বের করুন, যাদের মানের যোগফল target.” নিচের ডায়াগ্রামে “পরে” চাপুন:
- ১ · বুঝুনপ্রশ্ন করে অস্পষ্টতা দূর
- ২ · উদাহরণছোট ইনপুট ও edge case
- ৩ · Brute forceআগে একটি সঠিক সমাধান
- ৪ · উন্নত পদ্ধতিবারবার হওয়া কাজ কমান
- ৫ · কোডপরিষ্কার, আধুনিক C++
- ৬ · পরীক্ষাউদাহরণ দিয়ে dry run
- ৭ · জটিলতা ও ফলো-আপBig-O আর নতুন শর্ত
- উত্তর সবসময় থাকবে, নাকি নাও থাকতে পারে? - একই index দুবার নেওয়া যাবে? (না) - vector কি সাজানো? (না) - n কত বড়? মান কত বড়? ঋণাত্মক আছে? - index ফেরত দেব, নাকি মান?
[2, 7, 11, 15], target 9 -> (0, 1) [3, 3], target 6 -> (0, 1) একই মান, ভিন্ন index [-3, 4, 3, 90], target 0 -> (0, 2) ঋণাত্মক [5], target 10 -> নেই 5+5 চলবে না
for i in 0..n-1:
for j in i+1..n-1:
if a[i] + a[j] == target: return (i, j)
সময় O(n²), বাড়তি মেমরি O(1)
ভেতরের লুপ শুধু একটা প্রশ্ন করে: "target - a[j] কি আগে দেখেছি?" -> দেখা মানগুলো unordered_map-এ রাখি (মান -> index), খোঁজা গড়ে O(1)
for j in 0..n-1:
if seen আছে (target - a[j]): return (seen[..], j)
seen[a[j]] = j // আগে খুঁজি, পরে রাখি
return nullopt // "নেই" টাইপেই বলা
j=0: need=3, seen-এ নেই -> seen{3:0}
j=1: need=3, seen-এ আছে(0) -> (0, 1)
উল্টো ক্রমে (আগে রাখি, পরে খুঁজি) [5], 10
-> নিজের সাথেই জোড়া (0, 0) — ভুল!
Hash: সময় O(n) গড়ে, মেমরি O(n)
- vector সাজানো হলে? -> দুই পয়েন্টার, O(1) মেমরি
- সব জোড়া চাই? 3-sum? -> সাজিয়ে + দুই পয়েন্টার, O(n²)
- target - a[j] কি overflow করতে পারে?
- বুঝুন। সমস্যার বিবরণ প্রায়ই ইচ্ছা করেই অসম্পূর্ণ রাখা হয় — আপনি প্রশ্ন করেন কি না, সেটাই দেখা হয়। এই clarifying question-গুলোর উত্তর পুরো সমাধান বদলে দিতে পারে। যেমন vector সাজানো থাকলে একদম অন্য, আরও সহজ পদ্ধতি চলে।
- উদাহরণ। দেওয়া উদাহরণ ছাড়াও নিজে কয়েকটি লিখুন: একই মান দুবার, ঋণাত্মক, উত্তর নেই, একটিমাত্র উপাদান। এগুলোই পরে আপনার test case হবে। Edge case আগে লিখলে কোডে সেগুলো ভুলে যাওয়ার সম্ভাবনা কমে।
- Brute force. সবচেয়ে সরল brute force সমাধানটি মুখে বলুন, যদিও সেটি ধীর। এতে দেখান যে অন্তত একটি সঠিক উত্তর আপনার হাতে আছে। সময় না থাকলে এটাই কোড করবেন — কিছু না থাকার চেয়ে সঠিক ধীর সমাধান অনেক ভালো।
- উন্নত পদ্ধতি। জিজ্ঞেস করুন: “কোন কাজটি বারবার হচ্ছে?” এখানে ভেতরের লুপ শুধু একটি মান খোঁজে। খোঁজা দ্রুত করার যন্ত্র hash table, অর্থাৎ std::unordered_map (পর্ব ৩১)। O(n²) থেকে গড়ে O(n) — বিনিময়ে O(n) বাড়তি মেমরি।
- কোড। ইন্টারভিউয়ার রাজি হলে তবেই কোড লিখুন। প্রথমে ফাংশনের signature: ইনপুট const reference-এ, আর “উত্তর নাও থাকতে পারে” বোঝাতে std::optional. একটি খুঁটিনাটি: আগে খুঁজি, তারপর রাখি — এতে একটি উপাদান নিজের সাথে জোড়া হয় না।
- পরীক্ষা। “শেষ” বলার আগে নিজের উদাহরণ দিয়ে কোডটি লাইন ধরে চালান — এটাই dry run, যা পর্ব ৫২-এ অনুশীলন করেছেন। এখানে দেখা যাচ্ছে, ক্রম উল্টালে
[5], target 10-এ ভুল উত্তর আসত। - জটিলতা ও ফলো-আপ। শেষে সময় ও মেমরি আবার পরিষ্কার করে বলুন। hash-এর O(1) “গড়ে” — খারাপ ক্ষেত্রে collision বাড়লে ধীর হয় (অংশ ৭)। এরপর ইন্টারভিউয়ার শর্ত বদলান; প্রতিটি follow-up-এ আবার একই সাত ধাপ, শুধু দ্রুত।
#include <optional>
#include <print>
#include <unordered_map>
#include <utility>
#include <vector>
std::optional<std::pair<int, int>> two_sum(const std::vector<int>& a, int target) {
std::unordered_map<int, int> seen; // value -> index
seen.reserve(a.size());
for (int j = 0; j < std::ssize(a); ++j) {
if (auto it = seen.find(target - a[j]); it != seen.end())
return std::pair{it->second, j}; // look first ...
seen.emplace(a[j], j); // ... then insert
}
return std::nullopt;
}
int main() {
const std::vector<std::vector<int>> tests{{2, 7, 11, 15}, {3, 3}, {-3, 4, 3, 90}, {5}};
const int targets[]{9, 6, 0, 10};
for (std::size_t t = 0; t < tests.size(); ++t) {
if (auto r = two_sum(tests[t], targets[t])) std::println("({}, {})", r->first, r->second);
else std::println("no pair");
}
}প্রত্যাশিত আউটপুট: (0, 1), (0, 1), (0, 2), no pair. লক্ষ করুন কয়েকটি “প্রোডাকশন” অভ্যাস: ইনপুট const&-এ (কপি নেই), reserve দিয়ে আগেই জায়গা (rehash কম), if-এর ভেতরে initializer (C++17), আর ফেরত টাইপেই “উত্তর নেই” সম্ভাবনা — -1-এর মতো জাদুকরী মান নয়।
যা নিজে থেকে বলবেন: target - a[j] চরম মানে signed overflow করতে পারে, যা undefined behavior (পর্ব ৪৩)। বড় মানের জন্য হিসাবটা long long-এ করুন। emplace আগে থেকে থাকা key বদলায় না, তাই ডুপ্লিকেটে প্রথম index-টাই থাকে।
C-তে এই সমস্যার বড় অংশ ছিল নিজে একটি hash table লেখা; C++-এ STL সেটা দিয়ে দেয়, তাই ইন্টারভিউয়ার আপনার যুক্তি আর container বাছাইয়ে বেশি মন দেন। লিংকড লিস্টে C-তে malloc/free-এর দায় ছিল, এখানে মালিকানা std::unique_ptr-এর। একই সমস্যাগুলোর C সংস্করণ দেখতে পারেন C কোর্সের কোডিং ইন্টারভিউ পর্বে — দুটো পাশাপাশি রাখলে বোঝা যায় C++ কোথায় নিরাপত্তা আর সংক্ষিপ্ততা যোগ করে।
সময় ভাগের মোটা হিসাব (৪৫ মিনিট): বোঝা ও উদাহরণ ৫ মিনিট, পদ্ধতি ১০ মিনিট, কোড ১৫–২০ মিনিট, পরীক্ষা ৫ মিনিট, বাকিটা ফলো-আপ। কোড শুরুর আগে সম্মতি নিন: “এই পদ্ধতিতে লিখি?” আর ইন্টারভিউতে bits/stdc++.h বা using namespace std; এড়িয়ে চলুন — কনটেস্টে চলে (পর্ব ৫০), কিন্তু প্রোডাকশন-মানের কোড দেখাতে চাইলে দরকারি header আর std:: লিখুন।
৩ · অ্যারে, স্ট্রিং ও হ্যাশিং — প্রথম সারির প্রশ্ন
প্রায় প্রতিটি ইন্টারভিউর প্রথম প্রশ্ন এই গোত্রের। দেখতে সহজ, কিন্তু এখানেই বোঝা যায় আপনি std::string_view, const&, range-for আর STL algorithm স্বাভাবিকভাবে ব্যবহার করেন কি না। রিভিশনের জন্য: পর্ব ১৩ · vector ও array, পর্ব ১৪ · string ও string_view, পর্ব ৩১ · map ও unordered_map, পর্ব ৩৩ · STL algorithm.
সমস্যা ২ · বাক্যের শব্দগুলোর ক্রম উল্টানো (সহজ)
প্রশ্ন: “the sky is blue” থেকে “blue is sky the” বানান। শুরু, শেষ বা মাঝের বাড়তি space বাদ যাবে, শব্দের মাঝে থাকবে ঠিক একটি space.
জিজ্ঞেস করে নিন: space ছাড়া অন্য whitespace (tab) আছে? নতুন string ফেরত দেব, নাকি in-place বদলাব? খালি বা শুধু space থাকা ইনপুটে কী ফেরত দেব?
পদ্ধতি ও জটিলতা: std::istringstream দিয়ে শব্দগুলো আলাদা করুন (>> নিজেই whitespace বাদ দেয়), vector-এ রাখুন, std::ranges::reverse করুন, তারপর জোড়া দিন। সময় O(n), মেমরি O(n).
#include <algorithm>
#include <print>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
std::string reverse_words(std::string_view s) {
std::istringstream in{std::string{s}};
std::vector<std::string> words;
for (std::string w; in >> w;) words.push_back(std::move(w));
std::ranges::reverse(words);
std::string out;
for (const auto& w : words) {
if (!out.empty()) out += ' ';
out += w;
}
return out;
}
int main() {
for (std::string_view s : {"the sky is blue", " hello world ", "single", " "})
std::println("[{}]", reverse_words(s));
}প্রত্যাশিত আউটপুট: [blue is sky the], [world hello], [single], []. প্যারামিটার string_view হওয়ায় literal, std::string বা অন্য কোনো string-এর অংশ — সবই কপি ছাড়া পাঠানো যায়। std::move(w)-এ শব্দটি vector-এ সরে যায়; লুপের পরের ঘুরে >> আবার w-তে নতুন মান লেখে, তাই moved-from অবস্থা নিয়ে সমস্যা নেই।
ফলো-আপ: বাড়তি মেমরি O(1) রেখে in-place করুন (একটি std::string&-এ)। কৌশল: পুরো string উল্টান, তারপর প্রতিটি শব্দ আবার আলাদা করে উল্টান, শেষে বাড়তি space চেপে দিন — two pointers দিয়ে লেখা আর পড়ার অবস্থান আলাদা রেখে।
সমস্যা ৩ · Anagram যাচাই, আর group anagrams (সহজ → মাঝারি)
প্রশ্ন: (ক) দুটি শব্দ পরস্পরের anagram কি না বলুন — “listen” আর “silent” হ্যাঁ। (খ) একগুচ্ছ শব্দকে anagram-দলে ভাগ করুন।
জিজ্ঞেস করে নিন: শুধু ছোট হাতের ইংরেজি অক্ষর, নাকি যেকোনো byte? বড়-ছোট হাতের পার্থক্য ধরব? দলগুলোর ক্রম কি গুরুত্বপূর্ণ?
পদ্ধতি ও জটিলতা: (ক) দুই রকম: দুটোই sort করে তুলনা — O(n log n); অথবা প্রতিটি অক্ষর গোনা — O(n) সময়, ২৫৬ ঘরের নির্দিষ্ট মেমরি। (খ) প্রতিটি শব্দের অক্ষর সাজালে একটি “পরিচয়” (key) পাওয়া যায় — “eat”, “tea”, “ate” সবার key “aet”. key থেকে শব্দ-তালিকার map বানালেই দল হয়ে যায়। k দৈর্ঘ্যের n টি শব্দে সময় O(n · k log k).
#include <algorithm>
#include <array>
#include <map>
#include <print>
#include <string>
#include <string_view>
#include <vector>
bool is_anagram(std::string_view a, std::string_view b) {
if (a.size() != b.size()) return false;
std::array<int, 256> count{};
for (unsigned char c : a) ++count[c];
for (unsigned char c : b)
if (--count[c] < 0) return false;
return true;
}
std::vector<std::vector<std::string>> group_anagrams(const std::vector<std::string>& words) {
std::map<std::string, std::vector<std::string>> groups; // sorted letters -> words
for (const auto& w : words) {
std::string key = w;
std::ranges::sort(key);
groups[key].push_back(w);
}
std::vector<std::vector<std::string>> out;
for (auto& entry : groups) out.push_back(std::move(entry.second));
return out;
}
int main() {
std::println("{} {} {}", is_anagram("listen", "silent"), is_anagram("rat", "car"), is_anagram("", ""));
for (const auto& g : group_anagrams({"eat", "tea", "tan", "ate", "nat", "bat"})) std::println("{}", g);
}প্রত্যাশিত আউটপুট: প্রথম লাইনে true false true, তারপর ["bat"], ["eat", "tea", "ate"], ["tan", "nat"] (C++23-এ std::println vector সরাসরি ছাপাতে পারে)। গোনার সময় unsigned char ব্যবহার জরুরি: সাধারণ char অনেক platform-এ signed, তাই ১২৭-এর বেশি byte (যেমন UTF-8-এর বাংলা অক্ষরের অংশ) ঋণাত্মক index হয়ে অ্যারের বাইরে পড়ত।
কেন std::map? আউটপুটের ক্রম নির্দিষ্ট রাখতে। ইন্টারভিউতে সাধারণত unordered_map বলা হয় — গড়ে দ্রুত, কিন্তু দলগুলোর ক্রম অনির্দিষ্ট। এই trade-off-টা নিজে বলুন। ফলো-আপ: sort না করে key বানান — ২৬টি গণনা দিয়ে একটি string key — তাহলে সময় O(n · k).
সমস্যা ৪ · পুনরাবৃত্তিহীন দীর্ঘতম substring (মাঝারি)
প্রশ্ন: একটি string-এর এমন দীর্ঘতম টানা অংশের দৈর্ঘ্য বের করুন, যেখানে কোনো অক্ষর দুবার নেই। “abcabcbb” → 3 (“abc”), “pwwkew” → 3 (“wke”).
জিজ্ঞেস করে নিন: substring (টানা অংশ) নাকি subsequence (মাঝে বাদ দেওয়া চলে)? অক্ষরের সেট কত বড় — ASCII নাকি Unicode? খালি string-এ উত্তর 0 তো?
পদ্ধতি ও জটিলতা: Brute force: প্রতিটি শুরু থেকে যতদূর যাওয়া যায় — O(n²) বা তার বেশি। উন্নত: sliding window. একটি জানালা [left, right] ডানে বাড়ান; নতুন অক্ষরটি জানালার ভেতরে আগে থাকলে left-কে তার আগের অবস্থানের ঠিক পরে সরান। প্রতিটি অক্ষরের শেষ দেখা index একটি অ্যারেতে রাখুন। সময় O(n), মেমরি O(অক্ষরের সেট). নিচে “abba” দিয়ে ধাপে ধাপে দেখুন — এখানে একটি সূক্ষ্ম ফাঁদ আছে:
1ptrdiff_t longest_unique(string_view s) {2 last.fill(-1);3 ptrdiff_t best = 0, left = 0;4 for (right = 0; right < ssize(s); ++right) {5 c = s[right];6 left = max(left, last[c] + 1);7 last[c] = right;8 best = max(best, right - left + 1);9 }10 return best;11}
| right | |
| c | |
| left | |
| last[a] | |
| last[b] | |
| best |
- প্রস্তুতি। প্রতিটি অক্ষরের “শেষ দেখা index” = -1, অর্থাৎ এখনো দেখা হয়নি। জানালা খালি,
best= 0. - right = 0, 'a'.
last[a] + 1= 0, তাইleftবদলায় না। জানালা “a”, দৈর্ঘ্য ১,best= 1. - right = 1, 'b'. 'b' আগে দেখা হয়নি। জানালা “ab”,
best= 2. - right = 2, আবার 'b'. আগের 'b' ছিল index 1-এ, তাই
leftলাফিয়ে 2-এ যায় — জানালা এখন শুধু “b”. প্রতিটি অক্ষর জানালায় একবার ঢোকে,leftশুধু সামনে যায় — তাই মোট কাজ O(n). - right = 3, আবার 'a' — ফাঁদ। আগের 'a' ছিল index 0-এ, অর্থাৎ জানালার বাইরে।
maxনা থাকলেleftপিছিয়ে 1-এ যেত, আর জানালা হতো “bba” — যেখানে 'b' দুবার!maxনিশ্চিত করেleftকখনো পেছায় না। জানালা “ba”, দৈর্ঘ্য ২। - শেষ। উত্তর 2. এই ধরনের ছোট, চতুর test (“abba”) নিজে থেকে বানানো ইন্টারভিউতে খুব ভালো সংকেত — এটি দেখায় আপনি কোডের invariant (জানালায় কোনো ডুপ্লিকেট নেই) নিয়ে ভাবছেন।
#include <algorithm>
#include <array>
#include <print>
#include <string_view>
// Length of the longest substring with no repeated character (sliding window).
std::ptrdiff_t longest_unique(std::string_view s) {
std::array<std::ptrdiff_t, 256> last; // last index of each byte
last.fill(-1);
std::ptrdiff_t best = 0, left = 0; // window is s[left..right]
for (std::ptrdiff_t right = 0; right < std::ssize(s); ++right) {
const auto c = static_cast<unsigned char>(s[right]);
left = std::max(left, last[c] + 1); // jump past the previous copy
last[c] = right;
best = std::max(best, right - left + 1);
}
return best;
}
int main() {
for (std::string_view s : {"abcabcbb", "bbbbb", "pwwkew", "", "abba"})
std::println("\"{}\" -> {}", s, longest_unique(s));
}প্রত্যাশিত আউটপুট: 3, 1, 3, 0, 2 — পাঁচটি ইনপুটের জন্য। সব index একই signed টাইপে (std::ptrdiff_t) রাখায় right - left + 1-এ signed-unsigned মিশ্রণ নেই, আর -1 মানটিও স্বাভাবিকভাবে রাখা যায়। std::ssize (C++20) signed আকার দেয়।
ফলো-আপ: Unicode হলে ২৫৬ ঘরের অ্যারে চলবে না — unordered_map লাগবে, আর আগে UTF-8 থেকে code point বের করতে হবে। “সর্বোচ্চ k টি ভিন্ন অক্ষর” রূপভেদে একই জানালা, শুধু শর্ত বদলায় — গণনার map রেখে বাম প্রান্ত সরান (পর্ব ৫০)।
সমস্যা ৫ · প্রথম অপুনরাবৃত্ত অক্ষর (সহজ)
প্রশ্ন: string-এর প্রথম যে অক্ষর ঠিক একবারই আছে, সেটি বের করুন। “swiss” → 'w', “aabb” → নেই।
পদ্ধতি ও জটিলতা: দুবার পার হোন — প্রথমবার গুনুন, দ্বিতীয়বার মূল ক্রমে প্রথম যার গণনা ১, সেটি ফেরত দিন। সময় O(n), মেমরি নির্দিষ্ট (২৫৬ ঘর)। একবার পার হয়ে map-এর ক্রমে খুঁজলে ভুল হয়: unordered_map-এর কোনো নির্দিষ্ট ক্রম নেই।
std::optional<char> first_unique(std::string_view s) {
std::array<int, 256> count{};
for (unsigned char c : s) ++count[c];
for (char c : s)
if (count[static_cast<unsigned char>(c)] == 1) return c;
return std::nullopt; // "aabb": every character repeats
}Edge case ও ফলো-আপ: খালি string → nullopt; “leetcode” → 'l'. ফলো-আপে প্রায়ই আসে: অক্ষরগুলো একটি stream-এ একটি একটি করে আসছে, প্রতিবার “এ পর্যন্ত প্রথম অপুনরাবৃত্ত” বলতে হবে। তখন গণনার সাথে একটি queue রাখুন, আর সামনের অক্ষরের গণনা ১-এর বেশি হলে সেটি ফেলে দিন — প্রতি অক্ষরে গড়ে O(1).
৪ · স্ট্যাক, কিউ ও লিংকড লিস্ট
এই গোত্রে দুটি জিনিস একসাথে যাচাই হয়: ঠিক data structure চেনা, আর C++-এ মালিকানা (ownership) পরিষ্কার রাখা। রিভিশনের জন্য: পর্ব ১৭ · RAII ও smart pointer, পর্ব ৩০ · sequence container, পর্ব ৩৬ · move semantics.
সমস্যা ৬ · বন্ধনী ঠিকঠাক মেলানো কি না (সহজ)
প্রশ্ন: (), [], {} মেশানো একটি string-এ প্রতিটি খোলা বন্ধনী কি সঠিক ক্রমে সঠিক ধরনের বন্ধনী দিয়ে বন্ধ হয়েছে?
জিজ্ঞেস করে নিন: অন্য অক্ষর থাকবে কি (যেমন একটি গাণিতিক রাশি)? থাকলে উপেক্ষা করব তো? খালি string-কে balanced ধরব?
পদ্ধতি ও জটিলতা: শেষে যে খুলেছে, তাকেই আগে বন্ধ হতে হবে — এটাই LIFO, অর্থাৎ std::stack. খোলা বন্ধনী push; বন্ধ বন্ধনী এলে top-এর সাথে মিলিয়ে pop. সময় O(n), মেমরি O(n).
#include <print>
#include <stack>
#include <string_view>
bool is_balanced(std::string_view s) {
std::stack<char> open;
for (const char c : s) {
switch (c) {
case '(': case '[': case '{': open.push(c); break;
case ')': case ']': case '}': {
const char want = c == ')' ? '(' : c == ']' ? '[' : '{';
if (open.empty() || open.top() != want) return false;
open.pop();
break;
}
default: break; // ignore every other character
}
}
return open.empty(); // a leftover opener = unbalanced
}
int main() {
for (std::string_view s : {"()[]{}", "([)]", "{[()]}", "((", ")(", "", "f(a[i]) {}"})
std::println("\"{}\" -> {}", s, is_balanced(s));
}প্রত্যাশিত আউটপুট: সাতটি ইনপুটে ক্রমে true false true false false true true. দুটি ফাঁদ এখানে ধরা পড়ে: ")(" — খালি stack-এ top() ডাকা undefined behavior, তাই আগে empty() যাচাই; আর "((" — লুপ শেষে stack খালি না হলে উত্তর false.
ফলো-আপ: শুধু এক ধরনের বন্ধনী হলে stack-ও লাগে না — একটি গণনা যথেষ্ট (O(1) মেমরি)। প্রথম ভুলের অবস্থান ফেরত দিন। সবচেয়ে কম কয়টি বন্ধনী মুছলে balanced হবে?
সমস্যা ৭ · Min-stack — সবসময় O(1)-এ সর্বনিম্ন (মাঝারি)
প্রশ্ন: একটি stack class ডিজাইন করুন, যেখানে push, pop, top আর min — চারটিই O(1).
জিজ্ঞেস করে নিন: খালি stack-এ pop বা min ডাকলে কী হবে — exception, নাকি precondition (ডাকা নিষেধ)? টাইপ কি শুধু int, নাকি template?
পদ্ধতি ও জটিলতা: প্রতিটি উপাদানের পাশে “এখন পর্যন্ত নিচের সবার সর্বনিম্ন” জমা রাখুন। pop করলে সেই তথ্যও নিজে থেকে চলে যায়, আগের সর্বনিম্ন ফিরে আসে। সব কাজ O(1), মেমরি ২n.
#include <algorithm>
#include <print>
#include <stdexcept>
#include <vector>
class MinStack {
public:
void push(int x) {
const int m = data_.empty() ? x : std::min(x, data_.back().min);
data_.push_back({x, m});
}
void pop() { check(); data_.pop_back(); }
[[nodiscard]] int top() const { check(); return data_.back().value; }
[[nodiscard]] int min() const { check(); return data_.back().min; }
[[nodiscard]] bool empty() const noexcept { return data_.empty(); }
private:
struct Entry { int value; int min; }; // min of this entry and all below it
void check() const { if (data_.empty()) throw std::out_of_range("MinStack is empty"); }
std::vector<Entry> data_;
};
int main() {
MinStack s;
for (int x : {5, 3, 7, 3, 1}) { s.push(x); std::print("{} ", s.min()); }
std::println("");
while (!s.empty()) { std::print("{}/{} ", s.top(), s.min()); s.pop(); }
std::println("");
try { s.pop(); } catch (const std::out_of_range& e) { std::println("error: {}", e.what()); }
}প্রত্যাশিত আউটপুট: 5 3 3 3 1, তারপর 1/1 3/3 7/3 3/3 5/5, শেষে error: MinStack is empty. লক্ষ করুন ডুপ্লিকেট সর্বনিম্ন (দুটি 3) কোনো বিশেষ ব্যবস্থা ছাড়াই ঠিক চলছে — শুধু “আলাদা min-stack-এ ছোট হলে push” কৌশলে এটাই সাধারণ বাগ (< বনাম <=)।
ডিজাইনের কথা: std::vector-এর ওপর গড়া (পর্ব ৩০), private অংশে ডেটা — বাইরে থেকে invariant ভাঙা যায় না (পর্ব ২০)। [[nodiscard]] ভুল করে ফল ফেলে দিলে warning দেয়। ফলো-আপ: বাড়তি মেমরি কমান — শুধু সর্বনিম্ন বদলালে আলাদা stack-এ রাখুন; অথবা template বানিয়ে যেকোনো তুলনাযোগ্য টাইপে চালান (পর্ব ২৯, ৪০)।
সমস্যা ৮ · unique_ptr দিয়ে লিংকড লিস্ট উল্টানো (সহজ, কিন্তু খুব জনপ্রিয়)
প্রশ্ন: একমুখী linked list-এর head দেওয়া আছে। নতুন node না বানিয়ে লিস্টটি উল্টে নতুন head ফেরত দিন।
জিজ্ঞেস করে নিন: node-এর মালিক কে — raw pointer, নাকি std::unique_ptr? খালি লিস্ট আসতে পারে? (আসতে পারে।)
পদ্ধতি ও জটিলতা: C-র মতোই তিনটি হাত — উল্টানো অংশ (prev), বর্তমান node (head), বাকি অংশ (rest). পার্থক্য হলো, প্রতিটি “তীর ঘোরানো” এখানে মালিকানার হস্তান্তর, তাই প্রতিটি লাইনে std::move. সময় O(n), মেমরি O(1).
#include <initializer_list>
#include <iterator>
#include <memory>
#include <print>
#include <utility>
struct Node {
int val;
std::unique_ptr<Node> next; // a node owns the rest of the list
};
std::unique_ptr<Node> reverse(std::unique_ptr<Node> head) {
std::unique_ptr<Node> prev; // reversed part, starts empty
while (head) {
auto rest = std::move(head->next); // 1. hold on to the rest
head->next = std::move(prev); // 2. turn the arrow around
prev = std::move(head); // 3. grow the reversed part
head = std::move(rest); // 4. step forward
}
return prev;
}
std::unique_ptr<Node> make_list(std::initializer_list<int> vals) {
std::unique_ptr<Node> head;
for (auto it = std::rbegin(vals); it != std::rend(vals); ++it)
head = std::make_unique<Node>(*it, std::move(head));
return head;
}
void print(const Node* p) {
for (; p; p = p->next.get()) std::print("{} ", p->val);
std::println("|");
}
int main() {
print(reverse(make_list({1, 2, 3, 4, 5})).get());
print(reverse(nullptr).get());
print(reverse(make_list({9})).get());
}প্রত্যাশিত আউটপুট: 5 4 3 2 1 |, খালি লিস্টে শুধু |, আর 9 |. কোথাও delete নেই, তবু কোনো leak নেই — main-এর প্রতিটি অস্থায়ী লিস্ট লাইনের শেষে নিজেই মুছে যায় (RAII). reverse মান দিয়ে unique_ptr নেয়: signature-ই বলে দেয় “পুরো লিস্টের মালিকানা আমাকে দিন, নতুন মালিকানা ফেরত নিন।” পর্যবেক্ষণের জন্য print নেয় শুধু const Node* — মালিকানা নয়।
লুকানো ফাঁদ যা নিজে বলবেন: এই Node-এর destructor recursive — head মুছলে next মোছে, সে তার next… দশ লাখ node-এর লিস্টে এটি call stack ফুরিয়ে stack overflow ঘটাতে পারে। সমাধান: লিস্ট class-এর destructor-এ লুপ চালিয়ে একটি একটি করে node ছাড়ুন (অংশ ৭, প্র ০৩)। ফলো-আপ: recursive সংস্করণ লিখুন; k-টি করে দলে উল্টান; বাস্তব কোডে কেন প্রায়ই std::vector বা index-ভিত্তিক লিস্ট দ্রুত (cache, পর্ব ৪৯)।
৫ · ট্রি, হিপ, বাইনারি সার্চ ও DP
এই অংশের সমস্যাগুলোতে কৌশল চেনাই আসল: “সাজানো” শুনলে binary search, “স্তরে স্তরে” শুনলে queue, “সবচেয়ে বড় k টি” শুনলে heap, “কত উপায়ে / সর্বনিম্ন কত” শুনলে DP. রিভিশনের জন্য: পর্ব ১৭, পর্ব ৩০ · sequence container ও adaptor, পর্ব ৩৩ · STL algorithm, পর্ব ৩৪ · lambda, পর্ব ৫০ · কনটেস্টের প্যাটার্ন।
সমস্যা ৯ · BST-তে insert আর inorder traversal (সহজ)
প্রশ্ন: একটি binary search tree-তে key যোগ করুন, তারপর সব key সাজানো ক্রমে বের করুন।
জিজ্ঞেস করে নিন: ডুপ্লিকেট key কোথায় যাবে — বাদ, নাকি ডানে? tree কি balanced রাখতে হবে? (না হলে খারাপ ক্ষেত্রে উচ্চতা n.)
পদ্ধতি ও জটিলতা: insert: root থেকে ছোট হলে বাঁয়ে, নইলে ডানে, খালি জায়গা পেলে সেখানে নতুন node. Inorder traversal (বাঁ → root → ডান) BST-তে সবসময় সাজানো ক্রম দেয়। insert O(h), traversal O(n); h হলো উচ্চতা — balanced হলে log n, সাজানো ইনপুটে n.
#include <memory>
#include <print>
#include <vector>
struct Tree {
explicit Tree(int k) : key(k) {}
int key;
std::unique_ptr<Tree> left, right;
};
void insert(std::unique_ptr<Tree>& root, int key) { // iterative: no deep recursion
auto* slot = &root;
while (*slot) slot = key < (*slot)->key ? &(*slot)->left : &(*slot)->right;
*slot = std::make_unique<Tree>(key);
}
void inorder(const Tree* t, std::vector<int>& out) { // left, root, right = sorted
if (!t) return;
inorder(t->left.get(), out);
out.push_back(t->key);
inorder(t->right.get(), out);
}
int main() {
std::unique_ptr<Tree> root;
for (int k : {50, 30, 70, 20, 40, 60, 80, 35}) insert(root, k);
std::vector<int> sorted;
inorder(root.get(), sorted);
std::println("inorder: {}", sorted);
std::vector<int> none;
inorder(nullptr, none);
std::println("empty: {}", none);
}প্রত্যাশিত আউটপুট: inorder: [20, 30, 35, 40, 50, 60, 70, 80] আর empty: []. insert-এ একটি সুন্দর কৌশল: slot হলো “যে unique_ptr-এ নতুন node বসবে, তার ঠিকানা”। এতে root খালি থাকা আর গভীরে বসানো — দুটো একই কোডে হয়, আলাদা if লাগে না।
Edge case ও ফলো-আপ: সাজানো ইনপুট (1, 2, 3, …) দিলে tree একটি লম্বা লাইন হয়ে যায় — তখন recursive inorder গভীর হয় আর destructor-ও recursive, তাই বিশাল ইনপুটে stack overflow-র ঝুঁকি। বাস্তবে std::set/std::map (সাধারণত red-black tree, পর্ব ৩১) ব্যবহার করুন। ফলো-আপ: একটি tree বৈধ BST কি না যাচাই করুন (প্রতিটি node-এর জন্য অনুমোদিত সীমা নিচে পাঠিয়ে); k-তম ছোট key বের করুন।
সমস্যা ১০ · Level-order traversal — স্তরে স্তরে BFS (মাঝারি)
প্রশ্ন: binary tree-এর key-গুলো স্তর ধরে ফেরত দিন: [[50], [30, 70], [20, 40, 60, 80], [35]].
পদ্ধতি ও জটিলতা: Level-order মানে BFS: যে node আগে দেখা, তার সন্তানদের আগে দেখা — FIFO, অর্থাৎ std::queue. স্তর আলাদা করার কৌশল: প্রতিটি স্তর শুরুর সময় queue-তে ঠিক কয়টি node আছে গুনে রাখুন, ঠিক ততগুলোই বের করুন। সময় O(n), মেমরি O(সবচেয়ে চওড়া স্তর).
std::vector<std::vector<int>> level_order(const Tree* root) {
std::vector<std::vector<int>> levels;
std::queue<const Tree*> q;
if (root) q.push(root);
while (!q.empty()) {
auto& level = levels.emplace_back(); // C++17: returns a reference
for (auto n = q.size(); n > 0; --n) { // exactly one level per pass
const Tree* t = q.front();
q.pop();
level.push_back(t->key);
if (t->left) q.push(t->left.get());
if (t->right) q.push(t->right.get());
}
}
return levels;
}Edge case ও ফলো-আপ: খালি tree → খালি ফল (প্রথম if না থাকলে nullptr queue-তে ঢুকত)। queue-তে raw const Tree* রাখা ঠিক আছে, কারণ মালিক tree নিজেই, আর traversal চলাকালীন tree বদলাচ্ছে না। ফলো-আপ: zigzag ক্রম (প্রতি বিজোড় স্তর উল্টে দিন — std::ranges::reverse); প্রতিটি স্তরের ডানদিকের node (“right view”); tree-এর সর্বনিম্ন গভীরতা — BFS প্রথম পাতায় পৌঁছেই থামতে পারে, DFS পারে না।
সমস্যা ১১ · ঘোরানো সাজানো অ্যারেতে সার্চ (মাঝারি)
প্রশ্ন: একটি সাজানো অ্যারে কোনো অজানা বিন্দুতে ঘোরানো হয়েছে, যেমন [4, 5, 6, 7, 0, 1, 2]. O(log n)-এ target-এর index বের করুন, না থাকলে -1.
জিজ্ঞেস করে নিন: মানগুলো কি ভিন্ন? (ডুপ্লিকেট থাকলে খারাপ ক্ষেত্রে O(n) লাগে।) খালি অ্যারে আসতে পারে?
পদ্ধতি: Binary search-এর রূপভেদ। যেকোনো mid-এ দুই অর্ধেকের অন্তত একটি পুরোপুরি সাজানো। সাজানো অর্ধেকের সীমার ভেতরে target থাকলে সেদিকে যান, নইলে অন্যদিকে। প্রতি ধাপে খোঁজার জায়গা অর্ধেক — O(log n), মেমরি O(1).
#include <print>
#include <vector>
// Index of target in a rotated sorted array of distinct values, or -1.
int search_rotated(const std::vector<int>& a, int target) {
int lo = 0, hi = static_cast<int>(a.size()) - 1;
while (lo <= hi) {
const int mid = lo + (hi - lo) / 2;
if (a[mid] == target) return mid;
if (a[lo] <= a[mid]) { // left half is sorted
if (a[lo] <= target && target < a[mid]) hi = mid - 1;
else lo = mid + 1;
} else { // right half is sorted
if (a[mid] < target && target <= a[hi]) lo = mid + 1;
else hi = mid - 1;
}
}
return -1;
}
int main() {
const std::vector<int> a{4, 5, 6, 7, 0, 1, 2};
for (int t : {0, 4, 2, 7, 3}) std::print("{} ", search_rotated(a, t));
std::println("| {} {}", search_rotated({}, 1), search_rotated({1}, 1));
}প্রত্যাশিত আউটপুট: 4 0 6 3 -1 | -1 0. hi-কে int-এ রাখা ইচ্ছাকৃত: a.size() - 1 unsigned হলে খালি অ্যারেতে সেটি বিশাল সংখ্যা হয়ে যেত। lo + (hi - lo) / 2 লেখা (lo + hi) / 2-এর overflow এড়ায় — যদিও এই আকারে সমস্যা নেই, অভ্যাসটা ইন্টারভিউয়ার লক্ষ করেন। C++20-এ std::midpoint-ও আছে।
ফলো-আপ: ঘোরানোর বিন্দু (সর্বনিম্ন উপাদান) খুঁজুন; ডুপ্লিকেট থাকলে কী বদলায়; সাধারণ সাজানো অ্যারেতে নিজে না লিখে কখন std::ranges::lower_bound ব্যবহার করবেন (প্রায় সবসময় — পর্ব ৩৩)।
সমস্যা ১২ · k-তম বৃহত্তম সংখ্যা (মাঝারি)
প্রশ্ন: একটি অসাজানো vector-এর k-তম বৃহত্তম মান বের করুন। [3, 2, 1, 5, 6, 4], k = 2 → 5.
পদ্ধতি ও জটিলতা: তিনটি উত্তর, তিনটি trade-off — তিনটিই বলুন: (ক) পুরো sort: O(n log n). (খ) k আকারের min-heap: প্রতিটি মান ঢোকান, আকার k ছাড়ালে সবচেয়ে ছোটটি ফেলুন; শেষে top-ই উত্তর — O(n log k) সময়, O(k) মেমরি, আর ডেটা stream-এ এলেও চলে। (গ) std::nth_element: গড়ে O(n), কিন্তু ইনপুট বদলে দেয়।
int kth_largest(const std::vector<int>& a, int k) { // needs 1 <= k <= a.size()
std::priority_queue<int, std::vector<int>, std::greater<>> heap; // min-heap
for (int x : a) {
heap.push(x);
if (std::ssize(heap) > k) heap.pop(); // keep only the k largest
}
return heap.top();
}
// Alternative, average O(n), reorders a copy:
// std::ranges::nth_element(v, v.begin() + (k - 1), std::greater<>{}); return v[k - 1];Edge case ও ফলো-আপ: std::priority_queue ডিফল্টে max-heap — top-এ সবচেয়ে বড়; std::greater<> দিলে min-heap. এটা গুলিয়ে ফেলা খুব সাধারণ ভুল। k অবৈধ হলে (0, বা n-এর বেশি) খালি heap-এ top() undefined behavior — precondition লিখে রাখুন বা std::optional ফেরত দিন। ডুপ্লিকেট গোনা হবে কি না ([3, 3, 2], k = 2 → 3 নাকি 2?) জিজ্ঞেস করুন। ফলো-আপ: সবচেয়ে ঘন ঘন আসা k টি শব্দ (map দিয়ে গোনা + heap).
সমস্যা ১৩ · সিঁড়ি বেয়ে ওঠা ও কয়েন চেঞ্জ (মাঝারি)
প্রশ্ন: (ক) n ধাপের সিঁড়িতে একবারে ১ বা ২ ধাপ উঠা যায় — কত উপায়ে উপরে পৌঁছানো যায়? (খ) নির্দিষ্ট মানের কয়েন (প্রতিটি অসীম সংখ্যক) দিয়ে একটি টাকার পরিমাণ বানাতে সবচেয়ে কম কয়টি কয়েন লাগে?
পদ্ধতি: দুটোই dynamic programming. (ক) শেষ পদক্ষেপ ১ বা ২ ধাপের, তাই ways(n) = ways(n-1) + ways(n-2) — Fibonacci; শুধু শেষ দুটি মান রাখলে O(n) সময়, O(1) মেমরি। (খ) dp[x] = x বানানোর সর্বনিম্ন কয়েন; প্রতিটি কয়েন c-এর জন্য dp[x - c] + 1 চেষ্টা। সময় O(amount · কয়েনের সংখ্যা)। সরাসরি recursion করলে একই উপসমস্যা বহুবার হিসাব হয় — exponential; ফল জমিয়ে রাখা (memoization) বা নিচ থেকে টেবিল ভরাই সমাধান।
#include <print>
#include <vector>
long long climb_ways(int n) { // 1 or 2 steps at a time
long long a = 1, b = 1; // ways(0), ways(1)
for (int i = 2; i <= n; ++i) {
const long long c = a + b; // ways(i) = ways(i-1) + ways(i-2)
a = b;
b = c;
}
return b;
}
// Fewest coins that make `amount`, or -1 if impossible (bottom-up DP).
int min_coins(const std::vector<int>& coins, int amount) {
const int INF = amount + 1; // larger than any real answer
std::vector<int> dp(amount + 1, INF);
dp[0] = 0;
for (int x = 1; x <= amount; ++x)
for (int c : coins)
if (c <= x && dp[x - c] + 1 < dp[x]) dp[x] = dp[x - c] + 1;
return dp[amount] == INF ? -1 : dp[amount];
}
int main() {
std::println("stairs: {} {} {} {}", climb_ways(1), climb_ways(2), climb_ways(5), climb_ways(50));
std::println("coins: {} {} {} {}", min_coins({1, 3, 4}, 6), min_coins({2}, 3),
min_coins({1, 2, 5}, 11), min_coins({7}, 0));
}প্রত্যাশিত আউটপুট: stairs: 1 2 8 20365011074 আর coins: 2 -1 3 0. ৫০ ধাপের উত্তর int-এর সীমা (প্রায় ২১৪ কোটি) ছাড়িয়ে যায় — তাই long long; ইন্টারভিউতে উত্তরের আকার নিজে থেকে হিসাব করা ভালো সংকেত। কয়েনে {1, 3, 4} দিয়ে 6: “বড়টা আগে” (greedy) নেয় 4 + 1 + 1 = ৩টি, অথচ 3 + 3 = ২টিই যথেষ্ট — এটাই দেখায় কেন এখানে DP লাগে।
Edge case ও ফলো-আপ: amount 0 → 0টি কয়েন; বানানো অসম্ভব → -1. INF হিসেবে INT_MAX নিলে dp[x - c] + 1 overflow করত — তাই amount + 1. ফলো-আপ: সর্বনিম্ন নয়, কত উপায়ে বানানো যায় (লুপের ক্রম বদলালে বিন্যাস বনাম সমাবেশ গোনা হয়); একবারে ১, ২ বা ৩ ধাপ উঠা গেলে।
৬ · C++ ডিজাইন সমস্যা — LRU cache, Rule of Five, thread-safe queue
এখানে অ্যালগরিদম ছোট, কিন্তু ভাষার জ্ঞান গভীরভাবে যাচাই হয়: container-এর জটিলতা, iterator কখন বৈধ থাকে, মালিকানা, exception-safety আর thread. রিভিশনের জন্য: পর্ব ২০, পর্ব ২১, পর্ব ২২ · Rule of 0/3/5, পর্ব ৩২ · iterator, পর্ব ৩৬, পর্ব ৪৫ · thread ও mutex, পর্ব ৪৬ · atomics.
সমস্যা ১৪ · LRU cache — get আর put দুটোই O(1) (মাঝারি → কঠিন)
প্রশ্ন: নির্দিষ্ট ধারণক্ষমতার একটি cache বানান। get(key) মান দেয় (না থাকলে “নেই”); put(key, value) যোগ বা বদল করে। জায়গা ভরে গেলে সবচেয়ে দীর্ঘ সময় ব্যবহার না হওয়া (least recently used) key বাদ যাবে।
জিজ্ঞেস করে নিন: get-ও কি “ব্যবহার” ধরা হবে? (হ্যাঁ।) ধারণক্ষমতা 0 হলে? একাধিক thread থেকে ডাকা হবে কি?
পদ্ধতি: দুটি চাহিদা — key দিয়ে দ্রুত খোঁজা (hash map), আর ব্যবহারের ক্রম রাখা, যেখানে যেকোনো উপাদান O(1)-এ সামনে আনা যায় (doubly linked list). তাই std::list + std::unordered_map<key, list::iterator>. মূল কৌশল splice: কোনো node কপি বা নতুন করে না বানিয়ে লিস্টের সামনে সরানো — O(1), আর std::list-এর iterator এতে অবৈধ হয় না (iterator invalidation-এর নিয়ম, পর্ব ৩২)।
#include <list>
#include <optional>
#include <print>
#include <unordered_map>
#include <utility>
class LRUCache {
public:
explicit LRUCache(std::size_t capacity) : cap_(capacity) {}
std::optional<int> get(int key) {
auto it = pos_.find(key);
if (it == pos_.end()) return std::nullopt;
items_.splice(items_.begin(), items_, it->second); // move to front, O(1)
return it->second->second;
}
void put(int key, int value) {
if (cap_ == 0) return;
if (auto it = pos_.find(key); it != pos_.end()) {
it->second->second = value;
items_.splice(items_.begin(), items_, it->second);
return;
}
if (items_.size() == cap_) { // evict the least recent
pos_.erase(items_.back().first);
items_.pop_back();
}
items_.emplace_front(key, value);
pos_[key] = items_.begin();
}
private:
using Item = std::pair<int, int>; // (key, value)
std::size_t cap_;
std::list<Item> items_; // front = most recent
std::unordered_map<int, std::list<Item>::iterator> pos_; // key -> list node
};
int main() {
LRUCache c(2);
auto show = [&c](int key) { std::print("{} ", c.get(key).value_or(-1)); };
c.put(1, 10); c.put(2, 20);
show(1); // 10; key 1 is now most recent
c.put(3, 30); // evicts key 2
show(2); show(3);
c.put(1, 11); c.put(4, 40); // evicts key 3
show(1); show(3); show(4);
std::println("");
}প্রত্যাশিত আউটপুট: 10 -1 30 11 -1 40. ধাপগুলো খাতায় মিলিয়ে নিন: get(1)-এর পর 1 সামনে, তাই put(3) বাদ দেয় 2-কে; put(1, 11) 1-কে আবার সামনে আনে, তাই put(4) বাদ দেয় 3-কে। লিস্টে key-ও রাখা হয়েছে, কারণ পেছন থেকে বাদ দেওয়ার সময় map থেকেও সেই key মুছতে হয়।
যা নিজে থেকে বলবেন: ধারণক্ষমতা 0 হলে খালি লিস্টে back() ডাকা undefined behavior হতো — তাই শুরুতেই cap_ == 0 যাচাই। get const নয়, কারণ এটি ক্রম বদলায় — এটা ইন্টারভিউয়ারের প্রিয় প্রশ্ন। ফলো-আপ: template বানান (Key, Value); thread-safe করুন (পুরো get/put-এ একটি std::mutex — কারণ get-ও লেখে, তাই shared lock এখানে চলে না); LFU (সবচেয়ে কম বার ব্যবহৃত) কীভাবে আলাদা।
সমস্যা ১৫ · একটি Rule-of-Five টাইপ: নিজের মেমরি রাখা Buffer (মাঝারি)
প্রশ্ন: একটি class লিখুন যা heap-এ n টি int রাখে। copy করলে আলাদা স্বাধীন কপি হবে, move করলে শুধু pointer সরবে, কোথাও leak বা double delete হবে না।
জিজ্ঞেস করে নিন: বাস্তবে কি std::vector<int> ব্যবহার করা যায়? (প্রায় সবসময় হ্যাঁ — তখন Rule of Zero, কোনো বিশেষ ফাংশন লিখতেই হয় না। ইন্টারভিউয়ার এটা শুনতে চান, তারপর বলবেন “তবু নিজে লিখে দেখান।”)
পদ্ধতি: Rule of Five: destructor, copy constructor, copy assignment, move constructor, move assignment — একটি লিখলে সাধারণত পাঁচটিই ভাবতে হয়। এখানে copy-and-swap: assignment প্যারামিটার মান দিয়ে নেয়, তাই একটি ফাংশনই copy ও move দুই assignment-এর কাজ করে, আর self-assignment ও exception-safety নিজে থেকেই ঠিক থাকে।
#include <algorithm>
#include <cstddef>
#include <print>
#include <utility>
class Buffer {
public:
explicit Buffer(std::size_t n) : size_(n), data_(n ? new int[n]{} : nullptr) {}
~Buffer() { delete[] data_; } // 1. destructor
Buffer(const Buffer& o) : Buffer(o.size_) { // 2. copy constructor
std::copy_n(o.data_, size_, data_);
}
Buffer(Buffer&& o) noexcept // 3. move constructor
: size_(std::exchange(o.size_, 0)), data_(std::exchange(o.data_, nullptr)) {}
Buffer& operator=(Buffer o) noexcept { // 4 + 5. copy-and-swap
swap(o);
return *this;
}
void swap(Buffer& o) noexcept { std::swap(size_, o.size_); std::swap(data_, o.data_); }
int& operator[](std::size_t i) { return data_[i]; }
[[nodiscard]] std::size_t size() const noexcept { return size_; }
private:
std::size_t size_;
int* data_;
};
int main() {
Buffer a(3);
a[0] = 7;
Buffer b = a; // deep copy
b[0] = 99;
Buffer c = std::move(a); // steals the pointer
Buffer d(1);
d = c; // copy-assign through copy-and-swap
std::println("a.size={} b[0]={} c[0]={} d.size={} d[0]={}", a.size(), b[0], c[0], d.size(), d[0]);
}প্রত্যাশিত আউটপুট: a.size=0 b[0]=99 c[0]=7 d.size=3 d[0]=7. b বদলালেও a বা c-তে 99 যায়নি — copy গভীর। move-এর পর a খালি কিন্তু বৈধ অবস্থায় (std::exchange pointer-কে nullptr করে দিয়েছে), তাই এর destructor নিরাপদে delete[] nullptr করে — যা কিছুই করে না।
যা নিজে থেকে বলবেন: member-এর ঘোষণার ক্রম (size_ আগে, data_ পরে) initializer list-এর ক্রম নির্ধারণ করে — তালিকায় লেখার ক্রম নয়। move constructor noexcept হওয়া জরুরি; নইলে std::vector<Buffer> বড় হওয়ার সময় move-এর বদলে copy করে (অংশ ৭, প্র ০৬)। একই নিয়মে ফাইলের জন্য RAII FileHandle: constructor-এ std::fopen, ব্যর্থ হলে exception, destructor-এ std::fclose, copy নিষিদ্ধ (= delete), move অনুমোদিত — আর বাস্তবে আরও সহজ পথ: একটি ছোট deleter struct-সহ std::unique_ptr, বা সরাসরি std::ifstream (পর্ব ১৭, ৪৪)।
সমস্যা ১৬ · Thread-safe queue আর counter (মাঝারি → কঠিন)
প্রশ্ন: একাধিক producer thread কাজ রাখবে, consumer thread কাজ তুলবে। queue খালি হলে consumer অপেক্ষা করবে — CPU নষ্ট না করে। আর একটি সহজ রূপ: কয়েকটি thread মিলে একটি counter বাড়াবে, শেষে মান ঠিক থাকতে হবে।
পদ্ধতি: queue-এর প্রতিটি ব্যবহার একটি mutex-এর তালার ভেতরে; অপেক্ষার জন্য std::condition_variable, যা predicate-সহ wait-এ spurious wakeup-ও সামলায়। Counter-এর জন্য mutex লাগে না — std::atomic<int>-এর fetch_add একটি অবিভাজ্য কাজ।
template <typename T>
class SafeQueue {
public:
void push(T value) {
{
std::lock_guard lock(m_); // C++17: template argument deduced
q_.push(std::move(value));
} // unlock before notifying
cv_.notify_one();
}
T pop() { // blocks until an item exists
std::unique_lock lock(m_);
cv_.wait(lock, [this] { return !q_.empty(); });
T value = std::move(q_.front());
q_.pop();
return value;
}
private:
std::mutex m_;
std::condition_variable cv_;
std::queue<T> q_;
};
std::atomic<int> counter{0}; // thread-safe counter, no mutex
// in each thread: counter.fetch_add(1, std::memory_order_relaxed);যা নিজে থেকে বলবেন: std::lock_guard আর std::unique_lock দুটোই RAII — exception হলেও তালা খোলে। wait-এর জন্য unique_lock লাগে, কারণ অপেক্ষার সময় তালা খুলে আবার নিতে হয়। আলাদা empty() আর front() দুটি public ফাংশন দিলে দুটি ডাকের মাঝে অন্য thread ঢুকে পড়তে পারে — তাই “দেখা আর তোলা” একটি ফাংশনেই। সাধারণ int-এ counter++ একাধিক thread থেকে করা data race, অর্থাৎ undefined behavior; volatile এর সমাধান নয়। ফলো-আপ: সীমিত আকারের queue (দ্বিতীয় condition variable, “ভরা থাকলে producer অপেক্ষা করবে”); বন্ধ করার ব্যবস্থা (close() বা std::stop_token, পর্ব ৪৫); pop-এ timeout দিয়ে std::optional<T> ফেরত।
৭ · ভাবনার প্রশ্ন — C++ ইন্টারভিউর গভীর আলোচনা
কোড লেখার পর ইন্টারভিউয়ার প্রায়ই জিজ্ঞেস করেন, “এটা কেন এভাবে লিখলেন?” নিচের প্রশ্নগুলো সেই আলোচনার প্রস্তুতি। উত্তরের ভালো কাঠামো: এক লাইনে সারকথা → কারণ → একটি ফাঁদ বা বাস্তব উদাহরণ। প্রতিটি আগে নিজে মুখে উত্তর দিন, তারপর খুলুন। রিভিশনের জন্য: পর্ব ১০, পর্ব ৩১, পর্ব ৩৬, পর্ব ৪৩, পর্ব ৪৮, পর্ব ৪৯।
প্র ০১unordered_map-এ খোঁজা “O(1)” — তাহলে খারাপ ক্ষেত্রে O(n) কেন? ইন্টারভিউতে কীভাবে বলবেন? (মাঝারি)
std::unordered_map একটি hash table: key-এর hash থেকে একটি bucket ঠিক হয়, আর একই bucket-এ পড়া key-গুলো সেখানে একটি তালিকায় থাকে। hash ভালোভাবে ছড়ালে প্রতিটি bucket-এ গড়ে অল্প কয়েকটি key — তাই খোঁজা গড়ে O(1). কিন্তু সব key একই bucket-এ পড়লে (hash collision) সেই তালিকা n লম্বা, আর প্রতিটি খোঁজা O(n) — n টি insert মিলে O(n²).
এটা শুধু তত্ত্ব নয়। libstdc++-এ std::hash<int> কার্যত সংখ্যাটিকেই ফেরত দেয়, আর bucket-এর সংখ্যা সাধারণত একটি মৌলিক সংখ্যা। কেউ সেই সংখ্যার গুণিতক বেছে বেছে ইনপুট দিলে সব একই bucket-এ পড়ে। কনটেস্ট প্ল্যাটফর্মে এ কারণেই “anti-hash” test দেখা যায়, যা unordered_map ব্যবহারকারী সমাধানকে time limit-এ আটকে দেয়। ওয়েবের জগতে ২০১১ সালে নিরাপত্তা গবেষকেরা দেখিয়েছিলেন, অনেক জনপ্রিয় web platform-এ ইচ্ছাকৃত collision-ভরা একটি request সার্ভারকে দীর্ঘ সময় ব্যস্ত রাখতে পারে — এর নাম hash flooding. এর পর অনেক ভাষা এলোমেলো seed-যুক্ত hash ব্যবহার শুরু করে।
আরেকটি খরচ rehash: load factor সীমা ছাড়ালে পুরো টেবিল নতুন করে সাজানো হয় — সেই একটি insert O(n), যদিও গড়ে (amortized) O(1). আগে থেকে আকার জানলে reserve এটা এড়ায়।
ইন্টারভিউতে বলুন: “গড়ে O(1), খারাপ ক্ষেত্রে O(n); শত্রুভাবাপন্ন ইনপুট বা নিশ্চিত সীমা দরকার হলে std::map — প্রতিটি কাজ নিশ্চিতভাবে O(log n), সাথে ক্রমও পাই। ছোট ও নির্দিষ্ট পরিসরের key হলে সাধারণ অ্যারে সবচেয়ে দ্রুত।” এই এক বাক্যে বোঝা যায় আপনি Big-O-কে শুধু মুখস্থ সংখ্যা ভাবেন না।
মূল উপলব্ধি: জটিলতা বলার সময় “গড়ে” আর “খারাপ ক্ষেত্রে” আলাদা করে বলুন — আর সেই পার্থক্য কখন বাস্তবে গুরুত্বপূর্ণ, সেটাও।
প্র ০২reserve কখন ব্যবহার করবেন, আর কখন এটি অপ্রয়োজনীয় বা এমনকি ক্ষতিকর? (মাঝারি)
std::vector জায়গা ফুরোলে বড় একটি নতুন block নেয় (সাধারণত আগের দেড় বা দুই গুণ — বাস্তবায়নভেদে), সব উপাদান সেখানে move বা copy করে, পুরোনোটা ছেড়ে দেয়। এভাবে বাড়ানোয় প্রতিটি push_back গড়ে O(1) — amortized — কিন্তু মাঝে মাঝে একটি push পুরো O(n), আর প্রতিবার আগের সব iterator, pointer, reference অবৈধ হয়ে যায়।
reserve(n) আগেই অন্তত n উপাদানের জায়গা নেয়। উপকার তিনটি: (১) পুনঃবরাদ্দ ও কপি হয় না, (২) মেমরি allocator-এর কাছে একবারই চাওয়া হয়, (৩) n পর্যন্ত push করলে iterator বৈধ থাকে। unordered_map::reserve একইভাবে আগেই যথেষ্ট bucket বানায়, ফলে rehash হয় না। তাই যখন চূড়ান্ত আকার জানা বা ভালোভাবে অনুমান করা যায় — যেমন একটি vector থেকে আরেকটি বানানো, বা ফাইলের লাইন সংখ্যা আগে জানা — তখন reserve সহজ ও নিরাপদ উন্নতি।
ফাঁদও আছে। লুপের ভেতরে v.reserve(v.size() + 1) লিখলে জ্যামিতিক বৃদ্ধি নষ্ট হতে পারে — প্রতিটি push-এ নতুন বরাদ্দ, মোট O(n²). আকার না জেনে বিশাল সংখ্যা reserve করলে মেমরি নষ্ট। আর reserve আকার (size()) বদলায় না — v.reserve(10); v[3] = 1; undefined behavior; আকার চাইলে resize.
ইন্টারভিউর কোডে two sum বা group anagrams-এ একটি reserve লাইন দেখালে ইন্টারভিউয়ার বোঝেন আপনি container-এর ভেতরের খরচ জানেন। কিন্তু “কেন দিলেন?” প্রশ্নের উত্তর তৈরি রাখুন — না জেনে বসানো optimization খারাপ সংকেত। আর মনে রাখুন, আসল উন্নতি মাপা ছাড়া দাবি করবেন না (পর্ব ৪৯)।
মূল উপলব্ধি: আকার জানলে reserve; না জানলে container-কে নিজের নিয়মে বাড়তে দিন — জ্যামিতিক বৃদ্ধি ঠিক এই কাজের জন্যই বানানো।
প্র ০৩Recursive সমাধান সুন্দর, কিন্তু recursion-এর গভীরতা কেন বিপদ? unique_ptr-এর লিংকড লিস্টে এটা কোথায় লুকিয়ে থাকে? (কঠিন)
প্রতিটি ফাংশন-ডাক call stack-এ একটি frame নেয় — local ভ্যারিয়েবল, ফেরার ঠিকানা, register-এর মান। main thread-এর stack সাধারণত কয়েক মেগাবাইট (Linux-এ প্রায়ই ৮ MB, Windows-এ প্রায়ই ১ MB), আর নতুন thread-এর stack আরও ছোট হতে পারে। তাই recursion-এর গভীরতা লাখের ঘরে গেলে stack ফুরিয়ে যায় — stack overflow, সাধারণত crash. C++ standard এর কোনো সীমা বা নিশ্চয়তা দেয় না।
কোথায় গভীরতা n হয়? সাজানো ইনপুটে বানানো BST (একটি লম্বা লাইন), লিংকড লিস্টে recursive reverse, আর top-down DP যেখানে f(n) ডাকে f(n-1)-কে। balanced tree-তে গভীরতা log n — দশ লাখ node-এও মাত্র ২০-এর মতো — তাই সেখানে recursion নিরাপদ।
সবচেয়ে লুকানো উদাহরণ destructor. struct Node { int val; std::unique_ptr<Node> next; }; — head মুছলে তার destructor next-কে মোছে, সেটি তার next-কে… কোডে কোনো recursive ফাংশন দেখা যায় না, অথচ গভীরতা n. দশ লাখ node-এর লিস্ট scope থেকে বের হওয়ার মুহূর্তে crash হতে পারে। সমাধান: লিস্ট class-এর destructor-এ লুপ — while (head) head = std::move(head->next); — প্রতি ঘুরে একটি node মুছে যায়, আর তার next আগেই সরানো থাকায় শৃঙ্খল চলে না। tree-র জন্যও একই চিন্তা।
ইন্টারভিউতে recursive সমাধান লেখা দোষের নয় — প্রায়ই সেটাই সবচেয়ে পরিষ্কার। কিন্তু নিজে থেকে বলুন: “গভীরতা n পর্যন্ত যেতে পারে; ইনপুট বড় হলে iterative করব বা নিজের std::stack ব্যবহার করব।” সব recursion-কেই একটি স্পষ্ট stack দিয়ে লুপে রূপান্তর করা যায় — নিজের stack heap-এ থাকে, আর সেটি অনেক বড় হতে পারে।
মূল উপলব্ধি: জটিলতা বলার সময় call stack-এর মেমরিও গুনুন — recursive DFS-এর মেমরি O(1) নয়, O(গভীরতা)।
প্র ০৪ইন্টারভিউর কোডে প্যারামিটার: মান দিয়ে, const& দিয়ে, নাকি string_view/span দিয়ে? (সহজ → মাঝারি)
এই প্রশ্নে ইন্টারভিউয়ার দেখেন আপনি C++-এর খরচের মডেল বোঝেন কি না। একটি সহজ নিয়ম-তালিকা:
ছোট, সস্তা টাইপ (int, double, pointer, iterator, string_view) — মান দিয়ে (pass by value). এদের কপি এক-দুটি register, reference দিলে বরং পরোক্ষ পড়া লাগে।
বড় টাইপ, শুধু পড়ব (std::vector, std::string, নিজের class) — const T&. দশ লাখ উপাদানের vector মান দিয়ে নিলে প্রতিটি ডাকে পুরো কপি — O(n) সময় আর মেমরি, যা জটিলতা-বিশ্লেষণই বদলে দেয়। recursive ফাংশনে এই ভুল হলে O(n) সমাধান O(n²) হয়ে যায়।
শুধু পড়ার string — std::string_view: literal, std::string বা তার অংশ — সবই কপি ছাড়া। ফাঁদ: view মালিক নয়; ফাংশন থেকে view ফেরত দিলে বা অস্থায়ী string-এর view রাখলে dangling (পর্ব ১৪, ১৮)। একইভাবে অ্যারের জন্য std::span (C++20).
নিজের কাছে রেখে দেব (“sink”, যেমন constructor-এ member-এ রাখা) — মান দিয়ে নিয়ে std::move: ডাকার জায়গা অস্থায়ী মান দিলে move, নামযুক্ত ভ্যারিয়েবল দিলে একবার কপি। সমস্যা ২-এর push_back(std::move(w)) এই চিন্তারই অংশ।
বদলাব — non-const T&, অথবা আরও পরিষ্কার: নতুন মান ফেরত দিন। আধুনিক C++-এ ফেরত দেওয়া সস্তা (copy elision ও move), তাই “out parameter” এখন কম ব্যবহৃত।
মূল উপলব্ধি: signature-ই প্রথম ডকুমেন্টেশন — const& বলে “শুধু পড়ব”, মান + move বলে “নিয়ে নেব”, unique_ptr বলে “মালিকানা দিন”। ইন্টারভিউয়ার কোড পড়ার আগে signature পড়েন।
প্র ০৫কোড লেখা শেষ। ইন্টারভিউতে কম্পাইলার ছাড়া, জোরে বলে কীভাবে নিজের কোড পরীক্ষা করবেন? (সহজ)
হোয়াইটবোর্ড বা শেয়ার করা editor-এ প্রায়ই কোড চালানো যায় না। তাই “পরীক্ষা” মানে নিজের মাথায় আর মুখে চালানো — এবং এটা ইন্টারভিউয়ারের সামনে করাটাই মূল্যায়নের অংশ। একটি বাঁধা ক্রম রাখুন:
১. একটি সাধারণ উদাহরণ, লাইন ধরে। ভ্যারিয়েবলের মানগুলো পাশে একটি ছোট টেবিলে লিখুন — ঠিক অংশ ৩-এর stepper-এর মতো। জোরে বলুন: “right = 2, 'b' আগে ছিল 1-এ, তাই left হয় 2…”
২. Edge case-এর তালিকা। খালি ইনপুট, এক উপাদান, সব একই, সব ভিন্ন, সর্বোচ্চ আকার, ঋণাত্মক, ডুপ্লিকেট। প্রতিটির জন্য কোডের কোন লাইন কী করবে, মুখে বলুন। বেশিরভাগ বাগ এখানেই ধরা পড়ে।
৩. সীমানা আর টাইপ। লুপের শুরু ও শেষ (< নাকি <=), size() - 1 খালি container-এ, signed-unsigned তুলনা, overflow. খালি container-এ front(), top(), back() ডাকা হচ্ছে কি না।
৪. C++-নির্দিষ্ট চেক। কোনো iterator বা reference container বদলানোর পর ব্যবহার হচ্ছে কি না (iterator invalidation); কোনো view বা pointer মরে যাওয়া object-এর দিকে কি না; moved-from object ব্যবহার হচ্ছে কি না।
নিজের বাগ নিজে ধরলে বলুন, “এখানে off-by-one আছে, ঠিক করছি” — এটা দুর্বলতা নয়, খুব ইতিবাচক সংকেত। কম্পিউটার পেলে unit test-এর মতো কয়েকটি assert লিখে চালান, আর বাড়িতে অনুশীলনের সময় -fsanitize=address,undefined দিয়ে compile করুন (পর্ব ৪৩, ৪৮)। আরও এক ধাপ: ধীর কিন্তু নিশ্চিত brute force-এর সাথে এলোমেলো ইনপুটে ফল মিলিয়ে দেখা — stress testing (পর্ব ৫০)।
মূল উপলব্ধি: “শেষ” বলার আগে পরীক্ষা করা অভ্যাস — ইন্টারভিউয়ার সেই অভ্যাসটাই নিয়োগ দিতে চান, কারণ বাস্তব কাজে কেউ আপনার পেছনে বসে test case দেবে না।
প্র ০৬move constructor-এ noexcept না দিলে std::vector কেন ধীর হয়ে যেতে পারে? (কঠিন)
std::vector::push_back একটি শক্ত প্রতিশ্রুতি দেয় (strong exception guarantee): বড় হওয়ার সময় কোনো exception হলে vector আগের অবস্থাতেই থাকবে। ভাবুন, পুনঃবরাদ্দের সময় পুরোনো উপাদানগুলো নতুন block-এ move করা হচ্ছে, আর মাঝপথে একটি move exception ছুড়ল। ততক্ষণে কয়েকটি পুরোনো উপাদান “খালি” হয়ে গেছে — আগের অবস্থায় ফেরার আর উপায় নেই।
তাই vector তখনই move করে, যখন move constructor noexcept (অথবা টাইপটি copy করাই যায় না)। নইলে সে নিরাপদ পথ নেয় — copy. ভেতরে এই সিদ্ধান্ত নেয় std::move_if_noexcept. ফল: আপনার Buffer-এর move constructor-এ noexcept ভুলে গেলে প্রতিটি পুনঃবরাদ্দে সব Buffer-এর গভীর কপি হবে — প্রোগ্রাম ঠিক চলবে, কিন্তু নীরবে অনেক ধীর।
কম্পাইলারের নিজে বানানো (defaulted) move constructor সব member-এর move noexcept হলে নিজেও noexcept — Rule of Zero-র আরেকটি সুবিধা। নিজে লিখলে noexcept নিজে লিখতে হবে, আর সেটা সত্যি হতে হবে: pointer সরানো আর std::exchange exception ছোড়ে না, কিন্তু move-এর ভেতরে নতুন মেমরি বরাদ্দ করলে সেটি ছুড়তে পারে। noexcept ফাংশন থেকে exception বের হলে std::terminate ডাকা হয়।
যাচাইয়ের একটি সহজ উপায়: static_assert(std::is_nothrow_move_constructible_v<Buffer>); — ভুল থাকলে compile-এর সময়েই ধরা পড়ে (পর্ব ৩৯)। swap আর move assignment-ও একই কারণে noexcept রাখুন।
মূল উপলব্ধি: noexcept শুধু ডকুমেন্টেশন নয় — standard library এটা দেখে সিদ্ধান্ত বদলায়। Rule of Five লিখলে move-এ noexcept ছাড়া উত্তর অসম্পূর্ণ।
প্র ০৭Thread-safe counter: std::atomic, std::mutex, নাকি volatile? (মাঝারি)
counter++ দেখতে একটি কাজ, আসলে তিনটি: পড়া, এক যোগ করা, লেখা। দুটি thread একসাথে ৫ পড়লে দুজনেই ৬ লেখে — একটি বৃদ্ধি হারিয়ে গেল। C++-এর ভাষায় একাধিক thread একই সাধারণ ভ্যারিয়েবলে কমপক্ষে একজন লিখলে আর কোনো synchronization না থাকলে সেটা data race — undefined behavior, শুধু “ভুল সংখ্যা” নয়।
std::atomic<int>: fetch_add বা ++ একটি অবিভাজ্য কাজ; সাধারণ hardware-এ একটি বিশেষ CPU instruction, তালা ছাড়াই। শুধু একটি সংখ্যা বা flag হলে এটাই সেরা। গণনার জন্য std::memory_order_relaxed যথেষ্ট, কারণ অন্য ডেটার সাথে ক্রম রক্ষার দরকার নেই — তবে নিশ্চিত না হলে ডিফল্ট (sequentially consistent) রাখুন (পর্ব ৪৬)।
std::mutex: যখন একাধিক ভ্যারিয়েবল একসাথে সামঞ্জস্যপূর্ণ রাখতে হয় — যেমন queue-র ভেতরের সব অবস্থা, বা “counter আর সর্বশেষ সময়” একসাথে। দুটি আলাদা atomic দুটি আলাদা কাজ; তাদের মাঝখানে অন্য thread ঢুকতে পারে।
volatile: thread-এর জন্য নয়। এটি compiler-কে শুধু বলে প্রতিটি পড়া-লেখা সত্যিই করতে (hardware register-এর জন্য)। এটি atomicity দেয় না, thread-গুলোর মধ্যে ক্রমও নিশ্চিত করে না। Java-র volatile-এর অর্থ আলাদা — এই বিভ্রান্তি থেকেই প্রশ্নটি জনপ্রিয়।
ইন্টারভিউতে আরও এক ধাপ বলুন: অনেক thread একটি counter-এ বারবার লিখলে cache line নিয়ে টানাটানি হয়; প্রতিটি thread নিজের local গণনা রেখে শেষে একবার যোগ করলে অনেক দ্রুত। আর পরীক্ষার জন্য ThreadSanitizer (-fsanitize=thread) data race ধরতে পারে (পর্ব ৪৮)।
মূল উপলব্ধি: একটি মান → atomic; একাধিক মান একসাথে → mutex; volatile → thread-এর প্রশ্নে কখনো নয়।
৮ · কথা বলা, ক্যাপস্টোন ব্যাখ্যা আর AI
একই সমাধান জানা দুজন প্রার্থীর একজন নির্বাচিত হন, অন্যজন হন না — প্রায়ই পার্থক্যটা যোগাযোগে। এই দক্ষতাও অনুশীলনে বাড়ে, ঠিক কোডের মতো।
ইন্টারভিউর ঘরে পাঁচটি অভ্যাস
- জোরে ভাবুন (think aloud): “প্রথমে brute force ভাবছি… এটা O(n²), n যদি ১০৫ হয় তাহলে ধীর… তাই
unordered_mapভাবছি।” চুপ করে দুই মিনিট ভাবার চেয়ে অর্ধেক-গোছানো ভাবনা বলাও ভালো — ইন্টারভিউয়ার পথ দেখে সাহায্য করতে পারেন। - প্রশ্ন করুন, ধরে নেবেন না: “ইনপুট কি সাজানো?”, “খালি vector আসতে পারে?” — এগুলো দুর্বলতা নয়, পেশাদারিত্ব।
- Hint-কে উপহার ভাবুন: ইন্টারভিউয়ার “যদি অ্যারেটা সাজানো থাকত?” বললে সেটা একটা দরজা। ধন্যবাদ দিয়ে ধরুন, আর বলুন hint থেকে কী বুঝলেন।
- ভুল স্বীকার করুন, দ্রুত: dry run-এ নিজের বাগ ধরলে সাথে সাথে বলুন। নিজের ভুল নিজে ধরা ইন্টারভিউতে খুব ইতিবাচক সংকেত।
- না জানলে সৎ থাকুন: “
std::pmrনিয়ে কাজ করিনি, তবে যতটুকু জানি…” — বানিয়ে বলা অভিজ্ঞ ইন্টারভিউয়ার সাথে সাথে ধরে ফেলেন।
নিজের ক্যাপস্টোন প্রজেক্ট কীভাবে ব্যাখ্যা করবেন
“আপনার একটি প্রজেক্টের কথা বলুন” — এই প্রশ্নটি প্রায় নিশ্চিত। পর্ব ৫১-এর ক্যাপস্টোন (বা পর্ব ১৯-এর CLI টুল) এখানে আপনার সবচেয়ে বড় সম্পদ, কারণ এটি আপনি নিজে বানিয়েছেন। পাঁচ মিনিটের একটি কাঠামো তৈরি রাখুন:
- সমস্যা ও ব্যবহারকারী (৩০ সেকেন্ড): প্রজেক্টটি কী করে, কার জন্য।
- কাঠামো (১ মিনিট): প্রধান class বা module-গুলো, কে কার মালিক, ডেটা কোথা থেকে কোথায় যায়। খাতায় তিন-চারটি বাক্স এঁকে দেখাতে পারলে আরও ভালো।
- দুটি সিদ্ধান্ত ও তার কারণ (২ মিনিট): “এখানে
std::mapনিয়েছি, কারণ রিপোর্টে সাজানো ক্রম লাগত”; “ফাইল হ্যান্ডেল RAII class-এ রেখেছি, যাতে exception হলেও বন্ধ হয়।” trade-off বলুন — কী ছেড়ে দিয়েছেন। - একটি কঠিন বাগ (১ মিনিট): লক্ষণ, কীভাবে খুঁজলেন (sanitizer, debugger, test), আসল কারণ, কী শিখলেন।
- পরের উন্নতি (৩০ সেকেন্ড): আরও সময় পেলে কী বদলাতেন — এটা দেখায় আপনি নিজের কাজের দুর্বলতা চেনেন।
সততার নিয়ম: যে অংশ টিউটোরিয়াল, বন্ধু বা AI-এর সাহায্যে লিখেছেন, সেটা লুকাবেন না — বলুন কোন অংশ নিজে বুঝে বদলেছেন। ইন্টারভিউয়ার প্রায়ই প্রজেক্টের যেকোনো লাইন দেখিয়ে জিজ্ঞেস করেন, “এটা কেন?” যে কোড আপনি ব্যাখ্যা করতে পারেন না, তা আপনার পক্ষে নয়, বিপক্ষে কাজ করে। “দলগত প্রজেক্টে আমার অংশ ছিল…” — নিজের অবদান ঠিক যতটুকু, ততটুকুই বলুন।
আচরণগত প্রশ্নে STAR কাঠামো
“এমন একটা সময়ের কথা বলুন যখন…” — এ ধরনের প্রশ্নে STAR কাঠামোয় উত্তর গোছান: Situation (পরিস্থিতি), Task (আপনার দায়িত্ব), Action (আপনি নিজে কী করলেন), Result (ফল, আর কী শিখলেন)। ৪–৫টি সত্যি গল্প আগে থেকে তৈরি রাখুন: একটি কঠিন বাগ, একটি দলগত মতবিরোধ, একটি ব্যর্থতা ও তা থেকে শেখা, নিজে থেকে নেওয়া একটি উদ্যোগ। বানানো গল্প নয় — পরের প্রশ্নেই ফাঁক বেরিয়ে যায়।
একটি নমুনা: “ক্যাপস্টোন প্রজেক্টে আমার প্রোগ্রাম বড় ফাইলে মাঝে মাঝে crash করছিল (S). কারণ খুঁজে ঠিক করা আমার দায়িত্ব ছিল (T). আমি AddressSanitizer দিয়ে চালিয়ে দেখলাম, একটি string_view এমন একটি অস্থায়ী string-এর দিকে তাকিয়ে ছিল, যা আগেই মুছে গেছে। সেখানে মালিকানাসহ std::string রাখলাম, আর এই ধরনের ক্ষেত্রের জন্য একটি test যোগ করলাম (A). crash বন্ধ হলো; তারপর থেকে আমি view ফেরত দেওয়া প্রতিটি ফাংশনে lifetime আলাদা করে ভাবি (R).”
AI-এর যুগে ইন্টারভিউ: নিয়ম আর সততা
ইন্টারভিউ চলাকালীন AI-এর সাহায্য অনেক প্রতিষ্ঠানেই নিষিদ্ধ — live coding-এ, online assessment-এ, অনেক সময় take-home-এও। কিছু প্রতিষ্ঠান আবার AI ব্যবহারের অনুমতি দিয়ে দেখতে চায় আপনি সেটি কীভাবে যাচাই করেন। নিয়ম প্রতিষ্ঠানভেদে আলাদা আর সময়ের সাথে বদলায় — তাই আগে জিজ্ঞেস করে নিন, আর অনুমতি না থাকলে ব্যবহার করবেন না। নিষেধ ভেঙে ধরা পড়লে শুধু ওই ইন্টারভিউ নয়, ভবিষ্যতের সুযোগও নষ্ট হতে পারে।
কেন মুখস্থ নয়, বোঝা? জনপ্রিয় সমস্যার সমাধান অনলাইনে আছে, ইন্টারভিউয়ার তা জানেন। তাই আসল পরীক্ষা হয় ফলো-আপে: “এবার thread-safe করুন”, “ইনপুট stream-এ এলে?” মুখস্থ সমাধান এখানেই ভেঙে পড়ে। প্রস্তুতির সময় অবশ্য AI হতে পারে চমৎকার mock interview-এর সঙ্গী আর code review-এর সাহায্যকারী — শর্ত: উত্তর নয়, প্রশ্ন আর মূল্যায়ন চাইবেন।
নিচের prompt দিয়ে একটি পূর্ণ mock interview চালান। AI-কে স্পষ্ট বলা হয়েছে সমাধান না দিতে, আর শেষে কঠোরভাবে মূল্যায়ন করতে:
নিজের লেখা সমাধান যাচাই করাতে চাইলে আলাদা prompt দিন: “এই C++ সমাধানের বাগ, অনাবৃত edge case আর জটিলতার ভুল খুঁজে দাও; ঠিক করা কোড লিখবে না, শুধু কোন লাইনে কী সমস্যা বলবে।” এতে ঠিক করার কাজটা আপনার হাতেই থাকে।
AI দিয়ে অনুশীলনের চেকলিস্ট
৯ · কোর্স সমাপ্তি — ৫৫টি পর্বের পরে আপনি কোথায়
এটি ৮টি মডিউল আর ৫৫টি পর্বের এই কোর্সের শেষ পর্ব। একবার পেছনে তাকান। পর্ব ১-এ প্রশ্ন ছিল, “C++ কেন?” পর্ব ৩-এ দেখেছিলেন একটি .cpp ফাইল কীভাবে প্রোগ্রাম হয়। আজ আপনি একটি LRU cache-এর জন্য ঠিক container বাছতে পারেন, move constructor-এ কেন noexcept লাগে বোঝাতে পারেন, আর একটি অচেনা সমস্যাকে সাত ধাপে ভেঙে সমাধান করতে পারেন। আন্তরিক অভিনন্দন — C++-এর মতো বিশাল একটি ভাষার পুরো পথ পার হওয়া সত্যিই বড় অর্জন।
এখন আপনি যা পারেন
- টাইপ, reference, pointer, lifetime আর মালিকানা দিয়ে মেমরির ভেতরের ছবিটা নিজের ভাষায় ব্যাখ্যা করতে।
- class, constructor-destructor, Rule of 0/3/5, inheritance আর polymorphism দিয়ে নিজের টাইপ ডিজাইন করতে।
- template, STL container, iterator, algorithm, lambda আর ranges দিয়ে কম কোডে নির্ভুল সমাধান লিখতে।
- move semantics,
optional/variant/expected, constexpr, concepts — আধুনিক C++ বুঝে ব্যবহার করতে, আর undefined behavior চিনতে। - ফাইল, thread, atomics, CMake, test আর profiler দিয়ে একটি বাস্তব প্রজেক্ট দাঁড় করাতে।
- AI-এর লেখা C++ কোড যাচাই করতে — আর দরকারে তার ভুল ধরতে।
পরীক্ষা ও সার্টিফিকেট
লগইন করলে ড্যাশবোর্ডে প্রতিটি মডিউলের টেস্ট আর একটি ফাইনাল পরীক্ষা পাবেন। সব পর্ব পড়া শেষ করে ফাইনালে ৬০% বা তার বেশি পেলে একটি ফ্রি কোর্স-সমাপ্তির সার্টিফিকেট পাবেন। প্রতিটি পর্বের “পড়েছি” বোতাম চাপা থাকলে আপনার অগ্রগতি সেখানে দেখা যাবে। পরীক্ষার আগে পর্ব ৫২–৫৪-এর প্রশ্নব্যাংক একবার ঘুরে আসুন।
অনুশীলন চালিয়ে যাওয়ার উপায়
- প্রতিদিন অল্প: দিনে একটি সমস্যা, পুরো সাত ধাপে — সপ্তাহে একদিন বড় সেশনের চেয়ে বেশি কাজে দেয়। LeetCode, HackerRank, Codeforces আর বাংলাদেশে জনপ্রিয় Toph-এর মতো প্ল্যাটফর্মে এ ধরনের অনেক সমস্যা পাবেন।
- একটি ভুলের খাতা: প্রতিটি ভুলের পর তিন লাইন — কোন ধারণা মিস করেছিলাম, কোন edge case ভুলেছিলাম, পরেরবার কোন সংকেত দেখে চিনব।
- ক্যাপস্টোন বড় করুন: নতুন একটি feature, আরও test, একটি পরিষ্কার README — GitHub-এ রাখলে portfolio নিজে থেকেই বড় হবে।
- অন্যকে শেখান: বন্ধুকে move semantics বোঝাতে গেলেই বুঝবেন নিজের কোথায় ফাঁক আছে।
- মাঝে মাঝে ফিরে আসুন: কোর্স হোম থেকে যে পর্ব দুর্বল লাগে সেটি আবার পড়ুন।
এরপর কোন পথে
Data Structures & Algorithms
ইন্টারভিউর জন্য আরও গভীর ডেটা স্ট্রাকচার, গ্রাফ অ্যালগরিদম আর DP-র ধরন।
System Design
অভিজ্ঞ পদের ইন্টারভিউর বড় অংশ — cache, load balancer, ডেটাবেস ভাগ করা।
Operating Systems
Process, virtual memory, scheduling — thread আর মেমরির নিচের স্তর, সিস্টেম পদের ইন্টারভিউর প্রিয় বিষয়।
Software Engineering & Git
দলে কাজ, code review, version control — প্রজেক্টকে পেশাদার মানে নিয়ে যাওয়া।
বোঝা, যাচাই করা আর ব্যাখ্যা করতে পারা — এগুলোই আপনাকে ইঞ্জিনিয়ার বানায়।
৫৫টি পর্ব শেষ করা সহজ কাজ নয় — আপনার ধৈর্য আর পরিশ্রমকে আন্তরিক অভিনন্দন। প্রথম template error-এর লম্বা বার্তা, প্রথম dangling reference, প্রথম data race — সেগুলোর সামনে যে বিভ্রান্তি ছিল, তা-ই আজ আপনার শক্তি। ইন্টারভিউতে একবার বা দুবার ব্যর্থ হওয়া খুব স্বাভাবিক; প্রতিটি থেকে একটি শিক্ষা খাতায় লিখে নিন, আর পরেরটিতে আরও প্রস্তুত হয়ে যান। শুভকামনা।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমকোডিং ইন্টারভিউতে একটি সমস্যা পেলে ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- ❓ প্রশ্ন করে সমস্যা ও সীমা পরিষ্কার করা
- 📝 নিজে উদাহরণ ও edge case লেখা
- 🐢 একটি সঠিক brute force সমাধান মুখে বলা
- 🧭 বারবার হওয়া কাজ খুঁজে উন্নত পদ্ধতি বাছা
- ⌨️ ইন্টারভিউয়ারের সম্মতি নিয়ে কোড লেখা
- 🔍 নিজের উদাহরণ দিয়ে dry run, তারপর জটিলতা বলা
(মাঝারি) একটি LRU cache-এ get ও put দুটোই O(1) রাখতে কোন container-জোড়া সবচেয়ে উপযুক্ত?
- std::vector আর std::map
- std::map আর std::priority_queue
- std::list আর std::unordered_map (key থেকে list iterator)
- std::deque আর std::unordered_set
(মাঝারি) std::unordered_map-এ একটি key খোঁজার গড় ও খারাপ-ক্ষেত্রের সময়-জটিলতা কত?
- গড়ে O(1), খারাপ ক্ষেত্রে O(log n)
- গড়ে O(1), খারাপ ক্ষেত্রে O(n)
- গড়ে O(log n), খারাপ ক্ষেত্রে O(n)
- সবসময় O(1)
(মাঝারি) n টি সংখ্যা থেকে k-তম বৃহত্তম বের করতে k আকারের min-heap (std::priority_queue ও std::greater) ব্যবহার করলে সময় কত?
- O(n)
- O(n log n)
- O(n · k)
- O(n log k)
(সহজ) একটি string-এ ()[]{} বন্ধনী ঠিকভাবে মেলানো কি না যাচাই করতে কোন data structure স্বাভাবিক পছন্দ?
- std::stack
- std::queue
- std::set
- std::priority_queue
(সহজ) একটি binary tree-এর node-গুলো স্তরে স্তরে (level-order) বের করতে কোনটি লাগে?
- std::stack, কারণ এটি DFS
- std::queue, কারণ এটি BFS
- recursive inorder traversal
- std::map
(মাঝারি) পুনরাবৃত্তিহীন দীর্ঘতম substring-এর sliding window সমাধানে (left শুধু সামনে যায়) সময়-জটিলতা কত?
- O(n²)
- O(n log n)
- O(n)
- O(1)
(সহজ) একটি ফাংশন বড় একটি std::vector<int> শুধু পড়বে, বদলাবে না। প্যারামিটার কীভাবে নেওয়া উচিত?
- const std::vector<int>&
- std::vector<int> (মান দিয়ে)
- std::vector<int>&
- std::vector<int>&&
(মাঝারি) std::priority_queue<int> pq; তারপর ক্রমে 4, 1, 3 push করা হলো। pq.top() কত?
- 3
- 1
- অনির্দিষ্ট — বাস্তবায়নের ওপর নির্ভর করে
- 4
(কঠিন) নিজের লেখা move constructor-এ noexcept না থাকলে std::vector বড় হওয়ার সময় সাধারণত কী করে (টাইপটি copy করা গেলে)?
- compile error দেয়
- উপাদানগুলো move-এর বদলে copy করে
- সবসময় std::terminate ডাকে
- পুনঃবরাদ্দ বন্ধ করে দেয়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব শব্দ এখানে একসাথে:
মনে রাখুন
- ইন্টারভিউয়ার দেখেন বোঝা, যোগাযোগ, সঠিকতা, জটিলতা, টেস্টিং আর C++ কোডের মান — শুধু শেষ কোড নয়।
- প্রতিটি সমস্যায় সাত ধাপ: বুঝুন → উদাহরণ → brute force → উন্নত পদ্ধতি → কোড → পরীক্ষা → জটিলতা ও ফলো-আপ.
- “কোন কাজ বারবার হচ্ছে?” — hash, sliding window, stack/queue, heap, binary search বা DP দিয়ে সেটি দ্রুত করুন, আর trade-off বলুন।
- C++-এ signature-ই বার্তা:
const&,string_view,optional,unique_ptr; Rule of Zero আগে, দরকারে Rule of Five আরnoexceptmove. - মুখস্থ নয়, বুঝুন — ফলো-আপে বোঝাই কাজে আসে; নিজের প্রজেক্ট সৎভাবে ব্যাখ্যা করুন, আর ইন্টারভিউর সময় AI-এর নিয়ম আগে জেনে নিন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- C++ প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — দুর্বল বিষয় খুঁজে আবার পড়ুন।
- পর্ব ৫২ · আউটপুট কী হবে? — C++ কোড পড়ার অনুশীলনরিভিশনDry run-এর অভ্যাস, যা ইন্টারভিউর “পরীক্ষা” ধাপে সরাসরি কাজে লাগে।
- পর্ব ৫৩ · MCQ প্রশ্নব্যাংকরিভিশনপুরো কোর্সের ধারণা দ্রুত যাচাই — online assessment-এর MCQ অংশের প্রস্তুতি।
- পর্ব ৫৪ · লিখিত ও মৌখিক (ভাইভা) প্রশ্নআগের পর্বধারণার প্রশ্নে গুছিয়ে উত্তর দেওয়ার অনুশীলন।
- Data Structures & Algorithmsপরের কোর্সইন্টারভিউর জন্য আরও গভীর ডেটা স্ট্রাকচার ও অ্যালগরিদম।
- System Designগভীরে যানবড় সিস্টেমের নকশা — অভিজ্ঞ পদের ইন্টারভিউর মূল অংশ।