পাঠ ৩৩ · ৫৭-এর মধ্যে · মডিউল ৭
Home / Courses / Design and Analysis of Algorithms / শর্টেস্ট পাথ

ডাইজকস্ট্রা ও বেলম্যান-ফোর্ড শর্টেস্ট পাথ

Dijkstra's and Bellman-Ford shortest paths
১২ মিনিট পড়া মধ্যম-কঠিন · Intermediate-Advanced Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • heapq ব্যবহার করে Dijkstra's-এর একটি প্রমিত, সম্পূর্ণ ইমপ্লিমেন্টেশন
  • edge relaxation লুপ ব্যবহার করে Bellman-Ford-এর একটি সম্পূর্ণ ইমপ্লিমেন্টেশন
  • কেন Dijkstra's-এর গ্রিডি "ফাইনালাইজেশন" নন-নেগেটিভ ওয়েট ছাড়া ভেঙে পড়ে — একটি প্রকৃত, কনস্ট্রাক্টেড কাউন্টার-এক্সাম্পল দিয়ে
  • নেগেটিভ-ওয়েট সাইকেল ডিটেকশন কীভাবে Bellman-Ford-এ "বিনামূল্যে" আসে

১ · Dijkstra's অ্যালগরিদম ও এর গ্রিডি অনুমান

Dijkstra's অ্যালগরিদম একটি min-heap দিয়ে প্রতিবার সবচেয়ে কম dist মানসহ ভার্টেক্স বেছে নেয়, একে ফাইনালাইজ করে (আর কখনো তার দূরত্ব পুনর্বিবেচনা করে না), এবং তার প্রতিবেশীদের রিল্যাক্স করে। এই "ফাইনালাইজেশন" ধাপটিই মূল গ্রিডি অনুমান — এটি সঠিক থাকে শুধুমাত্র যদি সব এজ ওয়েট নন-নেগেটিভ হয়, কারণ তখনই এই যুক্তিটি সত্য থাকে: "যদি $u$-এর বর্তমান দূরত্ব heap-এর সবচেয়ে ছোট হয়, তাহলে $u$-এ পৌঁছানোর আর কোনো ছোট পাথ থাকতে পারে না, কারণ যেকোনো অন্য পাথ অন্তত একটি অন্য (এখনও প্রসেস-না-হওয়া) ভার্টেক্স দিয়ে যেতে হবে, যার দূরত্ব ইতিমধ্যেই $u$-এর দূরত্বের চেয়ে বড় বা সমান — এবং নন-নেগেটিভ ওয়েটের কারণে সেই পাথ আরও লম্বাই হবে, কখনো ছোট হবে না।" নেগেটিভ ওয়েট থাকলে এই যুক্তিটি ভেঙে পড়ে — একটি "এখনও প্রসেস-না-হওয়া" ভার্টেক্স দিয়ে একটি নেগেটিভ এজ ব্যবহার করে হঠাৎ অনেক ছোট দূরত্বে পৌঁছানো সম্ভব, কিন্তু ততক্ষণে $u$ ইতিমধ্যে ভুল দূরত্বসহ ফাইনালাইজড হয়ে গেছে।

২ · Bellman-Ford অ্যালগরিদম — রিল্যাক্সেশন পুনরাবৃত্তি

Bellman-Ford কোনো গ্রিডি অনুমান করে না — এটি সহজভাবে সব $|V|-1$ বার প্রতিটি এজ $(u,v,w)$-এর জন্য রিল্যাক্সেশন চেষ্টা করে:

$$\text{if } dist[u] + w < dist[v]: \quad dist[v] \leftarrow dist[u] + w$$

$|V|-1$ বার পুনরাবৃত্তি যথেষ্ট কারণ যেকোনো শর্টেস্ট পাথ (নেগেটিভ সাইকেল না থাকলে) সর্বোচ্চ $|V|-1$টি এজ ব্যবহার করে (একটি সিম্পল পাথ কখনো একটি ভার্টেক্স পুনরাবৃত্তি করে না), এবং প্রতিটি পাস অন্তত একটি অতিরিক্ত এজ "সঠিকভাবে প্রোপাগেট" করে নিশ্চিত করে। $|V|-1$ পাসের পরও যদি কোনো এজ রিল্যাক্স করা সম্ভব হয়, তার মানে একটি নেগেটিভ-ওয়েট সাইকেল আছে — এটি অ্যালগরিদমের একটি বিল্ট-ইন সাইকেল-ডিটেকশন সুবিধা।

A (স) w = 1 (direct) w = 4 B C w = -10
A→B সরাসরি এজ (w=1) Dijkstra-কে B ফাইনালাইজ করতে বাধ্য করে সবার আগে (heap-এ সবচেয়ে ছোট)। কিন্তু A→C→B (1+... আসলে 4−10=−6) পথটি প্রকৃতপক্ষে ছোট — Dijkstra এটি আর বিবেচনা করে না, কারণ B ততক্ষণে ফাইনালাইজড।

৩ · সত্যিকারের ডেমো — Dijkstra's ভুল, Bellman-Ford সঠিক

নিচে আমরা ঠিক উপরের ডায়াগ্রামের গ্রাফটি ইমপ্লিমেন্ট করছি: $A \to B$ (ওজন ১), $A \to C$ (ওজন ৪), $C \to B$ (ওজন −১০)। প্রকৃত শর্টেস্ট দূরত্ব $A \to B$ হলো $\min(1, \; 4 + (-10)) = \min(1, -6) = -6$। আমরা উভয় অ্যালগরিদম চালিয়ে, এবং একটি সম্পূর্ণ স্বাধীন ব্রুট-ফোর্স অল-পাথ সার্চ (L30-এর স্টাইলে) দিয়েও, দেখব কী ঘটে।

Python
import heapq

def dijkstra(adj, source, n):
    # প্রমিত Dijkstra's: একবার একটি ভার্টেক্স "ফাইনালাইজড" (visited) হয়ে গেলে
    # তার দূরত্ব আর পুনর্বিবেচনা করা হয় না -- এটিই নন-নেগেটিভ-ওয়েট অনুমানের উপর নির্ভরশীল অংশ
    dist = {v: float("inf") for v in range(n)}
    dist[source] = 0
    visited = set()
    pq = [(0, source)]
    while pq:
        d, u = heapq.heappop(pq)
        if u in visited:
            continue
        visited.add(u)
        for v, w in adj.get(u, []):
            if v in visited:
                continue
            nd = d + w
            if nd < dist[v]:
                dist[v] = nd
                heapq.heappush(pq, (nd, v))
    return dist

def bellman_ford(edges, source, n):
    dist = {v: float("inf") for v in range(n)}
    dist[source] = 0
    for _ in range(n - 1):
        updated = False
        for u, v, w in edges:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                updated = True
        if not updated:
            break
    for u, v, w in edges:
        if dist[u] + w < dist[v]:
            raise ValueError("নেগেটিভ-ওয়েট সাইকেল ডিটেক্টেড -- শর্টেস্ট পাথ সংজ্ঞায়িত নয়")
    return dist

def brute_force_shortest(adj, source, target):
    # সম্পূর্ণ স্বাধীন: সোর্স থেকে টার্গেটে সব সিম্পল পাথের খরচ গণনা করে সবচেয়ে ছোটটি বের করা
    best = [float("inf")]
    def explore(u, visited, cost):
        if u == target:
            best[0] = min(best[0], cost)
        for v, w in adj.get(u, []):
            if v not in visited:
                visited.add(v)
                explore(v, visited, cost + w)
                visited.remove(v)
    explore(source, {source}, 0)
    return best[0]

# ভার্টেক্স: 0=A (সোর্স), 1=B, 2=C
adj = {0: [(1, 1), (2, 4)], 1: [], 2: [(1, -10)]}
edge_list = [(0, 1, 1), (0, 2, 4), (2, 1, -10)]
n = 3
names = {0: "A", 1: "B", 2: "C"}

dist_dijkstra = dijkstra(adj, 0, n)
dist_bf = bellman_ford(edge_list, 0, n)
true_dist_B = brute_force_shortest(adj, 0, 1)

print("ভার্টেক্স | Dijkstra's dist | Bellman-Ford dist | ব্রুট-ফোর্স সত্য দূরত্ব")
for v in range(n):
    truth = brute_force_shortest(adj, 0, v) if v != 0 else 0
    print(f"  {names[v]}      | {dist_dijkstra[v]:>15} | {dist_bf[v]:>18} | {truth}")

print(f"\nA থেকে B-এর প্রকৃত শর্টেস্ট দূরত্ব (ব্রুট-ফোর্স): {true_dist_B}")
print(f"Dijkstra's-এর গণনা: {dist_dijkstra[1]}  -> সঠিক? {dist_dijkstra[1] == true_dist_B}")
print(f"Bellman-Ford-এর গণনা: {dist_bf[1]}  -> সঠিক? {dist_bf[1] == true_dist_B}")

assert dist_dijkstra[1] != true_dist_B, "প্রত্যাশা ছিল Dijkstra's ভুল উত্তর দেবে এই গ্রাফে!"
assert dist_bf[1] == true_dist_B, "Bellman-Ford-এর সঠিক উত্তর দেওয়ার কথা ছিল!"
print("\nনিশ্চিত হলো: এই নেগেটিভ-এজ গ্রাফে Dijkstra's ভুল, Bellman-Ford সঠিক।")

    
কেন Dijkstra's এভাবে ব্যর্থ হয়, ধাপে ধাপে: শুরুতে dist = {A:0, B:inf, C:inf}। $A$ পপ হয়ে ফাইনালাইজড, প্রতিবেশী রিল্যাক্স করে dist[B]=1, dist[C]=4 — heap-এ এখন (1,B) ও (4,C)। যেহেতু $1 < 4$, $B$ এখনই পপ হয়ে ফাইনালাইজড হয়ে যায় (dist[B]=1 হিসেবে স্থায়ী)। এরপর $C$ পপ হয়, তার প্রতিবেশী $B$ রিল্যাক্স করার চেষ্টা হয় ($4 + (-10) = -6$), কিন্তু আমাদের ইমপ্লিমেন্টেশনে (এবং টেক্সটবুক-স্ট্যান্ডার্ড Dijkstra's-এ) ইতিমধ্যে ফাইনালাইজড ভার্টেক্সে রিল্যাক্সেশন প্রয়োগ করা হয় না — তাই $-6$ কখনো $dist[B]$-তে পৌঁছায় না। Bellman-Ford কোনো ভার্টেক্সকে "ফাইনালাইজ" করে না — এটি প্রতিটি পাসে সব এজ পুনরায় রিল্যাক্স করে, তাই $C \to B$ এজটি সঠিকভাবে $dist[B] = -6$-এ পৌঁছে দেয়।
মূল কথা · Key takeaway

Dijkstra's-এর $O(E \log V)$ কমপ্লেক্সিটি Bellman-Ford-এর $O(VE)$-এর চেয়ে ভালো, কিন্তু এই স্পিড একটি অনুমানের বিনিময়ে আসে — নন-নেগেটিভ ওয়েট। যদি একটি সমস্যায় নেগেটিভ ওয়েট থাকার সম্ভাবনা থাকে (যেমন কারেন্সি আর্বিট্রেজ ডিটেকশন, যেখানে "ওজন" হলো এক্সচেঞ্জ রেটের লগারিদম যা নেগেটিভ হতে পারে), Bellman-Ford ছাড়া আর কোনো নিরাপদ বিকল্প নেই — অথবা গ্রাফটিকে রি-ওয়েট করে (Johnson's অ্যালগরিদম-স্টাইল) Dijkstra's প্রযোজ্য করে তোলা, যা এই কোর্সের স্কোপের বাইরে।

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

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

প্র ০১ যদি আমাদের Dijkstra's ইমপ্লিমেন্টেশনে "ফাইনালাইজড ভার্টেক্স রিল্যাক্স না করা" চেকটি সরিয়ে ফেলা হতো (অর্থাৎ ফাইনালাইজড ভার্টেক্সেও রিল্যাক্সেশন প্রয়োগ করা হতো), তাহলে কি এই নির্দিষ্ট উদাহরণে সঠিক উত্তর পাওয়া যেত?

এই নির্দিষ্ট ছোট উদাহরণে, হ্যাঁ — dist[B] আপডেট হয়ে -৬ হয়ে যেত (কারণ $B$-এর কোনো আউটগোয়িং এজ নেই বলে সেই ভুল মান আর "প্রোপাগেট" হয়নি)। কিন্তু এটি একটি সাধারণ সমাধান নয় — বড় গ্রাফে যেখানে ফাইনালাইজড ভার্টেক্সের আউটগোয়িং এজ ইতিমধ্যে ব্যবহার হয়ে গেছে (তাদের প্রতিবেশীদেরও ভুল মান দিয়ে রিল্যাক্স করা হয়ে গেছে), শুধু চেক সরানো যথেষ্ট নয় — পুরো গ্রাফ জুড়ে বারবার রিল্যাক্সেশন (ঠিক যা Bellman-Ford করে) প্রয়োজন হবে।

প্র ০২ Bellman-Ford-এ ঠিক $|V|-1$ বার পুনরাবৃত্তি যথেষ্ট কেন — $|V|$ বা $2|V|$ কেন নয়?

একটি নেগেটিভ-সাইকেল-বিহীন গ্রাফে যেকোনো শর্টেস্ট পাথ একটি সিম্পল পাথ (কোনো ভার্টেক্স পুনরাবৃত্তি হয় না, কারণ পুনরাবৃত্তি করলে সেই চক্রটি অপসারণ করে সমান বা ছোট একটি পাথ পাওয়া যেত)। একটি সিম্পল পাথে সর্বোচ্চ $|V|-1$টি এজ থাকতে পারে ($|V|$টি ভার্টেক্স, তাই $|V|-1$টি সংযোগ)। প্রতিটি পাস নিশ্চিত করে অন্তত একটি অতিরিক্ত এজ শর্টেস্ট-পাথ ট্রি-তে সঠিকভাবে প্রোপাগেট হয়েছে, তাই $|V|-1$ পাসের পর সব শর্টেস্ট পাথ (যাদের সর্বোচ্চ $|V|-1$টি এজ) সম্পূর্ণরূপে গণনা হয়ে যায়।

প্র ০৩ উপরের গ্রাফে যদি $C \to B$ এজের ওজন $-10$-এর বদলে $-2$ হতো, তাহলে কি Dijkstra's তখনও ভুল উত্তর দিত?

না। তাহলে $A \to C \to B$ পথের খরচ হতো $4 + (-2) = 2$, যা $A \to B$ সরাসরি এজের খরচ $1$-এর চেয়ে বড় — তাই প্রকৃত শর্টেস্ট দূরত্ব এখনও $1$ থাকত (সরাসরি এজের মাধ্যমে), এবং Dijkstra's ইতিমধ্যে এই সঠিক উত্তরই দিয়ে দিয়েছিল। Dijkstra's তখনই ভুল হয় যখন নেগেটিভ এজ দিয়ে যাওয়া বিকল্প পথ ইতিমধ্যে-ফাইনালাইজড দূরত্বের চেয়ে প্রকৃতপক্ষে ছোট হয়ে যায় — এই থ্রেশহোল্ডটি নির্দিষ্ট গ্রাফের সংখ্যার উপর নির্ভরশীল।

অনুশীলন

  1. চিন্তা করুন: উপরের কোড সেলে যদি $A \to C$ এজের ওজন ৪-এর বদলে ২ করা হয় (আর $C \to B$-এর ওজন $-10$-ই থাকে), তাহলে dist_bf[1] কী হবে বলে আপনার ধারণা?

    নতুন পথ $A \to C \to B$-এর খরচ হবে $2 + (-10) = -8$, যা সরাসরি এজের খরচ $1$-এর চেয়ে ছোট — তাই Bellman-Ford dist_bf[1] = -8 দেবে। Dijkstra's তখনও ভুলভাবে $1$ দেখাবে, একই কারণে।

  2. পরীক্ষা করুন: কোড সেলে adj[0]-এ $A \to C$-এর ওজন ৪ থেকে ২ করে (এবং edge_list-এও একই পরিবর্তন করে) Run চেপে আপনার অনুমান যাচাই করুন।

    আউটপুটে দেখা যাবে dist_bf[1] = -8 এবং ব্রুট-ফোর্স সত্য দূরত্বও $-8$ দেখাবে, নিশ্চিত করে Bellman-Ford এখনও সঠিক। dist_dijkstra[1] এখনও ১ দেখাবে (আগের মতোই ভুল), কারণ Dijkstra's এখনও $B$-কে সবার আগে ($1 < 2$) ফাইনালাইজ করে ফেলবে।

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

  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ অল-পেয়ার্স শর্টেস্ট পাথ, ম্যাক্স ফ্লো, ব্যাকট্র্যাকিং, স্ট্রিং অ্যালগরিদম, অ্যামর্টাইজড অ্যানালাইসিস, NP-কমপ্লিটনেস ও অ্যাপ্রক্সিমেশন — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
  • Data Structures & Algorithms কোর্স সহোদর কোর্স Dijkstra's ও Bellman-Ford-এর মৌলিক ইমপ্লিমেন্টেশন ও প্র্যাকটিস প্রবলেম সেই কোর্সে কভার করা হয়েছে।
  • সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git, Theory of Computation, Engineering Economics, Full-Stack Web Frameworks, Mobile App Development, Ethics in Computing & AI Safety, Software Testing & Quality Assurance ও Design and Analysis of Algorithms — সব এক জায়গায়।
পূর্ববর্তী পাঠ
স্ট্রংলি কানেক্টেড কম্পোনেন্ট — টার্জান ও কোসারাজু