এই পর্বে যা শিখবেন
- BFS-এর স্তরভিত্তিক ধারণা, আর queue, dist ও parent দিয়ে অ্যালগরিদমটি হাতে trace করা
- Unweighted গ্রাফে BFS কেন সবচেয়ে ছোট পথ দেয় — স্তরের যুক্তিতে প্রমাণ লেখা, আর parent pointer থেকে পথ বের করা
- O(V + E) আর O(V²) — কোন উপস্থাপনে কত সময়, কেন
- গ্রিড, multi-source, 0-1 BFS, bipartite যাচাই, component আর state space — একই কাঠামোয় নানা সমস্যা চেনা
- সবচেয়ে প্রচলিত ভুলগুলো (বিশেষ করে pop-এর সময় visited দেওয়া) ধরা, আর C++ ও Python-এ নিজে BFS লেখা
১ · ধারণা — পুকুরের ঢেউ আর বন্ধুদের মধ্যে খবর ছড়ানো
গ্রামের পুকুরে একটি ঢিল ফেলুন। পানিতে প্রথমে একটি ছোট বৃত্ত তৈরি হয়, তারপর তার বাইরে আরেকটি, তারপর আরও বড় একটি। কোনো বৃত্ত তার ভেতরের বৃত্তকে টপকে আগে দূরে পৌঁছায় না — ঢেউ ছড়ায় সমান তালে, স্তরে স্তরে. যে বিন্দুতে ঢেউ আগে পৌঁছায়, সেটি ঢিলের কাছে।
এবার একটি খবরের কথা ভাবুন। রাফি বিকেলে জানল যে কাল ক্লাস বন্ধ। সে তার পাঁচ বন্ধুকে মেসেজ দিল — এরা খবরটি পেল “এক ধাপে”. তারা আবার তাদের বন্ধুদের জানাল, যারা আগে শোনেনি — এরা পেল “দুই ধাপে”. যে একবার খবর শুনেছে, তাকে আবার জানানোর দরকার নেই। শেষে প্রত্যেকের জন্য একটি সংখ্যা দাঁড়ায়: রাফি থেকে কত ধাপ বন্ধুত্বের শিকল পেরিয়ে খবর তার কাছে এসেছে। এই সংখ্যাটিই রাফি থেকে তার দূরত্ব — বন্ধুত্বের graph-এ সবচেয়ে কম কয়টি edge পেরিয়ে পৌঁছানো যায়।
Breadth-first search (BFS, “প্রস্থ-প্রথম অনুসন্ধান”) ঠিক এই প্রক্রিয়ার অ্যালগরিদম। একটি উৎস vertex s থেকে শুরু করে প্রথমে s-এর সব প্রতিবেশী দেখা হয়, তারপর তাদের সব নতুন প্রতিবেশী, তারপর তাদেরও — যেন একটি ঢেউ। “Breadth” মানে প্রস্থ: BFS গভীরে ঢোকার আগে চওড়ায় পুরোটা শেষ করে। এর উল্টো কৌশল হলো একটি পথ ধরে যতদূর সম্ভব গভীরে যাওয়া — সেটি DFS, পরের পর্বের বিষয় (পর্ব ৪১).
উপমা — ব্যাংকের টোকেন। যে আগে টোকেন নেয়, সে আগে সেবা পায়। BFS-এ প্রতিটি নতুন দেখা vertex একটি টোকেন নিয়ে লাইনের পেছনে দাঁড়ায়, আর সামনে থেকে একজন একজন করে “সেবা” পায় — অর্থাৎ তার প্রতিবেশীদের খোঁজা হয়। যেহেতু দূরত্ব ১-এর সবাই দূরত্ব ২-এর সবার আগে টোকেন পেয়েছে, তারা আগেই সেবা পায়। এই লাইনটিই queue (পর্ব ১৪) — যে আগে ঢোকে, সে আগে বের হয় (FIFO).
গ্রাফের ভাষায় বললে (পরিভাষা আর উপস্থাপন পর্ব ৩৯-এ): উৎস s থেকে v-এর সবচেয়ে ছোট পথ-এর দৈর্ঘ্য δ(s, v) মানে s থেকে v-তে যাওয়ার যেকোনো পথের সবচেয়ে কম edge-সংখ্যা; কোনো পথ না থাকলে δ = ∞. BFS vertex-গুলোকে স্তরে ভাগ করে:
- স্তর L0 = {s} — দূরত্ব ০.
- স্তর L1 = s-এর সব প্রতিবেশী — দূরত্ব ১.
- স্তর Lk+1 = যারা আগের কোনো স্তরে নেই, কিন্তু Lk-এর কারও প্রতিবেশী — দূরত্ব k + 1.
যে vertex কোনো স্তরেই পড়ে না, সে s থেকে অগম্য (unreachable). এই স্তরগুলো আপনি আগেও দেখেছেন: tree-র level-order traversal (পর্ব ২৬) আসলে root থেকে BFS. পার্থক্য একটাই — tree-তে cycle নেই, তাই একই node দুবার আসার ভয় নেই; সাধারণ গ্রাফে আছে, তাই “আগে দেখেছি কি না” মনে রাখতে হয়।
ইতিহাস: জার্মান প্রকৌশলী Konrad Zuse ১৯৪৫ সালে তাঁর Plankalkül ভাষার কাজে গ্রাফের connected অংশ খোঁজার জন্য BFS-ধাঁচের একটি পদ্ধতি লিখেছিলেন, কিন্তু সেটি প্রকাশিত হয় অনেক পরে (১৯৭২)। আজকের পরিচিত রূপটি প্রকাশ করেন Edward F. Moore, ১৯৫৯ সালে — শিরোনাম ছিল “The shortest path through a maze”, অর্থাৎ গোলকধাঁধার সবচেয়ে ছোট পথ। আর ১৯৬১ সালে C. Y. Lee একই ধারণা দিয়ে সার্কিট বোর্ডে তার বসানোর পথ খোঁজেন, যা আজও “Lee algorithm” নামে পরিচিত (ভাবনার প্রশ্ন ০১).
২ · অ্যালগরিদম — queue, dist আর parent
BFS চালাতে তিনটি জিনিস লাগে:
- queue — যে vertex-গুলো আবিষ্কৃত হয়েছে কিন্তু যাদের প্রতিবেশী এখনো দেখা হয়নি। একে বলে frontier বা সীমান্ত — ঢেউয়ের সামনের কিনারা।
- dist[v] — উৎস থেকে v-এর দূরত্ব; শুরুতে সবার −1 (বা ∞), মানে “এখনো দেখা হয়নি”. আলাদা visited অ্যারে রাখতে পারেন, কিন্তু dist ≠ −1 নিজেই সেই কাজ করে।
- parent[v] — কোন vertex থেকে v-কে প্রথম দেখা হয়েছিল। এটি দিয়ে পরে পথ বানানো যায় (অংশ ৪).
Pseudocode — BFS from source s (0-based vertices)
BFS(G, s):
for each vertex v: dist[v] = -1; parent[v] = -1
dist[s] = 0
Q = empty queue; Q.push(s)
while Q is not empty:
u = Q.pop_front() // the oldest discovered vertex
for each neighbour v of u:
if dist[v] == -1: // first time anyone sees v
dist[v] = dist[u] + 1 // mark NOW, before pushing
parent[v] = u
Q.push_back(v)একটি মাত্র শর্ত — dist[v] == -1 — নিশ্চিত করে যে প্রতিটি vertex জীবনে একবারই queue-তে ঢোকে। একে বলে “push করার সময় চিহ্ন দেওয়া”, আর এটি না করলে কী বিপদ, তা অংশ ৮-এ দেখবেন। নিচের গ্রাফটি এই পর্বের প্রধান উদাহরণ: ৯টি vertex, ১০টি undirected edge — 0-1, 0-2, 1-3, 1-4, 2-4, 3-5, 4-5, 4-6, 5-7, 6-7; আর vertex 8 একা, তার কোনো edge নেই। ধরে নিন এগুলো ৯ জন বন্ধু, আর খবর শুরু হলো 0-এর কাছ থেকে। প্রতিটি vertex-এর প্রতিবেশী ছোট থেকে বড় ক্রমে রাখা (adjacency list):
| এই ধাপের কাজ | |
| নতুন দেখা (push) | |
| queue-র সবার dist (সামনে → পেছনে) |
- শুরু. উৎস 0-এর
dist[0] = 0, আর 0 ঢুকল queue-তে। বাকি সবার dist = −1, অর্থাৎ “এখনো দেখা হয়নি”. ছবিতে ফিকে সংখ্যাগুলো অদেখা vertex. vertex 8-এর কোনো রাস্তাই নেই। - pop 0. সামনে থেকে 0 বের হলো। তার প্রতিবেশী 1 আর 2 দুজনই অদেখা — তাই এখনই, push করার সময়, তাদের dist = 0 + 1 = 1 আর parent = 0 লেখা হলো। স্তর d=1 তৈরি।
- pop 1. 1-এর প্রতিবেশী 0 (আগেই দেখা, বাদ), 3 আর 4 (নতুন) — dist 2, parent 1. queue এখন [2 3 4], তাদের dist 1 2 2: সামনে ছোট, পেছনে বড়, পার্থক্য ১-এর বেশি নয়।
- pop 2. 2-এর প্রতিবেশী 0 আর 4 — দুজনই আগেই দেখা। 4-কে 1 আগেই আবিষ্কার করেছে, তাই parent[4] = 1 রয়ে গেল; নতুন কিছু push হলো না। স্তর ১ শেষ।
- pop 3. 3-এর অদেখা প্রতিবেশী শুধু 5: dist[5] = 3, parent 3. এখান থেকে স্তর d=3 শুরু।
- pop 4. 4-এর প্রতিবেশী 1, 2, 5 আগেই দেখা (5-কে মাত্রই 3 দেখেছে), নতুন শুধু 6: dist 3, parent 4. queue-তে এখন শুধু স্তর ৩-এর দুজন।
- pop 5. 5-এর অদেখা প্রতিবেশী 7: dist[7] = 4, parent 5. 7-ই শেষ স্তর d=4.
- pop 6. 6-এর প্রতিবেশী 4 আর 7, দুজনই দেখা। কিছু push হলো না।
- pop 7. queue খালি — BFS শেষ। প্রতিটি vertex ঠিক একবার push আর একবার pop হয়েছে। 8-এর dist এখনো −1: উৎস থেকে সেখানে কোনো পথ নেই। ক্রম 0 1 2 3 4 5 6 7 — স্তর ধরে ধরে।
শেষ অবস্থায় dist = [0, 1, 1, 2, 2, 3, 3, 4, −1]. লক্ষ করুন তৃতীয় সারিটি — queue-তে থাকা vertex-গুলোর dist সবসময় সামনে থেকে পেছনে অবনত নয় (কখনো কমে না), আর প্রথম ও শেষটির পার্থক্য সর্বোচ্চ ১. যেমন pop 1-এর পর “1 2 2”, pop 4-এর পর “3 3”. এই পর্যবেক্ষণটিই পরের অংশের প্রমাণের চাবি।
BFS-এর তিনটি নিয়ম মুখস্থ নয়, বুঝে রাখুন: (১) queue — তাই যে আগে দেখা, সে আগে প্রসারিত; (২) প্রথম দেখাতেই dist আর parent লেখা — পরে কেউ আর বদলায় না; (৩) প্রথম দেখাতেই push, তাই প্রত্যেকে ঠিক একবার queue-তে। এই তিনটির যেকোনো একটি ভাঙলে BFS হয় ভুল উত্তর দেয়, নয়তো অনেক ধীর হয়ে যায়।
৩ · কেন সবচেয়ে ছোট পথ — স্তরের প্রমাণ
BFS শুধু “সবাইকে খুঁজে পায়” না, বরং প্রত্যেককে সবচেয়ে কম ধাপে খুঁজে পায়। এটি অনেক অ্যালগরিদমের ভিত্তি, তাই একটি পরিষ্কার প্রমাণ জানা দরকার — বিশ্ববিদ্যালয় বা চাকরির লিখিত পরীক্ষায় এটি প্রায়ই চাওয়া হয়। দাবিটি এই:
ধাপ ১ — dist কখনো আসল দূরত্বের চেয়ে কম নয়
dist[v] = dist[u] + 1 লেখা হয় কেবল যখন u-v একটি edge. তাই dist[v] = k হলে parent ধরে পেছনে হাঁটলে s থেকে v-তে k টি edge-এর একটি সত্যিকারের পথ আছে। সবচেয়ে ছোট পথ এর চেয়ে লম্বা হতে পারে না: dist[v] ≥ δ(s, v). সমস্যা থাকলে একটাই হতে পারে — BFS কোনো vertex-কে প্রয়োজনের চেয়ে লম্বা পথে পেয়েছে। সেটি যে হয় না, তা দেখাতে হবে।
ধাপ ২ — queue-র অপরিবর্তনীয় নিয়ম (invariant)
যেকোনো মুহূর্তে queue-তে সামনে থেকে পেছনে v1, v2, …, vr থাকলে: dist[v1] ≤ dist[v2] ≤ … ≤ dist[vr] ≤ dist[v1] + 1. অর্থাৎ queue-তে সর্বোচ্চ দুটি ভিন্ন দূরত্ব থাকে, d আর d + 1, আর ছোটগুলো সামনে। এটি একটি invariant: শুরুতে সত্য (শুধু s), আর প্রতিটি ধাপ একে রক্ষা করে — সামনে থেকে u (dist d) বের হয়, পেছনে যারা ঢোকে তাদের dist d + 1; বাকিদের dist d বা d + 1, তাই ক্রম আর “পার্থক্য ≤ ১” দুটোই টিকে থাকে। ফল: vertex-গুলো dist-এর অবনত-নয় ক্রমে pop হয় — আগে সব স্তর ০, তারপর সব স্তর ১, এভাবে।
ধাপ ৩ — স্তর ধরে induction
দাবি: প্রতিটি k-এর জন্য, যাদের δ(s, v) = k, তারা সবাই dist = k পায়। Induction k-এর ওপর। k = 0: শুধু s, dist[s] = 0. ধরুন k পর্যন্ত সত্য, আর v-এর δ(s, v) = k + 1. একটি সবচেয়ে ছোট পথ s → … → u → v নিন; তাহলে δ(s, u) = k, তাই অনুমান অনুযায়ী dist[u] = k, আর u একসময় pop হয়। u pop হওয়ার মুহূর্তে দুটি সম্ভাবনা:
- v তখনো অদেখা — তাহলে u-ই তাকে দেখে, dist[v] = k + 1. ✔
- v আগেই দেখা — কোনো w তাকে দেখেছে, যে u-এর আগে pop হয়েছে। ধাপ ২ অনুযায়ী dist[w] ≤ dist[u] = k, তাই dist[v] = dist[w] + 1 ≤ k + 1. আবার ধাপ ১ অনুযায়ী dist[v] ≥ k + 1. তাই dist[v] = k + 1. ✔
আর s থেকে অগম্য v-এর কাছে কোনো পথই নেই, তাই কেউ তাকে push করে না — dist[v] = −1 থাকে, যা δ = ∞-এর সাথে মেলে। ∎
BFS tree আর edge-এর ধরন
প্রতিটি আবিষ্কৃত v (s বাদে) থেকে parent[v]-এর দিকে একটি edge রাখলে একটি tree পাওয়া যায় — BFS tree, যার root s. পৌঁছানো যায় এমন V′ টি vertex থাকলে এতে ঠিক V′ − 1 টি edge. ওপরের গ্রাফে ৮টি vertex পৌঁছানো যায়, tree-তে ৭টি edge: 0-1, 0-2, 1-3, 1-4, 3-5, 4-6, 5-7. বাকি তিনটি (2-4, 4-5, 6-7) non-tree edge. প্রমাণ থেকে একটি গুরুত্বপূর্ণ ফল আসে: undirected গ্রাফে যেকোনো edge (u, v)-এর জন্য |dist[u] − dist[v]| ≤ 1 — একটি edge হয় একই স্তরের দুজনকে জোড়ে, নয়তো পাশাপাশি দুই স্তরকে; কখনো একটি স্তর টপকে যায় না। (যুক্তি: u আগে pop হলে v তখন অদেখা থাকলে dist[v] = dist[u] + 1, আর দেখা থাকলে ধাপ ২-এ dist[v] ≤ dist[u] + 1.) অংশ ৭-এর bipartite যাচাই এই ফলের ওপর দাঁড়িয়ে। Directed graph-এ একটি edge u → v-এর জন্য dist[v] ≤ dist[u] + 1 সত্য, কিন্তু উল্টো দিকে edge অনেক স্তর পেছনেও যেতে পারে।
শুধু unweighted গ্রাফে। প্রমাণে আমরা ধরে নিয়েছি প্রতিটি edge-এর “দাম” সমান (১)। Edge-এর ওজন আলাদা হলে BFS ভুল উত্তর দেয়। উদাহরণ: A → B সরাসরি ১০ কিলোমিটার, আর A → C → B মোট ৩ কিলোমিটার। BFS B-কে এক ধাপে পেয়ে dist ১ দেবে — edge কম, কিন্তু পথ লম্বা। ওজনওয়ালা weighted graph-এ লাগে Dijkstra (পর্ব ৪৩); ওজন শুধু ০ বা ১ হলে একটি সস্তা মধ্যপথ আছে — 0-1 BFS (অংশ ৬)।
৪ · পথ পুনর্গঠন, কোড আর জটিলতা
dist শুধু বলে “কত ধাপ”; গুগল ম্যাপের মতো “কোন পথে” জানতে parent লাগে। parent[v] হলো v-এর ঠিক আগের vertex একটি সবচেয়ে ছোট পথে। তাই লক্ষ্য t থেকে শুরু করে parent ধরে উৎস পর্যন্ত হাঁটুন, তারপর ক্রমটি উল্টে দিন — এটিই path reconstruction. পথের দৈর্ঘ্য dist[t], তাই খরচ O(dist[t]) — BFS-এর তুলনায় নগণ্য। একটি parent অ্যারেই সবার পথ ধরে রাখে, O(V) জায়গায়।
| এই ধাপে | |
| path-এ এখন |
- শুরু x = 7. BFS শেষে parent অ্যারে থেকে শুধু tree-র edge-গুলো রাখলে ওপরের ছবি — BFS tree. লক্ষ্য 7 থেকে শুরু করে path-এ 7 রাখুন।
- x = parent[7] = 5. 7-কে প্রথম দেখেছিল 5, তাই 7-এর ঠিক আগের ধাপ 5. path এখন উল্টো ক্রমে 7, 5.
- x = parent[5] = 3. 5-কে আবিষ্কার করেছিল 3 (4 নয়, কারণ 3 আগে pop হয়েছিল)। প্রতি ধাপে dist ঠিক এক কমছে: 4 → 3 → 2.
- x = parent[3] = 1. আরও এক ধাপ ওপরে।
- x = parent[1] = 0. উৎসে পৌঁছে গেছি; parent[0] = −1, তাই পরের ধাপে লুপ থামবে।
- Reverse. জমানো ক্রম 7 5 3 1 0 — উল্টে দিলে পথ
0 1 3 5 7, ৪টি edge, ঠিক dist[7] = 4. খরচ O(পথের দৈর্ঘ্য). পথ অনন্য নয়: 0 1 4 5 7-ও ৪ ধাপের, কিন্তু parent অ্যারে যেটি প্রথম পেয়েছে সেটিই দেয়।
একটি সূক্ষ্ম বিষয়: 0 থেকে 7-এ ৪ ধাপের পথ দুটি — 0 1 3 5 7 আর 0 1 4 5 7. BFS যেটি দেবে, তা নির্ভর করে adjacency list-এ প্রতিবেশীর ক্রমের ওপর: এখানে 3 আগে pop হয়েছিল, তাই 5-এর parent 3. পরীক্ষায় “একটি সবচেয়ে ছোট পথ” লিখলে যেকোনোটি ঠিক; কিন্তু অনলাইন জাজে যদি নির্দিষ্ট একটি পথ চায় (যেমন lexicographically ছোটটি), তাহলে প্রতিবেশী সাজিয়ে রাখার মতো বাড়তি নিয়ম লাগে।
পুরো কোড — BFS, dist, parent আর পথ
নিচের প্রোগ্রাম ওপরের গ্রাফে 0 থেকে BFS চালায়, pop-এর ক্রম, dist আর parent ছাপে, তারপর 7, 6 আর 8-এর পথ বানায়। C++-এ queue হিসেবে std::queue; Python-এ collections.deque — কারণ Python list-এর pop(0) প্রতিবার পুরো list সরায়, O(n).
#include <algorithm>
#include <initializer_list>
#include <iostream>
#include <queue>
#include <utility>
#include <vector>
using namespace std;
void show(const char* name, const vector<int>& a) {
cout << name; for (int x : a) cout << ' ' << x; cout << '\n';
}
int main() {
int n = 9; // vertex 8 has no roads at all
vector<pair<int, int>> edges = {{0, 1}, {0, 2}, {1, 3}, {1, 4}, {2, 4},
{3, 5}, {4, 5}, {4, 6}, {5, 7}, {6, 7}};
vector<vector<int>> adj(n);
for (auto [u, v] : edges) { adj[u].push_back(v); adj[v].push_back(u); } // undirected
vector<int> dist(n, -1), parent(n, -1), order; // dist -1 = not discovered yet
queue<int> q; dist[0] = 0; q.push(0);
while (!q.empty()) {
int u = q.front(); q.pop(); order.push_back(u);
for (int v : adj[u])
if (dist[v] == -1) { // mark when PUSHING, not when popping
dist[v] = dist[u] + 1; parent[v] = u; q.push(v);
}
}
show("order :", order); show("dist :", dist); show("parent:", parent);
for (int t : {7, 6, 8}) {
if (dist[t] == -1) { cout << "0 -> " << t << ": unreachable\n"; continue; }
vector<int> path; // walk parent pointers back from t
for (int x = t; x != -1; x = parent[x]) path.push_back(x);
reverse(path.begin(), path.end());
cout << "0 -> " << t << ": " << dist[t] << " hops:";
show("", path);
}
}আউটপুট:
order : 0 1 2 3 4 5 6 7
dist : 0 1 1 2 2 3 3 4 -1
parent: -1 0 0 1 1 3 4 5 -1
0 -> 7: 4 hops: 0 1 3 5 7
0 -> 6: 3 hops: 0 1 4 6
0 -> 8: unreachable
প্রথম তিন লাইন ওপরের প্রথম ডায়াগ্রামের শেষ ধাপের সাথে হুবহু মেলে। order লাইনে স্তরগুলো পাশাপাশি: 0 | 1 2 | 3 4 | 5 6 | 7. vertex 8-এর dist −1 থেকে গেছে, তাই তার জন্য পথ বানানোর চেষ্টাই করা হয়নি — এই পরীক্ষা না করলে parent লুপ শুধু “8” ছাপত, যা ভুল উত্তর। adj একটি vector<vector<int>>; undirected গ্রাফে প্রতিটি edge দুই দিকেই রাখা হয়েছে, তাই adjacency list-এ মোট 2E = ২০টি ঘর। পথ বানাতে parent ধরে পেছনে হেঁটে reverse করা হয়েছে, খরচ O(dist[t]).
from collections import deque
n = 9 # vertex 8 has no roads at all
edges = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 4), (3, 5), (4, 5), (4, 6), (5, 7), (6, 7)]
adj = [[] for _ in range(n)]
for u, v in edges: # undirected: store both directions
adj[u].append(v); adj[v].append(u)
def bfs(s):
dist, parent = [-1] * n, [-1] * n # dist -1 = not discovered yet
dist[s] = 0
q, order = deque([s]), []
while q:
u = q.popleft()
order.append(u)
for v in adj[u]:
if dist[v] == -1: # mark when PUSHING, not when popping
dist[v], parent[v] = dist[u] + 1, u
q.append(v)
return dist, parent, order
def path(parent, t): # walk parent pointers back from t
p = []
while t != -1:
p.append(t)
t = parent[t]
return p[::-1]
dist, parent, order = bfs(0)
print("order :", *order)
print("dist :", *dist)
print("parent:", *parent)
for t in (7, 6, 8):
if dist[t] == -1:
print(f"0 -> {t}: unreachable")
else:
print(f"0 -> {t}: {dist[t]} hops:", " ".join(map(str, path(parent, t))))আউটপুট C++-এর সাথে হুবহু এক:
order : 0 1 2 3 4 5 6 7
dist : 0 1 1 2 2 3 3 4 -1
parent: -1 0 0 1 1 3 4 5 -1
0 -> 7: 4 hops: 0 1 3 5 7
0 -> 6: 3 hops: 0 1 4 6
0 -> 8: unreachable
deque-এর popleft() আর append() দুটোই O(1). dist[v], parent[v] = dist[u] + 1, u এক লাইনে দুটি অ্যাসাইনমেন্ট। path ফাংশন t থেকে −1 পর্যন্ত হেঁটে p[::-1] দিয়ে উল্টে দেয়। print("dist :", *dist)-এর * list-এর উপাদানগুলো আলাদা argument হিসেবে দেয়, তাই মাঝে একটি করে space — C++-এর আউটপুটের সাথে অক্ষরে অক্ষরে মেলে।
জটিলতা — O(V + E), আর matrix-এ কেন O(V²)
হিসাবটি দুই ভাগে করুন। (১) প্রতিটি vertex সর্বোচ্চ একবার push আর একবার pop হয়, কারণ push হয় কেবল dist = −1 থেকে বদলানোর সময়, আর সেটি একবারই ঘটে — মোট O(V). (২) একটি vertex u pop হলে তার adjacency list একবার পুরো দেখা হয়, deg(u) টি ধাপে। সব u মিলে Σ deg(u) = 2E (undirected) বা E (directed) — handshake lemma. মোট সময় Θ(V + E) (শুরুতে dist অ্যারে বানানো Θ(V) সহ)। বাড়তি জায়গা Θ(V): dist, parent আর queue — queue-তে একসাথে সর্বোচ্চ V টি vertex থাকে। গ্রাফ রাখতে আরও Θ(V + E).
গ্রাফ যদি adjacency matrix-এ থাকে, তাহলে u-এর প্রতিবেশী খুঁজতে পুরো সারি — V টি ঘর — দেখতে হয়, প্রতিবেশী যত কমই হোক। V টি vertex-এর প্রতিটির জন্য V: Θ(V²). ঘন গ্রাফে (E ≈ V²) দুটো প্রায় সমান, কিন্তু রাস্তার নেটওয়ার্ক বা সামাজিক গ্রাফের মতো বিরল (sparse) গ্রাফে পার্থক্য বিশাল:
| উপস্থাপন | BFS-এর সময় | V = 105, E = 2·105-এ মোটামুটি ধাপ | জায়গা (গ্রাফ) |
|---|---|---|---|
| Adjacency list | Θ(V + E) | ~ 5 · 105 — চোখের পলকে | Θ(V + E) |
| Adjacency matrix | Θ(V²) | 1010 — কয়েক সেকেন্ড থেকে মিনিট | Θ(V²) = 1010 ঘর — মেমরিতেই ধরে না |
| Edge list (শুধু edge-এর তালিকা) | Θ(V · E) যদি প্রতি pop-এ পুরো তালিকা ঘাঁটেন | 2 · 1010 | Θ(E) |
৫ · গ্রিডে BFS — ঢাকার রাস্তার গোলকধাঁধা
অনেক সমস্যায় গ্রাফ সরাসরি দেওয়া থাকে না, কিন্তু লুকিয়ে থাকে। ধরুন ঢাকার একটি এলাকার মানচিত্র একটি গ্রিড: প্রতিটি ঘর একটি মোড়, . মানে খোলা, # মানে মেট্রোরেল বা ড্রেনের কাজের জন্য বন্ধ। আপনি S-এ (বাসস্ট্যান্ড) আছেন, যেতে হবে T-তে (হাসপাতাল), আর প্রতি ধাপে ওপর, নিচ, বাঁ বা ডান পাশের খোলা ঘরে যাওয়া যায়। সবচেয়ে কম কয় ধাপ?
এখানে প্রতিটি খোলা ঘর একটি vertex, আর পাশাপাশি দুটি খোলা ঘরের মধ্যে একটি edge — একটি grid graph. গ্রাফটি আলাদা করে বানানোর দরকার নেই: প্রতিবেশী হিসাব করে নেওয়া যায় (r ± 1, c) আর (r, c ± 1) থেকে। এমন গ্রাফকে বলে implicit graph. R × C গ্রিডে V ≤ R·C আর প্রতিটি ঘরের সর্বোচ্চ ৪টি প্রতিবেশী, তাই E ≤ 2·R·C, আর BFS O(R·C) সময় ও জায়গা নেয়। dr[], dc[] অ্যারে দিয়ে চার দিকের লুপ লেখা কনটেস্টের প্রচলিত কৌশল; ৮ দিকে বা দাবার ঘোড়ার চালে চলতে শুধু অ্যারে দুটি বদলান।
#include <iostream>
#include <queue>
#include <string>
#include <utility>
#include <vector>
using namespace std;
int main() {
vector<string> g = {"S...#.....", // S = start, T = target, # = closed road
".##.#.###.",
".#..#...#.",
".#.####.#.",
".#......#.",
".####.#.#.",
"......#..T"};
int R = g.size(), C = g[0].size();
vector<vector<int>> d(R, vector<int>(C, -1));
vector<vector<pair<int, int>>> from(R, vector<pair<int, int>>(C));
int dr[] = {-1, 1, 0, 0}, dc[] = {0, 0, -1, 1}; // up, down, left, right
queue<pair<int, int>> q; d[0][0] = 0; q.push({0, 0});
while (!q.empty()) {
auto [r, c] = q.front(); q.pop();
for (int k = 0; k < 4; ++k) {
int nr = r + dr[k], nc = c + dc[k];
if (nr < 0 || nr >= R || nc < 0 || nc >= C) continue; // outside the map
if (g[nr][nc] == '#' || d[nr][nc] != -1) continue; // wall or already seen
d[nr][nc] = d[r][c] + 1; from[nr][nc] = {r, c}; q.push({nr, nc});
}
}
cout << "shortest path S -> T: " << d[R - 1][C - 1] << " steps\n";
for (auto p = from[R - 1][C - 1]; p != make_pair(0, 0); p = from[p.first][p.second])
g[p.first][p.second] = '*'; // draw the path, T back to S
for (const string& row : g) cout << row << '\n';
}আউটপুট:
shortest path S -> T: 17 steps
S***#.....
.##*#.###.
.#**#...#.
.#*####.#.
.#******#.
.####.#*#.
......#**T
৭ × ১০ মানচিত্রে S থেকে T-তে সবচেয়ে কম ১৭ ধাপ, আর * চিহ্নগুলো সেই পথ — ১৬টি তারা, প্রতিটি মাঝের একটি মোড়, শেষ ধাপে T. প্রতিটি ঘরের parent রাখা হয়েছে from[r][c]-এ, একটি pair হিসেবে; T থেকে পেছনে হেঁটে S-এ পৌঁছানো পর্যন্ত প্রতিটি ঘরে * বসানো হয়েছে। ওপরের সারির (0, 4) ঘরটি বন্ধ, তাই S থেকে সরাসরি ডানে যাওয়া যায় না; মানচিত্রের ডান-ওপরের অংশে BFS পৌঁছায় মাঝের রাস্তা ঘুরে, অনেক পরে (সেখানে দূরত্ব ১৯ থেকে ২৩)। দুটি continue লাইনই প্রায় সব গ্রিড-BFS-এর ভুলের জায়গা: আগে সীমার বাইরে কি না দেখুন, তারপরই g[nr][nc] পড়ুন — উল্টো ক্রমে পড়লে out-of-bounds access. কোনো পথ না থাকলে d[R-1][C-1] −1 থাকত; বাস্তব কোডে পথ আঁকার আগে সেটি পরীক্ষা করুন।
নিজে চালিয়ে দেখুন — ঢেউ কীভাবে ছড়ায়
নিচের টুলে একই মানচিত্র। “চালিয়ে দেখুন”, তারপর BFS চালান চাপুন: ঘরগুলো queue থেকে বের হওয়ার ক্রমে জ্বলবে, প্রতিটিতে S থেকে দূরত্ব; গাঢ় নীল = এখনকার ঢেউ, শেষে কমলা = সবচেয়ে ছোট পথ। কোনো রাস্তার ঘরে চাপ দিলে সেটি বন্ধ বা খোলা হয়।
const MAP = ["S...#.....", ".##.#.###.", ".#..#...#.", ".#.####.#.", ".#......#.", ".####.#.#.", "......#..T"];
let g = MAP.map(row => [...row]), timer = null;
const R = g.length, C = g[0].length, $ = id => document.getElementById(id);
function bfs() { // same BFS as the C++ program
const d = g.map(row => row.map(() => -1)), from = {}, seen = [[0, 0]]; d[0][0] = 0;
for (let i = 0; i < seen.length; i++) { // the array is the queue; i = its front
const [r, c] = seen[i];
for (const [a, b] of [[r - 1, c], [r + 1, c], [r, c - 1], [r, c + 1]])
if (a >= 0 && a < R && b >= 0 && b < C && g[a][b] !== "#" && d[a][b] === -1) {
d[a][b] = d[r][c] + 1; from[a + "," + b] = [r, c]; seen.push([a, b]);
}
}
const path = new Set(); // walk back from T, like parent[]
for (let p = from[(R - 1) + "," + (C - 1)]; p && (p[0] || p[1]); p = from[p + ""]) path.add(p + "");
return { d, seen, path };
}
function draw(res, k) { // show the first k cells taken from the queue
const shown = new Set(res.seen.slice(0, k).map(x => x + "")), done = k >= res.seen.length;
const wave = k > 0 ? res.d[res.seen[k - 1][0]][res.seen[k - 1][1]] : -1;
let h = ""; for (let r = 0; r < R; r++) for (let c = 0; c < C; c++) {
const key = r + "," + c, ch = g[r][c], on = shown.has(key);
const cls = ch === "#" ? "w" : !on ? "" : done && res.path.has(key) ? "p" : res.d[r][c] === wave ? "f" : "v";
h += `<div class="c ${cls}" data-k="${key}">${ch === "S" || ch === "T" ? ch : on ? res.d[r][c] : ""}</div>`;
}
const t = res.d[R - 1][C - 1]; $("grid").innerHTML = h;
$("msg").textContent = !done ? `ঢেউ এখন দূরত্ব ${wave}-এ` : t < 0 ? "T-তে পৌঁছানোর কোনো পথ নেই" : `সবচেয়ে ছোট পথ ${t} ধাপ, দেখা ঘর ${res.seen.length}টি`;
}
function play() {
clearInterval(timer); const res = bfs(); let k = 0;
timer = setInterval(() => { draw(res, ++k); if (k >= res.seen.length) clearInterval(timer); }, 70);
}
$("grid").onclick = e => { // click a road cell to open or close it
const k = e.target.dataset.k; if (!k) return; const [r, c] = k.split(",").map(Number);
if (g[r][c] === "." || g[r][c] === "#") { g[r][c] = g[r][c] === "#" ? "." : "#"; clearInterval(timer); draw(bfs(), 0); }
};
$("go").onclick = play; $("rs").onclick = () => { g = MAP.map(row => [...row]); clearInterval(timer); draw(bfs(), 0); };
const res0 = bfs();
console.log(`shortest path S -> T: ${res0.d[R - 1][C - 1]} steps`);
g.forEach((row, r) => console.log(row.map((ch, c) => (res0.path.has(r + "," + c) ? "*" : ch)).join("")));
draw(res0, res0.seen.length);কনসোলের আউটপুট C++ প্রোগ্রামের সাথে হুবহু এক:
shortest path S -> T: 17 steps
S***#.....
.##*#.###.
.#**#...#.
.#*####.#.
.#******#.
.####.#*#.
......#**T
এখানে queue হিসেবে একটি সাধারণ অ্যারে seen ব্যবহার করা হয়েছে, আর i হলো queue-র সামনের সূচক: push মানে seen.push, pop মানে i++. কোনো উপাদান মোছা হয় না, তাই প্রতিটি কাজ O(1) — আর বাড়তি লাভ, শেষে seen-এ পুরো BFS-এর ক্রম রয়ে যায়, যা অ্যানিমেশনে লাগে। পরীক্ষা ১: প্রথম সারির পঞ্চম ঘরটি, অর্থাৎ (0, 4), খুলে দিন — নতুন একটি শর্টকাট তৈরি হয়, আর পথ ১৭ থেকে কমে ১৫ ধাপ, ওপর দিয়ে ঘুরে। পরীক্ষা ২: “আগের মানচিত্র” চেপে ফিরে যান, তারপর পঞ্চম সারির ষষ্ঠ ঘর (4, 5) বন্ধ করুন — S-এর দিকের অংশটি পুরো বিচ্ছিন্ন হয়ে যায়, BFS মাত্র ২৩টি ঘর দেখে থামে, আর বার্তা জানায় T-তে পৌঁছানো যায় না। লক্ষ করুন, ঢেউ কখনো একটি সংখ্যা টপকে যায় না — প্রতিটি নতুন গাঢ় ঢেউয়ের সংখ্যা আগেরটির চেয়ে ঠিক ১ বেশি, অংশ ৩-এর invariant চোখের সামনে।
৬ · Multi-source BFS আর 0-1 BFS
Multi-source BFS — নিকটতম হাসপাতাল বা ফায়ার স্টেশন
ধরুন শহরের গ্রিডে কয়েকটি ফায়ার স্টেশন আছে, আর প্রতিটি মোড়ের জন্য জানতে চাই নিকটতম স্টেশন কত ধাপ দূরে। সরল উপায়: প্রতিটি স্টেশন থেকে আলাদা BFS চালিয়ে প্রতিটি ঘরে সর্বনিম্ন রাখা — k টি স্টেশনে O(k·R·C). অনেক ভালো উপায়: সব স্টেশনকে শুরুতেই dist = 0 দিয়ে একসাথে queue-তে রাখুন, তারপর সাধারণ BFS. এটিই multi-source BFS, আর এর খরচ একটি BFS-এর সমান, O(R·C).
| এই ঢেউয়ে নতুন | |
| মোট পাওয়া ঘর |
- ঢেউ ০. দুটি ফায়ার স্টেশন — বাঁ-ওপরের কোণে আর ডান পাশে। দুটিকেই dist = 0 দিয়ে শুরুতেই একসাথে queue-তে রাখা হলো। এটুকুই multi-source BFS-এর পার্থক্য।
- ঢেউ ১. দুই স্টেশনের পাশের ঘরগুলো একই সাথে ১ পেল। কোন ঘর কোন স্টেশনের কাছে, তা আলাদা করে ভাবতে হয়নি।
- ঢেউ ২. দুই ঢেউ ছড়াচ্ছে; মাঝের কলামের দেয়াল (#) পেরোতে ঢেউকে ঘুরে যেতে হচ্ছে।
- ঢেউ ৩. দুই ঢেউ মাঝখানে মিলেছে। যে ঘরে আগে যে ঢেউ পৌঁছায়, সেটিই তার কাছের স্টেশন — পরের ঢেউ সেখানে আর ঢোকে না, কারণ dist আগেই লেখা।
- ঢেউ ৪. নিচের সারির মাঝের দুটি ঘর, দুটিই ৪ ধাপ দূরে — বাঁয়েরটি বাঁ দিকের স্টেশন থেকে, ডানেরটি ডান দিকের স্টেশন থেকে। এখন প্রতিটি খোলা ঘরের সংখ্যা = নিকটতম স্টেশনের দূরত্ব। মোট কাজ একবার BFS-এর সমান, O(R·C) — প্রতিটি স্টেশন থেকে আলাদা BFS চালালে লাগত (স্টেশনের সংখ্যা) × O(R·C).
কেন ঠিক? একটি কাল্পনিক “super source” S* ভাবুন, যা প্রতিটি স্টেশনের সাথে একটি edge দিয়ে যুক্ত। S* থেকে BFS-এর প্রথম স্তরে ঠিক সব স্টেশন, আর প্রতিটি ঘরের dist = ১ + নিকটতম স্টেশনের দূরত্ব। Multi-source BFS সরাসরি সেই প্রথম স্তর থেকে শুরু করে, তাই অংশ ৩-এর প্রমাণ হুবহু খাটে। একই কৌশলে সমাধান হয় “কয়েকটি ঘরে আগুন লাগলে কত মিনিটে সব পুড়বে” বা “ঝুড়ির কয়েকটি পচা আম থেকে কত দিনে সব পচবে” ধাঁচের পরিচিত সমস্যা।
0-1 BFS — ওজন যখন শুধু ০ বা ১
কিছু সমস্যায় edge-এর ওজন দুই রকম: ০ আর ১. যেমন একটি রাস্তায় টোল দিতে হয় (১), অন্যটিতে না (০), আর প্রশ্ন “সবচেয়ে কম কয়বার টোল”; অথবা একটি গ্রিডে প্রতিটি ঘরে একটি তীর আছে — তীরের দিকে যাওয়া বিনা খরচে (০), অন্য দিকে যেতে তীর বদলাতে হয় (১). সাধারণ BFS এখানে ভুল, কারণ সে শুধু edge গোনে। Dijkstra (পর্ব ৪৩) ঠিক উত্তর দেয়, O((V + E) log V)-এ। কিন্তু একটি deque দিয়ে আরও ভালো করা যায় — 0-1 BFS:
Pseudocode — 0-1 BFS (weights are 0 or 1, 0-based vertices)
dist[v] = INF for all v; dist[s] = 0; DQ = deque with s
while DQ is not empty:
u = DQ.pop_front()
for each edge (u, v, w):
if dist[u] + w < dist[v]: // relax: found a cheaper way
dist[v] = dist[u] + w
if w == 0: DQ.push_front(v) // same distance as u: serve it next
else: DQ.push_back(v) // one more: wait behind the others| এই ধাপের কাজ | |
| deque (সামনে → পেছনে) |
- শুরু. edge-এর ওজন ০ বা ১: w0 = বিনা টোলের রাস্তা, w1 = টোল দিতে হয়। প্রশ্ন: 0 থেকে প্রতিটি জায়গায় কমপক্ষে কয়বার টোল? deque-তে শুধু 0, বাকিদের dist অসীম (∞).
- pop 0. 0 → 1 ওজন ০: dist[1] = 0, আর 1 যায় deque-র সামনে. 0 → 3 ওজন ১: dist[3] = 1, 3 যায় পেছনে. সাধারণ BFS হলে 3-কে ১ দিয়ে চূড়ান্ত ধরে ফেলত।
- pop 1. সামনে থেকে বের হলো 1, তার dist শূন্য। 1 → 2 ওজন ০: dist[2] = 0, সামনে।
- pop 2. 2 → 3 ওজন ০: 0 + 0 = 0 < 1, তাই dist[3] কমে 0 হলো আর 3 আবার ঢুকল সামনে। deque-তে এখন 3 দুবার — এটি স্বাভাবিক, ভুল নয়।
- pop 3. সামনের 3 (dist 0) বের হলো। 3 → 4 ওজন ১: dist[4] = 1, পেছনে।
- pop 3, আবার. পুরোনো কপিটি বের হলো, কিন্তু কোনো প্রতিবেশীর dist আর কমে না — কিছুই ঘটে না। (চাইলে “এই vertex আগেই চূড়ান্ত” পরীক্ষা করে বাদ দেওয়া যায়।)
- pop 4. deque খালি। ফল
dist = [0, 0, 0, 0, 1]: 3-তে টোল ছাড়াই যাওয়া যায় (0 → 1 → 2 → 3), যদিও সরাসরি রাস্তাটি টোলের। deque-র সামনে সবসময় সবচেয়ে ছোট dist থাকে — Dijkstra-র priority queue-র কাজটিই এখানে O(1)-এ হচ্ছে।
কেন ঠিক: সাধারণ BFS-এর মতোই একটি invariant — deque-তে সামনে থেকে পেছনে dist অবনত নয়, আর সর্বোচ্চ দুটি মান থাকে, d আর d + 1. সামনে থেকে বের হওয়া u-এর dist d সবচেয়ে ছোট; ওজন ০-এর প্রতিবেশী dist d পেয়ে সামনে যায়, ওজন ১-এর প্রতিবেশী d + 1 পেয়ে পেছনে — দুটোতেই ক্রম টিকে থাকে। তাই deque ঠিক Dijkstra-র priority queue-র মতো সবসময় সবচেয়ে ছোট dist-এর vertex আগে দেয়, কিন্তু প্রতিটি কাজ O(1). মোট সময় O(V + E): একটি vertex একাধিকবার ঢুকতে পারে (ওপরের 3-এর মতো), কিন্তু প্রতিবার dist কমে, আর deque-তে মাত্র দুটি মান থাকে বলে সর্বোচ্চ দুবার। সতর্কতা: এখানে “push-এর সময় visited” চলবে না — dist কমলে আবার ঢোকাতেই হবে; শর্তটি dist[u] + w < dist[v], যাকে বলে relaxation. আর ওজন ২ বা তার বেশি হলে এই কৌশল খাটে না; তখন Dijkstra.
৭ · একই BFS, নানা প্রশ্ন — bipartite, component আর state space
Bipartite যাচাই — দুই রঙে রাঙানো
একটি বিতর্ক উৎসবে অংশগ্রহণকারীদের দুই দলে ভাগ করতে হবে, যাতে প্রতিটি “প্রতিপক্ষ” জোড়া আলাদা দলে পড়ে। গ্রাফের ভাষায়: vertex-গুলোকে দুই রঙে রাঙানো যায় কি, যাতে প্রতিটি edge-এর দুই প্রান্ত আলাদা রঙের? এমন গ্রাফই bipartite graph. BFS দিয়ে উত্তর সহজ: জোড় স্তরের সবাই এক রং, বিজোড় স্তরের সবাই অন্য রং. তারপর প্রতিটি edge দেখুন — কোনো edge-এর দুই প্রান্তের রং এক হলে গ্রাফ bipartite নয়। কোডে আলাদা অ্যারেও লাগে না: color[v] = dist[v] % 2, আর BFS চলার সময়ই কোনো দেখা প্রতিবেশীর dist যদি dist[u]-এর সমান হয়, সংঘাত।
কেন এটি নিখুঁত পরীক্ষা: অংশ ৩ থেকে জানি প্রতিটি edge হয় পাশাপাশি দুই স্তরকে জোড়ে (রং আলাদা, ঠিক আছে), নয়তো একই স্তরের দুজনকে। একই স্তর k-এর u আর v-এর মধ্যে edge থাকলে, BFS tree-তে u আর v থেকে ওপরে উঠে তাদের প্রথম সাধারণ পূর্বপুরুষ (ancestor) a-তে পৌঁছান। a থেকে u আর a থেকে v — দুটি পথের দৈর্ঘ্যই k − dist[a], সমান। দুটি পথ আর edge u-v মিলে একটি cycle, যার দৈর্ঘ্য 2(k − dist[a]) + 1 — বিজোড়. বিজোড় দৈর্ঘ্যের odd cycle-কে দুই রঙে রাঙানো অসম্ভব। তাই: same-layer edge নেই ⇔ গ্রাফ bipartite ⇔ কোনো বিজোড় cycle নেই। প্রধান উদাহরণের গ্রাফে প্রতিটি edge পাশাপাশি স্তরে, তাই সেটি bipartite: {0, 3, 4, 7} আর {1, 2, 5, 6}. এতে 1-2 edge যোগ করলে একই স্তর ১-এর দুজন যুক্ত — cycle 0-1-2, দৈর্ঘ্য ৩, আর গ্রাফ আর bipartite নয়। একাধিক component থাকলে প্রতিটি থেকে আলাদা BFS চালাতে হবে। Edge যখন একে একে আসে আর প্রতিটির পর উত্তর চাই, তখন parity DSU (পর্ব ৩৩) বেশি সুবিধাজনক।
Connected component গোনা
একটি BFS শুধু উৎসের connected component-টি ঘুরে আসে। সবগুলো পেতে বাইরে একটি লুপ: প্রতিটি vertex v-এর জন্য, যদি v এখনো অদেখা হয়, component-এর গণনা এক বাড়ান আর v থেকে BFS চালান (dist অ্যারে সবার জন্য একবারই −1 দিয়ে শুরু, প্রতি BFS-এ নতুন করে নয়)। মোট সময় তবু O(V + E), কারণ প্রতিটি vertex আর edge সব BFS মিলে একবারই দেখা হয়। ওপরের গ্রাফে উত্তর ২: {0, …, 7} আর {8}. মানচিত্রে দ্বীপ গোনা বা ছবি আঁকার সফটওয়্যারের “রং ঢালার বালতি” — এই “flood fill” কাজটিও ঠিক এটাই।
State space-এ BFS — যখন vertex মানে একটি অবস্থা
BFS-এর সবচেয়ে শক্তিশালী ব্যবহার এমন সমস্যায়, যেখানে কোনো গ্রাফ চোখে পড়ে না: প্রতিটি সম্ভাব্য অবস্থা (state) একটি vertex, আর এক চালে যাওয়া একটি edge. “সবচেয়ে কম কয় চালে?” — প্রশ্নটি তখন state space-এ সবচেয়ে ছোট পথ।
- পানির জগ: একটি ৩ লিটার আর একটি ৫ লিটারের জগ, কোনো দাগ নেই। ঠিক ৪ লিটার মাপতে কম কয় চাল? অবস্থা = (ছোট জগে কত, বড় জগে কত); চাল = একটি জগ ভরা, খালি করা, বা একটি থেকে আরেকটিতে ঢালা (যতক্ষণ না একটি খালি বা অন্যটি ভরা হয়)। মোট অবস্থা মাত্র ৪ × ৬ = ২৪টি। (0, 0) থেকে BFS বলে সবচেয়ে কম ৬ চাল: (0,0) → (0,5) → (3,2) → (0,2) → (2,0) → (2,5) → (3,4).
- Word ladder: COLD থেকে WARM, প্রতি ধাপে একটি অক্ষর বদলে, আর প্রতিটি মধ্যবর্তী শব্দ অভিধানে থাকতে হবে: COLD → CORD → CARD → WARD → WARM. এখানে প্রতিটি শব্দ একটি vertex, এক অক্ষরের পার্থক্য একটি edge. পুরো গ্রাফ আগে বানানোর দরকার নেই: একটি শব্দের প্রতিটি অবস্থানে ২৬টি অক্ষর বসিয়ে দেখুন অভিধানের hash set-এ আছে কি না — L অক্ষরের শব্দে প্রতি vertex-এ 26·L টি সম্ভাব্য প্রতিবেশী।
- ধাঁধা আর খেলা: ৮-টাইলের স্লাইডিং পাজলে অবস্থা মানে টাইলগুলোর বিন্যাস — 9!/2 = ১,৮১,৪৪০টি পৌঁছানো যায় এমন অবস্থা, BFS-এর জন্য সহজ। দাবার বোর্ডে ঘোড়াকে এক ঘর থেকে আরেক ঘরে নিতে কম কয় চাল — ৬৪টি অবস্থা।
State space-এ অবস্থাকে একটি tuple, string বা পূর্ণসংখ্যায় এনকোড করে visited রাখুন (hash map বা অ্যারেতে), আর আগে হিসাব করুন অবস্থা কয়টি — সেটিই সময় আর মেমরি ঠিক করে (ভাবনার প্রশ্ন ০৫)।
আরও গভীরে — bidirectional BFS: উৎস আর লক্ষ্য দুটোই জানা থাকলে দুই দিক থেকে একসাথে BFS চালান, প্রতিবার ছোট frontier-টিকে এক স্তর বাড়িয়ে, আর দুই ঢেউ যখন প্রথম একটি সাধারণ vertex-এ মেলে তখন থামুন (সঠিক দৈর্ঘ্য পেতে মিলনের স্তরটি পুরো শেষ করে সব মিলন-বিন্দুর মধ্যে সর্বনিম্ন নিন)। প্রতিটি অবস্থার গড়ে b টি প্রতিবেশী আর উত্তর d ধাপ দূরে হলে এক দিকের BFS প্রায় bd টি অবস্থা ছোঁয়, আর bidirectional BFS ছোঁয় প্রায় 2·bd/2 — b = 10, d = 8-এ ১০ কোটির জায়গায় প্রায় ২০ হাজার। Word ladder আর বড় ধাঁধায় এটি প্রায়ই সময়সীমা পেরোনো আর না-পেরোনোর পার্থক্য; সামাজিক নেটওয়ার্কে “আপনাদের মধ্যে কয় ধাপ” প্রশ্নেও একই কৌশল কাজে লাগে।
৮ · সাধারণ ভুল — pop-এর সময় visited আর অন্যান্য ফাঁদ
BFS-এর কোড ছোট, কিন্তু ভুলের জায়গা কম নয়। সবচেয়ে প্রচলিতটি দেখতে নিরীহ: visited চিহ্ন দেওয়া হয় vertex queue থেকে বের হওয়ার সময়, ঢোকার সময় নয় — ফলে একই vertex বারবার queue-তে ঢোকে। একটি ছোট গ্রাফে দেখুন:
| এই ধাপের কাজ | |
| মোট push এ পর্যন্ত | |
| queue-র দৈর্ঘ্য |
- ভুল সংস্করণ. এখানে visited চিহ্ন দেওয়া হয় pop করার সময়, push করার সময় নয়। শুরুতে queue-তে 0.
- pop 0. 0 visited; 1 আর 2 push. এ পর্যন্ত সঠিক BFS-এর মতোই।
- pop 1. 1 visited. প্রতিবেশী 2 এখনো queue-তে বসে আছে, কিন্তু visited নয় — তাই 2 আবার push হলো! সাথে 3-ও। queue-তে এখন 2 দুবার।
- pop 2. 2 visited. প্রতিবেশী 3 queue-তে আছে, কিন্তু visited নয় — 3-ও দ্বিতীয়বার push.
- pop 2, আবার. দ্বিতীয় কপি বের হলো; pop-এর সময় পরীক্ষা থাকায় বাদ গেল। পরীক্ষাটি না থাকলে 2-এর প্রতিবেশীদের আবার ঘাঁটা হতো।
- pop 3. 3 visited. সব প্রতিবেশী visited, কিছু push নয়।
- pop 3, আবার. বাড়তি কপি বাদ। মোট push ৬টি, অথচ vertex মাত্র ৪টি — সঠিক BFS-এ ঠিক ৪টি push হতো। ঘন গ্রাফে (যেমন complete graph) এই ভুলে queue-তে Θ(E) = Θ(V²) টি উপাদান জমে; বড় গ্রিডে Memory Limit বা Time Limit পেরোনোর ঝুঁকি।
এই ভুলের তিন রূপ: (ক) ডায়াগ্রামের মতো pop-এর পর “আগেই visited হলে বাদ” পরীক্ষা থাকলে উত্তর ঠিক, কিন্তু queue-তে Θ(E) টি উপাদান; (খ) সেই পরীক্ষাও না থাকলে একই vertex বারবার প্রসারিত হয়; (গ) পরীক্ষা ছাড়াই push-এর সময় dist লিখলে পরে আসা লম্বা পথ সঠিক dist-কে মুছে দেয় — উত্তরই ভুল। সমাধান একটাই: যে মুহূর্তে প্রথম দেখলেন, সেই মুহূর্তে চিহ্ন দিন আর push করুন।
| ভুল | ফল | ঠিক উপায় |
|---|---|---|
| pop-এর সময় visited | queue-তে ডুপ্লিকেট, Θ(E) মেমরি; বড় গ্রিডে MLE/TLE | push-এর সময় dist আর visited লিখুন |
Python-এ list.pop(0), C++-এ vector-এর erase(begin()) | প্রতিটি pop O(n), পুরো BFS O(V²) | collections.deque, std::queue, বা অ্যারে + সামনের সূচক |
গ্রিডে আগে g[nr][nc] পড়া, পরে সীমা পরীক্ষা | out-of-bounds — crash বা অদ্ভুত উত্তর | আগে 0 ≤ nr < R, 0 ≤ nc < C, তারপর দেয়াল |
| সারি আর কলাম গুলিয়ে ফেলা (R × C বনাম C × R) | বর্গ নয় এমন গ্রিডে ভুল বা crash | একটি আয়তাকার উদাহরণে (যেমন ২ × ৫) পরীক্ষা করুন |
| Undirected গ্রাফে edge এক দিকেই রাখা | অনেক vertex “অগম্য” দেখায় | adj[u].push_back(v); adj[v].push_back(u); |
| ওজনওয়ালা গ্রাফে BFS | edge কম কিন্তু পথ লম্বা — ভুল উত্তর | ওজন ০/১ হলে 0-1 BFS, নইলে Dijkstra |
| একাধিক test case-এ dist বা visited নতুন করে শুরু না করা | দ্বিতীয় test থেকে ভুল উত্তর | প্রতি test-এ শুধু ব্যবহৃত অংশ −1 করুন |
| অগম্য লক্ষ্যের জন্য পথ বানানো | “পথ” হিসেবে শুধু লক্ষ্যটি ছাপা হয় | আগে dist[t] == −1 পরীক্ষা করুন |
পরীক্ষা ও ইন্টারভিউতে:
- Adjacency list আর matrix-এ BFS-এর time complexity? — list-এ Θ(V + E), matrix-এ Θ(V²), কারণ প্রতিটি vertex-এর পুরো সারি দেখতে হয়।
- BFS কেন unweighted গ্রাফে সবচেয়ে ছোট পথ দেয়? — queue-তে dist অবনত নয় আর পার্থক্য ≤ ১, তাই vertex স্তরের ক্রমে বের হয়; স্তর ধরে induction-এ dist[v] = δ(s, v).
- গ্রিডে কয়েকটি উৎস থেকে নিকটতম দূরত্ব দ্রুত কীভাবে? — multi-source BFS: সব উৎস dist 0 দিয়ে একসাথে queue-তে; O(R·C).
- BFS দিয়ে bipartite কীভাবে যাচাই করবেন? — রং = dist mod 2; একই রঙের দুই প্রান্তের কোনো edge মানে একটি বিজোড় cycle, তাই bipartite নয়।
- Edge-এর ওজন শুধু ০ বা ১ হলে কী ব্যবহার করবেন? — deque দিয়ে 0-1 BFS: ওজন ০ সামনে, ১ পেছনে; O(V + E).
৯ · AI-এর যুগে BFS — কোড নিখুঁত দেখায়, ফাঁদ থাকে ভেতরে
“একটি গ্রিডে BFS লিখে দাও” — AI কয়েক সেকেন্ডে কোড দেবে, কিন্তু BFS-এর ভুল compile error দেয় না, আর ছোট উদাহরণে উত্তরও প্রায়ই ঠিক আসে। AI-এর লেখা কোডে বারবার যা দেখা যায়: pop-এর সময় visited, Python-এ list দিয়ে queue, ওজনওয়ালা গ্রাফে BFS-কে “shortest path” বলে চালিয়ে দেওয়া, আর complexity-তে “O(V + E)” লেখা যখন কোডটি আসলে adjacency matrix ঘাঁটছে। আপনার কাজ হলো প্রতিটি দাবি নিজে যাচাই করা।
BFS-এর কোডে AI সাধারণত যেসব ভুল করে
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
| visited দেওয়া হয় pop-এর পর | প্রতিটি push গুনুন; complete graph K6-এ push ৬-এর বেশি হলে ভুল |
| ওজনওয়ালা গ্রাফে BFS দিয়ে “shortest path” | A→B ওজন ১০, A→C→B মোট ৩ — উত্তর ১০ এলে ভুল |
| 0-1 BFS-এ push-এর সময় visited | অংশ ৬-এর উদাহরণ চালান; dist[3] = 1 এলে ভুল, ঠিক উত্তর 0 |
| পথ পুনর্গঠনে reverse ভুলে যাওয়া বা অগম্য লক্ষ্য না দেখা | উৎস আর লক্ষ্য অদলবদল করে, আর একটি বিচ্ছিন্ন vertex দিয়ে পরীক্ষা |
| “BFS O(V + E)” অথচ কোড matrix-এর সারি ঘাঁটে | লুপটি দেখুন: for v in range(n) হলে Θ(V²) |
| Bipartite যাচাইয়ে শুধু একটি component দেখা | দুটি বিচ্ছিন্ন অংশের গ্রাফ দিন, দ্বিতীয়টিতে একটি ত্রিভুজ রাখুন |
AI-কে সমাধান লিখে দিতে না বলে আপনার কোড আর হাতে করা trace যাচাই করতে বলুন — আর প্রতিটি দাবির কারণ চান:
BFS-এর কোড যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১গোলকধাঁধা থেকে সার্কিট বোর্ড — Moore আর Lee-র কাজ কীভাবে BFS-কে প্রকৌশলের হাতিয়ার বানাল?
১৯৫০-এর দশকে Bell Labs-এর গবেষক Edward F. Moore কম্পিউটার আর স্বয়ংক্রিয় যন্ত্রের তত্ত্ব নিয়ে কাজ করছিলেন — তাঁর নামে “Moore machine” আজও automata তত্ত্বে পড়ানো হয়। সেই সময় Claude Shannon একটি যান্ত্রিক ইঁদুর “Theseus” বানিয়েছিলেন, যা একটি গোলকধাঁধায় পথ খুঁজে শিখত। ১৯৫৯ সালে Moore একটি সম্মেলনে “The shortest path through a maze” প্রবন্ধে দেখান কীভাবে একটি গোলকধাঁধার সবচেয়ে ছোট পথ যান্ত্রিকভাবে পাওয়া যায়: শুরুর ঘর থেকে প্রতিটি ধাপে পরের স্তরের ঘরগুলো চিহ্নিত করুন, যতক্ষণ না লক্ষ্যে পৌঁছান, তারপর সংখ্যা কমিয়ে কমিয়ে পেছনে ফিরুন। এটিই আজকের BFS আর parent pointer-এর পথ পুনর্গঠন।
প্রায় একই সময়ে আরেকটি প্রকৌশল-সমস্যা মাথাচাড়া দিচ্ছিল। প্রিন্টেড সার্কিট বোর্ডে শত শত উপাদানের মধ্যে তামার তার বসাতে হয়, আর দুটি তার পরস্পরকে কাটতে পারে না। ১৯৬১ সালে C. Y. Lee বোর্ডটিকে একটি গ্রিড হিসেবে দেখলেন: আগেই বসানো তার আর উপাদান হলো দেয়াল, আর নতুন তারের জন্য শুরুর পিন থেকে একটি “ঢেউ” ছড়ান — প্রতিটি ঘরে লিখুন সেখানে পৌঁছাতে কত ধাপ লাগে। ঢেউ লক্ষ্য পিনে পৌঁছালে সংখ্যা কমিয়ে কমিয়ে পেছনে ফিরলেই সবচেয়ে ছোট তার। এই Lee algorithm বা “maze routing” ঠিক অংশ ৫-এর গ্রিড BFS, আর এর একটি নিশ্চয়তা প্রকৌশলীদের কাছে অমূল্য ছিল: পথ থাকলে এটি অবশ্যই খুঁজে পায়, আর সবচেয়ে ছোটটিই পায়। দাম ছিল মেমরি — প্রতিটি ঘরে একটি সংখ্যা রাখতে হয়, তাই পরে প্রকৌশলীরা দুই প্রান্ত থেকে ঢেউ ছড়ানোর মতো নানা সাশ্রয়ী কৌশল বের করেন। আজকের chip ডিজাইনের routing সফটওয়্যারের অনেকগুলোর ভেতরে এখনো এই ঢেউ-বিস্তারের ধারণা আছে।
মূল উপলব্ধি: BFS-এর জন্ম কোনো বিমূর্ত গণিতের প্রশ্ন থেকে নয়, বরং খুব বাস্তব দুটি সমস্যা থেকে — গোলকধাঁধা আর তার বসানো। “স্তরে স্তরে ছড়াও, তারপর পেছনে ফেরো” — এই সরল কৌশলটি যেখানেই প্রতিটি ধাপের খরচ সমান, সেখানেই সবচেয়ে ছোট পথের নিশ্চয়তা দেয়।
প্র ০২“ছয় ধাপের বিচ্ছেদ” — সামাজিক নেটওয়ার্কে দুজন মানুষ কত দূরে, আর BFS কীভাবে এর উত্তর দেয়?
১৯৬০-এর দশকে আমেরিকান মনোবিজ্ঞানী Stanley Milgram একটি বিখ্যাত পরীক্ষা চালান। মধ্য-আমেরিকার কিছু মানুষকে একটি চিঠি দিয়ে বলা হলো, বোস্টনের একজন নির্দিষ্ট অপরিচিত মানুষের কাছে সেটি পৌঁছাতে — কিন্তু শুধু নিজের চেনা কাউকে পাঠিয়ে, যিনি লক্ষ্যের কাছাকাছি হতে পারেন। যে চিঠিগুলো পৌঁছেছিল, সেগুলো গড়ে প্রায় পাঁচ-ছয়জনের হাত ঘুরে পৌঁছেছিল। এখান থেকেই জনপ্রিয় ধারণা “six degrees of separation” — পৃথিবীর যেকোনো দুজন মানুষ মোটামুটি ছয় ধাপের পরিচয়ের শিকলে যুক্ত। পরীক্ষাটির পদ্ধতি নিয়ে পরে অনেক সমালোচনা হয়েছে (অনেক চিঠি পৌঁছায়ইনি), কিন্তু ধারণাটি টিকে যায়।
গ্রাফের ভাষায় প্রশ্নটি হলো: মানুষ vertex, পরিচয় edge — দুই vertex-এর মধ্যে সবচেয়ে ছোট পথ কত? এটি ঠিক BFS-এর প্রশ্ন। Milgram-এর সময় পুরো গ্রাফ কারও হাতে ছিল না, কিন্তু সামাজিক নেটওয়ার্কের যুগে আছে। ২০১১ সালে Facebook-এর গবেষকরা তখনকার প্রায় ৭২ কোটি সক্রিয় ব্যবহারকারীর গ্রাফে মেপে দেখান যে দুজন ব্যবহারকারীর মধ্যে গড় দূরত্ব পাঁচের কম, আর ২০১৬ সালে আরও বড় গ্রাফে তাঁরা জানান, দুজনের মাঝে গড়ে প্রায় সাড়ে তিনজন মধ্যবর্তী মানুষ। এত বড় গ্রাফে প্রতিটি জোড়ার জন্য BFS চালানো অসম্ভব — V টি উৎস থেকে BFS মানে O(V(V + E)). তাই গবেষকরা নমুনা নিয়ে, আর একসাথে অনেক উৎসের ঢেউ সংক্ষিপ্তভাবে রাখার সম্ভাবনাভিত্তিক কৌশল (যেমন HyperLogLog-ধাঁচের counter) দিয়ে দূরত্বের বিন্যাস অনুমান করেন।
গণিতবিদদের মধ্যে একই ধারণার একটি মজার রূপ আছে — Erdős number. হাঙ্গেরির গণিতবিদ Paul Erdős প্রায় দেড় হাজার গবেষণাপত্র লিখেছিলেন, শত শত সহ-লেখকের সাথে। Erdős-এর নিজের নম্বর ০, তাঁর সহ-লেখকদের ১, তাঁদের সহ-লেখকদের ২ — এভাবে। এটি আক্ষরিক অর্থেই সহ-লেখকত্বের গ্রাফে Erdős থেকে BFS-এর dist. চলচ্চিত্র জগতে একই খেলার নাম “Bacon number” — অভিনেতা Kevin Bacon থেকে একসাথে ছবিতে অভিনয়ের শিকল ধরে দূরত্ব, আর একটি জনপ্রিয় ওয়েবসাইট (Oracle of Bacon) BFS দিয়েই উত্তর দেয়।
মূল উপলব্ধি: “আমরা কত দূরে” প্রশ্নটি প্রতিদিনের — বন্ধুর সাজেশন, LinkedIn-এর “2nd connection” — আর তার পেছনে আছে BFS-এর স্তর। কিন্তু বিশাল গ্রাফে সঠিক BFS-এর খরচই প্রকৌশলীদের নমুনা আর অনুমানের দিকে ঠেলে দেয়।
প্র ০৩Web crawler কেন প্রায়ই BFS-এর মতো চলে, আর BFS-এর মেমরি কেন DFS-এর চেয়ে বড় সমস্যা হতে পারে?
একটি সার্চ ইঞ্জিনের প্রথম কাজ হলো ওয়েব “ঘুরে দেখা” — কিছু শুরুর পাতা থেকে লিংক ধরে নতুন পাতায় যাওয়া। এই প্রোগ্রামকে বলে crawler. ওয়েব একটি বিশাল directed graph: পাতা vertex, লিংক edge. কোন ক্রমে ঘোরা হবে? DFS হলে crawler একটি লিংকের শিকল ধরে গভীরে চলে যেতে পারে — যেমন একটি ক্যালেন্ডার সাইটের “পরের মাস” লিংক, যা অসীম পর্যন্ত চলে — আর গুরুত্বপূর্ণ পাতাগুলো অনেক দেরিতে পায়। BFS হলে আগে শুরুর পাতার কাছের পাতাগুলো আসে। ২০০১ সালের একটি পরিচিত গবেষণায় (Najork ও Wiener) দেখা যায়, breadth-first ক্রমে ঘুরলে শুরুর দিকেই উঁচু মানের পাতা (যেগুলোতে অনেক লিংক আসে) বেশি পাওয়া যায়। বাস্তব crawler-গুলো অবশ্য খাঁটি BFS নয় — তারা queue-কে অগ্রাধিকার দিয়ে সাজায়, একই সার্ভারে খুব দ্রুত বারবার অনুরোধ পাঠায় না, আর visited-এর জন্য বিশাল hash বা Bloom filter ব্যবহার করে।
কিন্তু BFS-এর একটি দাম আছে, যা ছোট উদাহরণে চোখে পড়ে না: frontier-এর আকার. ধরুন প্রতিটি অবস্থার নতুন প্রতিবেশী b টি করে — একে বলে branching factor. d স্তরে BFS-এর queue-তে একসাথে প্রায় bd টি অবস্থা থাকতে পারে — পুরো স্তরটি একসাথে। DFS-এর stack-এ থাকে শুধু বর্তমান পথ, প্রায় b·d. b = 10, d = 10-এ BFS-এর frontier প্রায় এক হাজার কোটি (1010), DFS-এর মাত্র শ-খানেক। এ কারণেই কৃত্রিম বুদ্ধিমত্তার সার্চে (যেমন ধাঁধা সমাধান) একটি মিশ্র কৌশল জনপ্রিয় — iterative deepening DFS: সীমা ১ দিয়ে DFS, তারপর সীমা ২, ৩, … — BFS-এর মতোই সবচেয়ে কম ধাপের উত্তর দেয়, কিন্তু মেমরি DFS-এর মতো কম। আগের স্তরগুলো বারবার ঘোরা হলেও মোট কাজ খুব বেশি বাড়ে না, কারণ শেষ স্তরটিই সবচেয়ে বড়।
মূল উপলব্ধি: BFS কাছের জিনিস আগে দেয় আর সবচেয়ে ছোট পথের নিশ্চয়তা দেয় — কিন্তু চওড়া গ্রাফে তার frontier বিস্ফোরিত হতে পারে। কোন সার্চ বেছে নেবেন, তা নির্ভর করে সময়ের চেয়ে মেমরি বেশি দামি কি না তার ওপর।
প্র ০৪সুপারকম্পিউটারের দৌড়ে BFS — Graph500 বেঞ্চমার্ক আর “direction-optimizing” BFS কী?
সুপারকম্পিউটারের গতি মাপার সবচেয়ে পরিচিত তালিকা TOP500, যা মূলত বিশাল ঘন matrix-এর হিসাবের গতি মাপে (LINPACK). কিন্তু আধুনিক অনেক কাজ — সামাজিক নেটওয়ার্ক বিশ্লেষণ, সাইবার নিরাপত্তা, জীববিজ্ঞানের প্রোটিন নেটওয়ার্ক — ঘন matrix নয়, বরং বিশাল বিরল গ্রাফ নিয়ে। সেখানে কাজের ধরন একেবারে আলাদা: মেমরির এলোমেলো জায়গা থেকে অল্প অল্প ডেটা পড়া, যেখানে প্রসেসরের গতির চেয়ে মেমরির দেরিই বাধা। এই ধরনের কাজ মাপতে ২০১০ সালে Graph500 নামে একটি বেঞ্চমার্ক শুরু হয়, আর তার কেন্দ্রীয় পরীক্ষাটি হলো — একটি বিশাল কৃত্রিম গ্রাফে BFS. ফল মাপা হয় TEPS-এ: প্রতি সেকেন্ডে কতগুলো edge পেরোনো গেল।
এত বড় গ্রাফে BFS দ্রুত করার সবচেয়ে বিখ্যাত কৌশলটি এসেছে ২০১২ সালে, Scott Beamer, Krste Asanović ও David Patterson-এর কাজে: direction-optimizing BFS. সাধারণ BFS “top-down”: frontier-এর প্রতিটি vertex তার সব প্রতিবেশীকে একে একে পরীক্ষা করে, অদেখা কি না। সামাজিক গ্রাফে কয়েক স্তর পরেই frontier বিশাল হয়ে যায়, আর এই প্রশ্নগুলোর বেশিরভাগই অপচয় — প্রতিবেশীরা আগেই দেখা। তখন উল্টো দিক থেকে চালানো সস্তা, “bottom-up”: প্রতিটি অদেখা vertex তার প্রতিবেশীদের মধ্যে খোঁজে কেউ frontier-এ আছে কি না, আর একজনকে পেলেই থামে। Frontier ছোট থাকলে top-down, বড় হলে bottom-up, আবার ছোট হলে top-down — এই সুইচ করে তাঁরা বড় গ্রাফে কয়েক গুণ গতি পান।
দুটি পদ্ধতিই একই BFS — একই স্তর, একই dist দেয়; শুধু প্রতিটি স্তর বানানোর কাজটি দুই দিক থেকে করা যায়। এটি একটি সুন্দর শিক্ষা: অ্যালগরিদমের তাত্ত্বিক জটিলতা (O(V + E)) একই থাকলেও বাস্তব গতি নির্ভর করে কোন edge আসলে ছোঁয়া হয় আর মেমরি কীভাবে পড়া হয় তার ওপর।
মূল উপলব্ধি: BFS এত মৌলিক যে সুপারকম্পিউটারের গ্রাফ-দক্ষতা মাপা হয় এটি দিয়েই। আর সবচেয়ে বড় উন্নতিটি এসেছে একটি সরল প্রশ্ন থেকে — “প্রতিবেশীকে জিজ্ঞেস করব, নাকি প্রতিবেশী আমাকে জিজ্ঞেস করবে?”
প্র ০৫৮-টাইলের পাজল থেকে ১৫-টাইলের পাজল আর রুবিকস কিউব — state space কতটা বড় হলে BFS আর চলে না?
স্লাইডিং টাইলের পাজল উনিশ শতকের শেষে আমেরিকায় একটি উন্মাদনা তৈরি করেছিল — ৪ × ৪ বোর্ডে ১৫টি টাইল আর একটি ফাঁকা ঘর। জোড়-বিজোড়ের (parity) একটি যুক্তিতে দেখা যায়, সম্ভাব্য বিন্যাসের ঠিক অর্ধেক থেকেই সমাধানে পৌঁছানো যায়। ছোট ভাই ৩ × ৩-এর ৮-টাইলের পাজলে পৌঁছানো যায় এমন অবস্থা 9!/2 = ১,৮১,৪৪০টি। এটি BFS-এর জন্য খুবই ছোট: সমাধান-অবস্থা থেকে একবার BFS চালালেই প্রতিটি অবস্থার সবচেয়ে কম চাল জানা যায়, আর দেখা যায় সবচেয়ে কঠিন অবস্থাগুলোর জন্য ৩১ চাল লাগে।
১৫-টাইলের পাজলে অবস্থা 16!/2 — প্রায় ১০ লাখ কোটি (1013). প্রতিটি অবস্থার জন্য এক বাইট রাখলেও ১০ টেরাবাইট। দীর্ঘদিন এর পুরো BFS অসম্ভব মনে হতো। ২০০৫ সালে Richard Korf ও Peter Schultze ডিস্ক-ভিত্তিক সমান্তরাল BFS দিয়ে পুরো state space ঘুরে দেখান যে যেকোনো অবস্থা সর্বোচ্চ ৮০ চালে সমাধান করা যায়। এখানে কৌশল ছিল visited-কে RAM-এ না রেখে ডিস্কে রাখা, আর একটি সুন্দর পর্যবেক্ষণ: স্তর-ভিত্তিক BFS-এ নতুন স্তর বানাতে শুধু আগের দুটি স্তর লাগে (undirected গ্রাফে প্রতিটি edge পাশাপাশি বা একই স্তরে — অংশ ৩) — তাই পুরো visited মনে রাখার দরকার নেই।
রুবিকস কিউবে অবস্থা প্রায় 4.3 × 1019 — সরাসরি BFS কল্পনারও বাইরে। “যেকোনো অবস্থা থেকে সর্বোচ্চ কত চাল” — এই “God’s number” প্রশ্নটি ২০১০ সালে মীমাংসা হয়: ২০ চাল (একটি নির্দিষ্ট চাল-গণনার নিয়মে)। Tomas Rokicki, Herbert Kociemba, Morley Davidson ও John Dethridge গাণিতিক প্রতিসাম্য আর বিশেষ সার্চ কৌশল মিলিয়ে, Google-এর দেওয়া বিপুল কম্পিউটার-সময় ব্যবহার করে এটি প্রমাণ করেন। সেখানে খাঁটি BFS নয়, বরং অবস্থাগুলোকে বড় দলে ভাগ করে প্রতিটি দলকে আলাদাভাবে সমাধান করা হয়েছিল।
প্রতিদিনের প্রোগ্রামিংয়ে শিক্ষাটি সরল: state space-এ BFS লেখার আগে অবস্থার সংখ্যা হিসাব করুন। কয়েক কোটি পর্যন্ত — একটি অ্যারে বা bitset-এ visited রেখে BFS চলে। তার বেশি হলে bidirectional BFS, প্রতিসাম্য দিয়ে অবস্থা কমানো, বা A* (একটি অনুমান-ফাংশন দিয়ে লক্ষ্যের দিকে ঝোঁকানো সার্চ) — কিংবা প্রশ্নটিই নতুন করে ভাবা।
মূল উপলব্ধি: BFS-এর খরচ O(V + E) — কিন্তু state space-এ V নিজেই exponential হতে পারে। অ্যালগরিদম ঠিক থাকলেও অবস্থার সংখ্যা ঠিক করে দেয় সমস্যাটি এক সেকেন্ডের, না এক শতাব্দীর।
অনুশীলন
খাতা-কলম নিন। প্রথম তিনটি হাতে করার, শেষটিতে কোড লিখতে হবে।
-
হাতে trace করুন: একটি undirected গ্রাফে ৭টি vertex (0–6) আর edge 0-1, 1-2, 0-3, 3-4, 1-4, 4-5, 2-5, 5-6. প্রতিটি vertex-এর প্রতিবেশী ছোট থেকে বড় ক্রমে। উৎস 0 থেকে BFS চালিয়ে pop-এর ক্রম, dist আর parent লিখুন, তারপর 0 থেকে 6-এর পথ বের করুন।
Adjacency: 0: 1, 3 · 1: 0, 2, 4 · 2: 1, 5 · 3: 0, 4 · 4: 1, 3, 5 · 5: 2, 4, 6 · 6: 5. pop 0 → push 1, 3. pop 1 → push 2, 4. pop 3 → 0, 4 দুজনই দেখা। pop 2 → push 5 (parent 2). pop 4 → 1, 3, 5 দেখা। pop 5 → push 6. pop 6.
ক্রম 0 1 3 2 4 5 6; dist =
[0, 1, 2, 1, 2, 3, 4]; parent =[−1, 0, 1, 0, 1, 2, 5]. পথ: 6 → 5 → 2 → 1 → 0, উল্টে 0 1 2 5 6 (৪ ধাপ)। ফাঁদ: 5-এর parent 2, 4 নয় — কারণ 2 আগে pop হয়েছে (queue-তে 2 ঢুকেছিল 4-এর আগে)। 0 1 4 5 6-ও সমান লম্বা, কিন্তু এই BFS সেটি দেবে না। -
Bipartite হাতে যাচাই করুন: ৫টি vertex-এর একটি cycle — edge 0-1, 1-2, 2-3, 3-4, 4-0. BFS-এর স্তর দিয়ে রং দিন (রং = dist mod 2) আর বলুন কোন edge-এ প্রথম সংঘাত ধরা পড়ে। তারপর বিজোড় cycle-টি লিখুন।
উৎস 0: dist[0] = 0. pop 0 → 1 আর 4, dist ১. pop 1 → 2, dist ২. pop 4 → 3, dist ২. pop 2 → প্রতিবেশী 3 আগেই দেখা, আর dist[3] = 2 = dist[2] — একই স্তরের দুজনের মধ্যে edge 2-3, সংঘাত। রং: 0 → 0; 1, 4 → 1; 2, 3 → 0, আর 2-3 দুজনই রং 0.
অংশ ৭-এর যুক্তিতে: 2 আর 3 থেকে BFS tree-তে ওপরে উঠে প্রথম সাধারণ পূর্বপুরুষ 0. পথ 0-1-2 আর 0-4-3, প্রতিটি দৈর্ঘ্য ২, সাথে edge 2-3 — cycle 0-1-2-3-4-0, দৈর্ঘ্য 2·2 + 1 = ৫, বিজোড়. তাই গ্রাফটি bipartite নয়। (যেকোনো বিজোড় দৈর্ঘ্যের cycle-এর জন্যই এটি সত্য; জোড় দৈর্ঘ্যের cycle, যেমন ৪ বা ৬, bipartite.)
-
ভেবে বলুন: (ক) একটি গ্রাফে V = 104 আর E = 3·104. adjacency matrix-এ BFS আর adjacency list-এ BFS — মোটামুটি কত ধাপ করে? (খ) একটি directed গ্রাফে edge-এর ওজন: A→B ১, A→C ০, C→B ০, B→D ১. সাধারণ BFS (ওজন উপেক্ষা করে edge গুনে) আর 0-1 BFS — A থেকে D-এর উত্তর কত দেবে?
(ক) Matrix: প্রতিটি vertex-এর জন্য পুরো সারি, V² = 108 ঘর দেখা — কয়েক দশমিক সেকেন্ড, আর matrix-এর জন্যই 108 ঘরের মেমরি। List: V + 2E = 104 + 6·104 = 7·104 ধাপ — হাজার গুণেরও বেশি কম। Directed হলে list-এ V + E = 4·104.
(খ) সাধারণ BFS: A থেকে B এক edge, B থেকে D আরেক — dist[D] = ২ “ধাপ”, কিন্তু এটি ওজনের যোগফল নয়; পথ A→B→D-এর ওজন ১ + ১ = ২. 0-1 BFS: A (0) → C ওজন ০ (dist 0, সামনে) → B ওজন ০ (dist 0, যদিও A→B সরাসরি ১ দিয়ে আগে পেছনে ঢুকেছিল) → D ওজন ১ — dist[D] = ১, পথ A→C→B→D. সাধারণ BFS এখানে ২ বলে, যা সবচেয়ে কম ওজন নয়।
-
কোড লিখুন: একটি R × C গ্রিডে
H= হাসপাতাল,#= বন্ধ রাস্তা,.= খোলা। প্রতিটি খোলা ঘর থেকে নিকটতম হাসপাতালের দূরত্ব বের করুন (পৌঁছানো না গেলে −1), multi-source BFS দিয়ে, O(R·C)-এ। অংশ ৬-এর ৪ × ৬ গ্রিড দিয়ে হাতে মেলান।মূল কৌশল: সব
H-কে dist 0 দিয়ে শুরুতেই queue-তে রাখা; বাকিটা অংশ ৫-এর গ্রিড BFS হুবহু।// multi-source BFS: distance from every cell to the nearest 'H' vector<vector<int>> nearest(const vector<string>& g) { int R = g.size(), C = g[0].size(); vector<vector<int>> d(R, vector<int>(C, -1)); queue<pair<int, int>> q; for (int r = 0; r < R; ++r) for (int c = 0; c < C; ++c) if (g[r][c] == 'H') { d[r][c] = 0; q.push({r, c}); } // all sources at once int dr[] = {-1, 1, 0, 0}, dc[] = {0, 0, -1, 1}; while (!q.empty()) { auto [r, c] = q.front(); q.pop(); for (int k = 0; k < 4; ++k) { int a = r + dr[k], b = c + dc[k]; if (a < 0 || a >= R || b < 0 || b >= C) continue; if (g[a][b] == '#' || d[a][b] != -1) continue; d[a][b] = d[r][c] + 1; q.push({a, b}); } } return d; }৪ × ৬ গ্রিডে ফল (সারি ধরে, # = −1):
0 1 2 # 3 2/1 2 3 # 2 1/2 # 3 2 1 0/3 4 4 3 # 1— ওপরের ডায়াগ্রামের শেষ ধাপ। সময় আর জায়গা O(R·C), হাসপাতাল যতগুলোই থাকুক। কোন হাসপাতাল সবচেয়ে কাছে তাও চাইলে আরেকটি অ্যারেsrc[a][b] = src[r][c]রাখুন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমউৎস s থেকে BFS-এর কাজগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- সব vertex-এর dist = −1 করা, তারপর dist[s] = 0 আর s-কে queue-তে push
- queue-র সামনে থেকে একটি vertex u বের করা (pop)
- u-এর adjacency list-এর প্রতিবেশী v একে একে দেখা
- v অদেখা হলে (dist[v] == −1) dist[v] = dist[u] + 1 আর parent[v] = u লেখা
- সেই v-কে queue-র পেছনে push করা; queue খালি না হওয়া পর্যন্ত আবার pop
V টি vertex আর E টি edge-এর একটি গ্রাফ adjacency list-এ রাখা। একটি উৎস থেকে BFS-এর time complexity কত?
- Θ(V²)
- Θ(V + E)
- Θ((V + E) log V)
- Θ(V · E)
একটি undirected, unweighted গ্রাফে উৎস s থেকে BFS চলার সময় queue-তে থাকা vertex-গুলোর dist সম্পর্কে কোনটি সবসময় সত্য?
- সবার dist সবসময় সমান
- সামনে থেকে পেছনে dist কমতে থাকে
- সামনে থেকে পেছনে dist অবনত নয়, আর প্রথম ও শেষটির পার্থক্য সর্বোচ্চ ১
- dist-গুলোর মধ্যে যেকোনো পার্থক্য থাকতে পারে
BFS-এ visited চিহ্ন vertex queue-তে ঢোকানোর সময় না দিয়ে queue থেকে বের করার সময় দিলে (আর বের করার পর আগে-দেখা হলে বাদ দিলে) কী ঘটে?
- একই vertex একাধিকবার queue-তে ঢোকে; উত্তর ঠিক থাকলেও মেমরি ও সময় বাড়ে
- অ্যালগরিদম অসীম লুপে আটকে যায়
- কিছু vertex একেবারেই দেখা হয় না
- কোনো পার্থক্য নেই, বরং একটু দ্রুত হয়
একটি গ্রিডে k টি হাসপাতাল আছে, আর প্রতিটি ঘর থেকে নিকটতম হাসপাতালের দূরত্ব চাই। R × C গ্রিডে সবচেয়ে দক্ষ পদ্ধতি আর তার সময় কোনটি?
- প্রতিটি হাসপাতাল থেকে আলাদা BFS, O(k · R · C)
- প্রতিটি ঘর থেকে BFS চালিয়ে প্রথম হাসপাতাল খোঁজা, O((R · C)²)
- Dijkstra, O(R · C · log(R · C))
- সব হাসপাতাল শুরুতেই queue-তে রেখে একটি multi-source BFS, O(R · C)
একটি গ্রাফের edge-এর ওজন শুধু ০ বা ১. 0-1 BFS-এ deque থেকে বের হওয়া u-এর একটি প্রতিবেশী v-এর dist কমলে v-কে কোথায় রাখা হয়?
- ওজন যা-ই হোক, সবসময় সামনে
- ওজন ০ হলে সামনে, ওজন ১ হলে পেছনে
- ওজন ০ হলে পেছনে, ওজন ১ হলে সামনে
- ওজন যা-ই হোক, সবসময় পেছনে
একটি connected undirected গ্রাফে BFS চালানোর পর দেখা গেল একটি edge-এর দুই প্রান্ত u আর v-এর dist সমান। এ থেকে কী নিশ্চিতভাবে বলা যায়?
- edge-টি BFS tree-র অংশ
- গ্রাফে কোনো cycle নেই
- গ্রাফটি bipartite নয়
- BFS-এ ভুল হয়েছে, কারণ এমন edge থাকতে পারে না
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- BFS = উৎস থেকে স্তরে স্তরে ছড়ানো ঢেউ; যন্ত্র একটি queue, আর dist ও parent অ্যারে।
- চিহ্ন দিন push করার সময়: dist[v] == −1 হলে dist[v] = dist[u] + 1, parent[v] = u, push. প্রত্যেকে ঠিক একবার queue-তে।
- Unweighted গ্রাফে dist[v] = δ(s, v) — queue-তে dist অবনত নয় ও পার্থক্য ≤ ১, স্তর ধরে induction; parent ধরে পেছনে হেঁটে পথ।
- সময় Θ(V + E) adjacency list-এ, Θ(V²) matrix-এ; গ্রিডে O(R·C).
- রূপভেদ: multi-source (সব উৎস একসাথে), 0-1 BFS (deque), bipartite (dist mod 2), component, state space; ওজন থাকলে Dijkstra.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪১ · Depth-first search (DFS)পরবর্তী পর্বগভীরে আগে: recursion বা stack দিয়ে গ্রাফ ঘোরা।
- ডেটা স্ট্রাকচার ও অ্যালগরিদম · কোর্স হোম৭২টি পর্বপুরো কোর্সের রোডম্যাপ — নয়টি মডিউল।
- পর্ব ৩৯ · গ্রাফ — উপস্থাপন আর পরিভাষাআগের ভিত্তিAdjacency list, matrix আর গ্রাফের মৌলিক শব্দগুলো।
- পর্ব ১৪ · Queue আর deque — সারির নিয়মআগের ভিত্তিBFS-এর queue আর 0-1 BFS-এর deque কীভাবে O(1)-এ কাজ করে।
- C++ প্রোগ্রামিং · vector, deque আর listপাশের ট্র্যাক
std::queue,std::dequeআরvector-এর ব্যবহার। - Python প্রোগ্রামিং · অ্যালগরিদম ও কনটেস্টে Pythonপাশের ট্র্যাক
collections.deque, দ্রুত ইনপুট আর কনটেস্টে Python-এর কৌশল।