এই পর্বে যা শিখবেন
- বাস্তবের কোন সমস্যাগুলো আসলে গ্রাফের সমস্যা — আর directed, undirected, weighted গ্রাফের পার্থক্য
- একই গ্রাফ adjacency matrix আর adjacency list-এ কেমন দেখায়, কোনটির মেমরি কত, কখন কোনটি নেবেন
- BFS ধাপে ধাপে — queue, visited array আর দূরত্ব কীভাবে স্তরে স্তরে ভরে ওঠে
- DFS ধাপে ধাপে — recursion-এর গভীরতা, backtracking, cycle ধরা আর কোর্সের ক্রম সাজানো
- AI-এর লেখা গ্রাফ কোডের সাধারণ ভুল আর যাচাইয়ের চেকলিস্ট
১ · গ্রাফ — চারপাশের সব সংযোগের এক ভাষা
ধরুন আপনি মিরপুরে থাকেন, যাবেন মতিঝিল। Google Maps খুললেন, কয়েক মুহূর্তেই রাস্তা দেখিয়ে দিল। Facebook খুললেন, পাশে লেখা “People you may know” — এমন কয়েকজন, যাঁদের সাথে আপনার অনেক বন্ধু মিলে যায়। ল্যাপটপে একটি সফটওয়্যার ইনস্টল করলেন, সে নিজেই আরও দশটি দরকারি প্যাকেজ খুঁজে এনে ঠিক ক্রমে বসিয়ে দিল।
তিনটি একেবারে আলাদা কাজ। কিন্তু ভেতরে একই প্রশ্ন: “কে কার সাথে যুক্ত, আর এক জায়গা থেকে আরেক জায়গায় কীভাবে পৌঁছাব?” এই ধরনের প্রশ্নের জন্য কম্পিউটার বিজ্ঞানের যে কাঠামো, তার নাম graph (গ্রাফ)।
এখানে “graph” মানে স্কুলের x-y অক্ষের লেখচিত্র নয়। এখানে গ্রাফের দুটি মাত্র উপাদান:
- Vertex (বা node): জিনিসগুলো — একটি স্টেশন, একজন মানুষ, একটি ওয়েব পেজ, একটি কোর্স।
- Edge: দুটি জিনিসের মাঝের সংযোগ — দুই স্টেশনের মাঝের লাইন, দুজনের বন্ধুত্ব, এক পেজ থেকে আরেক পেজের লিংক।
| বাস্তবের জিনিস | Vertex | Edge | যে প্রশ্নের উত্তর চাই |
|---|---|---|---|
| ঢাকা মেট্রো ও বাস রুট | স্টেশন / স্টপেজ | পাশাপাশি দুই স্টেশনের মাঝের লাইন | সবচেয়ে কম স্টেশন পেরিয়ে কীভাবে যাব? |
| দেশের সড়ক | জেলা শহর | দুই শহরের মাঝের মহাসড়ক (দূরত্বসহ) | রাজশাহী থেকে সিলেট সবচেয়ে কম কিলোমিটারে কোন পথে? |
| মানুষ | বন্ধুত্ব | কাকে “বন্ধু হতে পারেন” দেখাব? | |
| ওয়েব | ওয়েব পেজ | এক পেজ থেকে আরেক পেজের লিংক | কোন পেজ বেশি গুরুত্বপূর্ণ? কোন পেজ এখনো দেখা হয়নি? |
| শেখার কোর্স | কোর্স | “এটি পড়ার আগে ওটি শেষ করতে হবে” | কোন কোর্সটি কখন পড়া শুরু করা যাবে? |
একটি সুতা আর কিছু পুঁতির কথা ভাবুন। পুঁতিগুলো vertex, সুতার টুকরোগুলো edge. মালার মতো একটি সারিতে গাঁথলে সেটি একটি linked list (পর্ব ৩৩)। এক পুঁতি থেকে কয়েকটি শাখা বের করলে, আর শাখাগুলো কখনো একে অপরের সাথে না মিললে সেটি tree (পর্ব ৩৫)। কিন্তু যদি যেকোনো পুঁতিকে যেকোনো পুঁতির সাথে বাঁধার স্বাধীনতা থাকে, আর সুতা ঘুরে আবার আগের পুঁতিতে ফিরতে পারে — সেটিই গ্রাফ। List আর tree আসলে গ্রাফেরই বিশেষ, নিয়মে বাঁধা রূপ।
গ্রাফ তত্ত্বের জন্ম একটি ধাঁধা থেকে। প্রুশিয়ার Königsberg শহরে (আজকের রাশিয়ার Kaliningrad) নদীর মাঝে দুটি দ্বীপ, আর ডাঙা ও দ্বীপগুলো জুড়ে ছিল সাতটি সেতু। শহরের মানুষ ভাবতেন — প্রতিটি সেতু ঠিক একবার পার হয়ে কি পুরো শহর ঘোরা যায়? ১৭৩৬ সালে সুইস গণিতবিদ Leonhard Euler দেখালেন, এটি অসম্ভব। তাঁর বড় চিন্তাটি ছিল এই: সেতুর দৈর্ঘ্য বা দ্বীপের আকার একেবারেই অপ্রাসঙ্গিক, গুরুত্বপূর্ণ শুধু কোন জায়গা কোন জায়গার সাথে যুক্ত। এই চিন্তাই গ্রাফ — আর ঠিক এজন্যই একই কাঠামো দিয়ে মেট্রো লাইন আর Facebook বন্ধুত্ব দুটোই বোঝানো যায়।
২ · গ্রাফের রকমফের — দিক, ওজন আর ঘুরে আসা পথ
সব গ্রাফ এক রকম নয়। কোড লেখার আগে তিনটি প্রশ্নের উত্তর জেনে নিতে হয়, কারণ এর ওপর নির্ভর করে কোন algorithm কাজ করবে।
↔️ দিকহীন (Undirected)
সংযোগ দুই দিকেই খাটে। আপনি রাফির Facebook বন্ধু হলে রাফিও আপনার বন্ধু। মেট্রোর লাইনও তাই — আগারগাঁও থেকে ফার্মগেট গেলে ফিরেও আসা যায়। একে বলে undirected graph.
বন্ধুত্ব, দুইমুখী রাস্তা➡️ দিকওয়ালা (Directed)
প্রতিটি সংযোগের একটি দিক আছে। আপনি কোনো ক্রিকেটারকে X-এ follow করলে তিনি আপনাকে follow করেন না। পুরান ঢাকার একমুখী গলিও তাই। একে বলে directed graph.
follow, লিংক, prerequisite⚖️ ওজনওয়ালা (Weighted)
প্রতিটি edge-এর সাথে একটি সংখ্যা — দূরত্ব, ভাড়া বা সময়। ঢাকা-চট্টগ্রাম মহাসড়কের পাশে লেখা থাকে কত কিলোমিটার। একে বলে weighted graph.
কিলোমিটার, টাকা, মিনিটআরও দুটি শব্দ এই পর্বে বারবার আসবে। একটি vertex-এর সাথে কয়টি edge যুক্ত, তাকে বলে তার degree. আর কয়েকটি edge পেরিয়ে যদি আবার শুরুর vertex-এ ফিরে আসা যায়, সেই ঘুরে আসা পথটিকে বলে cycle.
Tree-তে কোনো cycle থাকে না — তাই tree ঘোরার সময় (পর্ব ৩৫) “এই node আগে দেখেছি কি না” ভাবতেই হয়নি। গ্রাফে cycle থাকা স্বাভাবিক। মিরপুর → আগারগাঁও → ফার্মগেট → গুলশান → মহাখালী → মিরপুর — এভাবে ঘুরে আসা যায়। এজন্য গ্রাফের প্রতিটি algorithm-এর সাথে একটি visited array থাকে। পুরো পর্বের সবচেয়ে গুরুত্বপূর্ণ বাক্য এটিই।
এই পর্বের সব উদাহরণে আমরা একটি কাল্পনিক ছোট রুট-মানচিত্র ব্যবহার করব — ঢাকার সাতটি পরিচিত এলাকা, আর তাদের মাঝে আটটি সরাসরি বাস/মেট্রো সংযোগ। (বাস্তবের রুট এমন নয়; শুধু বোঝার সুবিধার জন্য বানানো।) প্রতিটি এলাকার একটি নম্বর আছে, কারণ C-তে আমরা vertex-কে নম্বর দিয়েই চিনব:
[0] Mirpur ---- [1] Agargaon ---- [3] Farmgate ---- [5] Shahbag
| | |
| | |
[2] Mohakhali ------------------- [4] Gulshan ----- [6] Motijheel
এখানে V (vertex-এর সংখ্যা) = ৭, আর E (edge-এর সংখ্যা) = ৮। গ্রাফটি undirected আর ওজনহীন — প্রতিটি সংযোগ মানে “এক স্টপেজ”। খেয়াল করুন, এতে অন্তত দুটি ছোট cycle আছে।
৩ · C-তে গ্রাফ রাখার দুই উপায় — matrix আর list
কাগজে গ্রাফ আঁকা সহজ। কিন্তু কম্পিউটারের মেমরিতে তো ছবি রাখা যায় না — রাখতে হবে সংখ্যা। প্রশ্ন একটাই: “কে কার সাথে যুক্ত” — এই তথ্যটি কোন আকারে রাখব? দুটি জনপ্রিয় উত্তর আছে, আর দুটিই আপনি আগের পর্বগুলোতে শেখা জিনিস দিয়ে বানাতে পারবেন।
- Adjacency matrix: V × V মাপের একটি 2D array (পর্ব ২১)।
adj[i][j] = 1মানে i আর j যুক্ত, 0 মানে নয়। ঠিক যেন বাসের ভাড়ার চার্ট — বাঁ দিকে “কোথা থেকে”, ওপরে “কোথায়”। - Adjacency list: প্রতিটি vertex-এর জন্য শুধু তার প্রতিবেশীদের একটি তালিকা — linked list (পর্ব ৩৩) বা dynamic array (পর্ব ২০) দিয়ে। ঠিক যেন মোবাইলের contact list — শুধু যাঁদের চেনেন, তাঁদের নাম।
নিচের ডায়াগ্রামে একই রুট-মানচিত্র দুই আকারে দেখুন, আর ডান পাশে মেমরির হিসাব খেয়াল করুন:
[0] Mirpur -- [1] Agargaon -- [3] Farmgate -- [5] Shahbag | | | [2] Mohakhali --------------- [4] Gulshan --- [6] Motijheel
0 1 2 3 4 5 6 0: 0 1 1 0 0 0 0 Mirpur 1: 1 0 0 1 0 0 0 Agargaon 2: 1 0 0 0 1 0 0 Mohakhali 3: 0 1 0 0 1 1 0 Farmgate 4: 0 0 1 1 0 0 1 Gulshan 5: 0 0 0 1 0 0 1 Shahbag 6: 0 0 0 0 1 1 0 Motijheel
0 1 2 3 4 5 6 3: 0 1 0 0 1 1 0 Farmgate ↑ adj[3][5] == 1 → হ্যাঁ, এক ধাপেই উত্তর
0 Mirpur → 1 → 2
1 Agargaon → 0 → 3
2 Mohakhali → 0 → 4
3 Farmgate → 1 → 4 → 5
4 Gulshan → 2 → 3 → 6
5 Shahbag → 3 → 6
6 Motijheel → 4 → 5
Matrix : 100000 x 100000 = 10,000,000,000 ঘর
1 byte করে ধরলেও ≈ 10 GB (প্রায় সবই 0)
List : 100000 head (8 byte) + 400000 entry (16 byte)
মোট ≈ 7 MB
| Matrix-এর ঘর | |
| যার মধ্যে 1 | |
| List-এর entry | |
| প্রশ্নের খরচ |
- শুরুর ছবি। সাতটি এলাকা, আটটি সংযোগ। Vertex-এর নাম না রেখে নম্বর (0–6) রাখছি, যাতে এগুলো সরাসরি array-র index হিসেবে কাজ করে। নামগুলো আলাদা একটি string array-তে থাকবে।
- Adjacency matrix. ৭ × ৭ = ৪৯টি ঘর। Mirpur (সারি 0) যুক্ত Agargaon (1) আর Mohakhali (2)-এর সাথে, তাই সারি 0-এর কলাম 1 আর 2-তে 1. গ্রাফটি undirected, তাই প্রতিটি edge দুইবার লেখা —
adj[0][1]আরadj[1][0]দুটোই 1. ৮টি edge, তাই মোট ১৬টি 1, বাকি ৩৩টি ঘর শুধু 0 রাখছে। - Matrix-এর শক্তি — এক ধাপে উত্তর। “Farmgate (3) আর Shahbag (5) কি সরাসরি যুক্ত?” — শুধু
adj[3][5]দেখলেই হলো। গ্রাফ যত বড়ই হোক, খরচ একটিমাত্র ঘর দেখা, অর্থাৎ O(1). - Adjacency list. প্রতিটি vertex-এর পাশে শুধু তার আসল প্রতিবেশীরা। মোট entry ১৬টি (আবারও প্রতিটি edge দুইবার), সাথে ৭টি তালিকার মাথা। একটিও 0 রাখতে হয় না — যা নেই, তা লেখাই হয় না। মেমরি মোটামুটি V + E-এর সমানুপাতিক।
- List-এর শক্তি — প্রতিবেশী ঘোরা। “Farmgate-এর সব প্রতিবেশী কারা?” List-এ ঠিক ৩টি entry হাঁটলেই শেষ। Matrix-এ পুরো সারি 3-এর ৭টি ঘরই দেখতে হয়, যার ৪টিই অকাজের 0. BFS ও DFS ঠিক এই প্রশ্নটিই বারবার করে, তাই এখানে list এগিয়ে।
- শহর বড় হলে। ধরুন একটি বড় শহরের সড়ক-মানচিত্রে ১ লাখ মোড়, প্রতিটি মোড়ে গড়ে ৪টি রাস্তা। Matrix লাগবে ১,০০০ কোটি ঘর — প্রতিটি ঘর ১ byte ধরলেও প্রায় ১০ GB, যার প্রায় সবই 0. List-এ লাগবে কয়েক MB মাত্র। বাস্তবের রাস্তা, সামাজিক নেটওয়ার্ক বা ওয়েব — সবই এমন “পাতলা” গ্রাফ।
কোনটি কখন নেবেন, তার উত্তর নির্ভর করে গ্রাফটি কতটা “ঘন”। যে গ্রাফে প্রায় সবাই সবার সাথে যুক্ত, তাকে বলে dense graph; আর যেখানে প্রত্যেকে মাত্র অল্প কয়েকজনের সাথে যুক্ত, তাকে বলে sparse graph.
| বিষয় | Adjacency matrix | Adjacency list |
|---|---|---|
| মেমরি | V² ঘর — edge কম হলেও | মোটামুটি V + E |
| “i আর j কি যুক্ত?” | এক ধাপে, O(1) | i-এর তালিকা হাঁটতে হয় |
| “i-এর সব প্রতিবেশী” | পুরো সারি, V ধাপ | শুধু প্রতিবেশী কয়জন, তত ধাপ |
| C-তে বানানো | খুব সহজ — int adj[V][V] | malloc, pointer, free লাগে |
| কখন মানায় | ছোট বা dense গ্রাফ | বড় ও sparse গ্রাফ — বাস্তবের প্রায় সব |
C-তে adjacency list-এর একটি সাধারণ রূপ দেখতে এমন — পর্ব ৩৩-এর linked list-এর node, শুধু ভেতরে থাকে প্রতিবেশীর নম্বর:
struct Edge {
int to; /* প্রতিবেশী vertex-এর নম্বর */
struct Edge *next; /* এই vertex-এর পরের প্রতিবেশী */
};
struct Edge *head[V]; /* প্রতিটি vertex-এর তালিকার মাথা, শুরুতে NULL */
এই পর্বের runnable কোডে আমরা matrix ব্যবহার করব — শুধু এজন্য যে মাত্র ৭টি vertex-এ এটি সবচেয়ে কম লাইনে লেখা যায়, আর algorithm-এর মূল ভাবনা চোখে পড়ে। কিন্তু মনে রাখবেন: BFS বা DFS-এর ভেতরের যুক্তি দুই রূপেই হুবহু এক। বদলায় শুধু “প্রতিবেশীদের ঘোরো” লাইনটি — matrix-এ পুরো সারি, list-এ শুধু তালিকা।
৪ · BFS — পুকুরে ঢিল ছোড়ার মতো, স্তরে স্তরে খোঁজা
শান্ত পুকুরে একটি ঢিল ফেলুন। প্রথমে ছোট একটি বৃত্ত, তারপর তার বাইরে আরেকটি, তারপর আরও বড়। ঢেউ কখনো একদিকে লাফিয়ে দূরে চলে যায় না — সব দিকে সমানভাবে, স্তরে স্তরে ছড়ায়। BFS (Breadth-First Search) ঠিক এভাবে গ্রাফ খোঁজে: আগে শুরুর জায়গা থেকে ১ ধাপ দূরের সবাই, তারপর ২ ধাপ দূরের সবাই, তারপর ৩ ধাপ।
এই “আগে এলে আগে” ক্রম রাখার জন্য দরকার একটি queue (পর্ব ৩৪) — বাসস্ট্যান্ডের লাইনের মতো, যে আগে দাঁড়িয়েছে সে আগে উঠবে। BFS-এর পুরো নিয়ম মাত্র চার লাইনে:
Queue খালি না হওয়া পর্যন্ত: সামনে থেকে একজনকে বের করো,
তার যে প্রতিবেশী এখনো visited নয় — তাকে চিহ্ন দাও, দূরত্ব লেখো, queue-র পেছনে রাখো।
এবার Mirpur থেকে BFS চালাই। নিচে প্রতিটি এলাকার পাশে তার দূরত্ব (কয় স্টপেজ) লেখা আছে। ঝাপসা এলাকাগুলো এখনো খুঁজে পাওয়া যায়নি; লাল দাগের এলাকাটি এই ধাপে queue থেকে বের হয়েছে:
[0] Mirpur -- [1] Agargaon -- [3] Farmgate -- [5] Shahbag
| | |
[2] Mohakhali --------------- [4] Gulshan --- [6] Motijheel
| বের করা হলো | |
| queue (সামনে → পেছনে) | |
| visited |
- শুরু। Mirpur-কে visited চিহ্ন দিলাম, দূরত্ব 0, আর queue-তে রাখলাম। বাকি সবার দূরত্ব এখনো অজানা (
?)। খেয়াল করুন: চিহ্ন দেওয়া হচ্ছে queue-তে ঢোকানোর সময়, বের করার সময় নয় — কেন, তা ধাপ ৫-এ দেখবেন। - Mirpur বের হলো। তার প্রতিবেশী Agargaon আর Mohakhali — দুজনেই নতুন। দুজনকে চিহ্ন দিলাম, দূরত্ব 0 + 1 = ১, queue-র পেছনে রাখলাম। এটি প্রথম “ঢেউ”: ১ স্টপেজ দূরের সবাই।
- Agargaon বের হলো। প্রতিবেশী Mirpur (আগেই visited — বাদ) আর Farmgate (নতুন)। Farmgate-এর দূরত্ব = Agargaon-এর দূরত্ব + 1 = ২। সে queue-র একদম পেছনে, Mohakhali-র পরে।
- Mohakhali বের হলো। Mirpur বাদ, Gulshan নতুন — দূরত্ব ২। এখন queue-তে শুধু দূরত্ব ২-এর এলাকা। প্রথম স্তর পুরো শেষ, দ্বিতীয় স্তর শুরু। BFS কখনো এক স্তর শেষ না করে পরের স্তরে যায় না।
- Farmgate বের হলো — visited-এর আসল কাজ। প্রতিবেশী Agargaon (visited), Gulshan (visited!), আর Shahbag (নতুন, দূরত্ব ৩)। Gulshan এখনো queue-তে বসে আছে, কিন্তু তাকে ধাপ ৪-এই চিহ্ন দেওয়া হয়েছিল — তাই দ্বিতীয়বার queue-তে ঢুকল না। চিহ্ন যদি বের করার সময় দিতাম, Gulshan দুইবার queue-তে থাকত। বড় গ্রাফে এমন ডুপ্লিকেট কাজ বহুগুণ বেড়ে যায়।
- Gulshan বের হলো। Mohakhali আর Farmgate visited; Motijheel নতুন — দূরত্ব ২ + 1 = ৩। সাতটি এলাকাই এখন খুঁজে পাওয়া গেছে।
- Shahbag বের হলো। তার দুই প্রতিবেশী Farmgate আর Motijheel — দুজনেই visited. কিছুই যোগ হলো না। Visited না থাকলে এখানে Farmgate আবার queue-তে ঢুকত, সেখান থেকে Agargaon, তারপর Mirpur… cycle-এর কারণে খোঁজা কখনো থামত না।
- Motijheel বের হলো, queue খালি — কাজ শেষ। প্রতিটি এলাকার দূরত্বই এখন সবচেয়ে কম স্টপেজের সংখ্যা। Mirpur থেকে Motijheel ৩ স্টপেজ: Motijheel-কে Gulshan খুঁজে পেয়েছিল, Gulshan-কে Mohakhali, Mohakhali-কে Mirpur — উল্টো দিকে পড়লে পথ Mirpur → Mohakhali → Gulshan → Motijheel.
এবার এই পুরো কাজটি C-তে। কোডে আলাদা visited array রাখিনি — dist[v] == -1 মানেই “এখনো খুঁজে পাওয়া যায়নি”। আর queue বানানো হয়েছে একটি সাধারণ array আর দুটি index (head, tail) দিয়ে, যেমনটি পর্ব ৩৪-এ দেখেছেন:
#include <stdio.h>
#define N 7
const char *name[N] = {"Mirpur", "Agargaon", "Mohakhali", "Farmgate", "Gulshan", "Shahbag", "Motijheel"};
int adj[N][N];
int main(void) {
int e[8][2] = {{0,1}, {0,2}, {1,3}, {2,4}, {3,4}, {3,5}, {4,6}, {5,6}};
for (int i = 0; i < 8; i++) adj[e[i][0]][e[i][1]] = adj[e[i][1]][e[i][0]] = 1;
int dist[N], queue[N], head = 0, tail = 0;
for (int i = 0; i < N; i++) dist[i] = -1; /* -1 = not visited yet */
dist[0] = 0; queue[tail++] = 0; /* start at Mirpur */
while (head < tail) {
int u = queue[head++]; /* take from the front */
for (int v = 0; v < N; v++)
if (adj[u][v] && dist[v] == -1) { dist[v] = dist[u] + 1; queue[tail++] = v; }
}
for (int i = 0; i < N; i++) printf("%-10s %d stops\n", name[i], dist[i]);
return 0;
}প্রথম for লুপ edge-এর তালিকা থেকে matrix বানায় — প্রতিটি edge দুই দিকে লেখা হয়, কারণ গ্রাফ undirected. while লুপটিই BFS: queue[head++] সামনে থেকে বের করে, queue[tail++] = v পেছনে ঢোকায়। Queue-র array-র মাপ মাত্র N যথেষ্ট, কারণ visited চিহ্নের জন্য প্রতিটি vertex জীবনে একবারই ঢোকে। আউটপুটে দেখবেন Agargaon ও Mohakhali ১, Farmgate ও Gulshan ২, Shahbag ও Motijheel ৩ — ঠিক ওপরের ডায়াগ্রামের মতো। dist[v] == -1 শর্তটি মুছে চালালে কী হবে, ভেবে দেখুন: একই vertex বারবার queue-তে ঢুকবে, আর queue-র array তার সীমা ছাড়িয়ে লেখা শুরু করবে — একটি buffer overflow.
৫ · কেন BFS সবচেয়ে কম ধাপের পথ দেয় — আর কখন দেয় না
ওপরের ফলাফলটি কাকতালীয় নয়। BFS যেকোনো ওজনহীন গ্রাফে নিশ্চিতভাবে shortest path — সবচেয়ে কম edge-এর পথ — দেয়। যুক্তিটি সহজ:
- Queue-তে সবসময় আগে দূরত্ব d-এর সবাই, তারপর দূরত্ব d + 1-এর সবাই — কখনো উল্টো নয়।
- তাই একটি vertex-কে প্রথমবার যে স্তরে পাওয়া যায়, তার চেয়ে কাছের কোনো স্তরে সে থাকতে পারে না — থাকলে আগের ঢেউয়েই ধরা পড়ত।
- আর visited চিহ্ন থাকায় পরে কেউ তার দূরত্ব বাড়িয়ে নষ্ট করতে পারে না।
শুধু দূরত্ব নয়, পুরো পথটি পেতে চাইলে আরেকটি ছোট array রাখুন — parent array. নতুন vertex v-কে যখন u থেকে পাওয়া যায়, লিখে রাখুন parent[v] = u. শেষে গন্তব্য থেকে parent ধরে ধরে উল্টো হাঁটলে শুরুতে পৌঁছাবেন — ঠিক যেমন ধাপ ৮-এ আমরা Motijheel থেকে Mirpur-এ ফিরেছিলাম।
“Six degrees of separation” কথাটি শুনেছেন? ধারণাটি হলো — পৃথিবীর যেকোনো দুজন মানুষ মোটামুটি ছয়জন পরিচিতের শিকল দিয়ে যুক্ত। “আপনি আর অমুক তারকার মধ্যে কয়জন মানুষের দূরত্ব?” — এটি ঠিক BFS-এর প্রশ্ন। আপনি স্তর ০, আপনার বন্ধুরা স্তর ১, বন্ধুর বন্ধুরা স্তর ২…
ওজন এলেই BFS ভুল উত্তর দেয়। ধরুন Mirpur থেকে Motijheel সরাসরি একটি বাস আছে, কিন্তু সেটি ৯০ মিনিট নেয়; আর তিনটি মেট্রো স্টপেজ মিলিয়ে লাগে ৩০ মিনিট। BFS বলবে “১ ধাপের সরাসরি বাসই সেরা”, কারণ সে শুধু ধাপ গোনে, মিনিট নয়। যেখানে edge-এর ওজন (দূরত্ব, ভাড়া, সময়) আলাদা আলাদা, সেখানে লাগে Dijkstra's algorithm.
ডাচ কম্পিউটার বিজ্ঞানী Edsger W. Dijkstra ১৯৫৬ সালে আমস্টারডামে এই পদ্ধতিটি ভাবেন। পরে তিনি বলেছিলেন, একটি ক্যাফেতে বসে কাগজ-কলম ছাড়াই মাথায় মাথায় প্রায় বিশ মিনিটে এটি দাঁড় করিয়েছিলেন। প্রকাশ করেন ১৯৫৯ সালে, মাত্র তিন পাতার একটি লেখায়। ভাবনাটি BFS-এরই বড় ভাই: queue-র বদলে এমন একটি তালিকা, যেখান থেকে সবসময় “এখন পর্যন্ত সবচেয়ে কাছের” vertex-টি বের হয় — পর্ব ৩৬-এর priority queue. আজকের ম্যাপ অ্যাপ, ইন্টারনেটের রাউটিং প্রোটোকল (যেমন OSPF) — সবকিছুর ভিত্তিতে এই ধারণা। বিস্তারিত হিসাব ও প্রমাণ দেখবেন DSA কোর্সে।
৬ · DFS — গোলকধাঁধায় এক দেয়াল ধরে গভীরে যাওয়া
এবার অন্য একটি কৌশল। ধরুন আপনি একটি গোলকধাঁধার ভেতরে, হাতে একটি চক। প্রতিটি মোড়ে চক দিয়ে দাগ দেন (visited). একটি পথ ধরে যতদূর যাওয়া যায় এগিয়ে যান। সামনে দেয়াল, বা সব রাস্তাই দাগ দেওয়া? তাহলে শেষ যে মোড়ে একটি না-দেখা রাস্তা ছিল, সেখানে ফিরে আসুন, আর সেই রাস্তা ধরুন। একে বলে backtracking, আর এই পুরো কৌশলটির নাম DFS (Depth-First Search).
BFS-এর “আগে এলে আগে” ক্রমের জন্য লাগত queue. DFS-এর “সবশেষে যেখানে এসেছি, সেখান থেকেই আবার শুরু” ক্রমের জন্য লাগে উল্টোটি — stack (পর্ব ৩৪)। আর recursion-এ (পর্ব ১৫) এই stack আমাদের ফ্রি দেওয়া থাকে: প্রতিটি ফাংশন কল call stack-এ একটি নতুন frame রাখে, আর ফাংশন শেষ হলে আগের জায়গায় ফিরে যায়। তাই recursive DFS মাত্র কয়েক লাইনের:
void dfs(int u) {
visited[u] = 1; /* চক দিয়ে দাগ */
for (int v = 0; v < N; v++)
if (adj[u][v] && !visited[v])
dfs(v); /* গভীরে যাও; ফিরে এলে পরের রাস্তা */
} /* সব রাস্তা শেষ → ফেরত (backtrack) */
একই রুট-মানচিত্রে Mirpur থেকে DFS চালাই। প্রতিবেশীদের দেখা হবে ছোট নম্বর থেকে বড় নম্বরের ক্রমে। ডান পাশে call stack কতটা লম্বা হচ্ছে, খেয়াল করুন:
[0] Mirpur -- [1] Agargaon -- [3] Farmgate -- [5] Shahbag
| | |
[2] Mohakhali --------------- [4] Gulshan --- [6] Motijheel
| stack | |
| গভীরতা | |
| দেখার ক্রম |
dfs(Mirpur)— দাগ দিলাম। প্রতিবেশী Agargaon (1) আর Mohakhali (2). ছোট নম্বর আগে, তাই Agargaon-এ যাচ্ছি। Mohakhali “পরে দেখব” হিসেবে Mirpur-এর frame-এর ভেতরে অপেক্ষায় থাকল।dfs(Agargaon)— Mirpur আগেই দাগ দেওয়া, তাই পরের প্রতিবেশী Farmgate. BFS হলে এখন Mohakhali-র পালা আসত; DFS কিন্তু পাশে না তাকিয়ে সামনে এগোচ্ছে।dfs(Farmgate)— প্রতিবেশী Agargaon (দাগ আছে), Gulshan (নতুন), Shahbag (নতুন)। Gulshan-এর নম্বর ছোট, তাই আগে সেদিকে। Stack-এর গভীরতা এখন ৩।dfs(Gulshan)— প্রতিবেশী Mohakhali (2), Farmgate (3), Motijheel (6). Mohakhali নতুন, তাই সেদিকে।dfs(Mohakhali)— কানাগলি। প্রতিবেশী Mirpur আর Gulshan, দুজনেরই দাগ আছে। খেয়াল করুন: Mirpur দাগওয়ালা, অথচ সে Mohakhali-র “বাবা” (যেখান থেকে এসেছি) নয় — মানে আমরা একটি cycle খুঁজে পেয়েছি: Mirpur → Agargaon → Farmgate → Gulshan → Mohakhali → Mirpur. আর একটি মজার জিনিস: BFS-এ Mohakhali ছিল মাত্র ১ ধাপ দূরে, DFS তাকে পেল ৪ ধাপ ঘুরে। DFS-এর পথ সবচেয়ে ছোট পথ নয়।- Backtrack — Gulshan-এ ফেরা।
dfs(Mohakhali)শেষ, তার frame stack থেকে সরে গেল। আমরা ফিরে এলাম Gulshan-এরforলুপের ঠিক সেই জায়গায়, যেখানে থেমেছিলাম। Farmgate দাগওয়ালা, পরের প্রতিবেশী Motijheel — নতুন। dfs(Motijheel)— প্রতিবেশী Gulshan (দাগ আছে) আর Shahbag (নতুন)। আবার গভীরে।dfs(Shahbag)— সবচেয়ে গভীরে, গভীরতা ৬। প্রতিবেশী Farmgate আর Motijheel — দুজনেরই দাগ। সাতটি এলাকাই দেখা শেষ। ৭টি vertex-এর গ্রাফেই stack ৬ তলা উঁচু হলো; ১০ লাখ vertex-এর একটি লম্বা শিকলে stack-ও ১০ লাখ তলা হতে পারে — এটি মনে রাখুন।- সবাই ফিরে যায়। Shahbag, Motijheel, Gulshan, Farmgate, Agargaon — একে একে প্রত্যেকের লুপে আর নতুন কেউ নেই, তাই একে একে ফেরত। শেষে Mirpur-এর লুপে Mohakhali-র পালা এলো, কিন্তু সে আগেই দাগওয়ালা। Stack খালি, DFS শেষ। দেখার ক্রম: Mirpur, Agargaon, Farmgate, Gulshan, Mohakhali, Motijheel, Shahbag.
DFS কোথায় কাজে লাগে
🧩 গোলকধাঁধা ও ধাঁধা
Sudoku, দাবার কিছু ধাঁধা বা গোলকধাঁধা — “একটি পথ চেষ্টা করো, না মিললে ফিরে এসে অন্যটি” — সবই DFS-ধাঁচের backtracking.
🏝️ বিচ্ছিন্ন অংশ খোঁজা
বন্যায় কিছু রাস্তা ডুবে গেলে কোন এলাকাগুলো একে অপরের থেকে বিচ্ছিন্ন? প্রতিটি না-দেখা vertex থেকে একবার DFS চালান — যতবার চালাতে হলো, ততগুলো connected component.
🔁 Cycle ধরা
DFS চলার সময় যদি এমন কাউকে পাওয়া যায় যে এখনো stack-এ “চলমান” — মানে আমরা ঘুরে নিজের পথেই ফিরে এসেছি। পরের অংশে এর কোড দেখবেন।
📚 কাজের ক্রম সাজানো
কোন কোর্স আগে, কোন প্যাকেজ আগে ইনস্টল — এই প্রশ্নের উত্তরও DFS (বা BFS-এর একটি রূপ) দিয়ে বের হয়।
Recursion-এর গভীরতা একটি বাস্তব সীমা। প্রতিটি recursive কল stack-এ কিছু জায়গা নেয়, আর stack-এর মাপ সীমিত — Linux-এ সাধারণত ৮ MB, Windows-এ সাধারণত ১ MB. লাখ লাখ vertex-এর একটি লম্বা পথে recursive DFS চালালে প্রোগ্রাম stack overflow-এ crash করতে পারে (পর্ব ১৫)। সমাধান: recursion বাদ দিয়ে নিজের একটি stack (heap-এ malloc করা array) দিয়ে DFS লেখা। যুক্তি একই, শুধু stack-টি এখন আপনার নিয়ন্ত্রণে।
৭ · DFS দিয়ে cycle ধরা, আর কোর্সের সঠিক ক্রম সাজানো
আপনার কম্পিউটারে apt, npm বা pip দিয়ে কিছু ইনস্টল করলে package manager প্রথমে একটি directed গ্রাফ বানায়: “app-এর লাগবে web আর log; web-এর লাগবে json…”। তারপর ঠিক করে কোনটি আগে বসাতে হবে। কিন্তু যদি json-এর লাগে util, আর util-এর লাগে web, আর web-এর লাগে json? তাহলে কেউই আগে বসতে পারবে না — একটি cycle. ভালো package manager বা build system তখন ইনস্টল শুরুই করে না, স্পষ্ট জানিয়ে দেয় কোথায় চক্র।
Directed গ্রাফে cycle ধরতে DFS-এর একটি ছোট উন্নতি লাগে। শুধু “দেখেছি/দেখিনি” নয়, তিনটি অবস্থা রাখতে হয়: 0 = এখনো দেখিনি, 1 = এখন এর ভেতরে আছি (stack-এ চলছে), 2 = পুরো শেষ। DFS চলতে চলতে যদি এমন কাউকে পাই যার অবস্থা 1 — মানে নিজের চলমান পথেই ফিরে এসেছি, অর্থাৎ cycle:
#include <stdio.h>
#define N 5
const char *pkg[N] = {"app", "web", "json", "log", "util"};
int dep[N][N], state[N]; /* 0 = new, 1 = on current path, 2 = done */
int dfs(int u, int depth) {
printf("%*s%s\n", depth * 2, "", pkg[u]);
state[u] = 1;
for (int v = 0; v < N; v++) {
if (!dep[u][v]) continue;
if (state[v] == 1) { printf("CYCLE: %s -> %s\n", pkg[u], pkg[v]); return 1; }
if (state[v] == 0 && dfs(v, depth + 1)) return 1;
}
state[u] = 2;
return 0;
}
int main(void) {
dep[0][1] = dep[0][3] = dep[1][2] = dep[2][4] = dep[3][4] = dep[4][1] = 1;
if (!dfs(0, 0)) printf("No cycle - install order is safe\n");
return 0;
}dep[u][v] = 1 মানে “u-এর জন্য v লাগবে” — একটি directed edge. শেষ সংযোগ dep[4][1] (util-এর লাগে web) ইচ্ছে করে বসানো হয়েছে। আউটপুটে প্রতিটি নাম তার গভীরতা অনুযায়ী সরে সরে ছাপা হয় (%*s দিয়ে depth * 2টি space): app → web → json → util, আর util থেকে web-এ যেতেই দেখা গেল web-এর অবস্থা 1 — সে এখনো চলমান পথে। তাই ছাপা হয় CYCLE: util -> web. কেন শুধু “দেখেছি/দেখিনি” দিয়ে হতো না? ধরুন dep[4][1] নেই। তখন app → web → json → util শেষ হওয়ার পর app → log → util পথে util-কে আবার পাওয়া যাবে। এটি cycle নয় — util শুধু আগেই পুরো শেষ (অবস্থা 2). দুই অবস্থার হিসাবে এখানে ভুল করে “cycle” বলা হতো। main-এর প্রথম লাইন থেকে dep[4][1] = অংশটি মুছে চালালে দেখবেন “No cycle”.
Topological sort — কোন কোর্স আগে পড়বেন
Cycle না থাকলে একটি সুন্দর জিনিস করা যায়: সব vertex-কে এমন ক্রমে সাজানো, যাতে প্রতিটি “আগে লাগবে” সম্পর্ক মানা হয়। একে বলে topological sort. নিজে নিজে প্রোগ্রামিং শেখার একটি ছোট পরিকল্পনা ধরি — কোন কোর্সের আগে কোনটা পড়া দরকার:
- DSA (Data Structures) পড়ার আগে লাগবে C (প্রোগ্রামিং).
- Algo (Algorithms) পড়ার আগে লাগবে DSA আর Math (Discrete Math) — দুটোই।
- AI পড়ার আগে লাগবে Algo.
সবচেয়ে সহজ পদ্ধতিটি এমন: প্রতিটি কোর্সের in-degree গুনুন — অর্থাৎ তার আগে কয়টি কোর্স বাকি। যার বাকি শূন্য, সেটি এখনই নেওয়া যায়। নিলে, তার ওপর নির্ভরশীল কোর্সগুলোর বাকি একটি করে কমে যায়। (এটি Kahn-এর পদ্ধতি নামে পরিচিত, আর ভেতরে একটি queue ব্যবহার করে — BFS-এর আত্মীয়। DFS দিয়েও করা যায়: DFS শেষ হওয়ার ক্রমটি উল্টে দিলেই topological ক্রম।)
C ------> DSA ------> Algo ------> AI
^
Math -----------------+
| এখন নেওয়া যায় | |
| চূড়ান্ত ক্রম |
- বাকি গোনা। C আর Math-এর আগে কিছু লাগে না (বাকি ০)। DSA-এর বাকি ১, Algo-এর বাকি ২, AI-এর বাকি ১। শুরুতে পড়া যায় শুধু বাকি-শূন্য দুটি কোর্স।
- C নেওয়া হলো। এর ওপর নির্ভর করে DSA — তার বাকি ১ থেকে ০ হলো, তাই সে এখন “নেওয়া যায়” তালিকায় ঢুকল।
- Math নেওয়া হলো। Algo-এর বাকি ২ থেকে ১। এখনো DSA বাকি, তাই Algo অপেক্ষায়।
- DSA নেওয়া হলো। Algo-এর বাকি এবার ০ — এখন নেওয়া যায়।
- Algo নেওয়া হলো। AI-এর বাকি ০।
- AI নেওয়া হলো — সব শেষ। চূড়ান্ত ক্রমে প্রতিটি কোর্স তার সব prerequisite-এর পরে এসেছে। যদি কখনো তালিকা খালি হয়ে যায় অথচ কিছু কোর্স রয়ে যায় — মানে তাদের বাকি কখনো শূন্য হয়নি — তাহলে গ্রাফে নিশ্চয়ই একটি cycle আছে। ঠিক এভাবেই
make-এর মতো build system (পর্ব ২৯) বোঝে কোন ফাইল আগে বানাতে হবে।
৮ · গতি, তুলনা আর বাস্তবের গ্রাফ
BFS আর DFS দুটোরই খরচ O(V + E) (adjacency list ব্যবহার করলে)। কারণটি গুনে দেখা যায়: visited চিহ্নের জন্য প্রতিটি vertex একবারই queue/stack-এ ঢোকে — মোট V বার; আর প্রতিটি vertex-এর প্রতিবেশী-তালিকা একবারই হাঁটা হয় — সব তালিকা মিলিয়ে মোট entry মোটামুটি 2E. Matrix ব্যবহার করলে প্রতিবেশী খুঁজতে প্রতিবার পুরো সারি দেখতে হয়, তাই খরচ হয় O(V²)। এই “গুনে দেখা”র আনুষ্ঠানিক রূপ — time complexity আর Big-O — নিয়েই পরের পর্ব।
| বিষয় | BFS | DFS |
|---|---|---|
| ক্রম রাখে | Queue (আগে এলে আগে) | Stack বা recursion (শেষে এলে আগে) |
| খোঁজার ধরন | স্তরে স্তরে, ঢেউয়ের মতো | এক পথে গভীরে, তারপর ফেরা |
| সবচেয়ে কম ধাপের পথ? | হ্যাঁ — ওজনহীন গ্রাফে | না — যেকোনো একটি পথ |
| বাড়তি মেমরি | একটি পুরো স্তর queue-তে থাকতে পারে | একটি পুরো পথ stack-এ থাকতে পারে |
| সেরা কাজ | কম ধাপের পথ, “কত দূরে”, স্তরভিত্তিক খোঁজ | Cycle, বিচ্ছিন্ন অংশ, topological sort, ধাঁধা |
| খরচ (list) | O(V + E) | O(V + E) |
যেখানে প্রতিদিন গ্রাফ চলছে
- ম্যাপ ও রাইড-শেয়ারিং অ্যাপ: রাস্তার মোড় vertex, রাস্তা edge, ওজন হলো যানজটসহ আনুমানিক সময়। Dijkstra-র ধারণার ওপর দাঁড়ানো উন্নত পদ্ধতি দিয়ে পথ বের হয়।
- Package manager ও build system: dependency-র directed গ্রাফ, topological sort দিয়ে ইনস্টলের ক্রম, cycle থাকলে স্পষ্ট error.
- Web crawler: সার্চ ইঞ্জিনের প্রোগ্রাম কয়েকটি পরিচিত পেজ থেকে শুরু করে লিংক ধরে ধরে নতুন পেজ খোঁজে — অনেকটা BFS-এর মতো, আর visited তালিকা ছাড়া একই পেজ বারবার নামাত।
- নেটওয়ার্ক রাউটিং: ইন্টারনেটের router-গুলো নিজেদের মধ্যে একটি গ্রাফ; OSPF-এর মতো প্রোটোকল Dijkstra দিয়ে প্যাকেট পাঠানোর পথ হিসাব করে (দেখুন Computer Networks কোর্স)।
৯ · AI-এর যুগে গ্রাফ কোড — কোথায় চোখ রাখবেন
“C-তে BFS লিখে দাও” — যেকোনো AI কয়েক সেকেন্ডে দিয়ে দেবে, আর ছোট উদাহরণে সেটি চলবেও। সমস্যা হয় যখন আপনার আসল ডেটা আসে: হাজার হাজার vertex, cycle, বিচ্ছিন্ন অংশ, ওজন। AI-এর গ্রাফ কোডের ভুলগুলো সাধারণত ছোট গ্রাফে লুকিয়ে থাকে, বড় গ্রাফে ফেটে বেরোয়।
Cycle, বিচ্ছিন্ন অংশ আর বড় মাপ দিয়ে পরীক্ষা করুন — তখনই সত্যটা বেরোয়।
AI সাধারণত যেসব গ্রাফ-ভুল করে
| AI-এর ভুল | যা ঘটে | কীভাবে ধরবেন |
|---|---|---|
| Visited array নেই, বা tree-র কোড গ্রাফে বসানো | Cycle-এ অনন্ত লুপ, queue উপচে পড়া, crash | একটি তিন-vertex-এর ত্রিভুজ (A–B–C–A) দিয়ে চালান |
| Visited চিহ্ন queue থেকে বের করার সময় দেওয়া | একই vertex বহুবার queue-তে, অকারণে ধীর, queue-র array-ও ছোট পড়তে পারে | Queue-তে কতবার ঢুকছে গুনে ছাপান |
| বিশাল গ্রাফে recursive DFS | লম্বা পথে stack overflow, segmentation fault | ১ লাখ vertex-এর একটি সোজা শিকল দিয়ে পরীক্ষা করুন |
বড় sparse গ্রাফে int adj[V][V] | গিগাবাইট মেমরি চায়, প্রোগ্রাম খোলেই না | V কত হতে পারে জিজ্ঞেস করুন, V² × 4 byte হিসাব করুন |
| সবচেয়ে কম ধাপের পথের জন্য DFS, বা ওজনওয়ালা গ্রাফে BFS | চলে, কিন্তু ভুল (লম্বা) পথ দেয় — সবচেয়ে বিপজ্জনক | হাতে একটি ছোট উদাহরণ মিলিয়ে দেখুন |
| শুধু একটি vertex থেকে খোঁজা | বিচ্ছিন্ন অংশগুলো বাদ পড়ে যায় | দুটি আলাদা দ্বীপওয়ালা গ্রাফ দিন |
AI-কে গ্রাফ কোড চাওয়ার সময় গ্রাফের চরিত্র বলে দিন — মাপ, দিক, ওজন, cycle আছে কি না। এতে AI সঠিক representation আর algorithm বেছে নেওয়ার সুযোগ পায়, আর আপনিও যাচাইয়ের মাপকাঠি পান:
AI-এর গ্রাফ কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১Königsberg-এর সাত সেতুর ধাঁধা Euler কীভাবে সমাধান করলেন — আর একটি সেতুর ধাঁধা কীভাবে পুরো একটি গণিতের শাখা হয়ে গেল?
Königsberg শহরের চারটি ভূখণ্ড — নদীর দুই তীর আর দুটি দ্বীপ — সাতটি সেতু দিয়ে যুক্ত ছিল। প্রশ্ন ছিল: এমন একটি হাঁটা কি সম্ভব, যেখানে প্রতিটি সেতু ঠিক একবার পার হবেন?
Euler-এর প্রথম বড় পদক্ষেপ ছিল abstraction (পর্ব ০২)। তিনি মানচিত্রের সব খুঁটিনাটি ফেলে দিলেন — নদী কত চওড়া, সেতু কত লম্বা, দ্বীপ কত বড়, কিছুই আসে যায় না। থাকল শুধু চারটি বিন্দু (ভূখণ্ড) আর সাতটি রেখা (সেতু)। এটিই প্রথম গ্রাফ।
দ্বিতীয় পদক্ষেপ ছিল একটি সুন্দর যুক্তি। হাঁটার মাঝপথে আপনি যে ভূখণ্ডেই ঢোকেন, সেখান থেকে বেরিয়েও যান — অর্থাৎ প্রতিবার ঢোকা-বেরোনোয় দুটি সেতু খরচ হয়। তাই শুরু আর শেষের ভূখণ্ড ছাড়া বাকি সব ভূখণ্ডের সেতুর সংখ্যা (degree) জোড় হতে হবে। Königsberg-এ চারটি ভূখণ্ডেরই degree বিজোড় ছিল (একটির ৫, তিনটির ৩)। বিজোড় degree-র ভূখণ্ড সর্বোচ্চ দুটি থাকতে পারে, তাই হাঁটাটি অসম্ভব — চেষ্টা করার আগেই প্রমাণ।
এই যুক্তির শক্তি হলো, এটি যেকোনো শহরের যেকোনো সেতুর জন্য খাটে। আজ একে বলে Euler path-এর শর্ত। আর সেই দিন থেকে প্রশ্নটি দাঁড়াল: “আকার নয়, সংযোগ” — এই দৃষ্টিভঙ্গি দিয়ে আর কী কী বোঝা যায়? উত্তর এসেছে শত বছর ধরে: রসায়নে অণুর গঠন, বিদ্যুতের সার্কিট, মানচিত্র রং করার সমস্যা, আর বিংশ শতাব্দীতে কম্পিউটার নেটওয়ার্ক, ইন্টারনেট, সামাজিক মাধ্যম।
প্রকৌশলেও এই প্রশ্ন আজও জীবন্ত। একটি সিটি কর্পোরেশনের ময়লার গাড়ি বা চিঠি বিলির লোককে প্রতিটি রাস্তা অন্তত একবার ঘুরতে হয়, অথচ যতটা সম্ভব কম বাড়তি হাঁটায় — এটি Euler-এর ধাঁধারই বাস্তব রূপ, যাকে বলে “Chinese postman problem”.
মূল উপলব্ধি: ভালো সমাধানের শুরু প্রায়ই অপ্রাসঙ্গিক তথ্য ফেলে দেওয়ায়। একটি সমস্যাকে গ্রাফ হিসেবে দেখতে পারলে, শত বছরের জমানো algorithm আপনার হাতে চলে আসে।
প্র ০২BFS ওজনহীন গ্রাফে সবচেয়ে ছোট পথ দেয়, কিন্তু ওজন এলেই কেন ব্যর্থ হয়? Dijkstra আসলে কী বদলায়?
BFS-এর নিশ্চয়তা দাঁড়িয়ে আছে একটি গোপন অনুমানের ওপর: প্রতিটি edge-এর খরচ সমান। তখন “কম edge” আর “কম খরচ” একই কথা, আর স্তরে স্তরে এগোনো মানে খরচের ক্রমেই এগোনো।
ওজন আলাদা হলে এই মিল ভেঙে যায়। ধরুন A থেকে C সরাসরি একটি রাস্তা ১০ কিলোমিটার, আর A → B → C দুটি রাস্তা মিলে মাত্র ৩ কিলোমিটার। BFS প্রথম ঢেউয়েই C-কে “১ ধাপ দূরে” লিখে visited করে ফেলবে। পরে B থেকে ভালো পথ পেলেও আর বদলাবে না, কারণ visited মানে “চূড়ান্ত”। ফল: ১০ কিলোমিটারের পথ।
Dijkstra দুটি জিনিস বদলায়। প্রথমত, queue-র বদলে এমন একটি তালিকা, যেখান থেকে সবসময় এখন পর্যন্ত মোট খরচ সবচেয়ে কম এমন vertex বের হয় (বাস্তবে একে বলে priority queue, সাধারণত heap দিয়ে বানানো — পর্ব ৩৬)। দ্বিতীয়ত, একটি vertex-এর দূরত্ব যতক্ষণ না সে তালিকা থেকে বের হচ্ছে, ততক্ষণ ভালো পথ পেলে কমানো যায় — একে বলে relaxation. ওপরের উদাহরণে C-কে প্রথমে ১০ লেখা হবে, কিন্তু B (খরচ ১) আগে বের হবে, আর B থেকে C-র দূরত্ব ৩-এ নেমে আসবে।
একটি সূক্ষ্ম কথা: BFS আসলে Dijkstra-রই একটি বিশেষ রূপ — সব ওজন ১ হলে priority queue আর সাধারণ queue একই আচরণ করে। আরেকটি সূক্ষ্ম কথা: Dijkstra ঋণাত্মক ওজনে কাজ করে না (যেমন “এই রাস্তা নিলে ক্যাশব্যাক”)। তখন লাগে Bellman–Ford-এর মতো ধীর কিন্তু সাধারণ পদ্ধতি।
মূল উপলব্ধি: প্রতিটি algorithm-এর পেছনে কিছু অনুমান লুকিয়ে থাকে। “এটি কাজ করে” বলার আগে জিজ্ঞেস করুন — “কোন অনুমানে কাজ করে?” AI-এর কোড যাচাইয়ের সময় এই প্রশ্নটিই সবচেয়ে বেশি কাজে আসে।
প্র ০৩সারা দেশের কোটি কোটি রাস্তার মোড় থেকে ম্যাপ অ্যাপ কয়েক মুহূর্তে পথ বের করে কীভাবে? সাধারণ Dijkstra কি যথেষ্ট?
সরাসরি উত্তর: না। একটি বড় দেশের সড়ক-গ্রাফে কোটি কোটি vertex থাকতে পারে। প্রতিটি অনুরোধে সাধারণ Dijkstra চালালে সে গন্তব্যের দিকে তো বটেই, উল্টো দিকেও সমানভাবে ঢেউ ছড়াবে — ঢাকা থেকে চট্টগ্রামের পথ খুঁজতে গিয়ে রংপুরের গলিও দেখবে। আর একই সময়ে লাখো মানুষ পথ খুঁজছেন। বাস্তবের সিস্টেম তাই কয়েকটি কৌশল মেশায় (এখানে গুণগতভাবে বলছি; নির্দিষ্ট কোম্পানির ভেতরের খুঁটিনাটি সবসময় প্রকাশ্য নয়):
- দিকনির্দেশক আন্দাজ (heuristic): ১৯৬৮ সালে প্রকাশিত A* (A-star) পদ্ধতি Dijkstra-র সাথে একটি আন্দাজ যোগ করে — “এই মোড় থেকে গন্তব্য সরলরেখায় কত দূর?” ফলে ঢেউ গন্তব্যের দিকে ঝুঁকে এগোয়, অপ্রয়োজনীয় দিকে কম ছড়ায়।
- আগে থেকে প্রস্তুতি (preprocessing): রাস্তার মানচিত্র রোজ বদলায় না। তাই রাতে বসে আগে থেকেই গ্রাফ বিশ্লেষণ করে রাখা হয় — যেমন কোন মহাসড়কগুলো দূরের যাত্রায় প্রায় সবসময় লাগে, তাদের মাঝে “শর্টকাট” edge বানিয়ে রাখা। অনুরোধ এলে শুধু স্থানীয় রাস্তা থেকে মহাসড়কে ওঠা, মহাসড়ক ধরে লাফ, আর শেষে আবার স্থানীয় রাস্তা। Contraction hierarchies-এর মতো পদ্ধতি এই চিন্তা থেকেই।
এখানে একটি ক্লাসিক প্রকৌশল trade-off আছে: আগে থেকে বেশি হিসাব করে রাখলে (বেশি মেমরি, বেশি প্রস্তুতির সময়) প্রতিটি প্রশ্নের উত্তর অনেক দ্রুত হয়। পর্ব ১৫-এর memoization-এর মতোই — একবার খেটে রাখো, বারবার সুবিধা নাও।
মূল উপলব্ধি: পাঠ্যবইয়ের algorithm হলো ভিত্তি, শেষ উত্তর নয়। বাস্তবের সিস্টেম জেতে ডেটার বৈশিষ্ট্য কাজে লাগিয়ে — রাস্তা কম বদলায়, মানুষ সাধারণত মহাসড়ক ধরে দূরে যায়, গন্তব্যের একটি দিক আছে।
প্র ০৪“Six degrees of separation” কি সত্যি? সামাজিক গ্রাফ এত “ছোট” কেন, আর এর ভালো-মন্দ কী?
ধারণাটি পুরোনো। হাঙ্গেরির লেখক Frigyes Karinthy ১৯২৯ সালের একটি ছোটগল্পে কল্পনা করেছিলেন, পৃথিবীর যেকোনো দুজন মানুষ পাঁচ-ছয়জন পরিচিতের শিকলে যুক্ত। ১৯৬০-এর দশকে মার্কিন মনোবিজ্ঞানী Stanley Milgram একটি পরীক্ষা করেন: দূরের শহরের মানুষদের একটি চিঠি দিয়ে বলা হলো, শুধু ব্যক্তিগতভাবে চেনেন এমন কাউকে দিয়ে দিয়ে চিঠিটি একজন নির্দিষ্ট অচেনা মানুষের কাছে পৌঁছাতে। যে চিঠিগুলো পৌঁছেছিল, সেগুলো গড়ে মোটামুটি ছয় হাতের কাছাকাছি ঘুরে গিয়েছিল। (অনেক চিঠি পৌঁছায়ইনি — তাই পরীক্ষাটির পদ্ধতি নিয়ে সমালোচনাও আছে।)
অনেক পরে Facebook-এর গবেষকেরা তাঁদের পুরো ব্যবহারকারী-গ্রাফে দূরত্ব মেপেছিলেন — ২০১৬ সালের একটি হিসাবে দুজনের মাঝে গড়ে প্রায় সাড়ে তিনজন মানুষ পাওয়া গিয়েছিল, অর্থাৎ গড় দূরত্ব সাড়ে চার ধাপের মতো। এত বড় গ্রাফে প্রতিটি জোড়ার জন্য BFS চালানো অসম্ভব, তাই তাঁরা পরিসংখ্যানের চালাক আনুমানিক পদ্ধতি ব্যবহার করেছিলেন।
গ্রাফ এত ছোট কেন? সামাজিক গ্রাফে বেশিরভাগ বন্ধুত্ব স্থানীয় (একই পাড়া, একই কলেজ), কিন্তু অল্প কিছু “দূরের সেতু” থাকে — বিদেশে থাকা আত্মীয়, অন্য শহরে চাকরি করা বন্ধু। আর কিছু মানুষের হাজার হাজার সংযোগ থাকে (hub). এই অল্প কয়েকটি দূরের সংযোগই পুরো গ্রাফের দূরত্ব নাটকীয়ভাবে কমিয়ে দেয়। BFS-এর ভাষায়: প্রতিটি স্তরে মানুষের সংখ্যা গুণিতক হারে বাড়ে — ২০০ বন্ধু, তাঁদের প্রত্যেকের ২০০ বন্ধু — তাই কয়েক স্তরেই কোটি মানুষ ছুঁয়ে ফেলা যায়।
ভালো দিক: চাকরির খোঁজ, তথ্য ছড়ানো, বন্যার সময় সাহায্য সংগঠন — সবই দ্রুত হয়। মন্দ দিক: গুজব আর ভুয়া খবরও ঠিক একই গতিতে ছড়ায়; একটি রোগও তাই। আর “People you may know” কখনো এমন মানুষকে দেখায় যাঁকে আপনি দেখাতে চান না — গ্রাফ ব্যক্তিগত তথ্য ফাঁস করতে পারে, এমনকি আপনি নিজে কিছু না বললেও।
মূল উপলব্ধি: গ্রাফের আকার নয়, তার গঠনই আচরণ ঠিক করে। কয়েকটি দূরের সংযোগ আর কিছু hub একটি বিশাল জগৎকে ছোট করে দেয় — যা তথ্যের জন্য আশীর্বাদ, আর গুজবের জন্য বিপদ।
প্র ০৫Package বা dependency-র গ্রাফে cycle থাকলে ইনস্টল কেন ব্যর্থ হয়? বড় সফটওয়্যার প্রজেক্ট কীভাবে এই সমস্যা এড়ায়?
“A বসাতে আগে B লাগবে, B বসাতে আগে A লাগবে” — এখানে শুরু করার কোনো জায়গাই নেই। Topological sort-এর ভাষায়: cycle-এর প্রতিটি vertex-এর in-degree কখনো শূন্য হয় না, তাই কেউই “এখন নেওয়া যায়” তালিকায় ঢোকে না। Build system-ও একই সমস্যায় পড়ে — GNU make এমন অবস্থায় “Circular … dependency dropped” ধরনের সতর্কবার্তা দেয়। C-তে এর একটি পরিচিত রূপ দেখেছেন পর্ব ২৭-এ: দুটি header একে অপরকে #include করলে include guard না থাকলে preprocessor একই ফাইল বারবার ঢোকাতে ঢোকাতে nesting-এর সীমায় পৌঁছে error দিত।
আরেকটি সমস্যা হলো version-এর দ্বন্দ্ব: app চায় json-এর version 2, কিন্তু app-এর আরেক dependency চায় json-এর version 1. এটি শুধু গ্রাফ ঘোরা নয়, একটি কঠিন অনুসন্ধান-সমস্যা; সাধারণ অবস্থায় এটি খুবই কঠিন (NP-complete) প্রমাণিত। তাই আধুনিক package manager-গুলো চালাক backtracking বা SAT solver-ধাঁচের পদ্ধতি ব্যবহার করে — আবারও DFS-এর আত্মীয়।
বড় প্রজেক্ট কীভাবে এড়ায়:
- স্তরভিত্তিক নকশা: নিচের স্তরের module কখনো ওপরের স্তরকে ডাকতে পারবে না — নিয়ম করে দেওয়া। এতে dependency গ্রাফ সবসময় একমুখী থাকে, cycle তৈরিই হয় না।
- Interface আলাদা করা: দুটি module একে অপরকে চাইলে, তাদের দুজনের প্রয়োজনীয় অংশটি একটি তৃতীয় ছোট module-এ সরিয়ে নেওয়া (পর্ব ২৯-এর module নকশার মতো)।
- যন্ত্র দিয়ে পাহারা: অনেক প্রজেক্টের CI-তে একটি স্বয়ংক্রিয় পরীক্ষা থাকে যা নতুন কোডে dependency cycle ঢুকলেই merge আটকে দেয় — ভেতরে সেই DFS-এর তিন-অবস্থার cycle check.
মূল উপলব্ধি: Cycle শুধু algorithm-এর সমস্যা নয়, নকশার সমস্যার লক্ষণ। Dependency গ্রাফ পরিষ্কার একমুখী রাখা বড় সফটওয়্যারকে বোঝা, পরীক্ষা করা আর বদলানো সহজ করে — আর সেটি পাহারা দিতে পারে আপনার শেখা কয়েক লাইনের DFS.
অনুশীলন
বেশিরভাগ কাজ খাতা-কলমের — গ্রাফ এঁকে হাতে চালানোই সবচেয়ে ভালো শেখা।
-
হাতে BFS: ওপরের রুট-মানচিত্রে এবার Shahbag থেকে BFS চালান (প্রতিবেশী ছোট নম্বর আগে)। প্রতিটি এলাকার দূরত্ব আর queue থেকে বের হওয়ার ক্রম লিখুন।
Shahbag (5)-এর প্রতিবেশী Farmgate (3) আর Motijheel (6) — দুজনেই দূরত্ব ১। Farmgate বের হলে Agargaon (1) আর Gulshan (4) — দূরত্ব ২। Motijheel বের হলে Gulshan আগেই visited, নতুন কেউ নেই। Agargaon থেকে Mirpur (0), Gulshan থেকে Mohakhali (2) — দুজনেই দূরত্ব ৩।
বের হওয়ার ক্রম: Shahbag, Farmgate, Motijheel, Agargaon, Gulshan, Mirpur, Mohakhali. দূরত্ব: Shahbag 0, Farmgate 1, Motijheel 1, Agargaon 2, Gulshan 2, Mirpur 3, Mohakhali 3. প্রথম কোডে
dist[0] = 0; queue[tail++] = 0;-এর জায়গায় 5 বসিয়ে মিলিয়ে নিন। -
Matrix না list? নিচের তিনটি ক্ষেত্রে কোনটি নেবেন, কারণসহ বলুন — (ক) একটি ক্লাসের ৪০ জন ছাত্রের মধ্যে কে কার সাথে কথা বলে (প্রায় সবাই সবার সাথে) (খ) বাংলাদেশের সব ইউনিয়নের রাস্তার সংযোগ (গ) ১০ কোটি ব্যবহারকারীর একটি সামাজিক অ্যাপ।
(ক) Matrix — মাত্র ৪০ × ৪০ = ১,৬০০ ঘর, আর গ্রাফটি dense. “ক আর খ কি কথা বলে?” এক ধাপেই জানা যায়, কোডও সহজ।
(খ) List — কয়েক হাজার vertex, কিন্তু প্রতিটি ইউনিয়ন মাত্র কয়েকটি পাশের ইউনিয়নের সাথে যুক্ত; গ্রাফটি sparse. Matrix-এর প্রায় সব ঘর 0 হতো।
(গ) অবশ্যই list — ১০ কোটির বর্গ ঘর কোনো কম্পিউটারের মেমরিতে আঁটবে না। বাস্তবে এমন গ্রাফ অনেকগুলো সার্ভারে ভাগ করে রাখা হয়, আর প্রতিটি মানুষের বন্ধু-তালিকা আলাদা করে থাকে — মূলত adjacency list-এরই বড় রূপ।
-
DFS-এর ক্রম বদলান: DFS-এর ডায়াগ্রামে প্রতিবেশীদের এবার বড় নম্বর আগে দেখলে Mirpur থেকে দেখার ক্রম কী হতো? সবচেয়ে বেশি গভীরতা কত হতো?
Mirpur → Mohakhali (2, কারণ 2 > 1) → Gulshan (4) → Motijheel (6) → Shahbag (5) → Farmgate (3) → Agargaon (1). Agargaon-এর প্রতিবেশী Mirpur আর Farmgate দুজনেই visited, তাই এখান থেকে ফেরা শুরু।
দেখার ক্রম: Mirpur, Mohakhali, Gulshan, Motijheel, Shahbag, Farmgate, Agargaon — আর গভীরতা গিয়ে দাঁড়ায় ৭, অর্থাৎ পুরো গ্রাফটি একটি লম্বা পথে। শিক্ষা: DFS-এর ক্রম (আর stack-এর গভীরতা) প্রতিবেশী দেখার ক্রমের ওপর নির্ভর করে; একই গ্রাফের একাধিক “সঠিক” DFS ক্রম হতে পারে। BFS-এর দূরত্ব কিন্তু ক্রম বদলালেও বদলায় না।
-
Cycle খুঁজুন (কোড): দ্বিতীয় কোডে
dep[4][1] = 1মুছে দিন, আর তার বদলেdep[3][0] = 1(log-এর লাগে app) যোগ করুন। চালানোর আগে অনুমান করুন আউটপুট কী হবে, তারপর মিলিয়ে দেখুন।DFS app থেকে শুরু: app → web → json → util. util-এর এখন আর কোনো dependency নেই, তাই সে শেষ (অবস্থা 2), তারপর json আর web-ও শেষ। এরপর app-এর পরের dependency log. log-এর লুপ ছোট নম্বর থেকে শুরু হয়, তাই প্রথমেই দেখে app (নম্বর 0) — যার অবস্থা 1, মানে app এখনো চলমান পথে। তাই আউটপুট:
app web json util log CYCLE: log -> appআর যদি শুধু
dep[4][1]মুছতেন, কিছু যোগ না করতেন? তখন log → util-এ util-কে আবার পাওয়া যেত অবস্থা 2-এ — cycle নয় — আর শেষে ছাপা হতো “No cycle”. ঠিক এজন্যই অবস্থা 1 আর 2 আলাদা রাখা দরকার। Undirected গ্রাফে আবার নিয়ম আলাদা: সেখানে “দাগওয়ালা, কিন্তু যেখান থেকে এসেছি সে নয়” — এটুকুই cycle-এর প্রমাণ।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমBFS-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- সবার দূরত্ব -1 (অদেখা) করে রাখা
- শুরুর vertex-এর দূরত্ব 0 করে queue-তে রাখা
- Queue-র সামনে থেকে একটি vertex u বের করা
- u-এর প্রতিটি অদেখা প্রতিবেশীর দূরত্ব লিখে queue-র পেছনে রাখা
- Queue খালি হলে থামা — সব দূরত্ব চূড়ান্ত
একটি ওজনহীন বাস-রুট গ্রাফে সবচেয়ে কম স্টপেজের পথ বের করতে কোনটি ব্যবহার করবেন?
- DFS
- BFS
- Topological sort
- এলোমেলো পথে হাঁটা
AI-এর লেখা একটি BFS ছোট tree-তে ঠিক চলে, কিন্তু একটি ত্রিভুজ (A–B–C–A) গ্রাফে কখনো থামে না। সবচেয়ে সম্ভাব্য কারণ?
- Queue-র বদলে stack ব্যবহার হয়েছে
- Adjacency matrix ব্যবহার হয়েছে
- Visited চিহ্ন রাখা হয়নি
- -Wall দিয়ে compile করা হয়নি
১০ লাখ vertex আর প্রায় ৩০ লাখ edge-এর একটি সড়ক-গ্রাফ। কোন representation যুক্তিসংগত?
char adj[1000000][1000000]int adj[1000000][1000000]- শুধু সব edge-এর একটি লম্বা তালিকা, কোনো ভাগ ছাড়া
- Adjacency list
BFS-এ visited চিহ্ন কখন দেওয়া উচিত?
- Vertex-কে queue-তে ঢোকানোর সময়
- Vertex-কে queue থেকে বের করার সময়
- সব খোঁজা শেষ হওয়ার পরে
- শুধু শুরুর vertex-কে, বাকিদের দরকার নেই
লাখ লাখ vertex-এর একটি লম্বা শিকলের মতো গ্রাফে recursive DFS চালাতেই segmentation fault. কেন, আর সমাধান কী?
- Visited array নেই; visited যোগ করলেই ঠিক
- Call stack ফুরিয়ে গেছে (stack overflow); নিজের একটি stack দিয়ে DFS লেখা
- Adjacency list-এর বদলে matrix ব্যবহার করা
- DFS কখনো বড় গ্রাফে চলে না; সবসময় BFS ব্যবহার করুন
Package-এর dependency গ্রাফে topological sort চালালেন, কিন্তু কিছু package কখনো “এখন নেওয়া যায়” তালিকায় এলো না। এর মানে কী?
- গ্রাফটি খুব বড়
- গ্রাফটি undirected
- গ্রাফে একটি cycle আছে
- Edge-গুলোর ওজন নেই
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- গ্রাফ = vertex + edge; দিক থাকতে পারে (directed), ওজন থাকতে পারে (weighted), আর cycle থাকা স্বাভাবিক।
- Adjacency matrix সহজ ও “যুক্ত কি না” প্রশ্নে দ্রুত, কিন্তু মেমরি V²; বাস্তবের বড় sparse গ্রাফে adjacency list (V + E).
- BFS = queue, স্তরে স্তরে; ওজনহীন গ্রাফে সবচেয়ে কম ধাপের পথ। ওজন এলে Dijkstra.
- DFS = recursion বা stack, গভীরে গিয়ে ফেরা; cycle ধরা, বিচ্ছিন্ন অংশ, topological sort. গভীর গ্রাফে stack overflow-এর ঝুঁকি।
- Visited ছাড়া গ্রাফ algorithm নেই। দুটোরই খরচ O(V + E); AI-এর কোড cycle, বিচ্ছিন্ন অংশ আর বড় মাপ দিয়ে পরীক্ষা করুন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩৯ · কমপ্লেক্সিটি — Big-O দিয়ে গতি মাপাপরবর্তী পর্বO(V + E) আর O(V²)-এর আসল অর্থ, আর কোড না চালিয়েই গতির অনুমান।
- C প্রোগ্রামিং · কোর্স হোম৫৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Data Structures & Algorithmsগভীরে যানDijkstra, Bellman–Ford, minimum spanning tree আর আরও গ্রাফ algorithm বিস্তারিত।
- Design & Analysis of Algorithmsগভীরে যানBFS ও Dijkstra কেন সঠিক — প্রমাণ, আর greedy ও dynamic programming-এর সাথে সম্পর্ক।
- Computer Networksপাশের ট্র্যাকইন্টারনেটের router-গুলো গ্রাফ algorithm দিয়ে কীভাবে প্যাকেটের পথ ঠিক করে।