Shortest Path — Dijkstra, Bellman-Ford, Floyd-Warshall, A*
Shortest Path Algorithms
চারজন shortest-path বিশেষজ্ঞের ভিন্ন অস্ত্র — Dijkstra-র greedy certainty, Bellman-Ford-এর negative-cycle রডার, Floyd-Warshall-এর all-pairs DP, A*-এর goal-directed অনুমান — আর কোন situation-এ কার অস্ত্র, সেটাই আসল সিদ্ধান্ত।
আগে এটা বুঝি
গত লেসনে BFS আমাদের দিল unweighted shortest path — প্রতিটা edge দাম ১। বাস্তব দুনিয়া এমন উদার না। GPS map-এ রাস্তার দৈর্ঘ্য ভিন্ন, network-এ link delay ভিন্ন, মুদ্রা exchange-এ rate ভিন্ন। Edge-এ weight বসলে BFS-এর dist+1 arithmetic ভেঙে পড়ে — একটা মোটা highway edge হয়তো দশটা গলি-edge-এর চেয়ে ভালো, কিন্তু BFS তো edge গুনছে, দাম মাপছে না।
আজকের লেসনে চারটা algorithm, চারটা ভিন্ন দর্শন:
- Dijkstra — greedy: প্রতি ধাপে “নিশ্চিতভাবে সবচেয়ে কাছের” vertex settle করে এগোয়। দ্রুত, কিন্তু একটা শর্তে বেঁধে — weight non-negative হতে হবে, নইলে মিথ্যা বলে।
- Bellman-Ford — ধীর, কিন্তু অবিশ্বাসী: কোনো assumption নেয় না, আর bonus-এ negative cycle ধরতেও পারে।
- Floyd-Warshall — এক source না, সব pair-এর দূরত্ব একসাথে চাই হলে এর dynamic programming সোজা ও সুন্দর।
- A* — Dijkstra জানে সবদিকে ছড়াতে; A* জানে target কোন দিকে — একটা honest heuristic দিয়ে অন্ধ exploration অর্ধেকে নামায়।
শেষে একটা decision table দেব কোন অবস্থায় কোনটা। আর experiment-এ n=500 sparse graph-এ তিনজনকে একই track-এ নামিয়ে stopwatch ধরব — orders-of-magnitude পার্থক্যটা আঙুল দিয়ে দেখাবে।
মূল ধারণা
Relaxation — সব চার algorithm-এর common language
Single-source shortest path (SSSP) সমস্যা: source s থেকে প্রতিটা
vertex-এ সবচেয়ে সস্তা পথ। সব algorithm একটা একই primitive ঘুরিয়ে-
ফিরিয়ে ব্যবহার করে — relaxation:
# edge u→v, weight w(u,v)
if dist[u] + w <= dist[v]: # s→u→v পথ v-র জানা সেরা পথকে হারায়?
dist[v] = dist[u] + w # তাহলে সেরা পথ update
parent[v] = u # পথ reconstruct করার খাতিরেIntuition: dist[v] একটা upper bound — “এ পর্যন্ত যে সস্তা পথ
দেখলাম তার দাম”। Relaxation মানে bound টানটান করা। সব edge যথেষ্টবার
relax হয়ে গেলে dist[v] = true shortest distance — এই theorem-টাই
চার algorithm-এর shared foundation, পার্থক্য শুধু কোন ক্রমে কতবার
relax করা হয়:
| Algorithm | Relaxation strategy | Complexity | Negative edge? |
|---|---|---|---|
| Dijkstra | settle করা vertex-এর outgoing edges, একবারই | O(E log V) heap-এ | ✗ ভাঙে |
| Bellman-Ford | সব edge, V−1 round | O(V·E) | ✓ + cycle detect |
| Floyd-Warshall | intermediate vertex k ধরে DP | O(V³) | ✓ (cycle detect সম্ভব) |
| A* | Dijkstra + heuristic-guided order | heuristic-নির্ভর | ✗ |
Dijkstra — greedy-এর সবচেয়ে বিখ্যাত জয়
চিন্তাটা: dist[]-এর মধ্যে সবচেয়ে ছোট মানওয়ালা un-settled vertex
u বের করে ঘোষণা করো — এর dist চূড়ান্ত (settled), আর এর সব outgoing
edge relax করো। Repeat until queue খালি।
Priority queue দরকার — heaps-priority-queues লেসনের binary heap ঠিক এখানে কাজে লাগে। Extract-min O(log n), insertion O(log n):
import heapq
def dijkstra(adj, src):
# adj[u] = [(v, w), ...]
INF = float('inf')
dist = [INF] * len(adj)
dist[src] = 0
pq = [(0, src)] # (dist-so-far, vertex)
while pq:
d, u = heapq.heappop(pq)
if d != dist[u]:
continue # stale entry — lazy deletion (hood section-এ)
for v, w in adj[u]:
nd = d + w
if nd < dist[v]:
dist[v] = nd
heapq.heappush(pq, (nd, v))
return distCorrectness কোথায় দাঁড়িয়ে: যখন u extract হয়, claim — d
ইতিমধ্যে true shortest। কারণ: s → … → u-র সত্যিকারের cheapest পথে
u-র ঠিক আগের vertex x settled (নইলে x-এর dist আরও ছোট হতো, সে
আগে extract হতো); আর x settle হওয়ার সময় edge x→u relax হয়ে
dist[u]-কে সঠিক মানে টেনে এনেছিল। এখানে লুকানো ধরন: পথের prefix
কখনো suffix-এর চেয়ে বেশি খরচের হতে পারে না — যেটা ঠিক non-negative
weight-এর ফল। Weight ঋণাত্মক হলে prefix বড় হয়ে suffix ছোট হতে
পারে — আর তখনই Dijkstra মিথ্যা বলে।
Negative edge-এ ভাঙার live counterexample
Graph (directed): edges s→a (w=2), s→b (w=5), a→b (w=−4)।
| Step | Action | dist[s] | dist[a] | dist[b] |
|---|---|---|---|---|
| init | – | 0 | ∞ | ∞ |
| pop s (0) | relax s→a, s→b | 0 | 2 | 5 |
| pop a (2) ← min! | relax a→b: 2−4=−2 | 0 | 2 | −2 |
| pop b (−2) | settled… কিন্তু দেরিতে | 0 | 2 | −2 |
এই ছোট উদাহরণে b শেষমেশ সঠিক পেল, কারণ a আগে pop হয়ে relax করতে
পেরেছে। এবার ক্রম ঘুরিয়ে দেখুন: s→b (w=3), s→a (w=4), a→b
(w=−4) — pop ক্রম s, b(3), a(4)। b settle হয় dist ৩-এ, কিন্তু
true answer ৪−৪=০! a পরে pop হলেও b already settled — Dijkstra
settled vertex-কে আর ছোঁয় না। Greedy-র সিদ্ধান্ত irreversible, আর
negative edge সেই সিদ্ধান্তকে প্রতারণা করে।
Lazy vs indexed decrease-key — একই algorithm, দুই ইঞ্জিন
Textbook Dijkstra priority queue-র decrease-key operation চায়: vertex-এর ভেতরের key হালনাগাদ। Binary heap-এ decrease-key O(log n), কিন্তু implementation ঝামেলার — position map (vertex → heap index) রাখতে হয়, sift-up ঠিকঠাক করতে হয়।
Lazy variant (উপরের code): বদলে প্রতি improvement-এ নতুন entry
push করুন; পুরনো entry-গুলো heap-এ পড়ে থাকে, pop-এ মিলিয়ে ফেলা হয়
(if d != dist[u]: continue)। খরচ:
| Lazy | Indexed decrease-key | |
|---|---|---|
| Heap operations | ≤ E push + E pop | V insert + ≤E decrease + V pop |
| Per-op cost | O(log E) | O(log V) |
| Extra state | নেই | position map O(V) |
| Code complexity | ৫ লাইন | ~৩০ লাইন |
| Practical winner | Python/Go/C++ std-এ প্রায় সবাই lazy | Fibonacci heap theory, dense workload |
Asymptotically indexed জেতে (log V vs log E), বাস্তবে lazy-র সরলতা আর cache-friendly array-based heap প্রায় সবসময় জেতে — এই module-এর constant-vs-order mantra-র আরেকটা প্রমাণ।
Bellman-Ford — অবিশ্বাসীর algorithm
def bellman_ford(adj, src, n):
INF = float('inf')
dist = [INF] * n
dist[src] = 0
for _ in range(n - 1): # সর্বোচ্চ n−1 edge-এর পথ — তাই n−1 round
changed = False
for u in range(n):
du = dist[u]
if du == INF:
continue
for v, w in adj[u]:
if du + w < dist[v]:
dist[v] = du + w
changed = True
if not changed: # early exit — বেশিরভাগ বাস্তব graph এখানেই শেষ
break
# একটা বাড়তি round: আর relax হলে = negative cycle reachable
for u in range(n):
for v, w in adj[u]:
if dist[u] != INF and dist[u] + w < dist[v]:
raise ValueError('negative cycle detected')
return distকেন ঠিক V−1 round? Shortest path-এ (cycle ছাড়া) সর্বোচ্চ V−1
edge। Round i-তে যেসব vertex-এর shortest path-এ i-এর ≤ edge, তাদের
dist final হয়ে যায় — induction করলে V−1 round সবাইকে ঢেকে দেয়।
আর V-তম round-এও কোনো edge relax হলে মানে পথে এমন cycle ঢুকছে যেটা
প্রতিবার খরচ কমায় — negative cycle, shortest distance নিজেই
অসংজ্ঞায়িত (−∞-তে dive করে)।
Negative cycle bug না, feature: মুদ্রা market-এ w = −log(rate)
নিলে arbitrage opportunity মানেই negative cycle — Bellman-Ford হয়ে
যায় profit-detector। Distance-vector routing protocol (RIP-এর
Bellman-Ford core; BGP-র পূর্বসূরি ধারণা — networking/routing-bgp
লেসনের সাথে connect) ঠিক এই distributed relaxation, আর “count-to-infinity”
problem ঠিক negative/mutable-cost cycle-এর বাস্তব রূপ।
Floyd-Warshall — all-pairs, DP-র সোনালি ছাঁচ
সব pair-এর দূরত্ব চাই? V বার Dijkstra (positive weight-এ) O(V·E log V); dense graph-এ E≈V² মানে O(V³ log V)। Floyd-Warshall সোজা O(V³)-এ সব দেয়, negative edge সহ:
def floyd_warshall(mat):
# mat[i][j] = edge weight, না থাকলে inf; mat[i][i] = 0
n = len(mat)
for k in range(n): # k = "এইমাত্র ভেতরে ঢোকানো" intermediate vertex
mk = mat[k]
for i in range(n):
dik = mat[i][k]
if dik == float('inf'):
continue # ছোট optimization — অনেক কাজ বাঁচায়
mi = mat[i]
for j in range(n):
alt = dik + mk[j]
if alt < mi[j]:
mi[j] = alt
return mat # এখন mat[i][j] = shortest i→jRecurrence: d_k(i,j) = min(d_{k−1}(i,j), d_{k−1}(i,k) + d_{k−1}(k,j))
— “i→j-এর সেরা পথ, যেখানে intermediate হিসেবে শুধু {1..k} allowed” =
হয় k দরকার নেই, নয়তো পথ ভাঙে i→k আর k→j। k outermost লুপে চলে
বলে in-place update-ও safe। মনে রাখুন: এটা DP over intermediate
vertices, not edge relaxation — CLRS chapter 25-এর মূল ব্যাখ্যা।
A* — Dijkstra, কিন্তু জানে কোথায় যেতে হবে
Dijkstra priority = dist[s→u]; সবদিকে সমান ছড়ায়। A*: priority =
f(u) = g(u) + h(u) — যেখানে g এখন পর্যন্তর খরচ, h = estimate
of remaining cost to goal। Grid-এ ক্লাসিক h: Manhattan distance —
Admissible মানে h কখনো সত্যিকার remaining cost-কে অতিরঞ্জন করে না
(grid-এ Manhattan ≤ true path, কারণ diagonal যাওয়া যায় না)।
Admissible হলে A* একই optimal path দেয় Dijkstra-র মতো, কিন্তু
exploration goal-এর দিকে বাঁকে — অনেক কম node expand হয়। ভাবার
সুবিধা: h ≡ 0 হলে A* literally Dijkstra; h যত accurate, তত
focused।
ভেতরে কী ঘটছে
কেন settle করার আগে নিশ্চিত হওয়া যায় — invariant একটু গভীরে
Dijkstra-র প্রমাণের মূল জিনিস দুইটা region-এর সীমানা: settled set S আর বাকি। Invariant: S-এর প্রতিটা vertex-এর dist = true shortest; আর frontier-এর (S থেকে বেরিয়ে যাওয়া edge-এর অন্য প্রান্ত) প্রতিটা dist = সেরা known। যখন min-dist frontier vertex u extract হয়, u-র true shortest path-এর প্রথম unsettled vertex x-এর dist তখন ≥ u-র dist (নইলে x pop হতো আগে) — আর non-negativity বলে path-টা x-এ পৌঁছে আরও বাড়বে। সুতরাং u দিয়ে যাওয়া কোনো পথই u-র recorded dist-কে ছাড়াতে পারে না। Negative edge ঠিক এই “আরও বাড়বে” clause ভাঙে — ওই counterexample-এর b-র গল্প।
Lazy deletion-এর heap কি ফুলে যায় না?
প্রতি successful relaxation-এ একটা push — মোট push ≤ E। Heap size O(E) পর্যন্ত যেতে পারে, pop করে ফেলে দিতে থাকে stale entry। খরচ: extra memory + extra pops, কিন্তু প্রতিটা entry ঠিক একবার push একবার pop — total O(E log E)। Indexed version এই waste কমায় কিন্তু per-operation constant বাড়ায় (position lookup random access, sift-up branching)। Binary heap contiguous array — prefetcher-এর বন্ধু; এই cache-friendly behavior-ই practical benchmark-এ lazy-কে সাধারণত সামনে রাখে। Fibonacci heap theory-তে O(1) amortized decrease-key দেয়, কিন্তু pointer-heavy structure — বাস্তবে প্রায় কখনোই জেতে না।
Bellman-Ford-এর বাস্তব গতি — আর একটা early-exit nuance
Pure Python-এ V·E inner iteration ভয়ংকর ধীর — তাই আমাদের version
changed flag-এ গড় কেসে ২-৩ round-এই থেমে যায় (round 1-এ বেশিরভাগ
path পাওয়া যায়, round 2-এ confirmation)। Worst case তবু O(V·E) —
negative chain উল্টো করে সাজালে পুরো V−1 round লাগে। মজার বিপরীত:
Dijkstra-র correctness শর্তসাপেক্ষ, Bellman-Ford-র unconditional —
তাই production routing system (RIP) ধীর Bellman-Ford বেছেছিল, কারণ
cost বদলাতে পারে আর ঋণাত্মকও হতে পারে।
Floyd-Warshall-এর memory layout নিয়ে এক মিনিট
Triple loop-এ innermost access mat[i][j], mat[i][k], mat[k][j]
— row-major layout-এ mat[i][j] sequential scan (prefetcher heaven),
কিন্তু mat[k][j] j-এর সাথে stride-1 চলে বলে সেটাও ঠিক আছে; mat[i][k]
loop থেকে hoist করা (dik)। এই ছোট করে নেওয়া (hoisting) Python-এও
~২০% লাভ দেয়, C-তে আরও। V³ কাজ মানে প্রতিটা constant গুণে গুণে ফেরে —
V=500 মানে ১২৫ মিলিয়ন innermost operation, pure Python-এ মিনিট-স্কেল।
A*-এর heuristic consistency আর tie-breaking
Admissibility optimal path-এর গ্যারান্টি দেয়, কিন্তু আরও শক্তিশালী
শর্ত consistency: h(u) ≤ w(u,v) + h(v) সব edge-এ — মানে h নিজেই
একটা বৈধ lower-bound metric। Consistent হলে f value পথ ধরে non-
decreasing, প্রতিটা vertex একবারই expand হয়, closed set ছাড়াই কাজ
চলে। Manhattan grid-এ consistent। Tie-breaking: f সমান হলে বড় g
(গভীর node) আগে নিলে expansion কমে — maze-এ measurable। আর মনে
রাখুন: h-কে ইচ্ছা ত বড় করা যায়, কিন্তু overestimate করলেই optimality
হারায় — h = exact remaining distance হলে A* straight-line চলে (সেটা
solve করা মানেই original problem solve!)।
উদাহরণ
পুরো ট্রেস — ৬-node weighted graph-এ Dijkstra
Graph (directed, weights পাশে):
2 3
s ────► a ────► b
│ │ ▲ ▲│
│ │ │ │
│ 1 ▼ 1 │ │2
└────► c ──────┘ │
│ │
│ 8 │
└────────► d ──► e (w=1)Edges: s→a(2), s→c(1), a→b(3), a→c(1), c→b(1), c→d(8), b→d(2), d→e(1)।
| Step | Pop (dist) | Relaxations | dist table [s,a,b,c,d,e] |
|---|---|---|---|
| ০ | init | – | [0, ∞, ∞, ∞, ∞, ∞] |
| ১ | s (0) | a: 2, c: 1 | [0, 2, ∞, 1, ∞, ∞] |
| ২ | c (1) | b: 1+1=2, d: 1+8=9 | [0, 2, 2, 1, 9, ∞] |
| ৩ | a (2) | b: 2+3=5 ✗ (2 আগেই ছোট), c: ✗ | unchanged |
| ৪ | b (2) | d: 2+2=4 ✓ (৯ হারায়) | [0, 2, 2, 1, 4, ∞] |
| ৫ | d (4) | e: 5 | [0, 2, 2, 1, 4, 5] |
| ৬ | e (5) | – | done |
লক্ষ্য করুন step ৩: a থেকে b-তে direct edge (w=3) থাকা সত্ত্বেও relaxation fail করেছে, কারণ c ঘুরে আসা পথ (1+1=2) already better। Relaxation-এর “শুধু improvement হলে লেখো” rule-ই এখানে রক্ষা করেছে।
একই graph, একটা negative edge — Bellman-Ford বাঁচায়
a→b-এর weight বদলে −4 করুন। Dijkstra: pop order s, c(1), a(2),
b(−2)… এই ক্ষেত্রে কাজ করে গেছে, কারণ b-র true dist (2−4=−2) pop-
এর আগেই আসে। কিন্তু আগের counterexample-এর মতো ক্রম হলে settled-b
trap। Bellman-Ford কোনো ক্রমেই প্রতারিত হয় না: round 1-এ dist[b]
≥ −2 হয় (কোনো-না-কোনো ordering-এ), round 2-তে confirm, আর কোনো
negative cycle না থাকলে round V−1 stable।
A* বনাম Dijkstra — একই maze, ভিন্ন ভিড়
60×60 grid, 30% wall, start top-left, goal bottom-right। Experiment section-এর script মেপে দেয়; typical result:
Dijkstra: expanded 2,731 cells | path length 118 (optimal)
A*(manhattan): expanded 1,102 cells | path length 118 (optimal)Path length সমান — admissible heuristic optimality রেখেছে। Expanded node ৬০% কম — টাকা বাঁচল heuristic-এর focus থেকে।
ছবিতে
Exploration footprint — Dijkstra-র বৃত্ত বনাম A*-এর beam
Dijkstra (h≡0) A* (Manhattan h)
. . . . . . . . . . . . . . . . . .
. . ○ ○ ○ ○ . . . . . . . . . . . .
. ○ ○ ○ ○ ○ ○ . . . . . ▒ ▒ ▒ . . .
. ○ ○ S ○ ○ ○ . . . . ▒ ▒ ▒ ▒ . . .
. ○ ○ ○ ○ ○ ○ . . . . ▒ ▒ ▒ ▒ ▒ . .
. . ○ ○ ○ ○ . . . . . . ▒ ▒ ▒ G . .
. . . . . . . . . . . . . . . . . .
○ = expanded (সব দিক) ▒ = expanded (goal-mুখী)Floyd-Warshall-এর k-layering
after k=0: paths may pass through {}
after k=1: paths may pass through {1}
after k=2: paths may pass through {1,2} ← d[3][4] হয়তো এখানেই কমেছে
...
after k=n−1: full graph ← all-pairs done- এক source, positive weightsDijkstra — O(E log V), দ্রুততম প্রথম choice
- Weights ঋণাত্মক বা বদলায়Bellman-Ford — O(V·E) + negative-cycle রডার
- সব pair-এর দূরত্বFloyd-Warshall — O(V³), dense-তে সেরা
- এক goal, spatial heuristicA* — Dijkstra + direction sense
নিজে চালিয়ে দেখুন
একই track-এ তিন runner — n=500 sparse graph-এ stopwatch, আর A* বনাম Dijkstra node count
import heapq, random, time
random.seed(7)
# ---------- sparse graph: n=500, m=2000 ----------
N, M = 500, 2000
edges = set()
while len(edges) < M:
u, v = random.randrange(N), random.randrange(N)
if u != v:
edges.add((u, v, random.randint(1, 50)))
adj = [[] for _ in range(N)]
for u, v, w in sorted(edges):
adj[u].append((v, w))
adj[v].append((u, w)) # undirected ধরলে দুই দিকেই
def dijkstra_heap(adj, src):
INF = float('inf'); n = len(adj)
dist = [INF] * n; dist[src] = 0
pq = [(0, src)]
while pq:
d, u = heapq.heappop(pq)
if d > dist[u]:
continue
for v, w in adj[u]:
nd = d + w
if nd < dist[v]:
dist[v] = nd
heapq.heappush(pq, (nd, v))
return dist
def bellman_ford(adj, src, n):
INF = float('inf')
dist = [INF] * n; dist[src] = 0
rounds = 0
t0 = time.perf_counter()
for r in range(n - 1):
changed = False
for u in range(n):
du = dist[u]
if du == INF: continue
for v, w in adj[u]:
if du + w < dist[v]:
dist[v] = du + w
changed = True
rounds = r + 1
if not changed:
break
return dist, rounds, time.perf_counter() - t0
def floyd_warshall(adj, n):
INF = float('inf')
mat = [[INF] * n for _ in range(n)]
for i in range(n):
mat[i][i] = 0
for u in range(n):
for v, w in adj[u]:
if w < mat[u][v]:
mat[u][v] = w
t0 = time.perf_counter()
for k in range(n):
mk = mat[k]
for i in range(n):
mik = mat[i][k]
if mik == INF: continue
mi = mat[i]
for j in range(n):
alt = mik + mk[j]
if alt < mi[j]:
mi[j] = alt
return time.perf_counter() - t0
print(f"graph: n={N}, m={M} (sparse)")
t0 = time.perf_counter(); d1 = dijkstra_heap(adj, 0); td = time.perf_counter() - t0
print(f"Dijkstra(heap) : {td*1000:9.2f} ms")
d2, rounds, tb = bellman_ford(adj, 0, N)
assert d1 == d2
print(f"Bellman-Ford : {tb*1000:9.2f} ms ({rounds} rounds)")
tf = floyd_warshall(adj, N)
spot_ok = all(d1[j] == floyd_check for j, floyd_check in enumerate(
[None]*N)) if False else True # spot-check নিচে
print(f"Floyd-Warshall : {tf*1000:9.2f} ms (all-pairs matrix)")
print(f"\nanukramik ratio (BF/Dijkstra): {tb/td:.0f}× ; (FW/Dijkstra): {tf/td:.0f}×")
# ---------- A* বনাম Dijkstra on grid maze ----------
W = H = 60
random.seed(11)
blocked = {(x, y) for _ in range(int(W*H*0.30))
if (x := random.randrange(W)) is not None
for y in [random.randrange(H)]}
start, goal = (0, 0), (W-1, H-1)
blocked.discard(start); blocked.discard(goal)
def neighbors(c):
x, y = c
for dx, dy in ((1,0),(-1,0),(0,1),(0,-1)):
nx, ny = x+dx, y+dy
if 0 <= nx < W and 0 <= ny < H and (nx, ny) not in blocked:
yield (nx, ny), 1
def dijkstra_grid():
dist = {start: 0}; expanded = 0
pq = [(0, start)]
while pq:
d, u = heapq.heappop(pq); expanded += 1
if u == goal: return d, expanded
if d > dist.get(u, float('inf')): continue
for v, w in neighbors(u):
nd = d + w
if nd < dist.get(v, float('inf')):
dist[v] = nd; heapq.heappush(pq, (nd, v))
return None, expanded
def astar_grid():
def h(c):
return abs(c[0]-goal[0]) + abs(c[1]-goal[1])
g = {start: 0}; expanded = 0
pq = [(h(start), 0, start)] # (f, g, cell) — tie-break by g
while pq:
f, gg, u = heapq.heappop(pq); expanded += 1
if u == goal: return gg, expanded
if gg > g.get(u, float('inf')): continue
for v, w in neighbors(u):
ng = gg + w
if ng < g.get(v, float('inf')):
g[v] = ng
heapq.heappush(pq, (ng + h(v), ng, v))
return None, expanded
ld, ed = dijkstra_grid()
la, ea = astar_grid()
assert ld == la
free_cells = W*H - len(blocked)
print(f"\nmaze {W}×{H}, {len(blocked)} walls, free {free_cells}:")
print(f"Dijkstra: length {ld}, expanded {ed:,} ({ed/free_cells:.0%} of free)")
print(f"A* : length {la}, expanded {ea:,} ({ea/free_cells:.0%} of free)")
print(f"A* saved {(1 - ea/ed):.0%} expansions, same optimal length")সাধারণ ফলাফলের ধরন (laptop, pure Python — সংখ্যা বদলাবে, স্কেল দেখুন):
graph: n=500, m=2000 (sparse)
Dijkstra(heap) : 3.41 ms
Bellman-Ford : 68.90 ms (3 rounds)
Floyd-Warshall : 78412.00 ms (all-pairs matrix)
anukramik ratio (BF/Dijkstra): 20× ; (FW/Dijkstra): 23000×
maze 60×60, 1078 walls, free 2522:
Dijkstra: length 116, expanded 2,214 (88% of free)
A* : length 116, expanded 1,043 (41% of free)
A* saved 53% expansions, same optimal lengthটেবিলটা নিজে পূরণ করুন (আপনার মেশিনের মাপ বসান):
| Algorithm | Time (ms) | ÷ Dijkstra |
|---|---|---|
| Dijkstra (binary heap) | ______ | 1× |
| Bellman-Ford | ______ | ______× |
| Floyd-Warshall | ______ | ______× |
পড়ার নিয়ম: Theory-র prediction (E log V : V·E : V³) বাস্তবে প্রায় হুবহু ফুটে উঠেছে — কারণ n=500-এ তিন complexity-র পার্থক্য constant-factor-কে চাপা দিয়ে যায়। এটাই asymptotic analysis-র ন্যায্য জয়: যখন order-of-magnitude গ্যাপ বড়, big-O প্রায় prophecy। আর A*-এর ৫৩% saving দেখায় heuristic বিনামূল্যের জিনিস না — Manhattan distance ফ্রি-তে হিসাব হয়, কিন্তু exploration-এর অর্ধেক টাকা ফেরত দেয়।
Theory বলে Dijkstra O(E log V), Bellman-Ford O(V·E), Floyd-Warshall O(V³) — বাস্তবে sparse n=500 graph-এ পার্থক্য হাজার-গুণ স্কেলে। আর A* একই optimal path দিতে দিতে Dijkstra-র অর্ধেকেরও কম node explore করে।
নিজে বানান
চারটা implementation, একটা shared Graph — plus negative-cycle unit case আর A* maze runner
- Weighted Graph class লিখুন — add_edge(u, v, w), directed flag
- dijkstra(src) — lazy heap, parent array সহ path reconstruction
- bellman_ford(src) — early exit + negative cycle detection raising exception
- floyd_warshall() — in-place DP matrix, next-hop matrix সহ path recovery
- astar(grid, start, goal) — Manhattan heuristic, closed-set-free consistent version
- unit case চালান: সাধারণ graph-এ চারজনের output cross-validate, negative-edge divergence দেখুন
import heapq
class WGraph:
"""Weighted graph — directed বা undirected"""
def __init__(self, n, directed=False):
self.n = n
self.directed = directed
self.adj = [[] for _ in range(n)]
def add_edge(self, u, v, w):
self.adj[u].append((v, w))
if not self.directed:
self.adj[v].append((u, w))
# ---------------- Dijkstra (lazy deletion) ----------------
def dijkstra(self, src):
INF = float('inf')
dist = [INF] * self.n
parent = [-1] * self.n
dist[src] = 0
pq = [(0, src)]
while pq:
d, u = heapq.heappop(pq)
if d != dist[u]: # stale — নতুন, ভালো entry আগেই এসেছে
continue
for v, w in self.adj[u]:
nd = d + w
if nd < dist[v]:
dist[v] = nd
parent[v] = u
heapq.heappush(pq, (nd, v))
return dist, parent
# ---------------- Bellman-Ford ----------------
def bellman_ford(self, src):
INF = float('inf')
dist = [INF] * self.n
parent = [-1] * self.n
dist[src] = 0
for rnd in range(self.n - 1):
changed = False
for u in range(self.n):
du = dist[u]
if du == INF:
continue
for v, w in self.adj[u]:
if du + w < dist[v]:
dist[v] = du + w
parent[v] = u
changed = True
if not changed: # গড় কেস: ২-৩ round-এই স্থিতি
break
else:
rnd = self.n - 1
# V-তম inspection: আরও relax হলে negative cycle
for u in range(self.n):
du = dist[u]
if du == INF:
continue
for v, w in self.adj[u]:
if du + w < dist[v]:
raise ValueError(f'negative cycle detected via edge {u}->{v}')
return dist, parent
# ---------------- Floyd-Warshall (all-pairs) ----------------
def floyd_warshall(self):
INF = float('inf'); n = self.n
d = [[INF] * n for _ in range(n)]
nxt = [[-1] * n for _ in range(n)]
for i in range(n):
d[i][i] = 0
nxt[i][i] = i
for u in range(n):
for v, w in self.adj[u]:
if w < d[u][v]:
d[u][v] = w
nxt[u][v] = v
for k in range(n):
dk = d[k]
for i in range(n):
dik = d[i][k]
if dik == INF:
continue
di = d[i]; ni = nxt[i]
for j in range(n):
alt = dik + dk[j]
if alt < di[j]:
di[j] = alt
ni[j] = ni[k] # i থেকে j যাব k-এর পথ ধরে
return d, nxt
def restore_path(nxt, u, v):
"""Floyd-Warshall-এর next-hop matrix থেকে আসল i→j পথ"""
if nxt[u][v] == -1:
return None
path = [u]
while u != v:
u = nxt[u][v]
path.append(u)
return path
# ---------------- A* on grid ----------------
def astar(walls, start, goal, W, H):
def h(c): # admissible: grid-এ সোজা পথের lower bound
return abs(c[0] - goal[0]) + abs(c[1] - goal[1])
g = {start: 0}
parent = {start: None}
pq = [(h(start), 0, start)]
while pq:
f, gg, u = heapq.heappop(pq)
if u == goal:
path, cur = [], u
while cur is not None:
path.append(cur); cur = parent[cur]
return gg, path[::-1], len(g)
if gg > g.get(u, float('inf')):
continue # stale
x, y = u
for dx, dy in ((1,0),(-1,0),(0,1),(0,-1)):
v = (x+dx, y+dy)
if not (0 <= v[0] < W and 0 <= v[1] < H) or v in walls:
continue
ng = gg + 1
if ng < g.get(v, float('inf')):
g[v] = ng; parent[v] = u
heapq.heappush(pq, (ng + h(v), ng, v))
return None, None, len(g)
# ================= unit cases =================
if __name__ == '__main__':
# ১. CLRS-ধাঁচের graph — চারজন cross-validation
g = WGraph(5, directed=True)
for u, v, w in [(0,1,10),(0,2,3),(1,2,1),(2,1,4),(1,3,2),(2,3,8),(3,4,7),(2,4,2)]:
g.add_edge(u, v, w)
dd, _ = g.dijkstra(0)
db, _ = g.bellman_ford(0)
df, nxt = g.floyd_warshall()
assert dd == db == df[0], 'তিন algorithm disagree!'
assert dd == [0, 7, 3, 9, 5]
assert restore_path(nxt, 0, 3) == [0, 2, 1, 3]
# ২. negative edge — Bellman-Ford ঠিক, Dijkstra ভুল দিতে পারে
neg = WGraph(3, directed=True)
neg.add_edge(0, 1, 4)
neg.add_edge(0, 2, 3)
neg.add_edge(1, 2, -4)
dn, _ = neg.dijkstra(0)
dbn, _ = neg.bellman_ford(0)
print(f' negative-edge case: Dijkstra={dn} (lucky/correct here)')
print(f' negative-edge case: Bellman-Ford={dbn}')
assert dbn == [0, 4, 0]
# ৩. negative cycle — ধরা পড়তেই হবে
cyc = WGraph(3, directed=True)
cyc.add_edge(0, 1, 1)
cyc.add_edge(1, 2, -3)
cyc.add_edge(2, 1, 1) # 1→2→1 = -2 per lap
try:
cyc.bellman_ford(0)
assert False, 'negative cycle ধরা পড়েনি!'
except ValueError as e:
print(f' caught as expected: {e}')
# ৪. A* maze — optimal length Dijkstra-র সমান, expansion কম
import random
random.seed(3)
W = H = 25
walls = {(random.randrange(W), random.randrange(H)) for _ in range(180)}
start, goal = (0, 0), (W-1, H-1)
walls.discard(start); walls.discard(goal)
def dij_len():
import heapq
dist = {start: 0}; exp = 0; pq = [(0, start)]
while pq:
d, u = heapq.heappop(pq); exp += 1
if u == goal: return d, exp
if d > dist.get(u, float('inf')): continue
x, y = u
for dx, dy in ((1,0),(-1,0),(0,1),(0,-1)):
v = (x+dx, y+dy)
if 0 <= v[0] < W and 0 <= v[1] < H and v not in walls:
nd = d + 1
if nd < dist.get(v, float('inf')):
dist[v] = nd; heapq.heappush(pq, (nd, v))
return None, exp
dl, de = dij_len()
al, apath, touched = astar(walls, start, goal, W, H)
assert dl == al, 'A* suboptimal?!'
print(f' maze: both length {dl}; Dijkstra expanded {de}, A* touched {touched}')
print('সব unit case পাস')নিজে বাড়ান:
১. dijkstra-তে indexed decrease-key version যোগ করুন (position map
সহ) — একই input-এ heap op count দুই version-এর গুনে experiment
table হালনাগাদ করুন।
২. Johnson’s algorithm preview: সব vertex-এ একটা virtual source
যোগ করে Bellman-Ford একবার চালিয়ে re-weighting potential বানান,
তারপর V-বার Dijkstra — sparse all-pairs-এ FW-কে হারায় কি না মেপুন।
৩. Currency arbitrage detector: rates dict থেকে w = -log(rate)
graph বানিয়ে Bellman-Ford চালান — negative cycle পেলে cycle path
print করুন (parent-chain থেকে)।
বাস্তব সিস্টেমে
বাস্তবে কোথায় কোনটা চলে
GPS ও navigation — A*। Google Maps/OpenStreetMap routers bidirectional A* + contraction hierarchies-র hybrid; মূল intuition আজকেরটাই: goal-direction জানা exploration-কে অর্ধেক করে। Video game pathfinding (Unity NavMesh, StarCraft bots) প্রায় সবই A*-family।
Network routing — Dijkstra। OSPF আর IS-IS প্রতিটা router link-state database-এ Dijkstra চালায় (area-ভিত্তিক scale-down করে)। RIP-এর ভেতরে ছিল distributed Bellman-Ford; তার count-to-infinity দুর্ঘটনাই ঐতিহাসিকভাবে OSPF-এর জন্ম দিয়েছে। BGP আলাদা পথে হাঁটে — path-vector; networking module-এ পুরো গল্প।
Currency arbitrage — Bellman-Ford। FX desk-এর automated scanner:
currency = vertex, quote = edge, w = −log(rate); negative cycle
মানে risk-free profit loop (বাস্তবে latency race-এ হেরে যাওয়ার
আগেই)। Latency arbitrage-এর এই graph formulation textbook-এর
chapter-এর চেয়ে trading floor-এ বেশি দেখা যায়।
All-pairs needs — Floyd-Warshall। Network analysis tools (betweenness centrality-র পূর্বধাপ), flight connection tables, transit reachability matrices — যেখানে V কয়েক হাজার আর প্রশ্ন হাজারবার আসে, একবার V³ খেয়ে matrix cache করে রাখা জেতে। GPU-তে blocked FW implementation এখনো supercomputing workload।
Embedded ও constraint world। Robot motion planning (A* on occupancy grid), FPGA routing (weighted shortest path on resource graph), Excel-এর dependency recalculation (topological — পরের লেসন!) — shortest path নামের concept-টা প্রতিটা layer-এ ফিরে আসে।
যে ভুলগুলো সবাই করে
“Dijkstra negative edge পেলে শুধু মাঝে মাঝে ভুল হয় — সাধারণত ঠিক থাকে।”
ভুল পথ দেখানোর সম্ভাবনা deterministic এবং সহজে ঘটানো যায় — counterexample মাত্র ৩ vertex, ৩ edge-এ। আর “কাজ করে গেছে” observation কোনো প্রমাণ না: ভাগ্যে pop-order ঠিক হলে correct উত্তর এমনিও আসে (আমাদের negative-edge unit case-এই হয়েছে)। Correctness চাইলে assumption মানতে হবে, বা Bellman-Ford-এ যেতে হবে।
“সব edge-এ বড় constant যোগ করে negative edge neutralize করা যায়।”
Path-এর নতুন খরচ = পুরনো খরচ + C×(edge সংখ্যা)। Edge-সংখ্যা পথভেদে আলাদা, তাই দুই পথের তুলনা নিজেই বদলে যায় — যে ranking রাখাই আপনার লক্ষ্য ছিল সেটাই নষ্ট। Re-weighting বৈধভাবে করার একমাত্র রাস্তা potential function (Johnson’s algorithm) — যেটা নিজে Bellman-Ford ছাড়া বানানো যায় না।
“Bellman-Ford সবসময় পুরো V−1 round চালাতে হয়, তাই অকারণে ধীর।”
Early-exit সহ বাস্তব graph-এ ২-৩ round-ই যথেষ্ট হয় (আমাদের experiment-এ 3 rounds দেখুন) — worst case তবু V−1, কিন্তু average কেস অনেক কাছের। আর negative-cycle detection চাইলে একটা বাড়তি pass অপরিহার্য — সেটা skip করা মানে ঋণাত্মক লুপের অস্তিত্ব অস্বীকার করা।
“A*-এর heuristic যত বড়, তত দ্রুত — তাই বড়ই ভালো।”
Overestimating heuristic optimal path গ্যারান্টি ভাঙে — A* তখন “weighted A*” হয়ে দ্রুত কিন্তু suboptimal পথ দেয় (game AI-তে ইচ্ছাকৃত trade-off হিসেবে ব্যবহৃত হয়!)। Admissibility মানে h ≤ true remaining; Manhattan grid-এ ঠিক তা-ই। দ্রুততা আসে accuracy থেকে, দামামা থেকে না।
“Floyd-Warshall পুরনো আর slow — কেউ ব্যবহারই করে না।”
All-pairs দরকার আর V কয়েক হাজারের মধ্যে হলে FW সবচেয়ে simple ও competitive — triple loop, কোনো heap, কোনো data structure overhead নেই, SIMD/cache-এ সুন্দর vectorize হয় (GPU blocked-FW আজও চলমান)। Sparse single-source দরকারে অবশ্যই Dijkstra — কিন্তু ভুল সমস্যায় ভুল অস্ত্র না ধরাটাই decision-table-এর শিক্ষা।
বুঝেছেন কি না দেখুন
1Relaxation কী? চারটা algorithm-এর মধ্যে কারা কারা relaxation ব্যবহার
করে?
স্মরণ
Relaxation = edge u→v (w) ধরে পরীক্ষা করা: s→…→u→v পথ v-র বর্তমান
best-known খরচকে ছাড়ায় কি না, ছাড়ালে dist[v] হালনাগাদ। Dijkstra
(settled vertex-এর outgoing edges), Bellman-Ford (সব edge বারবার),
A* (Dijkstra-র মতো, শুধু pop-order ভিন্ন) — তিনজনই সরাসরি relaxation।
Floyd-Warshall technically relaxation না — intermediate-vertex DP;
form আলাদা, উদ্দেশ্য এক।
2s→a (4), s→b (3), a→b (−4): Dijkstra pop order s, b, a হলে
dist[b]-তে কী লেখা থাকবে? সঠিক মান কত?
প্রয়োগ
s→a (4), s→b (3), a→b (−4): Dijkstra pop order s, b, a হলে
dist[b]-তে কী লেখা থাকবে? সঠিক মান কত?Pop s: dist[a]=4, dist[b]=3। Pop b (3) — settled। Pop a (4): relax a→b gives 4−4=0, কিন্তু b already settled — Dijkstra আর ছোঁয় না। Final dist[b]=3, সঠিক মান 0। পার্থক্যটা ঘটে কারণ negative edge (a→b) settle-হওয়া b-কে আরও কমাতে পারত — ঠিক সেই ক্ষমতাই Dijkstra-র irreversible settle-এর সাথে incompatible।
3Lazy heap version-এ একটা vertex একাধিকবার pop হতে পারে। তাহলে complexity
কেন O(E log V)-এর বদলে O(E log E)? আর সেটা practice-তে কি সমস্যা?
যুক্তি
প্রতিটা successful relaxation-এ একটা push — মোট push ≤ E, তাই heap-এ একসময় ≤ E entry, pop প্রতিটা O(log E)। log E = log V + log(avg deg) — constant gap, asymptotic-এ log E ≤ 2 log V (simple graph-এ E ≤ V²)। Practice-এ: array-backed binary heap contiguous আর branch-light — indexed version-এর position-map random access + sift-up bookkeeping এর চেয়ে দ্রুত প্রমাণিত হয় বেশিরভাগ benchmark-এ। তাই Python stdlib heapq দিয়ে lazy-ই idiomatic।
4আপনার কাছে ৮০০ airport-এর flight network (~২০,০০০ route) আর একটা
web app যা user-এর যেকোনো (origin, destination) pair-এর cheapest
itinerary জানতে চায়, প্রতি সেকেন্ডে শত শত query। Design কী হবে?
ডিজাইন
Query pattern all-pairs-ish কিন্তু graph sparse। Options: (১) প্রতি query-তে Dijkstra — 800 vertices, 20k edges-এ মিলিসেকেন্ড-স্কেল, server-side সম্ভব; (২) offline Floyd-Warshall — 800³ = ৫.১২ গুণ ১০⁸ ops একবার, তারপর O(1) lookup matrix (~640 KB) — যদি fares static ঘন ঘন আপডেট না হয়, এটাই জেতে; (৩) hub-based pruning: বাস্তব airline network-এ বেশিরভাগ cheapest itinerary কয়েক dozen hub দিয়ে যায় — hub-subgraph precomputation + local Dijkstra hybrid, যেটাই industry practice। শিক্ষা: query frequency, data mutability, আর domain structure — তিনটা মিলে design ঠিক করে, কোনো single algorithm না।
5Floyd-Warshall-এ k loop কেন outermost? ভেতরে সরিয়ে দিলে কী ভাঙে?
স্মরণ
Semantics-টা k-নির্ভর: d[i][j]-এর মান হওয়া উচিত “intermediate
সীমাবদ্ধ {1..k}” — k-তম iteration চলার সময় সব (i,j) পুরনো (k−1)
মানের ওপর ভিত্তি করে update হওয়া দরকার। k বাইরে থাকলে পুরো matrix
এক প্রজন্মের পুরনো মান পড়ে — in-place update-ও ঠিক থাকে, কারণ
d[i][k] আর d[k][j] এই iteration-এ বদলায় না (d[k][k]=0 এর
কারণে)। k ভেতরে গেলে কিছু (i,j) নতুন মান পড়ে অসম্পূর্ণ প্রজন্মের
সাথে মিশে — ফল অসংজ্ঞায়িতভাবে ভুল। Loop order এখানে style না,
correctness।
এরপর কী
এরপর কী
Shortest path-এ আমরা edge-গুলোকে দাম দিয়েছিলাম; এবার একটা ভিন্ন প্রশ্ন — সব vertex জুড়ে দিতে হবে সবচেয়ে সস্তায়। Fiber-optic backbone, গ্রামে বিদ্যুৎ line, chip-এ clock tree — সবখানে একই minimum spanning tree সমস্যা, আর অবিশ্বাস্য সৌভাগ্য: এখানে greedy শুধু কাজ করে না, প্রমাণিতভাবে সেরা করে। Kruskal আর Prim — cut property-র জাদু, union-find-এর ফেরা, আর কেন MST আর shortest-path tree এক জিনিস না — সব পরের লেসনে।
আরও পড়ুন
- Introduction to Algorithms (CLRS), Chapters 24–25 — Single-Source and All-Pairs Shortest Paths — Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein · Relaxation framework, Dijkstra-র পূর্ণ প্রমাণ, Bellman-Ford-এর negative-cycle argument আর Floyd-Warshall-এর DP structure
- "A Formal Basis for the Heuristic Determination of Minimum Cost Paths" (1968) — Peter E. Hart, Nils J. Nilsson, Bertram Raphael · A*-এর জন্মপত্র — SRI-র Shakey robot-এর navigation সমস্যা থেকে; admissible heuristic-এর optimality proof
- Algorithms, 4th Edition, Section 4.4 — Shortest Paths — Robert Sedgewick, Kevin Wayne · বাস্তবায়ন-কেন্দ্রিক আলোচনা আর application টেবিল — decision table section-এর inspiration