ফ্লয়েড-ওয়ারশাল অল-পেয়ার্স শর্টেস্ট পাথ
এই পাঠে যা শিখবেন
- ফ্লয়েড-ওয়ারশালের রিকারেন্স ও অপটিমাল-সাবস্ট্রাকচার যুক্তি
- একটি সম্পূর্ণ, সরল $O(V^3)$ ইমপ্লিমেন্টেশন (তিনটি নেস্টেড লুপ)
- প্রতিটি সোর্স থেকে Dijkstra's চালিয়ে ফলাফল ক্রস-ভেরিফাই করার কৌশল
- কখন ফ্লয়েড-ওয়ারশাল $V$ বার Dijkstra's চালানোর চেয়ে ভালো (বা খারাপ) পছন্দ তার বিশ্লেষণ
১ · রিকারেন্স ও অপটিমাল সাবস্ট্রাকচার
ধরি ভার্টেক্সগুলো $1$ থেকে $n$ পর্যন্ত নাম্বার করা আছে। $D_k[i][j]$ সংজ্ঞায়িত করি "$i$ থেকে $j$-এ যাওয়ার শর্টেস্ট পাথের দৈর্ঘ্য, যেখানে শুধুমাত্র $\{1, \ldots, k\}$ সেটের ভার্টেক্সগুলোকে ইন্টারমিডিয়েট (মাঝপথের) ভার্টেক্স হিসেবে ব্যবহারের অনুমতি আছে" হিসেবে। মূল অন্তর্দৃষ্টি: $k=0$ থেকে $k=n$ পর্যন্ত ধাপে ধাপে "ইন্টারমিডিয়েট-ভার্টেক্স-বাজেট" বাড়াতে থাকলে, প্রতিটি ধাপে হয় ভার্টেক্স $k$ ব্যবহার করা লাভজনক, নয়তো নয়:
$$D_k[i][j] = \min\bigl(D_{k-1}[i][j], \; D_{k-1}[i][k] + D_{k-1}[k][j]\bigr)$$
এই রিকারেন্সের প্রতিটি পদ ব্যাখ্যা: প্রথম পদ $D_{k-1}[i][j]$ মানে "$k$ ব্যবহার না করেই যা পাওয়া যায়" (আগের সাবপ্রবলেম থেকে সরাসরি নেওয়া অপটিমাল সমাধান), দ্বিতীয় পদ $D_{k-1}[i][k] + D_{k-1}[k][j]$ মানে "$i$ থেকে $k$ পর্যন্ত (শুধু $\{1,\ldots,k-1\}$ ব্যবহার করে) তারপর $k$ থেকে $j$ পর্যন্ত (একইভাবে)" — এই দুটোর মধ্যে ছোটটাই $D_k[i][j]$। এটি অপটিমাল সাবস্ট্রাকচারের একটি প্রত্যক্ষ উদাহরণ: অপটিমাল পাথ হয় $k$ ব্যবহার করে অথবা করে না, এবং উভয় ক্ষেত্রেই বাকি অংশ নিজেই অপটিমাল হতে হবে (নাহলে সেই অংশ প্রতিস্থাপন করে আরও ছোট সম্পূর্ণ পাথ পাওয়া যেত)। বেস কেস $D_0[i][j]$ হলো এজ ওয়েট $w(i,j)$ (এজ না থাকলে $\infty$), এবং $D_0[i][i]=0$।
যেহেতু প্রতিটি $D_k$ টেবিল $n \times n$ এবং প্রতিটি এন্ট্রি $O(1)$ সময়ে আপডেট হয়, এবং $k$ মোট $n$ বার চলে, মোট কমপ্লেক্সিটি $\Theta(n \cdot n^2) = \Theta(n^3) = \Theta(V^3)$। প্রতিটি $D_k$ টেবিল শুধু $D_{k-1}$-এর উপর নির্ভর করে বলে, ইমপ্লিমেন্টেশনে একটি একক 2D অ্যারে in-place আপডেট করলেই চলে (আলাদা $n$টি টেবিল রাখার দরকার নেই)।
২ · ইমপ্লিমেন্টেশন ও প্রতিটি সোর্স থেকে Dijkstra's দিয়ে ক্রস-ভেরিফিকেশন
যেহেতু এই টেস্ট গ্রাফে কোনো নেগেটিভ এজ নেই, Dijkstra's (L33-এর প্রমিত, সঠিক ইমপ্লিমেন্টেশন) প্রতিটি সোর্স থেকে নিরাপদে ব্যবহার করা যায়। যদি ফ্লয়েড-ওয়ারশালের বাগ থাকত (রিকারেন্স ভুল প্রয়োগ, ইন্ডেক্সিং ভুল ইত্যাদি), এই দুটি সম্পূর্ণ ভিন্ন অ্যালগরিদমিক কৌশলের ফলাফল আলাদা হয়ে যেত — তাই এই ক্রস-চেক একটি জেনুইন সঠিকতা যাচাই।
import heapq
INF = float("inf")
def floyd_warshall(n, edges):
dist = [[INF] * n for _ in range(n)]
for i in range(n):
dist[i][i] = 0
for u, v, w in edges:
if w < dist[u][v]:
dist[u][v] = w
for k in range(n):
for i in range(n):
if dist[i][k] == INF:
continue
for j in range(n):
new_len = dist[i][k] + dist[k][j]
if new_len < dist[i][j]:
dist[i][j] = new_len
return dist
def dijkstra(adj, source, n):
dist = [INF] * 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, []):
nd = d + w
if nd < dist[v]:
dist[v] = nd
heapq.heappush(pq, (nd, v))
return dist
# একটি নন-নেগেটিভ-ওয়েট ডাইরেক্টেড গ্রাফ, ৬টি ভার্টেক্স, কিছু জোড়ার মধ্যে কোনো সরাসরি পথ নেই
n = 6
edges = [
(0, 1, 4), (0, 2, 1),
(1, 3, 1), (1, 2, 2),
(2, 1, 1), (2, 3, 5),
(3, 4, 3),
(4, 5, 2),
(5, 3, 1),
(2, 5, 8),
]
adj = {}
for u, v, w in edges:
adj.setdefault(u, []).append((v, w))
fw_dist = floyd_warshall(n, edges)
dijkstra_dist = [dijkstra(adj, s, n) for s in range(n)]
print("ফ্লয়েড-ওয়ারশাল বনাম প্রতিটি সোর্স থেকে Dijkstra's:")
all_agree = True
for i in range(n):
for j in range(n):
a, b = fw_dist[i][j], dijkstra_dist[i][j]
if a != b:
all_agree = False
print(f" পার্থক্য! dist[{i}][{j}]: FW={a}, Dijkstra={b}")
print(f"\nদুই পদ্ধতির সম্পূর্ণ ডিস্ট্যান্স ম্যাট্রিক্স একমত: {all_agree}")
assert all_agree, "ফ্লয়েড-ওয়ারশাল ও Dijkstra's-এর ফলাফল ভিন্ন -- একটিতে বাগ আছে!"
print("\nনমুনা সারি (সোর্স 0 থেকে সব ভার্টেক্সে দূরত্ব):")
print(" ফ্লয়েড-ওয়ারশাল:", fw_dist[0])
print(" Dijkstra's: ", dijkstra_dist[0])
dist[i][k] == INF: continue চেকটি একটি পারফরম্যান্স অপ্টিমাইজেশন (যদি $i$ থেকে $k$-এ
কোনো পাথ না থাকে, $k$-কে ইন্টারমিডিয়েট হিসেবে ব্যবহার করার কোনো লাভ নেই) — এটি সঠিকতা পরিবর্তন করে না, শুধু
অপ্রয়োজনীয় $\infty + w$ গণনা এড়ায়। যদি গ্রাফটি ডিসকানেক্টেড হয় (কিছু জোড়ার মধ্যে কোনো পথ না থাকে), উভয়
পদ্ধতিই সেই এন্ট্রিতে float("inf") দেবে — এবং all_agree চেক তখনও পাস করবে, কারণ
Python-এ float("inf") == float("inf") সত্য।
ফ্লয়েড-ওয়ারশালের $\Theta(V^3)$ বনাম প্রতিটি ভার্টেক্স থেকে Dijkstra's চালানোর $\Theta(V \cdot E\log V)$ — ঘন গ্রাফে ($E \approx V^2$) দুটো প্রায় সমতুল্য ($\Theta(V^3)$ বনাম $\Theta(V^3 \log V)$, ফ্লয়েড-ওয়ারশাল সামান্য ভালো), কিন্তু স্পার্স গ্রাফে ($E \ll V^2$) $V$ বার Dijkstra's চালানো উল্লেখযোগ্যভাবে দ্রুত। এছাড়া ফ্লয়েড-ওয়ারশাল নেগেটিভ এজ (নেগেটিভ সাইকেল ছাড়া) সহ্য করতে পারে — যা $V$ বার Dijkstra's চালিয়ে সম্ভব নয়, সেক্ষেত্রে $V$ বার Bellman-Ford চালাতে হতো ($\Theta(V^2 E)$, আরও ধীর)।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ ফ্লয়েড-ওয়ারশালের রিকারেন্স $D_k[i][j] = \min(D_{k-1}[i][j], D_{k-1}[i][k]+D_{k-1}[k][j])$-এ কেন $D_{k-1}$ ব্যবহার করা হয়, $D_k$ নিজে নয় (অর্থাৎ in-place আপডেট নিরাপদ কেন)?
in-place আপডেটে $dist[i][k]$ ও $dist[k][j]$ (যেখানে একটি ইনডেক্স নিজেই $k$) কখনো একই পাসে পরিবর্তিত হয় না — $dist[i][k]$ পরিবর্তন হতে হলে $dist[i][k] = \min(dist[i][k], dist[i][k]+dist[k][k])$ হতো, এবং $dist[k][k]=0$ (কোনো নেগেটিভ সাইকেল না থাকলে চিরকাল ০ থাকে), তাই এই আপডেট কখনো মান পরিবর্তন করে না। ফলে $dist[i][k]$ ও $dist[k][j]$ পুরো $k$-লুপ জুড়ে তাদের "$D_{k-1}$-মান"-ই ধরে রাখে, in-place আপডেট নিরাপদ করে তোলে।
প্র ০২ যদি গ্রাফে একটি নেগেটিভ-ওয়েট সাইকেল থাকত, ফ্লয়েড-ওয়ারশালের আউটপুটে এটি কীভাবে ধরা পড়ত?
অ্যালগরিদম শেষ হওয়ার পর, যদি কোনো ভার্টেক্স $i$-এর জন্য $dist[i][i] < 0$ হয়, তার মানে $i$ থেকে $i$-এ ফিরে আসার একটি নেগেটিভ-টোটাল-ওয়েট সাইকেল আছে। এই চেকটি সহজেই মূল কোডে একটি বাড়তি লুপ হিসেবে যোগ করা যায় (উপরের ইমপ্লিমেন্টেশনে যোগ করা হয়নি, কারণ আমাদের টেস্ট গ্রাফে কোনো নেগেটিভ এজই নেই)।
প্র ০৩ উপরের টেস্ট গ্রাফে ভার্টেক্স $0$ থেকে ভার্টেক্স $5$-এ পৌঁছানোর সরাসরি এজ নেই ($2 \to 5$ ওজন ৮ থাকলেও সেটি বেশি ব্যয়বহুল) — ফ্লয়েড-ওয়ারশাল কীভাবে সংক্ষিপ্ততর পথ খুঁজে পায়?
রিকারেন্সটি প্রতিটি সম্ভাব্য ইন্টারমিডিয়েট ভার্টেক্স ক্রমান্বয়ে বিবেচনা করে বলে, $k=1,2,3,4$ ধাপে ধাপে $0 \to 2 \to 1 \to 3 \to 4 \to 5$-এর মতো মাল্টি-হপ পথ আবিষ্কৃত হয়ে যায় (প্রতিটি হপ পূর্ববর্তী $k$-মানে ইতিমধ্যে সংক্ষিপ্ত করে রাখা), এবং শেষমেশ সরাসরি $2 \to 5$ (ওজন ৮)-এর চেয়ে ছোট মোট খরচ পাওয়া যায় — ঠিক যেভাবে Dijkstra's ধাপে ধাপে দূরত্ব রিফাইন করে, ফ্লয়েড-ওয়ারশাল ইন্টারমিডিয়েট-বাজেট বাড়িয়ে একই কাজ করে।
অনুশীলন
-
চিন্তা করুন: উপরের কোড সেলে যদি একটি নতুন এজ
(0, 5, 100)যোগ করা হয় (ভার্টেক্স ০ থেকে ৫-এ সরাসরি, কিন্তু খুব ব্যয়বহুল), তাহলেfw_dist[0][5]-এর মান কি পরিবর্তন হবে?না, পরিবর্তন হবে না — যদি বিদ্যমান মাল্টি-হপ পথের খরচ ইতিমধ্যে ১০০-এর চেয়ে কম হয় (যা এই গ্রাফে সত্য), রিকারেন্সের $\min$ অপারেশন স্বয়ংক্রিয়ভাবে সস্তা পথটিই বেছে নেবে। নতুন এজটি শুধু একটি বিকল্প যোগ করে, যা ইতিমধ্যে-পাওয়া ছোট মানকে প্রতিস্থাপন করতে পারবে না।
-
পরীক্ষা করুন: কোড সেলে
edgesলিস্টে(0, 5, 100)যোগ করে Run চেপে আপনার অনুমান যাচাই করুন, এবং নিশ্চিত করুনall_agreeএখনওTrue।fw_dist[0][5]অপরিবর্তিত থাকবে এবংdijkstra_dist[0][5]-এর সাথেও মিলে যাবে — দুটো অ্যালগরিদমই নতুন ব্যয়বহুল এজটিকে উপেক্ষা করে আগের সস্তা মাল্টি-হপ পথই বেছে নেবে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ ম্যাক্স ফ্লো, ব্যাকট্র্যাকিং, স্ট্রিং অ্যালগরিদম, অ্যামর্টাইজড অ্যানালাইসিস, NP-কমপ্লিটনেস ও অ্যাপ্রক্সিমেশন — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
- Data Structures & Algorithms কোর্স সহোদর কোর্স ফ্লয়েড-ওয়ারশালের মৌলিক ইমপ্লিমেন্টেশন ও প্র্যাকটিস প্রবলেম সেই কোর্সে কভার করা হয়েছে।
- সব 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 — সব এক জায়গায়।