07
مهبیایم اول ببینیم این الگوریتم Bellman-Ford اصلاً به چه دردی میخوره. فرض کنید یه نقشه دارید که چندتا شهر روشه و بین این شهرها جادههایی با فاصلههای مختلف وجود داره. حالا میخواید از یه شهر (مثلاً تهران) به یه شهر دیگه (مثلاً شیراز) برید و کوتاهترین مسیر رو پیدا کنید. اگه جادهها فقط فاصلههای مثبت داشته باشن، الگوریتمهایی مثل دیکسترا میتونن بهسرعت این کار رو براتون انجام بدن. اما اگه تو مسیر یه سری جادهها هزینهی منفی داشته باشن چی؟ مثلاً فرض کنید یه جاده به جای اینکه هزینهای ازتون بگیره، بهتون پول بده که ازش رد بشید (مثل تخفیف یا یارانه)! اینجاست که الگوریتم بلمن-فورد وارد بازی میشه.
الگوریتم بلمن-فورد یه روشه برای پیدا کردن کوتاهترین مسیر از یه نقطهی شروع (گره مبدا) به همهی گرههای دیگه تو یه گراف وزندار. نکتهی مهمش اینه که برخلاف دیکسترا، میتونه با وزنهای منفی هم کار کنه. البته یه شرط داره: تو گراف نباید چرخهی منفی وجود داشته باشه (چرخهای که مجموع وزنهاش منفی باشه)، وگرنه الگوریتم بهمون هشدار میده که همچین چیزی وجود داره و نمیتونه کوتاهترین مسیر رو پیدا کنه.
این الگوریتم Bellman-Ford به اسم دو نفر نامگذاری شده: ریچارد بلمن و لستر فورد. ریچارد بلمن یه ریاضیدان و دانشمند کامپیوتر بود که تو دههی ۱۹۵۰ روی مسائل بهینهسازی و برنامهریزی پویا کار میکرد. لستر فورد هم یه ریاضیدان دیگه بود که به همراه بلمن ایدههای اولیهی این الگوریتم رو مطرح کرد. این دو نفر تو سالهای ۱۹۵۶ و ۱۹۵۸ مقالههایی منتشر کردن که پایه و اساس این الگوریتم رو شکل داد. حالا که یه کم با تاریخچه آشنا شدیم، بیایم بریم سراغ اصل مطلب!
قبل از اینکه بریم سراغ خود الگوریتم، باید یه کم در مورد گراف صحبت کنیم، چون بلمن-فورد روی گرافها کار میکنه. گراف یه ساختار دادهست که از یه سری گره (یا رأس) و یال (یا لبه) تشکیل شده. گرهها مثل همون شهرهای نقشهمون هستن و یالها مثل جادههایی که این شهرها رو به هم وصل میکنن. هر یال میتونه یه وزن داشته باشه که میتونه مثبت، منفی یا حتی صفر باشه.
گرافها دو نوعن:
گراف جهتدار (Directed Graph): یالها جهت دارن، یعنی مثلاً از تهران به شیراز میتونید برید، ولی برعکسش نه.
گراف بدون جهت (Undirected Graph): یالها دوطرفهان، یعنی از تهران به شیراز و از شیراز به تهران میتونید برید.
الگوریتم Bellman-Ford روی هر دو نوع گراف کار میکنه، ولی معمولاً تو گرافهای جهتدار بیشتر استفاده میشه.
بیایم خودمون رو جای الگوریتم Bellman-Ford بذاریم و ببینیم چطور فکر میکنه. فرض کنید یه گراف داریم با چندتا گره و یال. ما یه گره مبدا انتخاب میکنیم (مثلاً گره A) و میخوایم فاصلهی کوتاهترین مسیر از A به همهی گرههای دیگه رو پیدا کنیم. الگوریتم بلمن-فورد این کار رو تو چند مرحله انجام میده:
مقداردهی اولیه:
فاصلهی گره مبدا به خودش رو صفر میذاریم (چون از A به A هیچ فاصلهای نیست!).
فاصلهی بقیهی گرهها رو بینهایت در نظر میگیریم (چون هنوز نمیدونیم چطور بهشون برسیم).
یه آرایه هم درست میکنیم که ذخیره کنه هر گره از کدوم گرهی قبلی اومده (این برای بازسازی مسیرها بهمون کمک میکنه).
تکرار روی یالها:
الگوریتم به تعداد |V|-1 بار (که |V| تعداد گرههاست) همهی یالهای گراف رو بررسی میکنه.
برای هر یال (u, v) با وزن w، چک میکنه که اگه از گره u به v بریم، فاصلهی کوتاهتری به v میرسیم یا نه. اگه فاصلهی جدید کمتر بود، فاصلهی v رو آپدیت میکنه.
چک کردن چرخهی منفی:
بعد از |V|-1 بار تکرار، یه بار دیگه همهی یالها رو بررسی میکنیم. اگه بازم تونستیم فاصلهی کوتاهتری پیدا کنیم، یعنی یه چرخهی منفی تو گراف وجود داره و الگوریتم بهمون هشدار میده.
بیایم یه مثال بزنیم که بهتر بفهمیم. فرض کنید یه گراف با ۴ گره داریم: A، B، C و D. یالها اینجورین:
A به B با وزن ۴
A به C با وزن ۲
B به C با وزن ۳
B به D با وزن ۵
C به D با وزن ۱
حالا میخوایم از A به همهی گرهها کوتاهترین مسیر رو پیدا کنیم.
فاصلهی A به خودش = ۰
فاصلهی A به B، C و D = بینهایت
آرایهی قبلی (predecessor) خالیه.
دور اول:
یال A->B (وزن ۴): فاصلهی B میشه ۴ (چون ۰ + ۴ = ۴).
یال A->C (وزن ۲): فاصلهی C میشه ۲.
یال B->C (وزن ۳): فاصلهی C از طریق B میشه ۴+۳=۷، که از ۲ بیشتره، پس عوضش نمیکنیم.
یال B->D (وزن ۵): فاصلهی D میشه ۴+۵=۹.
یال C->D (وزن ۱): فاصلهی D میشه ۲+۱=۳، که از ۹ کمتره، پس آپدیت میشه.
دور دوم:
دوباره یالها رو بررسی میکنیم. اینبار فاصلهها تغییر نمیکنن، چون به بهینهترین حالت رسیدن.
یه بار دیگه یالها رو چک میکنیم. هیچ فاصلهای آپدیت نمیشه، پس چرخهی منفی نداریم.
فاصلهی A به A = ۰
فاصلهی A به B = ۴
فاصلهی A به C = ۲
فاصلهی A به D = ۳
مسیرها: A->B، A->C، A->C->D
حالا که منطق الگوریتم Bellman-Ford رو فهمیدیم، بیایم یه پیادهسازی ساده به زبان پایتون بنویسیم:
class Graph: def __init__(self, vertices): self.V = vertices # تعداد گرهها self.graph = [] # لیست یالها
def add_edge(self, u, v, w):
self.graph.append([u, v, w])
def bellman_ford(self, src):
# مقداردهی اولیه
dist = [float("Inf")] * self.V
dist[src] = 0
predecessor = [None] * self.V
# |V|-1 بار تکرار
for _ in range(self.V - 1):
for u, v, w in self.graph:
if dist[u] != float("Inf") and dist[u] + w < dist[v]:
dist[v] = dist[u] + w
predecessor[v] = u
# چک کردن چرخهی منفی
for u, v, w in self.graph:
if dist[u] != float("Inf") and dist[u] + w < dist[v]:
print("گراف چرخهی منفی داره!")
return None, None
return dist, predecessor
مقایسه با دیکسترا:
مهمترین کاربرد الگوریتم Bellman-Ford پیدا کردن کوتاهترین مسیر از یه گره مبدا به بقیه گرهها تو یه گراف وزندار جهتداره. چیزی که این الگوریتم رو خاص میکنه، توانایی کار با وزنهای منفی روی یالهاست. مثلاً:
بلمن-فورد تو این موقعیتها میتونه کوتاهترین مسیر رو پیدا کنه، در حالی که الگوریتمهای مثل دیکسترا تو گرافهای با وزن منفی به مشکل میخورن.
یکی از ویژگیهای باحال الگوریتم Bellman-Ford اینه که میتونه چرخههای منفی تو گراف رو تشخیص بده. چرخهی منفی یه مسیر بستهست که مجموع وزن یالهاش منفی میشه. این ویژگی تو جاهای زیادی کاربرد داره:
تو شبکههای کامپیوتری و مخابراتی، الگوریتم Bellman-Ford بهعنوان یه الگوریتم مسیریابی استفاده میشه. مثلاً:
الگوریتم Bellman-Ford یه پایهی مهم برای الگوریتمهای پیچیدهتره. مثلاً تو الگوریتم جانسون که برای پیدا کردن کوتاهترین مسیر بین همهی جفت گرهها تو گرافهای با وزن منفی استفاده میشه، بلمن-فورد نقش کلیدی داره:
بلمن-فورد تو مسائل بهینهسازی که بهصورت گراف مدل میشن هم کاربرد داره:
تو شبکههای اجتماعی، گرهها میتونن افراد و یالها روابط بینشون باشن. اگه بخوایم تأثیرات مثبت یا منفی (مثلاً اعتماد یا بیاعتمادی) رو مدل کنیم، بلمن-فورد میتونه مسیرهای بهینهی تعامل یا تأثیرگذاری رو پیدا کنه. مثلاً:
تو طراحی بازیهای ویدیویی یا شبیهسازیها، گرافها برای مدلسازی نقشهها یا مسیرهای حرکت استفاده میشن. اگه تو بازی وزنهای منفی وجود داشته باشه (مثلاً پاداش برای انتخاب یه مسیر خاص)، بلمن-فورد میتونه مسیرهای بهینه رو برای کاراکترها یا هوش مصنوعی بازی پیدا کنه.
چون بلمن-فورد میتونه چرخههای منفی رو شناسایی کنه، تو سیستمهای نظارتی و امنیتی هم کاربرد داره:
قراره بلمن-فورد رو با الگوریتمهای دیکسترا، فلوید-وارشال، A* و جانسون مقایسه کنیم. هدفمون اینه که بفهمیم کی و کجا باید از بلمن-فورد استفاده کنیم و کی بهتره سراغ یه الگوریتم دیگه بریم. آمادهاید؟ بزن بریم!
قبل از مقایسه، یه مرور سریع: بلمن-فورد کوتاهترین مسیر از یه گره مبدا به همه گرههای دیگه تو یه گراف وزندار (جهتدار یا بدون جهت) پیدا میکنه. ویژگی بارزش اینه که میتونه وزنهای منفی رو مدیریت کنه و چرخههای منفی رو تشخیص بده. پیچیدگی زمانیش O(V * E) هست (V تعداد گرهها و E تعداد یالها) و برای گرافهای با وزن منفی یا تشخیص آربیتراژ تو بازارهای مالی عالیه.
حالا بیایم ببینیم چطور با بقیه الگوریتمها مقایسه میشه.
دیکسترا یکی از معروفترین الگوریتمها برای پیدا کردن کوتاهترین مسیر از یه گره مبدا به بقیه گرههاست. بیایم این دو تا رو کنار هم بذاریم:
فلوید-وارشال یه الگوریتم پویاست که کوتاهترین مسیر بین همه جفت گرهها تو گراف رو پیدا میکنه، نه فقط از یه مبدا.
A* یه الگوریتم جستجوی هدایتشدهست که برای پیدا کردن کوتاهترین مسیر از یه گره مبدا به یه گره مقصد خاص استفاده میشه، نه همه گرهها.
جانسون یه الگوریتم پیشرفتهست که کوتاهترین مسیر بین همه جفت گرهها تو گرافهای با وزن منفی (بدون چرخه منفی) پیدا میکنه.
برای اینکه یه دید کلی داشته باشید، یه جدول مقایسهای درست کردیم:
الگوریتم | پیچیدگی زمانی | وزن منفی | چرخه منفی | هدف اصلی | بهترین کاربرد |
---|---|---|---|---|---|
بلمن-فورد | O(V * E) | بله | تشخیص | کوتاهترین مسیر از مبدا | گراف با وزن منفی، تشخیص آربیتراژ |
دیکسترا | O((V + E) log V) | خیر | خیر | کوتاهترین مسیر از مبدا | مسیریابی GPS، گراف بدون منفی |
فلوید-وارشال | O(V³) | بله | تشخیص | کوتاهترین مسیر همه جفتها | تحلیل شبکههای اجتماعی |
A* | متغیر (بستگی به هیوریستیک) | خیر | خیر | کوتاهترین مسیر به مقصد خاص | بازیهای ویدیویی، مسیریابی |
جانسون | O(V² log V + V * E) | بله | خیر | کوتاهترین مسیر همه جفتها | گراف پراکنده با وزن منفی |
بیایم یه نسخهی پیشرفتهتر از کد رو ببینیم که مسیرهای کوتاهترین رو هم چاپ میکنه:
class Graph:
def __init__(self, vertices):
self.V = vertices
self.graph = []
def add_edge(self, u, v, w):
self.graph.append([u, v, w])
def print_path(self, predecessor, j):
if predecessor[j] is None:
print(j, end=” “)
return
self.print_path(predecessor, predecessor[j])
print(j, end=” “)
def bellman_ford(self, src):
dist = [float(“Inf”)] * self.V
dist[src] = 0
predecessor = [None] * self.V
for _ in range(self.V – 1):
updated = False
for u, v, w in self.graph:
if dist[u] != float(“Inf”) and dist[u] + w < dist[v]:
dist[v] = dist[u] + w
predecessor[v] = u
updated = True
if not updated:
break
for u, v, w in self.graph:
if dist[u] != float(“Inf”) and dist[u] + w < dist[v]:
print(“گراف چرخهی منفی داره!”)
return None, None
for i in range(self.V):
if dist[i] != float(“Inf”):
print(f”مسیر به گره {i}: “, end=””)
self.print_path(predecessor, i)
print(f”\nفاصله: {dist[i]}”)
else:
print(f”گره {i} غیرقابل دسترسیه”)
return dist, predecessor
# مثال استفاده
g = Graph(4)
g.add_edge(0, 1, 4)
g.add_edge(0, 2, 2)
g.add_edge(1, 2, 3)
g.add_edge(1, 3, 5)
g.add_edge(2, 3, 1)
distances, predecessors = g.bellman_ford(0)
الگوریتم بلمن-فورد یه ابزار قدرتمند و انعطافپذیره که تو خیلی از مسائل واقعی به کار میره. گرچه ممکنه به اندازهی دیکسترا سریع نباشه، اما توانایی کار با وزنهای منفی و تشخیص چرخههای منفی اون رو خاص میکنه. تو این پست سعی کردیم با زبون خودمونی و با جزئیات کامل، این الگوریتم رو توضیح بدیم و امیدوارم حالا حسابی باهاش آشنا شده باشید. اگه سوالی دارید یا دوست دارید یه مثال دیگه ببینید، تو کامنتها بگید تا باهم گپ بزنیم!
بیشتر بخوانید: “مزایای SvelteJS در توسعه وب“
در خبرنامه ما مشترک شوید و آخرین اخبار و به روزرسانی های را در صندوق ورودی خود مستقیماً دریافت کنید.
دیدگاه بگذارید