🌲 10.8: Minimum Yayılan Ağaç (MST)

Kruskal ve Prim Algoritmaları

📌 Bu Bölümde Öğrenecekleriniz

🌲

MST Nedir?

Temel kavram

🔀

Kruskal

Kenar tabanlı

📍

Prim

Düğüm tabanlı

🔗

Union-Find

Veri yapısı

🎯 Minimum Yayılan Ağaç Nedir?

📚 Tanım

Minimum Spanning Tree (MST), bir ağırlıklı grafın tüm düğümlerini birbirine bağlayan, toplam kenar ağırlığı en düşük olan ağaç yapısıdır.

🌳 Ağaç Özelliği:
N düğüm → N-1 kenar
🔄 Döngü Yok:
MST'de asla döngü olmaz
🔗 Bağlı:
Tüm düğümler erişilebilir
⚖️ Minimum:
Toplam ağırlık en az

💡 Günlük Hayat Analojisi

Bir şehirde tüm evleri elektrik şebekesine bağlamak istiyorsunuz. Her sokağa kablo döşemenin bir maliyeti var. Tüm evlerin elektrik alması için minimum kablo maliyeti ne olmalı?

Bu tam olarak MST problemidir: Evler = düğümler, sokaklar = kenarlar, kablo maliyeti = ağırlık.

🌍 Gerçek Dünya Uygulamaları

🔌 Elektrik Şebekesi

Şehirleri/evleri minimum kablo maliyetiyle birbirine bağlama. Türkiye'nin elektrik dağıtım ağı planlaması.

📡 Telekomünikasyon

Fiber optik kablo döşeme, baz istasyonu ağı kurma. Minimum altyapı maliyetiyle maksimum kapsama.

🛤️ Yol/Demiryolu Ağı

Köyleri/şehirleri birbirine bağlayan en ekonomik yol ağı. Türkiye hızlı tren ağı planlaması.

💧 Su/Doğalgaz Boru Hattı

Bölgelere minimum boru hattı maliyetiyle su/gaz dağıtımı.

🖥️ Bilgisayar Ağları

LAN/WAN tasarımı. Router'ları minimum kablo ile bağlama, spanning tree protocol.

🧬 Kümeleme (Clustering)

Single-linkage hierarchical clustering. MST kenarlarını keserek kümeler oluşturma.

🎮 İnteraktif MST Görselleştirmesi

Kruskal ve Prim algoritmalarının adım adım çalışmasını izleyin

📊 MST Durumu: Algoritmayı başlatın...

🟢 Kruskal Algoritması

Temel Fikir

Kenar odaklı, greedy (açgözlü) yaklaşım:

  1. Tüm kenarları ağırlığa göre küçükten büyüğe sırala
  2. Her kenarı sırayla incele
  3. Kenar döngü oluşturmuyorsa MST'ye ekle
  4. N-1 kenar eklenince dur

🔑 Kritik Soru: Döngü Oluşur mu?

İki düğüm zaten aynı bileşendeyse (bağlıysa), aralarına kenar eklersek döngü oluşur!

Çözüm: Union-Find (Disjoint Set Union) veri yapısı

🔗 Union-Find Veri Yapısı

Union-Find, kümeleri verimli şekilde yönetmek için kullanılır:

find(x)

x'in hangi kümeye ait olduğunu bul

Path Compression: Yol boyunca tüm düğümleri köke bağla → O(α(n)) ≈ O(1)

union(x, y)

x ve y'nin kümelerini birleştir

Union by Rank: Küçük ağacı büyüğe bağla → dengeli ağaç

Union-Find Veri Yapısı
class UnionFind:
    """
    Disjoint Set Union (DSU) / Union-Find
    
    Path Compression + Union by Rank optimizasyonları ile
    neredeyse O(1) amortized karmaşıklık sağlar.
    """
    
    def __init__(self, n):
        # Her düğüm başlangıçta kendi kümesinin kökü
        self.parent = list(range(n))
        # Rank: ağaç yüksekliği (yaklaşık)
        self.rank = [0] * n
        # Küme sayısı
        self.num_sets = n
    
    def find(self, x):
        """
        x'in kök düğümünü bul
        Path Compression: Yol üzerindeki tüm düğümleri köke bağla
        """
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])  # Recursive + memoization
        return self.parent[x]
    
    def union(self, x, y):
        """
        x ve y'nin kümelerini birleştir
        Union by Rank: Küçük ağacı büyüğe bağla
        
        Returns: True eğer birleştirme yapıldıysa (farklı kümelerdeydiler)
        """
        root_x = self.find(x)
        root_y = self.find(y)
        
        # Zaten aynı kümedeler
        if root_x == root_y:
            return False
        
        # Rank'e göre birleştir
        if self.rank[root_x] < self.rank[root_y]:
            root_x, root_y = root_y, root_x
        
        self.parent[root_y] = root_x
        
        if self.rank[root_x] == self.rank[root_y]:
            self.rank[root_x] += 1
        
        self.num_sets -= 1
        return True
    
    def connected(self, x, y):
        """x ve y aynı kümede mi?"""
        return self.find(x) == self.find(y)


# Demo
print("=== UNION-FIND DEMO ===\n")

uf = UnionFind(6)
print(f"Başlangıç: 6 ayrı küme (her düğüm kendi kümesinde)")
print(f"Küme sayısı: {uf.num_sets}\n")

operations = [
    (0, 1, "0 ve 1'i birleştir"),
    (2, 3, "2 ve 3'ü birleştir"),
    (0, 2, "0 ve 2'yi birleştir (artık 0,1,2,3 aynı kümede)"),
    (4, 5, "4 ve 5'i birleştir"),
]

for x, y, desc in operations:
    result = uf.union(x, y)
    print(f"union({x}, {y}): {desc}")
    print(f"  → Birleştirildi: {result}, Küme sayısı: {uf.num_sets}")

print(f"\n0 ve 3 bağlı mı? {uf.connected(0, 3)}")  # True
print(f"0 ve 5 bağlı mı? {uf.connected(0, 5)}")  # False
Çıktı bekleniyor...
Kruskal Algoritması - Tam Implementasyon
class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n
    
    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]
    
    def union(self, x, y):
        px, py = self.find(x), self.find(y)
        if px == py:
            return False
        if self.rank[px] < self.rank[py]:
            px, py = py, px
        self.parent[py] = px
        if self.rank[px] == self.rank[py]:
            self.rank[px] += 1
        return True


def kruskal_mst(n, edges):
    """
    Kruskal algoritması ile Minimum Spanning Tree bul
    
    Args:
        n: Düğüm sayısı (0'dan n-1'e)
        edges: [(u, v, weight), ...] kenar listesi
    
    Returns:
        (mst_edges, total_weight) tuple
    
    Karmaşıklık: O(E log E) - sıralama dominant
    """
    # 1. Kenarları ağırlığa göre sırala
    sorted_edges = sorted(edges, key=lambda e: e[2])
    
    uf = UnionFind(n)
    mst = []
    total_weight = 0
    
    print("Kenarlar (ağırlığa göre sıralı):")
    for u, v, w in sorted_edges:
        print(f"  ({u}-{v}, w={w})")
    print()
    
    # 2. Her kenarı incele
    for u, v, weight in sorted_edges:
        # 3. Döngü kontrolü
        if uf.union(u, v):
            # Farklı bileşenlerdeydi, birleştir
            mst.append((u, v, weight))
            total_weight += weight
            print(f"✅ Kenar ({u}-{v}, w={weight}) eklendi")
            
            # 4. N-1 kenar bulunca dur
            if len(mst) == n - 1:
                break
        else:
            print(f"❌ Kenar ({u}-{v}, w={weight}) DÖNGÜ OLUŞTURUR, atlandı")
    
    return mst, total_weight


# Örnek: 6 şehir arasında kablo döşeme
print("=== KRUSKAL ALGORİTMASI ===")
print("Şehirler: A=0, B=1, C=2, D=3, E=4, F=5\n")

edges = [
    (0, 1, 4),   # A-B
    (0, 5, 2),   # A-F
    (1, 2, 6),   # B-C
    (1, 5, 5),   # B-F
    (2, 3, 3),   # C-D
    (2, 4, 5),   # C-E
    (3, 4, 2),   # D-E
    (4, 5, 4),   # E-F
]

mst, total = kruskal_mst(6, edges)

print(f"\n🌲 MST Kenarları:")
for u, v, w in mst:
    print(f"  {u} — {v} (ağırlık: {w})")
print(f"\n📏 Toplam Maliyet: {total}")
Çıktı bekleniyor...

🔵 Prim Algoritması

Temel Fikir

Düğüm odaklı, greedy yaklaşım (Dijkstra'ya benzer):

  1. Rastgele bir düğümden başla
  2. MST'ye bağlı düğümlerden çıkan en küçük ağırlıklı kenarı seç
  3. Bu kenarın gittiği düğümü MST'ye ekle
  4. Tüm düğümler eklenene kadar tekrarla

⚡ Dijkstra vs Prim

Her ikisi de priority queue kullanır, ama:

  • Dijkstra: Başlangıçtan toplam mesafe minimize
  • Prim: Sadece o kenarın ağırlığı minimize (toplam değil!)
Prim Algoritması - Priority Queue ile
import heapq
from collections import defaultdict

def prim_mst(n, edges, start=0):
    """
    Prim algoritması ile Minimum Spanning Tree bul
    
    Args:
        n: Düğüm sayısı
        edges: [(u, v, weight), ...] kenar listesi
        start: Başlangıç düğümü
    
    Returns:
        (mst_edges, total_weight) tuple
    
    Karmaşıklık: O(E log V) - priority queue ile
    """
    # Adjacency list oluştur
    graph = defaultdict(list)
    for u, v, w in edges:
        graph[u].append((v, w))
        graph[v].append((u, w))
    
    visited = set()
    mst = []
    total_weight = 0
    
    # Min heap: (weight, from_node, to_node)
    # Başlangıç düğümü için -1 kullanıyoruz (ilk düğüm)
    pq = [(0, -1, start)]
    
    print(f"Başlangıç düğümü: {start}\n")
    
    while pq and len(visited) < n:
        weight, from_node, to_node = heapq.heappop(pq)
        
        # Zaten ziyaret edilmişse atla
        if to_node in visited:
            continue
        
        visited.add(to_node)
        
        if from_node != -1:
            mst.append((from_node, to_node, weight))
            total_weight += weight
            print(f"✅ Düğüm {to_node} eklendi (kenar: {from_node}-{to_node}, w={weight})")
        else:
            print(f"🚀 Başlangıç düğümü {to_node} eklendi")
        
        # Yeni düğümün komşularını heap'e ekle
        for neighbor, edge_weight in graph[to_node]:
            if neighbor not in visited:
                heapq.heappush(pq, (edge_weight, to_node, neighbor))
    
    return mst, total_weight


# Aynı örnek
print("=== PRİM ALGORİTMASI ===")
print("Şehirler: A=0, B=1, C=2, D=3, E=4, F=5\n")

edges = [
    (0, 1, 4),   # A-B
    (0, 5, 2),   # A-F
    (1, 2, 6),   # B-C
    (1, 5, 5),   # B-F
    (2, 3, 3),   # C-D
    (2, 4, 5),   # C-E
    (3, 4, 2),   # D-E
    (4, 5, 4),   # E-F
]

mst, total = prim_mst(6, edges, start=0)

print(f"\n🌲 MST Kenarları:")
for u, v, w in mst:
    print(f"  {u} — {v} (ağırlık: {w})")
print(f"\n📏 Toplam Maliyet: {total}")
Çıktı bekleniyor...

⚖️ Kruskal vs Prim Karşılaştırması

Özellik Kruskal Prim
Yaklaşım Kenar odaklı (global) Düğüm odaklı (local/incremental)
Veri Yapısı Union-Find Priority Queue (Min Heap)
Zaman Karmaşıklığı O(E log E) O(E log V)
Seyrek Graf (E ≈ V) ✅ Daha İyi 🔶 İyi
Yoğun Graf (E ≈ V²) 🔶 Orta ✅ Daha İyi
Paralel İşleme ✅ Kolay ❌ Zor
Dinamik Güncelleme 🔶 Orta ✅ Daha Kolay

📝 Hangi Algoritma Ne Zaman?

  • Seyrek graf (az kenar) → Kruskal tercih edilir
  • Yoğun graf (çok kenar) → Prim tercih edilir
  • Kenarlar verilmişse → Kruskal daha doğal
  • Adjacency list varsa → Prim daha doğal
  • Paralel işlem gerekiyorsa → Kruskal

📐 MST Özellikleri ve Teoremler

🔹 Cut Property

Bir grafın herhangi bir kesiti için, kesiti geçen minimum ağırlıklı kenar mutlaka MST'dedir.

🔹 Cycle Property

Bir döngüdeki maksimum ağırlıklı kenar, hiçbir MST'de bulunamaz.

🔹 Uniqueness

Tüm kenar ağırlıkları farklıysa, MST tektir. Aynı ağırlıklar varsa birden fazla MST olabilir.

📋 Özet

Algoritma Zaman Alan Not
Kruskal O(E log E) O(V) Union-Find gerektirir
Prim (Binary Heap) O(E log V) O(V) Dijkstra'ya benzer
Prim (Fibonacci Heap) O(E + V log V) O(V) Teorik olarak en hızlı