সরাসরি পর্বে যান
পর্ব ৪৪ · ৭২-এর মধ্যে · মডিউল ৬

Bellman-Ford আর Floyd-Warshall

Bellman-Ford & Floyd-Warshall — Negative Weights, Negative-Cycle Detection, SPFA, All-Pairs Shortest Paths, Transitive Closure & Johnson's Algorithm

Taught in Bangla, this lesson handles negative edge weights: Bellman-Ford with a traced round-by-round table, why V−1 rounds suffice, detecting and extracting negative cycles, SPFA and its pitfalls, Floyd-Warshall for all-pairs shortest paths, path recovery and transitive closure, Johnson's algorithm, and choosing among shortest-path algorithms, with C++ and Python code.

এই পর্বে যা শিখবেন

  • কোথায় ঋণাত্মক ওজনের edge আসে, আর কেন সেখানে Dijkstra ভুল উত্তর দেয় তা উদাহরণ দিয়ে দেখানো
  • Bellman-Ford হাতে trace করা, আর V−1 রাউন্ড কেন যথেষ্ট তা induction দিয়ে প্রমাণ করা
  • Negative cycle ধরা, cycle-টি বের করা, early termination আর SPFA-র সুবিধা ও বিপদ বোঝা
  • Floyd-Warshall-এর DP সূত্র, লুপের ক্রমের কারণ, পথ পুনর্গঠন আর transitive closure
  • BFS থেকে Johnson পর্যন্ত — সমস্যা দেখে সঠিক shortest-path অ্যালগরিদম বেছে নেওয়া

১ · ঋণাত্মক ওজন — যখন পথ চললে খরচ কমে

পর্ব ৪৩-এ Dijkstra দেখেছেন: একটি উৎস থেকে প্রতিটি vertex-এ সবচেয়ে কম খরচের পথ, যখন সব edge-এর ওজন শূন্য বা ধনাত্মক। কিন্তু বাস্তবের খরচ সবসময় ধনাত্মক থাকে না। একটি weighted graph-এ edge-এর ওজন মানে “এই পথে গেলে কত খরচ” — আর কিছু পথে গেলে খরচ উল্টে লাভ হয়ে যায়। তখন ওজন হয় ঋণাত্মক, আর আমাদের লাগে ঋণাত্মক ওজনের edge সামলাতে পারে এমন shortest path অ্যালগরিদম।

  • ক্যাশব্যাক আর বোনাস: একজন ডেলিভারি রাইডারের কাছে প্রতিটি রুটের খরচ = তেল আর সময় − কোম্পানির বোনাস। কোনো ব্যস্ত রুটে বোনাস তেলের খরচের চেয়ে বেশি হলে সেই edge-এর নিট খরচ ঋণাত্মক। bKash বা Nagad-এ কোনো নির্দিষ্ট পেমেন্টে ক্যাশব্যাক থাকলে লেনদেনের ধাপে ধাপে খরচ হিসাবেও একই ব্যাপার ঘটে।
  • ভর্তুকি: কোনো রুটে সরকার বা কোম্পানি ভর্তুকি দিলে, পরিবহনের মোট খরচের গ্রাফে সেই অংশ ঋণাত্মক হতে পারে।
  • শক্তি: বৈদ্যুতিক গাড়ি বা ই-বাইক ঢাল বেয়ে নামার সময় regenerative braking-এ ব্যাটারিতে কিছু শক্তি ফেরত পায়। “কত শক্তি খরচ” মাপলে উতরাইয়ের edge ঋণাত্মক।
  • মুদ্রা বিনিময়: সবচেয়ে বিখ্যাত উদাহরণ — নিচে বিস্তারিত।

মুদ্রা বিনিময় আর log-এর কৌশল

ধরুন তিনটি মুদ্রার মধ্যে বিনিময় হার এরকম (হারগুলো কাল্পনিক, শুধু বোঝার জন্য): ১ ডলারে 0.92 ইউরো, ১ ইউরোতে 0.86 পাউন্ড, আর ১ পাউন্ডে 1.28 ডলার। ১ ডলার নিয়ে পুরো চক্র ঘুরলে হাতে আসে 0.92 × 0.86 × 1.28 ≈ 1.0127 ডলার — শুধু হাতবদল করেই ১ শতাংশের বেশি লাভ। একে বলে arbitrage. প্রশ্ন: বড় একটি বিনিময়-টেবিলে এমন চক্র আছে কি না, কীভাবে খুঁজবেন?

সমস্যা হলো, পথে হারগুলো গুণ হয়, আর shortest path অ্যালগরিদম ওজন যোগ করে। সমাধান logarithm: log(a × b) = log a + log b. প্রতিটি হার r-এর জন্য edge-এর ওজন দিন w = −log r. তাহলে কোনো চক্রে হারগুলোর গুণফল > 1 হওয়া মানে log-এর যোগফল > 0, অর্থাৎ −log-এর যোগফল < 0. উদাহরণে: −ln 0.92 ≈ 0.0834, −ln 0.86 ≈ 0.1508, −ln 1.28 ≈ −0.2469; যোগফল ≈ −0.0127 — ঋণাত্মক। অর্থাৎ arbitrage খোঁজা = ঋণাত্মক যোগফলের চক্র খোঁজা. এই চক্রের নাম negative cycle, আর এটি ধরাই এই পর্বের Bellman-Ford-এর একটি প্রধান কাজ।

Dijkstra কোথায় ভুল করে

Dijkstra-র মূল যুক্তি ছিল greedy: heap থেকে যে vertex সবচেয়ে কম দূরত্ব নিয়ে বের হয়, তার দূরত্ব চূড়ান্ত — কারণ অন্য যেকোনো পথ আরও বড় দূরত্বের কোনো vertex দিয়ে যাবে, আর edge যোগ করলে দূরত্ব শুধু বাড়ে। ঋণাত্মক edge এই “শুধু বাড়ে” অনুমানটিই ভেঙে দেয়। তিনটি vertex-এর ছোট একটি উদাহরণ:

EdgeওজনDijkstra যা করে
S → A2A-কে প্রথমে বের করে, দূরত্ব 2 “চূড়ান্ত” ঘোষণা করে
S → B5তারপর B বের হয়, দূরত্ব 5
B → A−45 + (−4) = 1 < 2 — কিন্তু A তো আগেই চূড়ান্ত!

আসল উত্তর dist(A) = 1, পথ S → B → A. Dijkstra-র কোনো রূপ A-কে আবার না খুললে ভুল 2 থেকে যায়, আর A থেকে যাওয়া সব পথও ভুল হয়। (Lazy deletion-এর কিছু রূপ A-কে আবার খোলে বলে এই ছোট উদাহরণে ঠিক উত্তর দিতে পারে, কিন্তু তখন সময়ের নিশ্চয়তা হারিয়ে যায় — worst case-এ exponential.) আর একটি পরিচিত ভুল সমাধান হলো “সব ওজনে একটি বড় সংখ্যা যোগ করে ধনাত্মক বানাও” — এটি কেন কাজ করে না, অনুশীলন ৩-এ নিজেই দেখবেন।

Undirected গ্রাফে সাবধান। Directed graph-এ edge একমুখী। কিন্তু undirected গ্রাফে u–v edge-এর ওজন −3 হলে u → v → u যাওয়া-আসাতেই −6 — নিজেই একটি negative cycle. তাই undirected গ্রাফে একটি ঋণাত্মক edge থাকলেই (সেটি উৎস থেকে পৌঁছানো গেলে) “shortest path” অর্থহীন হয়ে যায়। এই পর্বের সব আলোচনা directed গ্রাফের।

২ · Relaxation আর Bellman-Ford-এর মূল ধারণা

প্রতিটি shortest-path অ্যালগরিদমের কেন্দ্রে একটিই ছোট কাজ: relaxation. প্রতিটি vertex v-এর জন্য রাখা থাকে dist[v] — “এখন পর্যন্ত জানা সেরা খরচ” (শুরুতে উৎসের 0, বাকি সবার ∞). Edge u → v (ওজন w) relax করা মানে প্রশ্ন করা: “u হয়ে v-তে গেলে কি এখনকার চেয়ে কম খরচ পড়ে?” পড়লে dist[v] কমিয়ে দিন, আর parent[v] = u লিখে রাখুন।

Pseudocode — Bellman-Ford (0-based vertices 0..V-1, edge list E)

relax(u, v, w):
    if dist[u] != INF and dist[u] + w < dist[v]:
        dist[v] = dist[u] + w;  parent[v] = u;  return true
    return false

bellmanFord(V, E, s):
    dist[*] = INF;  parent[*] = -1;  dist[s] = 0
    repeat V-1 times:                         // one "round"
        changed = false
        for each edge (u, v, w) in E:  if relax(u, v, w): changed = true
        if not changed: break                 // early termination
    for each edge (u, v, w) in E:             // the V-th, check round
        if dist[u] != INF and dist[u] + w < dist[v]:  report "negative cycle"

Dijkstra বুদ্ধি খাটিয়ে ঠিক করত কোন edge কখন relax হবে। Bellman-Ford কোনো বুদ্ধিই খাটায় না: প্রতিটি রাউন্ডে সব edge, কোনো বিশেষ ক্রম ছাড়াই, একবার করে relax করে — আর এটি V−1 রাউন্ড চালায়। এর জন্য গ্রাফ রাখার সবচেয়ে সহজ উপায় edge list: শুধু (u, v, w) তিনটির একটি তালিকা, adjacency list-ও লাগে না।

উপমা — গ্রামে খবর ছড়ানো। ধরুন একটি জেলার সব হাটের মধ্যে কোন রাস্তায় কত খরচ, সেই তালিকা প্রতিটি হাটের লোকের কাছে আছে। প্রতি সপ্তাহে (এক রাউন্ডে) প্রতিটি হাটের লোক পাশের হাটগুলোকে জানায়: “আমাদের হাট পর্যন্ত আসতে সবচেয়ে কম এত টাকা লাগে।” পাশের হাট সেই খবর আর রাস্তার খরচ মিলিয়ে দেখে, নিজের হিসাব কমানো যায় কি না। প্রথম সপ্তাহে শুধু সদরের পাশের হাটগুলো সঠিক খবর পায়, দ্বিতীয় সপ্তাহে তাদের পাশেরগুলো — খবর প্রতি সপ্তাহে অন্তত এক ধাপ এগোয়। V টি হাট থাকলে V−1 সপ্তাহে খবর সবচেয়ে দূরের হাটেও পৌঁছে যায়।

দুটি জরুরি খুঁটিনাটি। প্রথমত, dist[u] != INF শর্তটি বাদ দেওয়া যাবে না: INF যদি একটি বড় সংখ্যা হয়, তাহলে INF + (−3) < INF হয়ে একটি পৌঁছানো-যায়-না এমন vertex থেকে মিথ্যা “উন্নতি” ছড়াবে। দ্বিতীয়ত, edge কোন ক্রমে দেখা হচ্ছে তার ওপর রাউন্ডের সংখ্যা নির্ভর করে — ভাগ্য ভালো হলে এক রাউন্ডেই সব ঠিক, খারাপ হলে V−1 রাউন্ড লাগে। নিচের উদাহরণে ইচ্ছে করেই সবচেয়ে খারাপ ক্রম নেওয়া হয়েছে।

৩ · Bellman-Ford হাতে চালানো — রাউন্ড ধরে dist টেবিল

শহরের পাঁচটি পয়েন্ট (vertex 0 থেকে 4), উৎস 0 — রাইডারের গুদাম। ওজন হলো টাকায় নিট খরচ; ঋণাত্মক মানে বোনাস খরচের চেয়ে বেশি। এই আটটি edge ঠিক এই ক্রমে প্রতিটি রাউন্ডে দেখা হবে:

ক্রমe1e2e3e4e5e6e7e8
Edge4→31→42→10→20→10→32→43→2
ওজন2−3247861
dist আর parent টেবিলসবুজ = এই রাউন্ডে বদলেছে

            
প্রতিটি রাউন্ডের পর
রাউন্ড
যে relax সফল হলো
চূড়ান্ত হয়ে গেছে
  1. শুরু. dist[0] = 0, বাকি সবাই ∞, কারও parent নেই। মনে রাখুন edge-এর ক্রম: e1 = 4→3 প্রথমে, e4 = 0→2 চতুর্থে।
  2. রাউন্ড ১. e1, e2, e3 কাজে লাগে না — তাদের শুরুর vertex (4, 1, 2) এখনো ∞. e4: dist[2] = 4. e5: dist[1] = 7. e6: dist[3] = 8. e7: dist[2] এইমাত্র 4 হয়েছে, তাই dist[4] = 4 + 6 = 10. e8: 8 + 1 = 9 > 4, কিছু হলো না। এক edge-এর সেরা পথ dist[2] = 4 এখন সঠিক।
  3. রাউন্ড ২. e1: 10 + 2 = 12 > 8, না। e2: 7 − 3 = 4 < 10, তাই dist[4] = 4, parent 1. e3: 4 + 2 = 6 < 7, তাই dist[1] = 6, parent 2. বাকিরা কিছু বদলায় না। দুই edge-এর পথ 0→2→1 = 6 এখন সঠিক।
  4. রাউন্ড ৩. e1: 4 + 2 = 6 < 8, dist[3] = 6. e2: এবার dist[1] = 6, তাই 6 − 3 = 3 < 4, dist[4] = 3. e8: 6 + 1 = 7 > 4. তিন edge-এর পথ 0→2→1→4 = 3 সঠিক।
  5. রাউন্ড ৪. e1: 3 + 2 = 5 < 6, dist[3] = 5. আর কিছু বদলায় না। চার edge-এর পথ 0→2→1→4→3 = 4 + 2 − 3 + 2 = 5 — V − 1 = 4 রাউন্ড পুরোটাই লাগল, কারণ edge-এর ক্রম পথের উল্টো দিকে সাজানো।
  6. রাউন্ড ৫ — যাচাই. আটটি edge-এর একটিও relax হয় না। তাই কোনো negative cycle নেই, আর উত্তর চূড়ান্ত: dist = 0, 6, 4, 5, 3. parent ধরে পেছনে হাঁটলে 3 ← 4 ← 1 ← 2 ← 0 — এটিই পথ।

লক্ষ করুন, “চূড়ান্ত হয়ে গেছে” সারিটি কী বলছে: এই উদাহরণে রাউন্ড i-এর পর ঠিক সেই vertex-গুলো সঠিক, যাদের সেরা পথে i টি বা কম edge. সাধারণভাবে নিশ্চয়তাটি হলো “অন্তত এরা সঠিক” (edge-এর ক্রম ভালো হলে অন্যরাও আগেই ঠিক হতে পারে) — পরের অংশের প্রমাণের মূল দাবি এটিই। parent অ্যারে মিলে একটি shortest-path tree তৈরি হয়: উৎস থেকে প্রতিটি vertex-এ একটি করে সেরা পথ।

Early termination — শান্ত রাউন্ডে থামা

কোনো রাউন্ডে একটিও relax সফল না হলে পরের রাউন্ডগুলোও হুবহু একই হবে — dist অ্যারে বদলায়নি, edge-ও একই, তাই ফলও একই। তাই সেখানেই থামা নিরাপদ, আর সেই “শান্ত” রাউন্ডটি নিজেই প্রমাণ যে কোনো negative cycle নেই। Edge-এর ক্রম ভালো হলে (যেমন উৎস থেকে BFS ক্রমে সাজানো) প্রায়ই অল্প কয়েক রাউন্ডেই কাজ শেষ। Best case-এ, এক রাউন্ডে সব ঠিক হয়ে দ্বিতীয়টি শান্ত হলে, মোট O(E). তবে worst case বদলায় না: উপরের মতো খারাপ ক্রমে V − 1 রাউন্ডই লাগে।

৪ · কেন V−1 রাউন্ডই যথেষ্ট — প্রমাণ আর খরচ

বিশ্ববিদ্যালয় বা চাকরির লিখিত পরীক্ষায় এই প্রমাণটি খুব প্রচলিত প্রশ্ন। ধরে নিন উৎস s থেকে পৌঁছানো যায় এমন কোনো negative cycle নেই। আমাদের দেখাতে হবে, V − 1 রাউন্ডের পর প্রতিটি vertex v-এর জন্য dist[v] = δ(s, v), যেখানে δ মানে আসল সবচেয়ে কম খরচ।

ধাপ ১ — সেরা পথ সবসময় সরল পথ

একটি simple path (সরল পথ) কোনো vertex দুবার ছোঁয় না। কোনো পথে একটি vertex দুবার এলে, মাঝের অংশটি একটি cycle. Negative cycle নেই বলে সেই cycle-এর ওজন ≥ 0; তাই cycle-টি কেটে বাদ দিলে পথের খরচ বাড়ে না। বারবার কাটতে থাকলে শেষে একটি সরল পথ পাই, যার খরচ আগেরটির চেয়ে বেশি নয়। আর V টি vertex-এর সরল পথে সর্বোচ্চ V টি vertex, তাই সর্বোচ্চ V − 1 টি edge. অর্থাৎ প্রতিটি vertex-এর এমন একটি সেরা পথ আছে, যাতে edge V − 1-এর বেশি নয়।

ধাপ ২ — dist কখনো আসল মানের নিচে নামে না

dist[v] যখনই একটি সসীম মান পায়, তখন সেটি s থেকে v পর্যন্ত কোনো একটি সত্যিকারের পথের খরচ (parent ধরে সেই পথটি পাওয়া যায়)। কোনো পথের খরচ সেরা পথের চেয়ে কম হতে পারে না, তাই সবসময় dist[v] ≥ δ(s, v). একে upper-bound property বলে: dist সবসময় আসল উত্তরের একটি ওপরের সীমা, শুধু নামতে থাকে।

ধাপ ৩ — induction

Induction-এর দাবি P(i): “রাউন্ড i-এর পর, যে vertex-এর একটি সেরা পথে i টি বা কম edge আছে, তার dist সঠিক।”

  • ভিত্তি, i = 0: শুধু s নিজে, যার পথে 0 টি edge; dist[s] = 0 = δ(s, s). (Negative cycle নেই বলে δ(s, s) = 0.)
  • ধাপ, i − 1 থেকে i: ধরুন v-এর সেরা পথ s → … → u → v, যাতে i টি edge. তাহলে s → … → u অংশটি u-এর একটি সেরা পথ (নইলে u পর্যন্ত আরও সস্তা পথ নিয়ে v-এর পথও সস্তা করা যেত) — এবং এতে i − 1 টি edge. Inductive hypothesis অনুযায়ী রাউন্ড i − 1-এর পরেই dist[u] = δ(s, u). রাউন্ড i-তে edge u → v অবশ্যই একবার relax হয়, তখন dist[v] ≤ δ(s, u) + w(u, v) = δ(s, v). আর ধাপ ২ বলে dist[v] ≥ δ(s, v). তাই সমান।

ধাপ ১ আর ৩ একসাথে: প্রতিটি vertex-এর সেরা পথে ≤ V − 1 edge, তাই P(V − 1) সব vertex-কে সঠিক করে। ∎ ধাপ ৩-এর মাঝখানের যুক্তি — “সেরা পথের অংশও সেরা পথ” — এর নাম optimal substructure, যা dynamic programming-এর (পর্ব ৫২) প্রধান শর্ত। বাস্তবে Bellman-Ford হলো একটি DP: di(v) = “সর্বোচ্চ i টি edge ব্যবহার করে v-তে যাওয়ার সেরা খরচ”, আর di(v) = min(di−1(v), minu→v di−1(u) + w(u, v)). এক অ্যারেতে জায়গায় বসে হালনাগাদ করলে মান আরও আগে ঠিক হতে পারে, কিন্তু কখনো ভুল হয় না।

মূল ধারণা

Dijkstra-র প্রমাণ ছিল “কে আগে চূড়ান্ত হবে” — greedy. Bellman-Ford-এর প্রমাণ “কত edge পর্যন্ত পথ ঠিক হয়েছে” — edge-এর সংখ্যা ধরে induction. দ্বিতীয়টি ওজনের চিহ্নের ওপর নির্ভর করে না, তাই ঋণাত্মক ওজনেও টেকে। দাম দিতে হয় সময়ে।

খরচ

V − 1 টি রাউন্ড (যাচাই নিয়ে V), প্রতিটিতে E টি edge-এর প্রতিটিতে O(1) কাজ — সময় O(V · E). জায়গা: dist আর parent মিলে O(V), edge list আলাদা O(E). Dense graph-এ (E ≈ V²) এটি O(V³), আর sparse graph-এ (E ≈ V) O(V²). তুলনায় Dijkstra O((V + E) log V) — V = E = 105 হলে Bellman-Ford প্রায় 1010 ধাপ (কনটেস্টে সময়সীমার অনেক বাইরে), Dijkstra কয়েক মিলিয়ন। তাই নিয়ম: ঋণাত্মক edge না থাকলে Bellman-Ford নয়।

৫ · Negative cycle — ধরা আর খুঁজে বের করা

এবার গ্রাফ A-তে একটিমাত্র পরিবর্তন: কোম্পানি e8 = 3→2 রুটে বোনাস বাড়াল, ওজন 1 থেকে হলো −3. চক্র 2 → 1 → 4 → 3 → 2-এর ওজন এখন 2 + (−3) + 2 + (−3) = −2. রাইডার এই চক্রে যতবার ঘুরবেন, প্রতিবার ২ টাকা লাভ — অসীমবার ঘুরলে খরচ −∞. এমন গ্রাফে 1, 2, 3, 4-এর “shortest path” বলে কিছু নেই; ঠিক উত্তর হলো “negative cycle আছে” বলা। বাস্তবে এটি কোম্পানির দামের নিয়মে একটি ফাঁক, যা খুঁজে বের করাই কাজ।

পরীক্ষাটি সহজ: V − 1 রাউন্ডের পর আরও একটি (V-তম) রাউন্ড চালান। কোনো edge তখনও relax হলে, s থেকে পৌঁছানো যায় এমন একটি negative cycle আছে। দুই দিকেই প্রমাণ:

  • Cycle না থাকলে V-তম রাউন্ড শান্ত: অংশ ৪ বলে তখন সব dist সঠিক, আর সঠিক মান আর কমতে পারে না (ধাপ ২)।
  • Cycle থাকলে কিছু একটা বদলাবেই: ধরুন পৌঁছানো যায় এমন cycle v0 → v1 → … → vk = v0, ওজনের যোগফল < 0, অথচ কোনো edge relax হয় না। তাহলে প্রতিটি edge-এ dist[vi] ≤ dist[vi−1] + wi. সব k টি অসমতা যোগ করলে দুই পাশের dist-এর যোগফল একই (cycle-এ প্রতিটি vertex একবার করে আসে), তাই কেটে যায়: 0 ≤ Σ wi — কিন্তু Σ wi < 0. স্ববিরোধ. (cycle পৌঁছানো যায় বলে এর সব dist সসীম, তাই যোগ-বিয়োগ বৈধ।)

“আছে” বলাই যথেষ্ট নয় — প্রায়ই cycle-টি দেখাতে হয় (কোন মুদ্রাগুলো হাতবদল করলে লাভ)। কৌশল: V-তম রাউন্ডে যে vertex x relax হয়েছে, তার parent ধরে V বার পিছিয়ে যান. x নিজে হয়তো cycle-এর বাইরে, cycle থেকে বেরোনো কোনো লেজে; কিন্তু parent-পথে V বার হাঁটলে কোনো vertex অবশ্যই পুনরাবৃত্তি হয় (pigeonhole), অর্থাৎ পথটি একটি cycle-এ ঢুকে পড়েছে — আর তারপর সেখানেই ঘুরতে থাকে। সেই y থেকে আবার parent ধরে y-তে ফেরা পর্যন্ত হাঁটলেই পুরো cycle.

dist আর parentসবুজ = এই ধাপে গুরুত্বপূর্ণ

            
প্রতিটি ধাপে
কী হচ্ছে
সিদ্ধান্ত
  1. ৪ রাউন্ড শেষে. গ্রাফ A হলে এখানেই উত্তর চূড়ান্ত হতো। কিন্তু এখানে মানগুলো অদ্ভুত: dist[2] = 2, অথচ 0 থেকে 2-এ সরাসরি edge-ই 4, আর রাউন্ডে রাউন্ডে মান কমছেই। parent[1] = 2, parent[2] = 3, parent[3] = 4, parent[4] = 1 — parent-গুলো নিজেরাই একটি বৃত্ত বানিয়েছে।
  2. ৫ম রাউন্ড. e2 = 1→4: 5 − 3 = 2 < 3, dist[4] = 2. e3 = 2→1: 2 + 2 = 4 < 5, dist[1] = 4. V − 1 রাউন্ডের পরও relax সফল — তাই negative cycle আছে। শেষ যে vertex relax হলো, x = 1.
  3. ৫ বার পেছনে. parent ধরে: 1 থেকে 2, 2 থেকে 3, 3 থেকে 4, 4 থেকে 1, 1 থেকে 2. পাঁচ ধাপ পর y = 2. এখানে x নিজেও cycle-এ ছিল, কিন্তু সাধারণভাবে তা নিশ্চিত নয় — x হতে পারত cycle থেকে বেরোনো কোনো লেজের vertex. V ধাপ হাঁটলে লেজ পেরিয়ে cycle-এ ঢোকা নিশ্চিত।
  4. Cycle সংগ্রহ. y = 2 থেকে parent ধরে: 2, 3, 4, 1, তারপর আবার 2 — থামা। parent উল্টো দিকে নির্দেশ করে (কে আমাকে relax করেছে), তাই তালিকা উল্টে দিলে আসল দিক: 2 → 1 → 4 → 3 → 2.
  5. যাচাই. ওজন: 2→1 (+2), 1→4 (−3), 4→3 (+2), 3→2 (−3), যোগফল −2. আসল চক্রটি আমরা নিজেরাই বানিয়েছিলাম — অ্যালগরিদম ঠিক সেটিই ধরেছে। প্রতিটি পাক 2 টাকা কমায়, তাই 1, 2, 3, 4-এর dist “−∞”; শুধু dist[0] = 0 অর্থপূর্ণ।

এবার পুরো অ্যালগরিদম কোডে — দুটি নির্দিষ্ট গ্রাফে: A (cycle নেই) আর B (শুধু e8-এর ওজন বদলানো)। শান্ত রাউন্ড পেলে থামে, নইলে V-তম রাউন্ডের শেষ relax-করা vertex থেকে cycle বের করে।

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
struct Edge { int u, v; long long w; };
const long long INF = 1'000'000'000'000'000'000LL;

void bellmanFord(int n, const vector<Edge>& edges, int src) {
    vector<long long> d(n, INF); vector<int> par(n, -1);
    d[src] = 0; int x = -1;
    for (int r = 1; r <= n; ++r) {                    // n-1 rounds + 1 check round
        x = -1;
        for (auto [u, v, w] : edges)                   // never relax out of "infinity"
            if (d[u] != INF && d[u] + w < d[v]) { d[v] = d[u] + w; par[v] = u; x = v; }
        if (x == -1) {                                 // a quiet round: nothing can change any more
            cout << "no negative cycle, quiet round = " << r << "\ndist:";
            for (long long dv : d) cout << ' ' << dv;
            vector<int> path;
            for (int v = 3; v != -1; v = par[v]) path.push_back(v);
            cout << "\npath to 3:";
            for (int i = (int)path.size() - 1; i >= 0; --i) cout << ' ' << path[i];
            cout << '\n';
            return;
        }
    }
    for (int i = 0; i < n; ++i) x = par[x];           // step back n times: x is now ON the cycle
    vector<int> cyc{x};
    for (int v = par[x]; v != x; v = par[v]) cyc.push_back(v);
    cyc.push_back(x);
    reverse(cyc.begin(), cyc.end());
    cout << "negative cycle: ";
    for (size_t i = 0; i < cyc.size(); ++i) cout << (i ? " -> " : "") << cyc[i];
    cout << '\n';
}
int main() {
    vector<Edge> g = {{4,3,2}, {1,4,-3}, {2,1,2}, {0,2,4}, {0,1,7}, {0,3,8}, {2,4,6}, {3,2,1}};
    cout << "Graph A\n"; bellmanFord(5, g, 0);
    g[7].w = -3;                                       // the bonus on 3->2 grows: weight 1 becomes -3
    cout << "Graph B\n"; bellmanFord(5, g, 0);
}

আউটপুট:

Graph A
no negative cycle, quiet round = 5
dist: 0 6 4 5 3
path to 3: 0 2 1 4 3
Graph B
negative cycle: 2 -> 1 -> 4 -> 3 -> 2

গ্রাফ A-তে ৫ নম্বর রাউন্ডটি শান্ত — ঠিক প্রথম ডায়াগ্রামের মতো: চার রাউন্ডে কাজ, পঞ্চমটি যাচাই। dist আর পথ দুটোই ডায়াগ্রামের সাথে মেলে। গ্রাফ B-তে কোনো রাউন্ডই শান্ত হয় না, তাই লুপ n টি রাউন্ড শেষ করে বেরিয়ে আসে, আর x-এ থাকে শেষ relax-করা vertex 1. তারপর ৫ বার par ধরে হেঁটে আর cycle সংগ্রহ করে উল্টে দিয়ে পাওয়া গেল দ্বিতীয় ডায়াগ্রামের চক্রটি। INF হিসেবে 1018 আর long long নেওয়া হয়েছে, আর d[u] != INF যাচাই থাকায় INF-এর সাথে কিছু যোগই হয় না — তাই overflow-এর ভয় নেই। সময় O(VE), জায়গা O(V) (edge list বাদে)।

INF = 10**18

def bellman_ford(n, edges, src):
    d, par = [INF] * n, [-1] * n
    d[src] = 0
    for r in range(1, n + 1):                 # n-1 rounds + 1 check round
        x = -1
        for u, v, w in edges:
            if d[u] != INF and d[u] + w < d[v]:   # never relax out of "infinity"
                d[v], par[v], x = d[u] + w, u, v
        if x == -1:                            # a quiet round: nothing can change any more
            print(f"no negative cycle, quiet round = {r}")
            print("dist:", *d)
            path, v = [], 3
            while v != -1:
                path.append(v)
                v = par[v]
            print("path to 3:", *path[::-1])
            return
    for _ in range(n):                         # step back n times: x is now ON the cycle
        x = par[x]
    cyc, v = [x], par[x]
    while v != x:
        cyc.append(v)
        v = par[v]
    cyc.append(x)
    print("negative cycle:", " -> ".join(map(str, cyc[::-1])))

g = [(4, 3, 2), (1, 4, -3), (2, 1, 2), (0, 2, 4), (0, 1, 7), (0, 3, 8), (2, 4, 6), (3, 2, 1)]
print("Graph A")
bellman_ford(5, g, 0)
g[7] = (3, 2, -3)                              # the bonus on 3->2 grows: weight 1 becomes -3
print("Graph B")
bellman_ford(5, g, 0)

আউটপুট C++-এর সাথে হুবহু এক:

Graph A
no negative cycle, quiet round = 5
dist: 0 6 4 5 3
path to 3: 0 2 1 4 3
Graph B
negative cycle: 2 -> 1 -> 4 -> 3 -> 2

d[v], par[v], x = d[u] + w, u, v এক লাইনে তিনটি হালনাগাদ — ডান পাশের মানগুলো আগে হিসাব হয়, তাই কোনো গোলমাল নেই। Python-এ পূর্ণসংখ্যা overflow করে না, তবু 10**18 INF আর d[u] != INF যাচাই রাখা হয়েছে, যাতে দুই ভাষার যুক্তি হুবহু এক থাকে; float('inf')-ও চলে, কিন্তু তখন dist-এ int আর float মিশে যায়। Python-এ প্রতিটি রাউন্ড C++-এর চেয়ে কয়েক ডজন গুণ ধীর, তাই V · E প্রায় 107 ছাড়ালে কনটেস্টে সময়সীমা নিয়ে সাবধান।

সব negative cycle, শুধু উৎস থেকে পৌঁছানো নয়? উৎস s থেকে পৌঁছানো যায় না এমন cycle Bellman-Ford দেখতে পায় না, কারণ সেখানে dist ∞ থেকেই যায়। গ্রাফের যেকোনো জায়গার cycle খুঁজতে (যেমন arbitrage, যেখানে যেকোনো মুদ্রা থেকে শুরু করা যায়) একটি কাল্পনিক উৎস q যোগ করুন, যার থেকে প্রতিটি vertex-এ ওজন-0 edge — অথবা সমানভাবে, শুরুতেই সব dist = 0 বসিয়ে দিন। আর যদি জানতে হয় কোন কোন vertex-এর দূরত্ব −∞, তাহলে V-তম রাউন্ডে relax হওয়া প্রতিটি vertex থেকে BFS বা DFS চালিয়ে যতদূর পৌঁছানো যায় সবাইকে −∞ চিহ্ন দিন।

৬ · SPFA — queue দিয়ে Bellman-Ford, আর তার ফাঁদ

প্রথম ডায়াগ্রামে দেখুন: রাউন্ড ৪-এ আটটি edge-এর সাতটি দেখা হয়েছে অযথা, কারণ তাদের শুরুর vertex-এর dist আগের রাউন্ড থেকে বদলায়নি। Edge u → v শুধু তখনই নতুন কিছু দিতে পারে, যখন dist[u] সম্প্রতি কমেছে। এই পর্যবেক্ষণ থেকে SPFA (Shortest Path Faster Algorithm): একটি queue-তে রাখুন শুধু সেই vertex-গুলো, যাদের dist কমেছে কিন্তু যাদের বাইরের edge এখনো দেখা হয়নি।

// SPFA: adj[u] = list of (v, w); returns false if a negative cycle is reachable from s
bool spfa(int n, const vector<vector<pair<int,long long>>>& adj, int s, vector<long long>& d) {
    d.assign(n, INF); vector<int> cnt(n, 0); vector<char> inq(n, 0);
    queue<int> q; d[s] = 0; q.push(s); inq[s] = 1;
    while (!q.empty()) {
        int u = q.front(); q.pop(); inq[u] = 0;
        for (auto [v, w] : adj[u])
            if (d[u] + w < d[v]) {
                d[v] = d[u] + w;
                if (!inq[v]) {                    // not already waiting in the queue
                    if (++cnt[v] >= n) return false;   // queued n times: negative cycle
                    q.push(v); inq[v] = 1;
                }
            }
    }
    return true;
}

এখানে queue থেকে বের হওয়া u-এর dist সবসময় সসীম, তাই আলাদা INF যাচাই লাগে না। Negative cycle ধরার নিয়ম: কোনো vertex n বার queue-তে ঢুকলে থামুন — cycle না থাকলে প্রতিটি vertex সর্বোচ্চ V − 1 বার উন্নত হতে পারে (প্রতিটি “রাউন্ড”-এ একবার)। বাস্তবের অনেক গ্রাফে, বিশেষত এলোমেলো sparse গ্রাফে, SPFA প্রায়ই Bellman-Ford-এর চেয়ে অনেক দ্রুত — অনেক সময় প্রায় O(E)-এর কাছাকাছি।

SPFA-র worst case এখনো O(VE). “গড়ে দ্রুত” মানে নিশ্চয়তা নয়। গ্রিডের মতো গ্রাফে বা বিশেষভাবে বানানো ইনপুটে কোনো vertex বারবার queue-তে ফেরে, আর সময় Bellman-Ford-এর worst case-এ পৌঁছে যায়। কনটেস্টে problem setter-রা প্রায়ই এমন test রাখেন যা ঠিক SPFA-কে ধীর করে দেয়। নিয়মটি সোজা: ঋণাত্মক edge না থাকলে কখনো SPFA নয়, Dijkstra. ঋণাত্মক edge থাকলে SPFA চলতে পারে, কিন্তু সময়ের হিসাব করবেন O(VE) ধরে। AI বা অনলাইন লেখায় “SPFA-র complexity O(E)” দেখলে বিশ্বাস করবেন না।

SPFA নামটি জনপ্রিয় হয় চীনে, ১৯৯৪ সালে Duan Fanding-এর একটি প্রবন্ধের পর, বিশেষ করে সেখানকার প্রতিযোগিতামূলক প্রোগ্রামিংয়ে। কিন্তু queue-ভিত্তিক এই ধারণা নতুন নয়: Edward F. Moore-এর ১৯৫৯ সালের কাজেই এর মূল রূপ ছিল, তাই পশ্চিমের বইয়ে একে প্রায়ই “queue-based Bellman-Ford” বা Bellman-Ford-Moore বলে।

৭ · সব জোড়ার shortest path — Floyd-Warshall

এ পর্যন্ত প্রশ্ন ছিল একটি উৎস থেকে — single-source shortest path. কিন্তু অনেক সময় দরকার প্রতিটি জোড়া (i, j)-এর দূরত্ব: যেমন দেশের ৬৪টি জেলা সদরের মধ্যে বাস ভাড়ার পুরো টেবিল, বা একটি কোম্পানির সব অফিসের মধ্যে নেটওয়ার্কের দেরি। এটি all-pairs shortest path (APSP). প্রতিটি vertex থেকে একবার করে Bellman-Ford চালালে O(V²E) — dense গ্রাফে O(V⁴). অনেক ভালো একটি উপায় আছে, আর তার কোড মাত্র তিনটি লুপ।

DP-র ধারণা — মাঝের vertex একে একে খুলে দেওয়া

vertex-গুলোকে নম্বর দিন 1, 2, …, V (এই অংশের গণিতে 1-based; কোডে 0-based). সংজ্ঞা দিন:

dk[i][j] = i থেকে j-এর সেরা খরচ, যদি মাঝের vertex শুধু {1, …, k} থেকে নেওয়া যায়

k = 0 মানে কোনো মাঝের vertex নিষেধ — শুধু সরাসরি edge: d0[i][j] = w(i, j), না থাকলে ∞, আর d0[i][i] = 0. এটিই গ্রাফের adjacency matrix. এবার k-তম vertex “খুলে দিলে” i থেকে j-এর সেরা পথ দুই রকম হতে পারে:

  • k ব্যবহার করে না: তাহলে উত্তর আগের মতোই, dk−1[i][j].
  • k ব্যবহার করে: negative cycle নেই বলে সেরা পথটি সরল, তাই k একবারই আসে। পথ ভাঙে দুই টুকরোয় — i থেকে k, আর k থেকে j — আর দুই টুকরোর মাঝের vertex-গুলো {1, …, k − 1} থেকে। Optimal substructure বলে দুটি টুকরোই নিজেরা সেরা: dk−1[i][k] + dk−1[k][j].
dk[i][j] = min( dk−1[i][j],  dk−1[i][k] + dk−1[k][j] )

k = V-তে সব vertex খোলা, তাই dV হলো আসল দূরত্ব। এটিই Floyd-Warshall অ্যালগরিদম. উপমায়: ঢাকা থেকে রাজশাহী যাওয়ার সেরা ভাড়া খুঁজছেন। প্রথমে শুধু সরাসরি বাস। তারপর অনুমতি দিলেন “সিরাজগঞ্জ হয়ে যাওয়া যাবে” — প্রতিটি জোড়ার জন্য দেখলেন সিরাজগঞ্জ হয়ে সস্তা কি না। তারপর “নাটোরও চলবে” — এবার সিরাজগঞ্জ আর নাটোর দুটোই মাঝে থাকতে পারে। প্রতিটি নতুন শহর খোলার পর টেবিলটি একবার হালনাগাদ।

d[i][j] (সারি = i, কলাম = j)সবুজ = এই k-তে কমেছে; সবুজ সারি-নম্বর = k

            
প্রতিটি ধাপে
মাঝে যে vertex চলবে
যা কমল
  1. শুরু: adjacency matrix. ছয়টি edge: 0→1 (3), 0→2 (8), 1→3 (1), 2→1 (5), 3→0 (2), 3→2 (−5). কর্ণে (diagonal) 0, edge না থাকলে ∞.
  2. k = 0. প্রতিটি জোড়ায় দেখি d[i][0] + d[0][j] ছোট কি না। 0-তে ঢোকে শুধু 3→0, তাই সারি 3-ই সুযোগ পায়: d[3][1] = d[3][0] + d[0][1] = 2 + 3 = 5 (∞ ছিল). d[3][2]-এর জন্য 2 + 8 = 10 > −5, বদল নেই।
  3. k = 1. 1 থেকে বেরোয় শুধু 1→3, তাই কলাম 3 লাভবান: d[0][3] = 3 + 1 = 4, d[2][3] = 5 + 1 = 6. d[3][3]-এর জন্য 5 + 1 = 6 > 0, বদল নেই — কর্ণ ঋণাত্মক হয় না।
  4. k = 2. d[3][1] = d[3][2] + d[2][1] = −5 + 5 = 0 < 5. লক্ষ করুন, এই উন্নতি k = 0-এর পাওয়া পথকে (3→0→1) হারিয়ে দিল — নতুন পথ 3→2→1.
  5. k = 3. 3 সবচেয়ে “কেন্দ্রীয়” vertex: d[0][2] = 4 + (−5) = −1 (পথ 0→1→3→2), d[1][0] = 1 + 2 = 3, d[1][2] = 1 − 5 = −4, d[2][0] = 6 + 2 = 8. কর্ণে সব 0 — কোনো negative cycle নেই। এটিই চূড়ান্ত দূরত্বের টেবিল।

কেন k সবার বাইরের লুপে — আর এক matrix-এ কেন চলে

সূত্রে dk নির্ভর করে শুধু dk−1-এর ওপর। তাই k-এর একটি মান শেষ না করে পরের মানে যাওয়া যায় না: k বাইরে, i আর j ভেতরে. ভেতরের দুটি লুপের ক্রম (i-j না j-i) নিয়ে চিন্তা নেই। কিন্তু ভুল করে i-j-k লিখলে প্রতিটি d[i][j] একবারই হিসাব হয়, তখনকার অর্ধেক-তৈরি মান দিয়ে। ছোট উদাহরণ: শুধু তিনটি edge 0→2, 2→3, 3→1, প্রতিটির ওজন 1. আসল d[0][1] = 3 (0→2→3→1). i-j-k ক্রমে (i = 0, j = 1)-এর সময় d[0][3] তখনো ∞ (সেটি হিসাব হবে পরে, j = 3-এ), আর d[2][1]-ও ∞ (i = 2 আসবে পরে) — তাই d[0][1] চিরকাল ∞ থেকে যায়।

আর দ্বিতীয় প্রশ্ন: dk−1 আর dk-এর জন্য দুটি আলাদা matrix লাগার কথা, একটিতে কেন চলে? কারণ ধাপ k-তে সারি k আর কলাম k বদলায় না: d[i][k]-এর নতুন মান = min(d[i][k], d[i][k] + d[k][k]), আর d[k][k] = 0 (negative cycle না থাকলে) — তাই একই থাকে। অর্থাৎ ধাপ k-তে যেসব মান পড়া হয় (d[i][k] আর d[k][j]), সেগুলো ওই ধাপে কেউ বদলায় না। জায়গা তাই Θ(V²), Θ(V³) নয়।

#include <iomanip>
#include <iostream>
#include <string>
#include <vector>
using namespace std;
const long long INF = 1'000'000'000'000'000'000LL;
struct Edge { int u, v; long long w; };

int main() {
    const int n = 4;
    vector<vector<long long>> d(n, vector<long long>(n, INF));
    vector<vector<int>> nxt(n, vector<int>(n, -1));
    for (int i = 0; i < n; ++i) { d[i][i] = 0; nxt[i][i] = i; }
    for (auto [u, v, w] : vector<Edge>{{0,1,3}, {0,2,8}, {1,3,1}, {2,1,5}, {3,0,2}, {3,2,-5}}) {
        d[u][v] = w; nxt[u][v] = v;
    }
    for (int k = 0; k < n; ++k)                         // k MUST be the outer loop
        for (int i = 0; i < n; ++i) {
            if (d[i][k] == INF) continue;
            for (int j = 0; j < n; ++j)
                if (d[k][j] != INF && d[i][k] + d[k][j] < d[i][j]) {
                    d[i][j] = d[i][k] + d[k][j];
                    nxt[i][j] = nxt[i][k];              // first step: same as the first step towards k
                }
        }
    bool neg = false;
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) cout << setw(4) << (d[i][j] == INF ? string("INF") : to_string(d[i][j]));
        cout << '\n';
        neg = neg || d[i][i] < 0;
    }
    cout << "negative cycle: " << (neg ? "yes" : "no") << '\n';
    int s = 0, t = 2;
    cout << "path " << s << " -> " << t << ":";
    for (int u = s; ; u = nxt[u][t]) { cout << ' ' << u; if (u == t) break; }
    cout << " (cost " << d[s][t] << ")\n";
}

আউটপুট:

   0   3  -1   4
   3   0  -4   1
   8   5   0   6
   2   0  -5   0
negative cycle: no
path 0 -> 2: 0 1 3 2 (cost -1)

Matrix-টি ডায়াগ্রামের শেষ ধাপের হুবহু, কর্ণে সব 0, তাই “negative cycle: no”. শেষ লাইনটি nxt থেকে বানানো পথ — পরের অংশে ধাপে ধাপে দেখবেন। if (d[i][k] == INF) continue; শুধু গতি বাড়ায় না, INF-এর সাথে যোগও ঠেকায়। সময় O(V³) — worst case-এ V³ বার ভেতরের if (d[i][k] ∞ এমন সারি বাদ পড়ায় কিছু কাজ বাঁচে, কিন্তু ঘন গ্রাফে Θ(V³)); জায়গা Θ(V²) দুটি matrix-এর জন্য। পথ না থাকলে nxt[s][t] −1, তাই বাস্তবে পথ ছাপার আগে d[s][t] != INF যাচাই করবেন।

INF = 10**18
n = 4
d = [[0 if i == j else INF for j in range(n)] for i in range(n)]
nxt = [[i if i == j else -1 for j in range(n)] for i in range(n)]
for u, v, w in [(0, 1, 3), (0, 2, 8), (1, 3, 1), (2, 1, 5), (3, 0, 2), (3, 2, -5)]:
    d[u][v], nxt[u][v] = w, v

for k in range(n):                            # k MUST be the outer loop
    for i in range(n):
        if d[i][k] == INF:
            continue
        for j in range(n):
            if d[k][j] != INF and d[i][k] + d[k][j] < d[i][j]:
                d[i][j] = d[i][k] + d[k][j]
                nxt[i][j] = nxt[i][k]         # first step: same as the first step towards k

for row in d:
    print("".join(f"{'INF' if x == INF else x:>4}" for x in row))
print("negative cycle:", "yes" if any(d[i][i] < 0 for i in range(n)) else "no")
s, t, path = 0, 2, [0]
while path[-1] != t:
    path.append(nxt[path[-1]][t])
print(f"path {s} -> {t}:", *path, f"(cost {d[s][t]})")

আউটপুট C++-এর সাথে হুবহু এক:

   0   3  -1   4
   3   0  -4   1
   8   5   0   6
   2   0  -5   0
negative cycle: no
path 0 -> 2: 0 1 3 2 (cost -1)

f"{x:>4}" মানটিকে ৪ ঘর চওড়া জায়গায় ডানে সাজায়, C++-এর setw(4)-এর মতো। Python-এ তিন-স্তরের লুপ ধীর: V = 500-এ 1.25 × 108 বার ভেতরের কাজ, যা সাধারণ Python-এ কয়েক দশ সেকেন্ড বা তার বেশি সময় নিতে পারে। তখন প্রতিটি k-তে পুরো matrix একসাথে হালনাগাদ করুন (যেমন NumPy দিয়ে d = np.minimum(d, d[:, k:k+1] + d[k:k+1, :])) — একই অ্যালগরিদম, অনেক গুণ দ্রুত।

খরচ — কখন Θ(V³) ঠিক আছে

V = 100 হলে 106 ধাপ — চোখের পলকে। V = 500 হলে 1.25 × 108 — C++-এ সাধারণত এক সেকেন্ডের মধ্যে, কারণ ভেতরের লুপ খুব সরল আর একটি সারি ধরে ক্রমানুসারে মেমরি পড়ে, যা cache-এর জন্য চমৎকার। V = 2000 হলে 8 × 109 — কয়েক সেকেন্ডের বেশি, সাধারণ কনটেস্টের সীমার বাইরে। তাই মোটা দাগের নিয়ম: V প্রায় ৫০০ পর্যন্ত Floyd-Warshall নিশ্চিন্তে; এর বেশি হলে আর গ্রাফ sparse হলে প্রতিটি vertex থেকে Dijkstra (বা ঋণাত্মক edge থাকলে Johnson) ভাবুন। Floyd-Warshall-এর আরেকটি সুবিধা হলো এটি লেখা প্রায় অসম্ভব রকম সহজ, তাই ভুলের সুযোগও কম।

৮ · Floyd-Warshall-এর আরও কাজ — negative cycle, পথ, transitive closure

কর্ণে negative cycle

d[i][i] শুরু হয় 0 থেকে। শেষে যদি d[i][i] < 0 হয়, তাহলে i থেকে বেরিয়ে i-তে ফেরার একটি পথ আছে যার খরচ ঋণাত্মক — অর্থাৎ i একটি negative cycle-এর ওপর। উল্টোটাও সত্য: i কোনো negative cycle-এ থাকলে সেই cycle-এর vertex-গুলো যখন একে একে খোলা হয়, d[i][i] ঋণাত্মক হয়ে যায়। তাই কোডে একটি লাইনই যথেষ্ট: কোনো i-তে d[i][i] < 0 আছে কি না। কোন জোড়ার দূরত্ব −∞? যদি এমন একটি k থাকে যাতে d[i][k] আর d[k][j] দুটোই সসীম এবং d[k][k] < 0 — অর্থাৎ i থেকে একটি negative cycle-এ পৌঁছে সেখান থেকে j-তে যাওয়া যায়।

Negative cycle থাকলে সংখ্যা ফুলে ওঠে। তখন প্রতিটি k-তে কিছু মান আরও ঋণাত্মক হতে থাকে, আর খারাপ গ্রাফে মানগুলো প্রায় exponential হারে বাড়তে পারে — V কয়েকশো হলেই long long-ও overflow করতে পারে। যদি ইনপুটে negative cycle সম্ভব হয়, তাহলে মানগুলোকে একটি নিচের সীমায় আটকে রাখুন (যেমন −INF-এর নিচে নামতে দেবেন না), অথবা আগে Bellman-Ford দিয়ে cycle আছে কি না দেখে নিন।

পথ পুনর্গঠন — next[][]

শুধু দূরত্ব নয়, পথও চাই — যেমন ভাড়ার টেবিলের পাশে “কোন কোন শহর হয়ে”. প্রতিটি জোড়ার জন্য পুরো পথ রাখলে Θ(V³) জায়গা লাগত। কৌশল হলো path reconstruction-এর জন্য শুধু একটি সংখ্যা রাখা: next[i][j] = “i থেকে j-এর সেরা পথে i-এর পরের vertex”. শুরুতে প্রতিটি edge i → j-এর জন্য next[i][j] = j. যখন k হয়ে নতুন পথ পাওয়া যায় (i → … → k → … → j), তখন i থেকে প্রথম ধাপটি k-এর দিকের প্রথম ধাপের সমান: next[i][j] = next[i][k]. পথ বের করতে i থেকে শুরু করে বারবার next[·][j] ধরে এগোন, যতক্ষণ না j-তে পৌঁছান।

চূড়ান্ত next matrix (সারি = এখন যেখানে, কলাম = গন্তব্য)সবুজ = এই ধাপে পড়া ঘর

            
প্রতিটি ধাপে
এখন যেখানে (u)
পরের vertex = next[u][2]
এ পর্যন্ত খরচ
  1. শুরু u = 0. next[0][2] = 1: 0 থেকে 2-এ যেতে প্রথমে 1-এ যান। এই মান এসেছে k = 3-এ, যখন d[0][2] কমে −1 হলো: তখন next[0][2] = next[0][3], আর next[0][3] k = 1-এ বসেছিল next[0][1] = 1.
  2. u = 1. next[1][2] = 3. এটিও k = 3-এ বসেছে; পথ 1→3→2-এর খরচ −4.
  3. u = 3. next[3][2] = 2 — সরাসরি edge 3→2 (ওজন −5), যা শুরু থেকেই সেরা।
  4. পৌঁছানো. পথ 0 → 1 → 3 → 2, খরচ 3 + 1 + (−5) = −1 = d[0][2]. প্রতিটি ধাপে একটি করে লাফ, তাই পথ বের করতে O(পথের দৈর্ঘ্য) ≤ O(V) সময়, আর পুরো next matrix-এ Θ(V²) জায়গা।

Transitive closure — শুধু “যাওয়া যায় কি না”

কখনো খরচ জানার দরকারই নেই, শুধু প্রশ্ন: i থেকে j-এ কোনো পথ আছে কি? যেমন একটি কোর্সের prerequisite-এর গ্রাফে “Data Structures না পড়ে কি Compiler Design-এ যাওয়া যায়?”, বা কোনো সফটওয়্যারে কোন module পরোক্ষভাবে কোন module-এর ওপর নির্ভর করে। সব জোড়ার এই উত্তরের টেবিলকে বলে transitive closure. Floyd-Warshall-এর min আর + বদলে দিন OR আর AND দিয়ে:

// Warshall: reach[i][j] = true if j can be reached from i (reach[i][i] = true initially)
for (int k = 0; k < n; ++k)
    for (int i = 0; i < n; ++i)
        if (reach[i][k])
            for (int j = 0; j < n; ++j)
                reach[i][j] = reach[i][j] || reach[k][j];
// faster: store each row as a bitset<N> and do  if (reach[i][k]) reach[i] |= reach[k];  --  O(V^3 / 64)

যুক্তি একই: “মাঝে শুধু {0…k} ব্যবহার করে i থেকে j-এ যাওয়া যায়” = আগেই যাওয়া যেত, অথবা i থেকে k-তে আর k থেকে j-তে যাওয়া যায়। প্রতিটি সারিকে bit-এর সারি (std::bitset) হিসেবে রাখলে ভেতরের j-লুপ একসাথে ৬৪টি করে bit সামলায়, তাই V = 2000-এর মতো আকারেও কাজ চলে। একই কাঠামো আরও অনেক “সব জোড়া”-র প্রশ্নে খাটে — যেমন minimax path (পথের সবচেয়ে বড় edge যতটা সম্ভব ছোট), যেখানে min আর + বদলে হয় min আর max.

আরও গভীরে — Johnson-এর অ্যালগরিদম: বড় sparse গ্রাফে ঋণাত্মক edge নিয়ে সব জোড়া চাইলে Donald B. Johnson-এর (১৯৭৭) কৌশল হলো একবার Bellman-Ford চালিয়ে প্রতিটি vertex-কে একটি potential h(v) দেওয়া — একটি কাল্পনিক উৎস q থেকে প্রতিটি vertex-এ ওজন-0 edge যোগ করে h(v) = δ(q, v) (negative cycle থাকলে এখানেই ধরা পড়ে)। তারপর প্রতিটি edge-এর ওজন বদলে দিন: w′(u, v) = w(u, v) + h(u) − h(v), যা সবসময় ≥ 0, কারণ shortest path-এর triangle inequality বলে h(v) ≤ h(u) + w(u, v). যেকোনো s থেকে t পথে মাঝের h-গুলো কেটে যায় (telescoping), তাই প্রতিটি পথের খরচ ঠিক h(s) − h(t) পরিমাণ সরে — সেরা পথ সেরাই থাকে, আর এই reweighting-এর পর প্রতিটি vertex থেকে Dijkstra চালিয়ে d(s, t) = d′(s, t) − h(s) + h(t). মোট সময় O(VE + V(V + E) log V) = O(VE log V) (সংযুক্ত গ্রাফে), যা sparse গ্রাফে Floyd-Warshall-এর Θ(V³)-এর চেয়ে অনেক কম.

৯ · কোন shortest-path অ্যালগরিদম কখন

এই মডিউলে এখন পর্যন্ত shortest path-এর পাঁচ-ছয়টি উপায় দেখেছেন। কনটেস্ট বা ইন্টারভিউতে সমস্যা পড়ে প্রথম কাজ হলো তিনটি প্রশ্নের উত্তর খোঁজা: ওজন কেমন (নেই, 0/1, ধনাত্মক, ঋণাত্মক)? গ্রাফে cycle আছে কি (DAG কি না)? একটি উৎস না সব জোড়া, আর V, E কত বড়?

পরিস্থিতিঅ্যালগরিদমসময়মনে রাখুন
ওজন নেই (সব edge সমান)BFS (পর্ব ৪০)O(V + E)queue-র স্তরই দূরত্ব
ওজন শুধু 0 বা 10-1 BFSO(V + E)deque: ওজন 0 হলে সামনে, 1 হলে পেছনে push
ওজন ≥ 0, এক উৎসDijkstra (পর্ব ৪৩)O((V + E) log V)একটিও ঋণাত্মক edge থাকলে নয়
DAG, যেকোনো ওজন (ঋণাত্মকসহ)topological order-এ DP (পর্ব ৪২)O(V + E)cycle নেই, তাই negative cycle-ও নেই; longest path-ও এভাবে
ঋণাত্মক edge, এক উৎস; negative cycle খোঁজাBellman-Ford (বা SPFA)O(VE)V-তম রাউন্ডে cycle; SPFA-র worst case-ও O(VE)
সব জোড়া, V প্রায় ৫০০ পর্যন্ত, ঋণাত্মক চলবেFloyd-WarshallΘ(V³)k বাইরে; কর্ণে negative cycle
সব জোড়া, ওজন ≥ 0, sparseপ্রতিটি vertex থেকে DijkstraO(V (V + E) log V)V বড় হলে Floyd-এর চেয়ে ভালো
সব জোড়া, ঋণাত্মক edge, বড় sparseJohnsonO(VE log V)Bellman-Ford একবার + reweighting + V বার Dijkstra
শুধু “পৌঁছানো যায় কি”, সব জোড়াWarshall (bitset)O(V³ / 64)অথবা প্রতিটি vertex থেকে BFS/DFS, O(V (V + E))

টেবিলের একটি নীতি: সবচেয়ে নির্দিষ্ট পরিস্থিতির সবচেয়ে দ্রুত অ্যালগরিদম আগে খুঁজুন। ওজনহীন গ্রাফে Dijkstra লিখলে উত্তর ঠিক আসবে, কিন্তু অকারণে log V গুণ ধীর; DAG-এ ঋণাত্মক ওজন দেখে Bellman-Ford লিখলে O(VE) লাগবে, অথচ topological DP O(V + E)-এ একই কাজ করে। আর উল্টো ভুলটি বিপজ্জনক: ঋণাত্মক edge-এ Dijkstra — দ্রুত, কিন্তু ভুল।

Bellman-Ford নামে দুজন, কিন্তু ইতিহাসে আছেন অন্তত তিনজন। Lester R. Ford Jr. ১৯৫৬ সালে RAND Corporation-এর “Network Flow Theory” প্রতিবেদনে relaxation-এর এই পদ্ধতি বর্ণনা করেন। Richard Bellman — dynamic programming শব্দটির জনক — ১৯৫৮ সালে “On a Routing Problem” প্রবন্ধে একে DP-র সমীকরণ হিসেবে লেখেন। আর Edward F. Moore ১৯৫৯ সালে “The Shortest Path Through a Maze”-এ প্রায় একই অ্যালগরিদম স্বাধীনভাবে দেন (এই প্রবন্ধটি BFS-এর ইতিহাসেও গুরুত্বপূর্ণ; BFS দেখেছেন পর্ব ৪০-এ)। তাই অনেক বই একে Bellman-Ford-Moore বলে। সব জোড়ার দিকে: ১৯৫৯ সালে ফরাসি গবেষক Bernard Roy transitive closure-এর অ্যালগরিদম প্রকাশ করেন; ১৯৬২ সালে Stephen Warshall স্বাধীনভাবে Boolean matrix-এর জন্য একই ধারণা দেন; আর একই বছর Robert W. Floyd Communications of the ACM-এ “Algorithm 97: Shortest Path” নামে মাত্র কয়েক লাইনে আজকের তিন-লুপের রূপটি ছাপেন (Floyd-এর build-heap পর্ব ২৩-এ দেখেছেন)। তাই নামটি কখনো Roy-Warshall বা Roy-Floyd-Warshall-ও লেখা হয়।

পরীক্ষা ও ইন্টারভিউতে:

  • Bellman-Ford-এ V − 1 রাউন্ড কেন? — negative cycle না থাকলে সেরা পথ সরল, তাতে ≤ V − 1 edge; রাউন্ড i-এর পর ≤ i edge-এর সব সেরা পথ ঠিক (induction).
  • Negative cycle কীভাবে ধরবেন আর দেখাবেন? — V-তম রাউন্ডে কোনো edge relax হলে cycle আছে; সেই vertex থেকে parent ধরে V বার পিছিয়ে cycle-এ ঢুকে, সেখান থেকে ঘুরে cycle সংগ্রহ।
  • Floyd-Warshall-এ লুপের ক্রম আর জটিলতা? — k বাইরে (dk নির্ভর করে dk−1-এর ওপর), i, j ভেতরে; সময় Θ(V³), জায়গা Θ(V²).
  • Dijkstra ঋণাত্মক edge-এ কেন চলে না, আর সব ওজনে ধ্রুবক যোগ করলে কেন হয় না? — greedy “চূড়ান্ত” ঘোষণা ভাঙে; ধ্রুবক যোগে বেশি edge-এর পথ বেশি শাস্তি পায়, তাই সেরা পথ বদলে যায়।
  • Currency arbitrage কীভাবে খুঁজবেন? — ওজন −log(হার), তারপর সব dist = 0 দিয়ে Bellman-Ford; negative cycle = arbitrage.

১০ · AI-এর যুগে shortest path — কোড চলে, উত্তর ভুল

“Bellman-Ford লিখে দাও” — AI মুহূর্তে লিখবে, আর ছোট ধনাত্মক উদাহরণে সেটি ঠিক উত্তরও দেবে। এই অ্যালগরিদমগুলোর ভুল লুকিয়ে থাকে বিশেষ পরিস্থিতিতে: পৌঁছানো যায় না এমন vertex, negative cycle, লুপের ক্রম, overflow. আর complexity-র প্রশ্নে AI প্রায়ই SPFA-কে O(E) বলে, বা “ঋণাত্মক edge থাকলেও negative cycle না থাকলে Dijkstra চলে” — এমন আত্মবিশ্বাসী কিন্তু ভুল দাবি করে।

Shortest-path কোডে AI সাধারণত যেসব ভুল করে

AI-এর ভুলকীভাবে ধরবেন
relax-এ dist[u] != INF যাচাই নেইউৎস থেকে পৌঁছানো যায় না এমন একটি অংশে ঋণাত্মক edge রাখুন — উত্তরে ∞-এর বদলে অদ্ভুত বড় সংখ্যা এলে ধরা পড়বে
INF = INT_MAX, তারপর INF + woverflow-এ ঋণাত্মক হয়ে যায়; একটি edge-হীন vertex দিয়ে চালান
Floyd-Warshall-এ লুপ i-j-k বা k ভেতরে0→2→3→1 শিকলের মতো ইনপুটে d[0][1] দেখুন; brute force-এর সাথে মেলান
Bellman-Ford V − 2 বা V রাউন্ড, বা যাচাই-রাউন্ড বাদপথের উল্টো ক্রমে সাজানো edge list (এই পর্বের গ্রাফ A) দিয়ে চালান
Cycle দেখাতে V-তম রাউন্ডের vertex থেকে সরাসরি শুরু (V ধাপ পিছিয়ে না)cycle-এর বাইরে একটি লেজ রাখুন — আউটপুটে লেজের vertex এলে ভুল
“SPFA O(E)”, বা ঋণাত্মক edge-এ Dijkstra সুপারিশপ্রমাণ চান; অংশ ১-এর তিন-vertex উদাহরণে হাতে চালিয়ে দেখান
Undirected গ্রাফে ঋণাত্মক edge রেখে Bellman-Fordদুই দিকের edge মিলে নিজেই negative cycle — প্রশ্নটি আসলে অন্য কিছু চাইছে কি না ভাবুন

AI-কে অ্যালগরিদম লিখে দিতে না বলে, আপনার কোড, trace আর যুক্তি পরীক্ষা করতে বলুন — আর প্রতিটি দাবির প্রমাণ চান:

আমি DSA শিখছি, এখন Bellman-Ford আর Floyd-Warshall. নিচে আমার লেখা কোড আর একটি ছোট গ্রাফে আমার হাতে করা trace (প্রতি রাউন্ডের dist টেবিল) দিলাম: [এখানে কোড আর trace পেস্ট করুন] পুরো সমাধান নতুন করে লিখে দিও না। শুধু এগুলো করো: ১. আমার trace-এর প্রতিটি রাউন্ড মিলিয়ে দেখো, কোথায় অমিল বলো। ২. উৎস থেকে পৌঁছানো যায় না এমন vertex থাকলে আমার relax কী করে — INF-এর সাথে যোগ হয় কি? ৩. আমার negative cycle খোঁজার কোড cycle-এর বাইরের লেজের vertex ছাপতে পারে কি? একটি ছোট উদাহরণ দিয়ে দেখাও। ৪. আমার Floyd-Warshall-এর লুপের ক্রম ঠিক কি না, d_k-এর সংজ্ঞা দিয়ে যুক্তি দাও। ৫. প্রতিটি complexity দাবির (সময় আর জায়গা) ধাপে ধাপে কারণ দাও, শুধু উত্তর নয়।

Shortest-path কোড যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Bellman-Ford কি আসলে dynamic programming? Richard Bellman আর “dynamic programming” নামের পেছনের গল্পটি কী?

Richard Bellman ১৯৫০-এর দশকে আমেরিকার RAND Corporation-এ কাজ করতেন, যেখানে সামরিক আর অর্থনৈতিক পরিকল্পনার গাণিতিক সমস্যা নিয়ে গবেষণা হতো। সেখানে তিনি বহু ধাপের সিদ্ধান্তের সমস্যার একটি সাধারণ পদ্ধতি দাঁড় করান: বড় সমস্যার সেরা উত্তরকে ছোট উপ-সমস্যার সেরা উত্তর দিয়ে লেখা — যাকে আজ Bellman equation বলে। নিজের আত্মজীবনীতে তিনি লিখেছেন, “dynamic programming” নামটি বাছাই করা হয়েছিল কিছুটা কৌশল করে: তখনকার প্রতিরক্ষা দপ্তরের একজন উচ্চপদস্থ কর্মকর্তা “research” শব্দটিই পছন্দ করতেন না, তাই এমন একটি নাম দরকার ছিল যা শুনতে প্রভাবশালী আর যার বিরুদ্ধে আপত্তি তোলা কঠিন। “Programming” শব্দটি তখন কম্পিউটারের কোড নয়, বরং “পরিকল্পনা” বা “সূচি তৈরি” অর্থে ব্যবহৃত হতো — যেমন linear programming.

১৯৫৮ সালের “On a Routing Problem” প্রবন্ধে Bellman shortest path-কে ঠিক এই ভাষায় লেখেন: প্রতিটি শহর i-এর জন্য fi = গন্তব্যে পৌঁছানোর সেরা সময়, আর fi = minj (tij + fj). এই সমীকরণ নিজের ওপর নির্ভর করে — অজানাটি দুই পাশেই। Bellman এর সমাধান করেন ধাপে ধাপে আনুমানিক মান হালনাগাদ করে, যতক্ষণ না মান স্থির হয়। এটিই আমাদের রাউন্ড-ভিত্তিক relaxation.

DP-র ভাষায় পরিষ্কার করে বললে: অবস্থা (state) হলো (i, v) — “সর্বোচ্চ i টি edge ব্যবহার করে v-তে পৌঁছানোর সেরা খরচ”. Transition: di(v) = min(di−1(v), minu→v di−1(u) + w). টেবিলের আকার V × V, প্রতিটি সারি পূরণে O(E), তাই O(VE). আর আমরা যে এক অ্যারেতে হালনাগাদ করি, সেটি DP-র সাধারণ “জায়গা বাঁচানো” কৌশল — knapsack-এ (পর্ব ৫৩) একই জিনিস দেখবেন। Floyd-Warshall-ও DP, শুধু state ভিন্ন: (k, i, j). Dijkstra-ও এক অর্থে DP, যেখানে উপ-সমস্যাগুলো সমাধানের ক্রম greedy ভাবে ঠিক করা হয় — এই কারণেই ঋণাত্মক ওজনে সেই ক্রম ভেঙে পড়ে।

এই দৃষ্টিভঙ্গির ব্যবহারিক লাভ আছে। “সর্বোচ্চ K টি edge ব্যবহার করে সবচেয়ে সস্তা ফ্লাইট” — এমন প্রশ্ন (যেমন “বড়জোর K টি স্টপ”) Dijkstra দিয়ে সরাসরি হয় না, কিন্তু Bellman-Ford-এর DP-তে ঠিক K রাউন্ড চালালেই হয় — শুধু প্রতিটি রাউন্ডে আগের রাউন্ডের অ্যারের একটি কপি থেকে পড়তে হয়, যাতে এক রাউন্ডে একাধিক edge না পেরোনো যায়।

মূল উপলব্ধি: Bellman-Ford হলো “edge-এর সংখ্যা” ধরে সাজানো একটি DP. অ্যালগরিদমকে DP হিসেবে দেখতে পারলে তার প্রমাণ, তার সীমা আর তার নতুন রূপ (যেমন K-edge সীমা) — তিনটিই সহজ হয়ে যায়।

প্র ০২ইন্টারনেটের router-গুলো কি Bellman-Ford চালায়? “count to infinity” সমস্যা কী?

হ্যাঁ, একটি বিতরণ করা (distributed) রূপে। ইন্টারনেটের প্রথম দিককার ARPANET-এর routing, আর পরে বহুল ব্যবহৃত RIP (Routing Information Protocol) — দুটোই distance-vector routing-এর ওপর দাঁড়ানো। প্রতিটি router শুধু জানে তার সরাসরি প্রতিবেশী কারা আর তাদের পর্যন্ত লিংকের খরচ কত। কিছুক্ষণ পরপর প্রতিটি router প্রতিবেশীদের কাছে নিজের “দূরত্বের তালিকা” পাঠায় — “আমি থেকে নেটওয়ার্ক X এত দূরে”. প্রতিবেশী সেটি পেয়ে relax করে: “ওই router হয়ে গেলে X আমার থেকে তার দূরত্ব + লিংকের খরচ; আমার এখনকার চেয়ে কম হলে হালনাগাদ।” এটিই Bellman-Ford, শুধু কেউ একসাথে সব edge দেখে না — প্রতিটি vertex নিজের edge-গুলো নিজে relax করে। অংশ ২-এর হাটের উপমাটি আসলে এটিই।

সমস্যা হয় কোনো লিংক ভেঙে গেলে। ধরুন router A-র মাধ্যমে B নেটওয়ার্ক X-এ যেত, দূরত্ব 2. A–X লিংক ভাঙল। A ভাবে, “B তো বলছে X তার থেকে 2 দূরে — তাহলে B হয়ে আমার দূরত্ব 3.” অথচ B-র সেই 2 আসলে A-র মাধ্যমেই ছিল! এরপর B শোনে A-র দূরত্ব 3, তাই নিজেরটা করে 4; A করে 5 … দুজনে মিলে ধীরে ধীরে সংখ্যা বাড়াতে থাকে, আর সেই সময়টায় প্যাকেট দুজনের মধ্যে ঘুরপাক খায়। এর নাম count to infinity. খারাপ খবর (দূরত্ব বেড়েছে) ধীরে ছড়ায়, কারণ Bellman-Ford শুধু মান কমাতে জানে — ভালো খবর দ্রুত, খারাপ খবর ধীরে।

RIP-এর সমাধান খুব বাস্তববাদী: “অসীম” = 16. কোনো পথ 15 hop-এর বেশি হলে ধরা হয় পৌঁছানো যায় না। এতে গুনতি দ্রুত শেষ হয়, কিন্তু RIP বড় নেটওয়ার্কে চলে না। আরও কৌশল আছে — split horizon (যে প্রতিবেশীর কাছ থেকে পথ শিখেছেন, তাকে সেই পথের খবর ফেরত না পাঠানো), poison reverse ইত্যাদি। বড় নেটওয়ার্ক তাই অন্য পথে গেছে: OSPF-এর মতো link-state protocol-এ প্রতিটি router পুরো মানচিত্র জানে আর নিজে Dijkstra চালায়; আর ইন্টারনেটের বড় অংশগুলোর মধ্যে BGP পুরো পথের তালিকা পাঠায়, যাতে লুপ সহজে ধরা যায়।

মূল উপলব্ধি: Bellman-Ford-এর “শুধু প্রতিবেশীর খবর দিয়ে হিসাব” ধর্মটিই একে বিতরণ করা সিস্টেমে জনপ্রিয় করেছে, আর সেই একই ধর্ম (মান শুধু কমে) থেকে আসে তার দুর্বলতা। অ্যালগরিদমের গাণিতিক ধর্ম সরাসরি নেটওয়ার্কের আচরণ হয়ে দেখা দেয়।

প্র ০৩মুদ্রা বাজারে negative cycle পেলেই কি টাকা বানানো যায়? বাস্তবের arbitrage-এ কী কী বাধা আছে?

গাণিতিক অংশটি পরিষ্কার: হার r-এর edge-এ ওজন −log r দিলে লাভজনক চক্র = negative cycle, আর Bellman-Ford O(VE)-এ সেটি খুঁজে দেয়। কয়েক ডজন মুদ্রার পূর্ণ টেবিলে V প্রায় ৫০, E প্রায় ২৫০০ — হিসাবটি এক মিলিসেকেন্ডেরও কম। সমস্যা গণিতের বাইরে।

প্রথম বাধা খরচ আর spread. বাস্তবে কেনা আর বেচার হার আলাদা (bid আর ask), প্রতিটি লেনদেনে ফি আছে। আমাদের উদাহরণের ১.২৭ শতাংশ লাভ বাস্তব বাজারে প্রায় অসম্ভব বড়; আসল সুযোগ সাধারণত শতাংশের অনেক ক্ষুদ্র ভগ্নাংশ, আর ফি সেটুকু খেয়ে ফেলে। ফি-কে মডেলে আনতে হলে প্রতিটি edge-এর হার ফি বাদ দিয়ে নিতে হয় — তাতে বেশিরভাগ “cycle” মিলিয়ে যায়।

দ্বিতীয় বাধা গতি. বড় বাজারে বহু প্রতিষ্ঠান স্বয়ংক্রিয় প্রোগ্রাম দিয়ে ঠিক এই চক্রগুলোই খোঁজে। একটি সুযোগ দেখা দিলে কারও না কারও কেনাবেচায় দাম সমন্বয় হয়ে যায় খুব দ্রুত — অনেক সময় এক সেকেন্ডের অনেক কম সময়ে। তাই আজ এই প্রতিযোগিতা মূলত নেটওয়ার্কের দেরি আর হার্ডওয়্যারের — অ্যালগরিদম সবারই জানা। তৃতীয় বাধা পরিমাণ: হার শুধু অল্প পরিমাণের জন্য সত্য, বড় পরিমাণ কিনলে দাম সরে যায়, তাই চক্রটি “অসীমবার ঘোরা” যায় না — গণিতের −∞ বাস্তবে কয়েক টাকায় থেমে যায়।

চতুর্থ বাধা একটি প্রোগ্রামিংয়ের: floating point. log-এর মান দশমিক সংখ্যা, আর যোগফল −10−12-এর মতো ক্ষুদ্র ঋণাত্মক হলে সেটি সত্যিকারের সুযোগ, না rounding-এর ভুল? তাই বাস্তব কোডে relax-এর শর্ত লেখা হয় একটি ছোট ব্যবধান ε দিয়ে: d[u] + w < d[v] - eps. এটি কনটেস্টেও কাজের — দশমিক ওজনের Bellman-Ford-এ ε ছাড়া অসীম লুপ বা মিথ্যা cycle হতে পারে।

তবে কাঠামোটি অন্য জায়গায় খুব কাজের: কোনো দামের নিয়মে বা রূপান্তরের টেবিলে অসঙ্গতি ধরা। ক্যাশব্যাক আর রিওয়ার্ড পয়েন্টের নিয়ম বানানোর সময় কোম্পানিগুলো ঠিক এই প্রশ্নই করে: কোনো চক্রে ঘুরে কি গ্রাহক অসীম পয়েন্ট বানাতে পারবেন? Negative cycle খোঁজা এখানে একটি নিরাপত্তা-পরীক্ষা।

মূল উপলব্ধি: মডেল (−log, negative cycle) সঠিক হলেও বাস্তবতা মডেলের অনুমানগুলো ভাঙে — ফি, গতি, পরিমাণ, দশমিকের নির্ভুলতা। অ্যালগরিদম জানা প্রথম ধাপ; কোন অনুমান কোথায় ভাঙে তা জানা দ্বিতীয়।

প্র ০৪Floyd-Warshall ভুল লুপ-ক্রমে লিখলে কি সবসময় ভুল? আর এক matrix-এ জায়গায় বসে হালনাগাদ নিরাপদ কেন?

অংশ ৭-এর শিকলের উদাহরণে দেখেছেন, i-j-k ক্রমে একবার চালালে উত্তর ভুল হতে পারে: প্রতিটি d[i][j] একবারই হিসাব হয়, আর তখন d[i][k] বা d[k][j] যদি এখনো চূড়ান্ত না হয়, তাহলে ভালো পথটি হারিয়ে যায়। মজার কথা, ভুল ক্রমটি পুরোপুরি অকেজো নয়। প্রতিটি হালনাগাদ কোনো একটি সত্যিকারের পথের খরচ দেয়, তাই মান কখনো আসল দূরত্বের নিচে নামে না — শুধু কিছু জোড়া “যথেষ্ট নামে না”. ২০১৯ সালে Ikumi Hide, Soh Kumabe আর Takanori Maehara একটি সুন্দর ফল প্রকাশ করেন: ভুল i-j-k ক্রমের লুপ পুরোটা তিনবার চালালে উত্তর সবসময় সঠিক হয়। কারণটি সংক্ষেপে: প্রতিবার চালানোয় যে পথগুলো ঠিক হয়, সেগুলো জুড়ে পরের বার আরও লম্বা পথ ঠিক হয়, আর তিন দফায় সব সেরা পথ ঢেকে যায়। এটি অবশ্য ভুল কোড লেখার অজুহাত নয় — তিন গুণ সময়, আর পরীক্ষায় নম্বর কাটা।

দ্বিতীয় প্রশ্নটি আরও গভীর। সংজ্ঞায় dk আর dk−1 দুটি আলাদা টেবিল; এক টেবিলে লিখলে ধাপ k-র মাঝে কিছু ঘর নতুন (dk), কিছু পুরোনো (dk−1). এটি নিরাপদ, কারণ ধাপ k যে ঘরগুলো পড়ে — সারি k-এর d[k][j] আর কলাম k-এর d[i][k] — সেগুলো ধাপ k-তে বদলায় না: d[i][k] বদলাতে হলে লাগত d[i][k] + d[k][k] < d[i][k], অর্থাৎ d[k][k] < 0, যা negative cycle ছাড়া অসম্ভব। তাই পুরোনো আর নতুন মান সেখানে এক। আর বাকি ঘরগুলো একটু আগে কমে গেলেও ক্ষতি নেই: সেগুলো তখনও কোনো সত্যিকারের পথের খরচ, আর আসলের নিচে নামে না — অংশ ৪-এর upper-bound যুক্তির মতো।

এই বিশ্লেষণ থেকে আরও একটি বাস্তব শিক্ষা আসে: negative cycle থাকলে d[k][k] < 0 হয়ে যায়, তখন এই “নিরাপত্তা” আর থাকে না, আর মানগুলো এলোমেলোভাবে ঋণাত্মক দিকে ছুটতে থাকে। সেজন্যই negative cycle থাকতে পারে এমন ইনপুটে আগে থেকে সাবধানতা লাগে (অংশ ৮-এর সতর্কতা)।

Floyd নিজে ১৯৬২ সালে পুরো অ্যালগরিদম প্রকাশ করেছিলেন একটি ছোট ALGOL কোড হিসেবে, প্রায় কোনো প্রমাণ ছাড়া — সেই যুগের Communications of the ACM-এর “Algorithms” বিভাগের রীতি অনুযায়ী। এর সঠিকতার পূর্ণ যুক্তি পরে পাঠ্যবইগুলো দাঁড় করায়, ঠিক এই dk-এর সংজ্ঞা দিয়ে।

মূল উপলব্ধি: তিন লাইনের কোডের প্রতিটি সিদ্ধান্ত — কোন লুপ বাইরে, এক টেবিল না দুটি — একটি স্পষ্ট invariant থেকে আসে। Invariant জানলে কোড মুখস্থ করতে হয় না, আর ভুল সংস্করণ দেখলেই চেনা যায়।

প্র ০৫O(VE) আর O(V³) কি শেষ কথা? ঋণাত্মক ওজনের shortest path আর সব জোড়ার সমস্যায় গবেষণা কোথায় দাঁড়িয়ে?

প্রায় ছয় দশক ধরে ঋণাত্মক ওজনের single-source shortest path-এ Bellman-Ford-এর O(VE) ছিল সাধারণ গ্রাফের জন্য সেরা strongly polynomial সীমা। মাঝে পূর্ণসংখ্যার ওজনের জন্য উন্নতি এসেছিল — যেমন Andrew Goldberg-এর ১৯৯০-এর দশকের scaling অ্যালগরিদম, যার সময় ওজনের আকারের log-এর ওপর নির্ভর করে। বড় চমক আসে ২০২২ সালে: Aaron Bernstein, Danupon Nanongkai আর Christian Wulff-Nilsen দেখান যে পূর্ণসংখ্যার ওজনে ঋণাত্মক edge-সহ shortest path প্রায় linear সময়ে — E-এর সাথে কয়েকটি log গুণক — সমাধান করা যায়। একই সময়ে flow-এর ওপর আরও সাধারণ গবেষণাও প্রায় linear সময়ের ফল দেয়। এগুলোর ধ্রুবক আর জটিলতা এত বেশি যে কনটেস্ট বা সাধারণ সফটওয়্যারে এখনো Bellman-Ford আর SPFA-ই চলে, কিন্তু তত্ত্বে “O(VE)-ই সীমা” ধারণাটি ভেঙে গেছে। এরপর বাস্তব সংখ্যার (দশমিক) ওজনেও Bellman-Ford-এর চেয়ে দ্রুত strongly polynomial অ্যালগরিদম নিয়ে কাজ এসেছে।

সব জোড়ার দিকটি আরও রহস্যময়। Floyd-Warshall Θ(V³); একে min-plus “matrix গুণ” হিসেবে দেখা যায় — সাধারণ matrix গুণে যেখানে গুণ আর যোগ, এখানে যোগ আর min. সাধারণ matrix গুণ Strassen (পর্ব ৪৯) থেকে শুরু করে V³-এর অনেক নিচে নেমেছে, কিন্তু min-এর বিয়োগ নেই বলে সেই কৌশল এখানে খাটে না। ১৯৭৬ সালে Michael Fredman দেখান V³-এর চেয়ে সামান্য (log-এর ভগ্নাংশ গুণ) কম সম্ভব, আর ২০১৪ সালে Ryan Williams circuit complexity-র কৌশলে উন্নতিটি আরও বাড়ান — V³-কে 2√log V-এর মতো একটি গুণক দিয়ে ভাগ। কিন্তু V3−ε (যেকোনো ধ্রুবক ε > 0) এখনো কেউ পারেননি।

এই ব্যর্থতাকে গবেষকেরা একটি অনুমান বানিয়েছেন — APSP hypothesis: সাধারণ ওজনের সব জোড়ার shortest path V3−ε সময়ে সম্ভব নয়। এটি fine-grained complexity নামের একটি ক্ষেত্রের ভিত্তি, যেখানে প্রমাণ করা হয় “এই সমস্যা দ্রুত হলে APSP-ও দ্রুত হতো” — পর্ব ৬৪-এর NP-completeness-এর reduction-এর মতো, কিন্তু polynomial সময়ের ভেতরে। গ্রাফের radius, negative triangle খোঁজাসহ বহু সমস্যা এভাবে APSP-এর সমান কঠিন প্রমাণিত হয়েছে। বিশেষ ক্ষেত্রে অবশ্য দ্রুত উপায় আছে: ওজনহীন undirected গ্রাফে Raimund Seidel-এর (১৯৯৫) অ্যালগরিদম দ্রুত matrix গুণ ব্যবহার করে V³-এর নিচে নামে।

মূল উপলব্ধি: পাঠ্যবইয়ের সীমাগুলো অনেক সময় “আমরা এর চেয়ে ভালো জানি না” — “এর চেয়ে ভালো অসম্ভব” নয়। কোনটি প্রমাণিত নিম্নসীমা আর কোনটি শুধু অনুমান, তা আলাদা করে জানা অ্যালগরিদম বিশ্লেষণের একটি গুরুত্বপূর্ণ দক্ষতা।

অনুশীলন

খাতা-কলম নিন। প্রথম তিনটি হাতে করার, শেষটিতে কোড লিখতে হবে।

  1. হাতে trace করুন: চারটি vertex A, B, C, D; উৎস A. Edge-গুলো প্রতি রাউন্ডে এই ক্রমে দেখা হয়: C→D (3), B→C (−2), A→B (4), A→C (5), D→B (1). প্রতিটি রাউন্ডের পর dist লিখুন। কয় রাউন্ডে কাজ শেষ, আর negative cycle আছে কি?

    শুরু: A = 0, বাকি ∞. রাউন্ড ১: C→D, B→C কাজে লাগে না (C, B ∞); A→B: B = 4; A→C: C = 5; D→B: D ∞, কিছু না। dist = (0, 4, 5, ∞). রাউন্ড ২: C→D: D = 5 + 3 = 8; B→C: 4 − 2 = 2 < 5, C = 2; D→B: 8 + 1 = 9 > 4. dist = (0, 4, 2, 8). রাউন্ড ৩: C→D: 2 + 3 = 5 < 8, D = 5; D→B: 6 > 4. dist = (0, 4, 2, 5).

    রাউন্ড ৪ (V-তম, যাচাই): কিছুই বদলায় না — negative cycle নেই। একমাত্র cycle B→C→D→B-এর ওজন −2 + 3 + 1 = 2 > 0, মিলে গেল। V − 1 = 3 রাউন্ডই লাগল, কারণ D-এর সেরা পথ A→B→C→D-তে ৩টি edge, আর edge-এর ক্রম সেই পথের উল্টো।

  2. Floyd-Warshall হাতে: তিনটি vertex 0, 1, 2; edge 0→1 (4), 0→2 (5), 1→2 (−2), 2→0 (3). প্রথমে adjacency matrix লিখুন, তারপর k = 0, 1, 2-এর প্রতিটির পর কোন ঘর বদলায় বলুন। চূড়ান্ত matrix কী?

    শুরু: সারি 0 = (0, 4, 5), সারি 1 = (∞, 0, −2), সারি 2 = (3, ∞, 0). k = 0: d[2][1] = 3 + 4 = 7. k = 1: d[0][2] = 4 + (−2) = 2 < 5. k = 2: d[1][0] = −2 + 3 = 1; বাকিরা বদলায় না (যেমন d[0][1]: 2 + 7 = 9 > 4).

    চূড়ান্ত: সারি 0 = (0, 4, 2), সারি 1 = (1, 0, −2), সারি 2 = (3, 7, 0). কর্ণে সব 0 — negative cycle নেই; একমাত্র cycle 0→1→2→0-এর ওজন 4 − 2 + 3 = 5. লক্ষ করুন d[0][2] সরাসরি edge 5 নয়, বরং 1 হয়ে 2.

  3. ভেবে বলুন: একজন বললেন, “ঋণাত্মক ওজন সমস্যা? সবচেয়ে ছোট ওজন −3, তাই সব edge-এ 3 যোগ করুন — সব ওজন ≥ 0 হবে, তারপর Dijkstra চালান, শেষে বাড়তিটুকু বাদ দিন।” এই গ্রাফে দেখান কেন এটি ভুল: S→T (1), S→A (2), A→T (−3).

    আসল উত্তর: S→A→T = 2 + (−3) = −1, যা সরাসরি S→T = 1-এর চেয়ে কম। ৩ যোগ করলে ওজন হয় S→T = 4, S→A = 5, A→T = 0. এখন S→A→T = 5, আর S→T = 4 — Dijkstra সরাসরি পথটিই বেছে নেবে। ভুল!

    কারণ: k টি edge-এর পথের খরচ বাড়ে 3k. দুই-edge-এর পথ ৬ শাস্তি পায়, এক-edge-এর পথ ৩ — তাই বেশি edge-এর পথগুলো অন্যায্যভাবে পিছিয়ে পড়ে। ঠিক উপায়ে ওজন বদলাতে চাইলে Johnson-এর potential লাগে: w′ = w + h(u) − h(v), যাতে যেকোনো s থেকে t পথের খরচ একই পরিমাণ (h(s) − h(t)) সরে, edge-এর সংখ্যা যা-ই হোক।

  4. কোড লিখুন: n টি মুদ্রা আর একটি n × n টেবিল rate[i][j] (i মুদ্রার ১ একক দিলে j মুদ্রার কত পাওয়া যায়) দেওয়া আছে। কোনো arbitrage চক্র আছে কি না বলার ফাংশন লিখুন, আর time ও space complexity বলুন।

    প্রতিটি জোড়ায় ওজন −log(rate). চক্র যেকোনো মুদ্রা থেকে শুরু হতে পারে, তাই কাল্পনিক উৎসের বদলে শুরুতে সব dist = 0. দশমিকের ভুল এড়াতে ছোট ε.

    bool hasArbitrage(const vector<vector<double>>& rate) {
        int n = rate.size();
        const double eps = 1e-12;
        vector<double> d(n, 0.0);                  // = a virtual source joined to every currency
        for (int round = 0; round < n; ++round) {  // n-1 rounds + the check round
            bool changed = false;
            for (int u = 0; u < n; ++u)
                for (int v = 0; v < n; ++v) {
                    if (u == v || rate[u][v] <= 0) continue;
                    double w = -log(rate[u][v]);
                    if (d[u] + w < d[v] - eps) { d[v] = d[u] + w; changed = true; }
                }
            if (!changed) return false;            // a quiet round: no profitable cycle
        }
        return true;                               // still improving after n rounds
    }

    এখানে গ্রাফ পূর্ণ (E = n²), তাই সময় O(n · n²) = O(n³), জায়গা O(n) (টেবিল বাদে)। উদাহরণের তিন মুদ্রায় (0.92, 0.86, 1.28) এটি true দেয়। cycle-টিও চাইলে parent রেখে অংশ ৫-এর মতো V বার পিছিয়ে যান। বিকল্প: Floyd-Warshall চালিয়ে কোনো d[i][i] < 0 আছে কি না দেখুন — একই O(n³).

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমNegative cycle যাচাইসহ Bellman-Ford-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. উৎসের dist = 0, বাকি সবার dist = ∞ আর parent = −1 বসানো
  2. একটি রাউন্ডে সব edge একবার করে relax করা (dist[u] ∞ হলে বাদ)
  3. রাউন্ডটি V − 1 বার পর্যন্ত চালানো, কোনো রাউন্ডে কিছু না বদলালে আগেই থামা
  4. আরও একবার সব edge দেখা — কোনোটি relax হলে negative cycle আছে
  5. Cycle থাকলে সেই vertex থেকে parent ধরে V বার পিছিয়ে cycle-টি সংগ্রহ করা

V টি vertex আর E টি edge-এর directed গ্রাফে negative cycle যাচাইসহ Bellman-Ford-এর worst case time complexity কত?

  • O((V + E) log V)
  • O(V + E)
  • O(V · E)
  • Θ(V³) সব গ্রাফে

Negative cycle নেই এমন গ্রাফে Bellman-Ford-এর জন্য V − 1 রাউন্ডই যথেষ্ট কেন?

  • প্রতিটি রাউন্ডে ঠিক একটি vertex চূড়ান্ত হয়
  • প্রতিটি vertex-এর এমন সেরা পথ আছে যাতে সর্বোচ্চ V − 1 edge, আর রাউন্ড i-এর পর ≤ i edge-এর সেরা পথগুলো ঠিক থাকে
  • Edge-গুলো উৎস থেকে দূরত্ব অনুযায়ী সাজিয়ে নিলে
  • কারণ গ্রাফে সর্বোচ্চ V − 1 টি edge থাকে

Floyd-Warshall শেষ হওয়ার পর কোন লক্ষণ দেখে বলা যায় যে গ্রাফে negative cycle আছে?

  • কোনো d[i][j] ঋণাত্মক
  • কোনো d[i][j] এখনো ∞
  • d[i][j] আর d[j][i] আলাদা
  • কোনো i-এর জন্য d[i][i] < 0

Floyd-Warshall-এর তিনটি লুপের মধ্যে কোনটি সবচেয়ে বাইরে থাকতেই হবে, আর কেন?

  • মাঝের vertex k, কারণ ধাপ k-এর সব মান ধাপ k − 1-এর পূর্ণ টেবিলের ওপর নির্ভর করে
  • উৎস i, কারণ প্রতিটি উৎস আলাদা করে সমাধান করতে হয়
  • গন্তব্য j, কারণ শেষ ঘর আগে ঠিক হওয়া দরকার
  • যেকোনোটি, কারণ শেষে সব জোড়া একবার করে দেখা হয়

মুদ্রা বিনিময়ের হার r-এর টেবিলে লাভজনক চক্র (arbitrage) খুঁজতে প্রতিটি edge-এর ওজন কী নিয়ে shortest-path অ্যালগরিদম চালাতে হয়?

  • w = r
  • w = 1/r
  • w = −log r
  • w = log r

একটি DAG (directed acyclic graph)-এ কিছু edge-এর ওজন ঋণাত্মক। একটি উৎস থেকে সব vertex-এর shortest path সবচেয়ে দক্ষভাবে কীভাবে বের করবেন?

  • Bellman-Ford, O(VE)
  • Topological order-এ প্রতিটি vertex-এর বাইরের edge একবার relax, O(V + E)
  • Dijkstra, O((V + E) log V)
  • Floyd-Warshall, Θ(V³)

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. ঋণাত্মক edge-এ Dijkstra চলে না; ধ্রুবক যোগ করেও বাঁচানো যায় না। arbitrage = ওজন −log r নিয়ে negative cycle.
  2. Bellman-Ford: সব edge V − 1 বার relax, dist[u] != INF যাচাইসহ; সময় O(VE), জায়গা O(V). প্রমাণ: সরল পথে ≤ V − 1 edge, edge-এর সংখ্যা ধরে induction.
  3. V-তম রাউন্ডে relax হলে negative cycle; parent ধরে V বার পিছিয়ে cycle বের করুন। শান্ত রাউন্ডে আগেই থামুন। SPFA-র worst case-ও O(VE).
  4. Floyd-Warshall: d[i][j] = min(d[i][j], d[i][k] + d[k][j]), k বাইরে; Θ(V³) সময়, Θ(V²) জায়গা, V ≈ ৫০০ পর্যন্ত। d[i][i] < 0 মানে cycle; next[i][j] = next[i][k] দিয়ে পথ; OR/AND দিলে transitive closure.
  5. বাছাই: BFS → 0-1 BFS → Dijkstra → DAG DP → Bellman-Ford → Floyd-Warshall → Johnson (potential দিয়ে reweighting + V বার Dijkstra).

পরের পর্বে: পথ নয়, এবার পুরো নেটওয়ার্ক — সবচেয়ে কম খরচে সব গ্রাম বিদ্যুৎ লাইনে জোড়া দেওয়া। Kruskal আর Prim-এর minimum spanning tree, union-find আর heap-এর মিলন, আর cut property দিয়ে greedy-র প্রমাণ।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান