এই পর্বে যা শিখবেন
- Weighted graph-এ single-source shortest path সমস্যাটি নিখুঁতভাবে বলা, আর BFS কেন এখানে ভুল উত্তর দেয় তা দেখানো
- Relaxation আর Dijkstra-র greedy নিয়ম হাতে trace করা — dist টেবিল আর priority queue একসাথে
- Dijkstra-র শুদ্ধতা প্রমাণ করা, আর একটি ঋণাত্মক edge দিয়ে counterexample বানানো
- O(V²) অ্যারে সংস্করণ আর heap + lazy deletion সংস্করণের মধ্যে গ্রাফ দেখে বেছে নেওয়া, আর পথ পুনর্গঠন করা
- Multi-source, state-space আর গ্রিডের সমস্যাকে Dijkstra-তে রূপান্তর করা, আর A* কখন সঠিক উত্তর দেয় তা জানা
১ · সমস্যাটি — ঢাকা থেকে চট্টগ্রাম, সবচেয়ে কম সময়ে
ঈদের ছুটিতে আপনি ঢাকা থেকে গাড়িতে চট্টগ্রাম যাবেন। রাস্তা একটি নয়: সোজা মহাসড়ক ধরে কুমিল্লা হয়ে যাওয়া যায়, নরসিংদী ঘুরে যাওয়া যায়, চাঁদপুরের দিক দিয়েও যাওয়া যায়। প্রতিটি রাস্তায় সময় আলাদা — কোথাও জ্যাম, কোথাও ফাঁকা। প্রশ্নটি সরল: কোন পথে গেলে মোট সময় সবচেয়ে কম? মোবাইলের ম্যাপ অ্যাপ প্রতিদিন কোটি কোটি মানুষের জন্য ঠিক এই প্রশ্নের উত্তর দেয় — প্রতিটি মোড় একটি বিন্দু, প্রতিটি রাস্তার টুকরো একটি রেখা, আর রেখার গায়ে লেখা সেই মুহূর্তে পার হতে কত সময় লাগবে।
পর্ব ৩৯-এর ভাষায় এটি একটি graph: শহরগুলো vertex, রাস্তাগুলো edge. নতুন জিনিস হলো প্রতিটি edge-এর গায়ে একটি সংখ্যা — তার ওজন (weight). এমন গ্রাফকে বলে weighted graph. রাস্তা দুই দিকেই চলে বলে আমাদের উদাহরণটি undirected; একমুখী রাস্তা থাকলে হতো directed graph. নিচের মানচিত্রে সংখ্যাগুলো মিনিটে যাত্রার সময় (বোঝার সুবিধায় বানানো সংখ্যা, আসল সময় নয়)। ঢাকা–কুমিল্লার সরাসরি রাস্তায় এই মুহূর্তে বড় জ্যাম, তাই সেখানে 200.
Narsingdi
/ \
60 / \ 90
/ \
Dhaka ------200------ Cumilla ----60---- Feni ----110---- Chattogram
\ / /
150 \ / 70 /
\ / /
Chandpur ---------100----------
একটি পথের ওজন মানে তার সব edge-এর ওজনের যোগফল। u থেকে v-এর সবচেয়ে কম ওজনের পথকে বলে shortest path, আর সেই ওজনকে লেখা হয় δ(u, v) (ডেল্টা). পথ না থাকলে δ = ∞. আমাদের প্রশ্নটির নাম single-source shortest path (SSSP): একটি নির্দিষ্ট উৎস s (এখানে ঢাকা) থেকে প্রতিটি vertex-এর δ(s, v) বের করা। মজার ব্যাপার, শুধু চট্টগ্রামের উত্তর চাইলেও সেরা জানা পদ্ধতিগুলো পথে পড়া প্রায় সব শহরের উত্তর বের করে ফেলে — এক গন্তব্যের প্রশ্ন সাধারণত এক উৎসের প্রশ্নের চেয়ে সহজ নয়।
BFS কেন যথেষ্ট নয়
পর্ব ৪০-এ BFS unweighted গ্রাফে সবচেয়ে ছোট পথ দিত — কারণ সেখানে প্রতিটি edge-এর দাম সমান, তাই “কম edge” আর “কম দূরত্ব” একই কথা। ওজন এলেই এই সমতা ভাঙে। চট্টগ্রামে যাওয়ার কয়েকটি পথ দেখুন:
| পথ | edge-এর সংখ্যা | মোট সময় (মিনিট) |
|---|---|---|
| Dhaka → Cumilla → Feni → Chattogram | ৩ | 200 + 60 + 110 = 370 |
| Dhaka → Chandpur → Feni → Chattogram | ৩ | 150 + 100 + 110 = 360 |
| Dhaka → Narsingdi → Cumilla → Feni → Chattogram | ৪ | 60 + 90 + 60 + 110 = 320 |
| Dhaka → Chandpur → Cumilla → Feni → Chattogram | ৪ | 150 + 70 + 60 + 110 = 390 |
BFS স্তর ধরে চলে, তাই সে চট্টগ্রামে পৌঁছায় তিন edge-এর কোনো পথে — ৩৬০ বা ৩৭০ মিনিট। অথচ সেরা পথটিতে edge চারটি, সময় ৩২০ মিনিট। ঘুরপথ কখনো কখনো আসলে দ্রুত — ঢাকার যেকোনো চালক এটি জানেন। আর সব পথ গুনে দেখাও সমাধান নয়: পথের সংখ্যা গ্রাফের আকারের সাথে exponential হারে বাড়ে।
একটি সস্তা কৌশল, আর তার সীমা: ওজন ছোট পূর্ণসংখ্যা হলে প্রতিটি w-ওজনের edge-কে w টি একক-edge-এ ভেঙে BFS চালানো যায়। কিন্তু 200 মিনিটের রাস্তা মানে 199টি নকল vertex — ওজন বড় হলে গ্রাফ অসম্ভব রকম বড় হয়ে যায়। Dijkstra-র অ্যালগরিদম ঠিক এই কাজটিই করে, কিন্তু নকল vertex না বানিয়ে — “পানির ঢেউ কখন কোন শহরে পৌঁছায়” শুধু সেটুকু হিসাব রেখে।
২ · Relaxation — দূরত্বের অনুমান ধাপে ধাপে কমানো
Shortest path অ্যালগরিদমগুলো প্রতিটি vertex v-এর জন্য একটি সংখ্যা রাখে: dist[v] — “এখন পর্যন্ত s থেকে v-এ যাওয়ার যে সবচেয়ে ভালো পথ জানি, তার ওজন”. শুরুতে dist[s] = 0, বাকি সবার ∞ — কিছুই জানা নেই। তারপর একটিমাত্র কাজ বারবার করা হয়, যার নাম relaxation:
মানে: “u পর্যন্ত আমি dist[u] সময়ে যেতে পারি, তারপর এই রাস্তায় আরও w — তাহলে v-তে যাওয়ার এটি একটি নতুন পথ। আগের জানা পথের চেয়ে ভালো হলে সেটি মনে রাখি।” par[v] (parent বা predecessor) মনে রাখে v-তে আসার সেরা পথে আগের vertex কে — পথ ফিরে পেতে এটি লাগবে (অংশ ৬)। পেছনের ধারণাটি হলো: অনুমানটি সবসময় একটি ওপরের সীমা, আর প্রতিটি সফল relax সেই সীমাকে সত্যের দিকে একটু নামিয়ে আনে।
| যে edge relax হচ্ছে | |
| তুলনা | |
| dist[Feni], par[Feni] | |
| dist[Chattogram] | |
| dist[Cumilla] |
- শুরু. Cumilla আর Chandpur-এর জন্য 150 মিনিটের পথ জানা আছে, Feni আর Chattogram-এর জন্য এখনো কোনো পথ জানা নেই — তাই ∞.
- Cumilla → Feni. Cumilla পর্যন্ত 150, তারপর আরও 60: মোট 210, যা ∞-এর চেয়ে ছোট। dist[Feni] = 210, আর par[Feni] = Cumilla — “Feni-তে এসেছি Cumilla হয়ে”.
- Chandpur → Feni. নতুন প্রস্তাব 250, কিন্তু আগের 210 আরও ভালো। কিছুই বদলায় না। Relaxation কখনো কোনো dist বাড়ায় না।
- Feni → Chattogram. Feni-র এখনকার অনুমান 210 ধরে Chattogram-এ 320. লক্ষ করুন, এটি নির্ভর করছে Feni-র অনুমানের ওপর — Feni-র dist পরে কমলে Chattogram-এর edge আবার relax করতে হতো।
- Feni → Cumilla. উল্টো দিকের রাস্তা: 210 + 60 = 270, কিন্তু Cumilla-র 150 আরও ভালো — কিছু বদলায় না। Undirected গ্রাফে প্রতিটি রাস্তা দুই দিকেই relax হয়, কিন্তু সঠিক ক্রমে চললে উল্টো দিকেরগুলো কিছুই বদলায় না।
দুটি তথ্য মনে রাখুন, প্রমাণে লাগবে। (১) dist সবসময় একটি ওপরের সীমা: যেকোনো মুহূর্তে dist[v] ≥ δ(s, v), কারণ dist[v] সবসময় কোনো একটি বাস্তব পথের ওজন (বা ∞)। (২) dist শুধু কমে: relax মান বাড়ায় না। আর যখন সব edge (u, v)-এর জন্য dist[v] ≤ dist[u] + w(u, v) — অর্থাৎ আর কোনো relax কিছু বদলায় না — তখন dist-ই আসল উত্তর। একে বলে triangle inequality: δ(s, v) ≤ δ(s, u) + w(u, v), “ঘুরপথ সরাসরির চেয়ে ভালো হতে পারে না”.
তাহলে shortest path-এর সব অ্যালগরিদম আসলে একই খেলার ভিন্ন কৌশল: কোন edge কোন ক্রমে relax করব? পর্ব ৪৪-এর Bellman-Ford বলে “সব edge, V − 1 বার” — সহজ, কিন্তু O(VE). পর্ব ৪২-এর DAG-এ topological ক্রমে একবার করে — O(V + E). আর Dijkstra বলে: সবচেয়ে কাছের vertex থেকে শুরু করা, প্রতিটি vertex-এর edge ঠিক একবার।
৩ · Dijkstra-র greedy ধারণা — সবচেয়ে কাছেরটিকে চূড়ান্ত করা
পুকুরের ঠিক মাঝখানে একটি ঢিল ফেলুন। ঢেউ চারদিকে সমান গতিতে ছড়ায়, আর পানির ওপর ভাসা পাতাগুলোকে ছোঁয় ঠিক তাদের দূরত্বের ক্রমে — কাছেরটি আগে, দূরেরটি পরে। যে মুহূর্তে ঢেউ একটি পাতা ছোঁয়, সেই সময়ই তার সবচেয়ে ছোট দূরত্ব; পরে আর কোনো ঘুরপথে ঢেউ তার আগে পৌঁছাতে পারে না। Dijkstra-র অ্যালগরিদম এই ঢেউটিকে কম্পিউটারে চালায় — কিন্তু প্রতিটি মিনিট গুনে নয়, এক শহর থেকে পরের শহরে লাফিয়ে।
অ্যালগরিদমটি vertex-গুলোকে দুই দলে রাখে: settled (যাদের দূরত্ব চূড়ান্ত) আর বাকিরা। প্রতিটি ধাপে:
- যারা এখনো settled নয়, তাদের মধ্যে যার
distসবচেয়ে ছোট, তাকে বাছাই করুন — নাম দিন u. - u-কে settled ঘোষণা করুন:
dist[u]এখন থেকে চূড়ান্ত। - u থেকে বের হওয়া প্রতিটি edge relax করুন।
এটি একটি greedy অ্যালগরিদম: প্রতিটি ধাপে সেই মুহূর্তের সেরা দেখানো পছন্দটি নেয়, আর কখনো ফিরে তাকায় না। Greedy সব সমস্যায় ঠিক উত্তর দেয় না (পর্ব ৫০), তাই এখানে প্রমাণ দরকার — অংশ ৪-এ। “সবচেয়ে ছোট dist” বারবার খুঁজতে লাগে একটি priority queue (পর্ব ২৩)। নিচের pseudocode-এ দুটি সংস্করণ — একটি সাধারণ অ্যারে দিয়ে খোঁজে, অন্যটি min-heap দিয়ে।
Pseudocode — Dijkstra (0-based vertices, every weight w >= 0)
// Version 1: plain array, O(V^2) -- good for dense graphs
dijkstraArray(G, s):
dist[v] = INF, done[v] = false for every v; dist[s] = 0
repeat V times:
u = the vertex with done[u] == false and the smallest dist[u]
if dist[u] == INF: stop // everything left is unreachable
done[u] = true // settle u: dist[u] is final
for each edge (u, v, w):
if not done[v] and dist[u] + w < dist[v]:
dist[v] = dist[u] + w; par[v] = u
// Version 2: binary heap + lazy deletion, O((V+E) log V)
dijkstraHeap(G, s):
dist[v] = INF for every v; dist[s] = 0; push (0, s)
while the heap is not empty:
(d, u) = pop the smallest pair
if d != dist[u]: continue // stale pair -- u was settled earlier
for each edge (u, v, w):
if d + w < dist[v]:
dist[v] = d + w; par[v] = u; push (dist[v], v)দ্বিতীয় সংস্করণের কৌশলটি লক্ষ করুন। কোনো vertex-এর dist কমলে heap-এর ভেতরের পুরোনো জোড়াটি খুঁজে বদলানো (decrease-key) কঠিন, তাই নতুন জোড়াটি আরেকবার push করা হয়। পুরোনোটি heap-এ থেকেই যায়; পরে সেটি বের হলে d != dist[u] দেখে চেনা যায় যে এটি বাসি — বাদ। এর নাম lazy deletion. প্রথমবার যে জোড়াটি বের হয়, তার d-ই সবচেয়ে ছোট, তাই সেটিই u-কে settle করে। এবার আমাদের মানচিত্রে পুরোটা চালিয়ে দেখুন:
| Dhaka | |
| Narsingdi | |
| Cumilla | |
| Chandpur | |
| Feni | |
| Chattogram | |
| priority queue (ছোট থেকে বড়) | |
| এই ধাপে |
- শুরু. dist[Dhaka] = 0, বাকি সবাই ∞. Heap-এ একটিমাত্র জোড়া (0, Dhaka).
- Dhaka settle. তিনটি রাস্তা relax: Narsingdi 60, Chandpur 150, Cumilla 200 (জ্যামের রাস্তা)। তিনটি জোড়াই heap-এ গেল।
- Narsingdi settle (60). Heap-এর সবচেয়ে ছোট 60. Narsingdi → Cumilla: 60 + 90 = 150 < 200, তাই Cumilla-র dist কমে 150, par বদলে Narsingdi. নতুন জোড়া (150, Cumilla) push হলো — কিন্তু পুরোনো (200, Cumilla)-ও heap-এ রয়ে গেল। Narsingdi → Dhaka: 120 ≥ 0, কিছু হয় না।
- Cumilla settle (150). দুটি জোড়ার d সমান 150; সমতা ভাঙে vertex নম্বরে: Cumilla 2 < Chandpur 3, তাই Cumilla আগে। Cumilla → Feni: 210. Cumilla → Chandpur: 220 ≥ 150, কিছু হয় না।
- Chandpur settle (150). Chandpur → Feni: 250 ≥ 210; কোনো dist কমল না। সরাসরি ঢাকা থেকে আসা পথটিই Chandpur-এর সেরা।
- বাসি জোড়া. (200, Cumilla) বের হলো, কিন্তু dist[Cumilla] এখন 150 ≠ 200 — এটি পুরোনো খবর। কোনো কাজ না করে বাদ। এটিই lazy deletion.
- Feni settle (210). Feni → Chattogram: 210 + 110 = 320. বাকি দুটি রাস্তা (Cumilla, Chandpur দিকে) কিছু বদলায় না।
- Chattogram settle (320). Heap খালি — শেষ। par ধরে পেছনে হাঁটলে: Chattogram ← Feni ← Cumilla ← Narsingdi ← Dhaka. ৪টি edge-এর পথ, ৩২০ মিনিট — BFS-এর ৩ edge-এর পথের চেয়ে ৪০ মিনিট কম। settle হওয়ার ক্রম 0, 60, 150, 150, 210, 320 — কখনো কমেনি।
শেষ পর্যবেক্ষণটি গুরুত্বপূর্ণ: vertex-গুলো দূরত্বের ছোট থেকে বড় ক্রমে settle হয় — ঠিক পুকুরের ঢেউয়ের মতো। এটি কাকতালীয় নয়, পরের অংশের প্রমাণের মূল কথা।
১৯৫৬ সালে আমস্টারডামের Mathematical Centre-এ কাজ করতেন তরুণ ডাচ প্রোগ্রামার Edsger W. Dijkstra. নতুন ARMAC কম্পিউটারের ক্ষমতা সাধারণ মানুষকে দেখাতে তিনি এমন একটি সমস্যা খুঁজছিলেন, যা কম্পিউটার না-জানা মানুষও বোঝেন: নেদারল্যান্ডসের দুটি শহরের মধ্যে সবচেয়ে ছোট পথ। বহু বছর পরের একটি সাক্ষাৎকারে তিনি নিজে স্মৃতিচারণ করেন যে এক সকালে বাগদত্তার সাথে কেনাকাটা করে ক্লান্ত হয়ে তাঁরা একটি ক্যাফের বারান্দায় কফি খেতে বসেন, আর সেখানেই, কাগজ-কলম ছাড়া, প্রায় বিশ মিনিটে তিনি অ্যালগরিদমটি ভেবে ফেলেন। তাঁর মতে কাগজ-কলম না থাকাটাই সুবিধা ছিল — এড়ানো যায় এমন সব জটিলতা এড়াতে বাধ্য হতে হয়। প্রকাশ করেন তিন বছর পরে, ১৯৫৯ সালে, Numerische Mathematik পত্রিকায় তিন পৃষ্ঠার “A note on two problems in connexion with graphs” প্রবন্ধে — যার অন্য সমস্যাটি ছিল minimum spanning tree (পর্ব ৪৫)। Dijkstra ১৯৭২ সালে Turing Award পান।
৪ · কেন ঠিক কাজ করে — প্রমাণ, আর ঋণাত্মক edge-এ ব্যর্থতা
দাবি (invariant): সব edge-এর ওজন ≥ 0 হলে, কোনো vertex u যখন settled হয়, তখন dist[u] = δ(s, u), আর পরে আর কখনো বদলায় না। প্রমাণটি proof by contradiction — এটি বিশ্ববিদ্যালয়, চাকরি আর ইন্টারভিউর পরীক্ষায় প্রায়ই লিখতে হয়, তাই ধাপগুলো ভালো করে দেখুন।
- ধরে নিই দাবি মিথ্যা. তাহলে এমন একটি প্রথম vertex u আছে, যে settle হওয়ার মুহূর্তে dist[u] > δ(s, u). u ≠ s, কারণ dist[s] = 0 = δ(s, s). আর u-তে কোনো পথ আছে (নইলে δ = ∞ = dist).
- একটি আসল সবচেয়ে ছোট পথ P নিন, s থেকে u পর্যন্ত। P শুরু হয় settled দল থেকে (s) আর শেষ হয় বাইরে (u এখনো settled নয়)। তাই P-তে একটি প্রথম vertex y আছে যে settled নয়; তার ঠিক আগের vertex x settled.
- y-এর অনুমান ঠিক. x আগে settle হয়েছে, আর u-ই প্রথম ভুল — তাই dist[x] = δ(s, x). x settle হওয়ার সময় edge (x, y) relax হয়েছিল, তাই dist[y] ≤ δ(s, x) + w(x, y) = δ(s, y) (সবচেয়ে ছোট পথের যেকোনো অংশও সবচেয়ে ছোট পথ)। ওপরের সীমার নিয়মে dist[y] ≥ δ(s, y), তাই dist[y] = δ(s, y).
- এখানেই লাগে “ওজন ≥ 0”. y পথ P-তে u-এর আগে, আর y থেকে u পর্যন্ত বাকি অংশের ওজন ≥ 0. তাই δ(s, y) ≤ δ(s, u).
- বিরোধ. মিলিয়ে: dist[y] = δ(s, y) ≤ δ(s, u) < dist[u]. অর্থাৎ y-এর dist u-এর চেয়ে ছোট — তাহলে অ্যালগরিদম u নয়, y-কেই বাছাই করত। কিন্তু সে বাছাই করেছে u-কে। বিরোধ। তাই দাবি সত্য। ∎
লক্ষ করুন, ধাপ ৪ ছাড়া প্রমাণের আর কোথাও ওজনের চিহ্ন লাগেনি। যদি P-এর শেষ অংশে একটি ঋণাত্মক edge থাকে, তাহলে δ(s, y) > δ(s, u) হতে পারে — “দূরের” y থেকে ঘুরে এসে u-তে আরও কম খরচে পৌঁছানো যায়। তখন u-কে আগেভাগে চূড়ান্ত করাটাই ভুল। একটি counterexample দেখুন। এবার ওজন মানে টাকা: একটি রাস্তায় টোলের বদলে ঋণাত্মক −3 — ধরুন সেই রাস্তায় গেলে একটি পেমেন্ট অ্যাপ ৩ টাকা ক্যাশব্যাক দেয়। Directed গ্রাফ: S→A 2, S→B 4, B→A −3, A→T 1.
2 1
S ------> A ------> T
\ ^
4 \ | -3
v |
----> B
| dist[S], dist[A], dist[B], dist[T] | |
| settled | |
| এই ধাপে |
- শুরু. dist[S] = 0, বাকি ∞.
- S settle. A = 2, B = 4. সবচেয়ে ছোট এখন A.
- A settle (2). অ্যালগরিদম ধরে নিল A-র দূরত্ব চূড়ান্ত — কারণ “বাকি সব পথ অন্তত 4 দিয়ে শুরু, তারা 2-এর নিচে নামতে পারবে না”. এই যুক্তি শুধু ওজন ≥ 0 হলেই সত্য। A → T relax: T = 3.
- T settle (3). T-এর কোনো বের হওয়া edge নেই।
- B settle (4). B → A: 4 + (−3) = 1 < 2! কিন্তু A আগেই settled, তার edge আর relax হবে না — আর T-এর 3 যে A-র পুরোনো 2-এর ওপর দাঁড়িয়ে, তা কেউ ঠিক করে না। ফল: A = 2, T = 3.
- আসল উত্তর. S→B→A = 4 − 3 = 1, S→B→A→T = 2. Dijkstra দুটোতেই ভুল। “দূরের” B দিয়ে ঘুরে আসাই সস্তা — ঠিক যা প্রমাণের ধাপ ৪ নিষেধ করেছিল।
এবার কোডে, পাশাপাশি পর্ব ৪৪-এর Bellman-Ford দিয়ে — যে সব edge V − 1 বার relax করে, তাই relax-এর ক্রমের ওপর নির্ভর করে না:
#include <cstdio>
#include <vector>
using namespace std;
struct Edge { int u, v, w; };
int main() {
const int n = 4, INF = 1'000'000'000;
const char* nm = "SABT"; // 0 = S, 1 = A, 2 = B, 3 = T
const vector<Edge> es{{0, 1, 2}, {0, 2, 4}, {2, 1, -3}, {1, 3, 1}};
// textbook O(V^2) Dijkstra: settle the closest unsettled vertex and never touch it again
vector<int> d(n, INF);
vector<bool> done(n, false);
d[0] = 0;
for (int it = 0; it < n; ++it) {
int u = -1;
for (int x = 0; x < n; ++x)
if (!done[x] && (u == -1 || d[x] < d[u])) u = x;
done[u] = true;
for (const Edge& e : es) // tiny graph: just scan every edge
if (e.u == u && !done[e.v] && d[u] + e.w < d[e.v]) d[e.v] = d[u] + e.w;
}
// Bellman-Ford: relax every edge V-1 times (correct with negative edges, no negative cycle)
vector<int> b(n, INF);
b[0] = 0;
for (int it = 0; it < n - 1; ++it)
for (const Edge& e : es)
if (b[e.u] != INF && b[e.u] + e.w < b[e.v]) b[e.v] = b[e.u] + e.w;
for (int v = 0; v < n; ++v) printf("%c: dijkstra=%d bellman-ford=%d\n", nm[v], d[v], b[v]);
}আউটপুট:
S: dijkstra=0 bellman-ford=0
A: dijkstra=2 bellman-ford=1
B: dijkstra=4 bellman-ford=4
T: dijkstra=3 bellman-ford=2
A আর T-তে দুই অ্যালগরিদম আলাদা উত্তর দিল — Dijkstra-র 2 আর 3 ভুল, Bellman-Ford-এর 1 আর 2 ঠিক। Dijkstra-র অংশটি pseudocode-এর Version 1: প্রতিবার সব অনিষ্পন্ন vertex দেখে সবচেয়ে ছোটটি নেয় (O(V) খোঁজা, V বার — O(V²)), আর !done[e.v] শর্তের কারণে settled vertex-এর dist আর ছোঁয় না। ছোট গ্রাফ বলে এখানে প্রতিবার সব edge দেখা হয়েছে; আসল কোডে adjacency list থাকবে। INF 109: এর সাথে ছোট ওজন যোগ করলেও int-এর সীমা (প্রায় 2.1 × 109) ছাড়ায় না, কিন্তু বড় ওজনে overflow এড়াতে long long নিন, আর Bellman-Ford-এর মতো != INF যাচাই রাখুন।
INF = float("inf")
nm = "SABT" # 0 = S, 1 = A, 2 = B, 3 = T
n = 4
es = [(0, 1, 2), (0, 2, 4), (2, 1, -3), (1, 3, 1)]
# textbook O(V^2) Dijkstra: settle the closest unsettled vertex and never touch it again
d, done = [INF] * n, [False] * n
d[0] = 0
for _ in range(n):
u = min((x for x in range(n) if not done[x]), key=lambda x: d[x])
done[u] = True
for a, v, w in es:
if a == u and not done[v] and d[u] + w < d[v]:
d[v] = d[u] + w
# Bellman-Ford: relax every edge V-1 times
b = [INF] * n
b[0] = 0
for _ in range(n - 1):
for a, v, w in es:
if b[a] + w < b[v]:
b[v] = b[a] + w
for v in range(n):
print(f"{nm[v]}: dijkstra={d[v]} bellman-ford={b[v]}")এই সংস্করণটি এখানে চালানোর বোতাম ছাড়া দেওয়া হয়েছে; নিজের কম্পিউটারে চালালে আউটপুট C++-এর সাথে হুবহু এক। Python-এ float("inf")-এর সাথে যেকোনো সংখ্যা যোগ করলেও ∞ থাকে, তাই overflow-এর চিন্তা নেই, আর Bellman-Ford-এ আলাদা INF যাচাই লাগেনি। min(..., key=...) অনিষ্পন্ন vertex-গুলোর মধ্যে সবচেয়ে ছোট dist-ওয়ালাটি দেয় — O(V).
দুটি সাধারণ ভুল ধারণা। (ক) “সব ওজনে একটি ধ্রুবক যোগ করে ঋণাত্মক দূর করলেই হবে” — না। বেশি edge-এর পথে বেশিবার ধ্রুবক যোগ হয়, তাই পথগুলোর ক্রম বদলে যায় (অনুশীলন ২)। সঠিক উপায় হলো প্রতিটি vertex-এ একটি “potential” ধরে ওজন বদলানো — Johnson-এর পদ্ধতি, পর্ব ৪৪. (খ) কনটেস্টের lazy-heap সংস্করণে কোনো done[] থাকে না; ঋণাত্মক edge থাকলে (কিন্তু ঋণাত্মক cycle না থাকলে) সে একটি vertex-কে বারবার “আবার settle” করে শেষে ঠিক উত্তরই দেয় — কিন্তু তখন খারাপ ইনপুটে সময় exponential হতে পারে। O((V + E) log V)-এর নিশ্চয়তা শুধু ওজন ≥ 0 হলেই।
৫ · বাস্তবায়ন — O(V²) অ্যারে, নাকি heap আর lazy deletion?
অ্যালগরিদম একটাই, খরচ নির্ভর করে “সবচেয়ে ছোট dist” কীভাবে খুঁজছি তার ওপর। গ্রাফ রাখা হয় পর্ব ৩৯-এর adjacency list-এ: প্রতিটি u-এর জন্য (v, w) জোড়ার তালিকা।
| Priority queue | সবচেয়ে ছোট খোঁজা | dist কমানো | মোট সময় | কখন ভালো |
|---|---|---|---|---|
| সাধারণ অ্যারে (Version 1) | O(V), V বার | O(1), E বার | O(V² + E) = O(V²) | ঘন গ্রাফ, E ≈ V² |
| Binary heap + lazy deletion (Version 2) | O(log V), সর্বোচ্চ E + 1 বার | push O(log V), সর্বোচ্চ E বার | O((V + E) log V) | পাতলা গ্রাফ — রাস্তা, কনটেস্ট |
| Indexed heap + decrease-key | O(log V), V বার | O(log V), E বার | O((V + E) log V) | জায়গা O(V) রাখতে হলে |
| Fibonacci heap | amortized O(log V), V বার | amortized O(1), E বার | O(E + V log V) | তত্ত্বে; বাস্তবে ধ্রুবক বড় |
অ্যারে সংস্করণের হিসাব. V বার সবচেয়ে ছোট খোঁজা, প্রতিবার V টি ঘর দেখা — V². প্রতিটি vertex একবার settle হয়, তাই প্রতিটি edge একবার (undirected-এ দুই দিকে দুবার) relax — O(E). মোট O(V² + E), আর E ≤ V² বলে O(V²). বাড়তি জায়গা O(V). যখন প্রায় প্রতিটি জোড়ার মধ্যে edge আছে (E ≈ V²), তখন এটিই সেরা — heap সংস্করণ সেখানে O(V² log V).
Heap সংস্করণের হিসাব. প্রতিটি সফল relax একটি push. প্রতিটি vertex একবারই (প্রথম, অ-বাসি pop-এ) তার edge-গুলো relax করে, তাই সফল relax সর্বোচ্চ E টি, আর শুরুর জোড়াসহ push সর্বোচ্চ E + 1. যত push, ততই সর্বোচ্চ pop. Heap-এ কখনো E + 1-এর বেশি জোড়া থাকে না, তাই প্রতিটি কাজ O(log E) — আর E ≤ V² বলে log E ≤ 2 log V, অর্থাৎ O(log V). মোট O((V + E) log V), জায়গা O(V + E). রাস্তার মানচিত্রের মতো গ্রাফে E মাত্র V-এর কয়েক গুণ, তাই এটি প্রায় O(V log V) — দশ লাখ মোড়েও C++-এ সাধারণত এক সেকেন্ডের কম।
Decrease-key বনাম lazy deletion. পর্ব ২৩-এর indexed heap (pos[] অ্যারেসহ) ভেতরের জোড়ার key সরাসরি কমাতে পারে, তাই heap-এ কখনো V-এর বেশি জোড়া থাকে না। কিন্তু std::priority_queue বা Python heapq-এ decrease-key নেই। Lazy deletion-এ কোড চার লাইনেই শেষ, একই Big-O, শুধু heap বড় হয় (V-এর বদলে E) — এ কারণেই এটি কনটেস্টের মানক ছাঁচ। এবার পুরো প্রোগ্রাম, পথ পুনর্গঠনসহ:
#include <cstdio>
#include <functional>
#include <queue>
#include <string>
#include <utility>
#include <vector>
using namespace std;
int main() {
const vector<string> name{"Dhaka", "Narsingdi", "Cumilla", "Chandpur", "Feni", "Chattogram"};
const int n = (int)name.size();
vector<vector<pair<int, int>>> adj(n); // adj[u] = {(v, minutes)}
auto road = [&](int u, int v, int w) { adj[u].push_back({v, w}); adj[v].push_back({u, w}); };
road(0, 1, 60); road(0, 2, 200); road(0, 3, 150); road(1, 2, 90);
road(2, 3, 70); road(2, 4, 60); road(3, 4, 100); road(4, 5, 110);
const long long INF = 1LL << 60;
vector<long long> dist(n, INF);
vector<int> par(n, -1); // par[v] = previous city on the best path
priority_queue<pair<long long, int>, vector<pair<long long, int>>, greater<>> pq; // MIN-heap
dist[0] = 0; pq.push({0, 0});
while (!pq.empty()) {
auto [d, u] = pq.top(); pq.pop();
if (d != dist[u]) { printf("skip stale (%lld, %s)\n", d, name[u].c_str()); continue; }
printf("settle %-10s dist=%lld\n", name[u].c_str(), d);
for (auto [v, w] : adj[u])
if (d + w < dist[v]) { dist[v] = d + w; par[v] = u; pq.push({dist[v], v}); } // relax
}
vector<int> path; // walk back from the target, then reverse
for (int v = 5; v != -1; v = par[v]) path.push_back(v);
printf("Dhaka -> Chattogram: %lld min via", dist[5]);
for (int i = (int)path.size() - 1; i >= 0; --i) printf(" %s", name[path[i]].c_str());
printf("\n");
}আউটপুট:
settle Dhaka dist=0
settle Narsingdi dist=60
settle Cumilla dist=150
settle Chandpur dist=150
skip stale (200, Cumilla)
settle Feni dist=210
settle Chattogram dist=320
Dhaka -> Chattogram: 320 min via Dhaka Narsingdi Cumilla Feni Chattogram
প্রতিটি লাইন অংশ ৩-এর ডায়াগ্রামের এক-একটি ধাপ — settle-এর ক্রম, dist আর একমাত্র বাসি জোড়া (200, Cumilla). greater<> দিয়ে min-heap: priority_queue ডিফল্টে max-heap, এটি ভুলে গেলে সবচেয়ে বড় dist আগে বের হয় — উত্তর তবু ঠিক আসতে পারে, কিন্তু একই vertex বারবার প্রক্রিয়া হয়ে সময় বিস্ফোরিত হয়। জোড়ায় dist আগে, vertex পরে — কারণ pair প্রথম উপাদান দিয়ে তুলনা হয়। auto [d, u] = pq.top() একটি কপি নেয়; auto& লিখে reference নিলে pop()-এর পর সেটি ঝুলে থাকত। dist long long, কারণ অনেক edge-এর যোগফল int ছাড়াতে পারে; INF = 260-এর সাথে কখনো যোগ হয় না, কারণ শুধু settled (সসীম) d থেকেই relax হয়।
import heapq
name = ["Dhaka", "Narsingdi", "Cumilla", "Chandpur", "Feni", "Chattogram"]
n = len(name)
adj = [[] for _ in range(n)] # adj[u] = [(v, minutes)]
for u, v, w in [(0, 1, 60), (0, 2, 200), (0, 3, 150), (1, 2, 90),
(2, 3, 70), (2, 4, 60), (3, 4, 100), (4, 5, 110)]:
adj[u].append((v, w))
adj[v].append((u, w))
INF = float("inf")
dist, par = [INF] * n, [-1] * n # par[v] = previous city on the best path
dist[0] = 0
pq = [(0, 0)] # heapq is a MIN-heap of (dist, vertex)
while pq:
d, u = heapq.heappop(pq)
if d != dist[u]:
print(f"skip stale ({d}, {name[u]})")
continue
print(f"settle {name[u]:<10} dist={d}")
for v, w in adj[u]:
if d + w < dist[v]: # relax
dist[v], par[v] = d + w, u
heapq.heappush(pq, (dist[v], v))
path, v = [], 5 # walk back from the target, then reverse
while v != -1:
path.append(name[v])
v = par[v]
print(f"Dhaka -> Chattogram: {dist[5]} min via", " ".join(reversed(path)))আউটপুট C++-এর সাথে হুবহু এক:
settle Dhaka dist=0
settle Narsingdi dist=60
settle Cumilla dist=150
settle Chandpur dist=150
skip stale (200, Cumilla)
settle Feni dist=210
settle Chattogram dist=320
Dhaka -> Chattogram: 320 min via Dhaka Narsingdi Cumilla Feni Chattogram
heapq min-heap, আর tuple বাঁ থেকে তুলনা হয় — তাই (dist, vertex) জোড়া সরাসরি চলে, সমান dist-এ ছোট vertex নম্বর আগে, ঠিক C++-এর মতো। float("inf") শুধু তুলনায় ব্যবহার হয়; উৎস থেকে পৌঁছানো যায় না এমন vertex-এর dist শেষে inf থাকবে — ছাপার আগে আলাদা করে দেখুন। বড় গ্রাফে (লাখো edge) Python-এ ইনপুট পড়তে sys.stdin আর প্রতিটি লুপে লোকাল নাম ব্যবহার করলে গতি অনেক বাড়ে।
আরও গভীরে: Fredman ও Tarjan-এর Fibonacci heap-এ decrease-key amortized O(1), তাই Dijkstra নামে O(E + V log V)-এ — তুলনা-ভিত্তিক মডেলে এটি দীর্ঘদিন সেরা সীমা ছিল, কারণ Dijkstra দূরত্বের ক্রমে vertex বের করে, যা প্রায় সর্টিংয়ের সমান কঠিন (পর্ব ২৪)। ওজন ছোট পূর্ণসংখ্যা (≤ C) হলে bucket দিয়ে Dial-এর পদ্ধতি O(E + V·C), আর ওজন শুধু 0 বা 1 হলে deque দিয়ে 0-1 BFS O(V + E) (অংশ ৭)। ১৯৯৯ সালে Mikkel Thorup দেখান, ধনাত্মক পূর্ণসংখ্যার ওজনের undirected গ্রাফে SSSP O(V + E)-এও সম্ভব — যদিও সেই অ্যালগরিদম বাস্তবে প্রায় ব্যবহার হয় না।
৬ · পথ ফিরে পাওয়া, একাধিক উৎস — আর নিজে চালিয়ে দেখুন
Path reconstruction আর shortest path tree
dist শুধু বলে “কত মিনিট”, চালক জানতে চান “কোন রাস্তায়”. প্রতিটি সফল relax-এ আমরা par[v] = u লিখেছি — এই predecessor array-ই পুরো উত্তর। লক্ষ্য t থেকে শুরু করে par ধরে পেছনে হাঁটুন যতক্ষণ না −1 (উৎস) আসে, তারপর তালিকাটি উল্টে দিন — ঠিক কোডের শেষ তিন লাইন। পথে k টি vertex থাকলে সময় O(k). পেছনে হাঁটা কেন ঠিক? কারণ সবচেয়ে ছোট পথের প্রতিটি শুরুর অংশও সবচেয়ে ছোট পথ — Chattogram-এর সেরা পথ Feni হয়ে গেলে, সেই পথের Feni পর্যন্ত অংশটি Feni-র সেরা পথই।
সব v-এর জন্য (v, par[v]) edge-গুলো একসাথে নিলে একটি tree পাওয়া যায়, যার root s: shortest path tree. V − 1 টি edge-এ s থেকে সবার সেরা পথ ধরা থাকে। নিচের visualiser-এ সেটি সবুজ মোটা রেখায় দেখা যাবে। সাবধান: এটি পর্ব ৪৫-এর minimum spanning tree নয় — MST মোট ওজন কমায়, এই tree প্রতিটি vertex-এর s থেকে দূরত্ব কমায়। আমাদের মানচিত্রেই দুটি আলাদা।
Multi-source — সবচেয়ে কাছের ফায়ার স্টেশন
ঢাকার প্রতিটি এলাকা থেকে সবচেয়ে কাছের ফায়ার স্টেশনে পৌঁছাতে কত সময়? প্রতিটি স্টেশন থেকে আলাদা Dijkstra চালালে k গুণ কাজ। কৌশল: সব স্টেশনকে শুরুতেই dist = 0 দিয়ে heap-এ দিন, তারপর স্বাভাবিক Dijkstra চালান। ভাবুন একটি নকল super source আছে, যার থেকে প্রতিটি স্টেশনে 0-ওজনের edge — তার একটি সাধারণ Dijkstra আর এটি হুবহু এক কাজ করে। ফল: প্রতিটি vertex-এর সবচেয়ে কাছের উৎসের দূরত্ব, এক বারেই O((V + E) log V)-এ। par ধরে পেছনে গেলে বোঝা যায় কোন স্টেশন সবচেয়ে কাছে। এটিই multi-source shortest path.
নিজে চালিয়ে দেখুন — Dijkstra visualiser
“চালিয়ে দেখুন” চাপুন। প্রোগ্রাম প্রথমে নিজে পুরোটা চালিয়ে শেষ অবস্থা দেখাবে। তারপর আবার শুরু চেপে পরের ধাপ দিয়ে একটি একটি pop দেখুন, বা ওপরের তালিকা থেকে অন্য উৎস বেছে নিন। রং: লাল = এইমাত্র settled, সবুজ = settled, হলুদ = dist জানা কিন্তু চূড়ান্ত নয়, ধূসর = ∞. বৃত্তের ভেতরের সংখ্যা dist, সবুজ মোটা রেখা = shortest path tree-র edge.
const city = ["Dhaka", "Narsingdi", "Cumilla", "Chandpur", "Feni", "Chattogram"];
const xy = [[45, 95, 33], [150, 45, -25], [200, 105, 33], [130, 185, 33], [300, 160, 33], [405, 90, -25]]; // x, y, label offset
const roads = [[0, 1, 60], [0, 2, 200], [0, 3, 150], [1, 2, 90], [2, 3, 70], [2, 4, 60], [3, 4, 100], [4, 5, 110]];
let dist, par, done, pq, cur;
const $ = id => document.getElementById(id);
const byKey = (a, b) => a[0] - b[0] || a[1] - b[1]; // smallest (dist, vertex) first
function reset() {
const s = Number($("src").value);
dist = city.map(() => Infinity); par = city.map(() => -1); done = city.map(() => false);
dist[s] = 0; pq = [[0, s]]; cur = -1; draw("start at " + city[s]);
}
function step() { // one pop of the priority queue (a sorted array here)
if (!pq.length) { cur = -1; draw("done: queue is empty"); return false; }
const [d, u] = pq.sort(byKey).shift();
if (d !== dist[u]) { cur = -1; draw(`skip stale (${d}, ${city[u]})`); return true; }
done[u] = true; cur = u;
for (const [a, b, w] of roads) for (const [x, v] of [[a, b], [b, a]])
if (x === u && d + w < dist[v]) { dist[v] = d + w; par[v] = u; pq.push([dist[v], v]); }
draw(`settle ${city[u]} at ${d}`); return true;
}
function draw(msg) {
let g = "";
for (const [a, b, w] of roads) {
const [x1, y1] = xy[a], [x2, y2] = xy[b], on = par[b] === a || par[a] === b;
g += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${on ? "#16a34a" : "#94a3b8"}" stroke-width="${on ? 4 : 2}"/>`;
g += `<text x="${(x1 + x2) / 2}" y="${(y1 + y2) / 2 - 5}" class="w">${w}</text>`;
}
city.forEach((c, i) => {
const f = i === cur ? "#fca5a5" : done[i] ? "#86efac" : dist[i] < Infinity ? "#fde047" : "#e2e8f0";
g += `<circle cx="${xy[i][0]}" cy="${xy[i][1]}" r="18" fill="${f}" stroke="#334155"/>`;
g += `<text x="${xy[i][0]}" y="${xy[i][1] + 4}">${dist[i] === Infinity ? "∞" : dist[i]}</text>`;
g += `<text x="${xy[i][0]}" y="${xy[i][1] + xy[i][2]}" class="n">${c}</text>`;
});
$("g").innerHTML = g;
$("pq").textContent = "PQ: " + (pq.sort(byKey).map(([d, v]) => `(${d}, ${city[v]})`).join(" ") || "empty");
$("msg").textContent = msg; console.log(msg);
}
$("src").onchange = reset; $("bn").onclick = step; $("br").onclick = reset;
$("ba").onclick = () => { while (step()); };
reset(); while (step());শুরুতে কোড নিজে ঢাকা থেকে পুরো অ্যালগরিদম চালায়; console-এ প্রতিটি ধাপ:
start at Dhaka
settle Dhaka at 0
settle Narsingdi at 60
settle Cumilla at 150
settle Chandpur at 150
skip stale (200, Cumilla)
settle Feni at 210
settle Chattogram at 320
done: queue is empty
C++ আর Python প্রোগ্রামের সাথে মেলে — একই ক্রম, একই বাসি জোড়া। এখানে heap-এর বদলে প্রতিবার সাজানো একটি ছোট অ্যারে, কারণ ৬টি শহরে গতির প্রশ্ন নেই আর সাজানো তালিকা দেখতে সুবিধা; অ্যালগরিদমের যুক্তি একই। পরীক্ষা করুন: উৎস Chattogram বেছে নিন — এবার দূরত্বগুলো উল্টো দিক থেকে আসবে, আর shortest path tree-ও বদলে যাবে। আরেকটি পরীক্ষা: কোডে Dhaka–Cumilla-র 200 বদলে 140 করে আবার চালান — এবার সবুজ tree সরাসরি রাস্তাটিই নেবে, আর বাসি জোড়া আর থাকবে না।
৭ · অবস্থা যোগ করা Dijkstra, আর গ্রিডে Dijkstra
State-space Dijkstra — k টি টোল ফ্রি
অনেক সমস্যায় “কোথায় আছি” যথেষ্ট নয়, জানতে হয় “কী অবস্থায় আছি”. ধরুন আপনার কাছে একটি কুপন আছে, যা দিয়ে সর্বোচ্চ k টি রাস্তার খরচ মাফ। এখন একই শহরে দুই অবস্থায় পৌঁছানো আলাদা জিনিস: কুপন বাকি থাকলে সামনের দামি রাস্তা ফ্রি পাবেন, না থাকলে নয়। সমাধান: vertex-কে (শহর, এ পর্যন্ত ব্যবহৃত কুপন j) জোড়া হিসেবে দেখুন. প্রতিটি রাস্তা (u, v, w) থেকে দুই ধরনের edge: (u, j) → (v, j) ওজন w (টাকা দিয়ে), আর j < k হলে (u, j) → (v, j + 1) ওজন 0 (কুপন খরচ করে)। নতুন গ্রাফে সাধারণ Dijkstra, আর উত্তর হলো j-এর সব মানের মধ্যে dist[(t, j)]-এর সবচেয়ে ছোটটি। এটি state-space search-এর একটি উদাহরণ — একে “layered graph”-ও বলে: k + 1 টি তলায় একই মানচিত্রের কপি, আর 0-ওজনের সিঁড়ি দিয়ে এক তলা থেকে পরের তলায়।
S --8--> A --8--> T S --3--> B --12-> T
| pop হলো | |
| নতুন বা কমা dist | |
| priority queue |
- শুরু. অবস্থা (S, 0): S-এ আছি, কোনো কুপন খরচ হয়নি, dist 0.
- (S, 0) pop. প্রতিটি রাস্তায় দুটি বিকল্প: টাকা দিয়ে (j একই) বা কুপন দিয়ে (j বাড়ে, ওজন শূন্য)। তাই A,0 = 8, A,1 = 0, B,0 = 3, B,1 = 0.
- (A, 1) pop. কুপন শেষ (j = k), তাই A → T শুধু টাকা দিয়ে: T,1 = 0 + 8 = 8.
- (B, 1) pop. B → T টাকা দিয়ে 12 — কিন্তু T,1-এর জন্য 8 আগেই জানা, বদল নেই।
- (B, 0) pop. কুপন বাকি! B → T টাকা দিয়ে: T,0 = 15; কুপন দিয়ে: T,1 = 3 + 0 = 3 < 8 — কমে গেল। পুরোনো (8, T,1) এখন বাসি।
- (T, 1) pop — উত্তর 3. প্রথম যে T-অবস্থা বের হয়, তার dist-ই সব j-এর মধ্যে সবচেয়ে ছোট, তাই এখানেই থামা যায়। সেরা পরিকল্পনা: S → B টাকা দিয়ে (3), দামি B → T কুপনে। কুপন না থাকলে উত্তর হতো 15 (S → B → T); আর “কুপন ছাড়া সবচেয়ে সস্তা পথ বের করে তার সবচেয়ে দামি রাস্তাটি মাফ” পদ্ধতি এখানে কাকতালীয়ভাবে 15 − 12 = 3 দেয়, কিন্তু সাধারণভাবে ভুল: একটি পথ 1 + 1 + 1 = 3 আর অন্যটি 10 + 1 = 11 হলে সেই পদ্ধতি দেয় 2, অথচ দ্বিতীয় পথে 10 মাফ করলে 1.
খরচ: অবস্থা V(k + 1) টি, edge প্রায় 2E(k + 1) টি, তাই সময় O(k (V + E) log(kV)), জায়গা O(k(V + E)) — যদিও কোডে নতুন গ্রাফ বানাতে হয় না, শুধু dist[v][j] 2D অ্যারে আর heap-এ (d, v, j) তিনটি জিনিস। একই ছাঁচে আসে “জ্বালানি কতটা আছে”, “কোন চাবিগুলো হাতে” (bitmask, পর্ব ৫৫), “শেষ কোন দিকে চলছিলাম” — অবস্থার সংখ্যা ছোট রাখাই আসল নকশা।
গ্রিডে Dijkstra
একটি R × C মানচিত্রে প্রতিটি ঘরে ঢুকতে আলাদা খরচ — কাদা, পানি, পাকা রাস্তা। এখানে আলাদা করে গ্রাফ বানানোর দরকার নেই: প্রতিটি ঘর (r, c) একটি vertex, আর চার প্রতিবেশী (ওপর, নিচ, বাঁ, ডান) তার edge, ওজন = প্রতিবেশী ঘরে ঢোকার খরচ। এই grid graph-এ V = RC, E ≤ 4RC, তাই সময় O(RC log(RC)). কোডে প্রতিবেশী বের হয় dr[] = {-1, 1, 0, 0}, dc[] = {0, 0, -1, 1} অ্যারে দিয়ে, আর গ্রিডের বাইরে যাওয়া আটকাতে সীমা যাচাই। ওজন যদি শুধু 0 আর 1 হয় (যেমন “দেয়াল ভাঙলে 1, খোলা পথে 0”), তাহলে heap-এর দরকারই নেই: পর্ব ১৪-এর deque-এ 0-ওজনের প্রতিবেশী সামনে, 1-ওজনেরটি পেছনে push — এই 0-1 BFS O(V + E)-এ একই উত্তর দেয়, কারণ deque-টি সবসময় dist-এর ক্রমে সাজানো থাকে, আর তাতে কখনো দুটির বেশি আলাদা dist থাকে না।
৮ · A* search — লক্ষ্যের দিকে ঝুঁকে খোঁজা
Dijkstra-র ঢেউ সব দিকে সমানভাবে ছড়ায়। ঢাকা থেকে চট্টগ্রাম খুঁজতে গিয়ে সে উল্টো দিকের রাজশাহীর রাস্তাগুলোও ঘাঁটে — যতক্ষণ না ঢেউয়ের ব্যাসার্ধ চট্টগ্রামের দূরত্বে পৌঁছায়। একটি নির্দিষ্ট লক্ষ্য t থাকলে আমরা কি ঢেউকে t-এর দিকে ঠেলতে পারি? A* search (উচ্চারণ “এ-স্টার”) ঠিক তা করে। প্রতিটি vertex-এর জন্য একটি অনুমান h(v) ধরুন — “v থেকে t আর কতটা বাকি” — যাকে বলে heuristic. তারপর heap-এর key হিসেবে dist[v]-এর বদলে নিন f(v) = dist[v] + h(v): “এ পর্যন্ত খরচ + বাকি পথের অনুমান”. h(v) = 0 হলে এটি হুবহু Dijkstra.
- Admissible: h(v) কখনো আসল বাকি দূরত্ব δ(v, t)-এর বেশি নয় — অনুমানটি আশাবাদী। তাহলে A* সবসময় সঠিক সবচেয়ে ছোট পথ দেয়, আর t যখন heap থেকে বের হয় তখনই থামা যায়। রাস্তার মানচিত্রে দুই বিন্দুর সোজা-রেখার দূরত্ব admissible — কোনো রাস্তা সোজা রেখার চেয়ে ছোট হতে পারে না (সময় হলে সোজা দূরত্ব ÷ সর্বোচ্চ গতি)।
- Consistent: প্রতিটি edge-এ h(u) ≤ w(u, v) + h(v), আর h(t) = 0. Consistent হলে admissible-ও হয়, আর প্রতিটি vertex একবারই settle হয়। কারণটি সুন্দর: নতুন ওজন w′(u, v) = w(u, v) − h(u) + h(v) ধরলে সবগুলো ≥ 0, আর যেকোনো s–t পথের ওজন ঠিক h(t) − h(s) পরিমাণ বদলায় — সব পথে একই পরিমাণ। তাই A* আসলে এই নতুন ওজনে সাধারণ Dijkstra, আর অংশ ৪-এর প্রমাণ হুবহু খাটে।
- গ্রিডে: চার দিকে চলা আর প্রতি ধাপের খরচ অন্তত 1 হলে Manhattan distance |r − rt| + |c − ct| consistent. h যত আসল দূরত্বের কাছাকাছি, A* তত কম vertex ঘাঁটে; h = δ হলে (সমান f-এর জোড়ায় সমতা ঠিকভাবে ভাঙলে) সে প্রায় শুধু সেরা পথটিই হাঁটে।
Admissible নয় এমন h (যেমন সোজা দূরত্বের দ্বিগুণ) নিলে A* দ্রুত চলে কিন্তু উত্তর সবচেয়ে ছোট না-ও হতে পারে — গেমের চরিত্রের পথ খোঁজায় কখনো কখনো ইচ্ছে করেই এটি করা হয়। A* প্রকাশ করেন Peter Hart, Nils Nilsson আর Bertram Raphael, ১৯৬৮ সালে, Stanford Research Institute-এর Shakey নামের রোবটের পথ খোঁজার কাজ থেকে। কৃত্রিম বুদ্ধিমত্তার পাঠ্যবইয়ে আজও এটি অন্যতম কেন্দ্রীয় অ্যালগরিদম, আর ভিডিও গেমে পথ খোঁজার সবচেয়ে প্রচলিত পদ্ধতি।
পরীক্ষা ও ইন্টারভিউতে:
- Dijkstra-র time complexity কত? — অ্যারে দিয়ে O(V²); binary heap দিয়ে O((V + E) log V); Fibonacci heap দিয়ে O(E + V log V).
- ঋণাত্মক edge-এ Dijkstra কেন ভুল? — settle হওয়া vertex-এর দূরত্ব পরে কোনো ঋণাত্মক edge দিয়ে কমতে পারে; প্রমাণের δ(s, y) ≤ δ(s, u) ধাপটি ভাঙে। তখন Bellman-Ford.
- Unweighted গ্রাফে shortest path? — BFS, O(V + E); ওজন 0/1 হলে 0-1 BFS.
- Lazy deletion-এ heap-এর আকার কত হতে পারে? — সর্বোচ্চ E + 1; তবু প্রতিটি কাজ O(log V), কারণ log E ≤ 2 log V.
- সবচেয়ে কাছের হাসপাতাল প্রতিটি এলাকা থেকে? — সব হাসপাতালকে dist 0 দিয়ে একসাথে heap-এ — একটিমাত্র multi-source Dijkstra.
৯ · AI-এর যুগে Dijkstra — কোড চলে, কিন্তু কি ঠিক?
“Dijkstra লিখে দাও” — যেকোনো AI কয়েক সেকেন্ডে লিখবে, আর ছোট উদাহরণে সঠিক উত্তরও দেবে। সমস্যা লুকোয় সূক্ষ্ম জায়গায়: visited চিহ্ন push-এর সময় দেওয়া, C++-এ max-heap রেখে দেওয়া, বাসি জোড়া বাদ না দেওয়া, বা ঋণাত্মক ওজনের ইনপুটে আত্মবিশ্বাসের সাথে Dijkstra চালানো। এর অনেকগুলোতে উত্তর ঠিক আসে, শুধু বড় ইনপুটে সময়সীমা পেরোয় — যা ছোট পরীক্ষায় কখনো ধরা পড়ে না।
Dijkstra-র কোডে AI সাধারণত যেসব ভুল করে
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
| vertex-কে push করার সময়ই visited ধরা (BFS-এর অভ্যাস) | অংশ ৩-এর মানচিত্রে চালান: Cumilla 200-তেই আটকে যাবে, উত্তর 320-এর বদলে 360 |
priority_queue-এ greater ছাড়া (max-heap), বা dist-কে ঋণাত্মক না করে push | দীর্ঘ একটি শিকল-গ্রাফে pop গুনুন — V-এর অনেক গুণ হলে ভুল |
d != dist[u] বা d > dist[u] যাচাই বাদ | উত্তর ঠিক থাকে, কিন্তু ঘন গ্রাফে সময় বাড়ে; settle-এর সংখ্যা ছাপুন — V-এর বেশি হলে যাচাই নেই |
int dist, আর INF + w যোগ | বড় ওজন (109) আর লম্বা পথ দিয়ে চালান; ঋণাত্মক dist দেখলেই overflow |
| ঋণাত্মক ওজনেও Dijkstra, বা “longest path”-এ ওজন উল্টে Dijkstra | অংশ ৪-এর চার vertex-এর গ্রাফ দিন; Bellman-Ford বা brute force-এর সাথে মেলান |
| complexity “O(V log V)” বা “O(E log E), তাই O(V²)”-এর মতো এলোমেলো দাবি | push আর pop কতবার হয়, ধাপে ধাপে গুনতে বলুন |
AI-কে কোড লিখে দিতে না বলে, আপনার কোড আর trace যাচাই করতে বলুন — আর প্রতিটি দাবির কারণ চান:
Dijkstra-র কোড যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১ক্যাফেতে বিশ মিনিটে ভাবা একটি অ্যালগরিদম কেন ছয় দশক পরেও প্রতিটি ম্যাপ, রাউটার আর পাঠ্যবইয়ে?
১৯৫৬ সালের নেদারল্যান্ডসে কম্পিউটার ছিল হাতে গোনা। আমস্টারডামের Mathematical Centre-এ Edsger Dijkstra-র দায়িত্ব ছিল নতুন ARMAC মেশিনটি জনসাধারণের সামনে দেখানো। তাঁর চিন্তা ছিল ব্যবহারিক: এমন সমস্যা চাই, যার প্রশ্ন আর উত্তর দুটোই কম্পিউটার না-জানা মানুষ বোঝেন। দুটি শহরের মধ্যে সবচেয়ে ছোট পথ ঠিক তেমন — আর তিনি নিজের স্মৃতিচারণে বলেছেন, মেশিনের ছোট মেমরিতে আঁটানোর জন্য নেদারল্যান্ডসের শহরগুলোর একটি সরল মানচিত্র ব্যবহার করেছিলেন।
তাঁর নিজের বর্ণনা অনুযায়ী, অ্যালগরিদমটি এসেছিল এক সকালে, ক্যাফের বারান্দায় কফির টেবিলে, প্রায় বিশ মিনিটে, কাগজ-কলম ছাড়া। পরে তিনি বলেছেন, কাগজ-কলম না থাকায় এড়ানো যায় এমন সব জটিলতা এড়াতে বাধ্য হয়েছিলেন — আর এ কারণেই অ্যালগরিদমটি এত সরল। প্রকাশ করেন তিন বছর পরে, ১৯৫৯ সালে। Numerische Mathematik-এর মাত্র তিন পৃষ্ঠার সেই প্রবন্ধে দুটি সমস্যা ছিল: সব vertex জোড়া দেওয়ার সবচেয়ে ছোট মোট দৈর্ঘ্যের tree (আজকের MST, পর্ব ৪৫), আর দুটি vertex-এর মধ্যে সবচেয়ে ছোট পথ। মূল বর্ণনায় কোনো heap ছিল না — প্রতিবার সব প্রার্থী দেখে সবচেয়ে ছোটটি খোঁজা, অর্থাৎ এই পর্বের O(V²) সংস্করণ।
দীর্ঘায়ুর কারণ তিনটি। প্রথমত, ধারণাটি একটি গভীর সত্য ধরে — ঋণাত্মক নয় এমন ওজনে দূরত্বের ক্রমে vertex চূড়ান্ত করা যায়, তাই প্রতিটি edge একবার দেখলেই চলে। দ্বিতীয়ত, এটি প্রসারণযোগ্য: priority queue বদলে গতি বদলায় (heap, Fibonacci heap), heuristic যোগ করলে A*, অবস্থা যোগ করলে state-space search — মূল কাঠামো একই থাকে। তৃতীয়ত, বাস্তব সমস্যায় ঋণাত্মক ওজন খুব কম আসে — সময়, দূরত্ব, দেরি, খরচ প্রায় সবসময় ধনাত্মক। Dijkstra পরে structured programming, semaphore আর “Go To Statement Considered Harmful” চিঠির জন্যও বিখ্যাত হন, আর ১৯৭২ সালে Turing Award পান; কিন্তু সাধারণ প্রোগ্রামারের কাছে তাঁর নাম মানেই এই অ্যালগরিদম।
মূল উপলব্ধি: সেরা অ্যালগরিদম প্রায়ই সবচেয়ে সরল — একটি পরিষ্কার invariant থেকে জন্মানো। জটিলতা এড়ানোর চেষ্টাই এখানে আবিষ্কারের চাবি ছিল।
প্র ০২Dijkstra কি সবচেয়ে দ্রুত সম্ভব? “সর্টিংয়ের দেয়াল” কী, আর সাম্প্রতিক গবেষণা কী বলছে?
Binary heap দিয়ে O((V + E) log V), Fibonacci heap দিয়ে O(E + V log V). প্রশ্ন হলো, V log V অংশটি কি এড়ানো যায়? একটি যুক্তি বলে “সহজে নয়”: Dijkstra vertex-গুলোকে দূরত্বের ক্রমে settle করে — অর্থাৎ পাশাপাশি একটি সর্টিংও করে ফেলে। একটি তারকা-আকৃতির গ্রাফ নিন, যেখানে s থেকে প্রতিটি vertex-এ একটি edge, ওজন = সাজানোর সংখ্যাগুলো। Dijkstra-র settle-ক্রম তখন সেই সংখ্যাগুলোর সাজানো ক্রম। তুলনা-ভিত্তিক সর্টিংয়ের নিম্নসীমা Ω(n log n) (পর্ব ২৪), তাই যে অ্যালগরিদম দূরত্বের ক্রমও দেয়, তাকে তুলনার মডেলে এই দাম দিতেই হবে। একে বলা হয় “sorting barrier”.
কিন্তু SSSP-র উত্তর শুধু দূরত্বগুলো, ক্রম নয়। তাই দেয়ালটি আসলে Dijkstra-র পদ্ধতির, সমস্যার নয়। ১৯৯৯ সালে Mikkel Thorup দেখান যে ধনাত্মক পূর্ণসংখ্যার ওজনের undirected গ্রাফে SSSP linear সময়ে করা যায় — তিনি পুরো ক্রম না বানিয়ে গ্রাফটিকে একটি স্তরভিত্তিক কাঠামোয় ভাগ করেন। তবে সেটি পূর্ণসংখ্যার বিশেষ সুবিধা কাজে লাগায়। সাধারণ বাস্তব-সংখ্যার ওজন আর directed গ্রাফে বহু দশক ধরে কেউ Dijkstra + Fibonacci heap-কে হারাতে পারেননি।
২০২৪ সালে কয়েকজন গবেষক (Robert Tarjan-সহ) প্রমাণ করেন, উপযুক্ত heap দিয়ে Dijkstra একটি শক্তিশালী অর্থে “universally optimal” — যদি দূরত্বের ক্রমও আউটপুট হিসেবে চাওয়া হয়, তাহলে প্রতিটি গ্রাফ-আকৃতির জন্য এটি সেরা সম্ভব অ্যালগরিদমের সমান। আর ২০২৫ সালে আরেকটি দল directed গ্রাফে প্রথমবার এই দেয়াল ভাঙার একটি নির্ধারিত (deterministic) অ্যালগরিদম দেখান, যার সময় m log2/3 n ধাঁচের — m edge, n vertex. মূল কৌশল হলো Dijkstra আর Bellman-Ford-এর মিশ্রণ: কিছু অংশে কয়েক ধাপ Bellman-Ford চালিয়ে “প্রার্থী” vertex-এর সংখ্যা কমানো, যাতে পুরো ক্রম সাজাতে না হয়।
বাস্তবে এর প্রভাব এখনো তাত্ত্বিক: ধ্রুবক বড়, আর heap-ভিত্তিক Dijkstra যথেষ্ট দ্রুত। কিন্তু এটি দেখায় যে ছয় দশকের পুরোনো একটি প্রশ্নেও গবেষণা থেমে নেই।
মূল উপলব্ধি: একটি নিম্নসীমা সবসময় একটি নির্দিষ্ট মডেল আর নির্দিষ্ট আউটপুটের জন্য। কী চাওয়া হচ্ছে (শুধু দূরত্ব, নাকি ক্রমও) তা বদলালেই দেয়ালের জায়গা বদলায়।
প্র ০৩ঋণাত্মক ওজন বাস্তবে কোথায় আসে, আর তখন Dijkstra-কে কি একেবারেই বাদ দিতে হয়?
সময় আর দূরত্ব কখনো ঋণাত্মক হয় না, কিন্তু টাকা, শক্তি বা লাভ-ক্ষতি হয়। কয়েকটি উদাহরণ: ক্যাশব্যাক বা ভর্তুকি পাওয়া রাস্তা; বৈদ্যুতিক গাড়ি পাহাড় থেকে নামার সময় ব্যাটারি চার্জ ফিরে পায় (regenerative braking) — শক্তির হিসাবে edge ঋণাত্মক; আর মুদ্রা বিনিময়। শেষটি সবচেয়ে বিখ্যাত: টাকা থেকে ডলার, ডলার থেকে ইউরো, ইউরো থেকে আবার টাকা — হার গুণ হয়। প্রতিটি হার r-কে −log r ওজনে বদলালে গুণফল হয় যোগফল, আর “ঘুরে এসে বেশি টাকা” মানে একটি ঋণাত্মক cycle. ঋণাত্মক cycle থাকলে “সবচেয়ে ছোট পথ” অর্থহীন — বারবার ঘুরলে খরচ অসীমভাবে কমে। Bellman-Ford (পর্ব ৪৪) এটি ধরতে পারে: V − 1 বার পরেও কোনো edge relax হলে cycle আছে।
কিন্তু Bellman-Ford O(VE) — বড় গ্রাফে ধীর। এখানে আসে একটি সুন্দর কৌশল: Johnson-এর পদ্ধতি (Donald Johnson, ১৯৭৭)। প্রতিটি vertex-এ একটি সংখ্যা p(v) ধরুন (“potential”), আর ওজন বদলান w′(u, v) = w(u, v) + p(u) − p(v). যেকোনো s–t পথের ওজন তখন ঠিক p(s) − p(t) পরিমাণ বদলায় — সব পথে সমান, তাই সবচেয়ে ছোট পথ একই থাকে। p হিসেবে একটি নকল উৎস থেকে Bellman-Ford-এর দূরত্ব নিলে triangle inequality বলে প্রতিটি w′ ≥ 0. ফলে একবার Bellman-Ford, তারপর প্রতিটি উৎস থেকে দ্রুত Dijkstra — সব জোড়ার shortest path O(VE log V)-এ, পাতলা গ্রাফে Floyd-Warshall-এর O(V³)-এর চেয়ে ভালো। অংশ ৮-এর A*-এর consistent heuristic আসলে একই potential-এর ধারণা — শুধু লক্ষ্য ভিন্ন।
আরেকটি প্রচলিত ভুল ধরন হলো “longest path”-এ সব ওজন ঋণাত্মক করে Dijkstra চালানো। সাধারণ গ্রাফে longest simple path NP-hard (পর্ব ৬৪), আর ঋণাত্মক ওজনে Dijkstra-র প্রমাণ তো খাটেই না। শুধু DAG-এ (পর্ব ৪২) topological ক্রমে relax করে longest path linear সময়ে পাওয়া যায় — যেমন প্রজেক্টের কাজগুলোর critical path বের করতে।
মূল উপলব্ধি: ঋণাত্মক ওজন দেখলেই Dijkstra বাদ নয় — ওজনগুলোকে সঠিক potential দিয়ে রূপান্তর করলে আবার Dijkstra-র গতি পাওয়া যায়। কিন্তু ধ্রুবক যোগ করার মতো সহজ রূপান্তর কাজ করে না।
প্র ০৪কোটি মোড়ের দেশজোড়া রাস্তার মানচিত্রে ম্যাপ অ্যাপ এক সেকেন্ডের ভগ্নাংশে পথ দেয় কীভাবে — শুধু Dijkstra-তেই কি হয়?
একটি বড় দেশের রাস্তার গ্রাফে কয়েক কোটি vertex থাকতে পারে। Heap-Dijkstra O((V + E) log V) — একটি প্রশ্নে কয়েক সেকেন্ড। কিন্তু একটি ম্যাপ সার্ভিসে প্রতি সেকেন্ডে অসংখ্য মানুষ পথ চান, আর প্রত্যেকে উত্তর চান চোখের পলকে। তাই বাস্তব রাউটিং ব্যবস্থাগুলো সাধারণত দুই ধাপে কাজ করে: আগে থেকে গ্রাফটিকে প্রক্রিয়া (preprocessing) করে রাখা, তারপর প্রতিটি প্রশ্নে খুব অল্প vertex দেখা।
প্রথম সহজ উন্নতি bidirectional search: একটি ঢেউ s থেকে, আরেকটি t থেকে (উল্টো edge-এ), দুটি মিললে থামা। দুটি ছোট বৃত্তের ক্ষেত্রফল একটি বড় বৃত্তের চেয়ে অনেক কম, তাই কাজ মোটামুটি অর্ধেক বা তারও কম। তারপর A* — কিন্তু সোজা-রেখার দূরত্ব রাস্তার গ্রাফে দুর্বল অনুমান। Andrew Goldberg ও Chris Harrelson ২০০৫ সালে ALT পদ্ধতি দেন: কয়েকটি “landmark” vertex থেকে আগে সব দূরত্ব বের করে রাখা, আর triangle inequality দিয়ে অনেক ভালো admissible heuristic বানানো।
সবচেয়ে বড় লাফ এসেছে hierarchy-ভিত্তিক কৌশল থেকে। Contraction hierarchies (Robert Geisberger ও সহকর্মীরা, ২০০৮) প্রতিটি vertex-কে গুরুত্ব অনুযায়ী সাজায় — গলির মোড় নিচে, মহাসড়কের সংযোগ ওপরে — আর কম গুরুত্বের vertex একে একে “সরিয়ে” তার জায়গায় shortcut edge বসায়। প্রশ্নের সময় দুই দিক থেকেই শুধু “ওপরের দিকে” যাওয়া হয়, তাই কোটি vertex-এর গ্রাফেও প্রতিটি প্রশ্নে মাত্র কয়েকশো vertex দেখতে হয় — Dijkstra-র চেয়ে হাজার গুণের বেশি দ্রুত। ঢাকা থেকে চট্টগ্রাম যেতে আপনিও তো তাই করেন: গলি থেকে বড় রাস্তা, বড় রাস্তা থেকে মহাসড়ক, শেষে আবার গলি।
আরেকটি বাস্তব জটিলতা হলো বদলাতে থাকা ওজন: জ্যাম প্রতি মিনিটে বদলায়। তাই আধুনিক ব্যবস্থায় গ্রাফের কাঠামোর প্রক্রিয়া আর ওজনের হালনাগাদ আলাদা রাখার কৌশলও আছে। কিন্তু এই সব কৌশলের ভেতরে প্রতিটি প্রশ্নের মূল ইঞ্জিন এখনো Dijkstra-র মতো একটি priority-queue খোঁজ।
মূল উপলব্ধি: বড় স্কেলে অ্যালগরিদমের চেয়ে বড় ভূমিকা রাখে preprocessing আর সমস্যার গঠন (রাস্তার স্তরবিন্যাস) কাজে লাগানো। Dijkstra সেখানে ভিত্তি, শেষ কথা নয়।
প্র ০৫ইন্টারনেটের router-গুলো একে অপরকে কীভাবে পথ জানায় — Dijkstra আর Bellman-Ford দুজনেই কেন সেখানে আছে?
আপনার পাঠানো প্রতিটি packet অনেকগুলো router পেরিয়ে যায়, আর প্রতিটি router-কে ঠিক করতে হয় পরের ধাপে কাকে দেবে। সেই সিদ্ধান্ত থাকে তার routing table-এ, আর টেবিল ভরা হয় routing protocol দিয়ে। একটি নেটওয়ার্কের (যেমন একটি ISP বা বিশ্ববিদ্যালয়ের) ভেতরে মূলত দুই ঘরানার protocol আছে, আর আশ্চর্যের বিষয়, দুই ঘরানা এই মডিউলের দুই অ্যালগরিদমের সাথে মেলে।
Link-state routing, যেমন OSPF আর IS-IS: প্রতিটি router তার নিজের সংযোগগুলো আর তাদের খরচ (link cost) পুরো নেটওয়ার্কে ছড়িয়ে দেয়। ফলে প্রত্যেক router-এর কাছে পুরো মানচিত্রের একটি কপি থাকে, আর প্রত্যেকে নিজেকে উৎস ধরে Dijkstra চালায় — OSPF-এর মানদণ্ডে এটিকে “shortest path first” হিসাব বলা হয়। ফল একটি shortest path tree, যা থেকে প্রতিটি গন্তব্যের পরের ধাপ (next hop) টেবিলে ওঠে। Link cost ঋণাত্মক হতে পারে না, তাই Dijkstra নিরাপদ।
Distance-vector routing, যেমন পুরোনো RIP: কোনো router পুরো মানচিত্র জানে না, শুধু প্রতিবেশীদের কাছ থেকে শোনে “আমার থেকে X কত দূরে”, আর নিজের হিসাব বদলায় — dist[X] = min(প্রতিবেশীর দূরত্ব + সংযোগের খরচ)। এটি হুবহু Bellman-Ford-এর relaxation, শুধু অনেক কম্পিউটারে ছড়ানো আর এলোমেলো ক্রমে। এর বিখ্যাত দুর্বলতা “count to infinity”: একটি সংযোগ ভাঙলে দুই router একে অপরের পুরোনো খবর বিশ্বাস করে দূরত্ব একটু একটু করে বাড়াতে থাকে। তাই RIP-এ “16 মানে অসীম” ধরনের সীমা রাখা হয়েছে।
আর বিভিন্ন নেটওয়ার্কের মধ্যে, পুরো ইন্টারনেট জুড়ে, BGP কাজ করে আরেক নীতিতে — সেখানে “সবচেয়ে ছোট” নয়, ব্যবসায়িক চুক্তি আর নীতিই প্রধান। অর্থাৎ shortest path একটি গাণিতিক আদর্শ, আর বাস্তব নেটওয়ার্ক সেটি মানে যতটা সম্ভব, যতটা নিরাপদ।
মূল উপলব্ধি: পুরো মানচিত্র জানা থাকলে Dijkstra (কেন্দ্রীভূত, দ্রুত); শুধু প্রতিবেশীর খবর থাকলে Bellman-Ford-এর ধারণা (বিকেন্দ্রীভূত, সরল, কিন্তু ধীরে মেলে)। অ্যালগরিদম বাছাই নির্ভর করে কে কী তথ্য জানে তার ওপর।
অনুশীলন
খাতা-কলম নিন। প্রথমটি হাতে trace, দ্বিতীয় আর চতুর্থটি ভাবনার, তৃতীয়টিতে কোড লিখতে হবে।
-
হাতে trace করুন: undirected গ্রাফ: A–B 4, A–C 2, B–C 1, B–D 5, C–D 8, C–E 10, D–E 2. A থেকে heap + lazy deletion দিয়ে Dijkstra চালান (সমান dist-এ অক্ষরের ক্রম)। কোন ক্রমে vertex settle হয়, প্রতিটির dist আর par কী, কয়টি বাসি জোড়া বাদ পড়ে, আর A থেকে E-র পথ কী?
A (0): B = 4, C = 2. C (2): B: 2 + 1 = 3 < 4 → 3 (par C); D = 10; E = 12. B (3): D: 3 + 5 = 8 < 10 → 8 (par B). এরপর heap-এর (4, B) বাসি — বাদ। D (8): E: 8 + 2 = 10 < 12 → 10 (par D). (10, D) বাসি — বাদ। E (10). শেষে (12, E) বাসি — বাদ।
ক্রম A, C, B, D, E; dist 0, 2, 3, 8, 10. বাসি জোড়া ৩টি: (4, B), (10, D), (12, E). পথ: E ← D ← B ← C ← A, অর্থাৎ A → C → B → D → E, ওজন 2 + 1 + 5 + 2 = 10. সরাসরি A → B (4) বা C → E (10) দেখতে লোভনীয়, কিন্তু দুটিই সেরা পথের অংশ নয়।
-
ভেবে বলুন: একজন বললেন, “ঋণাত্মক ওজন থাকলে সবচেয়ে ছোট ওজনটির পরম মান সব edge-এ যোগ করে দিন, তারপর Dijkstra চালান।” অংশ ৪-এর গ্রাফে (S→A 2, S→B 4, B→A −3, A→T 1) এটি করে দেখান কেন কাজ করে না।
সব edge-এ 3 যোগ: S→A 5, S→B 7, B→A 0, A→T 4. নতুন গ্রাফে S থেকে T: S→A→T = 9, S→B→A→T = 7 + 0 + 4 = 11. তাই Dijkstra বেছে নেবে S→A→T, যার আসল ওজন 2 + 1 = 3. কিন্তু আসল সবচেয়ে ছোট পথ S→B→A→T, ওজন 2.
কারণ: k টি edge-এর পথে ধ্রুবকটি k বার যোগ হয়। বেশি edge-এর পথ বেশি “শাস্তি” পায়, তাই পথগুলোর ক্রম বদলে যায়। সঠিক রূপান্তর হলো vertex-ভিত্তিক potential (Johnson): w′(u, v) = w(u, v) + p(u) − p(v), যাতে প্রতিটি s–t পথ ঠিক একই পরিমাণ বদলায়।
-
কোড লিখুন: সব ওজন ধনাত্মক এমন একটি গ্রাফে s থেকে প্রতিটি vertex-এ কয়টি আলাদা সবচেয়ে ছোট পথ আছে, তা গুনুন (উত্তর 109 + 7 দিয়ে mod). Heap + lazy deletion ব্যবহার করুন, আর complexity বলুন।
ধারণা:
cnt[v]= v-তে সবচেয়ে ছোট পথের সংখ্যা। relax-এ ছোট পথ পেলেcnt[v] = cnt[u](পুরোনো গণনা বাতিল); সমান দৈর্ঘ্যের পথ পেলেcnt[v] += cnt[u].const long long MOD = 1'000'000'007; vector<long long> dist(n, INF), cnt(n, 0); dist[s] = 0; cnt[s] = 1; pq.push({0, s}); while (!pq.empty()) { auto [d, u] = pq.top(); pq.pop(); if (d != dist[u]) continue; // stale: u is already final for (auto [v, w] : adj[u]) { if (d + w < dist[v]) { dist[v] = d + w; cnt[v] = cnt[u]; pq.push({dist[v], v}); } else if (d + w == dist[v]) cnt[v] = (cnt[v] + cnt[u]) % MOD; } }কেন ঠিক: u যখন settle হয় (প্রথম অ-বাসি pop), তখন dist[u] চূড়ান্ত, আর u-তে আসার সব সবচেয়ে ছোট পথের শেষ-আগের vertex-গুলো (dist আরও ছোট, কারণ ওজন ধনাত্মক) আগেই settle হয়ে cnt[u]-তে যোগ করে গেছে — তাই cnt[u]-ও চূড়ান্ত। 0-ওজনের edge থাকলে এই যুক্তি ভাঙে (সমান dist-এর দুই vertex কে আগে বের হবে, ঠিক নেই), তাই “ধনাত্মক” শর্ত জরুরি। সময় O((V + E) log V), জায়গা O(V + E).
-
ভেবে বলুন: একটি নেটওয়ার্কে প্রতিটি সংযোগ সফলভাবে কাজ করার সম্ভাবনা p (0 < p ≤ 1) জানা, আর একটি পথের সাফল্যের সম্ভাবনা মানে তার সব সংযোগের p-এর গুণফল। s থেকে t-এর সবচেয়ে নির্ভরযোগ্য পথ বের করতে Dijkstra কীভাবে ব্যবহার করবেন? কেন এটি নিরাপদ?
গুণফল সর্বোচ্চ করা মানে log-এর যোগফল সর্বোচ্চ করা, মানে −log p-এর যোগফল সর্বনিম্ন করা। তাই প্রতিটি edge-এর ওজন ধরুন w = −log p. যেহেতু 0 < p ≤ 1, log p ≤ 0, তাই w ≥ 0 — ঋণাত্মক ওজন নেই, Dijkstra নিরাপদ, আর সবচেয়ে ছোট পথই সবচেয়ে নির্ভরযোগ্য পথ।
আরেক উপায়: log না নিয়ে সরাসরি “গুণফল সর্বোচ্চ” max-heap দিয়ে চালানো — এটিও কাজ করে, কারণ প্রতিটি edge-এ গুণফল শুধু কমে বা একই থাকে (p ≤ 1), যা “ওজন ≥ 0”-এর সমতুল্য শর্ত। কিন্তু p > 1 হতে পারলে (যেমন মুদ্রা বিনিময়ের হার) w ঋণাত্মক হয়ে যায় — তখন Bellman-Ford, প্র ০৩ দেখুন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমBinary heap আর lazy deletion দিয়ে Dijkstra-র ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- সব dist = ∞, উৎসের dist = 0, আর (0, উৎস) heap-এ push
- heap থেকে সবচেয়ে ছোট (d, u) জোড়া pop করা
- d ≠ dist[u] হলে জোড়াটি বাসি — বাদ দিয়ে আবার pop
- u-এর প্রতিটি edge (u, v, w) relax করা, dist[v] কমলে par[v] = u আর (dist[v], v) push
- heap খালি হলে থামা, আর par ধরে পেছনে হেঁটে পথ বের করা
একটি গ্রাফে dist[u] = 5, dist[v] = 10, আর u থেকে v-তে ওজন 3-এর একটি edge আছে। edge (u, v) relax করার পর dist[v] কত?
- 13
- 10
- 8
- 7
undirected গ্রাফ: A–B 4, A–C 1, C–B 2, B–D 5, C–D 8. A থেকে Dijkstra চালালে dist[D] কত?
- 8
- 9
- 6
- 14
ঋণাত্মক ওজনের edge থাকলে Dijkstra-র অ্যালগরিদম ভুল উত্তর দিতে পারে কেন?
- heap ঋণাত্মক key সাজাতে পারে না
- একটি vertex settle হওয়ার পর কোনো ঋণাত্মক edge দিয়ে তার দূরত্ব আরও কমতে পারে, কিন্তু অ্যালগরিদম তাকে আর ছোঁয় না
- শুধু ঋণাত্মক cycle থাকলেই ভুল হয়, একক ঋণাত্মক edge-এ নয়
- relax-এ ঋণাত্মক সংখ্যা যোগ করলে overflow হয়
V টি vertex আর E টি edge-এর গ্রাফে binary heap আর lazy deletion দিয়ে Dijkstra-র worst case time complexity কত?
- O(V²)
- O(V log V)
- O(E + V log V)
- O((V + E) log V)
প্রায় প্রতিটি vertex-জোড়ার মধ্যে edge আছে (E ≈ V²) এমন একটি ঘন গ্রাফে কোন Dijkstra সবচেয়ে উপযুক্ত?
- সাধারণ অ্যারে দিয়ে প্রতিবার সবচেয়ে ছোট খোঁজা, O(V²)
- binary heap আর lazy deletion
- BFS
- Bellman-Ford
A* search-এ heuristic h-কে admissible বলা হয় যখন—
- h(v) সবসময় আসল বাকি দূরত্বের চেয়ে বড় বা সমান
- h(v) সবসময় 0
- h(v) কখনো v থেকে লক্ষ্যের আসল সবচেয়ে ছোট দূরত্বের বেশি নয়
- প্রতিটি edge-এ h(u) ≤ w(u, v) + h(v)
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Weighted গ্রাফে কম edge মানেই কম ওজন নয় — তাই BFS নয়, Dijkstra. মূল কাজ relax: dist[u] + w < dist[v] হলে কমানো।
- Greedy নিয়ম: সবচেয়ে ছোট dist-এর অনিষ্পন্ন vertex settle করা। ওজন ≥ 0 হলে settle হওয়া dist চূড়ান্ত — প্রমাণ বিরোধের মাধ্যমে।
- একটি ঋণাত্মক edge-ই যথেষ্ট ভুল করাতে; তখন Bellman-Ford (পর্ব ৪৪)। ধ্রুবক যোগ করে সারানো যায় না।
- অ্যারে: O(V²), ঘন গ্রাফে। Heap + lazy deletion: O((V + E) log V),
d != dist[u]হলে বাদ। Fibonacci heap: O(E + V log V). par[]দিয়ে পথ; সব উৎস dist 0 দিয়ে multi-source; (vertex, অবস্থা) দিয়ে state-space; 0/1 ওজনে 0-1 BFS; লক্ষ্য থাকলে admissible h দিয়ে A*.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪৪ · Bellman-Ford আর Floyd-Warshallপরবর্তী পর্বঋণাত্মক ওজন, ঋণাত্মক cycle আর সব জোড়ার shortest path.
- ডেটা স্ট্রাকচার ও অ্যালগরিদম · কোর্স হোম৭২টি পর্বপুরো কোর্সের রোডম্যাপ — নয়টি মডিউল।
- পর্ব ২৩ · Heap, heapsort আর priority queueআগের ভিত্তিsift-up, sift-down, indexed heap আর decrease-key — Dijkstra-র ইঞ্জিন।
- পর্ব ৪০ · Breadth-first search (BFS)আগের ভিত্তিunweighted গ্রাফে স্তরে স্তরে খোঁজা — Dijkstra-র সরল রূপ।
- C++ প্রোগ্রামিং · Sequence containerভাষার দিক
vector,dequeআরpriority_queue-এর মতো container adaptor. - Python প্রোগ্রামিং · অ্যালগরিদম ও কনটেস্টে Pythonপাশের ট্র্যাক
heapq, দ্রুত ইনপুট আর কনটেস্টে Python-এর কৌশল।