Bölüm 16: Kümeler ve Union-Find

C ile Veri Yapıları ve Algoritmalar
Ayrık kümelerBirleşim ve küme bulmaata dizisiKötü durum O(n²)Ağırlıklı kuralAzaltma kuralıTers AckermannBileşen · Kruskal
Sercan KÜLCÜ · Giresun Üniversitesi · Bilgisayar Mühendisliği

Giriş

Öğrenme Çıktıları

Modellemek

İkili ayrık kümeleri çocuktan ataya bağlı ağaçlarla göstermek.

Gerçeklemek

Tek bir ata dizisiyle Kume_Bul ve Birlesim yordamlarını yazmak.

Kötü durumu bulmak

Hangi işlem dizisinin ağacı dejenere ettiğini ve O(n²) maliyeti göstermek.

İyileştirmek

Ağırlıklı kural ve azaltma (collapsing) kuralını uygulamak, lemmaları yorumlamak.

Analiz etmek

Tarjan–van Leeuwen sonucunu ve ters Ackermann fonksiyonunu açıklamak.

Kullanmak

Bağlantılı bileşen sayma ve Kruskal algoritmasında union-find'ı uygulamak.

Ayrık Kümeler

Kümelerde birleşim (union) ve bulma (find)

Evren {0, 1, …, n−1}; kümeler ikili ayrık (pairwise disjoint): S₁ ∩ S₂ = S₂ ∩ S₃ = S₁ ∩ S₃ = ∅. n = 10 için:

0123456789S₁ = {0, 6, 7, 8}S₂ = {1, 4, 9}S₃ = {2, 3, 5}

Her küme bir ağaçtır; bağlar çocuklardan atalara doğrudur (oklar yukarı).

Hocadan“Çok çok temel bir veri yapısıdır, ama birçok problemde kullanırız. Öğrenilmeli ki kullanılabilsin.”

Ayrık Kümeler

İki işlem ve birleşimin iki biçimi

1 · Ayrık küme birleşimi

Si ∪ Sj = {x : x ∈ Si ∨ x ∈ Sj}. Birleşme sonrası kümeler bağımsız olarak bulunmaz: S₁ ∪ S₂ = {0, 6, 7, 8, 1, 4, 9}.

2 · Küme_bul(i)

i öğesini içeren kümeyi bulur (kümenin adı = ağacın kökü).

0123456789

Kökler 0, 4, 2. Birleşim için köklerden birinin ata bilgisi diğer kökü gösterecek şekilde günlenir.

0123456789

S₁ ∪ S₂: 4'ün atası 0 yapılır; S₂ ağacı 0'ın alt ağacı olur.

0123456789

S₂ ∪ S₁: bu kez 0'ın atası 4 yapılır. İkisi de doğrudur; hangisinin seçileceği maliyeti etkiler.

Ayrık Kümeler

Kümenin adı: kök düğüm

Küme adıbağ (kök)
S₁→ 0
S₂→ 4
S₃→ 2

Adlar metin olarak gerekiyorsa ayrı bir tabloda kökü gösteren bir bağ tutulur.

Birleşim ve Küme_bul algoritmalarını incelerken her küme adını ağacın kökü ile ilişkilendireceğiz: “S₁ kümesi” yerine “0 nolu küme” diyeceğiz.

Böylece Küme_bul(8) == Küme_bul(6) sorusu “8 ile 6 aynı kümede mi?” demektir.

Hocadan“Önce problemi bir matematikçi gibi, programlama bilmeyen birisi gibi ele alıyoruz; öncelikle problemi çizge teorisindeki terminoloji ile doğru ifade edebilmeliyiz.”

Dizi ile Gerçekleme

Nasıl gerçekleyelim?

YöntemBirleşimKüme bulSorun
Bağlı listeO(1)O(n)arama yavaş
Sıralı tutmak——sıralamak O(n lg n)
ata dizisi (ağaç)O(1)ağaç derinliğiderinliği küçük tutmalı
Hocadan“En güvenilir, en sağlam, en hızlı veri yapısı olan diziyi kullanarak gerçeklemenin bir yolu var mı?”
Düğümler 0'dan n−1'e numaralı olduğundan numarayı indis olarak kullanırız: her düğüm yalnızca bir alana (atasına) gereksinim duyar.

Dizi ile Gerçekleme

ata dizisi: her indis atasına bir bağ

01234567890-1142-1324-15260708094ataata[i] = i'nin atası; kökte −1
  • 1 ve 9'un atası 4; 3 ve 5'in atası 2; 6, 7 ve 8'in atası 0.
  • 0, 2 ve 4 kümelerin kökleridir: ata[kök] = −1 (negatif = kök).
#define N 10
int ata[N] = { -1, 4, -1, 2, -1, 2, 0, 0, 0, 4 };

Dizi ile Gerçekleme

İlk sürüm: Kume_Bul_1 ve Birlesim_1

int Kume_Bul_1(int i) {
  for (; ata[i] >= 0; i = ata[i])
    ;                /* negatife dek ilerle */
  return i;
}
void Birlesim_1(int i, int j) {
  ata[i] = j;        /* i kümesini j'ye bağla */
}
Küme_bul(i)

Ata bilgisi negatif bir değer olana (köke) dek ilerler; kökü döndürür.

Birleşim(i, j)

i ve j kümeleri (kökleri) aktarıldıktan sonra biri diğerinin alt ağacı olacak şekilde günlenir: O(1).

Ön koşul

i ve j kök olmalıdır; değilse önce Kume_Bul_1 ile kökler bulunur.

Dizi ile gerçekleme · kodu adım adım çalıştır

Kod izleme: Kume_Bul_1(9), Birlesim_1(4, 0)

0123456789i0-1142-1324-15260708094ataKume_Bul_1(9)

Kume_Bul_1(9): 9 hangi kümede?

0123456789i0-1142-1324-15260708094ataKume_Bul_1(9)

ata[9] = 4 ≥ 0 → i = ata[i] = 4.

0123456789i0-1142-1324-15260708094ataKume_Bul_1(9)

ata[4] = −1 < 0: kök. return 4 → 9, 4 nolu kümede.

01234567890-1142-1324-15260708094ataBirlesim_1(4, 0)

Birlesim_1(4, 0): iki kök veriliyor.

01234567890-1142-132405260708094ataBirlesim_1(4, 0)

ata[4] = 0: 4'ün ağacı 0'ın altına girdi. Tek atama: O(1).

0123456789i0-1142-132405260708094ataKume_Bul_1(9)

Yeniden Kume_Bul_1(9): yol bir düğüm uzadı.

0123456789i0-1142-132405260708094ataKume_Bul_1(9)

ata[9] = 4 ≥ 0 → i = 4.

0123456789i0-1142-132405260708094ataKume_Bul_1(9)

ata[4] = 0 ≥ 0 → i = 0.

0123456789i0-1142-132405260708094ataKume_Bul_1(9)

return 0: artık 9, 0 nolu kümede. Maliyet = düğümün derinliği.

int Kume_Bul_1(int i) {
  for (; ata[i] >= 0; i = ata[i])
    ;                /* negatife dek ilerle */
  return i;
}
void Birlesim_1(int i, int j) {
  ata[i] = j;        /* i kümesini j'ye bağla */
}

Analiz · kodu adım adım çalıştır

Kod izleme: Birlesim_1(0,1) … Birlesim_1(3,4), Kume_Bul_1(0)

012340-11-12-13-14-1atabaşlangıç

n = 5 tek öğeli küme: Si = {i}, tümünde ata[i] = −1.

01234011-12-13-14-1ataBirlesim_1(0, 1)

Birlesim_1(0, 1): ata[0] = 1. Her çağrı O(1).

0123401122-13-14-1ataBirlesim_1(1, 2)

Birlesim_1(1, 2): ata[1] = 2. Her çağrı O(1).

012340112233-14-1ataBirlesim_1(2, 3)

Birlesim_1(2, 3): ata[2] = 3. Her çağrı O(1).

01234011223344-1ataBirlesim_1(3, 4)

Birlesim_1(3, 4): ata[3] = 4. Her çağrı O(1).

01234i011223344-1ataKume_Bul_1(0)

Ağaç bir zincire dönüştü; derinlik n. Şimdi Kume_Bul_1(0).

01234i011223344-1ataKume_Bul_1(0)

i = ata[0] = 1 (1. ilerleme).

01234i011223344-1ataKume_Bul_1(0)

i = ata[1] = 2 (2. ilerleme).

01234i011223344-1ataKume_Bul_1(0)

i = ata[2] = 3 (3. ilerleme).

01234i011223344-1ataKume_Bul_1(0)

i = ata[3] = 4 (4. ilerleme).

01234011223344-1ataKume_Bul_1(0)

return 4 — 4 = n−1 ilerleme. Kume_Bul_1(1), (2)… sırayla n−2, n−3, … ilerler: toplam Σ = O(n²).

int Kume_Bul_1(int i) {
  for (; ata[i] >= 0; i = ata[i])
    ;                /* negatife dek ilerle */
  return i;
}
void Birlesim_1(int i, int j) {
  ata[i] = j;        /* i kümesini j'ye bağla */
}

Analiz

Kume_Bul_1 ve Birlesim_1'in analizi

/* n tek öğeli kümeyle başla */
Birlesim_1(0, 1);
Birlesim_1(1, 2);
...
Birlesim_1(n-2, n-1);
Kume_Bul_1(0);
Kume_Bul_1(1);
...
Kume_Bul_1(n-1);
  • n−1 birleşim: her biri sabit → toplam O(n), sorun yok.
  • Ağaç zincire döner; derinlik n olur.
  • (n−i). düzeydeki i için Kume_Bul_1 O(i) sürer: 0 için n−1, 1 için n−2… ilerleme.
  • Tek bir Kume_Bul_1 O(n)'e, n tanesi toplamda Σ i = O(n²)'ye çıkar.
Bu işlem dizisi ağaç yapısını bozar. Suçlu birleşimin kendisi değil, hangi ağacın hangisine bağlandığıdır. Çözüm: ağırlıklı kural.

Analiz

Bir parantez: en kötü durumu test etmek

En kötü durum girdisi

Worst case analizinde kod, çakılacağı ya da işletiminin en uzun süreceği verilerle test edilmelidir. Bunu saptamak zordur.

Burada

Zincir üreten birleşim sırası, Kume_Bul_1 için tam da böyle bir girdidir: rastgele testler onu kolayca kaçırır.

Hocadan“Beta sürümü ne demektir? ‘Biz kafasızız, gözden kaçırdığımız hatalar olabilir. Kafasızlığımızı affedin, hata bulursanız bizi bilgilendirin ki, onun üstüne gidelim.’ Software Testing ayrı bir alandır; onun üzerine de makaleler okunmalı.”

Ağırlıklı Kural

Ağırlıklı kural (weighting rule)

Tanım: i ağacındaki düğüm sayısı j ağacındakinden azsa j, i'nin atası; aksi hâlde i, j'nin atası yapılır. (Düğüm sayısı çok olan ata olur.)

0123456Küçük, büyüğe eklenirse: derinlik 30123456Büyük, küçüğe eklenirse: derinlik 4
Hocadan“Sual: Acaba her kümede (ağaçta) kaç düğüm olduğu bilgisi için ayrı bir dizi mi tutmamız gerekir? El-Cevap: Kökler dışındaki tüm düğümler için ata dizisinde pozitif değerler tutuluyordu. Öyleyse köklere ilişkin düğüm sayısı bilgisi de ata dizisinde negatif olarak tutulabilir.”

Ağırlıklı Kural

Birlesim_2: kökte eksi düğüm sayısı

void Birlesim_2(int i, int j) {
  /* ağırlıklı kural; i ve j kök olmalı */
  int p = ata[i] + ata[j];  /* -(toplam düğüm) */
  if (ata[i] > ata[j]) {    /* i'de daha az düğüm */
    ata[i] = j;             /* i'yi j'ye bağla */
    ata[j] = p;             /* j'nin sayısını günle */
  } else {                  /* j'de az ya da eşit */
    ata[j] = i;
    ata[i] = p;
  }
}
Kökte −sayı

Kökteki değer −(düğüm sayısı): tek öğeli küme −1, 8 öğeli küme −8.

Neden >?

Değerler negatif: ata[i] > ata[j], i'nin daha az düğümü olduğu anlamına gelir (−2 > −4).

Maliyet

Hâlâ O(1); ek dizi gerekmez.

Ağırlıklı kural · kodu adım adım çalıştır

Kod izleme: Birlesim_2 (0,1) (2,3) (4,5) (6,7)

012345670-11-12-13-14-15-16-17-1ataBirlesim_2(0, 1)

Birlesim_2(0, 1): 0 kümesinde 1, 1 kümesinde 1 düğüm.

012345670-11-12-13-14-15-16-17-1ataBirlesim_2(0, 1)

p = -1 + (-1) = -2: birleşik kümede 2 düğüm.

012345670-11-12-13-14-15-16-17-1ataBirlesim_2(0, 1)

-1 > -1 yanlış (j'de az ya da eşit) → else: j, i'ye bağlanacak.

012345670-2102-13-14-15-16-17-1ataBirlesim_2(0, 1)

ata[1] = 0, ata[0] = -2. En derin düğüm 2. düzeyde.

012345670-2102-13-14-15-16-17-1ataBirlesim_2(2, 3)

Birlesim_2(2, 3): 2 kümesinde 1, 3 kümesinde 1 düğüm.

012345670-2102-13-14-15-16-17-1ataBirlesim_2(2, 3)

p = -1 + (-1) = -2: birleşik kümede 2 düğüm.

012345670-2102-13-14-15-16-17-1ataBirlesim_2(2, 3)

-1 > -1 yanlış (j'de az ya da eşit) → else: j, i'ye bağlanacak.

012345670-2102-2324-15-16-17-1ataBirlesim_2(2, 3)

ata[3] = 2, ata[2] = -2. En derin düğüm 2. düzeyde.

012345670-2102-2324-15-16-17-1ataBirlesim_2(4, 5)

Birlesim_2(4, 5): 4 kümesinde 1, 5 kümesinde 1 düğüm.

012345670-2102-2324-15-16-17-1ataBirlesim_2(4, 5)

p = -1 + (-1) = -2: birleşik kümede 2 düğüm.

012345670-2102-2324-15-16-17-1ataBirlesim_2(4, 5)

-1 > -1 yanlış (j'de az ya da eşit) → else: j, i'ye bağlanacak.

012345670-2102-2324-2546-17-1ataBirlesim_2(4, 5)

ata[5] = 4, ata[4] = -2. En derin düğüm 2. düzeyde.

012345670-2102-2324-2546-17-1ataBirlesim_2(6, 7)

Birlesim_2(6, 7): 6 kümesinde 1, 7 kümesinde 1 düğüm.

012345670-2102-2324-2546-17-1ataBirlesim_2(6, 7)

p = -1 + (-1) = -2: birleşik kümede 2 düğüm.

012345670-2102-2324-2546-17-1ataBirlesim_2(6, 7)

-1 > -1 yanlış (j'de az ya da eşit) → else: j, i'ye bağlanacak.

012345670-2102-2324-2546-276ataBirlesim_2(6, 7)

ata[7] = 6, ata[6] = -2. En derin düğüm 2. düzeyde.

void Birlesim_2(int i, int j) {
  /* ağırlıklı kural; i ve j kök olmalı */
  int p = ata[i] + ata[j];  /* -(toplam düğüm) */
  if (ata[i] > ata[j]) {    /* i'de daha az düğüm */
    ata[i] = j;             /* i'yi j'ye bağla */
    ata[j] = p;             /* j'nin sayısını günle */
  } else {                  /* j'de az ya da eşit */
    ata[j] = i;
    ata[i] = p;
  }
}

Ağırlıklı kural · kodu adım adım çalıştır

Kod izleme: Birlesim_2 (0,2) (4,6) (0,4)

012345670-2102-2324-2546-276ataBirlesim_2(0, 2)

Birlesim_2(0, 2): 0 kümesinde 2, 2 kümesinde 2 düğüm.

012345670-2102-2324-2546-276ataBirlesim_2(0, 2)

p = -2 + (-2) = -4: birleşik kümede 4 düğüm.

012345670-2102-2324-2546-276ataBirlesim_2(0, 2)

-2 > -2 yanlış (j'de az ya da eşit) → else: j, i'ye bağlanacak.

012345670-41020324-2546-276ataBirlesim_2(0, 2)

ata[2] = 0, ata[0] = -4. En derin düğüm 3. düzeyde.

012345670-41020324-2546-276ataBirlesim_2(4, 6)

Birlesim_2(4, 6): 4 kümesinde 2, 6 kümesinde 2 düğüm.

012345670-41020324-2546-276ataBirlesim_2(4, 6)

p = -2 + (-2) = -4: birleşik kümede 4 düğüm.

012345670-41020324-2546-276ataBirlesim_2(4, 6)

-2 > -2 yanlış (j'de az ya da eşit) → else: j, i'ye bağlanacak.

012345670-41020324-4546476ataBirlesim_2(4, 6)

ata[6] = 4, ata[4] = -4. En derin düğüm 3. düzeyde.

012345670-41020324-4546476ataBirlesim_2(0, 4)

Birlesim_2(0, 4): 0 kümesinde 4, 4 kümesinde 4 düğüm.

012345670-41020324-4546476ataBirlesim_2(0, 4)

p = -4 + (-4) = -8: birleşik kümede 8 düğüm.

012345670-41020324-4546476ataBirlesim_2(0, 4)

-4 > -4 yanlış (j'de az ya da eşit) → else: j, i'ye bağlanacak.

012345670-810203240546476ataBirlesim_2(0, 4)

ata[4] = 0, ata[0] = -8. En derin düğüm 4. düzeyde.

void Birlesim_2(int i, int j) {
  /* ağırlıklı kural; i ve j kök olmalı */
  int p = ata[i] + ata[j];  /* -(toplam düğüm) */
  if (ata[i] > ata[j]) {    /* i'de daha az düğüm */
    ata[i] = j;             /* i'yi j'ye bağla */
    ata[j] = p;             /* j'nin sayısını günle */
  } else {                  /* j'de az ya da eşit */
    ata[j] = i;
    ata[i] = p;
  }
}

Ağırlıklı Kural

Soru: Ağacı ne kadar dejenere eder, bozar?

Hocadan“Algoritma doğru, etkili ve yazılım mühendisliği kurallarına göre yazılacak. Ama bitmiyor, en çok bozacak veriyi sorguluyoruz. Arka planda ne olursa, kodum en fazla çalışır? Bilgisayar bilimi, bilgisayar mühendisliği işte budur!”
Hocadan“‘Hepiniz C, Java vs. biliyor musunuz?’ Evet, biliyorlar. ‘Tamam, hepiniz işe girin, aynı maaş!’ Bilgisayar mühendisi der ki: ‘Bir dakika! Ben bilgisayar mühendisiyim. Nasıl aynı maaş olur? Sen bir soru sor bakalım! Bir soru sor, nasıl çözdüğüme bak!’”
Ağırlıklı kuralda en kötü durum: her seferinde eşit büyüklükte iki ağacı birleştirmek (Birlesim_2 izlemelerindeki gibi). Her birleşimde küçük ağacın tüm düğümlerinin derinliği 1 artar, ama ağaç en az iki katına çıkar.

Ağırlıklı Kural

Lemma: ağırlıklı birleşimle derinlik logaritmiktir

Lemma (Horowitz)T, Birlesim_2 ile çeşitli kümelerin birleşimi sonucu elde edilmiş m düğümlü bir ağaç ise T'nin derinliği (düzey sayısı) ⌊lg m⌋ + 1'den büyük olamaz.
  • İspat fikri (tümevarım): bir düğümün derinliği yalnızca onun ağacı daha küçük ya da eşit bir ağaca bağlanınca 1 artar.
  • O zaman düğümün yer aldığı ağaç en az iki katına çıkar.
  • m düğüme en çok lg m kez iki katına çıkılabilir → derinlik ≤ ⌊lg m⌋ + 1.
01234567m = 8 → düzey sayısı 4 = ⌊lg 8⌋ + 1
Hocadan“Matematik bölümü olsaydık ‘İspatlayınız.’ diye soru devam edecekti. […] Karmaşıklığı n'den ⌊lg n⌋+1'e düşürdük. Algoritma geliştirme bu, işte! Ama yerimizde durmuyoruz, devam…”

Azaltma Kuralı

Azaltma kuralı (collapsing rule)

Son durumdaki yapıda Küme_bul(7) köke 3 ilerlemede ulaşır. Program Küme_bul(7)'yi tekrar tekrar işletirse maliyet katlanır. Düğüm sayısı az olanı çok olana eklesek bile derinlik artabiliyor; derinliği denetleyemediğimize göre bir adım ileri gideriz:

TanımEğer j, i düğümünden köke olan yol üzerinde bir düğüm ise (i kök değil) j düğümü doğrudan köke bağlanır.
012345677 → 6 → 4 → 0 yolundaki her düğüm köke bağlanacak
Hocadan“Bunun graf teorisindeki karşılığı ‘kaplama’ ya da ‘kapama’. Graf teorisi ödevlerinden birinde, hangisi olduğu söylenmeyecek, bu kullanılmalı. Problem farklı, alt yapısı bu.”

Azaltma Kuralı

Kume_bul_2: önce kök bulunur, sonra yol köke bağlanır

int Kume_bul_2(int i) {
  int r, s;
  for (r = i; ata[r] >= 0; r = ata[r])
    ;                   /* kökü bul */
  while (i != r) {      /* yolu azalt */
    s = ata[i];         /* i'nin atasını sakla */
    ata[i] = r;         /* i'yi doğrudan köke bağla */
    i = s;              /* atasına geç */
  }
  return r;
}
1. geçiş (for)

r kökü bulur (ata bilgisi negatif).

2. geçiş (while)

Yol üzerindeki her düğüm doğrudan r'ye bağlanır. Köke zaten bağlı bir düğüm için de (gereksiz ama zararsız) bir kez çalışır.

Kazanç

Aynı yoldaki sonraki aramalar bir çırpıda köke ulaşır.

Azaltma kuralı · kodu adım adım çalıştır

Kod izleme: Kume_bul_2(7)

01234567i0-810203240546476ataKume_bul_2(7)

Kume_bul_2(7).

01234567ir0-810203240546476ataKume_bul_2(7)

ata[7] = 6 ≥ 0 → r = 6.

01234567ir0-810203240546476ataKume_bul_2(7)

ata[6] = 4 ≥ 0 → r = 4.

01234567ir0-810203240546476ataKume_bul_2(7)

ata[4] = 0 ≥ 0 → r = 0.

01234567ir0-810203240546476ataKume_bul_2(7)

ata[0] = -8 < 0: kök r = 0.

01234567ir0-810203240546476ataKume_bul_2(7)

i = 7 ≠ r: s = ata[7] = 6 saklanır.

01234567ir0-810203240546470ataKume_bul_2(7)

ata[7] = 0: 7 doğrudan köke bağlandı; i = s = 6.

01234567ir0-810203240546470ataKume_bul_2(7)

i = 6 ≠ r: s = ata[6] = 4 saklanır.

01234567ir0-810203240546070ataKume_bul_2(7)

ata[6] = 0: 6 doğrudan köke bağlandı; i = s = 4.

01234567ir0-810203240546070ataKume_bul_2(7)

i = 4 ≠ r: s = ata[4] = 0 saklanır.

01234567r0-810203240546070ataKume_bul_2(7)

ata[4] = 0 (zaten köke bağlıydı: fazladan bir tur); i = 0.

01234567i r0-810203240546070ataKume_bul_2(7)

i == r: döngü biter, return 0. ata = [-8, 0, 0, 2, 0, 4, 0, 0].

int Kume_bul_2(int i) {
  int r, s;
  for (r = i; ata[r] >= 0; r = ata[r])
    ;                   /* kökü bul */
  while (i != r) {      /* yolu azalt */
    s = ata[i];         /* i'nin atasını sakla */
    ata[i] = r;         /* i'yi doğrudan köke bağla */
    i = s;              /* atasına geç */
  }
  return r;
}

Azaltma kuralı · kodu adım adım çalıştır

Kod izleme: yine Kume_bul_2(7), sonra Kume_bul_2(5)

01234567i0-810203240546070ataKume_bul_2(7)

Kume_bul_2(7).

01234567ir0-810203240546070ataKume_bul_2(7)

ata[7] = 0 ≥ 0 → r = 0.

01234567ir0-810203240546070ataKume_bul_2(7)

ata[0] = -8 < 0: kök r = 0.

01234567ir0-810203240546070ataKume_bul_2(7)

i = 7 ≠ r: s = ata[7] = 0 saklanır.

01234567r0-810203240546070ataKume_bul_2(7)

ata[7] = 0 (zaten köke bağlıydı: fazladan bir tur); i = 0.

01234567i r0-810203240546070ataKume_bul_2(7)

i == r: döngü biter, return 0. ata = [-8, 0, 0, 2, 0, 4, 0, 0].

01234567i0-810203240546070ataKume_bul_2(5)

Kume_bul_2(5).

01234567ir0-810203240546070ataKume_bul_2(5)

ata[5] = 4 ≥ 0 → r = 4.

01234567ir0-810203240546070ataKume_bul_2(5)

ata[4] = 0 ≥ 0 → r = 0.

01234567ir0-810203240546070ataKume_bul_2(5)

ata[0] = -8 < 0: kök r = 0.

01234567ir0-810203240546070ataKume_bul_2(5)

i = 5 ≠ r: s = ata[5] = 4 saklanır.

01234567ir0-810203240506070ataKume_bul_2(5)

ata[5] = 0: 5 doğrudan köke bağlandı; i = s = 4.

01234567ir0-810203240506070ataKume_bul_2(5)

i = 4 ≠ r: s = ata[4] = 0 saklanır.

01234567r0-810203240506070ataKume_bul_2(5)

ata[4] = 0 (zaten köke bağlıydı: fazladan bir tur); i = 0.

01234567i r0-810203240506070ataKume_bul_2(5)

i == r: döngü biter, return 0. ata = [-8, 0, 0, 2, 0, 0, 0, 0].

int Kume_bul_2(int i) {
  int r, s;
  for (r = i; ata[r] >= 0; r = ata[r])
    ;                   /* kökü bul */
  while (i != r) {      /* yolu azalt */
    s = ata[i];         /* i'nin atasını sakla */
    ata[i] = r;         /* i'yi doğrudan köke bağla */
    i = s;              /* atasına geç */
  }
  return r;
}

Azaltma Kuralı

“For vardı, bir de while ekledik!”

Hocadan“‘For vardı, bir de while döngüsü ekledik. Bu nasıl iyileştirme, anlamadım!’ Ben de anlamadım, bakalım. Küme_bul_2(7) ikinci kez çağrılınca ve sonraki çağrılarda bir çırpıda r=0 bulacaktır.”
ÇağrıKume_Bul_1 (ilerleme)Kume_bul_2 (ilerleme + bağlama)
1. Küme_bul(7)33 + 3
2. Küme_bul(7)31 + 1
10 kez Küme_bul(7)306 + 9·2 = 24
Küme_bul(5) (sonra)22 + 2, ardından 1
İlk çağrı yaklaşık iki kat pahalıdır, ama yolu kalıcı olarak kısaltır. Maliyet, birçok işlem üzerinden amortize edilince neredeyse sabite iner.

Azaltma Kuralı

Lemma (Tarjan ve van Leeuwen): neredeyse sabit

LemmaHer biri tek düğümlü kümelerle başlanıp f küme bulma ve u birleştirme (ağırlıklı kural + azaltma kuralı) herhangi bir karışık sırada uygulandığında gereken en çok zaman T(f, u) olsun. u ≥ n/2 için:

k₁·(n + f·α(f+n, n)) ≤ T(f, u) ≤ k₂·(n + f·α(f+n, n))

  • α: ters Ackermann fonksiyonu; çok yavaş artar.
  • Evrendeki atom sayısı kadar öğe için bile uygulamada α ≤ 4.
  • İşlem başına maliyet n'e bölünce neredeyse O(1).
/* Ackermann: çok hızlı büyür */
A(m, n) = n + 1                  m = 0
        = A(m-1, 1)              m > 0, n = 0
        = A(m-1, A(m, n-1))      m > 0, n > 0
Hocadan“1 milyon öğe var; biri O(1)'de buluyor, öteki hâlâ listede arıyor… Hani dedik ya, aynı işe girecek olan matematikçi ile mühendisin farkı budur işte!”

Azaltma Kuralı

Ackermann fonksiyonu ve tersi

n = 0n = 1n = 2n = 3n = 4
m = 012345
m = 123456
m = 2357911
m = 35132961125
m = 41365533265536−3……
  • A(5, 1) = 22…2 − 3 (65536 tane 2'nin üs kulesi): akıl almaz büyüklük.
  • Ters: α(m, n) = min{ i ≥ 1 : A(i, ⌊m/n⌋) ≥ lg n } — A çok hızlı büyüdüğü için α çok yavaş büyür.

Ackermann fonksiyonunun birden çok tanımı vardır. Yukarıdaki tanımla A(3,4) = 125'tir; Horowitz'in kullandığı tanımda (A(1,j) = 2j …) ise A(3,1) = 16'dır. Hangi tanım seçilirse seçilsin sonuç değişmez: uygulamada α ≤ 4.

Azaltma Kuralı

Sınav sorusu: Ackermann fonksiyonunu özyinelemeli yazınız

Sınav sorusuAckermann fonksiyonunu özyinelemeli olarak yazınız. Ardından A(1, 2) çağrısının çağrı yığınını adım adım gösteriniz.
Çözüm
unsigned long ackermann(unsigned long m,
                        unsigned long n) {
  if (m == 0) return n + 1;
  if (n == 0) return ackermann(m - 1, 1);
  return ackermann(m - 1, ackermann(m, n - 1));
}

A(1,2) = A(0, A(1,1)) = A(0, A(0, A(1,0))) = A(0, A(0, A(0,1))) = A(0, A(0, 2)) = A(0, 3) = 4. A(4, 2) bile hiçbir bilgisayarda bitmez; yığın taşar.

Özyineleme · kodu adım adım çalıştır

Kod izleme: ackermann(1, 2)

A(1,2)etkin çağrılar (soldan sağa derinleşir)

ackermann(1, 2) çağrılır.

A(1,2)etkin çağrılar (soldan sağa derinleşir)

A(1,2): önce içteki A(1,1) hesaplanmalı.

A(1,2)A(1,1)etkin çağrılar (soldan sağa derinleşir)

A(1,1): önce içteki A(1,0) hesaplanmalı.

A(1,2)A(1,1)A(1,0)etkin çağrılar (soldan sağa derinleşir)

A(1,0): n = 0 → A(0, 1) çağrılır.

A(1,2)A(1,1)A(1,0)A(0,1)etkin çağrılar (soldan sağa derinleşir)A(0,1) = 2

A(0,1): m = 0 → return 2.

A(1,2)A(1,1)A(1,0)etkin çağrılar (soldan sağa derinleşir)A(1,0) = 2

A(1,0) = A(0,1) = 2, dönülür.

A(1,2)A(1,1)etkin çağrılar (soldan sağa derinleşir)

İç çağrı 2 verdi → dış çağrı A(0,2).

A(1,2)A(1,1)A(0,2)etkin çağrılar (soldan sağa derinleşir)A(0,2) = 3

A(0,2): m = 0 → return 3.

A(1,2)A(1,1)etkin çağrılar (soldan sağa derinleşir)A(1,1) = 3

A(1,1) = 3.

A(1,2)etkin çağrılar (soldan sağa derinleşir)

İç çağrı 3 verdi → dış çağrı A(0,3).

A(1,2)A(0,3)etkin çağrılar (soldan sağa derinleşir)A(0,3) = 4

A(0,3): m = 0 → return 4.

A(1,2)etkin çağrılar (soldan sağa derinleşir)A(1,2) = 4

A(1,2) = 4.

unsigned long ackermann(unsigned long m,
                        unsigned long n) {
  if (m == 0) return n + 1;
  if (n == 0) return ackermann(m - 1, 1);
  return ackermann(m - 1, ackermann(m, n - 1));
}

Azaltma Kuralı

Bir parantez: bu lemmalar nereden geliyor?

Hocadan“Tarjan ünlü bir graf teoricidir, birçok teorisi var. Bu lemma, Tarjan ile Jan van Leeuwen’in ortak çalışmasından çıktı (1984).”
Hocadan“Lisansta 4 yıl okunup bir meslek kazanılıyor, doktorada ise yalnızca bir problem üzerine 4 yıl çalışılıyor. Dünyada çözülememiş bir problem çözülüyor […] O yüzden grup içinde çalışılırsa sürekli öğrenilir.”
Hocadan“Bu konular bir haftada bitmez; hiç çalışılmazsa hiç bitmez. Her gün düzenli olarak 2–3 saat çalışmak gerekir. Haftanın konusu hemen o hafta tekrar edilmeli.”

Uygulama · kodu adım adım çalıştır

Kod izleme: bağlantılı bileşen sayma

014235çizge: 6 düğüm, 4 kenar0123450-11-12-13-14-15-1ata

Kenarlar u–v: (0,1), (2,3), (1,4), (3,2). Kaç bağlantılı bileşen var?

014235çizge: 6 düğüm, 4 kenar0123450-11-12-13-14-15-1atasayi = 6

Her düğüm kendi kümesi: ata = −1, sayi = 6.

014235çizge: 6 düğüm, 4 kenar0123450-11-12-13-14-15-1atasayi = 6

Kenar (0, 1): Kume_bul_2(0) = 0, Kume_bul_2(1) = 1.

014235çizge: 6 düğüm, 4 kenar0123450-2102-13-14-15-1atasayi = 5

0 ≠ 1: farklı bileşenler → Birlesim_2(0, 1), sayi = 5.

014235çizge: 6 düğüm, 4 kenar0123450-2102-13-14-15-1atasayi = 5

Kenar (2, 3): Kume_bul_2(2) = 2, Kume_bul_2(3) = 3.

014235çizge: 6 düğüm, 4 kenar0123450-2102-2324-15-1atasayi = 4

2 ≠ 3: farklı bileşenler → Birlesim_2(2, 3), sayi = 4.

014235çizge: 6 düğüm, 4 kenar0123450-2102-2324-15-1atasayi = 4

Kenar (1, 4): Kume_bul_2(1) = 0, Kume_bul_2(4) = 4.

014235çizge: 6 düğüm, 4 kenar0123450-3102-232405-1atasayi = 3

0 ≠ 4: farklı bileşenler → Birlesim_2(0, 4), sayi = 3.

014235çizge: 6 düğüm, 4 kenar0123450-3102-232405-1atasayi = 3

Kenar (3, 2): Kume_bul_2(3) = 2, Kume_bul_2(2) = 2.

014235çizge: 6 düğüm, 4 kenar0123450-3102-232405-1atasayi = 3

a == b = 2: 3 ile 2 zaten aynı bileşende; kenar bir döngü kapatır, sayı değişmez.

014235çizge: 6 düğüm, 4 kenar0123450-3102-232405-1atasayi = 3

return 3: bileşenler {0,1,4}, {2,3}, {5}. Toplam O(n + m·α).

int bilesen_say(int n, int m, int u[], int v[]) {
  for (int i = 0; i < n; i++) ata[i] = -1;
  int sayi = n;               /* her düğüm ayrı */
  for (int e = 0; e < m; e++) {
    int a = Kume_bul_2(u[e]), b = Kume_bul_2(v[e]);
    if (a != b) {             /* farklı bileşen */
      Birlesim_2(a, b);
      sayi--;
    }
  }
  return sayi;
}

Uygulamalar

Union-find başka nerelerde?

Eşdeğerlik sınıfları

i ≡ j bildirimlerinden sınıfları çıkarmak (Horowitz'in eşdeğerlik problemi).

Ağ bağlantısı

“Bu iki bilgisayar bağlı mı?” sorularını çevrimiçi yanıtlamak (Sedgewick'in açılış örneği).

Görüntü işleme

Aynı renkteki komşu pikselleri bölgelere ayırmak (bağlantılı bileşen etiketleme).

Süzülme (percolation)

Izgarada üstten alta açık yol var mı? Monte Carlo deneylerinin çekirdeği.

Derleyiciler

Tip çıkarımında birleştirme (unification), takma ad (alias) analizi.

Çevrimdışı LCA

Tarjan'ın en yakın ortak ata algoritması.

Kruskal algoritması: kenarlar ağırlığa göre sıralanır; bir kenarın uçları için Küme_bul farklı kökler verirse kenar ağaca alınır ve iki küme birleştirilir, aynı kökü verirse kenar bir döngü kapatır ve reddedilir. Maliyet: sıralama O(m lg m) + union-find ≈ O(m·α).

Özet

Sınav sorusu: ata dizisinin içeriği

Sınav sorusuBirlesim_2 ve Kume_bul_2 verilmiştir; kümeler S₁ = {0, 6, 7, 8}, S₂ = {1, 4, 9}, S₃ = {2, 3, 5} (şekildeki ağaçlar).
(i) İlk başta ata dizisinin içeriği nedir?
(ii) Birlesim_2(4, 2); Kume_bul_2(5); Birlesim_2(0, 4); Kume_bul_2(7); Kume_bul_2(3); çağrılarından sonra ata ne olur? Not: (i) hatalıysa (ii) değerlendirilmez.
0123456789
Çözüm
  1. ata = [-4, 4, -3, 2, -3, 2, 0, 0, 0, 4] — köklerde eksi düğüm sayısı (−4, −3, −3).
  2. Birlesim_2(4,2): eşit (−3, −3) → 2, 4'e bağlanır, ata[4] = −6. Kume_bul_2(5): 5 → 2 → 4, 5 doğrudan 4'e bağlanır. Birlesim_2(0,4): −4 > −6 → 0, 4'e bağlanır, ata[4] = −10. Kume_bul_2(7): 7 → 0 → 4, 7 köke bağlanır. Kume_bul_2(3): 3 → 2 → 4, 3 köke bağlanır. Sonuç: [4, 4, 4, 4, -10, 4, 0, 4, 0, 4].

Özet

Karmaşıklık özeti

SürümBirleşimKüme bul (en kötü)n−1 birleşim + m bulma
Birlesim_1 + Kume_Bul_1O(1)O(n)O(n + m·n)
Birlesim_2 (ağırlıklı)O(1)O(lg n)O(n + m lg n)
Birlesim_2 + Kume_bul_2O(1)O(lg n) tek işlemO(n + m·α(m+n, n))

Bellek: yalnızca n tamsayılık ata dizisi. α ≤ 4 olduğundan son satır uygulamada doğrusaldır.

Birleşim tek başına hep O(1)'dir; maliyeti belirleyen, ağacın derinliği yani Küme_bul'dur. Ağırlıklı kural derinliği sınırlar, azaltma kuralı yolları kısaltır.

Özet

Sık yapılan hatalar

1Birleşime kök yerine rastgele düğüm vermek: ata[i] = j i'nin alt ağacını koparır.
2Aynı kümedeki iki kökü birleştirmek (i == j): kök kendini gösterir, sonsuz döngü.
3Ağırlıklı kuralda negatif değerlerin karşılaştırmasını ters yorumlamak (−2 > −4 “daha az düğüm”).
4Birleşimde kökteki düğüm sayısını (p) günlemeyi unutmak.
5Kume_bul_2'de atayı saklamadan (s) bağlamak: yol kaybolur.
6Başlangıçta ata dizisini −1 ile doldurmamak.

Özet

Alıştırmalar

  1. n = 6, ata tümüyle −1. Birlesim_2(0,1); Birlesim_2(2,3); Birlesim_2(0,2); Birlesim_2(4,5) sonrası ata?
  2. Ağırlıklı kural kullanılırken 16 düğümlü bir ağacın derinliği en çok kaçtır?
  3. Ağırlık yerine yükseklik (rank) tutan bir birleşim kuralı yazın. Ne değişir?
  4. Kume_bul_2'yi özyinelemeli yazın (tek satırlık yol sıkıştırma).
  5. Kruskal'da bir kenarın reddedilmesi neden döngü demektir?
  6. n düğüm, m kenarlı bir çizgede bileşen sayısını bulmanın maliyeti nedir?
Cevapları göster
  1. [-4, 0, 0, 2, -2, 4] (0 kökünde 4 düğüm; 4 kökünde 2).
  2. ⌊lg 16⌋ + 1 = 5 düzey.
  3. Kökte −(yükseklik) tutulur; eşitse biri bağlanır ve yükseklik 1 artar. Derinlik yine O(lg n); azaltma ile rank üst sınır olur.
  4. int bul(int i) { return ata[i] < 0 ? i : (ata[i] = bul(ata[i])); }
  5. Uçlar zaten aynı kümede, yani aralarında seçilmiş kenarlardan oluşan bir yol var; kenar yolu döngüye kapatır.
  6. O(n + m·α(m+n, n)) — pratikte doğrusal.

Özet

Bölüm özeti

Model

Ayrık kümeler = kökü küme adı olan, çocuktan ataya bağlı ağaçlar.

Gösterim

Tek ata dizisi; kökte negatif değer.

Kötü durum

Rastgele bağlama zincir üretir: n bulma O(n²).

Ağırlıklı kural

Küçük ağaç büyüğe: derinlik ≤ ⌊lg m⌋ + 1.

Azaltma kuralı

Yol köke bağlanır; sonraki aramalar bir çırpıda.

Sonuç

İkisi birlikte: işlem başına α ≤ 4 — neredeyse O(1).

Özet

Kaynaklar ve devam

  • E. Horowitz, S. Sahni, S. Anderson-Freed, Fundamentals of Data Structures in C, Bölüm 5.10 (Set Representation).
  • T. H. Cormen vd., Introduction to Algorithms (CLRS), “Data Structures for Disjoint Sets” bölümü.
  • R. Sedgewick, Algorithms in C, Bölüm 1 (union-find ile bağlantılılık).
  • R. E. Tarjan, J. van Leeuwen, “Worst-case analysis of set union algorithms”, J. ACM 31(2), 1984.

Son

Bölüm 16: Kümeler ve Union-Find
Sonraki bölüm: Bölüm 17: Minimum Kapsayan Ağaçlar · Ders sayfası
1 / 1 C ile Veri Yapıları