…bir oyunda her seferinde farklı ama her zaman çözülebilir labirentler üretebilecek ve “bu ikisi bağlı mı?” sorusunu milyonlarca nesne için bile anında cevaplayabileceksiniz.
Bir zindanda 10.000 oda var. Oyuncular kapıları birer birer açıyor.
Her kapı açıldıktan sonra soru: “Giriş ile hazine odası arasında artık bir yol var mı?”
Her seferinde tüm haritayı baştan mı tarayacağız? 10.000 kapı × 10.000 oda = 100 milyon adım… 😩
Bugünkü kahramanımız bu soruyu neredeyse tek adımda cevaplıyor: union–find.
| union(p, q) | birleştir | p ile q arasına bağlantı ekle |
| find(p) | bul | p’nin bileşeninin “temsilcisi” (kök) kim? |
| connected(p, q) | bağlı mı? | p’den q’ya bir yol var mı? → find(p) == find(q) |
| connected component | bağlı bileşen | Birbirine bağlı nesnelerin en büyük grubu |
| root / parent | kök / ebeveyn | Ağacın tepesi / bir üstteki düğüm |
| weighted | ağırlıklı | Birleştirirken ağaç boyutlarını dikkate almak |
| path compression | yol sıkıştırma | find sırasında ağacı düzleştirmek |
| percolation | perkolasyon (sızma) | Bir malzemenin bir ucundan diğerine geçiş olması |
N nesne var: 0, 1, 2, …, N−1 (odalar, piksel, oyuncu…).
union(p, q): p ile q arasına bağlantı ekleconnected(p, q): p ile q arasında (doğrudan ya da dolaylı) yol var mı?“Bağlı olmak” şu kurallara uyar:
Bu kurallar bize nesneleri bağlı bileşenlere ayırmamızı sağlar: aynı bileşendekiler bağlı, farklıdakiler değil.
Dictionary<string, int>) çeviririz.
Bugün bu sınıfın içini dört farklı şekilde yazacağız ve hızlarını karşılaştıracağız. Kullanan kod hiç değişmeyecek (soyutlama!).
Union Find Introduction
WilliamFiset · 5 dk 46 sn · YouTube’da aç
Bu sunumun başından sonuna kuracağımız yapının kısa ve görsel bir haritası: kümeler, kökler, union ve find. Sonraki slaytlardaki her sürüm bu fikrin bir iyileştirmesi.
Aynı konuyu başka bir anlatımla, kendi elinizle oynayarak görmek çok işe yarar. Kartlar yeni sekmede açılır; çoğu İngilizce ama görsel ve etkileşimli.
0–1, 2–3, 1–3, 4–5, 0–2. Kaç bileşen kalır?id[i] = i’nin bileşen numarasıconnected(p, q): id[p] == id[q] mi? → 2 erişim, süper hızlı ⚡union(p, q): id’si id[p] olan herkesi id[q] yap → tüm diziyi tara 🐌
🎒 list(range(n)) / Array.from({ length: n }, (_, i) => i): “0’dan n−1’e kadar sayılardan oluşan liste”. İkincisi modern JS yazımı; geleneksel hali: boş dizi + for ile push(i).
| ifade | ne demek | neden böyle |
|---|---|---|
id[i] | i’nin grup numarası | aynı gruptakiler aynı numarayı taşır |
connected | iki numarayı karşılaştır | tek adım: sorgu çok hızlı |
for (i …) if (id[i] === pid) | p’nin grubundaki herkesi q’nun grubuna taşı | her birleştirme bütün diziyi tarar: ~N iş; N birleştirme ~N² |
pid ve qid önceden okunur | döngü sırasında id[p] değişir | döngüde id[p] kullanılırsa yarıda yanlış gruba geçilir (klasik hata) |
count-- | grup sayısı bir azaldı | “kaç ada kaldı?” sorusunun cevabı |
Ağaçlar dersinde düğümleri nesnelerle ve left/right referanslarıyla bağlamıştık. Burada daha basit bir yol var:
id[i] = i’nin ebeveyninin numarasıid[r] == r)while p != id[p]:p = id[p]
find(p): p’nin kökünü bul (ebeveyne çıka çıka)connected(p, q): kökler aynı mı?union(p, q): p’nin kökünü, q’nun köküne bağla → tek bir dizi değeri değişir!find çok yavaş: en kötü N adım. “😈 Kötü sıra” düğmesini deneyin!
| ifade | ne demek | neden böyle |
|---|---|---|
id[i] | i’nin ebeveyni (kök kendini gösterir) | grup bir ağaçtır; kimlik kökün numarası |
find döngüsü | köke kadar ebeveyn ebeveyn çık | maliyet = ağacın yüksekliği |
id[i] = j | bir kökü öbür köke bağla | birleştirme tek atama; ama kötü sırada ağaç zincire döner |
if (i === j) return | zaten aynı ağaçta | aynı kökü kendine bağlamak döngü yaratırdı |
| en kötü durum | ~N yükseklik | sonraki iyileştirme: küçüğü büyüğün altına (ağırlıklı) |
union(0,1), union(0,2), …, union(0,N−1) sırasıyla yapılırsa ağacın yüksekliği ne olur?find(0) N adım sürer. Quick-union’ın zayıf noktası: ağacın şeklini hiç düşünmüyor.find’ını kullanarak quick-union yapıyor (parent[find(p)] = find(q)) ve bağlantı sorularının cevaplarını kontrol ediyor. Tablodaki “en derin ağaç” sütununa da bak: “kötü sıra” testinde neden bu kadar derin?sz[]
| ifade | ne demek | neden böyle |
|---|---|---|
sz[i] | i köküne bağlı ağaçtaki düğüm sayısı | yalnızca köklerin sz’si anlamlı |
if (sz[i] < sz[j]) | küçük ağacı büyüğün altına bağla | küçük ağaçtaki her düğümün derinliği 1 artar ama ağaç en az iki katına çıkar |
sz[j] += sz[i] | birleşen ağacın boyunu güncelle | sonraki birleşmelerde doğru karşılaştırma için |
| sonuç | yükseklik en çok lg N | bir düğüm en çok lg N kez “iki katı büyük” bir ağaca katılabilir |
Bir x düğümünün derinliği ne zaman artar? Sadece x’in ağacı, kendisinden büyük ya da eşit bir ağacın altına girdiğinde.
O anda x’in içinde bulunduğu ağacın boyutu en az iki katına çıkar.
1 düğümden başlayıp kaç kez iki katına çıkabilir? N’e ulaşana kadar: en fazla lg N kez. (Arama dersindeki “kaç kez yarıya?” sorusunun tersi!)
find, union ve connected en kötü durumda ~lg N erişim yapar.
N = 1 milyar → en fazla 30 adım. Quick-find’da 1 milyar!
find(p) kökü ararken zaten yoldaki düğümlerin hepsinden geçiyor. Madem geçiyoruz, onları köke yakınlaştıralım!
| ifade | ne demek | neden böyle |
|---|---|---|
id[p] = id[id[p]] | p’yi dedesine bağla | her find yolu yarıya indirir (yarılama) |
| tek satır ek | ağırlıklı union’a eklenir | find çağrıldıkça ağaçlar kendiliğinden düzleşir |
| sonuç | pratikte sabit iş | lg* N ≤ 5: evrendeki atom sayısı kadar eleman için bile |
| tam sıkıştırma (alternatif) | yoldaki herkesi doğrudan köke bağla | ikinci bir geçiş gerekir; yarılama tek geçişte neredeyse aynı etkiyi verir |
Ağırlıklandırma + yol sıkıştırma ile M işlem, ~M · lg* N sürer.
lg* N (“log yıldız”): 1’e inene kadar kaç kez logaritma alabilirsin?
| N | lg* N |
|---|---|
| 2 | 1 |
| 16 | 3 |
| 65.536 | 4 |
| 265536 (evrendeki atomlardan çok, çok fazla) | 5 |
Pratikte: N ne olursa olsun işlem başına ≤ 5 adım. “Neredeyse sabit zaman.”
| algoritma | başlatma | union | find / connected | M işlem, N nesne |
|---|---|---|---|---|
| quick-find | N | N | 1 | M · N |
| quick-union | N | ağaç yüksekliği* | ağaç yüksekliği | M · N (en kötü) |
| ağırlıklı QU | N | lg N* | lg N | M · lg N |
| ağırlıklı QU + yol sıkıştırma | N | ≈ 1* | ≈ 1 | ≈ M (M · α(N)) |
* kökleri bulma maliyeti dahil. 10⁹ union ve 10⁹ find: quick-find ile 30 yıl, ağırlıklı + yol sıkıştırma ile 6 saniye. İyi algoritma, süper bilgisayardan değerlidir.
parent[p] = parent[parent[p]]) ve “en derin ağaç” sütununun nasıl değiştiğine bak. Sonra iki geçişli tam sıkıştırmayı dene.
🎒 2B ızgarayı tek boyuta indirmek: hücre (x, y) → y * cols + x. Union–find nesneleri 0…N−1 diye numaralanmış ister.
| ifade | ne demek | neden böyle |
|---|---|---|
c = y * cols + x | iki boyutlu hücreyi tek numaraya çevir | union-find düz numaralarla çalışır |
walls.push([c, c + 1]), [c, c + cols] | her hücrenin sağ ve alt duvarı | her iç duvar bir kez listelenir |
shuffle(walls) | duvarları rastgele sıraya koy | her çalıştırmada farklı labirent |
if (!uf.connected(a, b)) | iki taraf henüz bağlı değilse duvarı yık | bağlıysa yıkmak döngü yaratırdı: tek çözümlü labirent |
| sonuç | tam hücre − 1 geçit | bir yayılan ağaç: her hücreye ulaşılır, iki hücre arasında tek yol (rastgele Kruskal) |
N×N bir ızgarada her hücre p olasılıkla açık. Üstten dökülen su, açık hücrelerden geçerek alta ulaşabiliyor mu?
connected(üst, alt)?int dizisi: id[] ve sz[] → N nesne için ~8N baytDers: hızlı algoritma > hızlı bilgisayar. Quick-find ile 30 yıl süren iş, doğru algoritmayla 6 saniye.
Kaynak: Wikipedia: Hex · Gale 1979
| ad | platform | ne yapar / sınırları | nasıl eklenir |
|---|---|---|---|
| scipy DisjointSet | Python (SciPy 1.6+) | merge, connected, subset; hazır ayrık küme. | pip install scipy |
| networkx.utils.UnionFind | Python | Basit union-find yardımcı sınıfı. | pip install networkx |
| scipy.ndimage.label | Python | Izgarada bağlı bileşen etiketleme (görüntü, harita). | pip install scipy |
| boost::disjoint_sets | C++ | Rank ve yol sıkıştırmalı ayrık kümeler. | Boost |
| kendi sınıfınız | C# / Unity | .NET’te hazır yok; 30 satırlık bir sınıf (int[] parent, int[] size) yeterli ve çöp üretmez. | — |
“Unity 6, C#. Hex oyunu yaz: 11×11 altıgen ızgara (eksenel koordinatlar), iki oyuncu sırayla taş koyar. UnionFind sınıfı (int[] parent, int[] size, yol sıkıştırmalı ve özyinelemesiz Find, boyuta göre Union); her oyuncu için iki sanal kenar düğümü; her hamleden sonra kazanma kontrolü. Altıgen komşuluklarını ve kazanma kontrolünü test eden NUnit testleri ekle.”
9-0 3-4 5-8 7-2 2-1 5-7 0-3 4-2 bağlantıları için quick-find’ın id[] dizisini her adımda yazın. Sonra aynısını ağırlıklı quick-union için yapıp ormanı çizin.findMax(p) ekleyin (ipucu: köklerde bir dizi daha tutun).