Algoritmalar · Ders Notu

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

Bir kasiyer para üstünü verirken önce en büyük banknotu, sonra sığan en büyük madeni parayı uzatır. Bir salon görevlisi gün içindeki toplantıları yerleştirirken önce en erken biten toplantıyı takvime yazar. İkisi de geleceği hesaplamaz; o an en iyi görünen adımı atar ve bu kararı bir daha değiştirmez. Bu tür algoritmalara açgözlü algoritma denir. Açgözlü yaklaşım hızlıdır ve kodu kısadır, ama her problemde en iyi sonucu vermez. Not önce yaklaşımın genel kalıbını ve doğru sonuç verdiği koşulları, yani açgözlü seçim özelliğini ve optimal alt yapıyı tanıtır. Doğruluğu kanıtlamanın temel aracı olan değiştirme argümanı burada ayrıntılı işlenir. Ardından beş klasik problem ele alınır: para üstü, etkinlik seçimi, son tarihli iş sıralama, kesirli sırt çantası ve Huffman kodlama. Minimum kapsayan ağaç algoritmaları ise kesim özelliği üzerinden açgözlü bir gözle yeniden değerlendirilir. Her problem için Java kodu, adım adım izleme, maliyet hesabı ve açgözlü yaklaşımın yanıldığı durumlara karşı örnekler verilir.

Öğrenme çıktıları

Yaklaşım
Açgözlü stratejiyi tanımlamak; açgözlü seçim özelliğinin ve optimal alt yapının hangi durumda doğru sonucu garanti ettiğini açıklamak; değiştirme argümanıyla basit bir doğruluk kanıtı kurmak.
Klasik problemler
Para üstü, etkinlik seçimi, iş sıralama ve kesirli sırt çantası problemlerini açgözlü olarak çözmek; kodlarını adım adım izlemek; açgözlü yaklaşımın başarısız olduğu durumlar için karşı örnek vermek.
Kapsayan ağaçlar
Kesim özelliğinin Kruskal, Prim ve Borůvka'nın açgözlü seçimini neden doğru kıldığını açıklamak; üç algoritmayı seçim kuralı, veri yapısı ve süre bakımından karşılaştırmak.
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; Huffman kodunun en iyi olduğunu gerekçelendirmek.

7.1Açgözlü yaklaşım

Birçok algoritma problemi bir dizi karardan oluşur. Hangi para verilecek, hangi etkinlik seçilecek, hangi iş hangi saate konacak, hangi kenar ağaca eklenecek? Bu kararların sırasıyla verilmesi gerekir ve her karar sonrakileri etkiler. Bütün karar dizilerini tek tek denemek doğru sonucu verir, ama dizilerin sayısı girdiyle birlikte üstel olarak büyür. Açgözlü yaklaşım bu ağacın yalnız tek bir dalını izler: her adımda o an en iyi görünen seçeneği alır ve yoluna devam eder.

7.1.1Adım adım karar vermek

Tanım 7.1

Açgözlü algoritma (greedy algorithm), çözümü adım adım kuran ve her adımda bir ölçüte göre o an en iyi görünen seçeneği seçen algoritmadır. Verilen bir karar sonradan geri alınmaz; algoritma geri izleme (backtracking) yapmaz.

Açgözlü bir algoritma bir define avcısına benzer. Avcı her yol ayrımında en çok altın görünen yöne sapar. Haritanın bütününe bakmaz; yalnız önündeki seçeneklere odaklanır. Yolculuk planlarken her kavşakta hedefe en kısa görünen yolu seçmek de aynı davranıştır. Bazen bu kısa görünen yol bir çıkmaza ya da uzun bir dolambaca götürür. Algoritma geri dönmediği için bu yanlış adımı düzeltemez.

Yaklaşımın iki yüzü bu örnekte açıkça görülür. Bir yanda hız ve sadelik vardır. Her adımda yalnız bir seçenek seçilir, seçilmeyenler bir daha incelenmez. Çoğu açgözlü algoritma adayları bir kez sıralar ve sonra tek geçişte karar verir. Öbür yanda ise garanti eksikliği vardır. O an en iyi görünen seçim, bütünün en iyi çözümüne her zaman götürmez. Açgözlü algoritma birçok problemde iyi bir yaklaşık çözüm üretir, ama en iyi (optimal) çözümü bulduğu ancak kanıtla gösterilebilir.

Bu notta “en iyi çözüm”, “optimal çözüm” ve “optimum” aynı anlamda kullanılır. Bir problemin birden çok en iyi çözümü olabilir. Örneğin aynı sayıda etkinliği içeren iki farklı etkinlik kümesi, ikisi de en büyük sayıya ulaşıyorsa ikisi de en iyidir. Açgözlü algoritmanın bunlardan herhangi birini bulması yeterlidir.

7.1.2Genel şablon

Açgözlü algoritmaların çoğu aynı iskeleti izler. Önce hangi seçimlerin kısıtları bozmadığı, yani uygun (olurlu) çözümlerin ne olduğu belirlenir. Ardından bir açgözlü ölçüt seçilir: en ucuz, en erken biten, en kârlı, en sık geçen gibi. Adaylar bu ölçüte göre sıralanır. Sonra her aday sırayla incelenir. Aday mevcut çözüme eklendiğinde kısıtlar bozulmuyorsa eklenir, bozuluyorsa atlanır. Çözüm tamamlanınca algoritma durur. Sunumdaki şablon kod bu iskeleti Java biçiminde gösterir:

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;
}

Koddaki Cozum ve Aday soyut adlardır; her problemde başka bir anlam taşırlar. Para üstünde aday bir madeni para, çözüm verilen paraların listesidir. Etkinlik seçiminde aday bir etkinlik, çözüm seçilmiş etkinliklerin kümesidir. sirala çağrısı adayları açgözlü ölçüte göre dizer. uygunMu yeni adayın kısıtları bozup bozmadığını denetler. Döngü her adayı bir kez ele alır ve ekle ile çözüme katılan aday bir daha çıkarılmaz. Koddaki yorumlar bu iki özelliği vurgular: denetim kısıtları korur, ekleme ise geri alınmaz.

sıralı adaylara1✓a2✓a3✗a4✓a5✗a6✓tek geçiş: her aday bir kez incelenir, karar geri alınmaz →çözüm = {a1, a2, a4, a6}
Şekil 7.1: Açgözlü şablonun tek geçişi. Adaylar ölçüte göre sıralanmıştır; her aday bir kez incelenir. Uygun olanlar (a1, a2, a4, a6) çözüme eklenir, kısıtı bozanlar (a3, a5) atlanır. Hiçbir karar geri alınmaz.

Şekil 7.1 bu tek geçişi gösterir. Altı aday sıralanmış olarak gelir. a1 ve a2 kısıtları bozmaz ve eklenir. a3, o ana kadar seçilenlerle çatışır ve atlanır. a4 eklenir, a5 atlanır, a6 eklenir. Sonuç {a1, a2, a4, a6} kümesidir. a3'ün atlanması kalıcıdır: a4 ve a6 eklendikten sonra a3'e geri dönülüp “şimdi uygun mu?” diye sorulmaz.

Şablonun maliyeti iki kısımdan oluşur. n adayın sıralanması O(n log n) sürer. Ardından gelen tarama her aday için bir uygunMu denetimi yapar. Denetim sabit sürede bitiyorsa tarama O(n) sürer ve toplam maliyeti sıralama belirler: O(n log n). Bazı problemlerde adaylar baştan sıralanmaz; her adımda en iyi aday bir öncelikli kuyruktan alınır. Huffman kodlamada ve Prim algoritmasında durum budur. Kuyruk işlemlerinin her biri O(log n) sürdüğü için toplam maliyet yine çoğunlukla O(n log n) düzeyindedir. Denetim pahalıysa, örneğin iş sıralamadaki gibi bir boş zaman dilimi aranıyorsa, maliyetin baskın kısmı denetime geçer.

Sık yapılan hata

Şablonu doğru uygulamanın algoritmayı doğru yaptığını sanmak. Şablon yalnız bir iskelettir; sonucu ölçüt belirler. Aynı iskelet, etkinlik seçiminde “en erken biten” ölçütüyle en iyi sonucu verir, “en erken başlayan” ölçütüyle ise çok kötü bir sonuç verebilir. Her yeni problemde seçilen ölçütün doğru olduğu ayrıca kanıtlanmalıdır.

7.1.3Açgözlü seçim özelliği ve optimal alt yapı

Açgözlü bir algoritmanın en iyi sonucu vermesi için problemin iki özelliği birlikte taşıması gerekir. Birincisi ilk adımla ilgilidir: algoritmanın yaptığı ilk seçim, en iyi çözüme giden yolu kapatmamalıdır. İkincisi geri kalan işle ilgilidir. İlk seçimden sonra kalan problem aynı türden, daha küçük bir problem olmalıdır. Onun en iyi çözümü ilk seçimle birleşince bütünün en iyi çözümü çıkmalıdır.

Tanım 7.2

Bir problemde, açgözlü ölçüte göre yerel olarak en iyi olan seçimi içeren en az bir en iyi çözüm varsa, problem açgözlü seçim özelliği (greedy-choice property) taşır. Başka bir deyişle, ilk açgözlü seçim hiçbir zaman “yanlış yola” sokmaz.

Tanım 7.3

Bir seçim yapıldıktan sonra kalan alt problemin en iyi çözümü, o seçimle birlikte bütün problemin en iyi çözümünü veriyorsa, problem optimal alt yapı (optimal substructure) taşır.

İki özelliğin birlikte ne sağladığı tümevarımla görülür. Açgözlü seçim özelliği, ilk seçimi içeren bir en iyi çözüm olduğunu söyler. Bu seçim yapıldıktan sonra kalan problem aynı türdendir. Optimal alt yapı, kalan problemin en iyi çözümü ile ilk seçimin birleşiminin bütünün en iyi çözümü olduğunu söyler. Kalan problemde de açgözlü seçim özelliği geçerli olduğu için algoritmanın ikinci seçimi de yanlış yola sokmaz. Bu böyle sürer; her adımda elimizdeki kısmi çözüm, bir en iyi çözümün parçası olarak kalır. Algoritma bittiğinde kısmi çözüm tamdır ve en iyidir.

Tanımdaki “en az bir” sözcükleri önemlidir. Açgözlü seçim özelliği, her en iyi çözümün açgözlü seçimi içerdiğini söylemez. Etkinlik seçimi bunun iyi bir örneğidir: sunumdaki on etkinlikli örnekte en büyük boyut 6'dır ve bu boyutta iki farklı küme vardır. Bunlardan yalnız biri en erken biten etkinliği içerir. Özellik için bu kadarı yeterlidir.

Optimal alt yapı yalnız açgözlü algoritmalara özgü değildir. Dinamik programlama da bu özelliğe dayanır. İki yaklaşımı ayıran, açgözlü seçim özelliğidir. Dinamik programlama bir sonraki adımda hangi seçimin en iyi olduğunu bilmez; bütün seçenekleri dener ve alt problemlerin sonuçlarını bir tabloda saklar. Açgözlü algoritma ise hangi seçimin güvenli olduğunu baştan bilir ve yalnız onu yapar. Bu yüzden açgözlü algoritma, uygulanabildiği problemlerde dinamik programlamadan çok daha hızlıdır.

İki özellik birlikte sağlanmıyorsa açgözlü algoritma yalnız bir sezgisel (heuristic) yöntemdir. Sezgisel yöntem hızlıdır ve çoğu girdide makul bir sonuç verir. Ancak sonucun en iyiden ne kadar kötü olabileceğini kendi başına söylemez. {4, 3, 1} paralarıyla para üstü bunun en basit örneğidir: 6 tutarı için açgözlü algoritma 3 para verir, oysa 2 para yeterlidir.

7.1.4Değiştirme argümanı

Açgözlü seçim özelliğini kanıtlamanın en yaygın yolu değiştirme argümanıdır (exchange argument). Kanıt keyfi bir en iyi çözümle başlar. Bu çözüme O denir. O açgözlü seçimi zaten içeriyorsa söylenecek bir şey yoktur. İçermiyorsa, O'daki uygun bir öğe çıkarılır ve yerine açgözlü seçim konur. Kanıtın asıl işi iki şeyi göstermektir:

  1. Değişiklikten sonra çözüm hâlâ uygundur; hiçbir kısıt bozulmaz.
  2. Değişiklikten sonra çözüm daha kötü olmaz; amaç değeri ya aynı kalır ya iyileşir.

Bu iki madde gösterilince açgözlü seçimi içeren bir en iyi çözüm bulunmuş olur. Optimal alt yapı ile birlikte tümevarım uygulanır ve sonuç bütün adımlara yayılır.

Argümanın gücü, en iyi çözümün neye benzediğini bilmeye gerek bırakmamasıdır. O hakkında yalnız uygun ve en iyi olduğu bilinir. Kanıt O'yu adım adım açgözlü çözüme benzetir ve her adımda kalitenin düşmediğini gösterir. Açgözlü kanıtların çoğu bu kalıba uyar. Etkinlik seçiminde ilk etkinlik değiştirilir. Kesirli sırt çantasında bir nesnenin bir parçası başka bir nesnenin parçasıyla yer değiştirir. Kesim özelliğinde bir kenar başka bir kenarla, Huffman kodlamada iki yaprak birbiriyle yer değiştirir.

Örnek 7.1

Bir öğrenci n sınav kâğıdından en fazla k tanesini okuyabilir; her kâğıdın bir puanı vardır ve toplam puan en büyük olmalıdır. Açgözlü ölçüt açıktır: en yüksek puanlı k kâğıt seçilir. Değiştirme argümanı şöyle kurulur. En yüksek puanlı kâğıt a olsun ve O onu içermeyen bir en iyi seçim olsun. O'dan herhangi bir b kâğıdı çıkarılıp yerine a konur. Kâğıt sayısı değişmediği için seçim uygundur. puan(a) ≥ puan(b) olduğu için toplam azalmaz. Demek ki a'yı içeren bir en iyi seçim vardır. a seçildikten sonra kalan problem, geri kalan n − 1 kâğıttan k − 1 tanesini seçmektir ve aynı türdendir.

Örnek kolay görünür, çünkü kısıt yalnız seçilen öğe sayısıyla ilgilidir. Kısıtlar karmaşıklaştıkça değiştirmenin uygunluğu bozmadığını göstermek zorlaşır. Etkinlik seçiminde bu adım bitiş zamanlarının karşılaştırılmasını, Huffman kodlamada ise ağaçtaki derinliklerin karşılaştırılmasını gerektirir.

Sık yapılan hata

Değiştirme argümanında yalnız “daha kötü olmaz” kısmını gösterip uygunluğu atlamak. Değiştirilen çözüm uygun değilse, yani kısıtlardan birini bozuyorsa, kanıt hiçbir şey göstermez. 0/1 sırt çantasında yüksek oranlı bir nesneyi düşük oranlı bir nesnenin yerine koymak kârı artırabilir, ama yeni nesne daha ağırsa kapasite aşılır. Oran ölçütünün bu problemde yanılmasının nedeni tam olarak budur.

7.1.5Güçlü ve zayıf yanlar

Açgözlü yaklaşımın güçlü yanları kısaca şöyle sıralanabilir:

  • Anlaşılır ve kısa kod. Algoritmanın tamamı çoğu zaman bir sıralama ve bir döngüden oluşur. Bu notta verilen Java kodlarının hiçbiri yirmi satırı geçmez.
  • Hız. Sıralama ve tek geçiş toplam O(n log n) sürer. Aynı problemi dinamik programlama ile çözmek çoğu zaman bir tablo doldurmayı ve daha yüksek bir maliyeti gerektirir.
  • Az bellek. Yalnız mevcut kısmi çözüm tutulur. Alt problemlerin sonuçlarını saklayan bir tablo gerekmez.
  • Doğru problemde en iyi sonuç. Minimum kapsayan ağaç, Huffman kodlama ve etkinlik seçimi gibi problemlerde açgözlü algoritma kanıtlanmış olarak en iyidir.
  • Zor problemlerde iyi bir başlangıç. En iyi çözümü bulmanın çok pahalı olduğu problemlerde açgözlü algoritma hızlı bir yaklaşık çözüm ya da daha iyi yöntemler için bir başlangıç noktası verir.

Zayıf yanlar da en az bunlar kadar önemlidir:

  • Garanti yoktur. {1, 3, 4} paralarıyla para üstü ve 0/1 sırt çantası, açgözlü sonucun en iyiden kötü olduğu bilinen örneklerdir.
  • Her problem için ayrı kanıt gerekir. Bir açgözlü algoritmanın bir problemde doğru olması, benzer görünen başka bir problemde doğru olacağını göstermez.
  • Ölçüt seçimi belirleyicidir. Aynı problem için akla gelen birçok ölçütten genellikle yalnız biri doğrudur. Yanlış ölçüt, kodu tamamen doğru yazılmış bir algoritmayı yanlış yapar.
  • Hata düzeltilemez. Bir karar verildikten sonra geri alınmadığı için erken yapılan kötü bir seçimin etkisi sonuna kadar taşınır.

7.1.6Açgözlü yaklaşım ve böl ve fethet

Böl ve fethet de bir problemi daha küçük problemlere indirger. Bu benzerlik iki yaklaşımın karıştırılmasına yol açabilir. Aradaki fark, küçültmenin nasıl yapıldığındadır. Böl ve fethet problemi önceden belirlenmiş bir kuralla böler, örneğin diziyi ortadan ikiye. Bu bölmede bir seçim yoktur. Sonra bütün parçaları çözer ve sonuçları birleştirir. Açgözlü yaklaşım ise bir seçim yapar ve bu seçimden sonra geriye tek bir alt problem kalır. O alt problem çözülür; başka bir parça yoktur. Tablo 7.1 farkları beş başlık altında toplar.

Tablo 7.1: Açgözlü yaklaşım ile böl ve fethetin karşılaştırması.
AçgözlüBöl ve fethet
Kararher adımda tek, yerel en iyi seçim; geri alınmazsabit bir kuralla bölünür; seçim yapılmaz
Alt problemlerseçimden sonra tek alt problem kalırbirbirinden bağımsız birkaç alt problem
Doğrulukaçgözlü seçim özelliği ve optimal alt yapı kanıtlanmalıbirleştirme adımının doğruluğuna bağlı
Maliyetgenelde O(n log n): sıralama ve tek geçişözyineleme bağıntısı ve ana teoremle bulunur
Örneketkinlik seçimi, Kruskal, Huffmanbirleştirme sıralaması, Karatsuba

Maliyet satırı da bu farkı yansıtır. Birleştirme sıralamasının süresi T(n) = 2T(n/2) + O(n) bağıntısından ana teoremle bulunur ve O(n log n)'dir. Açgözlü bir algoritmanın süresi için genellikle bir bağıntı kurmaya gerek yoktur; sıralama ve tarama maliyetleri doğrudan toplanır. Doğruluk satırı ise iki yaklaşımın en önemli farkını gösterir. Böl ve fethette alt problemler doğru çözülür ve birleştirme doğruysa sonuç kendiliğinden doğrudur. Açgözlü yaklaşımda sonucun doğru olması, yapılan seçimin güvenli olmasına bağlıdır ve bu ayrıca kanıtlanmalıdır.

7.2Para üstü problemi

Açgözlü yaklaşımın en tanıdık örneği para üstü vermektir. Bir kasiyer 68 kuruşu en az sayıda madeni parayla ödemek isterse önce 50'lik, sonra 10'luk, sonra 5'lik ve en sonda üç tane 1'lik verir. Her adımda kalan tutara sığan en büyük parayı seçer. Bu yöntem gündelik para sistemlerinde kusursuz çalışır. Ancak para birimleri farklı seçilirse aynı yöntem en iyi sonucu kaçırabilir. Bu kesim hem yöntemin işlediği hem de yanıldığı durumları inceler.

7.2.1Problem ve açgözlü ölçüt

Tanım 7.4

Para üstü problemi (coin change problem): pozitif tam sayı değerli k para türü p1 > p2 > … > pk = 1 ve bir T tutarı verilir. Her türden istenen sayıda para kullanılabilir. Toplamı tam olarak T olan en az sayıda para bulunmak istenir.

En küçük paranın 1 olması, her tutarın ödenebilmesini garanti eder. Bu koşul yoksa bazı tutarlar hiç ödenemez; örneğin yalnız 3 ve 5'lik paralarla 7 ödenemez. Açgözlü ölçüt şöyledir: kalan tutardan büyük olmayan en büyük para seçilir; tutar sıfır olana kadar bu adım tekrarlanır. Sunumdaki Java kodu bu kuralı iki iç içe döngüyle uygular:

// 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;
}

Kod, para dizisinin büyükten küçüğe sıralı geldiğini varsayar. Dış döngü her para türünü bu sırayla bir kez ele alır. İç while döngüsü, o para kalan tutara sığdığı sürece onu kullanır: tutardan düşer ve sayacı artırır. Para artık sığmadığında iç döngü biter ve bir sonraki, daha küçük para türüne geçilir. En küçük para 1 olduğu için son türden sonra tutar her zaman sıfırdır.

Maliyet hesabı iki kısımdan oluşur. Dış döngü k kez döner ve her dönüşte en az bir while koşulu sınanır; bu O(k) eder. İç döngünün gövdesi her çalıştığında bir para seçilir; gövde toplam adet kez çalışır. Toplam süre O(k + adet)'tir. Para dizisi sıralı gelmezse buna O(k log k)'lik bir sıralama eklenir. İç döngü bölme ve kalan işlemiyle de yazılabilir: adet += tutar / p; tutar %= p;. Bu sürüm para sayısından bağımsız olarak O(k) sürer ve büyük tutarlarda daha hızlıdır. Sonuç aynıdır.

7.2.2Kanonik bir sistemde adım adım

Sunumdaki ilk örnekte paralar {50, 25, 10, 5, 1}, tutar 68'dir. Tablo 7.2 her adımı verir.

Tablo 7.2: {50, 25, 10, 5, 1} paralarıyla 68 tutarının açgözlü ödenmesi.
AdımSığan en büyük paraKalan tutarPara sayısı
başlangıç—680
15068 − 50 = 181
210 (25 sığmaz)18 − 10 = 82
358 − 5 = 33
413 − 1 = 24
512 − 1 = 15
611 − 1 = 06

Sonuç 68 = 50 + 10 + 5 + 1 + 1 + 1, yani 6 paradır. Bu, 68 için gerçekten en az para sayısıdır. Daha azının mümkün olmadığı şöyle görülebilir. 1 dışındaki bütün paralar 5'in katıdır ve 68'in 5'e bölümünden kalan 3'tür. Bu yüzden her çözümde en az üç tane 1'lik para bulunur. En fazla 5 paralık bir çözüm olsaydı, 1'likler dışındaki en fazla iki para 65'i ödemek zorunda kalırdı. Oysa iki parayla 65 ödenemez: 50 + 10 = 60, 50 + 25 = 75 ve 25 + 25 = 50'dir; 65'e eşit bir iki para toplamı yoktur. Tek parayla ya da hiç parasız da ödenemez.

madeni paralar50251051seçilenler50105111kalan tutar0para sayısı: 6
Şekil 7.2: 68 tutarının açgözlü ödenmesinin son durumu. Seçilenler 50, 10, 5, 1, 1, 1; kalan tutar 0, para sayısı 6.

Açgözlü sonucun her tutar için en iyi olduğu para sistemlerine kanonik para sistemi denir. {50, 25, 10, 5, 1} ve {25, 10, 5, 1} kanonik sistemlerdir. Türk lirasının madeni paralarına karşılık gelen {100, 50, 25, 10, 5, 1} kuruş da kanoniktir. Euro sisteminin {200, 100, 50, 20, 10, 5, 2, 1} sent dizisi de öyledir. Notun doğrulama betiği bu sistemlerin her birinde 1 ile 499 arasındaki bütün tutarları dinamik programlamayla karşılaştırmış ve hiçbir farka rastlamamıştır. Gündelik para sistemlerinin kanonik olması tesadüf değildir; sistemler zaten bu özellik gözetilerek tasarlanır.

7.2.3Kod izleme: {4, 3, 1} ve 10

Şimdi para birimleri {4, 3, 1} olsun ve paraUstu 10 tutarıyla çağrılsın. Paralar büyükten küçüğe sıralı verilmiştir. Bu sistem kanonik değildir; açgözlü sonucun en iyi olup olmayacağı baştan bilinmez. Tablo 7.3 kodun her adımında değişkenlerin değerini gösterir.

Tablo 7.3: paraUstu({4, 3, 1}, 10) izlemesi.
AdımpKoşul tutar >= pİşlemtutaradet
1410 ≥ 4 doğru4 düşülür61
246 ≥ 4 doğru4 düşülür22
342 ≥ 4 yanlışwhile biter22
432 ≥ 3 yanlış3'lük hiç kullanılmaz22
512 ≥ 1 doğru1 düşülür13
611 ≥ 1 doğru1 düşülür04
710 ≥ 1 yanlışwhile biter, return 404
para431tutarkalan 10adet0 para431tutar4411kalan 0adet4en iyi4333 para
Şekil 7.3: paraUstu({4, 3, 1}, 10): başlangıç durumu (üstte) ve son durum (altta). Açgözlü 4 + 4 + 1 + 1 ile 4 para kullanır; en iyi çözüm 4 + 3 + 3 ile 3 paradır.

Kod 4 döndürür: 10 = 4 + 4 + 1 + 1. Oysa 10 = 4 + 3 + 3 yalnız 3 para kullanır. Hata ikinci adımda yapılmıştır. İlk 4'lük seçildikten sonra kalan 6 tutarı için açgözlü kural yine 4'ü seçer, çünkü 4 hâlâ sığar. Oysa 6, iki tane 3'lükle ödenebilirdi. İkinci 4'lük seçildiği anda kalan 2 için 3'lük para işe yaramaz hâle gelir ve 2, iki tane 1'likle ödenir. İzlemenin dördüncü satırı bu sonucu gösterir: 3'lük para hiç kullanılmaz.

7.2.4Açgözlü seçim neden yanılır?

Aynı sistemde 6 tutarı daha da kısa bir karşı örnektir. Açgözlü algoritma önce 4'ü seçer, kalan 2'yi iki tane 1'likle öder: 6 = 4 + 1 + 1, 3 para. En iyi çözüm ise 6 = 3 + 3, 2 paradır. Şekil 7.4 bu iki sonucu gösterir.

madeni paralar431seçilenler411kalan tutar0para sayısı: 3 madeni paralar431seçilenler33kalan tutar0para sayısı: 2en iyi: 2 para
Şekil 7.4: {4, 3, 1} paralarıyla 6 tutarı. Açgözlü 4 + 1 + 1 ile 3 para kullanır (üstte); en iyi çözüm 3 + 3 ile 2 paradır (altta).

Burada açgözlü seçim özelliği bozulmuştur. İlk açgözlü seçim 4'tür, ama 6'nın tek en iyi çözümü olan 3 + 3, 4'ü içermez. 4'ü içeren her çözüm en az 3 para kullanır. Değiştirme argümanı da bu sistemde işlemez: en iyi çözümdeki 3'lüklerden biri 4'lükle değiştirilirse toplam 7 olur ve çözüm uygunluğunu kaybeder.

Bir para sisteminin kanonik olup olmadığını anlamak için tek bir tutarın denenmesi yetmez. {4, 3, 1} sisteminde 1'den 10'a kadar olan tutarlardan yalnız 6 ve 10'da açgözlü yanılır; 1–5, 7, 8 ve 9 tutarlarında açgözlü sonuç en iyidir. 20'ye kadar hatalı tutarlar 6, 10, 14 ve 18'dir. Sunumdaki ikinci karşı örnek sistemi {10, 7, 1}'dir. Bu sistemde ilk hatalı tutar 14'tür: açgözlü 10 + 1 + 1 + 1 + 1 ile 5 para kullanır, en iyi çözüm 7 + 7 ile 2 paradır. 15 için açgözlü 6 para (10 + beş tane 1) verir, en iyi çözüm 7 + 7 + 1 ile 3 paradır.

Örnek 7.2

Açgözlü para üstünün yanıldığı, en büyük parası en küçük olan para sistemi hangisidir? En büyük para 2 ise sistem {2, 1}'dir ve her tutar için açgözlü en iyidir: önce olabildiğince 2'lik, en fazla bir tane 1'lik. En büyük para 3 ise olası sistemler {3, 1} ve {3, 2, 1}'dir. İkisinde de herhangi bir tutar 3q + r (r ≤ 2) biçiminde ödenir ve daha iyisi yoktur. En büyük para 4 olan sistemler {4, 1}, {4, 2, 1}, {4, 3, 1} ve {4, 3, 2, 1}'dir. Bunlardan yalnız {4, 3, 1} kanonik değildir ve ilk karşı örneği 6'dır. Doğrulama betiği bu sonucu bütün küçük sistemleri tarayarak bulmuştur. Demek ki {1, 3, 4} ve 6 tutarı, açgözlünün yanıldığı en küçük örnektir.

Bir sistemin kanonik olup olmadığı genel olarak kolayca görülmez. Komşu paraların birbirinin katı olması yeterli bir koşuldur: {100, 50, 10, 5, 1} gibi bir sistemde her büyük para, kendinden küçük paraların yerine geçebilir. Ancak bu koşul gerekli değildir; {25, 10, 5, 1} sisteminde 25, 10'un katı değildir, ama sistem yine kanoniktir. Kozen ve Zaks'ın gösterdiği gibi, kanonik olmayan bir sistemin en küçük karşı örneği en büyük iki paranın toplamından küçüktür. Bu yüzden bir sistemi denetlemek için bu sınıra kadar olan tutarları dinamik programlamayla karşılaştırmak yeterlidir. {4, 3, 1} için sınır 7'dir ve 6 bu aralıktadır. {10, 7, 1} için sınır 17'dir ve 14 bu aralıktadır.

Sık yapılan hata

Açgözlü para üstünü birkaç tutarla deneyip doğru sanmak. {4, 3, 1} sisteminde 1'den 5'e kadar bütün tutarlar doğru sonuç verir; ilk hata 6'da ortaya çıkar. Birkaç başarılı deneme bir kanıt değildir. Açgözlü bir algoritmanın doğruluğu ya kanıtla gösterilir ya da, para üstündeki gibi, sonlu bir aralığın tamamı en iyi yöntemle karşılaştırılarak denetlenir.

7.2.5En iyi sonuç: dinamik programlama

Para sistemi kanonik değilse en iyi sonucu dinamik programlama verir. Fikir, her tutar için en az para sayısını küçükten büyüğe hesaplamaktır. enAz[t], t tutarını ödemek için gereken en az para sayısı olsun. enAz[0] = 0'dır. t > 0 için son kullanılan para p ise geriye t − p tutarı kalır ve onun da en iyi biçimde ödenmesi gerekir. Bütün paralar denenir ve en iyisi seçilir:

enAz[t] = 1 + min { enAz[t − p] : p ≤ t }

Bu bağıntı optimal alt yapının bir örneğidir: t'nin en iyi çözümünden son para çıkarılırsa, kalan paralar t − p'nin en iyi çözümünü oluşturur. Açgözlü algoritmadan farkı, son paranın hangisi olacağının bilinmemesi ve bütün seçeneklerin denenmesidir. Tablo 7.4 {4, 3, 1} sistemi için 0'dan 10'a kadar tabloyu açgözlü sonuçla yan yana verir.

Tablo 7.4: {4, 3, 1} sisteminde açgözlü sonuç ve enAz[t].
t012345678910
açgözlü01211232234
enAz[t]01211222233

Tablonun iki hücresi ayrıntılı hesaplanırsa bağıntının işleyişi görülür. t = 6 için üç seçenek vardır: son para 1 ise 1 + enAz[5] = 3, son para 3 ise 1 + enAz[3] = 2, son para 4 ise 1 + enAz[2] = 3. En küçüğü 2'dir ve 3'lük parayla elde edilir. t = 10 için seçenekler 1 + enAz[9] = 4, 1 + enAz[7] = 3 ve 1 + enAz[6] = 3'tür; en küçüğü 3'tür. Açgözlü algoritma ise yalnız 4'lük seçeneği izler ve 10 için 4 para bulur.

Dinamik programlamanın bedeli süredir. Tablo T + 1 hücrelidir ve her hücre k para türünü dener; toplam O(k · T) zaman ve O(T) bellek gerekir. Açgözlü algoritma ise O(k + adet) sürede biter ve ek bellek kullanmaz. Sunumdaki etkileşimli slayt iki yöntemi yan yana çalıştırır: istenen para kümesi ve tutar girilir, açgözlü sonuç ile dinamik programlamanın en iyi sonucu karşılaştırılır. Hazır düğmelerden biri {1, 7, 10} ve 14 örneğini, öteki kanonik TL kuruş sistemini ve 68 tutarını yükler.

Sık yapılan hata

paraUstu'yu sırasız bir para dizisiyle çağırmak. Kod dizinin büyükten küçüğe sıralı olduğunu varsayar ve bunu denetlemez. {1, 5, 10} dizisiyle çağrılırsa ilk tür 1 olduğu için bütün tutar 1'liklerle ödenir; 68 için 68 para döner. Ya çağıran taraf diziyi sıralamalı ya da metodun başına bir sıralama eklenmelidir.

7.3Etkinlik seçimi

Bir konferans salonunu kullanmak isteyen birçok etkinlik vardır. Her etkinliğin bir başlangıç ve bir bitiş saati bellidir. Salonda aynı anda yalnız bir etkinlik yapılabilir. Yönetim, birbiriyle çakışmayan olabildiğince çok etkinliği kabul etmek ister. Etkinliklerin süresi ya da önemi değil, yalnız sayısı önemlidir. Bu problem, açgözlü bir algoritmanın en iyi sonucu verdiği ve bunun kısa bir kanıtla gösterilebildiği klasik bir örnektir.

7.3.1Problemin tanımı

Tanım 7.5

Etkinlik seçimi (activity selection): n etkinlik verilir; i. etkinlik [si, fi) yarı açık aralığını kaplar (si < fi). İki etkinlik [si, fi) ve [sj, fj), sj ≥ fi ya da si ≥ fj ise çakışmaz. Birbiriyle çakışmayan en fazla sayıda etkinlikten oluşan bir küme bulunmak istenir.

Aralıkların yarı açık olması, bir etkinliğin bittiği anda bir başkasının başlayabileceği anlamına gelir. Örneğin [3, 4) ile [4, 6) çakışmaz. Bitiş anı etkinliğe dahil değildir. Sunumdaki örnekte on etkinlik vardır (Tablo 7.5).

Tablo 7.5: Sunumdaki on etkinlik.
Etkinlik12345678910
başlangıç s12465803710
bitiş f35687924912

Kaba kuvvetle bütün alt kümeleri denemek 2n küme demektir; on etkinlik için 1024 küme, kırk etkinlik için bir trilyonu aşan küme. Açgözlü algoritma ise etkinlikleri bir kez sıralar ve tek geçişte seçer.

7.3.2Hangi ölçüt?

Açgözlü bir algoritma kurmak için önce “her adımda hangi etkinlik seçilsin?” sorusuna cevap vermek gerekir. Akla gelen dört ölçüt vardır:

  1. En erken başlayan. Salon olabildiğince erken dolsun.
  2. En kısa süren. Kısa etkinlikler salonu az meşgul eder.
  3. En az çakışan. Başka etkinlikleri en az engelleyen seçilsin.
  4. En erken biten. Salon olabildiğince erken boşalsın.

Her biri makul görünür, ama yalnız sonuncusu her girdide en iyi sonucu verir. Bir ölçütün yanlış olduğunu göstermek için tek bir karşı örnek yeter. Şekil 7.5 ilk iki ölçüt için sunumdaki karşı örnekleri gösterir.

En erken başlayanaçgözlü 1 · en iyi 3En kısa sürenaçgözlü 1 · en iyi 20246810
Şekil 7.5: Yanlış ölçütlere karşı örnekler. Turuncu: açgözlünün seçtiği etkinlik; yeşil: en iyi çözüm. En erken başlayan ölçütü 1 etkinlik seçer, en iyisi 3'tür. En kısa süren ölçütü 1 etkinlik seçer, en iyisi 2'dir.

Üstteki örnekte [0, 10) etkinliği en erken başlar. Açgözlü algoritma onu seçer ve onunla çakışan [1, 3), [4, 6) ve [7, 9) etkinliklerini atar; sonuç 1 etkinliktir. Oysa bu üç kısa etkinlik birbiriyle çakışmaz ve birlikte seçilebilir. Alttaki örnekte [4, 7) en kısa etkinliktir (3 birim). Açgözlü onu seçer, ama bu etkinlik hem [0, 5) ile hem [6, 11) ile çakışır. Sonuç yine 1 etkinliktir; en iyi çözüm ise iki uzun etkinliği birlikte seçer.

En az çakışan ölçütü daha ince bir karşı örnek gerektirir, çünkü küçük örneklerde çoğu zaman doğru sonuç verir. Tablo 7.6 on bir etkinlikli bir karşı örnek verir. En iyi çözüm A, B, C ve D'den oluşur ve 4 etkinlik içerir. Ancak çakışma sayısı en küçük olan etkinlik M'dir: yalnız B ve C ile çakışır. Açgözlü önce M'yi seçer ve böylece B'yi de C'yi de kaybeder. Kalan etkinliklerden en fazla iki tane daha seçilebilir; sonuç 3 etkinliktir. Doğrulama betiği eşitliklerin her türlü bozulmasını denemiş ve bu ölçütün bu girdide 3'ü geçemediğini göstermiştir.

Tablo 7.6: “En az çakışan” ölçütüne karşı örnek.
EtkinlikAralıkÇakıştığı etkinlik sayısı
A[0, 3)3
B[4, 7)4
C[8, 11)4
D[12, 15)3
X1, X2, X3[2, 5) (üç kopya)4'er
Y1, Y2, Y3[10, 13) (üç kopya)4'er
M[6, 9)2

En erken biten ölçütünün arkasındaki düşünce şudur: salonu en erken boşaltan etkinlik, geri kalan etkinliklere en çok zaman bırakır. Geri kalan zaman ne kadar uzunsa, oraya o kadar çok etkinlik sığabilir. Ölçüt etkinliğin süresine ya da başlangıcına değil, yalnız bitişine bakar.

7.3.3Kod ve izleme

Sunumdaki sec metodu etkinlikleri bitiş zamanına göre sıralar, ilkini seçer ve sonra her etkinliği son seçilenin bitişiyle karşılaştırır:

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;
    }
  }
}

Kodda Etkinlik sınıfının id, bas ve bitis alanları vardır. Arrays.sort çağrısındaki (x, y) -> x.bitis - y.bitis ifadesi bir karşılaştırıcıdır: x'in bitişi daha küçükse negatif bir değer döner ve x öne geçer. Java'nın nesne dizileri için kullandığı sıralama kararlıdır; bitişi eşit iki etkinlik dizideki ilk sıralarını korur. i değişkeni son seçilen etkinliğin dizideki yerini tutar. Sıralı dizinin ilk öğesi en erken biten etkinliktir ve koşulsuz seçilir. Döngü j = 1'den başlar. e[j].bas >= e[i].bitis koşulu, j. etkinliğin son seçilenden sonra başladığını, yani onunla çakışmadığını sınar. Koşul doğruysa etkinlik yazdırılır ve i güncellenir.

Burada önemli bir gözlem vardır: yeni etkinlik yalnız son seçilenle karşılaştırılır, bütün seçilenlerle değil. Bu yeterlidir, çünkü etkinlikler bitişe göre sıralıdır. Son seçilen, seçilenlerin içinde en geç biten etkinliktir. Yeni etkinlik ondan sonra başlıyorsa, daha önce biten bütün seçilenlerden de sonra başlar.

Sıralama sonrası dizi şöyledir: #7 [0, 2), #1 [1, 3), #8 [3, 4), #2 [2, 5), #3 [4, 6), #5 [5, 7), #4 [6, 8), #6 [8, 9), #9 [7, 9), #10 [10, 12). #6 ile #9'un bitişi aynıdır (9); kararlı sıralama #6'yı önde tutar. Tablo 7.7 döngünün her adımını verir.

024681012#7#1#8#2#3#5#4#6#9#10
Şekil 7.6: Bitiş zamanına göre sıralanmış on etkinlik (yukarıdan aşağı). Yatay eksen zamandır.
Tablo 7.7: sec(e) izlemesi. e[i].bitis son seçilenin bitişidir.
jEtkinlike[j].base[i].bitisKararYeni i
0#7 [0, 2)0—ilk etkinlik, seçilir0
1#1 [1, 3)121 < 2, çakışır, atlanır0
2#8 [3, 4)323 ≥ 2, seçilir2
3#2 [2, 5)242 < 4, atlanır2
4#3 [4, 6)444 ≥ 4, seçilir4
5#5 [5, 7)565 < 6, atlanır4
6#4 [6, 8)666 ≥ 6, seçilir6
7#6 [8, 9)888 ≥ 8, seçilir7
8#9 [7, 9)797 < 9, atlanır7
9#10 [10, 12)10910 ≥ 9, seçilir9

Program 7 8 3 4 6 10 yazar; 6 etkinlik seçilmiştir. Bu sayı gerçekten en büyüktür: doğrulama betiği 1024 alt kümenin hepsini denemiş ve 7 etkinlikli çakışmasız bir küme bulamamıştır. 6 etkinlikli iki küme vardır: açgözlünün bulduğu {7, 8, 3, 4, 6, 10} ve {1, 8, 3, 4, 6, 10}. İkincisi, #7 yerine #1 ile başlar. Bu durum açgözlü seçim özelliğinin “en az bir” en iyi çözümden söz ettiğini somut olarak gösterir: en erken biten #7'yi içermeyen bir en iyi çözüm de vardır, ama #7'yi içeren bir tane de vardır.

024681012#7#1#8#2#3#5#4#6#9#10
Şekil 7.7: Seçimin sonu. Yeşil etkinlikler seçilmiştir (#7, #8, #3, #4, #6, #10); kesikli çerçeveliler atlanmıştır. Dikey kesikli çizgi son seçilenin bitişidir (12).

İzlemedeki üç adım, yarı açık aralıkların önemini gösterir. #3 tam 4'te başlar ve #8 tam 4'te biter; #4 tam 6'da başlar ve #3 tam 6'da biter; #6 tam 8'de başlar ve #4 tam 8'de biter. Koşul >= yerine > yazılsaydı, son seçilenin bittiği anda başlayan etkinlikler atlanırdı. #8'den sonra #3 yerine #5 [5, 7) seçilirdi; ardından #6 [8, 9) ve #10 [10, 12) gelirdi ve sonuç 5 etkinlikle kalırdı.

Maliyet açıktır. Sıralama O(n log n), tek geçiş O(n) sürer; toplam O(n log n). Etkinlikler bitişe göre sıralı gelirse sıralama gereksizdir ve algoritma O(n) sürede biter. Ek bellek olarak yalnız bir i değişkeni tutulur.

Örnek 7.3

On bir etkinlik (s, f) çiftleriyle verilsin: (1, 4), (3, 5), (0, 6), (5, 7), (3, 9), (5, 9), (6, 10), (8, 11), (8, 12), (2, 14), (12, 16). Etkinlikler zaten bitişe göre sıralıdır. İlk etkinlik (1, 4) seçilir ve son bitiş 4 olur. (3, 5) ve (0, 6) 4'ten önce başlar, atlanır. (5, 7) seçilir, son bitiş 7 olur. (3, 9), (5, 9) ve (6, 10) 7'den önce başlar, atlanır. (8, 11) seçilir, son bitiş 11 olur. (8, 12) ve (2, 14) atlanır. (12, 16) seçilir. Sonuç 4 etkinliktir: (1, 4), (5, 7), (8, 11), (12, 16).

Sık yapılan hata

Yeni etkinliği son seçilenle değil, dizide bir önceki etkinlikle karşılaştırmak; yani e[j].bas >= e[j - 1].bitis yazmak. Önceki etkinlik atlanmış olabilir. İzlemede j = 3'teki #2 [2, 5) bunu gösterir: bir önceki etkinlik #8'in bitişi 4'tür ve 2 < 4 olduğu için sonuç doğru çıkar. Ama j = 4'teki #3 [4, 6) atlanmış #2'nin bitişi olan 5 ile karşılaştırılır; 4 < 5 olduğu için yanlışlıkla atlanır. Karşılaştırma her zaman son seçilen etkinlikle yapılmalıdır.

7.3.4En erken biten neden doğrudur?

Teorem 7.1 (Etkinlik seçiminde açgözlü seçim)

a1 en erken biten etkinlik olsun. Etkinlik seçimi probleminin a1'i içeren bir en iyi çözümü vardır. Bu yüzden “en erken biteni seç, onunla çakışanları at, kalanlarla tekrarla” algoritması en fazla sayıda etkinlik seçer.

Kanıt değiştirme argümanıyla kurulur. O = {b1, b2, …, bk} bitişe göre sıralı bir en iyi çözüm olsun. b1 = a1 ise söylenecek bir şey yoktur. Değilse O' = (O − {b1}) ∪ {a1} kümesi kurulur. a1 en erken biten etkinlik olduğu için f(a1) ≤ f(b1)'dir. O çakışmasız olduğu için f(b1) ≤ s(b2)'dir. Bu iki eşitsizlik birleşince f(a1) ≤ s(b2) bulunur. b2, …, bk etkinliklerinin hepsi b2'den sonra başladığı için a1 bunların hiçbiriyle çakışmaz. Demek ki O' uygundur. Eleman sayısı da değişmemiştir: |O'| = |O| = k. Öyleyse O' de bir en iyi çözümdür ve a1'i içerir.

Optimal alt yapı için a1 ve onunla çakışan etkinlikler atılır. Kalan etkinlikler, f(a1)'den sonra başlayan etkinliklerdir ve aynı türden bir problem oluşturur. O''den a1 çıkarılınca kalan küme bu küçük problemin bir çözümüdür. Küçük problemin daha büyük bir çözümü olsaydı, ona a1 eklenerek O''den büyük bir çözüm elde edilirdi; bu, O''nin en iyi olmasıyla çelişir. Tümevarımla algoritmanın her adımdaki seçimi bir en iyi çözümün parçası olarak kalır. □

Kanıtta en erken biten etkinliğin özel bir rolü vardır: değiştirilen b1'den daha geç bitmez. Bu yüzden onun yerine geçtiğinde sonraki etkinliklere engel olmaz. En erken başlayan etkinlik için aynı argüman kurulamaz: en erken başlayan etkinlik çok geç bitebilir ve b2 ile çakışabilir. Şekil 7.5'teki [0, 10) tam olarak bu durumdadır.

Aynı problem ters yönden de çözülebilir. Etkinlikler başlangıca göre büyükten küçüğe sıralanır ve her adımda en geç başlayan etkinlik seçilir. Bu, zaman ekseni ters çevrildiğinde en erken biten ölçütünün aynısıdır ve aynı kanıtla en iyi sonucu verir. Problemin bir genellemesi olan ağırlıklı etkinlik seçiminde ise her etkinliğin bir değeri vardır ve toplam değer en büyük olmalıdır. Bu sürümde hiçbir basit açgözlü ölçüt doğru değildir; problem dinamik programlamayla çözülür.

7.4Son tarihli iş sıralama

Bir atölyede tek bir makine var ve birkaç sipariş sırada bekliyor. Her sipariş makinede bir gün sürer. Her siparişin bir teslim tarihi ve bir kârı vardır; sipariş teslim tarihine kadar bitirilirse kâr kazanılır, bitirilmezse sipariş kaybedilir. Atölye hangi siparişleri hangi günlere koyarsa toplam kârı en büyük olur? Bu problem etkinlik seçimine benzer, ama burada bütün işlerin süresi aynıdır ve önemli olan sayı değil, kârdır.

7.4.1Problemin tanımı

Tanım 7.6

Son tarihli iş sıralama (job sequencing with deadlines): n iş verilir. Her iş 1 birim sürer; i. işin bir son tarihi di ve bir kârı ki vardır. Zaman 1, 2, 3, … diye numaralanmış birim dilimlere (slot) bölünmüştür ve her dilimde en fazla bir iş yapılır. İş di. dilimin sonuna kadar bitirilirse ki kâr kazanılır. Toplam kârı en büyük olan iş kümesi ve dilim ataması bulunmak istenir.

Bir işin di son tarihi, işin 1'den di'ye kadar olan dilimlerden birine konabileceği anlamına gelir. Son tarihi 1 olan bir iş yalnız birinci dilime konabilir. Son tarihi 3 olan bir iş birinci, ikinci ya da üçüncü dilime konabilir. Sunumdaki örnekte altı iş vardır (Tablo 7.8).

Tablo 7.8: Sunumdaki altı iş.
İş123456
son tarih411132
kâr201040305030

İlk bakışta üç işin (2, 3 ve 4) son tarihi 1'dir ve bunlardan yalnız biri yapılabilir. Hangisinin yapılacağı ve öteki işlerin nereye konacağı birlikte düşünülmelidir.

7.4.2Açgözlü ölçüt ve en geç boş dilim

Açgözlü algoritma iki karar verir. Birincisi işlerin hangi sırayla ele alınacağıdır: işler kâra göre azalan sırayla incelenir, yani en kârlı iş önce. İkincisi her işin hangi dilime konacağıdır: iş, son tarihinden önceki en geç boş dilime konur. Son tarihine kadar boş dilim kalmamışsa iş atlanır ve bir daha ele alınmaz.

İkinci kararın gerekçesi şudur. Bir işi gereğinden erken bir dilime koymak, son tarihi daha yakın olan bir işin yerini kapatabilir. Örneğin son tarihi 3 olan bir iş birinci dilime konursa, sonradan gelecek ve son tarihi 1 olan bir işin gidebileceği tek yer dolmuş olur. Aynı iş üçüncü dilime konsaydı birinci dilim boş kalırdı. En geç boş dilim, erken dilimleri son tarihi daha yakın işlere bırakır ve esnekliği korur.

7.4.3Kod ve izleme

Sunumdaki isSirala metodu bu iki kuralı uygular ve toplam kârı döndürür:

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;
}

Is sınıfının id, sonTarih ve kar alanları vardır. Arrays.sort çağrısındaki karşılaştırıcı b.kar - a.kar, kârı büyük olan işi öne alır. Kararlı sıralama sayesinde eşit kârlı işler ilk sıralarını korur. slot dizisi n dilimi tutar: slot[j], j + 1. dilime konan işin numarasıdır, boşsa −1'dir. Java'da diziler 0'dan başladığı için birinci dilim slot[0]'dadır. n iş en fazla n dilim kullanabilir; bu yüzden n'den büyük son tarihler n'ye indirilir. Math.min(n, is[i].sonTarih) - 1 ifadesi bunu yapar ve işin konabileceği en geç dilimin indisini verir. İç döngü bu dilimden geriye doğru ilk boş dilimi arar. Boş dilim bulunursa iş yerleştirilir, kâr toplama eklenir ve break ile arama biter. Döngü j = −1'e kadar inerse uygun dilim yoktur ve iş sessizce 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—2—3—4—5—6—
Şekil 7.8: İşler kâra göre azalan sırada; altı dilim boş. Eşit kârlı İş 4 ve İş 6 ilk sıralarını korur.

Kâra göre sıralanmış dizi İş 5 (50), İş 3 (40), İş 4 (30), İş 6 (30), İş 1 (20), İş 2 (10) şeklindedir. Tablo 7.9 her işin yerleştirilmesini adım adım verir. Tabloda dilimler 1'den numaralanır.

Tablo 7.9: isSirala(is, 6) izlemesi.
İşSon tarihKârBakılan dilimlerKararDilimler 1–6toplam
535033. dilime– – 5 – – –50
314011. dilime3 – 5 – – –90
41301 (dolu)atlanır3 – 5 – – –90
623022. dilime3 6 5 – – –120
142044. dilime3 6 5 1 – –140
21101 (dolu)atlanır3 6 5 1 – –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—
Şekil 7.9: Sonuç: dilimler 1–4'te sırasıyla İş 3, İş 6, İş 5, İş 1; toplam kâr 140. İş 4 ve İş 2 yapılamaz.

Metot 140 döndürür. Makine birinci gün İş 3'ü, ikinci gün İş 6'yı, üçüncü gün İş 5'i, dördüncü gün İş 1'i yapar. Beşinci ve altıncı dilimler boş kalır; hiçbir işin son tarihi 4'ü geçmediği için bu dilimlerin kullanılması zaten mümkün değildir. 140'ın en büyük toplam kâr olduğu, doğrulama betiğinde altı işin bütün alt kümeleri denenerek gösterilmiştir.

İzlemenin ilginç adımı İş 4'tür. Kârı 30'dur ve İş 6 ile eşittir, ama son tarihi 1'dir ve birinci dilimi daha kârlı İş 3 almıştır. İş 4 için başka bir dilim yoktur. İş 6 ise son tarihi 2 olduğu için ikinci dilime sığar. Eşit kârlı iki işten birinin yapılıp ötekinin yapılamaması, son tarihlerin farklı olmasından kaynaklanır.

Örnek 7.4

Beş iş (numara, son tarih, kâr) olarak verilsin: (1, 2, 100), (2, 1, 19), (3, 2, 27), (4, 1, 25), (5, 3, 15). Kâra göre sıra İş 1, İş 3, İş 4, İş 2, İş 5'tir. İş 1'in son tarihi 2'dir; 2. dilim boştur ve iş oraya konur (toplam 100). İş 3'ün son tarihi de 2'dir. 2. dilim doludur, bir öncekine bakılır: 1. dilim boştur ve iş oraya konur (toplam 127). İş 4'ün ve İş 2'nin son tarihi 1'dir; 1. dilim doludur ve ikisi de atlanır. İş 5'in son tarihi 3'tür; 3. dilim boştur (toplam 142). Sonuç: makine sırasıyla İş 3, İş 1 ve İş 5'i yapar, toplam kâr 142'dir.

7.4.4Yanlış ölçütler

İki karardan her biri yanlış seçilebilir. Önce işlerin sırası ele alınsın. İşleri son tarihe göre sıralamak doğal görünür: önce acil işler yapılsın. Ancak bu ölçüt kârı dikkate almaz. Son tarihi 1 olan iki iş düşünülsün: birinin kârı 10, ötekinin 40'tır. Yalnız biri yapılabilir. Son tarihe göre sıralamada (kararlı sıralamayla) ilk gelen iş seçilir; ilk gelen kârı 10 olan işse sonuç 10'dur. Kâra göre sıralama ise 40'lık işi seçer. Sunumdaki altı işte son tarihe göre sıralama ve en geç boş dilim kuralı 110 kâr verir; kâr sırası 140 verir.

Sonra dilim seçimi ele alınsın. İş, son tarihinden önceki en erken boş dilime konsun. Sunumdaki altı işte İş 5 (son tarih 3) birinci dilime gider. Ardından gelen İş 3'ün tek seçeneği birinci dilimdir, ama o dolmuştur; 40 kârlı iş kaybedilir. Algoritma sonunda İş 5, İş 6 ve İş 1'i yapar ve yalnız 100 kâr elde eder. En geç boş dilim kuralıyla aynı işler 140 kâr getirir. İki işli bir örnek daha kısadır: X'in son tarihi 2, kârı 50; Y'nin son tarihi 1, kârı 40 olsun. En erken boş dilim kuralı X'i birinci dilime koyar ve Y'yi kaybeder: 50. En geç boş dilim kuralı X'i ikinci dilime koyar, Y birinci dilime sığar: 90.

Sık yapılan hata

İşi son tarihinden önceki en erken boş dilime koymak. Bu kural, sonradan gelecek ve son tarihi yakın olan işlere yer bırakmaz. Doğru kural en geç boş dilimdir. Kodda bu, iç döngünün j'yi son tarihten başlatıp geriye saymasıyla sağlanır; döngü 0'dan başlayıp ileri sayarsa algoritma yanlış olur.

7.4.5Doğruluk ve maliyet

Algoritmanın doğruluğu iki parçada gösterilir. Birinci parça, en geç boş dilim kuralının bir işi ancak gerçekten sığmıyorsa reddettiğidir.

Teorem 7.2 (En geç boş dilim)

Algoritmanın o ana kadar kabul ettiği işler kümesi A ve sıradaki iş x olsun. En geç boş dilim kuralı x için boş bir dilim bulamıyorsa, A ∪ {x} kümesindeki işler hiçbir dilim atamasıyla son tarihlerine yetiştirilemez.

x için boş dilim bulunamadığına göre 1'den d(x)'e kadar bütün dilimler doludur. t ≥ d(x), 1'den t'ye kadar bütün dilimlerin dolu olduğu en büyük sayı olsun; t + 1. dilim boştur ya da t = n'dir. 1–t dilimlerindeki her işin son tarihi t'den küçük ya da eşittir. Bunun nedeni şudur: son tarihi t'den büyük olan bir iş yerleştirildiği anda t + 1. dilim boştu, çünkü dilimler hiçbir zaman boşaltılmaz. En geç boş dilim kuralı o işi t + 1'e ya da daha geç bir dilime koyardı, 1–t arasına koymazdı. Öyleyse 1–t dilimlerindeki t işin ve x'in, yani t + 1 işin hepsinin son tarihi en fazla t'dir. t dilime t + 1 iş sığmaz. □

İkinci parça, kâra göre azalan sıranın doğru olduğudur ve bir değiştirme argümanıyla gösterilir. En kârlı iş a olsun ve tek başına son tarihine yetişebilsin. O, a'yı içermeyen bir en iyi çözüm olsun. O'nun 1–d(a) dilimlerinden biri boşsa a oraya eklenir ve kâr artar; bu, O'nun en iyi olmasıyla çelişir. Hepsi doluysa bu dilimlerden birindeki b işi çıkarılıp yerine a konur. a kendi son tarihinden önceki bir dilime konduğu için çözüm uygundur. k(a) ≥ k(b) olduğu için kâr azalmaz. Demek ki a'yı içeren bir en iyi çözüm vardır. Sunum bu sonucu daha genel bir dille de anar: birlikte son tarihlerine yetiştirilebilen iş kümeleri bir matroid oluşturur ve matroidlerde ağırlığa göre azalan açgözlü seçim her zaman en iyidir. Kruskal algoritmasının doğruluğu da aynı genel sonucun bir örneğidir.

Maliyet şöyle hesaplanır. Sıralama O(n log n) sürer. Her iş için dilim araması en fazla min(n, di) dilime bakar; en büyük son tarih dmax ise arama toplam O(n · dmax) sürer. dmax, n'ye kadar çıkabildiği için en kötü durum O(n2)'dir. Örneğin bütün işlerin son tarihi n ise i. iş i dilime bakar ve toplam 1 + 2 + … + n = n(n + 1)/2 bakış yapılır. Bu maliyet ayrık kümeler (union-find) yapısıyla düşürülebilir. Her dolu dilim, solundaki ilk boş dilimi gösteren bir kümeye bağlanır. Bir işin en geç boş dilimi tek bir bul çağrısıyla bulunur; dilim dolunca soldaki kümeyle birlestir yapılır. Yol kısaltma ve rütbeye göre birleştirme ile her işlem neredeyse sabit sürer ve dilim araması toplamda neredeyse O(n)'e iner. Toplam maliyeti bu durumda sıralama belirler.

7.5Kesirli sırt çantası

Bir yolcunun sırt çantası en fazla 50 kilogram taşıyabilir. Önünde birkaç çuval baharat vardır; her çuvalın bir ağırlığı ve bir değeri bellidir. Baharat bölünebilir: bir çuvalın yarısı alınırsa değerinin yarısı kazanılır. Yolcu çantaya ne koyarsa taşıdığı değer en büyük olur? Bu soru kesirli sırt çantası problemidir. Nesneler bölünemediğinde, yani her nesne ya bütünüyle alındığında ya hiç alınmadığında problemin adı 0/1 sırt çantasıdır. İki problem birbirine çok benzer, ama açgözlü yaklaşım yalnız birincisinde en iyi sonucu verir.

7.5.1Problem ve oran ölçütü

Tanım 7.7

Kesirli sırt çantası (fractional knapsack): n nesne ve bir W kapasitesi verilir; i. nesnenin kârı ki, ağırlığı wi'dir. Her nesneden 0 ≤ xi ≤ 1 oranında bir parça alınabilir. Toplam ağırlık Σ xi wi ≤ W koşulunu sağlayan ve toplam kârı Σ xi ki en büyük yapan seçim bulunmak istenir.

Bir nesnenin ne kadar “verimli” olduğunu ağırlık başına getirdiği kâr gösterir. Bu değere nesnenin oranı denir: ki / wi. Açgözlü ölçüt, oranı en büyük nesneden başlayarak sığdığı kadarını almaktır. Bir nesnenin tamamı sığıyorsa tamamı alınır. Sığmıyorsa kalan kapasite kadar bir parçası alınır ve çanta dolar. Sunumdaki örnekte kapasite W = 50'dir ve üç nesne vardır (Tablo 7.10).

Tablo 7.10: Sunumdaki üç nesne (W = 50).
NesneKârAğırlıkOran
160106
2100205
3120304

Oranlar 6, 5 ve 4'tür; nesneler zaten oran sırasındadır. Nesne 1'in tamamı alınır (10 kg, 60 kâr). Nesne 2'nin tamamı alınır (20 kg, 100 kâr). Geriye 20 kg kapasite kalır, ama nesne 3 30 kg'dır. Nesne 3'ün 20/30'u alınır ve 120 · 20/30 = 80 kâr kazanılır. Toplam 60 + 100 + 80 = 240'tır.

kesirli123 · 20/30kâr 2400W = 50
Şekil 7.10: 50 kg'lık çantanın dolumu: nesne 1 ve 2 tümüyle, nesne 3'ün 20/30'u. Toplam kâr 240.

Dikkat edilirse oran ölçütü kârı en büyük nesneyi önce almaz. Nesne 3'ün kârı en büyüktür (120), ama ağırlığı da en büyüktür ve kilogram başına en az kârı getirir. Kâra göre azalan sırayla almak nesne 3'ün tamamını (30 kg, 120) ve nesne 2'nin tamamını (20 kg, 100) seçer ve 220'de kalır. Ağırlığa göre artan sırayla almak bu örnekte oran sırasıyla aynı sonucu verir, ama bu bir rastlantıdır: ağır ve çok kârlı bir nesne ile hafif ama değersiz bir nesne bulunan bir girdide yanılır.

7.5.2Kod ve izleme

Sunumdaki kesirli metodu nesneleri orana göre azalan sıralar ve çantayı doldurur:

static double kesirli(Nesne[] s, int W) {
  Arrays.sort(s, (a, b) -> Double.compare(b.oran(), a.oran()));
  double kar = 0;
  int kalan = W;
  for (Nesne x : s) {
    if (x.agirlik <= kalan) {        // tamamı sığar
      kalan -= x.agirlik;
      kar += x.kar;
    } else {                         // kesirli parça
      kar += x.kar * (double) kalan / x.agirlik;
      break;
    }
  }
  return kar;
}

Nesne sınıfının kar ve agirlik alanları ile oran() metodu vardır; oran(), (double) kar / agirlik değerini döndürür. Karşılaştırıcı Double.compare(b.oran(), a.oran()) oranı büyük olanı öne alır. Burada çıkarma yerine Double.compare kullanılır, çünkü oranlar ondalıklıdır ve iki oranın farkı bir tam sayıya çevrilirse 0,5 gibi farklar kaybolur. kalan, çantada kalan kapasitedir. Döngü her nesne için önce tamamının sığıp sığmadığına bakar. Sığıyorsa kapasiteden ağırlığı düşer ve kârı ekler. Sığmıyorsa else dalında nesnenin kalan / agirlik kadarlık bir parçasını alır. Çanta artık tam dolmuştur ve break ile döngüden çıkılır. Kesirli parçanın kârı hesaplanırken (double) dönüşümü önemlidir: kalan / x.agirlik tam sayı bölmesi olarak yazılsaydı 20/30 sıfıra yuvarlanırdı.

Tablo 7.11: kesirli(s, 50) izlemesi.
Nesne (oran)Koşul agirlik <= kalanİşlemkalankar
başlangıç—sıralama: 1, 2, 3500
1 (6)10 ≤ 50 doğrutamamı alınır4060
2 (5)20 ≤ 40 doğrutamamı alınır20160
3 (4)30 ≤ 20 yanlış20/30'u alınır: 120 · 20/30 = 80; break20240

Son satırda kalan 20 olarak kalır, çünkü else dalı kapasiteyi güncellemez; hemen ardından döngü kırıldığı için bu değer bir daha kullanılmaz. Metot 240.0 döndürür.

nesnekârağırlıkoran16010621002053120304çanta W = 50kalan = 50kar = 0 nesnekârağırlıkoran16010621002053120304çanta W = 5012320/30kalan = 0kar = 240
Şekil 7.11: Kesirli sırt çantası: başlangıçta boş çanta (üstte) ve dolmuş çanta (altta). Nesne 1 ve 2 tümüyle, nesne 3'ün 20/30'u alınmıştır; kalan 0, kâr 240.

Maliyetin baskın kısmı sıralamadır: O(n log n). Tarama en fazla n nesneye bakar ve O(n) sürer. Sıralama yerine her adımda en büyük oranlı nesne bir öbekten alınabilir; çanta erken dolarsa öbekten yalnız birkaç nesne çıkarılır ve toplam süre O(n + m log n)'ye iner (m, alınan nesne sayısıdır). Doğrusal zamanda çalışan, ortanca bulmaya dayanan bir yöntem de vardır.

Örnek 7.5

Kapasite W = 10, nesneler (kâr, ağırlık) olarak (10, 5), (40, 4), (30, 6), (50, 3) olsun. Oranlar sırasıyla 2, 10, 5 ve yaklaşık 16,7'dir. Oran sırası (50, 3), (40, 4), (30, 6), (10, 5) olur. (50, 3)'ün tamamı alınır: kalan 7, kâr 50. (40, 4)'ün tamamı alınır: kalan 3, kâr 90. (30, 6) sığmaz; 3/6'sı alınır ve 30 · 3/6 = 15 kâr eklenir. Toplam 105'tir. (10, 5) hiç alınmaz.

7.5.3Oran ölçütü neden en iyidir?

Teorem 7.3 (Kesirli sırt çantasında oran ölçütü)

Nesneleri oranlarına göre azalan sırayla, sığdığı kadarıyla alan açgözlü algoritma kesirli sırt çantası probleminin en iyi çözümünü bulur.

Nesneler k1/w1 ≥ k2/w2 ≥ … ≥ kn/wn olacak biçimde numaralansın. Toplam ağırlık W'den küçükse her şey alınır ve söylenecek bir şey yoktur; bu yüzden çantanın dolduğu varsayılır. Açgözlü çözüm G, bir en iyi çözüm O olsun. O farklıysa, iki çözümün ayrıldığı ilk nesne i olsun. Açgözlü yöntem i'den sığabildiği kadar aldığı için O, i'den daha az almıştır. O'nun da çantası dolu olduğundan, bu eksiklik i'den sonra gelen bir j nesnesinden fazladan alınarak kapatılmıştır. O'da j'nin δ kilogramı çıkarılıp yerine i'nin δ kilogramı konur. Ağırlık değişmez, çözüm uygundur. Kâr δ · (ki/wi − kj/wj) kadar değişir ve bu değer negatif değildir, çünkü i'nin oranı j'ninkinden küçük değildir. Değişiklik O'yu kârını düşürmeden G'ye bir adım yaklaştırır. Bu işlem sonlu kez tekrarlanınca O, G'ye dönüşür; demek ki G'nin kârı O'nunkinden az değildir. □

Kanıtın can alıcı adımı, ağırlığın küçük parçalar hâlinde değiştirilebilmesidir. δ kilogram bir nesneden çıkarılıp öbürüne eklenebilir, çünkü nesneler bölünebilir. En yüksek oranlı nesnenin her kilogramı, aynı ağırlıktaki başka her seçenekten en az onun kadar kâr getirir. Bu yüzden onu sonuna kadar almamak için bir neden yoktur.

7.5.40/1 sırt çantasında oran ölçütü

Nesneler bölünemezse kanıttaki değiştirme adımı çöker. Aynı üç nesne ve 50 kg'lık çanta ele alınsın, ama bu kez her nesne ya bütünüyle alınsın ya hiç alınmasın. Oran ölçütü nesne 1'i ve nesne 2'yi alır (30 kg, 160 kâr). Nesne 3 kalan 20 kg'a sığmaz ve atlanır. Sonuç 160'tır. Oysa nesne 2 ile nesne 3 birlikte tam 50 kg tutar ve 220 kâr getirir. Bütün seçimler karşılaştırılınca en iyisinin bu olduğu görülür. Tek nesneler 60, 100 ve 120 getirir. İkili seçimler 1 + 2 = 160, 1 + 3 = 180 ve 2 + 3 = 220 getirir. Üç nesne birden 60 kg tutar ve sığmaz.

Tablo 7.12: Aynı nesnelerle kesirli ve 0/1 sırt çantası (W = 50).
YöntemSeçimAğırlıkKâr
kesirli, oran ölçütü1, 2 ve 3'ün 20/30'u50240 (en iyi)
0/1, oran ölçütü1, 230160
0/1, en iyi2, 350220

0/1 durumunda oran ölçütü 20 kg'lık boşluğu dolduramaz. Kesirli durumda bu boşluk nesne 3'ün bir parçasıyla doldurulur. 0/1 durumunda ise parça alınamaz ve boşluk değersiz kalır. Değiştirme argümanı da işlemez: en iyi çözümdeki nesne 3 çıkarılıp yerine nesne 1 konursa ağırlık 30 kg'a düşer ama kâr 220'den 160'a iner. Ağırlıklar eşit olmadığı için değiştirme “aynı ağırlık, daha çok kâr” biçiminde yapılamaz. 0/1 sırt çantası dinamik programlamayla O(n · W) sürede çözülür.

Kesirli çözümün değeri 0/1 çözümünün değeri için bir üst sınırdır: 0/1 çözümü kesirli problemin de uygun bir çözümüdür, bu yüzden kesirli en iyinin değerini geçemez. Örnekte 220 ≤ 240'tır. Bu sınır, 0/1 sırt çantasını dal ve sınır yöntemiyle çözen algoritmalarda budama için kullanılır.

Sık yapılan hata

Kesirli problemde en iyi olan oran ölçütünü 0/1 sırt çantasına uygulamak. İki problemin girdisi aynıdır, yalnız “nesne bölünebilir mi?” sorusunun cevabı farklıdır. Bu küçük fark açgözlü seçim özelliğini ortadan kaldırır. Oran ölçütü 0/1 problemde ancak bir sezgisel olarak kullanılabilir.

7.6Minimum kapsayan ağaçta açgözlü seçim

Minimum kapsayan ağaç, açgözlü yaklaşımın en başarılı uygulamalarından biridir. Bağlı, ağırlıklı ve yönsüz bir çizgenin kapsayan ağacı, çizgenin bütün düğümlerini içeren, bağlı ve döngüsüz bir alt çizgesidir; V düğümlü bir çizgede tam olarak V − 1 kenarı vardır. Kenar ağırlıklarının toplamı en küçük olan kapsayan ağaca minimum kapsayan ağaç (minimum spanning tree, MST) denir. Bir şehirler ağını en az kablo ile bağlamak bu problemin tipik bir uygulamasıdır. Kruskal, Prim ve Borůvka algoritmaları MST'yi bulur ve üçü de açgözlüdür. Bu kesim algoritmaları baştan anlatmaz; üçünün açgözlü seçiminin neden güvenli olduğunu tek bir özellik üzerinden açıklar.

7.6.1Kesim özelliği

Bir çizgenin düğümleri iki boş olmayan kümeye ayrılsın: S ve V − S. Bu ayrıma bir kesim (cut) denir. Bir ucu S'de, öteki ucu V − S'de olan kenarlar kesimi geçer. Her kapsayan ağaç kesimi geçen en az bir kenar içermek zorundadır; yoksa S ile V − S arasında bağlantı kurulamaz.

Teorem 7.4 (Kesim özelliği)

Bağlı, ağırlıklı, yönsüz bir çizgenin herhangi bir kesimi alınsın. Kesimi geçen kenarlar arasında ağırlığı en küçük olan kenar e ise, e'yi içeren bir MST vardır. Kesimi geçen en hafif kenar tekse, bu kenar her MST'de bulunur.

Kanıt yine değiştirme argümanıdır. T, e = (u, v)'yi içermeyen bir MST olsun; u ∈ S, v ∈ V − S. T bir ağaç olduğu için u'dan v'ye tek bir yol vardır. Bu yol S'den başlayıp V − S'de bittiğine göre kesimi en az bir kez geçer; geçtiği kenar e' olsun. T'ye e eklenir ve e' çıkarılır. e' çıkarılınca ağaç iki parçaya ayrılır, e bu iki parçayı yeniden bağlar; sonuç yine bir kapsayan ağaçtır. Ağırlık w(e) − w(e') kadar değişir. e kesimi geçen en hafif kenar olduğu için bu fark sıfırdan büyük değildir. Yeni ağaç T'den ağır olmadığı için o da bir MST'dir ve e'yi içerir. e tek en hafif kenarsa fark kesin olarak negatiftir; bu durumda T MST olamaz ve e'yi içermeyen MST yoktur. □

43562794811pqrstuSV − S
Şekil 7.12: Kesim özelliği. Kesikli eğri düğümleri S = {p, q, r} ve V − S = {s, t, u} diye ayırır. Kesimi geçen kenarlar (mavi kesikli ve kırmızı) 9, 4, 8 ve 11'dir. En hafifi r–s (4) MST'dedir.

Şekil 7.12'deki çizgede kesimi dört kenar geçer: p–s (9), r–s (4), r–t (8) ve q–t (11). En hafifleri r–s'dir ve tek en hafif kenar olduğu için her MST'de bulunur. Çizgenin MST'si gerçekten t–u (2), p–r (3), p–q (4), r–s (4) ve s–u (6) kenarlarından oluşur; toplam ağırlık 19'dur. Kesimi geçen kenarlardan yalnız r–s ağaca girer. Bu tesadüf değildir: MST'nin S ile V − S'yi tam olarak bir kenarla bağlaması yeterlidir ve bu kenar en ucuzu olmalıdır.

Kesim özelliğinin açgözlü algoritmalar için anlamı şudur. Algoritma o ana kadar seçtiği kenarlarla bazı bileşenler kurmuştur. Bu bileşenlerden birini öbürlerinden ayıran bir kesim seçilir. Seçilmiş kenarların hiçbiri bu kesimi geçmez. Kesimi geçen en hafif kenar eklenirse, seçilmiş kenarlarla birlikte yine bir MST'nin parçası olarak kalınır. Kanıttaki değiştirme bu durumda da çalışır, çünkü çıkarılan e' kesimi geçer ve bu yüzden seçilmiş kenarlardan biri değildir. Algoritma bu yüzden hiçbir seçimini geri almak zorunda kalmaz.

Ağırlıkların hepsi farklıysa her kesimin en hafif kenarı tektir ve MST de tektir. Şekildeki çizgede p–q ve r–s'nin ikisinin de ağırlığı 4'tür, yani ağırlıklar tümüyle farklı değildir; yine de bu çizgenin MST'si tektir. Tekliğin koşulu yeterlidir, gerekli değildir. Eşit ağırlıklar birden çok MST'ye yol açabilir; ama bütün MST'lerin toplam ağırlığı aynıdır.

Örnek 7.6

Dört düğümlü bir çizgede düğümler {A, B} ve {C, D} olarak ayrılıyor; kesimi geçen kenarlar A–C 5, B–C 3 ve B–D 4'tür. Kesimi geçen en hafif kenar B–C'dir (3) ve tek en hafif kenardır; kesim özelliğine göre her MST'de bulunur. A–C ve B–D için kesim özelliği tek başına bir şey söylemez. Bunların MST'de olup olmadığı çizgenin geri kalan kenarlarına bağlıdır. Örneğin A–B ve C–D kenarları da varsa ve hafifse, MST kesimi yalnız B–C ile geçer.

Sık yapılan hata

Kesim özelliğini “en hafif kenarlar her zaman güvenlidir” diye okumak. Güvenli olan, bir kesimi geçen en hafif kenardır. Bütün çizgenin en hafif kenarları sırayla alınırsa bir döngü oluşabilir: döngünün üçüncü kenarı, ilk ikisinin zaten bağladığı iki düğümü yeniden bağlar ve hiçbir kesimi geçmez. Kruskal algoritması bu yüzden döngü kuran kenarı atar; atılan kenar, uçlarını ayıran hiçbir kesimin geçen kenarı değildir.

7.6.2Kruskal, Prim ve Borůvka: üç kesim

Üç MST algoritması aynı özelliği farklı kesimlerle kullanır. Fark, her adımda hangi kesimin seçildiğidir. Tablo 7.13 üç algoritmayı yan yana verir.

Tablo 7.13: Kruskal, Prim ve Borůvka'nın açgözlü seçimi.
KruskalPrimBorůvka
Açgözlü seçimtüm çizgedeki, döngü kurmayan en hafif 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
Kullandığı kesimkenarın uçlarını ayıran iki bileşenağaç ile geri kalan düğümlerher bileşen ile geri kalanı
Veri yapısısıralama ve ayrık kümeleröncelikli kuyrukayrık kümeler
ZamanO(E log V)O(E log V) (ikili öbek)O(E log V)
Uygun olduğuseyrek çizge, kenar listesiyoğun çizge, komşuluk yapısıparalel ya da dağıtık hesaplama

Kruskal kenarları artan ağırlık sırasıyla inceler. Sıradaki kenar iki farklı bileşeni bağlıyorsa eklenir, aynı bileşenin iki düğümünü bağlıyorsa döngü kuracağı için atlanır. Eklenen kenarın kullandığı kesim, uçlarından birinin bileşeni ile geri kalan düğümler arasındaki kesimdir. Bu kesimi geçen daha hafif bir kenar olsaydı daha önce incelenir ve eklenirdi; demek ki eklenen kenar kesimin en hafifidir. Bileşen denetimi ayrık kümeler yapısıyla yapılır. Süreyi sıralama belirler: O(E log E). E ≤ V2 olduğu için log E ≤ 2 log V'dir ve bu süre O(E log V) diye de yazılır.

Prim tek bir ağacı bir düğümden başlayarak büyütür. Her adımda ağaçtaki bir düğümü ağaç dışındaki bir düğüme bağlayan en hafif kenar eklenir. Kullanılan kesim, ağacın düğümleri ile geri kalan düğümler arasındaki kesimdir. Ağaç dışındaki her düğümün ağaca en ucuz bağlantısı bir öncelikli kuyrukta tutulur. İkili öbekle süre O((V + E) log V)'dir; bağlı çizgede bu O(E log V)'ye eşittir. Yoğun çizgelerde dizi taramalı bir sürüm O(V2) ile daha hızlı olabilir.

Borůvka turlar hâlinde çalışır. Her turda her bileşen kendinden çıkan en hafif kenarı bulur ve bu kenarların hepsi birlikte eklenir. Her bileşen kendi kesimini, yani “bu bileşen ile geri kalan her şey” kesimini kullanır. Her tur bileşen sayısını en az yarıya indirdiği için en fazla ⌈log2 V⌉ tur vardır ve her tur O(E) sürer; toplam O(E log V)'dir. Bileşenlerin seçimleri birbirinden bağımsız olduğu için algoritma paralel çalışmaya uygundur. Ağırlıklar eşit olabiliyorsa tutarlı bir eşitlik bozucu gerekir; yoksa iki bileşen eşit ağırlıklı farklı kenarlar seçip bir döngü kurabilir.

Üç algoritma aynı çizgede aynı MST'yi bulur, yeter ki ağırlıklar farklı olsun. Ağırlıklar farklıysa MST tektir ve hepsinin bulduğu ağaç odur. Eşit ağırlıklar varsa farklı ağaçlar bulabilirler, ama toplam ağırlıkları aynıdır. Farkları kenarları ele alma sırasında ve gereken veri yapısındadır. Tüm kenarların ağırlığı aynı olduğunda örneğin her kapsayan ağaç bir MST'dir; Kruskal bu durumda kenar listesinin sırasına göre herhangi bir kapsayan ağacı üretir.

Bir kenarın ağırlığı tek başına onun MST'de olup olmayacağını söylemez. Her MST'de bulunan kenarlar, bir kesimin tek en hafif kenarı olanlardır. Hiçbir MST'de bulunmayan kenarlar ise bir döngünün tek en ağır kenarı olanlardır; bu ikinci kurala döngü özelliği denir ve ters silme algoritması ona dayanır. Kenarlara aynı sabitin eklenmesi MST'yi değiştirmez, çünkü kenarların sırası korunur ve bütün kapsayan ağaçlarda V − 1 kenar olduğundan bütün toplamlar aynı miktarda artar. Kenarlar azalan sırada işlenirse Kruskal aynı biçimde maksimum kapsayan ağacı bulur.

7.7Huffman kodlama

Bir metin bilgisayarda bitlerle saklanır. Her karaktere bir bit dizisi, yani bir kod sözcüğü atanır ve metin bu kodların art arda yazılmasıyla oluşur. ASCII gibi kodlamalarda her karakterin kodu aynı uzunluktadır. Ancak gerçek metinlerde karakterler eşit sıklıkta geçmez. Türkçe bir metinde “a” ve “e” harfleri çok sık, “ğ” ve “j” çok seyrek görülür. Sık karakterlere kısa, seyrek karakterlere uzun kodlar verilirse metnin toplam uzunluğu kısalır. Huffman kodlama bu fikri en iyi biçimde uygulayan açgözlü bir algoritmadır.

7.7.1Sabit ve değişken uzunluklu kodlar

Huffman kodlama kayıpsız bir veri sıkıştırma yöntemidir: sıkıştırılmış veri çözüldüğünde özgün veri bit bit aynen geri gelir. Yöntemi David Huffman 1952'de, öğrenciyken bir dönem ödevi olarak geliştirmiştir. Sunumdaki örnek metin 25 karakter uzunluğundadır ve yalnız altı farklı karakter içerir: F, O, R, G, E ve T. Altı karakteri birbirinden ayırmak için sabit uzunluklu bir kodda ⌈log2 6⌉ = 3 bit gerekir; 2 bit yalnız dört karakteri ayırabilir. Bu durumda metin 25 × 3 = 75 bit tutar. Her karakter 8 bitlik ASCII ile saklansaydı 200 bit tutardı.

Değişken uzunluklu bir kodda kod sözcüklerinin uzunlukları farklıdır. Metnin toplam uzunluğu her karakterin frekansı ile kod uzunluğunun çarpımlarının toplamıdır:

Toplam bit = Σ frekans(c) · uzunluk(c)

Bu toplamı küçültmek için en sık karakterlerin kodları kısa tutulmalıdır. Ancak kodlar keyfî biçimde kısaltılamaz; kısaltılmış kodların hâlâ tek anlamlı çözülebilmesi gerekir.

7.7.2Önek-özgür kodlar

Kodlar a = 0, b = 01, c = 1 olsun. “01” bit dizisi alındığında bunun “ab” mi yoksa yalnız “b” mi olduğu anlaşılamaz. Sorunun kaynağı, a'nın kodu 0'ın, b'nin kodu 01'in başında yer almasıdır. Kod sözcükleri arasına bir ayırıcı konarak bu sorun çözülebilir, ama ayırıcı da bit harcar. Daha iyi bir çözüm, hiçbir kodun bir başka kodun başlangıcı olmamasıdır.

Tanım 7.8

Hiçbir kod sözcüğünün bir başka kod sözcüğünün öneki olmadığı koda önek-özgür kod (prefix-free code) denir. Önek-özgür bir kodla yazılmış bit dizisi, ayırıcı kullanmadan, soldan sağa okunarak tek anlamlı çözülür.

Önek-özgür kodlar ikili ağaçlarla gösterilir. Kökten başlanır; sol kenar 0, sağ kenar 1 anlamına gelir. Her karakter bir yapraktadır ve kökten o yaprağa giden yoldaki bitler karakterin kodudur. Bir karakter iç düğümde dursaydı, onun kodu altındaki yaprakların kodlarının öneki olurdu. Karakterler yalnız yapraklarda durduğu için hiçbir kod bir başkasının öneki olamaz. Bir kodun uzunluğu, karakterin ağaçtaki derinliğidir.

011acb✗ a, b'nin yolunda0101cab✓ hepsi yaprak: 00, 01, 1
Şekil 7.13: Solda a bir iç düğümdedir; kodu 0, b'nin kodu 01'in önekidir ve çözme belirsizdir. Sağda bütün karakterler yapraktadır; kodlar 00, 01 ve 1 önek-özgürdür.

Çözme işlemi ağaç üzerinde yapılır. Kökten başlanır; her bit okunduğunda 0 ise sol, 1 ise sağ çocuğa inilir. Bir yaprağa varılınca o yapraktaki karakter yazılır ve köke dönülür. Bir kod sözcüğünün bittiği an, yaprağa varıldığı andır ve bu an kesin olarak bilinir. Örneğin Şekil 7.13'ün sağındaki ağaçla “00110” dizisi 00 | 1 | 1 | 0… diye okunur: ilk iki bit a'yı, sonraki bit c'yi, bir sonraki yine c'yi verir. Son 0 tek başına bir yaprağa ulaşmaz; dizi eksik gelmiştir.

Optimal bir önek-özgür kodun ağacı tam bir ikili ağaçtır: her iç düğümün iki çocuğu vardır. Tek çocuklu bir iç düğüm olsaydı, o düğüm atlanıp çocuğu yukarı taşınabilirdi; altındaki bütün kodlar bir bit kısalırdı ve kodlar yine önek-özgür kalırdı. Şekildeki sol ağaçta kökün sol çocuğu olan a düğümünün tek çocuğu vardır; sağ ağaç bu yüzden daha iyidir.

Örnek 7.7

Dört kod kümesi ele alınsın. {0, 10, 110, 111} önek-özgürdür: 0 ile başlayan tek kod 0'dır, 10 ile başlayan tek kod 10'dur, 110 ve 111 birbirinin öneki değildir. Bu küme dört yapraklı, sağa doğru uzayan bir ağaca karşılık gelir. {0, 01, 110, 111} önek-özgür değildir; 0, 01'in önekidir. {1, 10, 00, 01} önek-özgür değildir; 1, 10'un önekidir. {00, 01, 0, 11} önek-özgür değildir; 0 hem 00'ın hem 01'in önekidir.

7.7.3Algoritmanın adımları

Huffman algoritması ağacı aşağıdan yukarıya kurar. Her karakter başlangıçta kendi frekansıyla tek düğümlü bir ağaçtır. Her adımda frekansı en küçük iki ağaç birleştirilir: ikisi yeni bir iç düğümün çocukları olur ve yeni düğümün frekansı çocuklarının frekanslarının toplamıdır. Tek ağaç kalınca o ağaç Huffman ağacıdır. Adımlar şöyle sıralanır:

  1. Her karakterin frekansı hesaplanır.
  2. Her karakter için bir yaprak oluşturulur. En küçük frekanslı iki düğüm birleştirilerek yeni bir iç düğüm yapılır; bu adım tek düğüm kalana kadar tekrarlanır.
  3. Ağaçta her karakterin kodu, kökten yaprağa giden yoldan okunur: sol kenar 0, sağ kenar 1.
  4. Metin bu kodlarla kodlanır.

Açgözlü seçim ikinci adımdadır: her birleştirmede o an en küçük frekanslı iki ağaç seçilir. Bu iki ağacın yaprakları birleştirmeden sonra bir seviye derine iner, yani kodları bir bit uzar. En seyrek karakterlerin kodlarını uzatmak, toplam bit sayısını en az artıran seçimdir. Sunumdaki örneğin frekansları Tablo 7.14'te verilmiştir.

Tablo 7.14: Örnek metindeki karakter frekansları (toplam 25 karakter).
KarakterFORGET
Frekans234457

n karakter için tam olarak n − 1 birleştirme yapılır, çünkü her birleştirme ağaç sayısını bir azaltır ve n ağaçtan bir ağaca inilir. Bu yüzden Huffman ağacında n yaprak ve n − 1 iç düğüm vardır. Örnekte altı karakter, beş birleştirme vardır.

7.7.4Ağacı kurmak: kod ve izleme

Sunumdaki huffman metodu ağacı bir öncelikli kuyrukla kurar:

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();
}

Dugum sınıfının dört alanı vardır: karakter h, frekans f, sol ve sağ çocuk. Yapraklarda çocuklar null'dır. Kuyruk, (a, b) -> a.f - b.f karşılaştırıcısıyla kurulan bir PriorityQueue'dur. Java'nın bu sınıfı bir en küçük öbektir (min-heap); poll() her çağrıldığında frekansı en küçük düğümü çıkarır. İlk döngü her karakter için bir yaprak oluşturup kuyruğa ekler. while döngüsü kuyrukta birden çok düğüm olduğu sürece döner. Her turda iki kez poll() çağrılır; ilk çıkan sol, ikinci çıkan sağ çocuk olur. Yeni iç düğümün karakteri '\0''dır, çünkü iç düğümler bir karakter taşımaz; frekansı iki çocuğun toplamıdır. Yeni düğüm kuyruğa geri eklenir. Döngü bitince kuyrukta tek düğüm kalır ve bu düğüm ağacın köküdür.

Tablo 7.15: huffman(h, f) izlemesi. Parantez içindeki sayılar iç düğümlerdir.
TurSol (1. poll)Sağ (2. poll)Yeni düğümTur sonunda kuyruk
başlangıç———F2, O3, R4, G4, E5, T7
1F2O3(5) = 2 + 3R4, G4, E5, (5), T7
2G4R4(8) = 4 + 4E5, (5), T7, (8)
3(5)E5(10) = 5 + 5T7, (8), (10)
4T7(8)(15) = 7 + 8(10), (15)
5(10)(15)(25) = 10 + 15(25)

Tablodaki kuyruk içerikleri frekans sırasıyla yazılmıştır; öbeğin dizideki gerçek düzeni farklıdır. İkinci ve üçüncü turlarda eşitlik vardır. İkinci turda G4 ile R4'ün, üçüncü turda (5) ile E5'in frekansı aynıdır. Hangisinin önce çıkacağı öbeğin iç düzenine, dolayısıyla karakterlerin kuyruğa eklenme sırasına bağlıdır. Tablo sunumdaki izlemeyi izler. Karakterler F, O, R, G, E, T sırasıyla eklendiğinde Java'nın PriorityQueue'su üçüncü turda önce E5'i çıkarır ve E sol çocuk olur. Bu durumda kodlar E = 00, F = 010, O = 011 olur; geri kalan kodlar değişmez. Kod uzunlukları ve toplam bit sayısı iki durumda da aynıdır: E 2 bit, F ve O 3 bit.

F2O3E5T7G4R458
Şekil 7.14: Üçüncü turun başı: (5) ve (8) iç düğümleri kurulmuştur. Sıradaki en küçük iki düğüm (5) ve E5'tir (vurgulu).
0101010101F2O3E5T7G4R458101525
Şekil 7.15: Tamamlanmış Huffman ağacı. Kök 25'tir; sol kenarlar 0, sağ kenarlar 1 ile etiketlidir.

Ağacın yapısı tablodan okunabilir. Kökün sol çocuğu (10), sağ çocuğu (15)'tir. (10)'un çocukları (5) ve E, (15)'in çocukları T ve (8)'dir. (5)'in altında F ve O, (8)'in altında G ve R vardır. E ve T derinlik 2'de, F, O, G ve R derinlik 3'tedir. En sık iki karakter, en kısa kodları almıştır.

Ağacın toplam bit maliyeti iç düğümlerin frekanslarının toplamına da eşittir: 5 + 8 + 10 + 15 + 25 = 63. Bunun nedeni şudur: bir karakter, kökten yaprağına giden yoldaki her iç düğümün frekansına bir kez katkı yapar. Yoldaki iç düğüm sayısı karakterin derinliğidir. Böylece her karakterin frekansı derinliği kadar sayılmış olur ve toplam Σ frekans · derinlik'e eşit çıkar. Bu gözlem, Huffman'ın her birleştirmede neden en küçük iki frekansı seçtiğini de açıklar: her birleştirmenin maliyeti yeni iç düğümün frekansıdır ve algoritma her adımda bu maliyeti en küçük tutar.

Maliyet şöyledir. n yaprağın kuyruğa eklenmesi O(n log n) sürer. Döngü n − 1 kez döner ve her turda iki poll ile bir add yapar; her biri O(log n)'dir. Toplam O(n log n)'dir. Burada n farklı karakter sayısıdır, metnin uzunluğu değildir. Frekanslar baştan sıralıysa iki sıradan kuyruk ile O(n) sürede çalışan bir sürüm de yazılabilir: birinci kuyruk sıralı yaprakları, ikinci kuyruk sırayla oluşan iç düğümleri tutar. Oluşan iç düğümlerin frekansları artan sırada geldiği için ikinci kuyruk kendiliğinden sıralı kalır.

Örnek 7.8

Altı karakterin frekansları a:5, b:9, c:12, d:13, e:16, f:45 olsun (toplam 100). Birleştirmeler sırasıyla şöyledir: a5 + b9 = 14; c12 + d13 = 25; (14) + e16 = 30; (25) + (30) = 55; f45 + (55) = 100. Kök 100'ün sol çocuğu f, sağ çocuğu (55)'tir. Kodlar f = 0, c = 100, d = 101, a = 1100, b = 1101, e = 111 olur. Toplam bit 45 · 1 + 12 · 3 + 13 · 3 + 16 · 3 + 5 · 4 + 9 · 4 = 224'tür. İç düğümlerin toplamı da aynı sonucu verir: 14 + 25 + 30 + 55 + 100 = 224. Sabit 3 bitlik kodla metin 300 bit tutardı; kazanç yaklaşık %25'tir. Bu örnekte f çok baskın olduğu için tek bitlik bir kod alır.

7.7.5Kodları üretmek: kod ve izleme

Ağaç kurulduktan sonra her karakterin kodu ağacın bir kez gezilmesiyle bulunur. Sunumdaki özyinelemeli kodUret metodu bunu yapar:

static void kodUret(Dugum d, String kod) {
  if (d.sol == null) {      // yaprak
    System.out.println(d.h + " = " + kod);
    return;
  }
  kodUret(d.sol, kod + "0"); // sol: 0
  kodUret(d.sag, kod + "1"); // sağ: 1
}

Metot bir düğüm ve o düğüme gelene kadar biriken bit dizisini alır. Düğüm bir yapraksa, yani sol çocuğu null'sa, karakter ve kodu yazdırılır. Huffman ağacı tam bir ikili ağaç olduğu için yalnız sol çocuğa bakmak yeterlidir: iç düğümlerin iki çocuğu da vardır, yaprakların hiç çocuğu yoktur. Düğüm iç düğümse metot önce sol çocuğa koda “0” ekleyerek, sonra sağ çocuğa “1” ekleyerek iner. Bu bir önce-sıra (preorder) gezintisidir; yapraklar soldan sağa sırayla yazdırılır. İlk çağrı kodUret(kok, "") biçimindedir.

Tablo 7.16: kodUret(kok, "") izlemesi (çağrı sırasıyla).
ÇağrıDüğümkodNe olur?
1(25), kök""iç düğüm; sola "0" ile inilir
2(10)"0"iç düğüm; sola "00" ile inilir
3(5)"00"iç düğüm; sola "000" ile inilir
4F"000"yaprak: F = 000 yazılır
5O"001"yaprak: O = 001 yazılır
6E"01"yaprak: E = 01 yazılır
7(15)"1"iç düğüm; sola "10" ile inilir
8T"10"yaprak: T = 10 yazılır
9(8)"11"iç düğüm; sola "110" ile inilir
10G"110"yaprak: G = 110 yazılır
11R"111"yaprak: R = 111 yazılır
01010101012510155E01T108F000O001G110R111
Şekil 7.16: Kod üretiminin sonu. Her yaprağın altında kodu yazılıdır; vurgulu yol son yazılan R = 111'in yoludur.

Metot toplam 11 kez çağrılır: 6 yaprak ve 5 iç düğüm için birer kez. Her düğüm bir kez ziyaret edildiği için gezinti O(n) düğüm ziyareti yapar. Özyineleme derinliği ağacın yüksekliği kadardır; örnekte kökten en derin yaprağa 3 kenar vardır. Dizgi birleştirme kod + "0" her çağrıda yeni bir dizgi oluşturur ve uzunluğu kadar zaman alır. Bu maliyet de sayılırsa toplam iş, bütün kodların uzunlukları toplamıyla orantılıdır. Kodlar bir StringBuilder ile ya da bir karakter dizisinde tutulursa bu ek maliyet önlenir.

Sık yapılan hata

Metinde tek bir farklı karakter varsa (örneğin “AAAA”) Huffman ağacı tek düğümden oluşur: kök aynı zamanda yapraktır. kodUret(kok, "") bu durumda karakterin kodu olarak boş dizgiyi yazar ve metin 0 bitle kodlanmış gibi görünür. Çözücü kaç karakter olduğunu bilemez. Bu özel durumda karaktere en az 1 bitlik bir kod (örneğin “0”) verilmelidir.

7.7.6Kodlama, çözme ve kazanç

Tablo 7.17 her karakterin kodunu ve metindeki toplam katkısını verir.

Tablo 7.17: Huffman kodları ve bit hesabı.
KarakterFrekansKodUzunlukBit (frekans · uzunluk)
F200036
O300139
E501210
T710214
G4110312
R4111312
Toplam2563

Huffman kodu metni 63 bitle yazar; sabit 3 bitlik kod 75 bit kullanır. Kazanç (75 − 63) / 75 = %16'dır. Karakter başına ortalama 63 / 25 = 2,52 bit düşer. Kazanç, frekansların ne kadar dengesiz olduğuna bağlıdır. Bütün karakterler eşit sıklıkta geçseydi kazanç çok az olurdu; a–f örneğindeki gibi bir karakter çok baskınsa kazanç büyür.

Kodlama için her karakterin kodu sırayla yazılır. “FORTE” sözcüğü 000 | 001 | 111 | 10 | 01 olur, yani 13 bit. Sabit kodla aynı sözcük 5 × 3 = 15 bit tutardı. Kodlama sırasında bitler arasına ayırıcı konmaz; buradaki dikey çizgiler yalnız okumayı kolaylaştırmak içindir.

Çözme için ağaç kullanılır. “1100110” bit dizisi alınsın. Kökten başlanır. 1 ile sağa, (15)'e; 1 ile sağa, (8)'e; 0 ile sola, G'ye inilir. G yazılır ve köke dönülür. 0 ile sola, (10)'a; 1 ile sağa, E'ye inilir. E yazılır, köke dönülür. 1 ile sağa, (15)'e; 0 ile sola, T'ye inilir. T yazılır. Sonuç 110 | 01 | 10, yani “GET”'tir. Çözme her bit için bir kenar iner; m bitlik bir dizi O(m) sürede çözülür.

Sol ve sağ çocukların yeri keyfîdir. Örneğin T, (15)'in sağ çocuğu yapılabilir. Bu durumda kodlar değişir, ama kod uzunlukları ve toplam 63 bit değişmez. Aynı nedenle eşit frekanslı düğümlerin hangisinin önce seçildiği de kod uzunluklarını değiştirebilir, ama toplam bit sayısını değiştirmez. Bir metnin birden çok en iyi önek-özgür kodu olabilir.

Sıkıştırılmış veri tek başına çözülemez; aynı ağaç ya da kod tablosu çözücünün elinde de bulunmalıdır. Bu yüzden ağaç (ya da her karakterin kod uzunluğu) sıkıştırılmış veriyle birlikte gönderilir. Kısa metinlerde bu ek bilgi kazancın tamamını yiyebilir. 25 karakterlik örnekte altı karakterin kod tablosu, kazanılan 12 bitten çok daha fazla yer tutar. Huffman kodlama bu yüzden uzun metinlerde ve büyük dosyalarda anlamlıdır.

Bir kodun ne kadar iyi olabileceğinin teorik bir alt sınırı da vardır. Karakter c'nin metindeki oranı pc ise karakter başına ortalama bit sayısı H = −Σ pc log2 pc değerinin altına inemez. Bu değere metnin entropisi denir. Örnek metnin entropisi yaklaşık 2,48 bittir; Huffman kodu 2,52 bit kullanır. Huffman kodunun ortalaması her zaman entropi ile entropi artı bir bit arasındadır.

Sık yapılan hata

Huffman ile sıkıştırılmış veriyi ağaç ya da kod tablosu olmadan çözmeye çalışmak. Bit dizisi hangi karakterin hangi kodla yazıldığını içermez. Kodlayıcı ile çözücü aynı ağacı kullanmalıdır. Sol ve sağ çocukların yeri ya da eşitliklerin bozulma biçimi farklı olan iki ağaç, aynı frekanslardan farklı kodlar üretir.

7.7.7Huffman kodu neden en iyidir?

Bir önek-özgür kod ağacı T'nin maliyeti B(T) = Σ f(c) · dT(c)'dir; f(c) karakterin frekansı, dT(c) ağaçtaki derinliğidir. Huffman'ın kurduğu ağacın bu maliyeti en küçük yaptığı iki adımda gösterilir: açgözlü seçim ve optimal alt yapı.

Teorem 7.5 (Huffman'da açgözlü seçim)

Frekansı en küçük iki karakter x ve y olsun. x ile y'nin en derin seviyede kardeş yapraklar olduğu bir en iyi kod ağacı vardır.

T bir en iyi ağaç olsun. En iyi ağaç tam bir ikili ağaçtır; bu yüzden en derin seviyede birbirinin kardeşi olan iki yaprak bulunur. Bunlar a ve b olsun. x ile a'nın yerleri değiştirilsin. Maliyet şu kadar değişir:

B(T') − B(T) = f(x) · d(a) + f(a) · d(x) − f(x) · d(x) − f(a) · d(a) = −(f(a) − f(x)) · (d(a) − d(x))

x en küçük frekanslı karakter olduğu için f(a) − f(x) ≥ 0'dır. a en derinde olduğu için d(a) − d(x) ≥ 0'dır. Çarpım negatif değildir, önündeki eksi işaretiyle fark sıfırdan büyük olamaz: B(T') ≤ B(T). Aynı değiştirme y ile b için yapılır. Sonuçta x ve y en derin seviyede kardeştir ve maliyet artmamıştır; ağaç yine en iyidir. □

xbpq⇄pbxqx en az frekanslı ama sığx en derine: toplam bit artmaz
Şekil 7.17: Değiştirme argümanı. Solda en düşük frekanslı x sığ bir yerdedir, p en derindedir. Sağda ikisinin yeri değiştirilmiştir; x en derine iner ve toplam bit artmaz.
Teorem 7.6 (Huffman'da optimal alt yapı)

x ve y, frekansı f(z) = f(x) + f(y) olan yeni bir z karakteriyle değiştirilsin ve küçülen alfabe için bir en iyi ağaç T' olsun. T''de z yaprağının yerine, çocukları x ve y olan bir iç düğüm konursa asıl alfabe için bir en iyi ağaç T elde edilir.

x ve y, z'den bir seviye derindedir. Bu yüzden B(T) = B(T') − f(z) · d(z) + (f(x) + f(y)) · (d(z) + 1) = B(T') + f(x) + f(y) olur. T'nin en iyi olmadığı varsayılsın: maliyeti daha küçük bir T'' vardır. Önceki teoreme göre T'''de x ve y'nin kardeş olduğu varsayılabilir. T'''de bu iki yaprak silinip ebeveynleri z yaprağı yapılırsa küçülen alfabe için maliyeti B(T'') − f(x) − f(y) < B(T') olan bir ağaç çıkar. Bu, T''nin en iyi olmasıyla çelişir. □

İki teorem birlikte tümevarımla Huffman algoritmasının doğruluğunu verir. Algoritma ilk adımda en küçük iki frekansı birleştirir; açgözlü seçim teoremi bu seçimi içeren bir en iyi ağaç olduğunu söyler. Birleştirilen iki karakter tek bir z karakteri gibi davranır ve problem bir karakter küçülür. Optimal alt yapı teoremi, küçük problemin en iyi ağacından asıl problemin en iyi ağacının elde edildiğini söyler. Böylece Huffman kodu, önek-özgür kodlar arasında Σ frekans · derinlik toplamını en küçük yapan koddur.

7.7.8Maliyet ve kullanım alanları

Huffman kodlamanın bütün aşamalarının maliyeti şöyledir. m karakterlik bir metinde n farklı karakter olsun. Frekans sayımı metni bir kez okur: O(m). Ağacın kurulması O(n log n), kodların üretilmesi O(n) düğüm ziyareti sürer. Kodlama metni bir kez daha okur ve her karakterin kodunu tablodan alır: O(m) karakter ve çıktının bit sayısı kadar iş. Genellikle n, m'den çok küçüktür (örneğin bir baytlık karakterlerde n ≤ 256); bu yüzden toplam süre pratikte metnin uzunluğuyla doğru orantılıdır.

Huffman kodlama tek başına değil, çoğu zaman başka yöntemlerle birlikte kullanılır. ZIP ve gzip dosyalarının ve PNG resimlerinin kullandığı DEFLATE biçimi, önce tekrar eden dizgileri geriye dönük göstericilerle kısaltır, sonra çıkan sembolleri Huffman ile kodlar. JPEG resim ve MP3 ses biçimlerinde de veri önce dönüştürülür ve nicelenir; son aşamadaki entropi kodlaması Huffman kodlarıyla yapılır. Dizgi Sıkıştırma simülatörü RLE ve LZW yöntemlerini adım adım gösterir. LZW sözlük tabanlıdır: tekrar eden dizgilere kod verir. Huffman ise frekans tabanlıdır: tek tek karakterlere frekanslarına göre kod verir. İki yaklaşımın farkı simülatördeki örneklerle Huffman'ın sonuçları karşılaştırılarak görülebilir.

7.8Diğer açgözlü algoritmalar

Açgözlü fikir birçok başka algoritmanın da temelindedir. Bunların bir kısmı kanıtlanmış olarak en iyi sonucu verir. Bir kısmı ise en iyi sonucu garanti etmez, ama sonucun en iyiden ne kadar uzak olabileceği bilinir.

Dijkstra ve Prim. İki algoritma da her adımda bir öncelikli kuyruktan en küçük anahtarlı düğümü alır ve onu kalıcı olarak seçer. Dijkstra'da anahtar, kaynaktan o düğüme bilinen en kısa uzaklıktır; Prim'de, düğümü ağaca bağlayan en ucuz kenarın ağırlığıdır. Seçilen düğüm bir daha değiştirilmez. Dijkstra negatif ağırlıklı kenar bulunmayan çizgelerde en kısa yolları doğru bulur; negatif kenar varsa kalıcı sayılan bir uzaklık sonradan kısalabilir ve açgözlü seçim yanılır. Prim ise kesim özelliğine dayandığı için ağırlıkların işaretinden bağımsız olarak doğrudur.

Çizge boyama. Komşu düğümlerin farklı renk alması koşuluyla bir çizgeyi en az renkle boyamak NP-zor bir problemdir. Açgözlü boyama düğümleri belli bir sırayla ele alır ve her düğüme komşularının kullanmadığı en küçük rengi verir. Hızlıdır, ama kullanılan renk sayısı sıraya bağlıdır. Altı düğümlü bir döngü düşünülsün: a1 – b2 – a3 – b1 – a2 – b3 – a1. Bu döngü iki renkle boyanabilir: a'lar bir renk, b'ler öbür renk. Düğümler a1, a2, a3, b1, b2, b3 sırasıyla boyanırsa gerçekten iki renk yeter. Ancak sıra a1, b1, a2, b2, a3, b3 olursa a1 ve b1 komşu olmadıkları için ikisi de 1 rengini alır. a2, b1'e komşu olduğu için 2'yi, b2, a1'e komşu olduğu için yine 2'yi alır. a3 hem b1'e (1) hem b2'ye (2) komşudur ve 3'ü alır; b3 de aynı nedenle 3'ü alır. Kötü bir sıra bir renk fazla kullandırmıştır. Açgözlü boyama en fazla “en büyük derece artı bir” renk kullanır; bu sınır her zaman sağlanır.

Kutulara yerleştirme (bin packing). Boyutları verilen nesneler, kapasitesi sabit olan en az sayıda kutuya yerleştirilmek istenir. Bu problem de NP-zordur. İlk sığan kuralı her nesneyi sığdığı ilk kutuya koyar; hiçbirine sığmıyorsa yeni bir kutu açar. En iyi sığan kuralı ise nesneyi, koyulduğunda en az boşluk bırakan kutuya koyar. Kapasite 10 ve nesneler 2, 5, 4, 7, 1, 3, 8 olsun. İlk sığan bu sırayla dört kutu açar: [2, 5, 1], [4, 3], [7], [8]. Nesneler önce büyükten küçüğe sıralanırsa (azalan sırayla ilk sığan) üç kutu yeter: [8, 2], [7, 3], [5, 4, 1]. Toplam boyut 30 olduğu için üçten az kutu mümkün değildir. Azalan sırayla ilk sığanın en fazla yaklaşık 11/9 · OPT + 1 kutu kullandığı kanıtlanmıştır; burada OPT en az kutu sayısıdır.

En büyük kapsama (maximum coverage). Bir öğeler evreni ve bu öğelerin alt kümeleri verilir; en fazla k küme seçilerek olabildiğince çok öğe kapsanmak istenir. Açgözlü algoritma her adımda henüz kapsanmamış öğelerden en çoğunu kapsayan kümeyi seçer. Bu yöntemin en iyinin en az 1 − 1/e ≈ 0,632'si kadar öğe kapsadığı, yani en iyinin yaklaşık %63'üne ulaştığı kanıtlanmıştır. Daha iyi bir oranı polinom sürede garanti eden bir algoritma, P = NP olmadıkça yoktur.

Bu son örnekler, açgözlü algoritmaların NP-zor problemlerdeki rolünü gösterir. En iyi çözümü bulmak çok pahalıdır, ama açgözlü bir algoritma hızlıca bir çözüm üretir ve bu çözümün kalitesi için kanıtlanmış bir alt sınır verir. Böyle algoritmalara yaklaşım algoritması denir; çözümün en iyiye en kötü durumdaki oranına da yaklaşım oranı denir.

7.9Karşılaştırma ve sık yapılan hatalar

Tablo 7.18 notta incelenen problemleri açgözlü ölçüt, süre ve en iyilik bakımından karşılaştırır.

Tablo 7.18: Açgözlü algoritmaların karşılaştırması.
ProblemAçgözlü ölçütZamanEn iyi mi?
Para üstüsığan en büyük paraO(k + adet)yalnız kanonik sistemlerde
Etkinlik seçimien erken bitenO(n log n)evet
İş sıralamaen kârlı iş, en geç boş dilimO(n2)evet
Kesirli sırt çantasıen büyük kâr/ağırlık oranıO(n log n)evet (0/1 sürümde hayır)
MSTkesimi geçen en hafif kenar (Kruskal, Prim, Borůvka)O(E log V)evet
Huffmanen düşük frekanslı iki ağaçO(n log n)evet

Tablonun son sütunu, açgözlü yaklaşımın en önemli dersini özetler. Dört problemde açgözlü algoritma en iyidir ve bu, birer değiştirme argümanıyla kanıtlanmıştır. Para üstünde sonuç para sistemine bağlıdır. 0/1 sırt çantasında ise oran ölçütü en iyiyi kaçırır. İş sıralamadaki O(n2), sunumdaki doğrudan dilim aramasının en kötü durumudur; ayrık kümelerle bu da O(n log n)'ye iner.

Notun farklı yerlerinde ayrıntılı ele alınan hatalar şöyle toplanabilir:

  • Açgözlüyü kanıtsız doğru saymak. {1, 3, 4} paralarıyla 6 tutarında açgözlü 3 para, en iyi çözüm 2 para kullanır. Her problemde açgözlü seçim özelliği gösterilmelidir; birkaç başarılı deneme kanıt değildir.
  • Yanlış ölçüt. Etkinlik seçiminde en erken başlayan, en kısa süren ya da en az çakışan etkinliği seçmek en iyi sonucu garanti etmez. Doğru ölçüt en erken bitendir.
  • 0/1 sırt çantasında oran ölçütü. Kesirli problemde en iyi olan oran ölçütü, nesneler bölünemediğinde en iyiyi garanti etmez.
  • En erken boş dilim. İş sıralamada işi son tarihinden önceki en erken boş dilime koymak, son tarihi yakın işlere yer bırakmaz. En geç boş dilim seçilmelidir.
  • Kesim özelliğini yanlış okumak. Güvenli olan, bir kesimi geçen en hafif kenardır. Tüm çizgedeki en hafif kenarlar körü körüne alınırsa döngü oluşabilir; Kruskal bu yüzden döngü kuran kenarı atar.
  • Huffman'da ağaçsız çözme. Kod tablosu ya da ağaç gönderilmezse veri çözülemez. Tek karakterli metinde kök bir yapraktır; koda en az 1 bit verilmelidir.

7.10Özet

  • Açgözlü algoritma: çözümü adım adım kurar, her adımda bir ölçüte göre o an en iyi görünen seçimi yapar ve kararlarını geri almaz. Tipik yapı sıralama ve tek geçiştir; süre çoğunlukla O(n log n)'dir.
  • Doğruluk koşulları: açgözlü seçim özelliğine göre ilk seçimi içeren bir en iyi çözüm vardır. Optimal alt yapıya göre kalan problemin en iyi çözümü, ilk seçimle birlikte bütünün en iyi çözümünü verir. İki özellik birlikte sağlanırsa açgözlü algoritma en iyidir; sağlanmazsa yalnız bir sezgiseldir.
  • Değiştirme argümanı: keyfi bir en iyi çözümün bir parçası açgözlü seçimle değiştirilir; çözümün uygun kaldığı ve kötüleşmediği gösterilir. Etkinlik seçimi, iş sıralama, kesirli sırt çantası, kesim özelliği ve Huffman kanıtları bu kalıbı kullanır.
  • Para üstü: sığan en büyük para seçilir; O(k + adet). Kanonik sistemlerde (ör. {50, 25, 10, 5, 1}) en iyidir; {4, 3, 1} sisteminde 6 ve 10 tutarları, {10, 7, 1} sisteminde 14 tutarı karşı örnektir. En iyi sonuç enAz[t] = 1 + min enAz[t − p] bağıntısıyla O(k · T) sürede bulunur.
  • Etkinlik seçimi: etkinlikler bitişe göre sıralanır; son seçilenin bitişinden sonra başlayan her etkinlik seçilir. O(n log n); sunumdaki on etkinlikten 6'sı seçilir.
  • İş sıralama: işler kâra göre azalan sırayla, son tarihinden önceki en geç boş dilime konur. Doğrudan aramayla en kötü O(n2), ayrık kümelerle neredeyse sıralama süresi. Sunumdaki altı işte toplam kâr 140'tır.
  • Kesirli sırt çantası: nesneler kâr/ağırlık oranına göre azalan sırayla, sığdığı kadar alınır; son nesneden bir parça alınabilir. O(n log n); örnekte 240. 0/1 sürümde aynı ölçüt 160 verir, en iyisi 220'dir.
  • MST ve kesim özelliği: bir kesimi geçen en hafif kenar bir MST'ye aittir. Kruskal (iki bileşeni ayıran kesim), Prim (ağaç ile geri kalanı) ve Borůvka (her bileşen ile geri kalanı) bu özelliği farklı kesimlerle kullanır; üçü de O(E log V)'dir.
  • Huffman kodlama: en düşük frekanslı iki ağaç tekrar tekrar birleştirilir; sol kenar 0, sağ kenar 1. Kodlar önek-özgürdür ve Σ frekans · derinlik toplamı en küçüktür. Örnekte 63 bit (sabit kodla 75); ağaç O(n log n) sürede kurulur ve çözücüye gönderilmelidir.
  • Yaklaşım algoritmaları: çizge boyama, kutulara yerleştirme ve en büyük kapsama gibi NP-zor problemlerde açgözlü algoritmalar en iyiyi garanti etmez, ama kanıtlanmış oranlarla iyi çözümler üretir.

7.11Kavram listesi

7.12Kendini Dene

  1. Para sistemi {9, 6, 5, 1} olsun. 11 ve 12 tutarları için açgözlü para üstü kaç para kullanır? En iyi çözümler nedir? Bu sistem kanonik midir?

    Cevap

    11 için açgözlü 9 + 1 + 1 ile 3 para kullanır; en iyisi 6 + 5 ile 2 paradır. 12 için açgözlü 9 + 1 + 1 + 1 ile 4 para kullanır; en iyisi 6 + 6 ile 2 paradır. Sistem kanonik değildir. 11, bu sistemde açgözlünün yanıldığı en küçük tutardır ve en büyük iki paranın toplamı olan 15'ten küçüktür.

  2. Sekiz etkinlik (s, f) çiftleriyle verilmiştir ve 1'den 8'e numaralıdır: (0, 3), (1, 2), (2, 5), (3, 4), (4, 7), (5, 6), (6, 9), (8, 10). En erken biten ölçütü hangi etkinlikleri seçer?

    Cevap

    Bitişe göre sıra: #2 (1, 2), #1 (0, 3), #4 (3, 4), #3 (2, 5), #6 (5, 6), #5 (4, 7), #7 (6, 9), #8 (8, 10). #2 seçilir (son bitiş 2). #1 0'da başlar, atlanır. #4 3'te başlar, seçilir (4). #3 atlanır. #6 5'te başlar, seçilir (6). #5 atlanır. #7 6'da başlar, seçilir (9). #8 8'de başlar, 8 < 9 olduğu için atlanır. Sonuç 4 etkinliktir: #2, #4, #6, #7. Bütün alt kümeler denenirse 5 etkinlikli çakışmasız bir küme bulunamaz.

  3. Etkinlik seçiminde her adımda en geç başlayan etkinliği seçen algoritma en iyi sonucu verir mi? Gerekçelendiriniz.

    Cevap

    Evet. Zaman ekseni ters çevrilirse (her [s, f) aralığı [−f, −s) olur) en geç başlayan etkinlik, en erken biten etkinliğe dönüşür ve çakışma ilişkisi değişmez. En erken biten için yapılan değiştirme argümanı aynen geçerlidir. En geç başlayan a, bir en iyi çözümün en son etkinliğinin yerine konabilir. a o etkinlikten daha erken başlamaz ve önceki etkinliklerle çakışmaz.

  4. Yedi iş (ad, son tarih, kâr) olarak verilmiştir: (A, 3, 35), (B, 4, 30), (C, 4, 25), (D, 2, 20), (E, 3, 15), (F, 1, 12), (G, 2, 5). Açgözlü iş sıralama dilimleri nasıl doldurur ve toplam kâr kaçtır? İşler en erken boş dilime konsaydı sonuç ne olurdu?

    Cevap

    Kâr sırası A, B, C, D, E, F, G'dir. A → 3. dilim; B → 4. dilim; C'nin son tarihi 4'tür, 4 ve 3 doludur, 2. dilime konur; D'nin son tarihi 2'dir, 2 doludur, 1. dilime konur. E, F ve G için uygun boş dilim kalmamıştır. Dilimler 1–4: D, C, A, B; toplam kâr 20 + 25 + 35 + 30 = 110, en iyi sonuçtur. En erken boş dilim kuralıyla A 1. dilime, B 2. dilime, C 3. dilime gider; D, E, F ve G'ye yer kalmaz ve toplam 90'da kalır.

  5. Kapasite W = 20, nesneler (kâr, ağırlık) olarak (48, 12), (30, 10), (28, 10), (5, 1) olsun. Kesirli sırt çantasının değeri nedir? Nesneler bölünemeseydi oran ölçütü ne bulurdu, en iyi sonuç ne olurdu?

    Cevap

    Oranlar 4; 3; 2,8 ve 5'tir. Oran sırası (5, 1), (48, 12), (30, 10), (28, 10)'dur. (5, 1) ve (48, 12) tümüyle alınır: 13 kg, 53 kâr. Kalan 7 kg için (30, 10)'un 7/10'u alınır: 21. Kesirli değer 74'tür. 0/1 durumunda oran ölçütü (5, 1) ve (48, 12)'yi alır; ardından gelen iki nesne de kalan 7 kg'a sığmaz ve sonuç 53'tür. En iyi 0/1 seçim (30, 10) ile (28, 10)'dur: tam 20 kg, 58 kâr.

  6. Frekanslar A:1, B:1, C:2, D:3, E:5, F:8 olsun. Huffman ağacının birleştirmelerini, kod uzunluklarını ve toplam bit sayısını bulunuz. Sabit uzunluklu kodla karşılaştırınız.

    Cevap

    Birleştirmeler: 1 + 1 = 2; 2 + 2 = 4; 3 + 4 = 7; 5 + 7 = 12; 8 + 12 = 20. Her birleştirme bir önceki birleştirmenin sonucunu içerdiği için ağaç tek yöne doğru uzar. Kod uzunlukları F 1, E 2, D 3, C 4, A ve B 5 bittir. Toplam bit 8 · 1 + 5 · 2 + 3 · 3 + 2 · 4 + 1 · 5 + 1 · 5 = 45'tir; iç düğümlerin toplamı da 2 + 4 + 7 + 12 + 20 = 45 verir. Altı karakter için sabit kod 3 bit ister: 20 × 3 = 60 bit.

  7. Kodlar F = 000, O = 001, E = 01, T = 10, G = 110, R = 111 olsun. 11001101001111 bit dizisi hangi metne çözülür? Aynı metin 3 bitlik sabit kodla kaç bit tutardı?

    Cevap

    Soldan okunur: 110 → G, 01 → E, 10 → T, 10 → T, 01 → E, 111 → R. Metin “GETTER”dır. Huffman ile 14 bit, sabit kodla 6 × 3 = 18 bit tutar. Hiçbir kod bir başkasının öneki olmadığı için her kodun bittiği yer kesin olarak bellidir.

  8. Aşağıdaki iki ifade doğru mudur? (a) Ağırlıkları farklı bağlı bir çizgede en hafif kenar her MST'dedir. (b) En ağır kenar hiçbir MST'de bulunmaz.

    Cevap

    (a) Doğrudur. En hafif kenarın iki ucundan biri S'ye, öteki V − S'ye konarak bir kesim kurulur. Bu kenar kesimi geçen kenarların en hafifidir ve tek en hafif olduğu için kesim özelliğine göre her MST'dedir. (b) Yanlıştır. En ağır kenar bir köprü olabilir: silindiğinde çizge ikiye ayrılıyorsa her kapsayan ağaçta bulunmak zorundadır. Bir kenarın MST'den dışlanması için bir döngünün tek en ağır kenarı olması gerekir.

  9. Kesirli sırt çantası için verilen değiştirme argümanı, 0/1 sırt çantasında hangi adımda bozulur?

    Cevap

    Kanıt, en iyi çözümden düşük oranlı bir nesnenin δ kilogramını çıkarıp yerine yüksek oranlı nesnenin δ kilogramını koyar. Bu değiştirme ağırlığı korur ve kârı azaltmaz. 0/1 durumunda nesneler parça parça alınamaz; ancak bütün nesneler değiştirilebilir. Ağırlıkları farklı iki nesne değiştirildiğinde ya kapasite aşılır ya da çantada değersiz bir boşluk kalır. Uygunluk koşulu ya da “daha kötü olmaz” koşulu bozulur.

  10. n = 106 işin hepsinin son tarihi n olsun. Doğrudan dilim aramasıyla iş sıralama kaç dilim bakışı yapar? Ayrık kümelerle bu sayı yaklaşık ne olur?

    Cevap

    i. iş en geç dilimden başlayıp geriye doğru ilk boş dilimi arar; önceki i − 1 iş en sondaki dilimleri doldurduğu için i dilime bakar. Toplam 1 + 2 + … + n = n(n + 1)/2 ≈ 5 · 1011 bakıştır. Ayrık kümelerle her iş birkaç bul ve birlestir çağrısıyla yerleşir ve toplam iş neredeyse doğrusal, yani birkaç milyon adım düzeyindedir. Bu durumda süreyi n log2 n ≈ 2 · 107 karşılaştırma yapan sıralama belirler.

7.13İleri okuma

  • T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein, Introduction to Algorithms (4. baskı, 2022), Bölüm 15: etkinlik seçimi, açgözlü stratejinin öğeleri ve Huffman kodlarının en iyiliği, burada özetlenen kanıtların tam biçimiyle verilir.
  • J. Kleinberg ve É. Tardos, Algorithm Design (2006), Bölüm 4: aralık çizelgeleme, “açgözlü önde kalır” ve değiştirme argümanı kanıt teknikleri ile en az çakışan ölçütüne karşı örnekler ayrıntılı tartışılır.
  • R. Sedgewick ve K. Wayne, Algorithms (4. baskı, 2011), Kesim 5.5: Huffman sıkıştırmasının Java ile eksiksiz bir uygulaması, ağacın dosyaya yazılması ve okunması dahil.
  • D. A. Huffman, “A Method for the Construction of Minimum-Redundancy Codes” (1952): yöntemin ilk yayımlandığı kısa ve okunaklı özgün makale.

7.14Kaynaklar

  1. T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein, Introduction to Algorithms, 4. baskı, MIT Press, 2022 (Bölüm 15: Greedy Algorithms; Bölüm 21: Minimum Spanning Trees).
  2. D. A. Huffman, “A Method for the Construction of Minimum-Redundancy Codes”, Proceedings of the IRE 40(9), 1098–1101, 1952.
  3. J. Kleinberg, É. Tardos, Algorithm Design, Addison-Wesley, 2006 (Bölüm 4: Greedy Algorithms).
  4. D. Kozen, S. Zaks, “Optimal Bounds for the Change-Making Problem”, Theoretical Computer Science 123(2), 377–388, 1994.
  5. J. B. Kruskal, “On the Shortest Spanning Subtree of a Graph and the Traveling Salesman Problem”, Proceedings of the American Mathematical Society 7(1), 48–50, 1956.
  6. R. Sedgewick, K. Wayne, Algorithms, 4. baskı, Addison-Wesley, 2011 (Kesim 4.3: Minimum Spanning Trees; Kesim 5.5: Data Compression).

İlgili sayfalar: Bölüm 7 sunumu · Çalışma soruları · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.