کافه‌تدریس

درخت پوشای کمینه در گراف – بررسی کامل الگوریتم‌های پریم و کراسکال

Minimum Spanning Tree

در دنیای پیچیده ارتباطات، یکی از مهم‌ترین مسائل، بهینه‌سازی و کاهش هزینه‌ها است. نظریه گراف، به عنوان یکی از ابزارهای قدرتمند در علوم کامپیوتر و ریاضیات، به ما کمک می‌کند تا این مسئله را به طور علمی و دقیق حل کنیم. درخت پوشای کمینه (Minimum Spanning Tree – MST) یکی از مفاهیم کلیدی در نظریه گراف است که به ما اجازه می‌دهد تا شبکه‌ها را با کمترین هزینه ممکن به هم متصل کنیم. با استفاده از MST می‌توانیم مسیرهایی بهینه برای ارتباطات، حمل و نقل و حتی طراحی مدارهای الکتریکی پیدا کنیم. در این مطلب، با تعاریف، ویژگی‌ها، الگوریتم‌ها و کاربردهای درخت پوشای کمینه آشنا خواهیم شد و خواهیم دید که چگونه می‌توانیم از این مفهوم برای بهینه‌سازی سیستم‌های مختلف استفاده کنیم.

فهرست مطالب پنهان‌کردن فهرست
  1. 1. درخت پوشای کمینه چیست؟
  2. 2. مقدمه‌ای بر نظریه گراف
  3. 3. تعریف گراف و اجزای آن
  4. 4. انواع گراف
    1. 4.1. گراف جهت‌دار
    2. 4.2. گراف بدون جهت
    3. 4.3. گراف وزن‌دار
    4. 4.4. گراف بدون وزن
  5. 5. تعریف دقیق درخت پوشای کمینه
  6. 6. یکتایی درخت پوشای کمینه
    1. 6.1. گراف با یال‌هایی شامل وزن‌های منحصر به فرد
    2. 6.2. گراف با یال‌هایی شامل وزن‌های تکراری
  7. 7. الگوریتم پریم
    1. 7.1. تاریخچه الگوریتم پریم
    2. 7.2. الگوریتم پریم چطور کار می‌کند؟
    3. 7.3. بررسی گام‌به‌گام مراحل اجرای الگوریتم پریم
      1. 7.3.1. انتخاب راس F
      2. 7.3.2. انتخاب راس G
      3. 7.3.3. انتخاب راس D
      4. 7.3.4. انتخاب راس C
      5. 7.3.5. انتخاب راس B
      6. 7.3.6. انتخاب راس A
      7. 7.3.7. انتخاب راس E
    4. 7.4. مرتبه زمانی الگوریتم پریم
      1. 7.4.1. استفاده از ماتریس مجاورتی
      2. 7.4.2. استفاده از لیست مجاورتی
        1. 7.4.2.1. استفاده از هیپ دودویی
        2. 7.4.2.2. استفاده از فیبوناچی هیپ
    5. 7.5. پیاده‌سازی الگوریتم پریم در پایتون
    6. 7.6. مزایا و معایب الگوریتم پریم
  8. 8. الگوریتم کراسکال
    1. 8.1. تاریخچه الگوریتم کراسکال
    2. 8.2. الگوریتم کراسکال چطور کار می‌کند؟
    3. 8.3. بررسی گام‌به‌گام مراحل اجرای الگوریتم کراسکال
      1. 8.3.1. مرتب‌سازی یال‌ها بر اساس وزن
      2. 8.3.2. ایجاد مجموعه‌های مجزا
      3. 8.3.3. انتخاب یال FG
      4. 8.3.4. انتخاب یال AB
      5. 8.3.5. انتخاب یال DF
      6. 8.3.6. انتخاب یال CG
      7. 8.3.7. انتخاب یال AF
      8. 8.3.8. انتخاب یال EF
    4. 8.4. مرتبه زمانی الگوریتم کراسکال
    5. 8.5. پیاده‌سازی الگوریتم کراسکال در پایتون
    6. 8.6. مزایا و معایب الگوریتم‌ کراسکال
  9. 9. مقایسه الگوریتم‌های پریم و کراسکال
  10. 10. کاربردهای درخت پوشای کمینه
  11. 11. کلام آخر درباره درخت پوشای کمینه
  12. 12. سوالات متداول
    1. 12.1. چرا درخت پوشای کمینه در طراحی شبکه‌های کامپیوتری مهم است؟
    2. 12.2. تفاوت‌های اصلی بین الگوریتم پریم و الگوریتم کراسکال چیست؟
    3. 12.3. چگونه می‌توان از درخت پوشای کمینه در بهینه‌سازی مسیرهای حمل و نقل استفاده کرد؟
    4. 12.4. چه عواملی بر یکتایی (Uniqueness) درخت پوشای کمینه تاثیر می‌گذارند؟
    5. 12.5. چگونه الگوریتم‌های MST در بهینه‌سازی سیستم‌های الکتریکی مورد استفاده قرار می‌گیرند؟
  13. 13. یادگیری تحلیل داده را از امروز شروع کنید!

درخت پوشای کمینه چیست؟

درخت پوشای کمینه یا MST درختی است که تمامی رئوس گراف را با کمترین مجموع وزن یال‌ها به هم متصل می‌کند. به عبارت دیگر، MST درختی است که هر دو راس را بدون ایجاد حلقه به هم متصل می‌کند و وزن مجموع یال‌ها را به حداقل می‌رساند.

یکی از ویژگی‌های مهم MST این است که برای یک گراف معین ممکن است چندین درخت پوشای کمینه وجود داشته باشد، اما مجموع وزن یال‌های آن‌ها همیشه یکسان است. همچنین، MST تنها برای گراف‌های همبند قابل تعریف است.

مقدمه‌ای بر نظریه گراف

نظریه گراف (Graph Theory) یکی از شاخه‌های مهم و بنیادین ریاضیات و علوم کامپیوتر است که به مطالعه گراف‌ها و ویژگی‌های آن‌ها می‌پردازد. این نظریه به ما کمک می‌کند تا مسائل پیچیده و متنوعی را که در سیستم‌های متصل به وجود می‌آیند، با استفاده از مدل‌های ریاضی تحلیل و حل کنیم.

تعریف گراف و اجزای آن

گراف مجموعه‌ای از راس‌ها (گره‌ها) و یال‌ها (لبه‌ها) است که در آن هر یال به دو راس متصل است. راس‌ها می‌توانند نمایانگر اشیاء مختلفی مانند شهرها، کامپیوترها، ایستگاه‌های مترو یا هر چیزی که نیاز به اتصال با دیگران دارد، باشند. یال‌ها نیز نشان‌دهنده روابط یا مسیرهای بین این رئوس هستند. به عنوان مثال، در یک شبکه کامپیوتری، رئوس می‌توانند کامپیوترها و یال‌ها کابل‌های اتصال بین آن‌ها باشند.

انواع گراف

گراف‌ها انواع مختلفی دارند که ما در اینجا به مهم‌ترین آن‌ها که در ان مقاله به درک آن‌ها نیاز داریم، می‌پردازیم:

گراف جهت‌دار

در گراف‌های جهت‌دار (Directed Graph)، یال‌ها دارای جهت هستند و نشان می‌دهند که ارتباط تنها از یک راس به راس دیگر وجود دارد. این نوع گراف‌ها برای مدل‌سازی روابط یک‌طرفه مناسب هستند.

گراف بدون جهت

در گراف‌های بدون جهت (Undirected Graph)، یال‌ها جهت ندارند و ارتباط دوطرفه بین رئوس را نشان می‌دهند. این نوع گراف‌ها برای مدل‌سازی روابط متقابل مناسب هستند.

گراف وزن‌دار

در گراف‌های وزن‌دار (Weighted Graph)، هر یال دارای وزنی است که می‌تواند نمایانگر هزینه، فاصله، زمان یا هر معیار دیگری باشد. این نوع گراف‌ها برای مسائلی که در آن‌ها کمیت‌ها مهم هستند، مناسب‌اند.

گراف بدون وزن

در گراف‌های بدون وزن (Unweighted Graph)، یال‌ها وزنی ندارند و تنها وجود یا عدم وجود یک ارتباط را نشان می‌دهند. این نوع گراف‌ها برای مسائلی که در آن‌ها تنها ساختار روابط مهم است، مناسب‌اند.

برای آشنایی بیشتر با گراف و انواع آن پیشنهاد می‌کنیم مقاله نظریه گراف: پلی بین ریاضیات و دنیای واقعی را بخوانید.

باید توجه کرد که ما در مبحث درخت پوشای کمینه، تنها با گراف‌های همبند، بدون جهت و وزن‌دار سر و کار داریم، زیرا در این نوع گراف‌ها، هدف یافتن مسیری است که تمامی رئوس گراف را با کمترین مجموع وزن یال‌ها به هم متصل کند. گراف‌های ناهمبند اصلا درخت پوشا ندارند و در گراف‌های جهت‌دار، مفهوم درخت پوشا به دلیل وجود جهت‌ها پیچیده‌تر می‌شود و در گراف‌های بدون وزن نیز، اگرچه می‌توان درخت پوشا را تعریف کرد، اما مفهوم «کمینه» در این گراف‌ها چندان معنا ندارد.

پیش از تعریف درخت پوشای کمینه، بیایید نگاهی به تعریف درخت پوشا در گراف‌ها بیاندازیم:

درخت پوشا یک زیرگراف از گراف اصلی است که شامل همه رئوس گراف اصلی باشد و حاوی هیچ حلقه‌ای نباشد. همچنین تعداد یال‌های درخت پوشا برابر با n−1 است، که در آن n تعداد راس‌های گراف اصلی است. این ویژگی نشان‌دهنده این است که درخت پوشا کمترین تعداد یال‌های ممکن را برای حفظ همبندی دارد.

تعریف دقیق درخت پوشای کمینه

درخت پوشای کمینه یک گراف بدون جهت و وزن‌دار است که:

در واقع درخت پوشای کمینه درخت پوشایی است که مجموع وزن‌های آن نسبت به سایر درخت‌های پوشای گراف اصلی، کمینه باشد. برای مثال در مورد گراف زیر:

همه گراف‌های زیر، درخت پوشای گراف بالا هستند اما تنها اولین آن‌ها درخت پوشای کمینه است:

اگر مجموع وزن یال‌های درخت پوشای کمینه را حساب کنید، می‌بیند که برابر چهار و از مجموع وزن یال‌های سایر درخت‌های پوشا کمتر است.

یکتایی درخت پوشای کمینه

یکی از ویژگی‌های مهم و قابل تحقیق درخت پوشای کمینه در نظریه گراف، یکتایی آن است. یکتایی MST به عوامل مختلفی بستگی دارد و در شرایط متفاوت می‌تواند تغییر کند. به طور کلی، برای یک گراف معین ممکن است چندین درخت پوشای کمینه وجود داشته باشد، اما مجموع وزن یال‌های آن‌ها همیشه یکسان است. در این بخش به بررسی جزئیات یکتایی درخت پوشای کمینه و شرایط مختلف آن می‌پردازیم.

گراف با یال‌هایی شامل وزن‌های منحصر به فرد

در صورتی که تمامی یال‌های یک گراف وزن‌دار دارای وزن‌های منحصر به فرد (متمایز) باشند، درخت پوشای کمینه آن گراف حتما یکتا خواهد بود. به این معنا که تنها یک مجموعه از یال‌ها وجود دارد که مجموع وزن آن‌ها کمترین است و تمامی رئوس گراف را به هم متصل می‌کند. این ویژگی از آنجا ناشی می‌شود که در هر مرحله از انتخاب یال‌ها، یال با کمترین وزن به طور قطعی انتخاب می‌شود و هیچ تردیدی در انتخاب وجود ندارد. بنابراین، یک گراف با یال‌های وزن‌دار منحصر به فرد همیشه یک MST یکتا خواهد داشت.

برای مثال در گراف زیر که وزن‌ همه یال‌هایش منحصر به فرد است، تنها یک درخت پوشای کمینه داریم که می‌توانید آن را در شکل زیر ببنید:

گراف با یال‌هایی شامل وزن‌های تکراری

در گراف‌هایی که برخی از یال‌ها دارای وزن‌های تکراری هستند، ممکن است بیش از یک درخت پوشای کمینه داشته باشیم. به عنوان مثال، اگر چند یال با وزن یکسان وجود داشته باشند که می‌توانند رئوس گراف را به هم متصل کنند، انتخاب هر یک از این یال‌ها می‌تواند به درخت‌های پوشای کمینه متفاوتی منجر شود، اما مجموع وزن آن‌ها همچنان کمترین خواهد بود. در این حالت، MST‌ها ممکن است ترکیبات مختلفی از یال‌های با وزن یکسان داشته باشند.

برای مثال در گراف زیر که وزن‌ برخی از یال‌هایش یکسان است:

سه درخت پوشای کمینه داریم که می‌توانید آن‌ها را در شکل زیر ببنید:

همان طور که می‌بینید، مجموع وزن همه یال‌های این سه درخت یکسان و برابر با پنج است.

البته این توضیحات به این معنی نیست که اگر درخت پوشای کمینه یک گراف یکتا باشد، آن گراف حتما دارای وزن‌های متمایز است. زیرا شاید در یک گراف با وزن‌ها تکراری بتوانیم از بین دو یال با وزن یکسان یکی را انتخاب کنیم اما این موضوع همیشه صادق نیست. به مثال زیر توجه کنید:

همان طور که مشخص است تنها درخت پوشای کمینه این گراف در شکل سمت راست رسم شده و مجموع وزن یال‌هایش برابر سه است. با این حال گراف اصلی دارای سه یال هم‌وزن است و تمایزی بین آن‌ها نیست.

این موضوع نشان می‌دهد که داشتن یک MST یکتا به تنهایی نمی‌تواند دلیلی بر منحصر به فرد بودن وزن یال‌های گراف باشد.

احتمالا با توجه به مصال‌های بالا متوجه شده‌اید که پیدا کردن درخت پوشای کمینه در گراف‌های بزرگ چندان کار آسانی نیست. به همین دلیل باید از الگوریتم‌های خاصی که می‌توانند درخت پوشای کمینه گراف‌ها را پیدا کنند، استفاده کنیم. در ادامه دو تا از اصلی‌ترین و بهینه‌ترین این الگوریتم‌ها را بررسی می‌کنیم.

الگوریتم پریم

الگوریتم پریم (Prim’s Algorithm) یکی از الگوریتم‌های معروف برای پیدا کردن درخت پوشای کمینه در گراف‌های وزن‌دار است. الگوریتم پریم به صورت تکراری عمل می‌کند و در هر مرحله یالی را به درخت اضافه می‌کند که کمترین وزن را دارد و یک راس جدید را به درخت اضافه می‌کند. این فرآیند تا زمانی ادامه می‌یابد که تمامی رئوس به درخت اضافه شوند و درخت پوشای کمینه تشکیل شود.

تاریخچه الگوریتم پریم

توسعه اولیه الگوریتم پریم به سال ۱۹۳۰ برمی‌گردد، زمانی که ریاضی‌دان چک به نام وارتن (Vojtěch Jarník) اولین نسخه از این الگوریتم را در مقاله‌ای منتشر کرد. این مقاله در یکی از ژورنال‌های ریاضیاتی منتشر شد و الگوریتمی برای پیدا کردن درخت پوشای کمینه در گراف‌های وزن‌دار ارائه داد. این الگوریتم بعدها در کشورهای اروپایی به عنوان الگوریتم Jarník شناخته شد.

اما این پایان کار نبود و در سال ۱۹۵۷، رابرت پریم (Robert C. Prim) ریاضی‌دان و دانشمند کامپیوتر آمریکایی، به طور مستقل الگوریتمی مشابه الگوریتم Jarník را کشف و آن را به صورت گسترده‌تری معرفی کرد. پریم در مقاله خود این الگوریتم را به عنوان یک روش کارآمد برای یافتن درخت پوشای کمینه در گراف‌های وزن‌دار توضیح داد. از آن زمان، این الگوریتم در ایالات متحده و دیگر کشورها به نام الگوریتم پریم شناخته می‌شود.

الگوریتم پریم چطور کار می‌کند؟

در این بخش نحوه عملکرد الگوریتم پریم را بررسی می‌کنیم:

بررسی گام‌به‌گام مراحل اجرای الگوریتم پریم

برای انجام این کار بیایید الگوریتم پریم را روی گراف زیر اجرا کنیم تا درخت پوشای کمینه آن را بدست آوریم:

انتخاب راس F

برای این کار، همان‌طور که توضیح دادیم ابتدا از یک راس دلخواه شروع می‌کنیم و آن را در مجموعه بازدیدشده‌های قرار می‌دهیم. ما راس F را انتخاب می‌کنیم اما فرقی نمی‌کند که از کدام راس شروع کنیم و در هر صورت پریم جواب درست را بر می‌گرداند. سایر راس‌ها به مجموعه بازدیدنشده‌ها انتقال می‌یابند.

انتخاب راس G

حال پریم وزن یال بین راس F و همه همسایه‌های آن را بررسی و کمترین آن‌ها را انتخاب می‌کند. همان طور که در شکل مشخص است، یال‌های متصل به F عبارتند از:

FA (7), FB (8), FC (7), FD (5), FE (8), FG (4)

که کمترین وزن مربوط به یال FG با وزن چهار است. پس، یال FG به عنوان اولین یال درخت پوشای کمینه ما انتخاب می‌شود. سپس راس G را از مجموعه بازدیدنشده‌ها به مجموعه بازدیدشده‌ها انتقال می‌دهیم و به این ترتیب مجموعه رئوس بازدیدشده به صورت {F, G} و مجموعه رئوس بازدیدنشده به صورت  {A, B, C, D, E} درمی‌آید:

انتخاب راس D

حال پریم وزن یال بین راس‌های F و G و همه همسایه‌های آن‌ها را بررسی و کمترین آن‌ها را انتخاب می‌کند. همان طور که در شکل مشخص است، یال‌های انتخاب‌نشده متصل به F و G عبارتند از:

FA (7), FB (8), FC (7), FD (5), FE (8), GD (5), GC (6)

که کمترین وزن مربوط به یال‌های FD و GD با وزن پنج است. اما سوال اینجاست که کدام یک توسط الگوریتم پریم انتخاب می‌شود؟

پاسخ این سوال به نحوه پیاده‌سازی دقیق الگوریتم پریم بستگی دارد. در چنین شرایطی اگر چند یال با وزن‌های یکسان وجود داشته باشد، ترتیب انتخاب آن‌ها می‌تواند به ترتیب پیمایش مجموعه بازدیدشده بستگی داشته باشد. در پیاده‌سازی‌های مختلف این الگوریتم، نحوه اولویت‌بندی یال‌ها با وزن‌های مساوی ممکن است متفاوت باشد اما نتیجه تفاوتی نخواهد داشت و مجموع وزن یال‌های درخت پوشای کمینه در هر دو حالت یکسان خواهد بود.

ما در اینجا به دلخواه یال GD را انتخاب می‌کنیم. سپس راس D را از مجموعه بازدیدنشده‌ها به مجموعه بازدیدشده‌ها انتقال می‌دهیم و به این ترتیب مجموعه رئوس بازدیدشده به صورت {F, G, D} و مجموعه رئوس بازدیدنشده به صورت {A, B, C, E} درمی‌آید:

انتخاب راس C

در مرحله بعدی، الگوریتم پریم همسایه‌های همه رئوس مجموعه بازدیدشده F، G و D را بررسی و کمترین وزن یال متصل به راسی که در مجموعه بازدیدنشده قرار دارد را انتخاب می‌کند. همان طور که در شکل مشخص است، یال‌های انتخاب‌نشده متصل به رئوسF، G و D عبارتند از:

FA (7), FB (8), FC (7)، FE (8), ED (9), GC (6)

که کمترین وزن مربوط به یال GC با وزن شش است. پس، یال GC به عنوان یال بعدی درخت پوشای کمینه انتخاب می‌شود. سپس راس C را از مجموعه بازدیدنشده‌ها به مجموعه بازدیدشده‌ها انتقال می‌دهیم و به این ترتیب مجموعه رئوس بازدیدشده به صورت {F, G, D, C} و مجموعه رئوس بازدیدنشده به صورت {A, B, E} درمی‌آید:

انتخاب راس B

در گام بعدی، الگوریتم پریم همسایه‌های همه رئوس مجموعه بازدیدشده را بررسی و کمترین وزن یال متصل به راسی که در مجموعه بازدیدنشده قرار دارد را انتخاب می‌کند. همان طور که در شکل مشخص است، یال‌های متصل به رئوس F، G، D و C عبارتند از:

FA (7), FB (8), FE (8), CB (7), ED (9)

که کمترین وزن مربوط به یال‌های AB و CB با وزن هفت است. ما از بین آن‌ها به دلخواه یال CB را به عنوان یال بعدی درخت پوشای کمینه انتخاب می‌شود. سپس راس B را از مجموعه بازدیدنشده‌ها به مجموعه بازدیدشده‌ها انتقال می‌دهیم و به این ترتیب مجموعه رئوس بازدیدشده به صورت {F, G, D, C, B} و مجموعه رئوس بازدیدنشده به صورت {A, E} درمی‌آید:

انتخاب راس A

حال الگوریتم پریم همسایه‌های همه رئوس مجموعه بازدیدشده را بررسی و کمترین وزن یال متصل به راسی که در مجموعه بازدیدنشده قرار دارد را انتخاب می‌کند. همان طور که در شکل مشخص است، یال‌های متصل به رئوس F، G، D، C و B عبارتند از:

FA (7), FE (8), BA (5), ED (9)

که کمترین وزن مربوط به یال BA با وزن پنج است. پس، یال BA به عنوان یال بعدی درخت پوشای کمینه انتخاب می‌شود. سپس راس A را از مجموعه بازدیدنشده‌ها به مجموعه بازدیدشده‌ها انتقال می‌دهیم و به این ترتیب مجموعه رئوس بازدیدشده به صورت {F, G, D, C, B, A} و مجموعه رئوس بازدیدنشده به صورت {E} درمی‌آید:

انتخاب راس E

در اخرین مرحله الگوریتم پریم همسایه‌های همه رئوس مجموعه بازدیدشده را بررسی و کمترین وزن یال متصل به راسی که در مجموعه بازدیدنشده قرار دارد را انتخاب می‌کند. همان طور که در شکل مشخص است، یال‌های متصل به رئوسF، G، D، C، B و A عبارتند از:

FE (8), ED (9)

که کمترین وزن مربوط به یال FE با وزن هشت است. پس، یال FE به عنوان یال بعدی درخت پوشای کمینه انتخاب می‌شود. سپس راس E را از مجموعه بازدیدنشده‌ها به مجموعه بازدیدشده‌ها انتقال می‌دهیم و به این ترتیب مجموعه رئوس بازدیدشده به صورت {F, G, D, C, B, A, E} و مجموعه رئوس بازدیدنشده خالی می‌شود. در این مرحله، الگوریتم به پایان می‌رسد و درخت پوشای کمینه کامل می‌شود:

مرتبه زمانی الگوریتم پریم

پیچیدگی زمانی این الگوریتم به ساختار داده‌ای که برای پیاده‌سازی آن استفاده می‌شود بستگی دارد:

استفاده از ماتریس مجاورتی

اگر از ماتریس مجاورتی برای نمایش گراف استفاده شود، پیچیدگی زمانی‌اش برابر با O(V2) خواهد بود، که V تعداد رئوس گراف است.

استفاده از لیست مجاورتی

اگر از لیست مجاورتی برای نمایش گراف استفاده شود، بسته به اینکه از هیپ دودویی استفاده کنیم یا هیپ فیبوناچی مرتبه مانی این الگوریتم متفاوت است:

استفاده از هیپ دودویی

در صورت استفاده از هیپ دودویی (Binary Heap) برای پیاده‌سازی این الگوریتم، پیچیدگی زمانی آن به صورت زیر خواهد بود:

با جمع‌بندی این موارد، پیچیدگی زمانی کلی الگوریتم پریم با استفاده از هیپ دودویی برابر خواهد بود با:

O((V + E) \log V) = O(E \log V)

استفاده از فیبوناچی هیپ

اگر از هیپ فیبوناچی (Fibonacci Heap) برای پیاده‌سازی این الگوریتم استفاده شود، پیچیدگی زمانی الگوریتم بهبود می‌یابد:

با جمع‌بندی این موارد، پیچیدگی زمانی کلی الگوریتم پریم با استفاده از هیپ فیبوناچی برابر خواهد بود با:

O(V \log V + E)

پیاده‌سازی الگوریتم پریم در پایتون

برای این منظور، ابتدا یک تابع به نام prim_algorithm ایجاد می‌کنیم. سپس مجموعه‌ای به نام visited برای ذخیره رئوس بازدیدشده ایجاد و راس شروع را در آن قرار می‌دهیم. سپس تمامی رئوس گراف به جز راس شروع را در مجموعه‌ای به نام unvisited قرار می‌دهیم. سپس لیستی به نام mst_edges برای ذخیره یال‌های درخت پوشای کمینه تعریف می‌کنیم. سپس الگوریتم تا زمانی که تمامی رئوس بازدید نشده‌اند، ادامه پیدا می‌کند. در هر تکرار، یالی با کمترین وزن که یک سر آن در مجموعه بازدیدشده و سر دیگر آن در مجموعه بازدیدنشده قرار دارد، انتخاب می‌شود. این یال به mst_edges اضافه شده و راس مقصد به مجموعه بازدیدشده‌ها افزوده و از مجموعه بازدیدنشده‌ها حذف می‌شود. این فرآیند تا زمانی که تمامی رئوس بازدید شوند، ادامه پیدا می‌کند و درخت پوشای کمینه تولید می‌شود:

def prim_algorithm(graph, start_node):
  # Initialize the visited set with the start node
  visited = set(start_node)
  # All nodes except the start node are unvisited
  unvisited = set(graph.keys()) - visited
  # List to store the edges of the MST
  mst_edges = []
  # Continue until all nodes are visited
  while unvisited:
    min_edge = None
    for node in visited:
      for neighbor, weight in graph[node].items():
        # Consider only unvisited neighbors
        if neighbor in unvisited:
          # Find the edge with minimum weight
          if min_edge is None or weight < min_edge[2]:
            min_edge = (node, neighbor, weight)
    # Exit if no more edges can be added
    if min_edge is None:
      break
    # Unpack the minimum edge
    source, destination, weight = min_edge
    # Add the edge to the MST
    mst_edges.append((source, destination, weight))
    # Mark the neighbor as visited
    visited.add(destination)
    # Remove the neighbor from unvisited
    unvisited.remove(destination)
    # Return MST after each edge adding
    print(mst_edges)

برای اجرای این الگوریتم روی گراف شکلی که در بخش قبلی آن را بررسی کردیم، به صورت زیر عمل کرده و می‌بینیم که نتیجه با چیزی که ما بدست آوردیم یکسان است:

graph = {
  'A': {'B': 5, 'E': 10, 'F': 7},
  'B': {'A': 5, 'C': 7, 'F': 8},
  'C': {'B': 7, 'F': 7, 'G': 6},
  'D': {'E': 9, 'F': 5, 'G': 5},
  'E': {'A': 10, 'D': 9, 'F': 8},
  'F': {'A': 7, 'B': 8, 'C': 7, 'D': 5, 'E': 8, 'G': 4},
  'G': {'C': 6, 'D': 5, 'F': 4}
}

prim_algorithm(graph, 'F')

[(‘F’, ‘G’, 4)]
[(‘F’, ‘G’, 4), (‘G’, ‘D’, 5)]
[(‘F’, ‘G’, 4), (‘G’, ‘D’, 5), (‘G’, ‘C’, 6)]
[(‘F’, ‘G’, 4), (‘G’, ‘D’, 5), (‘G’, ‘C’, 6), (‘C’, ‘B’, 7)]
[(‘F’, ‘G’, 4), (‘G’, ‘D’, 5), (‘G’, ‘C’, 6), (‘C’, ‘B’, 7), (‘B’, ‘A’, 5)]
[(‘F’, ‘G’, 4), (‘G’, ‘D’, 5), (‘G’, ‘C’, 6), (‘C’, ‘B’, 7), (‘B’, ‘A’, 5), (‘F’, ‘E’, 8)]

مزایا و معایب الگوریتم پریم

این الگوریتم مزایای متعددی دارد که در زیر به برخی از مهم‌ترین آن‌ها اشاره می‌کنیم:

پریم با وجود مزایای متعدد، معایبی نیز دارد که در زیر به برخی از مهم‌ترین آن‌ها اشاره می‌کنیم:

الگوریتم کراسکال

الگوریتم کراسکال (Kruskal’s Algorithm)، یکی دیگر از الگوریتم‌های معروف برای پیدا کردن درخت پوشای کمینه در گراف‌های وزن‌دار است. ایده اصلی الگوریتم کراسکال بر اساس مرتب‌سازی یال‌های گراف بر اساس وزن آن‌ها و اضافه کردن یال‌ها به درخت پوشا به ترتیب کمترین وزن، بدون ایجاد حلقه است. این روش باعث می‌شود که در نهایت درخت پوشایی تشکیل شود که مجموع وزن یال‌های آن حداقل باشد.

تاریخچه الگوریتم کراسکال

الگوریتم کراسکال توسط جوزف کراسکال (Joseph Kruskal)، ریاضی‌دان و آمارشناس آمریکایی، در سال ۱۹۵۶ معرفی شد. این الگوریتم به سرعت به یکی از ابزارهای مهم در نظریه گراف و علوم کامپیوتر تبدیل شد. ایده اصلی الگوریتم کراسکال بر اساس مرتب‌سازی یال‌های گراف بر اساس وزن و اضافه کردن یال‌ها به ترتیب کمترین وزن بدون ایجاد حلقه است. این روش ساده و کارآمد به سرعت مورد توجه قرار گرفت و در طراحی و بهینه‌سازی شبکه‌ها و سیستم‌های مختلف به کار رفت.

الگوریتم کراسکال چطور کار می‌کند؟

در این بخش نحوه عملکرد الگوریتم کراسکال را بررسی می‌کنیم:

بررسی گام‌به‌گام مراحل اجرای الگوریتم کراسکال

برای انجام این کار بیایید الگوریتم کراسکال را روی همان گرافی که الگوریتم پریم را روی آن اجرا کردیم، اجرا کنیم تا درخت پوشای کمینه آن را بدست آوریم:

مرتب‌سازی یال‌ها بر اساس وزن

ابتدا، تمام یال‌ها را بر اساس وزن مرتب می‌کنیم و حاصل مرتب‌سازی به صورت زیر است:

(G, F, 4)
(A, B, 5)
(F, D, 5)
(G, D, 5)
(G, C, 6)
(B, C, 7)
(A, F, 7)
(F, C, 7)
(F, B, 8)
(E, F, 8)
(E, D, 9)
(A, E, 10)

ایجاد مجموعه‌های مجزا

ابتدا هر راس را در مجموعه جداگانه‌ای قرار می‌دهیم:

\large \{A\}\{B\}\{C\}\{D\}\{E\}\{F\}\{G\}

انتخاب یال FG

در این مرحله یال کمینه FG با وزن چهار را انتخاب می‌کنیم. این یال از بین سایر یال‌ها کمترین وزن را دارد و راس‌های دو سر آن در دو مجموعه جدا قرار دارد، بنابراین انتخاب آن باعث ایجاد حلقه نمی‌شود و می‌توانیم آن را به درخت پوشای کمینه اضافه کنیم. پس مجموعه‌های F و G را ادغام کرده و مجموعه‌های جدید به صورت زیر خواهند بود:

\large \{A\}, \{B\}, \{C\}, \{D\}, \{E\}, \{F, G\}

گراف ما تا این لحظه به شکل زیر است:

انتخاب یال AB

اکنون یال AB با وزن ۵ را انتخاب می‌کنیم. این یال از بین سایر یال‌ها کمترین وزن را دارد و راس‌های دو سر آن در دو مجموعه جدا قرار دارد، بنابراین می‌توانیم آن را به درخت پوشای کمینه اضافه ‌کنیم. پس مجموعه‌های A و B را ادغام کرده و مجموعه‌های جدید به صورت زیر خواهند بود:

\large \{A, B\}, \{C\}, \{D\}, \{E\}, \{F, G\}

گراف ما تا این لحظه به شکل زیر است:

انتخاب یال DF

سپس یال DF با وزن ۵ را انتخاب می‌کنیم. این یال از بین سایر یال‌ها کمترین وزن را دارد و راس‌های دو سر آن در دو مجموعه جدا قرار دارد، بنابراین می‌توانیم آن را به درخت پوشای کمینه اضافه ‌کنیم. پس مجموعه‌ D را با مجموعه F و G ادغام کرده و مجموعه‌های جدید به صورت زیر خواهند بود:

\large \{A, B\}, \{C\}, \{E\}, \{F, G, D\}

گراف ما تا این لحظه به شکل زیر است:

انتخاب یال CG

در این مرحله باید یال CG با وزن ۶ را انتخاب ‌کنیم. این یال از بین سایر یال‌ها کمترین وزن را دارد و راس‌های دو سر آن در دو مجموعه جدا قرار دارد، بنابراین می‌توانیم آن را به درخت پوشای کمینه اضافه ‌کنیم. پس مجموعه‌ C را با مجموعه F و G و D ادغام کرده و مجموعه‌های جدید به صورت زیر خواهند بود:

\large \{A, B\}, \{E\}, \{F, G, D, C\}

گراف ما تا این لحظه به شکل زیر است:

انتخاب یال AF

در این مرحله باید یال AF با وزن ۷ را انتخاب ‌کنیم. این یال از بین سایر یال‌ها کمترین وزن را دارد و راس‌های دو سر آن در دو مجموعه جدا قرار دارد، بنابراین می‌توانیم آن را به درخت پوشای کمینه اضافه ‌کنیم. پس مجموعه‌ A و B را با مجموعه F و G و D و C ادغام کرده و مجموعه‌های جدید به صورت زیر خواهند بود:

\large \{A, B, F, G, D, C\}, \{E\}

گراف ما تا این لحظه به شکل زیر است:

انتخاب یال EF

در این مرحله باید یال EF با وزن ۸ را انتخاب ‌کنیم. این یال از بین سایر یال‌ها کمترین وزن را دارد و راس‌های دو سر آن در دو مجموعه جدا قرار دارد، بنابراین می‌توانیم آن را به درخت پوشای کمینه اضافه ‌کنیم. پس مجموعه‌ E را با مجموعه A و B و F و G و D و C ادغام کرده و به این ترتیب تمامی رئوس در یک مجموعه قرار می‌گیرند:

\large \{A, B, F, G, D, C, E\}

به این ترتیب همه راس‌ها به هم متصل شده‌اند و درخت پوشای کمینه ما بدست می‌آید:

همان طور که می‌بینید شکل درخت پوشای کمینه حاصل از الگوریتم کراسکال با الگوریتم پریم متفاوت است اما اگر مجموع وزن یال‌های این درخت‌ها را جمع بزنید می‌بینید که هر دو برابر ۳۵ هستند. دلیل این امر وجود یال‌هایی با وزن یکسان در گراف اصلی است. در واقع اگر وزن همه یال‌های گراف متمایز باشد، خروجی الگوریتم‌های کراسکال و پریم دقیقا یکسان خواهد بود.

نکته دیگری که خوب است به آن توجه کنید، این است که چون الگوریتم پریم در هر مرحله فقط یال‌هایی که به راس‌های بازدیدشده متصل هستند را انتخاب می‌کند، همواره درون یک گراف همبند پیش می‌رود اما کراسکال چون یال‌ها را به ترتیب وزنشان انتخاب می‌کند، به صورت ناهمبند پیش می‌رود تا در نهایت درخت پوشای کمینه کامل شود.

مرتبه زمانی الگوریتم کراسکال

پیچیدگی زمانی کراسکال به مراحل مختلف الگوریتم بستگی دارد:

با جمع‌بندی مراحل بالا می‌توان گفت به طور کلی، پیچیدگی زمانی الگوریتم کراسکال برابر خواهد بود با:

پیاده‌سازی الگوریتم کراسکال در پایتون

برای این کار ابتدا باید کلاس UnionFind را تعریف کنیم که برای مدیریت مجموعه‌های مجزا استفاده می‌شود. این کلاس شامل دو تابع اصلی find و union است. تابع find برای پیدا کردن ریشه مجموعه‌ای که یک عنصر در آن قرار دارد و تابع union برای ادغام دو مجموعه جداگانه استفاده می‌شود:

# Union-Find class
class UnionFind:
  # Initialize
  def __init__(self, n):
    self.parent = list(range(n))
    self.rank = [0] * n
  # Find root
  def find(self, u):
    if self.parent[u] != u:
      self.parent[u] = self.find(self.parent[u])
    return self.parent[u]
  # Union sets
  def union(self, u, v):
    root_u = self.find(u)
    root_v = self.find(v)
    if root_u != root_v:
      if self.rank[root_u] > self.rank[root_v]:
        self.parent[root_v] = root_u
      elif self.rank[root_u] < self.rank[root_v]:
        self.parent[root_u] = root_v
      else:
        self.parent[root_v] = root_u
        self.rank[root_u] += 1

سپس در تابع kruskal_algorithm، ابتدا نگاشت بین رئوس و اندیس‌ها را ایجاد کرده و یال‌ها را با استفاده از این نگاشت استخراج و بر اساس وزنشان مرتب می‌کنیم. سپس، با استفاده از کلاس UnionFind، یال‌های مرتب‌شده را بررسی و اگر یال انتخابی باعث ایجاد حلقه نشود، آن را به درخت پوشای کمینه اضافه می‌کنیم:

# Kruskal's algorithm
def kruskal_algorithm(graph):
  # List of nodes
  nodes = list(graph.keys())
  # Map each node to an index
  node_index = {node: idx for idx, node in enumerate(nodes)}
  # Create a list of edges with weights
  edges = [(weight, node_index[u], node_index[v]) for u in graph for v, weight in graph[u].items()]
  # Sort edges by weight
  edges.sort()
  # Number of nodes in the graph
  n = len(graph)
  # Initialize Union-Find structure
  uf = UnionFind(n)
  # List to store the edges of the MST
  mst_edges = []
  # Process edges
  for weight, u, v in edges:
    # If nodes u and v are not connected, add the edge to the MST
    if uf.find(u) != uf.find(v):
      uf.union(u, v)
      mst_edges.append((nodes[u], nodes[v], weight))
      # Print the current MST edges
      print(mst_edges)

برای اجرای این الگوریتم روی گراف شکلی که در بخش قبلی آن را بررسی کردیم، به صورت زیر عمل کرده و می‌بینیم که نتیجه با چیزی که ما بدست آوردیم یکسان است:

graph = {
  'A': {'B': 5, 'E': 10, 'F': 7},
  'B': {'A': 5, 'C': 7, 'F': 8},
  'C': {'B': 7, 'F': 7, 'G': 6},
  'D': {'E': 9, 'F': 5, 'G': 5},
  'E': {'A': 10, 'D': 9, 'F': 8},
  'F': {'A': 7, 'B': 8, 'C': 7, 'D': 5, 'E': 8, 'G': 4},
  'G': {'C': 6, 'D': 5, 'F': 4}
}

kruskal_algorithm(graph)

[(‘F’, ‘G’, 4)]
[(‘F’, ‘G’, 4), (‘A’, ‘B’, 5)]
[(‘F’, ‘G’, 4), (‘A’, ‘B’, 5), (‘D’, ‘F’, 5)]
[(‘F’, ‘G’, 4), (‘A’, ‘B’, 5), (‘D’, ‘F’, 5), (‘C’, ‘G’, 6)]
[(‘F’, ‘G’, 4), (‘A’, ‘B’, 5), (‘D’, ‘F’, 5), (‘C’, ‘G’, 6), (‘A’, ‘F’, 7)]
[(‘F’, ‘G’, 4), (‘A’, ‘B’, 5), (‘D’, ‘F’, 5), (‘C’, ‘G’, 6), (‘A’, ‘F’, 7), (‘E’, ‘F’, 8)]

مزایا و معایب الگوریتم‌ کراسکال

الگوریتم کراسکال مزایای منحصر به فردی دارد که در ادامه آن‌ها را بررسی می‌‌کنیم:

این الگوریتم نیز علاوه بر مزایای خود، دارای معایبی است که در ادامه به آن‌ها اشاره می‌کنیم:

مقایسه الگوریتم‌های پریم و کراسکال

ویژگی‌هاالگوریتم پریمالگوریتم کروسکال
نوع الگوریتمحریصانهحریصانه
کاربرد اصلییافتن درخت پوشای کمینهیافتن درخت پوشای کمینه
پیچیدگی زمانی (ماتریس مجاورت)O(V2)O(ElogE)
پیچیدگی زمانی (لیست مجاورت و هیپ باینری)O(ElogV)O(ElogE)
شروع از یک راس دلخواهلبه‌ها با کمترین وزن
روش انتخاب لبه‌هااضافه کردن لبه‌ها به گراف در حال ساختاضافه کردن لبه‌ها به ترتیب وزن
قابلیت استفاده با گراف‌های متراکمعملکرد بهترعملکرد بهتر با گراف‌های پراکنده
ساختار داده مورد نیازماتریس مجاورت یا لیست مجاورتلیست لبه‌ها
عملکرد کلیساده‌تر و قابل درک‌تر برای گراف‌های متراکممناسب‌تر برای گراف‌های با تعداد زیاد لبه‌ها
تعداد مقایسه لبه‌هاکمتربیشتر
پشتیبانی از گراف‌های جهت‌دارخیرخیر
قابلیت استفاده با گراف‌های وزن‌داربلهبله
استفاده از هیپبله، در پیاده‌سازی با هیپ باینریخیر، به طور مستقیم
مدیریت جنگل‌های گرافیک درخت پوشای منفردچندین جنگل که در نهایت یکی می‌شوند
گام‌های افزایشیاضافه کردن یال به درخت پوشااضافه کردن یال به مجموعه جنگل‌ها
ایده اصلیگسترش یک زیرمجموعه از گرافترکیب زیرمجموعه‌ها به تدریج
تضمین درخت پوشای کمینهبلهبله

کاربردهای درخت پوشای کمینه

درخت پوشای کمینه کاربردهای گسترده‌ای دارد که از طراحی شبکه‌های کامپیوتری و سیستم‌های حمل و نقل گرفته تا مدارهای الکتریکی و شبکه‌های توزیع آب و برق را شامل می‌شود. این الگوریتم‌ها می‌توانند در بهینه‌سازی مسیرها، کاهش هزینه‌های نصب و نگهداری، و بهبود عملکرد سیستم‌ها نقش بسزایی ایفا کنند. در ادامه کاربردهای این درخت را در حوزه‌های مختلف بررسی می‌کنیم:

کلام آخر درباره درخت پوشای کمینه

در این مقاله با مفهوم درخت پوشای کمینه (MST) و اهمیت آن در بهینه‌سازی شبکه‌ها و کاهش هزینه‌ها آشنا شدیم. درخت پوشای کمینه به ما این امکان را می‌دهد تا با کمترین هزینه ممکن تمامی رئوس یک گراف را به هم متصل کنیم. همچنین دو الگوریتم معروف پریم و کراسکال برای پیدا کردن MST را بررسی کردیم. هر یک از این الگوریتم‌ها ویژگی‌ها و کاربردهای خاص خود را دارند و بسته به نوع گراف و نیازهای مسأله می‌توان از آن‌ها بهره برد.

الگوریتم پریم با انتخاب یال‌های کم‌وزن به صورت تکراری و افزودن آن‌ها به یک زیرمجموعه از گراف، تا زمانی که تمامی رئوس درخت پوشا شوند، عمل می‌کند. این الگوریتم به خصوص در گراف‌های چگال کارایی خوبی دارد. از سوی دیگر، الگوریتم کراسکال با مرتب‌سازی یال‌ها بر اساس وزن و انتخاب یال‌ها به ترتیب وزن، بدون ایجاد حلقه، درخت پوشای کمینه را تشکیل می‌دهد. این روش در گراف‌های پراکنده که تعداد یال‌ها نسبت به تعداد راس‌ها کمتر است، عملکرد بهتری دارد.

در نهایت، MST به عنوان یکی از ابزارهای قدرتمند در نظریه گراف، نقش مهمی در بهبود عملکرد و کارایی سیستم‌های مختلف از جمله شبکه‌های کامپیوتری، حمل و نقل و سیستم‌های الکتریکی دارد. با استفاده از این الگوریتم‌ها می‌توانیم به طور کارآمد و دقیق شبکه‌ها را بهینه‌سازی کنیم و بهترین مسیرها را برای ارتباطات و حمل و نقل پیدا کنیم. این مفاهیم و الگوریتم‌ها نه تنها در علوم کامپیوتر و ریاضیات بلکه در بسیاری از زمینه‌های مهندسی و علمی دیگر نیز کاربردهای گسترده‌ای دارند.

سوالات متداول

چرا درخت پوشای کمینه در طراحی شبکه‌های کامپیوتری مهم است؟

درخت پوشای کمینه به ما این امکان را می‌دهد تا تمامی گره‌های یک شبکه را با کمترین هزینه ممکن به هم متصل کنیم. این ویژگی به بهینه‌سازی پهنای باند (Bandwidth) و کاهش هزینه‌های انتقال داده کمک می‌کند.

 تفاوت‌های اصلی بین الگوریتم پریم و الگوریتم کراسکال چیست؟

الگوریتم پریم از یک نود شروع کرده و یال‌ها را به ترتیب کمترین وزن اضافه می‌کند تا تمامی نودها پوشش داده شوند. الگوریتم کراسکال ابتدا یال‌ها را بر اساس وزن مرتب کرده و سپس آن‌ها را به ترتیب اضافه می‌کند بدون اینکه حلقه‌ای ایجاد شود. الگوریتم پریم برای گراف‌های چگال (Dense Graphs) مناسب‌تر است، در حالی که الگوریتم کراسکال در گراف‌های پراکنده (Sparse Graphs) عملکرد بهتری دارد.

چگونه می‌توان از درخت پوشای کمینه در بهینه‌سازی مسیرهای حمل و نقل استفاده کرد؟

درخت پوشای کمینه می‌تواند در طراحی مسیرهای بهینه برای حمل و نقل استفاده شود تا هزینه‌های سوخت و زمان سفر کاهش یابد. با استفاده از MST، تمامی نقاط مهم (مانند شهرها یا ایستگاه‌ها) با کمترین هزینه ممکن به هم متصل می‌شوند.

چه عواملی بر یکتایی (Uniqueness) درخت پوشای کمینه تاثیر می‌گذارند؟

اگر تمامی یال‌های یک گراف دارای وزن‌های منحصر به فرد باشند، درخت پوشای کمینه یکتا خواهد بود. در گراف‌هایی که وزن یال‌ها تکراری هستند، ممکن است چندین درخت پوشای کمینه با مجموع وزن‌های یکسان وجود داشته باشد.

چگونه الگوریتم‌های MST در بهینه‌سازی سیستم‌های الکتریکی مورد استفاده قرار می‌گیرند؟

در سیستم‌های الکتریکی، درخت پوشای کمینه می‌تواند به طراحی مدارهایی با کمترین طول سیم (Wire Length) و کمترین افت ولتاژ (Voltage Drop) کمک کند. این کار باعث کاهش هزینه‌های تولید و بهبود کارایی مدارهای الکتریکی می‌شود.

یادگیری تحلیل داده را از امروز شروع کنید!

دنیای داده‌ها جذاب است و دانستن علم داده، توانایی تحلیل داده‌، یا بازاریابی مبتنی بر داده، شما را برای فرصت‌های شغلی بسیاری مناسب می‌کند. فارغ از رشته‌ و پیش‌زمینه‌، می‌توانید حالا شروع کنید و از سطح مقدماتی تا پیشرفته بیاموزید. اگر دوست دارید به این حوزه وارد شوید، پیشنهاد می‌کنیم با کلیک روی این لینک قدم اول را همین حالا بردارید.

مشاوران کافه‌تدریس به شما کمک می‌کنند مسیر یادگیری برای ورود به این حوزه را شروع کنید:

دوره جامع دیتا ساینس و ماشین لرنینگ

خروج از نسخه موبایل