Bölüm 12: Açgözlü Algoritmalar

Algoritmalar
Açgözlü yaklaşımPara üstüEtkinlik seçimiİş sıralama BorůvkaUnion-FindKruskalHuffman kodlama
Sercan KÜLCÜ · Giresun Üniversitesi · Bilgisayar Mühendisliği

Giriş

Öğrenme Çıktıları

Yaklaşım

Açgözlü (greedy) stratejiyi tanımlamak; açgözlü seçim özelliği ve optimal alt yapının ne zaman doğru sonuç garantilediğini açıklamak.

Klasik problemler

Para üstü, etkinlik seçimi, iş sıralama ve kesirli sırt çantası problemlerini açgözlü olarak çözmek; başarısız olduğu durumlara karşı örnek vermek.

Kapsayan ağaçlar

Borůvka ve Kruskal algoritmalarını Union-Find ile uygulamak, adım adım izlemek ve O(E log V) karmaşıklığını gerekçelendirmek.

Veri sıkıştırma

Huffman ağacını kurmak, önek-özgür kodları türetmek, bir metni kodlayıp çözmek ve kazancı bit olarak hesaplamak.

Açgözlü Yaklaşım

Adım Adım Kazananlar

  • Açgözlü algoritmalar (greedy algorithms), her adımda o an en iyi görünen seçeneği seçerek ilerler.
  • Tıpkı bir define arayan gibi: her aşamada en fazla kazancı sağlayacak seçeneği tercih eder.
  • Basamaklı bir yaklaşımdır: problemi bütün olarak ele almaz, mevcut en iyi seçeneğe odaklanır.
  • Verilen bir karar geri alınmaz; geri dönüş (backtracking) yoktur.
  • Yolculuk planlarken her kavşakta en kısa görünen yolu seçmek gibi.
Kazanç ve risk

Hızlı ve basit bir yöntem sunar; kod yapısı genellikle kısa ve anlaşılırdır.

Ancak en iyi (optimal) çözümü bulmayı garanti etmez; çoğu durumda iyi bir yaklaşık çözüm üretir.

Açgözlü Yaklaşım

Açgözlü Paradigma: Genel Şablon

Cozum acgozlu(Aday[] adaylar) {
  Cozum s = new Cozum();   // boş çözüm
  sirala(adaylar);         // ölçüte göre
  for (Aday a : adaylar) {
    if (s.uygunMu(a))      // kısıt bozulmaz
      s.ekle(a);           // geri alınmaz
  }
  return s;
}
  1. Uygun çözümler kümesini belirle (hangi seçimler kısıtları bozmaz?).
  2. Bir açgözlü ölçüt seç: en ucuz, en erken biten, en kârlı, en sık…
  3. Ölçüte göre yerel olarak en iyi adayı seç; uygunsa çözüme ekle.
  4. Çözüm tamamlanana kadar tekrarla.
Çoğu açgözlü algoritmanın maliyeti sıralama ya da öncelik kuyruğu işlemlerinden gelir: tipik olarak O(n log n).

Açgözlü Yaklaşım

Ne Zaman Doğru Sonuç Verir?

Açgözlü seçim özelliği

(Greedy-choice property) Yerel olarak en iyi seçimi içeren en az bir en iyi çözüm vardır. İlk açgözlü seçim hiçbir zaman "yanlış yola" sokmaz.

Optimal alt yapı

(Optimal substructure) Seçim yapıldıktan sonra kalan alt problemin en iyi çözümü, ilk seçimle birlikte tüm problemin en iyi çözümünü verir.

Kanıt tekniği: değiştirme argümanı

(Exchange argument) Herhangi bir en iyi çözüm O alınır. O, açgözlü seçimi içermiyorsa, O'daki bir öğe açgözlü seçimle değiştirilir; çözüm uygun kalır ve daha kötü olmaz. Böylece açgözlü seçimi içeren bir en iyi çözüm bulunur; tümevarımla tüm adımlar için geçerlidir.

İki özellik de sağlanmıyorsa açgözlü algoritma yalnızca bir sezgisel (heuristic) yöntemdir: hızlıdır ama sonucu en iyiden kötü olabilir.

Açgözlü Yaklaşım

Avantajlar ve Dezavantajlar

Avantajlar

  • Anlaması ve uygulaması kolay; kod kısa.
  • Hızlı: çoğunlukla sıralama + tek geçiş, O(n log n).
  • Az bellek: yalnızca mevcut çözüm tutulur.
  • Doğru problemde (MST, Huffman, etkinlik seçimi) en iyi sonucu verir.
  • Zor problemlerde iyi bir yaklaşık çözüm ya da başlangıç noktası sağlar.

Dezavantajlar

  • En iyi çözümü garanti etmez (para üstü {1, 3, 4}, 0/1 sırt çantası).
  • Doğruluğu her problem için ayrıca kanıtlanmalıdır.
  • Ölçüt seçimi kritik: yanlış ölçüt yanlış sonuç verir.
  • Verilen karar geri alınmaz; hatayı düzeltme şansı yoktur.

Açgözlü Yaklaşım

Para Üstü Problemi (Coin Change)

Belirli bir tutarı en az sayıda madeni parayla ödemek.

Açgözlü ölçüt: kalan tutardan büyük olmayan en büyük parayı seç; tutar sıfır olana kadar tekrarla.

// para[] büyükten küçüğe sıralı
static int paraUstu(int[] para, int tutar) {
  int adet = 0;
  for (int p : para) {
    while (tutar >= p) {
      tutar -= p;
      adet++;
    }
  }
  return adet;
}
Her zaman doğru mu?

{50, 25, 10, 5, 1} gibi kanonik para sistemlerinde evet: 68 = 50 + 10 + 5 + 1 + 1 + 1.

{4, 3, 1} sisteminde 6 için açgözlü 4 + 1 + 1 (3 para) verir; en iyisi 3 + 3 (2 para). Genel durumda dinamik programlama gerekir.

Zaman: O(k + adet), k = para türü sayısı.

Açgözlü Yaklaşım · Adım adım

Para Üstü: Adım Adım

madeni paralar50251051seçilenlerkalan tutar68para sayısı: 0

Örnek 1: paralar {50, 25, 10, 5, 1}, tutar 68. En büyük paradan başla.

madeni paralar50251051seçilenler50kalan tutar18para sayısı: 1

Kalan tutara sığan en büyük para 50 → kalan 68 − 50 = 18.

madeni paralar50251051seçilenler5010kalan tutar8para sayısı: 2

Kalan tutara sığan en büyük para 10 → kalan 18 − 10 = 8.

madeni paralar50251051seçilenler50105kalan tutar3para sayısı: 3

Kalan tutara sığan en büyük para 5 → kalan 8 − 5 = 3.

madeni paralar50251051seçilenler501051kalan tutar2para sayısı: 4

Kalan tutara sığan en büyük para 1 → kalan 3 − 1 = 2.

madeni paralar50251051seçilenler5010511kalan tutar1para sayısı: 5

Kalan tutara sığan en büyük para 1 → kalan 2 − 1 = 1.

madeni paralar50251051seçilenler50105111kalan tutar0para sayısı: 6

Kalan tutara sığan en büyük para 1 → kalan 1 − 1 = 0. Toplam 6 para — bu para sistemi için en iyi sonuç.

madeni paralar431seçilenlerkalan tutar6para sayısı: 0

Örnek 2: paralar {4, 3, 1}, tutar 6. En büyük paradan başla.

madeni paralar431seçilenler4kalan tutar2para sayısı: 1

Kalan tutara sığan en büyük para 4 → kalan 6 − 4 = 2.

madeni paralar431seçilenler41kalan tutar1para sayısı: 2

Kalan tutara sığan en büyük para 1 → kalan 2 − 1 = 1.

madeni paralar431seçilenler411kalan tutar0para sayısı: 3

Kalan tutara sığan en büyük para 1 → kalan 1 − 1 = 0.

madeni paralar431seçilenler33kalan tutar0para sayısı: 2en iyi: 2 para

Açgözlü 4 + 1 + 1 = 3 para verdi; oysa 3 + 3 = 2 para yeterli. Yerel en iyi seçim (4) küresel en iyiyi kaçırdı.

Açgözlü Yaklaşım

Deneyin: Açgözlü ve En İyi Para Üstü

En iyi sonuç dinamik programlama ile bulunur: enAz[t] = 1 + min enAz[t − p]. Paralar 1–999 arası tam sayı, en fazla 8 tür; tutar en fazla 999. Enter = hesapla.

Açgözlü Yaklaşım

Açgözlü, Dinamik Programlama, Böl ve Fethet

AçgözlüDinamik programlamaBöl ve fethet
Kararher adımda tek, yerel en iyi seçim; geri alınmaztüm seçenekler denenir, alt sonuçlar tablodaproblem bağımsız parçalara bölünür
Alt problemlerseçimden sonra tek alt problem kalırörtüşen alt problemlerörtüşmeyen alt problemler
Doğrulukaçgözlü seçim özelliği gereklioptimal alt yapı yeterlibirleştirme adımına bağlı
Maliyetgenelde O(n log n)genelde polinom, daha yavaş, ek bellekyineleme bağıntısı ile
ÖrnekKruskal, Huffman, etkinlik seçimi0/1 sırt çantası, genel para üstübirleştirmeli sıralama
Açgözlü yaklaşım, DP'nin "her adımda yalnızca bir seçeneğe bakan" özel durumu gibi düşünülebilir: seçimin doğru olduğu kanıtlanabiliyorsa tablo gerekmez.

Etkinlik Seçimi

Etkinlik Seçimi (Activity Selection)

  • Her etkinliğin bir başlangıç si ve bitiş fi zamanı var; aynı anda tek etkinlik yapılabilir (tek salon).
  • Amaç: birbiriyle çakışmayan en fazla sayıda etkinliği seçmek. [si, fi) ile [sj, fj) çakışmaz ⇔ sj ≥ fi ya da si ≥ fj.
  • Açgözlü ölçüt: en erken biten etkinliği seç, onunla çakışanları at, kalanlarla tekrarla.
Etkinlik12345678910
başlangıç12465803710
bitiş35687924912
Zaman: sıralama O(n log n) + tek geçiş O(n). Etkinlikler zaten bitişe göre sıralıysa O(n).

Etkinlik Seçimi · Kodu adım adım çalıştır

sec(e): Kodu Adım Adım İzle

024681012#7#1#8#2#3#5#4#6#9#10

Etkinlikler bitiş zamanına göre sıralandı (yukarıdan aşağı). Kesikli çizgi: son seçilenin bitişi.

024681012#7#1#8#2#3#5#4#6#9#10

En erken biten etkinlik (#7, [0, 2)) her zaman seçilir.

024681012#7#1#8#2#3#5#4#6#9#10

#1 [1, 3): başlangıç 1 < 2 → çakışıyor, atlandı.

024681012#7#1#8#2#3#5#4#6#9#10

#8 [3, 4): başlangıç 3 ≥ 2 → seçildi, i = 2.

024681012#7#1#8#2#3#5#4#6#9#10

#2 [2, 5): başlangıç 2 < 4 → çakışıyor, atlandı.

024681012#7#1#8#2#3#5#4#6#9#10

#3 [4, 6): başlangıç 4 ≥ 4 → seçildi, i = 4.

024681012#7#1#8#2#3#5#4#6#9#10

#5 [5, 7): başlangıç 5 < 6 → çakışıyor, atlandı.

024681012#7#1#8#2#3#5#4#6#9#10

#4 [6, 8): başlangıç 6 ≥ 6 → seçildi, i = 6.

024681012#7#1#8#2#3#5#4#6#9#10

#6 [8, 9): başlangıç 8 ≥ 8 → seçildi, i = 7.

024681012#7#1#8#2#3#5#4#6#9#10

#9 [7, 9): başlangıç 7 < 9 → çakışıyor, atlandı.

024681012#7#1#8#2#3#5#4#6#9#10

#10 [10, 12): başlangıç 10 ≥ 9 → seçildi, i = 9. Sonuç: 6 etkinlik (7, 8, 3, 4, 6, 10).

static void sec(Etkinlik[] e) {
  Arrays.sort(e, (x, y) -> x.bitis - y.bitis);
  int i = 0;                 // son seçilen
  System.out.print(e[i].id + " ");
  for (int j = 1; j < e.length; j++) {
    if (e[j].bas >= e[i].bitis) {
      System.out.print(e[j].id + " ");
      i = j;
    }
  }
}

Etkinlik Seçimi

Neden En Erken Biten?

Akla gelen diğer ölçütler en iyi sonucu garanti etmez (turuncu: açgözlünün seçtiği, yeşil: en iyi çözüm):

En erken başlayanaçgözlü 1 · en iyi 3En kısa sürenaçgözlü 1 · en iyi 20246810

"En az çakışan" ölçütünün de karşı örneği vardır.

Değiştirme argümanı

a1 en erken biten etkinlik, O ise bir en iyi çözüm olsun. O'nun ilk etkinliği b ≠ a1 ise b yerine a1 konur: f(a1) ≤ f(b) olduğundan O'nun diğer etkinlikleriyle çakışma oluşmaz ve etkinlik sayısı aynı kalır.

Demek ki a1'i içeren bir en iyi çözüm vardır. a1 ile çakışanlar atılınca kalan problem aynı yapıdadır (optimal alt yapı).

İş Sıralama

Son Tarihli İş Sıralama (Job Sequencing)

  • Her iş 1 birim sürer, bir son tarihi (deadline) ve bir kârı vardır.
  • Kâr, iş yalnızca son tarihine kadar bitirilirse kazanılır; aynı anda tek iş yapılır.
  • Amaç: toplam kârı en büyük yapmak.
  • Açgözlü ölçüt: işleri kâra göre azalan sırayla ele al; her işi son tarihinden önceki en geç boş zaman dilimine koy; boş dilim yoksa işi atla.
  • En geç dilimi seçmek, erken dilimleri son tarihi daha yakın işlere bırakır.
İşson tarihkâr
1420
2110
3140
4130
5350
6230

Zaman: sıralama O(n log n) + slot arama O(n · dmax) → en kötü O(n²). Union-Find ile slot arama neredeyse O(n)'e iner.

İş Sıralama · Kodu adım adım çalıştır

isSirala(is, 6): Kodu Adım Adım İzle

kâra göre sıralı işlerİş 5t ≤ 3kâr 50İş 3t ≤ 1kâr 40İş 4t ≤ 1kâr 30İş 6t ≤ 2kâr 30İş 1t ≤ 4kâr 20İş 2t ≤ 1kâr 10slot (zaman dilimi)1—2—3—4—5—6—

İşler kâra göre azalan sıralandı (eşit kârlı İş 4 ve İş 6 özgün sırasını korur). Tüm slotlar boş (−1).

kâra göre sıralı işlerİş 5t ≤ 3kâr 50İş 3t ≤ 1kâr 40İş 4t ≤ 1kâr 30İş 6t ≤ 2kâr 30İş 1t ≤ 4kâr 20İş 2t ≤ 1kâr 10slot (zaman dilimi)1—2—3İş 54—5—6—

İş 5 (son tarih 3, kâr 50): en geç uygun slot 3 boş → yerleştirildi. toplam = 50.

kâra göre sıralı işlerİş 5t ≤ 3kâr 50İş 3t ≤ 1kâr 40İş 4t ≤ 1kâr 30İş 6t ≤ 2kâr 30İş 1t ≤ 4kâr 20İş 2t ≤ 1kâr 10slot (zaman dilimi)1İş 32—3İş 54—5—6—

İş 3 (son tarih 1, kâr 40): en geç uygun slot 1 boş → yerleştirildi. toplam = 90.

kâra göre sıralı işlerİş 5t ≤ 3kâr 50İş 3t ≤ 1kâr 40İş 4t ≤ 1kâr 30İş 6t ≤ 2kâr 30İş 1t ≤ 4kâr 20İş 2t ≤ 1kâr 10slot (zaman dilimi)1İş 32—3İş 54—5—6—

İş 4 (son tarih 1, kâr 30): slot 1 dolu (İş 3) ve daha erken slot yok → iş atlanır.

kâra göre sıralı işlerİş 5t ≤ 3kâr 50İş 3t ≤ 1kâr 40İş 4t ≤ 1kâr 30İş 6t ≤ 2kâr 30İş 1t ≤ 4kâr 20İş 2t ≤ 1kâr 10slot (zaman dilimi)1İş 32İş 63İş 54—5—6—

İş 6 (son tarih 2, kâr 30): en geç uygun slot 2 boş → yerleştirildi. toplam = 120.

kâra göre sıralı işlerİş 5t ≤ 3kâr 50İş 3t ≤ 1kâr 40İş 4t ≤ 1kâr 30İş 6t ≤ 2kâr 30İş 1t ≤ 4kâr 20İş 2t ≤ 1kâr 10slot (zaman dilimi)1İş 32İş 63İş 54İş 15—6—

İş 1 (son tarih 4, kâr 20): en geç uygun slot 4 boş → yerleştirildi. toplam = 140.

kâra göre sıralı işlerİş 5t ≤ 3kâr 50İş 3t ≤ 1kâr 40İş 4t ≤ 1kâr 30İş 6t ≤ 2kâr 30İş 1t ≤ 4kâr 20İş 2t ≤ 1kâr 10slot (zaman dilimi)1İş 32İş 63İş 54İş 15—6—

İş 2 (son tarih 1, kâr 10): slot 1 dolu (İş 3) ve daha erken slot yok → iş atlanır.

kâra göre sıralı işlerİş 5t ≤ 3kâr 50İş 3t ≤ 1kâr 40İş 4t ≤ 1kâr 30İş 6t ≤ 2kâr 30İş 1t ≤ 4kâr 20İş 2t ≤ 1kâr 10slot (zaman dilimi)1İş 32İş 63İş 54İş 15—6—

Sonuç: slotlar 1–4 = İş 3, İş 6, İş 5, İş 1 → toplam kâr 140. İş 4 ve İş 2 yapılamaz.

static int isSirala(Is[] is, int n) {
  Arrays.sort(is, (a, b) -> b.kar - a.kar);
  int[] slot = new int[n];
  Arrays.fill(slot, -1);
  int toplam = 0;
  for (int i = 0; i < n; i++) {
    int j = Math.min(n, is[i].sonTarih) - 1;
    for (; j >= 0; j--) {
      if (slot[j] == -1) {
        slot[j] = is[i].id;
        toplam += is[i].kar;
        break;
      }
    }
  }
  return toplam;
}

Sırt Çantası

Kesirli Sırt Çantası (Fractional Knapsack)

Kapasite W = 50. Nesneler bölünebiliyorsa açgözlü ölçüt: kâr / ağırlık oranı en büyük olanı al.

Nesnekârağırlıkoran
160106
2100205
3120304

Kesirli: 1 + 2 tümüyle, 3'ün 20/30'u → 60 + 100 + 80 = 240 (en iyi). Zaman O(n log n).

0/1 sırt çantasında başarısız

Nesneler bölünemiyorsa aynı ölçüt 1 ve 2'yi alır: 160 (30 kg; 3 sığmaz).

En iyi çözüm 2 + 3 = 220. 0/1 sırt çantası açgözlü seçim özelliğini sağlamaz; dinamik programlama ile O(n · W) zamanda çözülür.

En Küçük Kapsayan Ağaç

En Küçük Kapsayan Ağaç ve Kesme Özelliği

  • En küçük kapsayan ağaç (minimum spanning tree, MST): bağlı, ağırlıklı, yönsüz bir çizgenin tüm düğümlerini en az toplam ağırlıkla bağlayan ağaç; V − 1 kenar.
  • Kesme özelliği (cut property): düğümler S ve V − S diye ikiye ayrılırsa, bu kesmeyi geçen en hafif kenar bir MST'de yer alır.
  • Borůvka, Kruskal ve Prim bu özelliği farklı biçimlerde kullanan açgözlü algoritmalardır.
  • Ağırlıklar farklıysa MST tektir.
43562794811pqrstuSV − S

Kesmeyi geçen kenarlar: 9, 4, 8, 11. En hafifi r–s (4) MST'dedir.

Borůvka

Borůvka'nın Algoritması

  • Çizge teorisinde kullanılan bir en küçük kapsayan ağaç algoritmasıdır (Otakar Borůvka, 1926).
  • Başlangıçta her düğüm bir alt ağaç (bileşen) oluşturur.
  • Her turda, her alt ağacın dışarı çıkan en az maliyetli kenarı bulunur ve bu kenarlar eklenerek alt ağaçlar birleştirilir.
  • Açgözlü: her bileşen için yerel olarak en iyi kenarı seçer (kesme özelliği: bileşen ile geri kalan arasındaki en hafif kenar).
  • Her turda en az bir kenar eklenir ve ağaç büyür.
  1. Adım 1: Her düğüm bir alt ağaç olarak başlar.
  2. Adım 2: Her alt ağaçtan, dışarı çıkan en az maliyetli kenar seçilir.
  3. Adım 3: Seçilen kenarlarla alt ağaçlar birleştirilir.
  4. Adım 4: Alt ağaçlar tek bir ağaçta toplanana kadar tekrarlanır.
Çalışma zamanı: O(E log V) — E kenar, V düğüm sayısı.

Borůvka · Adım adım

Borůvka Adım Adım

1367114893220181551910171116412ABCDEFGHIJKL

Başlangıç: her düğüm tek başına bir alt ağaç → 12 bileşen, 0 kenar.

1367114893220181551910171116412ABCDEFGHIJKL

Tur 1 · düğüm A: dışarı çıkan en ucuz kenar A–C (6).

1367114893220181551910171116412ABCDEFGHIJKL

Tur 1 · düğüm B: dışarı çıkan en ucuz kenar B–D (1).

1367114893220181551910171116412ABCDEFGHIJKL

Tur 1 · düğüm C: dışarı çıkan en ucuz kenar A–C (6) — bu kenar zaten seçilmişti.

1367114893220181551910171116412ABCDEFGHIJKL

Tur 1 · düğüm D: dışarı çıkan en ucuz kenar B–D (1) — bu kenar zaten seçilmişti.

1367114893220181551910171116412ABCDEFGHIJKL

Tur 1 · düğüm E: dışarı çıkan en ucuz kenar E–F (2).

1367114893220181551910171116412ABCDEFGHIJKL

Tur 1 · düğüm F: dışarı çıkan en ucuz kenar E–F (2) — bu kenar zaten seçilmişti.

1367114893220181551910171116412ABCDEFGHIJKL

Tur 1 · düğüm G: dışarı çıkan en ucuz kenar G–I (5).

1367114893220181551910171116412ABCDEFGHIJKL

Tur 1 · düğüm H: dışarı çıkan en ucuz kenar G–H (15).

1367114893220181551910171116412ABCDEFGHIJKL

Tur 1 · düğüm I: dışarı çıkan en ucuz kenar G–I (5) — bu kenar zaten seçilmişti.

1367114893220181551910171116412ABCDEFGHIJKL

Tur 1 · düğüm J: dışarı çıkan en ucuz kenar J–L (4).

1367114893220181551910171116412ABCDEFGHIJKL

Tur 1 · düğüm K: dışarı çıkan en ucuz kenar G–K (10).

1367114893220181551910171116412ABCDEFGHIJKL

Tur 1 · düğüm L: dışarı çıkan en ucuz kenar J–L (4) — bu kenar zaten seçilmişti.

1367114893220181551910171116412ABCDEFGHIJKL

Tur 1 sonu: seçilen 7 farklı kenar eklendi → 5 bileşen: {A,C}, {B,D}, {E,F}, {G,H,I,K}, {J,L}. Ağırlık: 43.

1367114893220181551910171116412ABCDEFGHIJKL

Tur 2 · bileşen {A, C}: dışarı çıkan en ucuz kenar B–C (7).

1367114893220181551910171116412ABCDEFGHIJKL

Tur 2 · bileşen {B, D}: dışarı çıkan en ucuz kenar D–F (3).

1367114893220181551910171116412ABCDEFGHIJKL

Tur 2 · bileşen {E, F}: dışarı çıkan en ucuz kenar D–F (3) — bu kenar zaten seçilmişti.

1367114893220181551910171116412ABCDEFGHIJKL

Tur 2 · bileşen {G, H, I, K}: dışarı çıkan en ucuz kenar K–L (12).

1367114893220181551910171116412ABCDEFGHIJKL

Tur 2 · bileşen {J, L}: dışarı çıkan en ucuz kenar K–L (12) — bu kenar zaten seçilmişti.

1367114893220181551910171116412ABCDEFGHIJKL

Tur 2 sonu: seçilen 3 farklı kenar eklendi → 2 bileşen: {A,B,C,D,E,F}, {G,H,I,J,K,L}. Ağırlık: 65.

1367114893220181551910171116412ABCDEFGHIJKL

Tur 3 · bileşen {A, B, C, D, E, F}: dışarı çıkan en ucuz kenar E–J (18).

1367114893220181551910171116412ABCDEFGHIJKL

Tur 3 · bileşen {G, H, I, J, K, L}: dışarı çıkan en ucuz kenar E–J (18) — bu kenar zaten seçilmişti.

1367114893220181551910171116412ABCDEFGHIJKL

Tur 3 sonu: tek bileşen kaldı. MST: 11 kenar (V − 1), toplam ağırlık 83. Yalnızca 3 tur yetti.

Borůvka

Borůvka: Java Kodu

static int boruvka(int n, int[][] kenar) {
  AyrikKume kume = new AyrikKume(n);
  int bilesen = n, toplam = 0;
  while (bilesen > 1) {
    int[] enUcuz = new int[n];     // kök -> kenar no
    Arrays.fill(enUcuz, -1);
    for (int i = 0; i < kenar.length; i++) {
      int a = kume.bul(kenar[i][0]), b = kume.bul(kenar[i][1]);
      if (a == b) continue;        // bileşen içi kenar
      int w = kenar[i][2];
      if (enUcuz[a] < 0 || w < kenar[enUcuz[a]][2]) enUcuz[a] = i;
      if (enUcuz[b] < 0 || w < kenar[enUcuz[b]][2]) enUcuz[b] = i;
    }
    for (int c = 0; c < n; c++) {   // seçilenleri ekle
      if (enUcuz[c] < 0) continue;
      int[] k = kenar[enUcuz[c]];
      if (kume.bul(k[0]) != kume.bul(k[1])) {   // iki kez eklenmesin
        kume.birlestir(k[0], k[1]);
        toplam += k[2]; bilesen--;
      }
    }
  }
  return toplam;
}

Borůvka

Borůvka: Analiz

Tur sayısı ≤ log₂ V

Her bileşen en az bir başka bileşenle birleşir; bu yüzden her turda bileşen sayısı en az yarıya iner. Örnekte 12 → 5 → 2 → 1: turlarda 7, 3 ve 1 kenar eklendi.

Bir tur = O(E)

Her kenar bir kez taranır (Union-Find işlemleri neredeyse sabit). Toplam: O(E log V).

Eşit ağırlıklar

İki bileşen eşit ağırlıklı farklı kenarlar seçerse döngü oluşabilir. Eşitlik tutarlı bozulmalı: örn. (ağırlık, kenar no) çifti karşılaştırılır.

Paralelleştirilebilir

Bir turdaki bileşenler birbirinden bağımsız en ucuz kenarını arar; bu yüzden paralel ve dağıtık MST algoritmalarının temelidir.

Union-Find

Union-Find (Ayrık Kümeler) Veri Yapısı

class AyrikKume {
  int[] ebeveyn, rutbe;
  AyrikKume(int n) {
    ebeveyn = new int[n];
    rutbe = new int[n];
    for (int i = 0; i < n; i++) ebeveyn[i] = i;
  }
  int bul(int x) {          // yol sıkıştırma
    if (ebeveyn[x] != x)
      ebeveyn[x] = bul(ebeveyn[x]);
    return ebeveyn[x];
  }
  void birlestir(int x, int y) {
    int kx = bul(x), ky = bul(y);
    if (kx == ky) return;
    if (rutbe[kx] < rutbe[ky]) {
      int t = kx; kx = ky; ky = t;
    }
    ebeveyn[ky] = kx;       // rütbeye göre
    if (rutbe[kx] == rutbe[ky]) rutbe[kx]++;
  }
}
  • Amaç: küme birleştirme ve küme bulma işlemlerini hızlı yapmak.
  • Union(x, y) → birlestir: x ve y'nin kümelerini birleştirir (bir kök diğerinin çocuğu olur).
  • Find(x) → bul: x'in bulunduğu kümenin kök elemanını bulur.
  • Her küme bir ağaçtır; ebeveyn[kök] = kök.
  • Yol sıkıştırma + rütbeye (rank) göre birleştirme ile işlem başına amortize O(α(n)); α pratikte ≤ 4.

Kruskal

Kruskal Algoritması

  • Amaç: ağırlıklı, bağlı bir çizgenin en küçük kapsayan ağacını bulmak.
  • Açgözlü yaklaşım: her adımda kalan kenarlar arasından en küçük ağırlıklı olanı seçerek ilerler.
  • Döngü denetimi Union-Find ile yapılır: uçların kökleri aynıysa kenar döngü oluşturur.
  1. Tüm kenarları ağırlıklarına göre sırala.
  2. En küçük ağırlıklı kenarı seç.
  3. Kenar mevcut ormanda döngü oluşturmuyorsa ekle (iki kümeyi birleştir), oluşturuyorsa at.
  4. V − 1 kenar eklenene (ya da kenarlar bitene) kadar tekrarla.
Doğruluk: eklenen kenar, iki farklı bileşeni ayıran kesmenin en hafif kenarıdır (kesme özelliği). Zaman: sıralama O(E log E) = O(E log V) + E · O(α(V)).

Kruskal · Kodu adım adım çalıştır

kruskal(5, kenar): Kodu Adım Adım İzle

1345672acbdeabcdeebeveyn[ ]aabbccddee

Kenarlar ağırlığa göre sıralandı: 1, 2, 3, 4, 5, 6, 7. Her düğüm kendi kümesinin kökü (ebeveyn[x] = x).

1345672acbdeabcdeebeveyn[ ]aabbccddee

Kenar a–c (1): bul(a) = a, bul(c) = c → kökler farklı, döngü oluşmaz.

1345672acbdeabcdeebeveyn[ ]aabbacddee

birlestir(a, c): ebeveyn[c] = a. Kenar ağaca eklendi; toplam = 1, sayac = 1.

1345672acbdeabcdeebeveyn[ ]aabbacddee

Kenar d–e (2): bul(d) = d, bul(e) = e → kökler farklı, döngü oluşmaz.

1345672acbdeabcdeebeveyn[ ]aabbacddde

birlestir(d, e): ebeveyn[e] = d. Kenar ağaca eklendi; toplam = 3, sayac = 2.

1345672acbdeabcdeebeveyn[ ]aabbacddde

Kenar a–b (3): bul(a) = a, bul(b) = b → kökler farklı, döngü oluşmaz.

1345672acbdeabcdeebeveyn[ ]aaabacddde

birlestir(a, b): ebeveyn[b] = a. Kenar ağaca eklendi; toplam = 6, sayac = 3.

1345672acbdeabcdeebeveyn[ ]aaabacddde

Kenar b–c (4): bul(b) = bul(c) = a → aynı küme; eklenirse döngü oluşur, atlanır.

1345672acbdeabcdeebeveyn[ ]aaabacddde

Kenar b–d (5): bul(b) = a, bul(d) = d → kökler farklı, döngü oluşmaz.

1345672acbdeabcdeebeveyn[ ]aaabacadde

birlestir(b, d): ebeveyn[d] = a. Kenar ağaca eklendi; toplam = 11, sayac = 4. sayac = n − 1 → döngüden çıkılır.

1345672acbdeabcdeebeveyn[ ]aaabacadde

Sonuç: a–c, d–e, a–b, b–d → toplam 11. c–d (6) ve c–e (7) hiç incelenmedi.

static int kruskal(int n, int[][] kenar) {
  Arrays.sort(kenar, (x, y) -> x[2] - y[2]);
  AyrikKume kume = new AyrikKume(n);
  int toplam = 0, sayac = 0;
  for (int[] k : kenar) {
    int u = k[0], v = k[1], w = k[2];
    if (kume.bul(u) != kume.bul(v)) {
      kume.birlestir(u, v);
      toplam += w;
      sayac++;
      if (sayac == n - 1) break;
    }
  }
  return toplam;
}

Kruskal

Örnek: Altı Düğümlü Çizge

Kenarlar: (A,B) 2, (A,D) 4, (B,C) 1, (B,D) 3, (C,E) 2, (D,E) 1, (D,F) 4, (E,F) 3. Sıralı işleme:

kenarwkarar
B–C1eklendi (toplam 1)
D–E1eklendi (toplam 2)
A–B2eklendi (toplam 4)
C–E2eklendi (toplam 6)
B–D3reddedildi: döngü
E–F3eklendi (toplam 9)
A–D4incelenmez (5 kenar tamam)
D–F4incelenmez (5 kenar tamam)
24132143ABCDEF

Ağaç: B–C → D–E → A–B → C–E → E–F, toplam 9 (5 kenar = V − 1).

Kruskal

Kruskal, Prim ve Borůvka

KruskalPrimBorůvka
Açgözlü seçimtüm çizgedeki en hafif güvenli kenarağaçtan çıkan en hafif kenarher bileşenden çıkan en hafif kenar
Büyümeorman → ağaçtek ağaç, düğüm düğümorman, turlarla birleşme
Veri yapısısıralama + Union-Findöncelik kuyruğuUnion-Find
ZamanO(E log V)O(E log V) (ikili yığın)O(E log V)
Uygun olduğuseyrek çizge, kenar listesiyoğun çizge, komşulukparalel / dağıtık
Üçü de aynı çizgede aynı MST'yi bulur (ağırlıklar farklıysa MST tektir). Borůvka örneğinde Kruskal da 83 verir.

Huffman Kodlama

Huffman Kodlama

  • Kayıpsız veri sıkıştırma için kullanılır (David Huffman, 1952).
  • Değişken uzunluklu kodlar kullanır: sık kullanılan karakterlere kısa, seyrek olanlara uzun kodlar atanır.
  • Önek-özgür (prefix-free) kodlar üretir: hiçbir kod, başka bir kodun öneki olamaz → ayırıcı gerekmeden tek anlamlı çözülür.
  • Açgözlü adım: en düşük frekanslı iki ağacı birleştir.
Önek-özgür neden önemli?

Kodlar a = 0, b = 01, c = 1 olsun. "01" → "ab" mi, "b" mi? Belirsiz: 0, 01'in önekidir.

Huffman kodları ikili ağacın yapraklarında durduğu için hiçbir kod diğerinin öneki olamaz.

Kullanım: ZIP/DEFLATE, gzip, PNG, JPEG ve MP3'ün entropi kodlama aşaması.

Huffman Kodlama

Huffman Kodlama: Adımlar

  1. Adım 1: Her karakterin frekansı hesaplanır.
  2. Adım 2: Frekanslara göre bir Huffman ağacı oluşturulur: her karakter bir yaprak; en küçük iki frekanslı düğüm birleştirilerek yeni bir iç düğüm yapılır.
  3. Adım 3: Ağaçta her karaktere karşılık gelen kodlar atanır: sol kenar 0, sağ kenar 1.
  4. Adım 4: Veri, oluşturulan Huffman kodlarıyla kodlanır.

Örnek frekanslar (toplam 25 karakter):

karakterFORGET
frekans234457

n karakter için n − 1 birleştirme; öncelik kuyruğu ile O(n log n).

Huffman Kodlama · Kodu adım adım çalıştır

Huffman Ağacını Kur: Kodu Adım Adım İzle

F2O3E5T7G4R4

Her karakter için frekansıyla bir yaprak oluşturulup öncelik kuyruğuna (min-yığın) eklenir.

F2O3E5T7G4R4

poll() iki kez: en küçük frekanslar F2 (sol) ve O3 (sağ).

F2O3E5T7G4R45

Yeni iç düğüm 5 = 2 + 3; çocukları F2 ve O3, kuyruğa geri eklenir.

F2O3E5T7G4R45

poll() iki kez: en küçük frekanslar G4 (sol) ve R4 (sağ). G4 ile R4 eşit; hangisinin önce çıkacağı yığına bağlıdır.

F2O3E5T7G4R458

Yeni iç düğüm 8 = 4 + 4; çocukları G4 ve R4, kuyruğa geri eklenir.

F2O3E5T7G4R458

poll() iki kez: en küçük frekanslar (5) (sol) ve E5 (sağ). (5) ile E5 eşit; hangisinin önce çıkacağı yığına bağlıdır, kod uzunlukları değişmez.

F2O3E5T7G4R45810

Yeni iç düğüm 10 = 5 + 5; çocukları (5) ve E5, kuyruğa geri eklenir.

F2O3E5T7G4R45810

poll() iki kez: en küçük frekanslar T7 (sol) ve (8) (sağ).

F2O3E5T7G4R4581015

Yeni iç düğüm 15 = 7 + 8; çocukları T7 ve (8), kuyruğa geri eklenir.

F2O3E5T7G4R4581015

poll() iki kez: en küçük frekanslar (10) (sol) ve (15) (sağ).

F2O3E5T7G4R458101525

Yeni iç düğüm 25 = 10 + 15; çocukları (10) ve (15), kuyruğa geri eklenir. Kuyrukta tek düğüm kaldı: kök.

0101010101F2O3E5T7G4R458101525

Döngü biter, kök (25) döndürülür. Sol kenar 0, sağ kenar 1: F=000, O=001, E=01, T=10, G=110, R=111.

static Dugum huffman(char[] h, int[] f) {
  PriorityQueue<Dugum> kuyruk =
    new PriorityQueue<>((a, b) -> a.f - b.f);
  for (int k = 0; k < h.length; k++)
    kuyruk.add(new Dugum(h[k], f[k]));
  while (kuyruk.size() > 1) {
    Dugum sol = kuyruk.poll();
    Dugum sag = kuyruk.poll();
    Dugum ata = new Dugum('\0', sol.f + sag.f);
    ata.sol = sol;
    ata.sag = sag;
    kuyruk.add(ata);
  }
  return kuyruk.poll();
}

Huffman Kodlama

Kodlar, Kodlama ve Çözme

kar.frekanskoduzunlukbit
F200036
O300139
E501210
T710214
G4110312
R4111312
toplam63
  • Sabit uzunluk: 6 karakter → 3 bit, 25 × 3 = 75 bit. Huffman: 63 bit (%16 kazanç, ortalama 2,52 bit).
  • Kodlama: "FORTE" → 000 001 111 10 01 = 13 bit (sabit kodla 15).
  • Çözme: kökten başla, 0 → sol, 1 → sağ; yaprağa gelince karakteri yaz ve köke dön: 1100110 → 110 | 01 | 10 → "GET".
  • Sol ve sağ çocuğun yeri keyfidir (ör. T sağ çocuk yapılabilir): kodlar değişir ama kod uzunlukları ve 63 bit değişmez.

Huffman Kodlama

Huffman Neden En İyi?

Açgözlü seçim

En düşük frekanslı iki karakter x ve y'nin, en derin seviyede kardeş olduğu bir en iyi kod ağacı vardır: daha derindeki bir karakterle yer değiştirmek toplam biti artırmaz (değiştirme argümanı).

Optimal alt yapı

x ve y, frekansı f(x) + f(y) olan tek bir "z" karakteri yapılırsa, küçülen problemin en iyi ağacından z açılarak asıl problemin en iyi ağacı elde edilir.

Maliyet

Toplam bit = Σ frekans(c) · derinlik(c). Huffman, önek-özgür kodlar arasında bunu en küçük yapar.

Karmaşıklık

Frekans sayımı O(m) (m = metin uzunluğu); ağaç O(n log n); kodlama O(m). Ağaç (ya da kod tablosu) da çözücüye gönderilmelidir.

Uygulamalar

Diğer Açgözlü Algoritmalar

Dijkstra ve PrimHer adımda uzaklığı / bağlantı maliyeti en küçük düğümü kalıcı olarak seçer (Bölüm 4). Negatif kenarsız çizgede en iyisidir.
Çizge boyamaDüğümleri sırayla, komşularının kullanmadığı en küçük renkle boya. Hızlı ama renk sayısı en az olmayabilir (NP-zor).
Kutulara yerleştirme(Bin packing) İlk sığan / en iyi sığan: nesneyi sığdığı ilk kutuya koy. Azalan sırayla ilk sığan en fazla ≈ 11/9 · OPT + 1 kutu kullanır.
En büyük kapsama(Maximum coverage) Her adımda en çok yeni eleman kapsayan kümeyi seç: en iyinin en az (1 − 1/e) ≈ %63'ünü garanti eder.
NP-zor problemlerde açgözlü algoritmalar, kanıtlanmış oranlı yaklaşım algoritmaları (Bölüm 7) olarak değerlidir.

Özet

Özet

ProblemAçgözlü ölçütZamanEn iyi mi?
Para üstüsığan en büyük paraO(k + adet)kanonikse
Etkinlik seçimien erken bitenO(n log n)evet
İş sıralamaen kârlı iş, en geç boş slotO(n²)evet
Kesirli sırt çantasıen büyük kâr/ağırlıkO(n log n)evet (0/1: hayır)
Borůvkaher bileşenin en ucuz dış kenarıO(E log V)evet
Kruskaldöngü kurmayan en hafif kenarO(E log V)evet
Huffmanen düşük frekanslı iki ağaçO(n log n)evet
Union-Findbul / birlestir, yol sıkıştırma + rütbeye göre birleştirme → amortize O(α(n))

Özet

Etkileşimli Simülatörler

En Küçük Kapsayan AğaçRastgele bağlı ağırlıklı bir çizge üretin; Prim ve Kruskal algoritmalarını adım adım, yan yana çalıştırıp seçilen kenarları ve toplam ağırlığı karşılaştırın.
Dizgi SıkıştırmaRLE (Run Length Encoding) ve LZW sıkıştırmasını adım adım izleyin, sıkıştırma oranını görün. Huffman bu sayfada yok; sözlük tabanlı LZW ile frekans tabanlı Huffman'ı karşılaştırmak için kullanın.

Özet

Sık Yapılan Hatalar

Açgözlüyü kanıtsız doğru saymak{1, 3, 4} paralarıyla 6: açgözlü 3 para, en iyi 2. Her problemde açgözlü seçim özelliği gösterilmelidir.
Yanlış ölçütEtkinlik seçiminde en erken başlayan ya da en kısa etkinlik ölçütü yanlıştır; doğru ölçüt en erken biten.
0/1 sırt çantasında oranKesirli problemde en iyi olan oran ölçütü, bölünemeyen nesnelerde başarısızdır (160 < 220).
Döngü denetimiKruskal'da ebeveyn[u] != ebeveyn[v] yetmez; kökler karşılaştırılmalı: bul(u) != bul(v).
Borůvka'da eşitlikEşit ağırlıklarda tutarsız seçim döngü kurabilir; aynı kenar iki bileşence seçilince iki kez eklenmemelidir.
Huffman'da ağaçsız çözmeKod tablosu ya da ağaç gönderilmezse veri çözülemez. Tek karakterli metinde kök yapraktır: koda en az 1 bit verilmelidir.

Özet

Örnek Problemler

  1. {1, 5, 10, 25} paralarıyla 63 açgözlü olarak kaç parayla ödenir? {1, 7, 10} ile 14 için açgözlü ve en iyi sonuç nedir?
  2. Etkinlikler (s, f): (1,4), (3,5), (0,6), (5,7), (3,9), (5,9), (6,10), (8,11), (8,12), (2,14), (12,16). En fazla kaç etkinlik seçilir?
  3. Frekanslar a:5, b:9, c:12, d:13, e:16, f:45. Huffman kod uzunluklarını ve toplam biti bulun.
  4. Borůvka örnek çizgesine Kruskal uygulayın: kenarlar hangi sırayla eklenir?
  5. V = 1000 düğümlü bir çizgede Borůvka en fazla kaç tur sürer?
Cevapları göster
  1. 63 = 25 + 25 + 10 + 1 + 1 + 1 → 6 para. 14: açgözlü 10 + 1 + 1 + 1 + 1 = 5 para; en iyi 7 + 7 = 2 para.
  2. Bitişe göre sırala, en erken biteni al: (1,4), (5,7), (8,11), (12,16) → 4 etkinlik.
  3. f = 1, c = d = e = 3, a = b = 4 bit (ör. f=0, c=100, d=101, a=1100, b=1101, e=111) → 45 + 36 + 39 + 48 + 20 + 36 = 224 bit (sabit 3 bitle 300).
  4. B–D 1, E–F 2, D–F 3, J–L 4, G–I 5, A–C 6, B–C 7, G–K 10, K–L 12, G–H 15, E–J 18 → toplam 83 (Borůvka ile aynı).
  5. Bileşen sayısı her turda en az yarıya iner: ⌈log₂ 1000⌉ = 10 tur.

Özet

Alıştırmalar

  1. İşler (id, son tarih, kâr): (a,2,100), (b,1,19), (c,2,27), (d,1,25), (e,3,15). Açgözlü iş sıralamasını uygulayın.
  2. W = 10, nesneler (kâr, ağırlık): (10,5), (40,4), (30,6), (50,3). Kesirli sırt çantasının değeri nedir?
  3. "ABRACADABRA" için Huffman kodlaması kaç bit tutar? Sabit uzunluklu kodla karşılaştırın.
  4. Kruskal'ın tüm kenarlar aynı ağırlıkta olduğunda ne ürettiğini ve O(E log V) maliyetinin hangi adımdan geldiğini açıklayın.
  5. Açgözlü para üstünün en iyi sonuç vermediği, en küçük tutarlı bir para sistemi bulun ve gerekçelendirin.
Cevaplar / ipuçları
  1. Kâr sırası a, c, d, b, e: a → slot 2, c → slot 1, d ve b sığmaz, e → slot 3 → c, a, e; kâr 142.
  2. Oranlar 2, 10, 5, 16,7: 50 (3 kg) + 40 (4 kg) + 30'un 3/6'sı (15) = 105.
  3. A5 B2 R2 C1 D1 → A 1 bit, diğerleri 3 bit (eşitlik seçimine göre 2–4 bit) → 23 bit; sabit 3 bitle 33 bit.
  4. Herhangi bir kapsayan ağaç (her ağaç en küçüktür); maliyetin baskın kısmı kenarların sıralanması, Union-Find neredeyse doğrusal.
  5. {1, 3, 4} ve 6: açgözlü 4+1+1, en iyi 3+3. Kanonik sistemlerde (ör. 1, 5, 10, 25) her büyük para küçükleri en iyi biçimde "kapsar".

Özet

Kaynaklar

  • Cormen, Leiserson, Rivest ve Stein, Introduction to Algorithms, 4. baskı, 2022 (Bölüm 15: Greedy Algorithms; Bölüm 19: Disjoint Sets; Bölüm 21: MST).
  • Kleinberg ve Tardos, Algorithm Design, 2006 (Bölüm 4: Greedy Algorithms).
  • Sedgewick ve Wayne, Algorithms, 4. baskı, 2011 (4.3 MST, 5.5 Data Compression).
  • D. A. Huffman, "A Method for the Construction of Minimum-Redundancy Codes", Proceedings of the IRE 40(9), 1952.
  • J. B. Kruskal, "On the Shortest Spanning Subtree of a Graph and the Traveling Salesman Problem", Proc. AMS 7(1), 1956.
  • O. Borůvka, "O jistém problému minimálním", Práce Moravské přírodovědecké společnosti 3, 1926.
  • R. E. Tarjan, "Efficiency of a Good But Not Linear Set Union Algorithm", Journal of the ACM 22(2), 1975.

Son

Bölüm 12: Açgözlü Algoritmalar
Sonraki bölüm: Bölüm 13: Geri Dönüşlü Algoritmalar
1 / 1 Sercan KÜLCÜ, Tüm hakları saklıdır.