🌲 10.3: Quick-Union Algoritması

Ağaç Tabanlı Yaklaşım (Lazy Approach)

📌 Quick-Union Yaklaşımı

Quick-Find'ın union işlemi çok yavaştı (O(N)). Quick-Union yaklaşımında, elemanları ağaç yapısı ile temsil ederek union işlemini hızlandıracağız.

Artık her eleman bir küme ID'si değil, bir parent (ebeveyn) tutuyor.

📊 Veri Yapısı: Ağaç (Forest)

💡 Temel Fikir

  • parent[] dizisi: Her eleman kendi ebeveynini (parent) tutar
  • Root (Kök): parent[i] == i olan eleman
  • p ve q bağlıroot(p) == root(q)
  • Her küme bir ağaç, tüm kümeler bir orman (forest)
Örnek: {0, 1, 2, 5, 6} ve {3, 4, 8, 9} kümeleri Ağaç 1: Ağaç 2: 1 8 /|\ / \ 0 2 7 3 9 | | 5 4 | 6 parent[] dizisi: index: 0 1 2 3 4 5 6 7 8 9 parent: [1, 1, 1, 8, 3, 2, 5, 1, 8, 8] ↑ ↑ ↑ ↑ root root değil root Root bulma örnekleri: - root(0) = 1 (0→1, parent[1]=1 ✓) - root(6) = 1 (6→5→2→1, parent[1]=1 ✓) - root(4) = 8 (4→3→8, parent[8]=8 ✓)

🔧 İşlemler

🔍 Find (Root Bulma)

Algoritma:

  1. p'den başla
  2. parent[p] ≠ p iken, p = parent[p]
  3. Root'a ulaşınca döndür

Karmaşıklık: O(ağaç yüksekliği)

🔗 Union

Algoritma:

  1. rootP = find(p)
  2. rootQ = find(q)
  3. Eşit değilse: parent[rootP] = rootQ

Karmaşıklık: O(ağaç yüksekliği)

🎯 Union Neden Hızlı?

Quick-Find'da union için tüm diziyi tarıyorduk (O(N)).

Quick-Union'da sadece bir pointer değiştiriyoruz: Bir ağacın kökünü diğer ağacın köküne bağla!

📝 Adım Adım Örnek

Başlangıç: Her eleman kendi parent'ı (10 ayrı ağaç) 0 1 2 3 4 5 6 7 8 9 (her biri root) parent[]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] ═══════════════════════════════════════════════════════════════ union(4, 3): 4'ün root'unu 3'ün root'una bağla root(4)=4, root(3)=3 → parent[4] = 3 ───────────────────────────────────────────────────────────────── 0 1 2 3 5 6 7 8 9 | 4 parent[]: [0, 1, 2, 3, 3, 5, 6, 7, 8, 9] ↑ değişti ═══════════════════════════════════════════════════════════════ union(3, 8): 3'ün root'unu 8'e bağla (3 zaten root) parent[3] = 8 ───────────────────────────────────────────────────────────────── 0 1 2 5 6 7 8 9 | 3 | 4 parent[]: [0, 1, 2, 8, 3, 5, 6, 7, 8, 9] ↑ değişti ═══════════════════════════════════════════════════════════════ union(6, 5): parent[6] = 5 ───────────────────────────────────────────────────────────────── 0 1 2 5 7 8 9 | | 6 3 | 4 parent[]: [0, 1, 2, 8, 3, 5, 5, 7, 8, 9] ↑ ═══════════════════════════════════════════════════════════════ union(9, 4): root(9)=9, root(4)=8 → parent[9] = 8 (4'ten başlayıp 3→8'e gidiyoruz) ───────────────────────────────────────────────────────────────── 0 1 2 5 7 8 | /|\ 6 3 9 | 4 parent[]: [0, 1, 2, 8, 3, 5, 5, 7, 8, 8] ↑ ═══════════════════════════════════════════════════════════════ union(2, 1): parent[2] = 1 ───────────────────────────────────────────────────────────────── 0 1 5 7 8 | | /|\ 2 6 3 9 | 4 parent[]: [0, 1, 1, 8, 3, 5, 5, 7, 8, 8] ↑ ═══════════════════════════════════════════════════════════════ connected(8, 9)? → root(8)=8, root(9)=8 → TRUE ✓ connected(5, 4)? → root(5)=5, root(4)=8 → FALSE ✗ ═══════════════════════════════════════════════════════════════ union(5, 0): parent[5] = 0 (6 artık 5→0 yolunu izleyecek) ───────────────────────────────────────────────────────────────── 0 1 7 8 | | /|\ 5 2 3 9 | | 6 4 parent[]: [0, 1, 1, 8, 3, 0, 5, 7, 8, 8] ↑ ═══════════════════════════════════════════════════════════════ union(7, 2): root(7)=7, root(2)=1 → parent[7] = 1 ───────────────────────────────────────────────────────────────── 0 1 8 | / \ /|\ 5 2 7 3 9 | | 6 4 parent[]: [0, 1, 1, 8, 3, 0, 5, 1, 8, 8] ↑ ═══════════════════════════════════════════════════════════════ union(6, 1): root(6)=0, root(1)=1 → parent[0] = 1 (6→5→0, 0 root) ───────────────────────────────────────────────────────────────── 1 8 / | \ /|\ 0 2 7 3 9 | | 5 4 | 6 parent[]: [1, 1, 1, 8, 3, 0, 5, 1, 8, 8] ↑ ═══════════════════════════════════════════════════════════════ connected(0, 7)? → root(0)=1, root(7)=1 → TRUE ✓ Son durum: 2 ağaç (2 bileşen)

💻 Python Implementasyonu

Quick-Union Implementasyonu
class QuickUnion:
    """
    Quick-Union Implementasyonu
    
    Lazy Approach (Tembel Yaklaşım):
    - Elemanları ağaç yapısında tut
    - Union: Sadece bir pointer değiştir
    - Find: Köke kadar yürü
    
    Veri yapısı:
    - parent[i]: i'nin ebeveyni
    - parent[i] == i ise, i bir root
    """
    
    def __init__(self, n: int):
        """
        N nesne ile Union-Find oluştur.
        Başlangıçta her eleman kendi ağacı (N root).
        """
        self.parent = list(range(n))  # Herkes kendi parent'ı
        self.n = n
        self._count = n
    
    def find(self, p: int) -> int:
        """
        p'nin root'unu bul.
        
        Root'a ulaşana kadar parent'ları takip et.
        Root: parent[x] == x olan eleman.
        
        Zaman: O(ağaç yüksekliği) - en kötü O(N)
        """
        while self.parent[p] != p:
            p = self.parent[p]
        return p
    
    def connected(self, p: int, q: int) -> bool:
        """p ve q aynı ağaçta mı?"""
        return self.find(p) == self.find(q)
    
    def union(self, p: int, q: int) -> None:
        """
        p ve q'nun ağaçlarını birleştir.
        
        Algoritma:
        1. Her ikisinin root'unu bul
        2. p'nin root'unu q'nun root'una bağla
        
        Zaman: O(ağaç yüksekliği) - find maliyeti
        """
        root_p = self.find(p)
        root_q = self.find(q)
        
        if root_p == root_q:
            return  # Zaten aynı ağaçta
        
        # p'nin root'unu q'nun root'una bağla
        self.parent[root_p] = root_q
        self._count -= 1
    
    def count(self) -> int:
        return self._count
    
    def display_tree(self, root=None):
        """Ağaç yapısını göster"""
        if root is None:
            # Tüm root'ları bul
            roots = [i for i in range(self.n) if self.parent[i] == i]
            print(f"Orman ({len(roots)} ağaç):")
            for r in roots:
                self.display_tree(r)
            return
        
        # Belirli bir root'un ağacını göster
        children = [i for i in range(self.n) if self.parent[i] == root and i != root]
        
        def get_descendants(node):
            result = [node]
            for i in range(self.n):
                if self.parent[i] == node and i != node:
                    result.extend(get_descendants(i))
            return result
        
        members = get_descendants(root)
        print(f"  Root {root}: {sorted(members)}")


# ===== DEMO =====
print("=" * 60)
print("QUICK-UNION DEMO")
print("=" * 60)

qu = QuickUnion(10)
print(f"\nBaşlangıç:")
print(f"parent[]: {qu.parent}")
qu.display_tree()

operations = [
    ("union", 4, 3),
    ("union", 3, 8),
    ("union", 6, 5),
    ("union", 9, 4),
    ("union", 2, 1),
    ("query", 8, 9),
    ("query", 5, 4),
    ("union", 5, 0),
    ("union", 7, 2),
    ("union", 6, 1),
    ("query", 0, 7),
]

print("\n" + "─" * 60)

for op in operations:
    if op[0] == "union":
        _, p, q = op
        print(f"\nunion({p}, {q}):")
        qu.union(p, q)
        print(f"parent[]: {qu.parent}")
        qu.display_tree()
    else:
        _, p, q = op
        # Root'ları göster
        root_p, root_q = qu.find(p), qu.find(q)
        result = root_p == root_q
        symbol = "✓" if result else "✗"
        print(f"\nconnected({p}, {q})?")
        print(f"  root({p})={root_p}, root({q})={root_q}")
        print(f"  Sonuç: {result} {symbol}")
Çıktı bekleniyor...

⚠️ Problem: Uzun Ağaçlar

En Kötü Durum

Union işlemleri kötü sırada gelirse, ağaç çok uzun olabilir!

En kötü senaryo: union(0,1), union(1,2), union(2,3), ... Her seferinde küçük ağacı büyüğe bağlıyoruz (ama yanlış şekilde): union(0,1): union(1,2): union(2,3): union(3,4): 1 2 3 4 | | | | 0 1 2 3 | | | 0 1 2 | | 0 1 | 0 N-1 union sonrası: N-1 | N-2 | ... | 1 | 0 Ağaç yüksekliği = N-1 ! find(0) için N adım gerekiyor → O(N)
En Kötü Durum Gösterimi
class QuickUnion:
    def __init__(self, n):
        self.parent = list(range(n))
        self.n = n
        self.find_steps = 0  # Adım sayacı
    
    def find(self, p):
        steps = 0
        while self.parent[p] != p:
            p = self.parent[p]
            steps += 1
        self.find_steps += steps
        return p
    
    def union(self, p, q):
        root_p = self.find(p)
        root_q = self.find(q)
        if root_p != root_q:
            self.parent[root_p] = root_q


# En kötü durum: Uzun zincir oluştur
print("=" * 50)
print("QUICK-UNION: EN KÖTÜ DURUM")
print("=" * 50)

n = 10
qu = QuickUnion(n)

print(f"\n{n} elemanlı yapı")
print("\nUnion işlemleri (kötü sıra):")

# Kötü sırayla union
for i in range(n - 1):
    qu.union(i, i + 1)
    print(f"  union({i}, {i+1})")

print(f"\nparent[]: {qu.parent}")

# Ağaç yapısını göster
print("\nOluşan ağaç (zincir):")
print(f"  {n-1}")
for i in range(n-2, -1, -1):
    print(f"  |")
    print(f"  {i}")

# Find maliyeti
print(f"\nFind maliyeti testi:")
qu.find_steps = 0

for i in range(n):
    steps_before = qu.find_steps
    root = qu.find(i)
    steps_taken = qu.find_steps - steps_before
    print(f"  find({i}) → {root} ({steps_taken} adım)")

print(f"\nToplam adım: {qu.find_steps}")
print(f"Ortalama: {qu.find_steps / n:.1f} adım/find")
print(f"\n⚠️ find(0) için {n-1} adım gerekti! (O(N))")
Çıktı bekleniyor...

📊 Quick-Find vs Quick-Union

Algoritma init find union connected
Quick-Find O(N) O(1) O(N) O(1)
Quick-Union O(N) O(N)* O(N)* O(N)*

* En kötü durumda (ağaç yüksekliği = N)

🤔 Hangisi Daha İyi?

İkisi de en kötü durumda O(N²) toplam maliyet!

  • Quick-Find: Her union O(N), M union → O(MN)
  • Quick-Union: Uzun ağaçta her find O(N), M find → O(MN)

🎯 Sonuç

Quick-Union'ın Sorunu

Ağaçlar dengesiz büyüyebilir. Bu da find işlemini yavaşlatır.

Çözüm fikri: Ağaçları dengeli tutsak?

  • Union yaparken hangi ağacı hangisine bağlayacağımızı akıllıca seçsek?
  • Küçük ağacı büyük ağaca bağlasak?

Bir sonraki bölüm: Weighted Quick-Union! 🌲⚖️

🎮 İnteraktif Quick-Union Simülasyonu

Union: ile
parent[] = [0,1,2,3,4,5,6,7] - Her düğüm kendi kümesinde