Algoritmalar · Ders Notu

Bölüm 4.3: Minimum Kapsayan Ağaç Algoritmaları

Bir şirket birkaç şehri elektrik hattıyla birbirine bağlamak istiyor. Her şehir çiftinin arasına hat çekmenin bir maliyeti vardır. Bütün şehirlerin birbirine ulaşması yeterlidir; iki şehrin doğrudan bağlı olması gerekmez. Şirket hangi hatları çekerse toplam maliyeti en küçük olur? Bu sorunun matematikteki adı minimum kapsayan ağaç problemidir. Not önce kapsayan ağacı, minimum kapsayan ağacı ve bütün algoritmaların dayandığı iki özelliği, kesim ve döngü özelliğini tanıtır. Ardından aynı problemi çözen dört açgözlü algoritma ele alınır. Kruskal kenarları küçükten büyüğe ekler, Prim ağacı tek düğümden büyütür. Borůvka bütün bileşenleri aynı anda büyütür, ters silme ise ağır kenarları siler. Kruskal'ın döngü denetimi için gereken ayrık kümeler yapısı da ayrıntılı işlenir. Her algoritma için Java kodu, adım adım izleme ve maliyet hesabı verilir; not, dört algoritmanın karşılaştırmasıyla biter.

Öğrenme çıktıları

Kavram
Kapsayan ağacı ve minimum kapsayan ağacı (MST) tanımlamak; ağacın V − 1 kenarlı olduğunu, kesim ve döngü özelliklerini açıklamak ve gerekçelendirmek.
Kruskal ve ayrık kümeler
Kenarları sıralayıp döngü oluşturmayanları seçmek; döngü denetimini ayrık kümelerle (bul / birleştir) yapmak; yol kısaltmanın ve rütbeye göre birleştirmenin etkisini izlemek.
Prim
Ağacı bir düğümden büyütmek; anahtar ve ebeveyn dizilerini hem dizi taramasıyla hem öncelikli kuyrukla adım adım izlemek; Prim ile Dijkstra arasındaki farkı açıklamak.
Borůvka, ters silme ve analiz
Bileşen tabanlı ve silme tabanlı yaklaşımları uygulamak; dört algoritmanın karmaşıklığını karşılaştırıp çizgenin yapısına göre uygun olanı seçmek.

4.3.1Kapsayan ağaç ve minimum kapsayan ağaç

Çizgeler, nesneler ve aralarındaki bağlantılar üzerine kurulan problemlerin ortak dilidir. Bu notta ele alınan çizgeler yönsüzdür: u ile v arasındaki kenar iki yönde de kullanılabilir. Çizgeler ayrıca ağırlıklıdır: her kenarın bir sayısal değeri vardır ve bu değer kablonun uzunluğunu, yolun maliyetini ya da iki nokta arasındaki farkı gösterebilir. Düğüm sayısı V, kenar sayısı E ile gösterilir. Aynı harfler bağlama göre düğüm ve kenar kümesini de anlatır; G = (V, E) yazımında olduğu gibi.

Ağaç kavramı ise ikili arama ağaçlarından ve öbeklerden tanıdıktır. Orada ağaç köklü bir veri yapısıdır. Çizge kuramında ağaç daha genel bir nesnedir: bağlı ve döngüsüz her yönsüz çizge bir ağaçtır. Kök yoktur, çocuk sayısı sınırı yoktur. Bir ağacın herhangi iki düğümü arasında tam olarak bir yol bulunur. Yol hiç olmasaydı çizge bağlı olmazdı; iki farklı yol olsaydı bu iki yol birlikte bir döngü oluştururdu.

4.3.1.1Problemin tanımı

Bağlı bir çizgeden bazı kenarları silerek bütün düğümleri hâlâ birbirine bağlayan, ama hiç döngü içermeyen bir alt çizge elde edilebilir. Böyle bir alt çizge, çizgenin bütün düğümlerini “kapsar”.

Tanım 4.3.1

G = (V, E) bağlı, yönsüz bir çizge olsun. G'nin bir kapsayan ağacı (spanning tree), G'nin bütün düğümlerini içeren, bağlı ve döngüsüz bir alt çizgesidir. Kapsayan ağaç yalnız G'de bulunan kenarları kullanır.

Kapsayan ağacın kaç kenarı olduğu, düğüm sayısından kesin olarak bellidir. Bu sonuç bütün algoritmaların durma koşulunda kullanılır.

Teorem 4.3.1 (Ağacın kenar sayısı)

V düğümlü her ağacın tam olarak V − 1 kenarı vardır. Ağaca yeni bir kenar eklemek tam olarak bir döngü oluşturur; ağaçtan bir kenar silmek ağacı iki parçaya böler.

Başlangıçta kenarsız V düğüm olsun. Bu durumda V ayrı parça vardır. Ağacın kenarları tek tek eklensin. Bir kenar aynı parçanın iki düğümünü bağlasaydı, o iki düğüm arasında zaten bir yol bulunduğu için bir döngü oluşurdu; ağaç döngüsüz olduğundan bu olmaz. Demek ki her kenar iki ayrı parçayı birleştirir ve parça sayısını tam olarak bir azaltır. Sonunda ağaç bağlı olduğu için tek parça kalır. V parçadan bir parçaya inmek için V − 1 kenar gerekir. İkinci kısım için ağaçta u ile v arasındaki tek yol düşünülür: yeni (u, v) kenarı bu yolla birlikte bir döngü oluşturur. Bir kenar silindiğinde ise o kenarın iki ucu arasında başka yol kalmaz; ağaç iki parçaya ayrılır. □

Bir çizgenin genellikle birçok kapsayan ağacı vardır. Kenarlar ağırlıklı olduğunda bu ağaçların toplam ağırlıkları da farklıdır. Asıl ilgilendiğimiz, toplamı en küçük olanıdır.

Tanım 4.3.2

Kenarları w(u, v) ile ağırlıklandırılmış bağlı, yönsüz bir çizgede bir kapsayan ağacın ağırlığı, kenar ağırlıklarının toplamıdır: w(T) = Σ(u,v) ∈ T w(u, v). Ağırlığı en küçük olan kapsayan ağaca minimum kapsayan ağaç (minimum spanning tree, MST) denir.

Tanımdaki “minimum” sözcüğü ağacın kendisini değil, toplam ağırlığı nitelendirir. Kenar sayısı her kapsayan ağaçta V − 1 olduğu için en az kenarlı ağaç diye bir arayış yoktur. Ağaçlar yalnız hangi kenarları seçtikleriyle ayrılır. Şekil 4.3.1 beş düğümlü küçük bir çizgeyi ve onun minimum kapsayan ağacını gösterir.

3154276aebcd
Şekil 4.3.1: a, b, c, d, e düğümlü ağırlıklı çizge. Kalın kenarlar (ae = 1, cd = 2, ab = 3, bc = 5) minimum kapsayan ağacı oluşturur: w(T) = 1 + 2 + 3 + 5 = 11.

Şekildeki çizgenin yedi kenarı vardır. Kapsayan ağaç beş düğümü bağlamak için dört kenar seçer; üç kenar dışarıda kalır. Kalın dört kenar bir ağaç oluşturur: a'dan e'ye ve b'ye, b'den c'ye, c'den d'ye gidilebilir ve hiçbir döngü yoktur. Dışarıda kalan be = 4, ec = 6 ve ed = 7 kenarlarının her biri ağaca eklenseydi bir döngü kapatırdı. Örneğin be eklenirse b → a → e → b döngüsü oluşur.

Sık yapılan hata

MST'yi “en hafif V − 1 kenar” sanmak. Şekildeki en hafif dört kenar ae = 1, cd = 2, ab = 3 ve be = 4'tür; toplamları 10'dur. Ama bu dört kenar bir ağaç değildir: a, b, e arasında döngü kurar ve {a, b, e} ile {c, d} birbirine bağlanmaz. Seçilen kenarlar hem döngüsüz hem bağlı olmalıdır; asıl zorluk da buradadır.

4.3.1.2Kaç kapsayan ağaç vardır?

MST'yi bulmanın en kaba yolu, bütün kapsayan ağaçları tek tek kurup ağırlıklarını karşılaştırmaktır. Bu yöntemin neden işe yaramadığını kapsayan ağaç sayısı gösterir. Her düğüm çiftinin bir kenarla bağlandığı çizgeye tam çizge denir. Arthur Cayley 1889'da n düğümlü tam çizgenin tam olarak nn−2 kapsayan ağacı olduğunu gösterdi. Bu sonuç Cayley formülü diye bilinir.

Tablo 4.3.1: n düğümlü tam çizgedeki kapsayan ağaç sayısı, nn−2.
nKapsayan ağaç sayısı
33
416
5125
10100.000.000
15yaklaşık 1,95 · 1015
Örnek 4.3.1

Dört düğümlü tam çizgenin altı kenarı vardır. Kapsayan ağaç üç kenar seçer. Altı kenardan üç kenar C(6, 3) = 20 farklı biçimde seçilir. Bu seçimlerin dördü üçgendir: dört düğümden herhangi üçü bir döngü kurar ve dördüncü düğüm dışarıda kalır. Üçgen olmayan her üç kenarlı seçim ise döngüsüzdür ve dört düğümü bağlar. Kapsayan ağaç sayısı 20 − 4 = 16'dır. Cayley formülü de 44−2 = 16 verir.

On beş düğümlü bir tam çizgede kapsayan ağaç sayısı katrilyon mertebesindedir. Bütün ağaçları denemek, küçük çizgelerde bile kullanışsızdır. Tam olmayan çizgelerde sayı daha azdır, ama yine hızla büyür: Şekil 4.3.1'deki yedi kenarlı çizgenin 21 kapsayan ağacı vardır. Bu ağaçların ağırlıkları 11 ile 21 arasında değişir ve 11 ağırlıklı yalnız bir ağaç vardır. Neyse ki MST'yi bulmak için ağaçları saymaya gerek yoktur. Problem, her adımda yerel olarak en iyi görünen seçimi yapan açgözlü (greedy) algoritmalarla doğrudan çözülür. Açgözlü yaklaşımın burada neden doğru sonuç verdiğini kesim ve döngü özellikleri açıklar.

4.3.1.3Örnek: “MST” çizgesi

Daha büyük bir örnek, kapsayan ağacın hangi kenarları seçtiğini ve hangilerini dışarıda bıraktığını görmeye yardım eder. Şekil 4.3.2'deki çizge M, S ve T harflerini çizer. Çizgenin 15 düğümü ve 20 kenarı vardır.

152251610781011254118202524124050
Şekil 4.3.2: M, S ve T harflerini çizen 15 düğümlü, 20 kenarlı çizge. Kalın 14 kenar minimum kapsayan ağacı oluşturur; toplam ağırlık 141'dir. İnce kenarlar (15, 16, 18, 25, 40, 50) ağaca girmez.

Kalın kenar sayısı 14'tür ve bu sayı V − 1 = 15 − 1'e eşittir. Ağırlıkların toplamı şöyle bulunur:

2 + 2 + 5 + 10 + 7 + 8 + 10 + 11 + 4 + 1 + 20 + 25 + 24 + 12 = 141

Dışarıda kalan altı ince kenarın ortak bir özelliği vardır. Her biri kalın kenarlarla birlikte bir döngü kapatır ve o döngünün en ağır kenarıdır. M harfinin üstündeki 15'lik kenar, iki çapraz 2'lik kenarla bir üçgen kurar; üçgenin en ağırı 15'tir. M'nin altındaki 16'lık kenar, iki bacak (5 ve 10) ve iki çapraz (2 ve 2) ile döngü kurar. S harfinin sol alt kısmındaki 25'lik ince kenar ise 11, 1 ve 4 ağırlıklı kalın kenarlarla bir döngü kapatır. Her durumda ince kenarın yerine döngünün öteki kenarları bağlantıyı sağlar ve daha ucuzdur.

Ağırlığı yüksek bazı kenarlar ise ağaçtadır: 20, 24 ve S ile T arasındaki 25. Bu kenarlar hiçbir döngünün en ağırı değildir. Örneğin S'nin sağ altından T'nin sağ üst ucuna uzanan 40'lık kenar uzun bir döngü kapatır. Bu döngüde 20, 25 ve 24'lük kenarlar da vardır, ama döngünün en ağırı 40'tır. Aynı biçimde 50'lik kenar 24 ve 12 ile bir üçgen kurar. Çizgede bir de köprü vardır: M ile S'yi bağlayan 7'lik kenar silinirse çizge iki parçaya ayrılır. Bir köprü hiçbir döngüde yer almadığı için her kapsayan ağaçta, dolayısıyla her MST'de bulunmak zorundadır.

Sık yapılan hata

Ağır bir kenarın ağaçta bulunmasını “o düğüme ulaşmanın tek yolu bu kenardır” diye açıklamak. Şekildeki 20, 24 ve 25'lik kalın kenarlar köprü değildir; her biri silinse de çizge bağlı kalır. Ağaçta bulunmalarının nedeni, onları içeren her döngüde daha ağır bir kenarın (40 ya da 50) bulunmasıdır. Bir kenarın ağırlığı tek başına bir şey söylemez; önemli olan, kenarın döngülerdeki ve kesimlerdeki konumudur.

4.3.1.4Kesim özelliği

Bütün MST algoritmaları iki basit gözleme dayanır. Birincisi düğümleri iki gruba ayırmakla ilgilidir.

Tanım 4.3.3

Bir çizgenin düğüm kümesini boş olmayan iki parçaya ayırmaya kesim (cut) denir ve (S, V − S) ile gösterilir. Bir ucu S'de, öbür ucu V − S'de olan kenara kesimi geçen kenar denir. Kesimi geçen kenarlar arasında ağırlığı en küçük olana kesimin en hafif kenarı denir.

Teorem 4.3.2 (Kesim özelliği)

Her kesimde, kesimi geçen en hafif kenar bir MST'ye aittir. Kesimi geçen en hafif kenar tek ise bu kenar her MST'de bulunur.

Kesimi geçen en hafif kenar e = (u, v) olsun; u ∈ S, v ∈ V − S. T, e'yi içermeyen bir MST olsun. T bir kapsayan ağaç olduğu için u'dan v'ye T içinde bir yol vardır. Bu yol S'de başlar, V − S'de biter; demek ki en az bir yerde kesimi geçer. Geçtiği kenar f olsun. T'den f silinir ve e eklenirse yeni bir T′ elde edilir. f silinince ağaç iki parçaya ayrılır: biri u'yu, öteki v'yi içerir. e bu iki parçayı yeniden bağlar. Böylece T′ de V − 1 kenarlı, bağlı bir kapsayan ağaçtır. Ağırlığı w(T′) = w(T) − w(f) + w(e)'dir. e kesimin en hafif kenarı olduğu için w(e) ≤ w(f), dolayısıyla w(T′) ≤ w(T) olur. T minimum olduğundan T′ de minimumdur ve e'yi içerir. e tek en hafif kenarsa w(e) < w(f) olur ve w(T′) < w(T) çelişkisi doğar; yani e'yi içermeyen bir MST yoktur. □

İspattaki fikre değiştirme argümanı (exchange argument) denir. Açgözlü seçimi içermeyen bir en iyi çözüm alınır. Bu çözümün bir parçası değiştirilir ve çözüm, o seçimi içeren, en az onun kadar iyi bir çözüme dönüşür. Aynı fikir açgözlü algoritmaların çoğunun doğruluk ispatında kullanılır.

kesim3154276aebcd
Şekil 4.3.3: {a, b} | {c, d, e} kesimi. Kesimi geçen kenarlar ae = 1, be = 4 ve bc = 5'tir; en hafifleri ae = 1 güvenlidir ve MST'dedir.
Örnek 4.3.2

Şekil 4.3.1'deki çizgede birkaç kesim incelenebilir.

  1. {a, b} | {c, d, e}: geçen kenarlar ae = 1, be = 4, bc = 5. En hafif ae'dir; MST'de bulunur.
  2. {d} | {a, b, c, e}: geçen kenarlar cd = 2 ve ed = 7. En hafif cd'dir; MST'de bulunur.
  3. {a, b, e} | {c, d}: geçen kenarlar bc = 5, ec = 6, ed = 7. En hafif bc'dir. Bu kenar çizgenin hafif kenarlarından biri değildir, yine de MST'ye girer.

Kesim özelliği yalnız kesimi geçen kenarlar hakkında konuşur. ab = 3 ilk kesimi geçmez; bu yüzden o kesim ab için bir şey söylemez. ab'nin MST'de olduğu başka bir kesimle gösterilir: {b} | {a, c, d, e} kesimini geçen kenarlar ab = 3, be = 4, bc = 5'tir ve en hafifi ab'dir.

4.3.1.5Döngü özelliği

İkinci gözlem, kesim özelliğinin bir bakıma tersidir. Kesim özelliği hangi kenarın kesinlikle seçilebileceğini söyler; döngü özelliği ise hangi kenarın kesinlikle dışarıda bırakılabileceğini söyler.

Teorem 4.3.3 (Döngü özelliği)

Çizgedeki herhangi bir döngünün en ağır kenarı tek ise bu kenar hiçbir MST'de yer almaz.

Bir C döngüsünün tek en ağır kenarı f = (u, v) olsun ve bir T MST'si f'yi içersin. T'den f silinirse ağaç iki parçaya ayrılır; u bir parçada, v öbür parçadadır. C'nin f dışındaki kenarları u'dan v'ye giden bir yol oluşturur. Bu yol bir parçadan öbürüne geçtiği için en az bir kenarı, örneğin e, iki parçayı birbirine bağlar. T − f + e yine bir kapsayan ağaçtır ve w(e) < w(f) olduğu için ağırlığı T'ninkinden küçüktür. Bu, T'nin minimum olmasıyla çelişir. □

3154276aebcd
Şekil 4.3.4: b–c–e döngüsünün kenarları 4, 5 ve 6'dır. En ağır kenar ec = 6 hiçbir MST'de yer almaz.

Döngü özelliği küçük bir uyarı içerir: en ağır kenar tek olmalıdır. Bir döngüde en ağır ağırlık iki kenarda birden görülürse, bunlardan biri bir MST'de bulunabilir. Örneğin kenarları 1, 2 ve 2 olan bir üçgende 2'lik kenarlardan biri mutlaka seçilir. Böyle durumda özellik yalnız şunu söyler: en ağır kenarlardan birini içermeyen bir MST vardır.

İki özellik birlikte algoritmaların iskeletini verir. Tablo 4.3.2 hangi algoritmanın hangi özelliği kullandığını özetler.

Tablo 4.3.2: İki özellik ve onları kullanan algoritmalar.
ÖzellikNe söyler?Kullanan algoritma
Kesim özelliğiKesimi geçen en hafif kenar güvenle eklenir.Kruskal, Prim, Borůvka
Döngü özelliğiDöngünün en ağır kenarı güvenle atılır.Ters silme

4.3.1.6MST ne zaman tektir?

Bir çizgenin birden fazla minimum kapsayan ağacı olabilir. Bunun için eşit ağırlıklı kenarlar gerekir. Kenarları 1, 2 ve 2 olan üçgende 1'lik kenar ile 2'lik kenarlardan herhangi biri bir MST oluşturur. İki farklı ağacın ağırlığı da 3'tür. Bütün ağırlıklar farklıysa böyle bir seçim serbestliği kalmaz.

Teorem 4.3.4 (MST'nin tekliği)

Bağlı bir çizgede bütün kenar ağırlıkları birbirinden farklıysa MST tektir.

T bir MST, e de T'nin herhangi bir kenarı olsun. T'den e silinince düğümler iki parçaya ayrılır ve bu iki parça bir kesim tanımlar. T'nin bu kesimi geçen tek kenarı e'dir. Kesimi geçen kenarlar arasında e'den hafif bir e′ olsaydı T − e + e′ daha hafif bir kapsayan ağaç olurdu; bu olamaz. Ağırlıklar farklı olduğundan e bu kesimin tek en hafif kenarıdır. Kesim özelliğine göre e her MST'de bulunur. Bu, T'nin bütün kenarları için geçerlidir. Her MST T'nin V − 1 kenarını içerir ve kendisi de V − 1 kenarlıdır; demek ki her MST T'nin aynısıdır. □

Teoremin tersi doğru değildir: eşit ağırlıklı kenarlar bulunan bir çizgenin MST'si yine tek olabilir. Şekil 4.3.1'deki ağırlıklar zaten farklıdır; bu yüzden orada tek MST vardır. Eşit ağırlıklı kenarlar bulunduğunda farklı algoritmalar, hatta aynı algoritmanın farklı kenar sıraları farklı ağaçlar bulabilir. Bulunan ağaçların toplam ağırlığı ise her zaman aynıdır.

4.3.1.7Açgözlü MST algoritmaları

Kesim özelliği genel bir yöntem önerir. Başta hiç kenar seçilmemiştir. Seçilen kenarlar her an bir MST'nin parçası olacak biçimde büyütülür. Her adımda, seçilmiş kenarların hiçbirinin geçmediği bir kesim bulunur ve o kesimin en hafif kenarı eklenir. Böyle bir kenara güvenli kenar denir: eklendiğinde seçilen kümenin yine bir MST'ye genişletilebileceği garanti edilir. V − 1 güvenli kenar eklenince ağaç tamamlanır.

Dört klasik algoritma bu genel yöntemin farklı uygulamalarıdır. Hepsi açgözlüdür: her adımda yerel olarak en iyi kenarı seçer ya da en kötüsünü atar ve bu karar hiçbir zaman geri alınmaz.

Tablo 4.3.3: Açgözlü MST algoritmalarına genel bakış.
AlgoritmaFikirYardımcı yapıZaman
Kruskal (1956)Kenarları küçükten büyüğe sırala, döngü oluşturmayanları ekleayrık kümelerO(E log E)
Prim (1957)Bir düğümden başla, ağacı en ucuz komşu kenarla büyütöncelikli kuyrukO((V + E) log V)
Borůvka (1926)Her bileşen kendi en ucuz kenarını aynı anda eklerayrık kümelerO(E log V)
Ters silmeKenarları büyükten küçüğe dene, bağlılığı bozmayanı silBFS/DFS ya da ayrık kümelerO(E · (V + E))

Dört algoritma da aynı ağırlıkta bir MST bulur. Ağırlıklar farklıysa, MST'nin tekliği nedeniyle aynı ağacı bulurlar. Aralarındaki fark kenarları hangi sırayla ele aldıklarında ve gereken veri yapısındadır. Kruskal ve Borůvka bir ormanla, yani birden çok ağaçla çalışır. Prim her an tek bir ağaç tutar. Ters silme ise bütün çizgeden başlar ve onu inceltir. Tablodaki yıllar da ilginçtir: en eski algoritma Borůvka'nınkidir ve bilgisayarlardan önce, bir elektrik ağı problemi için yazılmıştır.

4.3.1.8Uygulama alanları

MST, kuramsal bir merak değildir; birçok mühendislik probleminin ya kendisi ya da bir adımıdır.

  • Ağ tasarımı. Şehirleri, binaları ya da cihazları en az kabloyla bağlamak doğrudan bir MST problemidir. Elektrik, su, fiber optik ve yol ağları bu sınıfa girer. Borůvka'nın 1926 çalışmasının çıkış noktası da Moravya'nın elektrik ağıydı.
  • Kümeleme. Noktalar düğüm, aralarındaki uzaklıklar ağırlık sayılır. MST'den en ağır k − 1 kenar silinirse k parça kalır ve her parça bir kümedir. Bu yönteme tek bağlantılı kümeleme (single-linkage clustering) denir. Dokuz düğümlü Kruskal örneğinde MST'nin en ağır iki kenarı silinince {A, B}, {C, D, E, F, H} ve {G, I} kümeleri oluşur.
  • Yaklaşık algoritmalar. Metrik gezgin satıcı probleminde (TSP), MST'yi derinlik öncelikli dolaşıp tekrar eden düğümleri atlayan tur, en iyi turun en fazla 2 katıdır. TSP'nin kesin çözümü çok pahalıdır; MST ise hızla bulunur.
  • Görüntü işleme. Pikseller düğüm, komşu piksellerin renk farkı ağırlık sayılır. MST tabanlı yöntemler benzer renkteki bölgeleri bir araya toplayarak görüntüyü bölütler (segmentation).
  • Devre tasarımı. Bir yonga üzerinde birbirine bağlanması gereken bacakları en kısa toplam iletkenle bağlamak, VLSI yönlendirmesinin ön adımıdır.
  • Başka çizge problemleri. İki düğüm arasında, en ağır kenarı en hafif olan yola darboğaz yol denir. Böyle bir yol her zaman MST üzerindedir. Steiner ağacı problemi için de MST temel bir yaklaşık çözümdür.

4.3.2Kruskal algoritması

Kruskal algoritması MST problemine akla gelen en doğal yanıtı verir: önce en ucuz kenarlar alınır, yeter ki döngü oluşmasın. Joseph Kruskal bu algoritmayı 1956'da, gezgin satıcı problemiyle ilgili kısa bir makalede yayımladı. Aynı makalede ters silme algoritmasının fikri de yer alır.

4.3.2.1Fikir: ormanı birleştirmek

Başlangıçta her düğüm tek başına bir ağaçtır; çizge V ağaçlı bir ormandır. Kenarlar ağırlığa göre küçükten büyüğe sıralanır ve bu sırayla incelenir. İncelenen kenar iki farklı ağacın düğümlerini bağlıyorsa ağaca eklenir ve bu iki ağaç birleşir. Kenarın iki ucu zaten aynı ağaçtaysa, kenar eklendiğinde bir döngü oluşur; kenar atlanır. Her ekleme ağaç sayısını bir azaltır. V − 1 ekleme sonunda tek ağaç kalır ve algoritma biter.

1223457abcdefyeşil: seçilen · kırmızı: döngü (atla)mavi: sıradaki, iki ağacı birleştirir
Şekil 4.3.5: Kruskal'ın ara durumu: iki ağaçlı bir orman. Yeşil kenarlar seçilmiştir. Kırmızı kesikli 4'lük kenar aynı ağacın iki düğümünü bağlar ve döngü kurar; atlanır. Mavi 5'lik kenar iki ağacı birleştirir; eklenir.

Şekildeki ara durumda {a, b, c} ve {d, e, f} olmak üzere iki ağaç vardır. Sıradaki kenar bc = 4'tür. b ile c arasında b → a → c yolu zaten vardır; bc eklenirse bu yol ile birlikte bir döngü oluşur. Ardından gelen bd = 5 ise iki ayrı ağacı bağlar ve eklenir. Bu noktada orman tek ağaç olur.

Algoritmanın kısa bir değerlendirmesi şöyledir. Kruskal, kenar sayısının düğüm sayısına yakın olduğu seyrek çizgelerde çok uygundur. Kodu basittir: bir kenar listesi ve bir ayrık kümeler yapısı yeterlidir. Çizge bağlı değilse algoritma her bileşen için ayrı bir ağaç, yani bir minimum kapsayan orman üretir. Zayıf yanları da vardır. Kenarları sıralamanın maliyeti O(E log E)'dir. Kenar sayısının V2'ye yaklaştığı yoğun çizgelerde Prim'in dizi taramalı sürümü daha hızlı olabilir. Döngü denetimi için ek bir yapı gerekir.

4.3.2.2Adımlar ve sözde kod

Algoritmanın adımları şöyledir:

  1. Çizgedeki bütün kenarlar ağırlıklarına göre artan sırada sıralanır.
  2. Sıralı kenarlar arasından sıradaki en küçük ağırlıklı kenar seçilir.
  3. Kenar farklı ağaçlara ait iki düğümü birleştiriyorsa ağaca eklenir ve iki ağaç birleştirilir.
  4. Kenar aynı ağacın iki düğümünü bağlıyorsa döngü oluşturur ve atlanır.
  5. V − 1 kenar seçilene (ya da kenarlar bitene) kadar 2–4. adımlar tekrarlanır.
KRUSKAL(G):
  MST = [ ]
  ayrikKume.oluştur(V)
  kenarlar.sırala(artana göre)
  her bir (u, v, ağırlık) için kenarlar içinde:
    eğer ayrikKume.bul(u) ≠ ayrikKume.bul(v):
      MST.ekle((u, v, ağırlık))
      ayrikKume.birleştir(u, v)
  döndür MST

Sözde kodun tek gizemli yeri ayrikKume.bul(u) ≠ ayrikKume.bul(v) karşılaştırmasıdır. bul, bir düğümün hangi ağaçta olduğunu söyleyen bir temsilci döndürür. İki ucun temsilcisi farklıysa kenar iki ayrı ağacı bağlar. birleştir ise iki ağacı tek ağaçta toplar. “Bu iki düğüm aynı ağaçta mı?” sorusu ayrık kümeler yapısıyla neredeyse sabit zamanda yanıtlanır. Yapı bir sonraki kesimde ayrıntılı ele alınır.

4.3.2.3Adım adım örnek

Şekil 4.3.1'deki beş düğümlü çizgeye Kruskal uygulanır. Çizgenin kenarları ab = 3, ae = 1, bc = 5, be = 4, cd = 2, ed = 7 ve ec = 6'dır.

Örnek 4.3.3
  1. Başlangıç. Her düğüm ayrı bir ağaçtır: {a} {b} {c} {d} {e}. Seçilen kenar yoktur, w(T) = 0.
  2. Sıralama. Kenarlar artan ağırlığa göre dizilir: ae 1, cd 2, ab 3, be 4, bc 5, ec 6, ed 7.
  3. ae = 1. a ve e farklı ağaçlardadır; kenar eklenir. Ağaçlar {a, e} {b} {c} {d}, w(T) = 1.
  4. cd = 2. c ve d farklı ağaçlardadır; eklenir. Ağaçlar {a, e} {c, d} {b}, w(T) = 3.
  5. ab = 3. a ve b farklı ağaçlardadır; eklenir. Ağaçlar {a, b, e} {c, d}, w(T) = 6.
  6. be = 4. b ve e zaten aynı ağaçtadır. Kenar eklenirse b → a → e → b döngüsü oluşur; atlanır.
  7. bc = 5. b ve c farklı ağaçlardadır; eklenir. Tek ağaç kalır: {a, b, c, d, e}, w(T) = 11.

V − 1 = 4 kenar seçilmiştir; algoritma durur. ec = 6 ve ed = 7'ye bakmaya gerek kalmaz. Sonuç Şekil 4.3.1'deki ağaçtır.

ae1cd2ab3be4bc5ec6ed73154276aebcd
Şekil 4.3.6: be = 4 adımı. Üstteki tablo sıralı kenarları gösterir: yeşiller seçilmiş, kırmızı reddedilmiş, maviler henüz incelenmemiştir. b ile e aynı ağaçta (mavi düğümler) olduğu için be döngü kurar.

İzlemede reddedilen kenar yalnız be'dir. Bu kenar Şekil 4.3.1'deki çizgenin dördüncü en hafif kenarıdır. Kruskal onu ağırlığı yüzünden değil, konumu yüzünden atlar: b ve e daha ucuz iki kenarla zaten birbirine bağlıdır.

Sık yapılan hata

Döngü denetimini “u ile v arasında seçilmiş bir kenar var mı?” sorusuyla yapmak. Örnekteki be adımında b ile e arasında doğrudan bir kenar seçilmemiştir; yine de ikisi a üzerinden bağlıdır ve be döngü kurar. Doğru soru “u ve v aynı bileşende mi?” sorusudur; aradaki yolun uzunluğu önemsizdir.

4.3.2.4Kruskal neden doğrudur?

Teorem 4.3.5 (Kruskal'ın doğruluğu)

Bağlı bir çizgede Kruskal algoritmasının seçtiği kenarlar bir minimum kapsayan ağaç oluşturur.

Algoritmanın eklediği her kenarın güvenli olduğu gösterilir. Kruskal bir e = (u, v) kenarını eklemeye karar verdiğinde u ve v farklı ağaçlardadır. u'nun ağacı S olsun ve (S, V − S) kesimi düşünülsün. Seçilmiş kenarların hiçbiri bu kesimi geçmez, çünkü S bir ağacın tamamıdır. Kesimi geçen her kenar iki farklı ağacı bağlar; demek ki bu kenarlardan hiçbiri daha önce döngü gerekçesiyle atlanmamıştır. e'den hafif bir kesim kenarı olsaydı, sıralı listede e'den önce gelir ve o sırada da iki farklı ağacı bağladığı için eklenirdi. Öyleyse e kesimin en hafif kenarıdır ve kesim özelliğine göre güvenlidir. Atlanan kenarlar ise döngü kurduğu için hiçbir ağaçta yer alamaz. Çizge bağlı olduğundan sonunda V − 1 kenar seçilir ve bunlar bir MST oluşturur. □

Ağırlıklar farklıysa sıralı kenar listesi tek türlüdür ve Kruskal her zaman aynı ağacı bulur. Eşit ağırlıklı kenarlar bulunduğunda sıralama onları farklı sıralarla dizebilir. Bu durumda sonuç ağaç değişebilir, ama ağırlığı değişmez: kanıt hangi sıralama seçilirse seçilsin geçerlidir.

4.3.3Ayrık kümeler (union-find)

Kruskal'ın hızı, “iki düğüm aynı ağaçta mı?” sorusunun ne kadar çabuk yanıtlandığına bağlıdır. Her kenar için BFS ya da DFS ile yol aramak O(V) sürer ve toplam maliyeti O(E · V)'ye çıkarır. Ayrık kümeler yapısı aynı soruyu pratikte sabit zamanda yanıtlar.

4.3.3.1Yapı ve işlemler

Tanım 4.3.4

Ayrık kümeler (union-find, disjoint sets) yapısı, kesişmeyen kümelerden oluşan bir koleksiyonu tutar ve üç işlem sunar:

  • oluştur(V): her öğe kendi tek öğeli kümesini oluşturur.
  • bul(x) (find): x'in kümesinin temsilcisini döndürür. Aynı kümedeki bütün öğeler için aynı temsilci döner.
  • birleştir(x, y) (union): x'in ve y'nin kümelerini tek kümede toplar.

Yapının en yaygın gösterimi bir ormandır. Her küme bir ağaçtır ve ağacın kökü kümenin temsilcisidir. Ağaç bellekte tek bir dizide tutulur: ata[i], i'nin ebeveynidir; kökte ata[r] = r'dir. Bu ağaçların, MST'nin kendisiyle bir ilgisi yoktur. Yalnız “kim hangi kümede?” bilgisini taşırlar ve biçimleri çizgenin kenarlarından farklı olabilir.

oluştur(V) her düğüm için ata[i] = i yazar. bul(x) ebeveyn bağlantılarını izleyerek köke kadar çıkar ve kökü döndürür. birleştir(x, y) iki kökü bulur ve birini öbürünün altına bağlar. Kruskal'da kullanım şöyledir: bul(u) == bul(v) ise u ve v aynı ağaçtadır ve kenar bir döngü kapatır. Sunumdaki Java kodu şöyledir:

int bul(int[] ata, int i) {
  if (ata[i] != i)
    ata[i] = bul(ata, ata[i]); // yol sıkıştırma
  return ata[i];
}
void birlestir(int[] ata, int x, int y) {
  ata[bul(ata, x)] = bul(ata, y);
}

bul özyinelemelidir. ata[i] == i ise i köktür ve kendisi döndürülür. Değilse ebeveynin kökü özyinelemeyle bulunur. Dönüşte bulunan kök ata[i]'ye yazılır. Bu atama, metodun asıl işi olan kökü bulmanın yanında bir yan etki yaratır: yol üzerindeki her düğüm doğrudan köke bağlanır. Bu iyileştirmeye yol kısaltma (path compression) denir; kodun yorumundaki “yol sıkıştırma” aynı işlemin öbür adıdır. birlestir ise tek satırdır: x'in kökünün ebeveyni, y'nin kökü yapılır. İki öğe zaten aynı kümedeyse iki bul aynı kökü döndürür ve atama kökü kendine bağlar; yapı değişmez.

4.3.3.2Kruskal'da döngü denetimi

Ayrık kümelerin Kruskal içinde nasıl çalıştığı, beş düğümlü bir çizge üzerinde izlenir. Kenarlar sıralıdır: ac 1, de 2, ab 3, bc 4, bd 5, cd 6, ce 7. Her kabul edilen (u, v) kenarı için ata[bul(u)] = bul(v) uygulanır.

Tablo 4.3.4: Kruskal'da ayrık kümelerle döngü denetimi. Son sütun ata dizisini a, b, c, d, e sırasıyla verir.
Kenarbul(u)bul(v)Kararata
başlangıç———a b c d e
ac = 1acekle, ata[a] = cc b c d e
de = 2deekle, ata[d] = ec b c e e
ab = 3cbekle, ata[c] = bc b b e e
bc = 4bbaynı kök: döngü, atlac b b e e
bd = 5beekle, ata[b] = ec e b e e

ab adımında bul(a) önce a'nın ebeveyni c'ye çıkar. c köktür; kök c döner. bul(b) ise b'nin kendisini döndürür. Kökler farklı olduğu için kenar eklenir ve c kökü b'nin altına bağlanır. bc adımında bul(b) = b ve bul(c) de c → b yoluyla b'yi bulur. Kökler aynıdır; kenar döngü kapatır. Dört kenar seçildiğinde MST tamamlanır: ac + de + ab + bd = 1 + 2 + 3 + 5 = 11.

1234567acbdeAyrık küme ormanı (ok: ebeveyn)abcdeata[ ]caebbcedee
Şekil 4.3.7: MST tamamlandıktan sonra ayrık küme ormanı. Ebeveyn bağlantıları a → c → b → e zincirini kurmuştur; bul(a) köke 3 adımda ulaşır.

MST doğru bulunmuştur, ama ayrık küme ormanı uzun bir zincire dönüşmüştür. bul(a) çağrısı a → c → b → e yolunu izler. Bu durumda yol kısaltma devreye girer. bul(a) dönerken yol üzerindeki her düğümü doğrudan köke bağlar: ata[a] = ata[c] = e olur. b zaten köke bağlıdır. Sonraki aramalar tek adımda biter.

1234567acbdeAyrık küme ormanı (ok: ebeveyn)abcdeata[ ]eaebecedee
Şekil 4.3.8: bul(a) çağrısından sonra orman: yol üzerindeki a ve c doğrudan e köküne bağlanmıştır; ata dizisi e e e e e olur.

4.3.3.3Kod izleme: bul ve birlestir

Yol kısaltmanın özyineleme içinde nasıl gerçekleştiği, 0, …, 4 öğeli küçük bir örnekle adım adım izlenir. Çağrı sırası birlestir(0, 1), birlestir(1, 2), birlestir(2, 3), bul(0) ve birlestir(4, 0)'dır.

Tablo 4.3.5: bul ve birlestir izlemesi. ata dizisi 0, 1, 2, 3, 4 indisleri için verilmiştir.
AdımOlayata
0başlangıç, her öğe kök[0, 1, 2, 3, 4]
1birlestir(0, 1): bul(0) = 0, bul(1) = 1 → ata[0] = 1[1, 1, 2, 3, 4]
2birlestir(1, 2): kökler 1 ve 2 → ata[1] = 2[1, 2, 2, 3, 4]
3birlestir(2, 3): kökler 2 ve 3 → ata[2] = 3[1, 2, 3, 3, 4]
4bul(0) iner: 0 → 1 → 2 → 3; 3 köktür[1, 2, 3, 3, 4]
5dönüş: ata[2] = 3 (değişmez)[1, 2, 3, 3, 4]
6dönüş: ata[1] = 3[1, 3, 3, 3, 4]
7dönüş: ata[0] = 3; bul(0) = 3[3, 3, 3, 3, 4]
8birlestir(4, 0): bul(4) = 4, bul(0) = 3 (tek adım) → ata[4] = 3[3, 3, 3, 3, 3]
ayrık küme ormanı (ok: ebeveyn)01234ata[ ]1021323344
Şekil 4.3.9: Üç birleştirmeden sonra orman: kök kökün altına bağlandığı için 0 → 1 → 2 → 3 zinciri oluşmuştur.

İlk üç birleştirme kötü bir örüntü üretir. Her seferinde bir kök öbür kökün altına bağlanır ve zincir bir uzar. bul(0) artık üç adım sürer. Genel durumda n öğe bu biçimde zincirlenirse tek bir bul n − 1 adım sürer. bul(0) çağrısı özyinelemeyle köke iner: önce bul(1), sonra bul(2), sonra bul(3) çağrılır. bul(3) kökü bulur ve 3 döndürür. Özyineleme geri sarılırken her çerçeve kendi ata[i] değerine 3 yazar. Sonuçta 0, 1 ve 2 doğrudan 3'e bağlanır. Son birleştirmede bul(0) tek adımda biter ve 4 de 3'ün altına girer; bütün öğeler tek kümededir.

ayrık küme ormanı (ok: ebeveyn)01234ata[ ]3031323344
Şekil 4.3.10: bul(0) çağrısından sonra orman: yol üzerindeki 0, 1 ve 2 doğrudan kök 3'e bağlanmıştır.
Sık yapılan hata

birlestir'i ata[x] = y diye yazmak. Bu atama x'in kendisini y'ye bağlar; x kök değilse x eski ağacından koparılır, ağacın geri kalanı ise y'nin kümesine katılmaz. Örneğin ata = [1, 1, 2] iken ata[0] = 2 yazılırsa 0 ile 2 birleşir, ama 1 dışarıda kalır. Her zaman kökler birbirine bağlanmalıdır: ata[bul(x)] = bul(y).

4.3.3.4Yol kısaltma ve rütbeye göre birleştirme

Ayrık kümelerin hızı ağaçların yüksekliğine bağlıdır. İki iyileştirme yüksekliği küçük tutar. Birincisi yukarıda görülen yol kısaltmadır. İkincisi birleştirme anında hangi kökün hangisinin altına gireceğini akıllıca seçer. Her kökte bir rütbe (rank) tutulur; rütbe, ağacın yüksekliği için bir üst sınırdır. Birleştirmede rütbesi küçük kök, rütbesi büyük kökün altına bağlanır. Rütbeler eşitse biri öbürünün altına girer ve yeni kökün rütbesi bir artar. Bu yönteme rütbeye göre birleştirme (union by rank) denir.

void birlestirRutbe(int[] ata, int[] rutbe, int x, int y) {
  int a = bul(ata, x), b = bul(ata, y);
  if (a == b) return;                    // zaten aynı küme
  if (rutbe[a] < rutbe[b]) ata[a] = b;   // alçak ağaç yüksek ağacın altına
  else if (rutbe[a] > rutbe[b]) ata[b] = a;
  else { ata[b] = a; rutbe[a]++; }       // eşitse biri altına, rütbe artar
}

Kod önce iki kökü bulur; kökler aynıysa iş yoktur. Rütbeler farklıysa alçak ağaç yüksek ağacın altına girer ve hiçbir rütbe değişmez: yüksek ağacın yüksekliği artmaz. Yalnız eşit rütbeli iki ağaç birleştiğinde yükseklik bir artabilir; bu yüzden yalnız o durumda rütbe artırılır. Önceki izlemedeki çağrı dizisi bu metotla tekrarlanırsa zincir oluşmaz. birlestirRutbe(0, 1) rütbeleri eşit bulur, 1'i 0'ın altına bağlar ve 0'ın rütbesini 1 yapar. Sonraki birleştirmelerde 2, 3 ve 4 tek öğeli, rütbesi 0 olan ağaçlardır; hepsi rütbesi 1 olan 0'ın altına girer. Sonuçta ata = [0, 0, 0, 0, 0] olur ve ağacın yüksekliği 1'de kalır.

Teorem 4.3.6 (Rütbe ve küme boyu)

Rütbeye göre birleştirmede rütbesi r olan bir kökün ağacında en az 2r öğe bulunur. Bu yüzden n öğeli yapıda her ağacın yüksekliği en fazla ⌊log2 n⌋'dir ve bir bul işlemi O(log n) sürer.

Tümevarım kullanılır. Başlangıçta her ağaç tek öğelidir ve rütbesi 0'dır: 1 ≥ 20. Rütbesi farklı iki ağaç birleşince yeni kökün rütbesi değişmez ama ağacı büyür; eşitsizlik korunur. Rütbesi r olan iki ağaç birleşince yeni kökün rütbesi r + 1 olur. Her iki ağaçta en az 2r öğe vardır; yeni ağaçta en az 2 · 2r = 2r+1 öğe bulunur. Toplam öğe sayısı n olduğu için 2r ≤ n, yani r ≤ log2 n'dir. Yükseklik rütbeyi aşmadığı için aynı sınır yükseklik için de geçerlidir. □

Sınır sıkıdır. 8 öğe ikişer ikişer, sonra dörder dörder, sonra sekizli olarak eşit rütbeli ağaçlar hâlinde birleştirilirse rütbe 3'e ulaşır; 1024 öğede aynı yöntem rütbeyi 10'a çıkarır. Yol kısaltma da eklendiğinde yapı çok daha hızlıdır. Robert Tarjan 1975'te iki iyileştirme birlikte kullanıldığında m işlemin toplam maliyetinin O(m · α(n)) olduğunu gösterdi. Burada α, çok hızlı büyüyen Ackermann fonksiyonunun tersidir. Ters Ackermann fonksiyonu o kadar yavaş büyür ki evrendeki atom sayısından büyük her pratik n için α(n) ≤ 4'tür. Bu yüzden işlem başına maliyet amortize olarak sabit kabul edilir. Amortize maliyet, uzun bir işlem dizisinde işlem başına düşen ortalama maliyettir. Tek bir bul yine de uzun sürebilir, ama bu uzun yürüyüş sonraki çağrıları hızlandırır.

Sık yapılan hata

Yol kısaltmanın tek bir bul çağrısını hızlandırdığını sanmak. Kısaltma yapan çağrının kendisi uzun yolu baştan sona yürür. Kazanç sonraki çağrılardadır: yol üzerindeki düğümler artık köke bir adım uzaklıktadır. Tek bir işlemin en kötü maliyeti ile amortize maliyet bu yüzden ayrı ayrı değerlendirilmelidir.

4.3.4Kruskal kodu ve karmaşıklığı

4.3.4.1Java kodu

Kruskal'ın Java uygulaması bir kenar sınıfına dayanır. Kenar iki uç düğümü ve ağırlığı tutar. Collections.sort ile sıralanabilmesi için Comparable arayüzünü uygular ve kenarları ağırlığa göre karşılaştırır:

class Kenar implements Comparable<Kenar> {
  int u, v, w;                  // uçlar ve ağırlık
  Kenar(int u, int v, int w) { this.u = u; this.v = v; this.w = w; }
  public int compareTo(Kenar o) {
    return Integer.compare(w, o.w);   // ağırlığa göre artan
  }
}

Integer.compare(w, o.w), w − o.w çıkarmasının güvenli karşılığıdır. Çok büyük ya da negatif ağırlıklarda çıkarma taşabilir; Integer.compare taşmaz. Düğümler 1'den V'ye kadar numaralanır. Sunumdaki Kruskal kodu şöyledir:

void kruskal(int V, List<Kenar> ke) {
  Collections.sort(ke);         // artan
  int[] ata = new int[V + 1];
  for (int i = 1; i <= V; i++) ata[i] = i;
  List<Kenar> agac = new ArrayList<>();
  int k = 0;
  while (agac.size() < V - 1) {
    Kenar e = ke.get(k++);
    int x = bul(ata, e.u);
    int y = bul(ata, e.v);
    if (x != y) {               // ayrı ağaç
      agac.add(e);
      ata[x] = y;               // birleştir
    }
  }
}

Kod satır satır şöyle çalışır. Collections.sort(ke) kenar listesini ağırlığa göre artan sıraya koyar. Java'nın liste sıralaması kararlıdır: eşit ağırlıklı kenarlar listedeki ilk sıralarını korur. ata dizisi V + 1 uzunluktadır, çünkü düğümler 1'den başlar ve 0. hücre kullanılmaz. Döngü her düğümü kendi kümesinin kökü yapar. agac seçilen kenarları, k ise sıralı listede sıradaki kenarın indisini tutar.

while (agac.size() < V - 1) döngüsü V − 1 kenar seçilene kadar sürer. Her turda sıradaki kenar alınır ve k bir artar. İki ucun kökü x ve y olarak bulunur. Kökler farklıysa kenar ağaca eklenir ve ata[x] = y ile iki küme birleşir. Burada birlestir çağrılmaz, çünkü kökler zaten bulunmuştur; atama doğrudan yapılır. Kökler aynıysa hiçbir şey yapılmaz ve döngü sıradaki kenara geçer.

4.3.4.2Kod izleme: beş düğümlü çizge

Kod, düğümleri 1–5 olan bir çizge üzerinde izlenir. Kenarlar (1,3) = 2, (2,5) = 2, (1,2) = 3, (2,3) = 4, (3,4) = 4 ve (1,4) = 6'dır.

22344612534
Şekil 4.3.11: Beş düğümlü çizge: (1,3) = 2, (2,5) = 2, (1,2) = 3, (2,3) = 4, (3,4) = 4, (1,4) = 6. Aynı çizge Prim izlemelerinde de kullanılır.
Tablo 4.3.6: kruskal(5, ke) izlemesi. ata 1–5 indisleri için verilmiştir.
Kenar ex = bul(u)y = bul(v)İşlemataw(T)
———ilkleme[1, 2, 3, 4, 5]0
(1,3) = 213ekle, ata[1] = 3[3, 2, 3, 4, 5]2
(2,5) = 225ekle, ata[2] = 5[3, 5, 3, 4, 5]4
(1,2) = 335ekle, ata[3] = 5[3, 5, 5, 4, 5]7
(2,3) = 455döngü, atla[3, 5, 5, 4, 5]7
(3,4) = 454ekle, ata[5] = 4[3, 5, 5, 4, 4]11

Üçüncü satırda bul(1) 1'in ebeveyni 3'e çıkar ve 3'ü döndürür; bul(2) 5'i döndürür. Kökler farklıdır ve ata[3] = 5 ile iki ağaç birleşir. Dikkat edilirse değişen hücre 1'in ya da 2'nin hücresi değil, kök 3'ün hücresidir. Dördüncü satırda bul(2) ve bul(3) ikisi de 5'i bulur; (2,3) iki ayrı ağacı birleştirmediği için döngü kurar ve atlanır. Beşinci satırda 4 tek başınadır; (3,4) eklenir ve ata[5] = 4 olur. agac.size() artık 4 = V − 1'dir; döngü biter. (1,4) = 6 hiç incelenmez. MST (1,3), (2,5), (1,2), (3,4) kenarlarından oluşur ve w(T) = 11'dir.

22344612534
Şekil 4.3.12: Kruskal'ın sonucu: (1,3), (2,5), (1,2), (3,4); toplam ağırlık 11.

İzlemede eşit ağırlıklı iki çift vardır: (1,3) ile (2,5) ve (2,3) ile (3,4). Kararlı sıralama onları listedeki sırada bırakır. (3,4) listede (2,3)'ten önce gelseydi önce (3,4) eklenir, sonra (2,3) yine döngü gerekçesiyle atlanırdı. Bu çizgede sonuç değişmez. (2,3)'ün atlanmasının nedeni 2 → 1 → 3 yolunun daha ucuz kenarlarla zaten kurulmuş olmasıdır.

4.3.4.3Dokuz düğümlü örnek

Daha büyük bir örnek olarak A, …, I düğümlü, 15 kenarlı çizge ele alınır. Sıralı kenarlar şöyledir:

EF 1, CF 1, DE 2, FH 3, AB 4, EH 5, GI 5, DG 6, AC 7, BD 9, EG 10, BC 11, HI 12, EI 15, BF 20

EF1CF1DE2FH3AB4EH5GI5DG6AC7BD9EG10BC11HI12EI15BF2047119201126101553512ABCDEFGHIKümeler: tek düğümlüler · Ağırlık: 0 · Bağlı: HAYIR
Şekil 4.3.13: Dokuz düğümlü çizge ve sıralı kenar tablosu. Henüz hiçbir kenar seçilmemiştir.
Tablo 4.3.7: Dokuz düğümlü çizgede Kruskal.
KenarKararKümeler (tek düğümlüler hariç)Toplam
EF 1ekle{E, F}1
CF 1ekle{C, E, F}2
DE 2ekle{C, D, E, F}4
FH 3ekle{C, D, E, F, H}7
AB 4ekle{C, D, E, F, H} {A, B}11
EH 5döngü (E–F–H)değişmez11
GI 5ekle{C, D, E, F, H} {A, B} {G, I}16
DG 6ekle{C, D, E, F, G, H, I} {A, B}22
AC 7ekle{A, B, C, D, E, F, G, H, I}29

Dokuzuncu incelenen kenar AC = 7'dir ve sekizinci eklenen kenardır. 8 = V − 1 olduğu için algoritma burada durur; BD 9'dan sonraki altı kenara bakılmaz. MST'nin ağırlığı 29'dur. Bu ağaç, uygulama alanlarında söz edilen kümeleme örneğinin de kaynağıdır. En ağır iki MST kenarı AC = 7 ve DG = 6'dır. Bu iki kenar silinirse üç küme kalır: {A, B}, {C, D, E, F, H} ve {G, I}.

EF1CF1DE2FH3AB4EH5GI5DG6AC7BD9EG10BC11HI12EI15BF2047119201126101553512ABCDEFGHIKümeler: {C, D, E, F, H} {A, B} · Ağırlık: 11 · Bağlı: HAYIR
Şekil 4.3.14: EH = 5 adımı. E ve H, E–F–H yolu üzerinden zaten aynı kümededir; kenar döngü kurar ve eklenmez.
EF1CF1DE2FH3AB4EH5GI5DG6AC7BD9EG10BC11HI12EI15BF2047119201126101553512ABCDEFGHIKümeler: {A, B, C, D, E, F, G, H, I} · Ağırlık: 29 · Bağlı: EVET
Şekil 4.3.15: AC = 7 eklendikten sonra: sekiz kenar seçilmiştir, çizge bağlıdır ve MST ağırlığı 29'dur.

4.3.4.4Karmaşıklık

Kruskal'ın maliyeti işlemlerine ayrılarak hesaplanır. Tablo 4.3.8 her işlemin kaç kez yapıldığını ve maliyetini verir.

Tablo 4.3.8: Kruskal'ın karmaşıklık analizi.
İşlemKaç kezMaliyet
Kenarları ağırlığa göre sıralama1O(E log E)
oluştur(V)1O(V)
bul (her kenar için 2)≤ 2EO(E · α(V)) yol kısaltma + rütbe ile; O(E log V) yalnız rütbe ile
birleştirV − 1

Toplam O(E log E)'dir ve bu maliyeti sıralama belirler. Bağlı bir çizgede E ≥ V − 1 olduğu için O(V)'lik ilkleme de sıralamanın yanında küçük kalır. Çoğu kaynakta aynı sınır O(E log V) diye yazılır. İki yazım aynı mertebeyi gösterir. Yönsüz basit bir çizgede E ≤ V(V − 1)/2 < V2'dir. Logaritma alınırsa log E < 2 log V bulunur. Sabit çarpanlar mertebeyi değiştirmediği için O(E log E) = O(E log V)'dir. Örneğin V = 1000 düğümlü tam çizgede E = 499.500'dür; log2 E ≈ 18,9, 2 log2 V ≈ 19,9'dur.

Kenarlar zaten sıralı geliyorsa ya da ağırlıklar küçük tam sayılarsa durum değişir. İkinci durumda saymalı sıralama kenarları doğrusal zamanda sıralar. Bu durumlarda toplam süre neredeyse doğrusaldır: O(E · α(V)). Ayrık kümelerde yalnız rütbeye göre birleştirme kullanılırsa her işlem en kötü O(log V) sürer; yol kısaltma da eklenince işlem başına amortize maliyet O(α(V))'ye iner. Her iki durumda da toplamı sıralama belirler. Sunumdaki kod yalnız yol kısaltma yapar; rütbe kullanmaz. Bu kodun da pratikte çok hızlı olduğu bilinir.

Bellek gereksinimi kenar listesi ve ebeveyn dizisi kadardır: O(V + E). Kenar listesi zaten girdinin kendisidir; ek bellek ata dizisi ve seçilen V − 1 kenardır.

4.3.4.5Bağlı olmayan çizge

Sunumdaki kod, çizgenin bağlı olduğunu varsayar. Bağlı olmayan bir çizgede V − 1 kenar asla seçilemez. Liste bittiğinde döngü koşulu hâlâ doğrudur ve ke.get(k++) liste sınırının dışına çıkar.

Örnek 4.3.4

V = 6 ve kenarlar (1,2) = 2, (2,3) = 3, (4,5) = 1, (5,6) = 4, (4,6) = 5 olsun. Çizgenin iki bileşeni vardır: {1, 2, 3} ve {4, 5, 6}. Sıralı liste (4,5), (1,2), (2,3), (5,6), (4,6)'dır. İlk dört kenar eklenir; (4,6) döngü kurar ve atlanır. agac.size() = 4 < 5 olduğu için döngü sürer ve ke.get(5) çağrısı IndexOutOfBoundsException fırlatır.

Çözüm, döngü koşuluna liste sınırını eklemektir. Sonda agac.size() < V − 1 ise çizge bağlı değildir. Bu durumda seçilen kenarlar bir minimum kapsayan orman oluşturur: her bileşen için bir minimum kapsayan ağaç.

List<Kenar> kruskalOrman(int V, List<Kenar> ke) {
  Collections.sort(ke);
  int[] ata = new int[V + 1];
  for (int i = 1; i <= V; i++) ata[i] = i;
  List<Kenar> agac = new ArrayList<>();
  int k = 0;
  while (k < ke.size() && agac.size() < V - 1) {
    Kenar e = ke.get(k++);
    int x = bul(ata, e.u), y = bul(ata, e.v);
    if (x != y) { agac.add(e); ata[x] = y; }
  }
  return agac;                  // agac.size() < V - 1 ise orman
}

Örnekteki çizgede bu sürüm (4,5), (1,2), (2,3), (5,6) kenarlarını döndürür. Orman iki ağaçlıdır ve toplam ağırlığı 1 + 2 + 3 + 4 = 10'dur. Kenar sayısı V − 2'dir. Genel olarak c bileşenli bir çizgenin kapsayan ormanında V − c kenar bulunur.

4.3.5Prim algoritması

Kruskal bütün çizgeye yayılmış küçük ağaçlarla çalışır. Prim ise tek bir ağaçla başlar ve onu adım adım büyütür. Algoritma Robert C. Prim'in 1957 tarihli makalesiyle tanındı. Aynı yöntemi Vojtěch Jarník 1930'da bulmuştu; Edsger W. Dijkstra 1959'da yeniden keşfetti. Bu yüzden bazı kaynaklarda Jarník algoritması ya da Prim–Jarník algoritması adıyla geçer.

4.3.5.1Fikir: tek ağacı büyütmek

Bir başlangıç düğümü seçilir; ağaç başta yalnız bu düğümden oluşur. Her adımda ağaçta olmayan düğümler arasından, ağaca en ucuz kenarla bağlanan düğüm seçilir. Bu düğüm ve onu bağlayan kenar ağaca eklenir. Tüm düğümler ağaca girince algoritma biter. Her an tek bir ağaç vardır; Kruskal'daki orman yoktur.

236471sabcdkesimi geçen en ucuz kenar: 4
Şekil 4.3.16: Prim'in ara durumu. Ağaç {s, a, b}'dir. Kesikli çizgi ağaç ile geri kalan düğümler arasındaki kesimi gösterir. Kesimi geçen kenarlar 6, 7 ve 4'tür; en ucuzu 4 seçilir ve d ağaca girer.

Prim her adımda bir kesimin en hafif kenarını seçer: kesim, o ana kadar kurulan ağaç ile geri kalan düğümler arasındadır. Şekildeki c–d = 1 kenarı çizgenin en hafif kenarıdır, ama kesimi geçmez; iki ucu da ağaçta değildir. Prim bu kenarı ancak d ağaca girdikten sonra kullanabilir. Kruskal ise aynı çizgede işe c–d = 1 ile başlardı.

Algoritmanın değerlendirmesi şöyledir. Prim yoğun çizgelerde çok uygundur: komşuluk matrisi ile O(V2) sürede çalışır. Kenarları sıralamaya gerek yoktur. Komşuluk listesi ya da matrisiyle doğal biçimde çalışır. Zayıf yanı, başarımının kullanılan veri yapısına bağlı olmasıdır: dizi, ikili heap ve Fibonacci heap farklı süreler verir. Bağlı olmayan çizgede yalnız başlangıç düğümünün bileşenini kapsar.

4.3.5.2Anahtar ve ebeveyn

“Ağaca en ucuz kenarla bağlanan düğüm” her adımda yeniden aranırsa algoritma yavaşlar. Prim bu bilgiyi her düğüm için iki dizide saklar ve ağaç büyüdükçe günceller.

Tanım 4.3.5

Prim algoritmasında ağaçta olmayan bir v düğümü için:

  • anahtar[v] (key): v'yi şimdiye kadar kurulan ağaca bağlayan en ucuz kenarın ağırlığıdır. Başta ∞, kaynakta 0'dır.
  • ata[v] (parent, π): bu en ucuz kenarın ağaçtaki ucudur. Algoritma bittiğinde (ata[v], v) kenarları MST'yi verir.

Ağaca yeni bir u düğümü girdiğinde yalnız u'nun komşularının anahtarı değişebilir; çünkü ağaca bağlanmanın yeni yolları yalnız u üzerinden açılır. Ağaçta olmayan her v komşusu için w(u, v) < anahtar[v] ise anahtar ve ebeveyn güncellenir: anahtar[v] = w(u, v), ata[v] = u. Ardından anahtarı en küçük olan ağaç dışı düğüm seçilir. Bu düğümün anahtarı, kesimi geçen en hafif kenarın ağırlığıdır.

4.3.5.3Sözde kod

Prim'in adımları şöyle sıralanabilir:

  1. Başlangıç düğümü seçilir ve bu düğüme ait bütün kenarlar öncelikli kuyruğa eklenir.
  2. Kuyruktan, ağaçta olmayan bir düğüme giden en küçük ağırlıklı kenar seçilir.
  3. Seçilen kenar ile bağlantılı yeni düğüm ağaca eklenir.
  4. Yeni düğüme bağlı kenarlar kuyruğa eklenir; bütün düğümler ağaca girene kadar 2–4. adımlar tekrarlanır.

Sunumdaki sözde kod iki parçadan oluşur. İlk parça dizileri ilkler, ikinci parça ana döngüdür:

PRIM(G, kaynak):
  MST = [ ]
  mesafeler = [ ]
  ata = [ ]
  ziyaretEdildi = [ ]
  her bir v için V içinde:
    mesafeler[v] = ∞
    ziyaretEdildi[v] = 0
  mesafeler[kaynak] = 0
  öncelikKuyruğu.ekle(kaynak, 0)
döngü öncelikKuyruğu boş değil iken:
  u = öncelikKuyruğu.çıkar()
  eğer ziyaretEdildi[u] == 0:
    ziyaretEdildi[u] = 1
    her bir (u, v, ağırlık) için G[u] içinde:
      eğer ziyaretEdildi[v] == 0 ve
           ağırlık < mesafeler[v]:
        mesafeler[v] = ağırlık
        ata[v] = u
        öncelikKuyruğu.ekle(v, ağırlık)
her bir v için V içinde (kaynak hariç):
  MST.ekle((ata[v], v, mesafeler[v]))
döndür MST

Sözde koddaki mesafeler dizisi, tanımdaki anahtar dizisidir. Ad yanıltıcı olabilir: değer bir uzaklık değil, tek bir kenarın ağırlığıdır. Kod bir düğümü kuyruğa birden çok kez ekleyebilir. Bir düğümün anahtarı her küçüldüğünde kuyruğa yeni bir kayıt girer; eski kayıt silinmez, kuyrukta kalır. Bu yüzden bu sürüme tembel (lazy) Prim denir. Eski kayıt sonunda kuyruktan çıktığında ziyaretEdildi[u] denetimi onu atar. Kuyruk en fazla E kadar kayıt tutar; her kenar bir anahtar küçülmesine yol açabilir. Toplam süre O(E log E)'dir.

Sık yapılan hata

Tembel sürümde ziyaretEdildi denetimini atlamak. Eski bir kayıt kuyruktan çıktığında düğüm zaten ağaçtadır. Denetim olmazsa düğüm ikinci kez “ağaca girer”, komşuları yeniden incelenir ve ebeveyn dizisi bozulabilir. Her kuyruk çıkarımından sonra ilk iş, düğümün daha önce işlenip işlenmediğine bakmaktır.

4.3.5.4Adım adım: öncelikli kuyruk

Prim, Şekil 4.3.11'daki beş düğümlü çizgede 1'den başlatılır. Burada ders kitaplarındaki klasik gösterim kullanılır: kuyruk Q bütün düğümlerle başlar ve her düğüm anahtarıyla birlikte kuyrukta durur. Tablolardaki π, ebeveyndir; “/” henüz ebeveyn atanmadığını gösterir.

Tablo 4.3.9: Öncelikli kuyruklu Prim, kaynak 1. Kuyruk içeriği anahtara göre sıralıdır; her öğe düğüm (anahtar, π) biçimindedir.
AdımÇıkan düğümEklenen kenarKuyruk Q (sonrası)w(T)
0——1 (0, /), 2 (∞, /), 3 (∞, /), 4 (∞, /), 5 (∞, /)0
11—3 (2, 1), 2 (3, 1), 4 (6, 1), 5 (∞, /)0
23(1,3) = 22 (3, 1), 4 (4, 3), 5 (∞, /)2
32(1,2) = 35 (2, 2), 4 (4, 3)5
45(2,5) = 24 (4, 3)7
54(3,4) = 4boş11

Adımlar şöyle açıklanır. İlklemede yalnız kaynağın anahtarı 0'dır; bu yüzden ilk çıkan düğüm 1'dir. 1'in komşuları güncellenir: key(3) = 2, key(2) = 3, key(4) = 6. İkinci adımda en küçük anahtar 3'ündür; 3 çıkar ve (1,3) ağaca girer. 3'ün komşusu 4 için (3,4) = 4 < 6 olduğundan key(4) = 4, π(4) = 3 olur. Öbür komşu 2 için (2,3) = 4 > key(2) = 3 olduğundan değişiklik yoktur. Üçüncü adımda 2 çıkar ve (1,2) eklenir; 5'in anahtarı 2 olur. Dördüncü adımda 5 çıkar; ağaçta olmayan başka komşusu yoktur. Son adımda 4 çıkar ve (3,4) eklenir.

22344612534
Şekil 4.3.17: 1 kuyruktan çıktıktan sonra durum. Kesikli mavi çizgiler π ile gösterilen, henüz kesinleşmemiş en ucuz bağlantılardır: 3'e 2, 2'ye 3, 4'e 6.

Sonuç ağaç (1,3), (1,2), (2,5), (3,4) kenarlarından oluşur ve w(T) = 11'dir. Kruskal aynı çizgede aynı ağacı bulmuştu. Kenarların eklenme sırası ise farklıdır: Kruskal 2, 2, 3, 4 sırasıyla ekler; Prim 2, 3, 2, 4 sırasıyla. Prim'in 2'lik (2,5) kenarını 3'lük (1,2)'den sonra eklemesinin nedeni, 5'in ancak 2 ağaca girdikten sonra ağaca komşu olmasıdır.

22344612534
Şekil 4.3.18: Prim'in son durumu: kuyruk boştur. MST (1,3), (1,2), (2,5), (3,4) kenarlarından oluşur; w(T) = 11.

4.3.5.5Prim neden doğrudur?

Teorem 4.3.7 (Prim'in doğruluğu)

Bağlı bir çizgede Prim algoritmasının seçtiği (ata[v], v) kenarları bir minimum kapsayan ağaç oluşturur.

Bir adımın başında ağaçtaki düğümler S olsun ve (S, V − S) kesimi düşünülsün. Seçilmiş kenarların hepsinin iki ucu S'dedir; hiçbiri kesimi geçmez. anahtar[v], v'den S'ye giden en ucuz kenarın ağırlığıdır. Bu değişmez her adımda korunur, çünkü ağaca yeni düğüm girdiğinde yalnız onun komşularının değeri değişebilir ve güncelleme tam bunu yapar. Anahtarı en küçük olan u seçildiğinde (ata[u], u) kesimi geçen en hafif kenardır. Kesim özelliğine göre bu kenar güvenlidir. Her adımda ağaca bir düğüm ve bir güvenli kenar eklenir; V − 1 adım sonunda bir MST elde edilir. □

İspattan çıkan bir sonuç da başlangıç düğümünün önemsiz olduğudur. Farklı bir düğümden başlayan Prim, kenarları farklı sırada ekler. Ağırlıklar farklıysa sonunda yine aynı ağacı bulur; eşit ağırlıklar varsa farklı ama aynı ağırlıkta bir ağaç bulabilir.

4.3.6Prim kodu ve karmaşıklığı

4.3.6.1Komşuluk matrisli sürüm

Prim'in en kısa uygulaması öncelikli kuyruk kullanmaz. Çizge bir komşuluk matrisiyle verilir: G[u][v], u ile v arasındaki kenarın ağırlığıdır; kenar yoksa 0'dır. Anahtarı en küçük düğüm her adımda dizinin baştan sona taranmasıyla bulunur.

void prim(int[][] G, int kaynak) {
  int V = G.length;
  int[] anahtar = new int[V];
  int[] ata = new int[V];
  boolean[] agacta = new boolean[V];
  Arrays.fill(anahtar, Integer.MAX_VALUE);
  anahtar[kaynak] = 0;
  ata[kaynak] = -1;
  for (int s = 0; s < V - 1; s++) {
    int u = enKucukAnahtar(anahtar, agacta);
    agacta[u] = true;
    for (int v = 0; v < V; v++)
      if (G[u][v] != 0 && !agacta[v]
          && G[u][v] < anahtar[v]) {
        anahtar[v] = G[u][v];
        ata[v] = u;
      }
  }
}

Kod satır satır şöyle çalışır. anahtar dizisinin bütün hücrelerine Integer.MAX_VALUE yazılır; bu değer ∞ yerine kullanılır. Kaynağın anahtarı 0, ebeveyni −1'dir; −1 “ebeveyn yok” anlamına gelir. agacta dizisi hangi düğümlerin ağaca girdiğini tutar.

Dış döngü V − 1 kez döner. Her turda enKucukAnahtar ağaçta olmayan düğümler arasında anahtarı en küçük olanı bulur ve bu düğüm ağaca alınır. İç döngü bu düğümün bütün komşularını gezer. Üç koşul birlikte aranır: G[u][v] != 0 kenarın var olduğunu, !agacta[v] komşunun henüz ağaçta olmadığını, G[u][v] < anahtar[v] ise yeni kenarın eski en iyiden ucuz olduğunu söyler. Üçü de doğruysa anahtar ve ebeveyn güncellenir.

Dış döngünün V değil V − 1 kez dönmesi bilinçli bir seçimdir. V − 1 düğüm ağaca girdiğinde son düğümün anahtarı ve ebeveyni artık değişemez: onu ağaca bağlayan bütün kenarların öbür ucu ağaçtadır ve hepsi incelenmiştir. Son düğüm agacta dizisinde işaretlenmez, ama ata değeri doğrudur. Sunumdaki kod enKucukAnahtar yardımcısını kullanır. Bu yardımcı şöyle yazılabilir:

int enKucukAnahtar(int[] anahtar, boolean[] agacta) {
  int enKucuk = Integer.MAX_VALUE, indis = -1;
  for (int v = 0; v < anahtar.length; v++)
    if (!agacta[v] && anahtar[v] < enKucuk) {
      enKucuk = anahtar[v];
      indis = v;
    }
  return indis;
}

Yardımcı, eşitlikte küçük indisli düğümü seçer, çünkü karşılaştırma < ile yapılır: sonra gelen eşit anahtar, bulunan en küçüğü değiştirmez. Bağlı olmayan bir çizgede ağaç dışındaki bütün anahtarlar ∞ kalabilir; o zaman metot −1 döndürür ve prim içinde agacta[-1] erişimi hata verir. Kod bağlı çizge varsayar.

4.3.6.2Kod izleme: A–F çizgesi

Kod altı düğümlü bir çizgede izlenir. Düğümler A–F'dir ve indisleri 0–5'tir. Kaynak B'dir (indis 1). Kenarlar AB = 1, BC = 3, AD = 3, BD = 2, BE = 6, BF = 5, CE = 4, CF = 4, DE = 3 ve EF = 2'dir.

1332654432ABCDEF
Şekil 4.3.19: A–F çizgesi. Kaynak B'dir; başta bütün anahtarlar ∞, anahtar[B] = 0'dır.
Tablo 4.3.10: prim(G, 1) izlemesi. Anahtar dizisi A, B, C, D, E, F sırasıyla verilmiştir; ∞ başlangıç değeridir.
Tur suGüncellenen komşularanahtarw(T)
——ilkleme∞, 0, ∞, ∞, ∞, ∞0
0B (0)A → 1, C → 3, D → 2, E → 6, F → 5 (ebeveyn hepsinde B)1, 0, 3, 2, 6, 50
1A (1)yok: AD = 3 > anahtar[D] = 21, 0, 3, 2, 6, 51
2D (2)E → 3 (ebeveyn D)1, 0, 3, 2, 3, 53
3C (3)F → 4 (ebeveyn C); CE = 4 > 3, E değişmez1, 0, 3, 2, 3, 46
4E (3)F → 2 (ebeveyn E)1, 0, 3, 2, 3, 29
—(F)döngü biter; F'nin ebeveyni E1, 0, 3, 2, 3, 211

İlk turda enKucukAnahtar B'yi döndürür, çünkü yalnız B'nin anahtarı sonludur. B'nin beş komşusu da güncellenir. İkinci turda en küçük anahtar A'nındır (1); (B, A) ağaca girer. A'nın ağaç dışı tek komşusu D'dir, ama AD = 3, D'nin mevcut anahtarı 2'den büyüktür. Üçüncü turda D (2) girer ve E'nin anahtarı 6'dan 3'e iner: E'ye D üzerinden bağlanmak B üzerinden bağlanmaktan ucuzdur.

Dördüncü turda C ve E'nin anahtarları eşittir (3). enKucukAnahtar küçük indisli C'yi seçer. C'den F'ye 4'lük kenar, F'nin mevcut anahtarı 5'ten ucuzdur; F'nin ebeveyni C olur. Beşinci turda E (3) girer ve EF = 2 ile F'nin anahtarı 2'ye iner. Dış döngü beş tur döner ve biter. F ağaca resmen alınmaz, ama ebeveyni E'dir. MST (ata[v], v) kenarlarından okunur: BA, BC, BD, DE, EF. Toplam 1 + 3 + 2 + 3 + 2 = 11'dir.

1332654432ABCDEF
Şekil 4.3.20: Dördüncü tur: C ve E'nin anahtarları eşittir (3); küçük indisli C ağaca girer. Mavi kesikli kenarlar, henüz ağaçta olmayan düğümlerin mevcut en ucuz bağlantılarıdır.

Eşitlikte E seçilseydi ne olurdu? E girer, F'nin anahtarı 2 olur; sonra C (3) ve F (2) yarışır ve F seçilir. Sonunda C de girer. Seçilen kenarlar yine BA, BD, DE, EF, BC olurdu; yalnız sıra değişirdi. Bu çizgede eşit ağırlıklı kenarlar bulunmasına rağmen MST tektir.

1332654432ABCDEF
Şekil 4.3.21: Prim'in sonucu: AB, BD, BC, DE, EF; toplam 11.

4.3.6.3Tembel öncelikli kuyruk sürümü

Seyrek çizgelerde dizinin her turda baştan sona taranması gereksiz bir maliyettir. Java'nın PriorityQueue sınıfı en küçük öğeyi O(log n) sürede çıkarır. Sunumdaki tembel sürüm, sözde koddaki yöntemi Java ile yazar. Çizge komşuluk listesiyle verilir: komsu.get(u), u'dan çıkan kenarların listesidir. anahtar, ata, agacta ve komsu sınıf alanlarıdır.

void primPQ(int kaynak) {
  Arrays.fill(anahtar, SONSUZ);
  anahtar[kaynak] = 0;
  PriorityQueue<int[]> pq =
      new PriorityQueue<>((a, b) -> a[1] - b[1]);
  pq.add(new int[] { kaynak, 0 });
  while (!pq.isEmpty()) {
    int u = pq.poll()[0];
    if (agacta[u]) continue;  // eski
    agacta[u] = true;
    for (Kenar k : komsu.get(u))
      if (!agacta[k.v] && k.w < anahtar[k.v]) {
        anahtar[k.v] = k.w; ata[k.v] = u;
        pq.add(new int[] { k.v, k.w });
      }
  }
}

Kuyruktaki her kayıt iki öğeli bir dizidir: {düğüm, anahtar}. Karşılaştırıcı a[1] - b[1] kayıtları anahtara göre sıralar. Ağırlıklar küçük olduğu sürece çıkarma güvenlidir; çok büyük ağırlıklarda Integer.compare(a[1], b[1]) yazmak daha doğrudur. Döngü her turda en küçük kaydı çıkarır. Düğüm zaten ağaçtaysa kayıt eskidir ve continue ile atlanır. Değilse düğüm ağaca girer ve komşuları incelenir. Anahtarı küçülen her komşu için kuyruğa yeni bir kayıt eklenir. Eski kayıt silinmez; silme işi, kaydın kuyruktan çıkacağı ana ertelenir. Bu ertelemeye tembel silme denir.

Kod beş düğümlü çizgede, kaynak 1 ile izlenir. Tablo 4.3.11'deki kuyruk sütunu, kaydı çıkarmadan önceki içeriği anahtara göre sıralı verir.

Tablo 4.3.11: primPQ(1) izlemesi. Kayıtlar (düğüm, anahtar) biçimindedir; anahtar 1–5 indisleri için verilmiştir.
Kuyruk (önce)Çıkanİşlemanahtar
(1,0)(1,0)1 ağaca girer; 2 → 3, 3 → 2, 4 → 60, 3, 2, 6, ∞
(3,2) (2,3) (4,6)(3,2)3 girer (kenar 1–3); 4 → 4; 2 için 4 > 3, değişmez0, 3, 2, 4, ∞
(2,3) (4,4) (4,6)(2,3)2 girer (kenar 1–2); 5 → 20, 3, 2, 4, 2
(5,2) (4,4) (4,6)(5,2)5 girer (kenar 2–5); güncelleme yok0, 3, 2, 4, 2
(4,4) (4,6)(4,4)4 girer (kenar 3–4); güncelleme yok0, 3, 2, 4, 2
(4,6)(4,6)4 zaten ağaçta: eski kayıt, continue0, 3, 2, 4, 2

İkinci satırda 4'ün anahtarı 6'dan 4'e iner ve kuyruğa (4,4) kaydı eklenir. Eski (4,6) kaydı kuyrukta kalır. 4 ağaca (4,4) kaydıyla girer. (4,6) en son çıkar ve atlanır. Kuyruk boşalınca MST kenarları (ata[v], v) olarak okunur: 1–3 (2), 1–2 (3), 2–5 (2) ve 3–4 (4); toplam 11.

22344612534pq
Şekil 4.3.22: Son adım: eski (4,6) kaydı çıkarıldı ve atlandı; kuyruk boştur. Ağaç kenarları 1–3, 1–2, 2–5, 3–4'tür.

Tembel sürümün karşılığı, anahtarı yerinde küçülten istekli (eager) sürümdür. İstekli sürüm her düğüm için kuyrukta tek kayıt tutar ve anahtar küçüldüğünde o kaydı günceller. Bu işleme anahtar azaltma (decrease-key) denir. Java'nın PriorityQueue sınıfı bu işlemi doğrudan sunmaz; bu yüzden Java uygulamalarında tembel sürüm yaygındır. Tembel sürüm kuyrukta V yerine en fazla E kayıt tutar, ama log E ≤ 2 log V olduğu için mertebe değişmez.

4.3.6.4Dokuz düğümlü örnek

Kruskal'ın dokuz düğümlü örneği Prim ile A'dan başlanarak çözülür. Her satır, o adımda ağaca giren düğümden sonraki anahtar ve ebeveyn değerlerini verir.

Tablo 4.3.12: Dokuz düğümlü çizgede Prim (A'dan). Her hücrede anahtar / ebeveyn yazılıdır; “−” ebeveyn atanmadığını gösterir.
GirenABCDEFGHIToplam
A0/−4/A7/A∞∞∞∞∞∞0
B0/−4/A7/A9/B∞20/B∞∞∞4
C0/−4/A7/A9/B∞1/C∞∞∞11
F0/−4/A7/A9/B1/F1/C∞3/F∞12
E0/−4/A7/A2/E1/F1/C10/E3/F15/E13
D0/−4/A7/A2/E1/F1/C6/D3/F15/E15
H0/−4/A7/A2/E1/F1/C6/D3/F12/H18
G0/−4/A7/A2/E1/F1/C6/D3/F5/G24
I0/−4/A7/A2/E1/F1/C6/D3/F5/G29

Tablo birkaç şeyi açıkça gösterir. F'nin anahtarı önce B üzerinden 20 olur, C girince 1'e iner. D'nin anahtarı B üzerinden 9'dur, E girince 2'ye iner. I'nın anahtarı üç kez değişir: E üzerinden 15, H üzerinden 12, G üzerinden 5. Bir düğümün anahtarı yalnız küçülür; büyümesi mümkün değildir. H ağaca girdiğinde EH = 5 kenarı artık kullanılamaz, çünkü E ve H ikisi de ağaçtadır. Kruskal aynı kenarı döngü gerekçesiyle atlamıştı.

47119201126101553512ABCDEFGHI
Şekil 4.3.23: H ağaca girdikten sonra: toplam 18. Kırmızı kesikli EH = 5 artık döngü kurar; mavi kesikli kenarlar G ve I için mevcut en ucuz bağlantılardır.

Sonuç ağırlığı 29'dur ve Kruskal'ın bulduğu ağaçla aynıdır. İki algoritmanın kenar ekleme sıraları ise belirgin biçimde farklıdır. Kruskal ağırlıkları 1, 1, 2, 3, 4, 5, 6, 7 sırasıyla ekler. Prim 4, 7, 1, 1, 2, 3, 6, 5 sırasıyla ekler: önce A'nın çevresindeki pahalı kenarları alır, ucuz kenarlara ancak ağaç onlara ulaşınca erişir.

47119201126101553512ABCDEFGHI
Şekil 4.3.24: I ağaca girer: bütün düğümler ağaçtadır ve MST ağırlığı 29'dur.

4.3.6.5Karmaşıklık

Prim'in süresi iki işlemin maliyetine bağlıdır: anahtarı en küçük düğümü çıkarmak ve bir komşunun anahtarını azaltmak. Birinci işlem V kez yapılır. İkinci işlem en fazla E kez yapılır, çünkü her kenar iki ucundan biri ağaca girdiğinde bir kez incelenir. Yönsüz çizgede her kenar iki komşuluk listesinde görünür; incelemelerin sayısı 2E'yi aşmaz.

Tablo 4.3.13: Prim'in farklı uygulamalarının maliyeti.
UygulamaEn küçüğü çıkarAnahtar azaltToplam
Komşuluk matrisi + dizi taramasıV × O(V)O(1)O(V2)
Komşuluk listesi + ikili heapV × O(log V)E × O(log V)O((V + E) log V)
Tembel sürüm: kuyrukta kenar kopyaları≤ E ekleme ve çıkarma, her biri O(log E)O(E log E)
Fibonacci heapO(log V) amortizeO(1) amortizeO(E + V log V)

Matris sürümünde dış döngü V kez döner. Her turda enKucukAnahtar V hücreyi tarar ve iç döngü V komşuyu gezer. Toplam O(V2)'dir ve bu süre kenar sayısından bağımsızdır. Heap sürümünde her çıkarma ve her anahtar azaltma O(log V) sürer; toplam O((V + E) log V) olur. Bağlı çizgede E ≥ V − 1 olduğu için bu ifade çoğu zaman O(E log V) diye kısaltılır.

Hangi sürümün daha iyi olduğu çizgenin yoğunluğuna bağlıdır. Yoğun bir çizgede (E ≈ V2) matris sürümü O(V2) ile en iyisidir; heap sürümü O(V2 log V)'ye çıkar. Seyrek bir çizgede (E ≈ V) ise heap sürümü O(V log V) sürer; matris sürümü gereksiz yere O(V2) harcar.

Örnek 4.3.5

V = 1000 düğümlü bir çizgede iki sürüm karşılaştırılsın. Matris sürümü yaklaşık V2 = 106 adım atar. Heap sürümü için log2 1000 ≈ 10 alınır.

  1. E = 5000 (seyrek): E log V ≈ 5000 · 10 = 5 · 104. Heap sürümü yaklaşık 20 kat az iş yapar.
  2. E = 500.000 (tam çizgeye yakın): E log V ≈ 5 · 106. Matris sürümü yaklaşık 5 kat az iş yapar.

Sınır kabaca E log V ≈ V2 noktasındadır; burada E ≈ 105'tir. Sabit çarpanlar sınırı biraz kaydırır, ama yön değişmez.

4.3.6.6Prim ile Dijkstra arasındaki fark

Prim'in kodu, en kısa yol için kullanılan Dijkstra algoritmasının koduna çok benzer. İkisi de bir kaynaktan başlar, öncelikli kuyruktan en küçük anahtarlı düğümü çıkarır ve komşularını günceller. Tek fark anahtarın anlamıdır.

Tablo 4.3.14: Prim ile Dijkstra'nın karşılaştırması.
Prim (MST)Dijkstra (en kısa yol)
anahtar[v]v'yi ağaca bağlayan en ucuz tek kenarkaynaktan v'ye toplam yol uzunluğu
Güncellemew(u, v) < anahtar[v]uzaklik[u] + w(u, v) < uzaklik[v]
Sonuçtoplam ağırlığı en küçük ağaçen kısa yollar ağacı
Kaynaksonucu etkilemez (aynı ağırlık)sonucu belirler
Negatif ağırlıksorun değildoğru çalışmaz
Örnek 4.3.6

A–F çizgesinde B'den Dijkstra çalıştırılsın. Uzaklıklar A = 1, C = 3, D = 2, E = 5 (B → D → E) ve F = 5'tir. F'ye doğrudan BF = 5 kenarıyla gidilir; B → D → E → F yolu 2 + 3 + 2 = 7 tutar. En kısa yollar ağacı BA, BC, BD, DE, BF kenarlarından oluşur ve toplam ağırlığı 1 + 3 + 2 + 3 + 5 = 14'tür.

Prim ise F'yi E'ye bağlar: F'yi ağaca bağlamanın en ucuz yolu EF = 2'dir. Prim'in ağacı 11 ağırlıklıdır. İki ağaç da bütün düğümleri kapsar. Biri toplam ağırlığı, öbürü kaynaktan uzaklıkları küçültür.

Beş düğümlü çizgede de benzer bir fark görülür. 1'den 4'e en kısa yol uzunluğu 6'dır ve bu uzaklık hem doğrudan (1,4) kenarıyla hem 1 → 3 → 4 yoluyla elde edilir. Bu yüzden en kısa yollar ağacı (1,4) = 6 kenarını kullanabilir. MST onun yerine (3,4) = 4 kenarını seçer.

Sık yapılan hata

Prim'in güncelleme koşulunu anahtar[u] + G[u][v] < anahtar[v] diye yazmak. Bu satır Prim'i Dijkstra'ya dönüştürür ve sonuç bir en kısa yollar ağacı olur. A–F çizgesinde bu hata F'yi B'ye bağlar ve ağırlığı 11 yerine 14 olan bir ağaç verir. Prim'de anahtar her zaman tek bir kenarın ağırlığıdır; toplam yazılmaz.

4.3.7Borůvka algoritması

Minimum kapsayan ağacı bulan en eski algoritma Otakar Borůvka'nın 1926 tarihli çalışmasıdır. Borůvka, Moravya bölgesinin elektrik ağını en ucuz biçimde kurma problemiyle uğraşıyordu. Yöntem sonradan birkaç kez yeniden bulundu; Georges Sollin'in 1960'lardaki çalışması nedeniyle Sollin algoritması diye de anılır.

4.3.7.1Fikir: bütün bileşenler aynı anda

Başlangıçta her düğüm bağımsız bir bileşendir. Algoritma turlar hâlinde ilerler. Her turda her bileşen, kendisini dışarıya bağlayan en ucuz kenarı seçer. Seçilen kenarların hepsi birden eklenir ve bağladıkları bileşenler birleşir. Tek bileşen kalana kadar turlar sürer. Bağlı bir çizgede algoritma V − 1 kenar üretir.

Bir bileşenin en ucuz dış kenarı, o bileşen ile geri kalan düğümler arasındaki kesimin en hafif kenarıdır. Kesim özelliğine göre bu kenar güvenlidir. Kruskal ve Prim her adımda tek bir kesim seçer; Borůvka ise her turda bileşen sayısı kadar kesimi aynı anda kullanır.

1429367518abcdefghtur 1: her düğüm kendi en ucuz kenarınıseçer → 8 bileşen 3 bileşene iner
Şekil 4.3.25: Borůvka'nın ilk turu. Her düğüm kendi en ucuz kenarını seçer: ab = 1, cd = 2, bf = 3, ef = 6, gh = 1. Beş kenar aynı anda eklenir; sekiz bileşen üç bileşene iner.

Şekildeki çizgede e'nin en ucuz kenarı ef = 6'dır, çünkü öbür kenarı ae = 9'dur. f ise bf = 3'ü seçer. Bir kenar iki ucundan da seçilebilir; ab hem a'nın hem b'nin seçimidir ama bir kez eklenir. İlk tur sonunda bileşenler {a, b, e, f}, {c, d} ve {g, h}'dir. İkinci turda bc = 4 ve fg = 5 eklenir ve ağaç tamamlanır; toplam ağırlık 22 olur.

Algoritmanın en önemli üstünlüğü bileşenlerin seçimlerinin birbirinden bağımsız olmasıdır. Her bileşenin en ucuz kenarı ayrı bir işlemcide bulunabilir. Bu yüzden Borůvka paralel ve dağıtık hesaplamaya çok uygundur. Tur sayısı da küçüktür: en fazla ⌈log2 V⌉. Zayıf yanları da vardır. Kodlaması Kruskal ve Prim'den daha karmaşıktır. Eşit ağırlıklı kenarlar bulunduğunda tutarlı bir eşitlik bozucu gerekir; yoksa döngü oluşabilir.

4.3.7.2Adımlar ve sözde kod

  1. Başlangıçta her düğüm tek başına bir bileşen (küme) sayılır.
  2. Her bileşen için, onu başka bir bileşene bağlayan en küçük ağırlıklı kenar bulunur.
  3. Bulunan kenarların hepsi eklenir; bağladıkları bileşenler birleştirilir. Aynı kenar iki kez eklenmez.
  4. Tek bileşen kalana kadar 2. ve 3. adımlar tekrarlanır.
BORUVKA(G):
  MST = [ ]
  bileşenSayısı = |V|
  döngü bileşenSayısı > 1 iken:
    enUcuz = [ ]  // bileşen başına
    her bir e = (u, v, ağırlık) için E içinde:
      cu = ayrikKume.bul(u)
      cv = ayrikKume.bul(v)
      eğer cu ≠ cv:
        enUcuz[cu] = min(enUcuz[cu], e)
        enUcuz[cv] = min(enUcuz[cv], e)
    her bir (u, v, ağırlık) için enUcuz içinde:
      eğer ayrikKume.bul(u) ≠ ayrikKume.bul(v):
        MST.ekle((u, v, ağırlık))
        ayrikKume.birleştir(u, v)
        bileşenSayısı -= 1
  döndür MST

Borůvka kenarları sıralamaz. Her turda bütün kenarları bir kez tarar ve her bileşen için o ana kadar görülen en küçük dış kenarı tutar. Bileşenler ayrık kümeler yapısıyla izlenir: bir kenarın iki ucunun bul sonuçları aynıysa kenar bileşenin içindedir ve hesaba katılmaz. İkinci döngüde seçilen kenarlar eklenirken yeniden bul sorulur. Bunun nedeni, iki bileşenin aynı kenarı seçmiş olabilmesidir. İlk kez eklendiğinde iki bileşen birleşir; ikinci kez sırası geldiğinde iki ucu artık aynı bileşendedir ve kenar atlanır.

4.3.7.3Altı düğümlü örnek

Algoritma A–F düğümlü, dokuz kenarlı bir çizgede çalıştırılır. Kenarlar AB = 1, BC = 5, AD = 4, BE = 3, BD = 7, BF = 6, CF = 9, DE = 8 ve EF = 2'dir.

Tablo 4.3.15: Altı düğümlü çizgede Borůvka, tur 1. Her bileşen ve kendi en ucuz dış kenarı.
BileşenDış kenarlarEn ucuz
{A}AB 1, AD 4AB 1
{B}AB 1, BC 5, BE 3, BD 7, BF 6AB 1
{C}BC 5, CF 9BC 5
{D}AD 4, BD 7, DE 8AD 4
{E}BE 3, DE 8, EF 2EF 2
{F}BF 6, CF 9, EF 2EF 2

AB'yi ve EF'yi ikişer bileşen seçer, ama her biri bir kez eklenir. Tur 1'de dört farklı kenar eklenir: AB, BC, AD ve EF. İki bileşen kalır: {A, B, C, D} ve {E, F}. Toplam ağırlık 1 + 5 + 4 + 2 = 12'dir.

154736982ABCDEF
Şekil 4.3.26: Tur 1: her bileşenin seçtiği en ucuz dış kenar maviyle gösterilmiştir. Dört farklı kenar seçilir.

Tur 2'de iki bileşen arasındaki kenarlar BE = 3, BF = 6, CF = 9 ve DE = 8'dir. Bileşenlerin ikisi de en ucuz olanı, BE = 3'ü seçer. BE eklenir ve tek bileşen kalır. MST AB, BC, AD, EF, BE kenarlarından oluşur ve ağırlığı 12 + 3 = 15'tir.

154736982ABCDEF
Şekil 4.3.27: Tur 2 sonu: BE eklenmiştir ve tek bileşen kalmıştır. MST ağırlığı 15'tir.

Kruskal aynı çizgede kenarları 1, 2, 3, 4, 5 sırasıyla tek tek ekler: AB, EF, BE, AD, BC. Borůvka'da ise ilk turda dört kenar aynı anda eklenir. Bulunan ağaç aynıdır.

4.3.7.4On iki düğümlü örnek

Borůvka'nın gücü büyük çizgelerde daha açık görülür. A–L düğümlü, 20 kenarlı bir çizge ele alınır. Kenarlar şunlardır:

AB 13, AC 6, BC 7, BD 1, CD 14, CE 8, CH 20, DE 9, DF 3, EF 2, EJ 18, GH 15, GJ 19, GK 10, GI 5, HJ 17, IK 11, JK 16, JL 4, KL 12

Tablo 4.3.16: On iki düğümlü çizgede Borůvka turları.
TurBileşenlerin en ucuz kenarlarıEklenen farklı kenarlarBileşen sayısıToplam
1A: AC 6, B: BD 1, C: AC 6, D: BD 1, E: EF 2, F: EF 2, G: GI 5, H: GH 15, I: GI 5, J: JL 4, K: GK 10, L: JL 4AC, BD, EF, GI, GH, JL, GK12 → 543
2{A, C}: BC 7, {B, D}: DF 3, {E, F}: DF 3, {G, H, I, K}: KL 12, {J, L}: KL 12BC, DF, KL5 → 265
3{A, …, F}: EJ 18, {G, …, L}: EJ 18EJ2 → 183

İlk turda yedi farklı kenar eklenir ve bileşen sayısı 12'den 5'e iner. H'nin seçimi dikkat çekicidir: H'nin bütün kenarları pahalıdır (15, 17, 20) ve en ucuzu GH 15'tir. Bu kenar ağır olmasına rağmen güvenlidir, çünkü {H} kesiminin en hafif kenarıdır. K da GK 10'u seçer; IK 11 ve KL 12 daha pahalıdır. G, H, I ve K böylece tek bileşende toplanır.

1367114820932181519105171116412ABCDEFGHIJKL
Şekil 4.3.28: Tur 2: beş bileşen (renklerle) ve her birinin en ucuz dış kenarı (mavi). Yeşil kalın kenarlar tur 1'de eklenmiştir.

İkinci turda {A, C} bileşeni BC 7'yi seçer. {B, D} ve {E, F} bileşenleri aynı DF 3 kenarını seçer. Sağ taraftaki iki bileşen de KL 12'yi seçer. Üç farklı kenar eklenir ve iki bileşen kalır. Üçüncü turda iki yarıyı bağlayan yalnız iki kenar vardır: CH 20 ve EJ 18. Daha ucuz olan EJ 18'dir ve iki bileşen de onu seçer. MST üç turda tamamlanır ve ağırlığı 83'tür.

1367114820932181519105171116412ABCDEFGHIJKL
Şekil 4.3.29: Tur 3 sonu: EJ eklenmiştir; on iki düğüm tek bileşendedir. MST ağırlığı 83'tür.

Kruskal bu çizgede 11 kenarı 11 ayrı adımda ekler. Borůvka aynı ağacı üç turda kurar: 7 + 3 + 1 kenar. Her tur bütün kenarları bir kez tarar; toplam iş 3 · 20 = 60 kenar incelemesidir.

4.3.7.5Java kodu ve izleme

Sunumdaki Java kodu, ana döngünün gövdesini verir. kenarlar kenar listesi, ata ayrık kümelerin ebeveyn dizisi, agac seçilen kenarlar, bilesenSayisi ise kalan bileşen sayısıdır ve başta V'dir.

while (bilesenSayisi > 1) {
  Kenar[] enUcuz = new Kenar[V];
  for (Kenar e : kenarlar) {        // 1) en ucuzlar
    int k1 = bul(ata, e.u), k2 = bul(ata, e.v);
    if (k1 == k2) continue;         // aynı bileşen
    if (enUcuz[k1] == null || e.w < enUcuz[k1].w)
      enUcuz[k1] = e;
    if (enUcuz[k2] == null || e.w < enUcuz[k2].w)
      enUcuz[k2] = e;
  }
  for (Kenar e : enUcuz) {          // 2) hepsini ekle
    if (e == null) continue;
    int k1 = bul(ata, e.u), k2 = bul(ata, e.v);
    if (k1 == k2) continue;         // zaten eklendi
    agac.add(e);
    ata[k1] = k2;                   // birleştir
    bilesenSayisi--;
  }
}

Her turda enUcuz dizisi yeniden oluşturulur. Dizinin indisi bir bileşenin köküdür; hücre o bileşenin şimdiye kadar bulunan en ucuz dış kenarını tutar. Birinci döngü bütün kenarları tarar. Kenarın iki ucunun kökü aynıysa kenar bileşen içindedir ve continue ile geçilir. Değilse kenar iki kökün de adayıdır: her kök için hücre boşsa ya da yeni kenar daha ucuzsa hücreye yazılır.

İkinci döngü enUcuz dizisini gezer. Boş hücreler kök olmayan düğümlere aittir ve atlanır. Her aday kenar için kökler yeniden bulunur. Kökler aynıysa kenar bu turda zaten eklenmiştir ya da iki bileşen başka bir kenarla birleşmiştir; kenar atlanır. Değilse kenar ağaca girer, ata[k1] = k2 ile bileşenler birleşir ve bileşen sayısı bir azalır.

Kod, altı düğümlü örnekte izlenir. Düğümler A–F, indisler 0–5'tir. Kenarlar listede şu sırayla durur: AB, BC, AD, BE, BD, BF, CF, DE, EF.

Tablo 4.3.17: Borůvka kodunun izlemesi (altı düğümlü örnek).
AşamaOlaybilesenSayisi
Tur 1, taramaAB(1): enUcuz[A] = enUcuz[B] = AB. BC(5): enUcuz[C] = BC. AD(4): enUcuz[D] = AD. BE(3): enUcuz[E] = BE. BD(7): değişiklik yok. BF(6): enUcuz[F] = BF. CF(9), DE(8): değişiklik yok. EF(2): enUcuz[E] = enUcuz[F] = EF.6
Tur 1, eklemeenUcuz[A] = AB: kökler A, B → ekle, ata[A] = B5
enUcuz[B] = AB: iki uç aynı bileşende → atla5
enUcuz[C] = BC: kökler B, C → ekle, ata[B] = C4
enUcuz[D] = AD: kökler C, D → ekle, ata[C] = D3
enUcuz[E] = EF: kökler E, F → ekle, ata[E] = F2
enUcuz[F] = EF: aynı bileşen → atla2
Tur 2, taramaAB, BC, AD, BD, EF bileşen içinde kalır. Bileşenler arası BE(3), BF(6), CF(9), DE(8) içinden en ucuz BE: enUcuz[D] = enUcuz[F] = BE.2
Tur 2, eklemeenUcuz[D] = BE: kökler D, F → ekle, ata[D] = F1
enUcuz[F] = BE: aynı bileşen → atla; döngü biter1

Tur 1'in taramasında E'nin adayı önce BE(3) olur, sonra EF(2) gelince değişir. F'nin adayı da önce BF(6), sonra EF(2) olur. Ekleme döngüsünde AD eklenirken bul(A), A → B → C yolunu izler ve C'yi bulur. Kökler C ve D olduğu için ata[C] = D yazılır. Tur 2'de bileşen kökleri D ve F'dir; bu yüzden aday kenar enUcuz[D] ve enUcuz[F] hücrelerine yazılır. Sonuç ağırlığı 15'tir.

154736982ABCDEF
Şekil 4.3.30: Tur 2'nin taraması: iki bileşen (renkler) arasındaki en ucuz kenar BE(3) kesikli maviyle gösterilmiştir.

4.3.7.6Karmaşıklık ve eşitlik bozucu

Teorem 4.3.8 (Borůvka'nın tur sayısı)

V düğümlü bağlı bir çizgede Borůvka en fazla ⌈log2 V⌉ tur çalışır ve toplam süresi O(E log V)'dir.

Bir turun başında c > 1 bileşen olsun. Çizge bağlı olduğu için her bileşenin en az bir dış kenarı vardır ve her bileşen bir kenar seçer. Seçilen kenar bileşeni en az bir başka bileşenle birleştirir. Tur sonunda oluşan her yeni bileşen en az iki eski bileşeni içerir. Bu yüzden yeni bileşen sayısı en fazla ⌊c/2⌋'dir. k turdan sonra en fazla V/2k bileşen kalır; k ≥ log2 V olunca bu sayı 1'e iner. Her tur bütün kenarları bir kez tarar ve her kenar için iki bul çağrısı yapar. Ayrık kümelerin neredeyse sabit maliyeti ihmal edilirse bir tur O(E) sürer. Toplam O(E log V)'dir. □

İspattaki ⌊c/2⌋ gözlemi aslında biraz daha sıkı bir sınır verir. V = 1000 için bileşen sayısı en kötü durumda 1000, 500, 250, 125, 62, 31, 15, 7, 3, 1 diye iner; 9 tur yeter. ⌈log2 1000⌉ = 10 ise güvenli bir üst sınırdır. Uygulamada tur sayısı çoğu zaman daha da küçüktür, çünkü bir turda ikiden fazla bileşen birleşebilir. On iki düğümlü örnek 3 turda bitmiştir; ⌈log2 12⌉ = 4'tür. Borůvka kenarları sıralamaz; ağırlıkları yalnız karşılaştırır.

Eşit ağırlıklar Borůvka'da özel bir sorun yaratır. Örnek olarak kenarlarının hepsi 1 olan bir a, b, c üçgeni alınsın. Eşitlik keyfî çözülürse a ab'yi, b bc'yi, c de ca'yı seçebilir. Üç kenar aynı anda eklenirse bir döngü oluşur. Bunu önlemek için tutarlı bir eşitlik bozucu gerekir: ağırlıklar eşitse kenarlar örneğin listedeki indislerine göre karşılaştırılır. Böylece kenarların hepsi kesin bir sıraya girer ve döngü oluşamaz. Sunumdaki kodda e.w < enUcuz[k1].w karşılaştırması katı küçüktür; eşitlikte listede önce gelen kenar korunur. Bütün bileşenler aynı listeyi aynı sırayla taradığı için bu seçim tutarlıdır. Kodun ikinci döngüsündeki bul denetimi de ayrıca bir güvenlik sağlar: zaten bağlanmış iki ucu olan kenar eklenmez.

Sık yapılan hata

Bir turda seçilen kenarları, aynı turun içinde bileşenler güncellendikçe yeniden hesaplamak. Borůvka'da bir turun bütün seçimleri tur başındaki bileşenlere göre yapılır; ekleme ondan sonra gelir. Seçim ile ekleme iç içe yazılırsa algoritma Kruskal ile Prim arasında belirsiz bir şeye dönüşür ve tur sayısı sınırı geçerliliğini yitirir.

4.3.8Ters silme algoritması

4.3.8.1Fikir: Kruskal'ın aynası

Ters silme algoritması (reverse-delete) da açgözlüdür ve Kruskal'ın 1956 makalesinde önerilmiştir. Kruskal boş çizgeden başlayıp küçük kenarları eklerken, ters silme bütün çizgeden başlar ve büyük kenarları silmeye çalışır. Kenarlar ağırlıklarına göre azalan sırada sıralanır. En ağır kenardan başlanarak çizgenin bağlılığını bozmayan her kenar silinir. Silinmesi çizgeyi koparan kenar yerinde bırakılır.

349abc
Şekil 4.3.31: Ters silmenin temel adımı: a–b–c döngüsündeki en ağır kenar 9 silinir; a ile c hâlâ b üzerinden bağlıdır.

Algoritma döngü özelliğine dayanır. Bir kenar silindiğinde çizge bağlı kalıyorsa, kenarın iki ucu arasında başka bir yol vardır; kenar bir döngünün üzerindedir. Kenarlar azalan sırada işlendiği için bu döngünün kalan kenarları en fazla onun kadar ağırdır. Daha ağır kenarlar ise zaten ya silinmiş ya da köprü olduğu için kalmıştır. Bu yüzden silinen kenar, kalan çizgedeki bir döngünün en ağır kenarıdır ve döngü özelliğine göre bir MST'ye gerekmez. Silinemeyen kenar ise o anda bir köprüdür ve her kapsayan ağaçta bulunmak zorundadır.

Ters silmenin daha çok kuramsal ve öğretici bir değeri vardır. Her silme için bir bağlılık testi gerekir ve bu test pahalıdır; pratikte algoritma genellikle yavaştır.

4.3.8.2Adımlar, sözde kod ve karmaşıklık

  1. Bütün kenarlar ağırlığına göre azalan şekilde sıralanır.
  2. Sıradaki en ağır kenar silinir.
  3. Çizge artık bağlı değilse silinen kenar geri eklenir.
  4. Bütün kenarlar işlenene kadar 2. ve 3. adımlar tekrarlanır.
TERS_SİLME(G):
  MST = tüm kenarlar
  kenarlar.sırala(azalana göre)
  her bir (u, v, ağırlık) için kenarlar içinde:
    MST.sil((u, v, ağırlık))
    ayrikKume.sıfırla()
    her bir (x, y) için MST içinde:
      ayrikKume.birleştir(x, y)
    kök = ayrikKume.bul(V[0])
    eğer bir v için ayrikKume.bul(v) ≠ kök:
      MST.ekle((u, v, ağırlık))  // geri koy
  döndür MST

Sözde kod bağlılığı ayrık kümelerle sınar. Her silmeden sonra yapı sıfırlanır ve kalan bütün kenarlar için birleştir çağrılır. Sonra ilk düğümün kökü bulunur. Kökü bundan farklı bir düğüm varsa çizge kopmuştur ve silinen kenar bir kez geri konur. Geri konan kenar, döngünün başında silinen (u, v, ağırlık) kenarıdır; koşul satırındaki v ise yalnız “bir düğüm” anlamında kullanılmıştır ve iki ad birbirine karıştırılmamalıdır.

Karmaşıklık hesabı basittir. Her kenar için bir bağlılık testi yapılır. Test BFS ya da DFS ile O(V + E) sürer; ayrık kümelerle yeniden kurma da aynı mertebededir. E test toplam O(E · (V + E)) eder. Sıralamanın O(E log E) maliyeti bunun yanında küçük kalır. V = 1000, E = 5000 olan bir çizgede bu, yaklaşık 3 · 107 adım demektir; Kruskal aynı çizgede yaklaşık 6 · 104 adımla yetinir.

4.3.8.3Yedi düğümlü örnek

Ters silme, düğümleri 0–6 olan, 11 kenarlı bir çizgede çalıştırılır. Kenarların toplam ağırlığı 90'dır. Azalan sıra şöyledir:

3–4 15, 5–6 11, 1–3 9, 4–6 9, 1–2 8, 4–5 8, 1–4 7, 0–1 7, 3–5 6, 2–4 5, 0–3 5

Tablo 4.3.18: Yedi düğümlü çizgede ters silme.
KenarwSilinirse çizge bağlı mı?Sonuç
3–415evetsilindi
5–611evetsilindi
1–39evetsilindi
4–69hayır: 6 yalnız kalırkaldı
1–28evetsilindi
4–58evetsilindi
1–4, 0–1, 3–5, 2–4, 0–37, 7, 6, 5, 5hayır: her biri köprükaldı

İlk üç silme döngü kenarlarını kaldırır. 3–4 = 15 silindiğinde 3 ile 4, 3 → 0 → 1 → 4 yoluyla bağlı kalır. 4–6 = 9 sırası geldiğinde 5–6 zaten silinmiştir; 6'nın tek kenarı 4–6'dır. Bu kenar silinirse 6 yalnız kalır, bu yüzden geri konur. 1–2 = 8 ve 4–5 = 8 silinebilir, çünkü 2'ye 2–4 ile, 5'e 3–5 ile ulaşılır. Bu noktada kalan altı kenar bir ağaç oluşturur ve her biri köprüdür; hiçbiri silinemez. Kalan kenar sayısı 6 = V − 1'dir ve MST ağırlığı 9 + 7 + 7 + 6 + 5 + 5 = 39'dur.

58911871596752465130
Şekil 4.3.32: 4–6 = 9 adımı. Gri noktalı kenarlar silinmiştir. 5–6 silindiği için 6'nın tek bağlantısı 4–6'dır (kırmızı kesikli); kenar silinirse 6 kopar, bu yüzden geri konur.

Kruskal aynı çizgede önce 5, 5, 6, 7, 7 ağırlıklı kenarları ekler. Ardından 8'lik iki kenarı ve 9'luk 1–3'ü döngü gerekçesiyle atlar. Son olarak 4–6 = 9'u ekler. İki algoritma aynı ağaca ters yönlerden ulaşır: Kruskal neyin ekleneceğine, ters silme neyin kalacağına karar verir.

4.3.8.4Java kodu ve izleme

void tersSil(int V, List<Kenar> ke) {
  ke.sort((a, b) -> b.w - a.w); // azalan
  int i = 0;
  while (i < ke.size()) {
    Kenar e = ke.remove(i);     // geçici sil
    if (!bagliMi(V, ke)) {
      ke.add(i, e);             // geri koy
      i++;
    }
  }
}

ke.sort((a, b) -> b.w - a.w) kenarları azalan sıraya koyar; karşılaştırıcının b − a biçiminde yazılması sırayı tersine çevirir. i, listede incelenecek sıradaki kenarın indisidir. Döngünün her turunda kenar ke.remove(i) ile geçici olarak silinir. Silmeden sonra liste bir kayar ve sıradaki kenar yine i indisine gelir; bu yüzden silme başarılıysa i artırılmaz. Kenar silinince çizge kopuyorsa ke.add(i, e) kenarı eski yerine koyar ve i bir artar. Döngü i liste boyuna ulaşınca biter. Bu anda listede kalan kenarlar MST'dir. Kodun kullandığı bagliMi metodu BFS ile yazılabilir:

boolean bagliMi(int V, List<Kenar> ke) {
  List<List<Integer>> komsu = new ArrayList<>();
  for (int i = 0; i < V; i++) komsu.add(new ArrayList<>());
  for (Kenar e : ke) { komsu.get(e.u).add(e.v); komsu.get(e.v).add(e.u); }
  boolean[] goruldu = new boolean[V];
  ArrayDeque<Integer> kuyruk = new ArrayDeque<>();
  kuyruk.add(0); goruldu[0] = true;
  int sayac = 1;
  while (!kuyruk.isEmpty()) {
    int x = kuyruk.poll();
    for (int y : komsu.get(x))
      if (!goruldu[y]) { goruldu[y] = true; sayac++; kuyruk.add(y); }
  }
  return sayac == V;            // her düğüme ulaşıldı mı?
}

Metot önce kenar listesinden komşuluk listesini kurar, sonra 0. düğümden BFS başlatır ve ulaşılan düğümleri sayar. Sayı V'ye eşitse çizge bağlıdır. Her çağrı O(V + E) sürer; çağrı sayısı E olduğu için toplam maliyet yukarıda hesaplanan O(E · (V + E))'dir.

Kod, aynı yedi düğümlü çizgenin harfli sürümünde izlenir: 0–6 düğümleri A–G olarak adlandırılır. Kenar listesi alfabetik sırayla verilir: AB 7, AD 5, BC 8, BD 9, BE 7, CE 5, DE 15, DF 6, EF 8, EG 9, FG 11. Sıralama kararlıdır; eşit ağırlıklı kenarlar bu sırayı korur.

Tablo 4.3.19: tersSil(7, ke) izlemesi.
KenarbagliMiİşlemi (sonra)
DE 15truesilinmiş kalır0
FG 11truesilinmiş kalır0
BD 9truesilinmiş kalır0
EG 9false: G yalnız kalırgeri konur1
BC 8truesilinmiş kalır1
EF 8truesilinmiş kalır1
AB 7false: {A, D, F} kopargeri konur2
BE 7false: {C, E, G} kopargeri konur3
DF 6false: {F} kopargeri konur4
AD 5false: {D, F} kopargeri konur5
CE 5false: {C} kopargeri konur6

EG adımında FG zaten silinmiştir ve G'nin tek bağlantısı EG'dir. AB adımına gelindiğinde A'nın kenarları AB ve AD, D'nin kalan kenarları AD ve DF, F'nin kalan kenarı ise yalnız DF'dir. AB silinirse A, D ve F öbür düğümlerden kopar. Son satırdan sonra i = 6 liste boyuna eşittir ve döngü biter. Kalan ağaç AB, AD, BE, CE, DF ve EG kenarlarından oluşur; ağırlığı 39'dur.

7589751568911ABCDEFG
Şekil 4.3.33: Ters silmenin sonucu: AB, AD, BE, CE, DF, EG; toplam 39. Gri noktalı kenarlar silinmiştir.
Sık yapılan hata

Silme başarılı olduğunda da i'yi artırmak. ke.remove(i) listeyi bir sola kaydırır; sıradaki kenar artık i indisindedir. i artırılırsa her başarılı silmeden sonra bir kenar hiç incelenmeden atlanır ve sonuç bir ağaç değil, gereksiz kenarlar içeren bir alt çizge olabilir.

4.3.9Dört algoritmanın karşılaştırması

Dört algoritma aynı problemi çözer ve aynı kuramsal temele dayanır, ama çalışma biçimleri ve maliyetleri farklıdır. Tablo 4.3.20 farkları bir arada gösterir.

Tablo 4.3.20: Dört MST algoritmasının karşılaştırması.
KruskalPrimBorůvkaTers silme
Bakışkenar odaklıdüğüm odaklıbileşen odaklıkenar odaklı (silme)
Ara durumormantek ağaçormanbağlı çizge
Dayandığı özellikkesimkesimkesimdöngü
Veri yapısısıralı kenarlar + ayrık kümeleröncelikli kuyruk / diziayrık kümelerBFS/DFS
ZamanO(E log E)O(V2) ya da O((V + E) log V)O(E log V)O(E · (V + E))
En uygunseyrek çizgeyoğun çizgeparalel / dağıtıköğretim, kuram
Bağlı olmayan çizgeorman verirbir bileşenorman verirbileşenleri korur

Tablonun satırları birkaç pratik soruya karşılık gelir. Çizge bir kenar listesi olarak geliyor ve seyrekse Kruskal en kolay ve en hızlı seçimdir. Çizge bir komşuluk matrisiyle veriliyor ve yoğunsa, dizi taramalı Prim O(V2) ile en iyi sonucu verir; sıralama ya da heap gerekmez. Seyrek çizge komşuluk listesiyle verildiğinde heap'li Prim de Kruskal ile aynı mertebededir. İş birden çok işlemciye dağıtılacaksa Borůvka'nın bağımsız seçimleri büyük kolaylık sağlar. Ters silme ise üretim kodunda pek kullanılmaz; döngü özelliğinin algoritmik bir karşılığı olarak öğreticidir.

Bağlı olmayan çizgelerde davranış farkı da önemlidir. Kruskal ve Borůvka her bileşeni ayrı ayrı tamamlar ve bir minimum kapsayan orman döndürür. Prim yalnız başlangıç düğümünün bileşenini kapsar; ormanı bulmak için kapsanmayan her bileşenden yeniden başlatılmalıdır. Ters silme hiçbir bileşeni koparmadığı için bileşenleri olduğu gibi korur; bağlılık testinin bileşen sayısını koruyacak biçimde yazılması yeter.

Dokuz düğümlü çizge farkı somut olarak gösterir. Kruskal ve Prim aynı ağacı (29) bulur. Kruskal kenarları 1, 1, 2, 3, 4, 5, 6, 7 sırasıyla, Prim ise 4, 7, 1, 1, 2, 3, 6, 5 sırasıyla ekler. Kruskal'ın ekleme sırası ağırlık sırasıdır; Prim'in sırası ise ağacın büyüme yönünü izler.

Bu dört algoritma tarihin sonu değildir. Karger, Klein ve Tarjan 1995'te beklenen süresi doğrusal olan rastgele bir algoritma verdi. Bernard Chazelle 2000'de ters Ackermann fonksiyonuyla neredeyse doğrusal süreli belirlenimci bir algoritma yayımladı. Belirlenimci, kesin doğrusal süreli bir MST algoritmasının var olup olmadığı hâlâ açık bir sorudur.

4.3.10Sık yapılan hatalar

Bu kesim, notun içine dağılmış uyarıları ve sunumdaki hata listesini bir arada toplar.

Sık yapılan hata

MST'yi en kısa yollar ağacı sanmak. MST toplam ağırlığı küçültür; iki düğüm arasındaki MST yolu en kısa yol olmak zorunda değildir. A–F çizgesinde MST'deki B–F yolu B → D → E → F'dir ve uzunluğu 7'dir; doğrudan BF kenarı 5'tir.

Sık yapılan hata

Döngüyü komşulukla denetlemek. Kruskal'da “u ile v doğrudan bağlı mı?” sorusu yetmez. Sorulması gereken, iki ucun aynı bileşende olup olmadığıdır: bul(u) == bul(v).

Sık yapılan hata

Prim'de uzaklık toplamak. anahtar[u] + w yazmak Dijkstra'dır. Prim'de anahtar yalnız tek bir kenarın ağırlığıdır.

Sık yapılan hata

Negatif ağırlıktan korkmak. MST algoritmaları negatif ağırlıklarla da doğru çalışır, çünkü yalnız ağırlıkları karşılaştırır. Kesim ve döngü özelliklerinin ispatı da ağırlıkların işaretine bakmaz. Negatif ağırlık sorunu Dijkstra'ya özgüdür. Hatta bütün ağırlıklara aynı sabit eklenirse MST değişmez. Kenarların sırası korunur. Her kapsayan ağaçta V − 1 kenar olduğu için bütün ağaçların toplamı aynı miktarda artar.

Sık yapılan hata

Bağlı olmayan çizgeyi unutmak. Bağlı olmayan çizgenin MST'si yoktur; minimum kapsayan ormanı vardır. while (agac.size() < V − 1) koşulu kenarlar bittiğinde liste sınırını aşar. Koşula k < ke.size() eklenmelidir.

Sık yapılan hata

Yönlü çizgeye uygulamak. Bu algoritmalar yönsüz çizgeler içindir. Yönlü çizgede bir kökten bütün düğümlere ulaşan en ucuz yapıya minimum kapsayan dallanma denir ve bu problem Chu–Liu/Edmonds algoritmasıyla çözülür. Kruskal ya da Prim yönlü kenarlara olduğu gibi uygulanırsa yanlış sonuç verir.

4.3.11Özet

  • Kapsayan ağaç ve MST: bağlı, yönsüz bir çizgenin bütün düğümlerini içeren, bağlı ve döngüsüz alt çizgesi kapsayan ağaçtır ve tam V − 1 kenarlıdır. Toplam ağırlığı en küçük olana minimum kapsayan ağaç denir. Tam çizgede nn−2 kapsayan ağaç vardır; bu yüzden hepsini denemek yerine açgözlü algoritmalar kullanılır.
  • Kesim özelliği: her kesimin en hafif kenarı bir MST'ye aittir; kesimin en hafif kenarı tekse her MST'dedir. Kruskal, Prim ve Borůvka bu özelliğe dayanır.
  • Döngü özelliği: bir döngünün tek en ağır kenarı hiçbir MST'de yer almaz. Ters silme bu özelliğe dayanır. Bütün ağırlıklar farklıysa MST tektir.
  • Kruskal: kenarlar artan sırada incelenir; bul(u) ≠ bul(v) ise kenar eklenir ve iki ağaç birleşir. Süre O(E log E) = O(E log V)'dir ve sıralama baskındır.
  • Ayrık kümeler: ata[] ormanında her küme bir ağaçtır, kök temsilcidir. Yol kısaltma ve rütbeye göre birleştirme birlikte işlem başına amortize O(α(V)) verir; α pratikte 4'ü geçmez. Yalnız rütbe ile her işlem O(log V)'dir.
  • Prim: tek bir ağaç büyütülür; anahtar[v], v'yi ağaca bağlayan en ucuz tek kenarın ağırlığıdır. Dizi taramasıyla O(V2), ikili heap ile O((V + E) log V), tembel sürümle O(E log E), Fibonacci heap ile O(E + V log V) sürer.
  • Prim ve Dijkstra: kodlar neredeyse aynıdır; Prim tek kenar ağırlığını, Dijkstra kaynaktan toplam uzaklığı anahtar yapar. Prim'in sonucu kaynaktan bağımsız ağırlıktadır ve negatif ağırlıkla da doğrudur.
  • Borůvka: her turda her bileşen en ucuz dış kenarını seçer ve bu kenarlar aynı anda eklenir. Bileşen sayısı her turda en az yarıya iner; en fazla ⌈log2 V⌉ tur, toplam O(E log V). Eşit ağırlıklarda tutarlı bir eşitlik bozucu gerekir.
  • Ters silme: kenarlar azalan sırada silinir; çizge koparsa kenar geri konur. Her silmede bağlılık testi yapıldığı için süre O(E · (V + E))'dir.
  • Seçim: seyrek çizge ve kenar listesi → Kruskal; yoğun çizge ve komşuluk matrisi → dizi taramalı Prim; paralel hesaplama → Borůvka; bağlı olmayan çizgede orman → Kruskal ya da Borůvka.

4.3.12Kavram listesi

4.3.13Kendini Dene

Sorular 1–6, aynı K çizgesini kullanır. K'nin düğümleri A–F, kenarları AB = 7, AC = 3, AD = 6, BC = 4, BE = 2, CD = 5, CE = 8, CF = 9, DF = 1 ve EF = 10'dur.

  1. K'de {A, C} | {B, D, E, F} kesimini geçen kenarlar hangileridir? Kesim özelliği hangi kenar için ne söyler?

    Cevap

    Kesimi geçen kenarlar AB 7, AD 6, BC 4, CD 5, CE 8 ve CF 9'dur. AC 3 kesimi geçmez, çünkü iki ucu da aynı taraftadır. En hafif geçen kenar BC 4'tür ve tek en hafif olduğu için her MST'de bulunur.

  2. Kruskal K'de hangi kenarları hangi sırayla ekler? Kaç kenar reddedilir, hangi kenarlar hiç incelenmez? w(T) kaçtır?

    Cevap

    Sıralı liste DF 1, BE 2, AC 3, BC 4, CD 5, AD 6, AB 7, CE 8, CF 9, EF 10'dur. İlk beş kenarın hepsi iki ayrı ağacı bağlar ve eklenir; hiçbir kenar reddedilmez. Beşinci ekleme V − 1 = 5'e ulaştığı için AD, AB, CE, CF ve EF hiç incelenmez. w(T) = 1 + 2 + 3 + 4 + 5 = 15.

  3. Prim K'de A'dan çalıştırılırsa düğümler hangi sırayla ağaca girer? C ağaca girdikten sonra anahtar dizisi nedir?

    Cevap

    Sıra A (0), C (3), B (4), E (2), D (5), F (1)'dir. A'dan sonra anahtarlar B = 7, C = 3, D = 6, E = F = ∞ olur. C girince B, BC ile 4'e; D, CD ile 5'e iner; E = 8 ve F = 9 olur. Dizi A, …, F için 0, 4, 3, 5, 8, 9'dur. Ebeveynler B → C, D → C, E → B, F → D olarak sonuçlanır; toplam yine 15'tir.

  4. Borůvka K'de kaç turda biter? Her turda hangi kenarlar eklenir?

    Cevap

    Tur 1'de A ve C aynı AC 3'ü, B ve E aynı BE 2'yi, D ve F aynı DF 1'i seçer. Üç farklı kenar eklenir ve {A, C}, {B, E}, {D, F} bileşenleri oluşur. Tur 2'de {A, C} ve {B, E} BC 4'ü, {D, F} CD 5'i seçer. İki kenar eklenir ve tek bileşen kalır: 2 tur, ağırlık 15.

  5. Ters silme K'de hangi kenarları siler? İlk “kalır” kararı hangi kenarda verilir ve neden?

    Cevap

    Silinenler EF 10, CF 9, CE 8, AB 7 ve AD 6'dır. İlk “kalır” kararı CD 5'te verilir: AD ve CF silindikten sonra D ile F'yi geri kalan düğümlere bağlayan tek kenar CD'dir. Ardından BC, AC, BE ve DF de köprü oldukları için kalır.

  6. K'de A'dan Dijkstra çalıştırılsa en kısa yollar ağacı hangi kenarlardan oluşur ve ağırlığı kaçtır? Bu ağacın Prim'in A'dan bulduğu ağaçtan farkı nedir?

    Cevap

    Uzaklıklar B = 7, C = 3, D = 6, E = 9 (A → B → E), F = 7 (A → D → F)'dir. Ağaç AB, AC, AD, BE, DF kenarlarından oluşur ve ağırlığı 7 + 3 + 6 + 2 + 1 = 19'dur. Prim'in ağacı (AC, BC, CD, BE, DF) 15 ağırlıklıdır. Dijkstra B'ye doğrudan AB 7 ile gider; A → C → B yolu da 7'dir ve daha kısa değildir. Prim ise B'yi C'ye 4'lük kenarla bağlar.

  7. 0–6 öğeli bir ayrık kümeler yapısında sunumdaki yol kısaltmalı bul ve birlestir kullanılıyor. Sırasıyla birlestir(0, 1), (2, 3), (0, 2), (4, 5), (6, 4), (5, 0) çağrılıyor. Son ata dizisi nedir? Ardından bul(6) neyi değiştirir?

    Cevap

    Adımlar: ata[0] = 1; ata[2] = 3; bul(0) = 1, bul(2) = 3 → ata[1] = 3; ata[4] = 5; bul(6) = 6, bul(4) = 5 → ata[6] = 5. Son çağrıda bul(5) = 5; bul(0), 0 → 1 → 3 yolunu izler ve 0'ı doğrudan 3'e bağlar. Ardından ata[5] = 3 yazılır. Dizi [3, 3, 3, 3, 5, 3, 5] olur. bul(6), 6 → 5 → 3 yolunu izler ve ata[6] = 3 yapar: [3, 3, 3, 3, 5, 3, 3]. 4 bu yolda olmadığı için 5'e bağlı kalır.

  8. K'nin en büyük kapsayan ağacı nasıl bulunur ve ağırlığı kaçtır?

    Cevap

    Kruskal kenarları azalan sırayla işler (ya da bütün ağırlıklar −w yapılıp olağan Kruskal çalıştırılır). Sıra EF 10, CF 9, CE 8, AB 7, AD 6, CD 5'tir. EF, CF eklenir; CE döngü kurar (C–F–E); AB, AD eklenir; son olarak CD eklenir. Ağaç EF, CF, AB, AD, CD'dir ve ağırlığı 10 + 9 + 7 + 6 + 5 = 37'dir.

  9. 50 düğümlü, 3 bileşenli bir çizgede minimum kapsayan ormanda kaç kenar vardır? Sunumdaki Kruskal kodu bu çizgede ne yapar?

    Cevap

    Her bileşen için (bileşen boyu − 1) kenar gerekir; toplam 50 − 3 = 47 kenardır. Sunumdaki kod 49 kenar bekler. 47 kenar ekledikten sonra liste biter ve ke.get(k++) IndexOutOfBoundsException fırlatır.

  10. V = 104 düğümlü bir çizgede Prim'in iki sürümü karşılaştırılıyor: dizi taramalı O(V2) ve ikili heap'li O((V + E) log V). E = 5 · 104 ve E = 5 · 107 için hangisi daha az iş yapar?

    Cevap

    V2 = 108, log2 104 ≈ 13,3'tür. E = 5 · 104 için heap sürümü yaklaşık 6 · 104 · 13,3 ≈ 8 · 105 adım atar; yüz kattan fazla daha iyidir. E = 5 · 107 (tam çizgenin aşağı yukarı tamamı) için heap sürümü yaklaşık 6,6 · 108 adım atar; dizi sürümü 6–7 kat daha iyidir.

4.3.14İleri okuma

  • T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein, Introduction to Algorithms (4. baskı, 2022), Bölüm 19 ve 21: ayrık kümelerin ters Ackermann analizini ve genel MST yöntemini, güvenli kenar kavramıyla birlikte ispatlarıyla verir.
  • R. Sedgewick ve K. Wayne, Algorithms (4. baskı, 2011), Kesim 1.5 ve 4.3: ayrık kümeleri, tembel ve istekli Prim'i ve Kruskal'ı Java koduyla ve deneysel ölçümlerle anlatır.
  • R. L. Graham ve P. Hell, “On the History of the Minimum Spanning Tree Problem”, Annals of the History of Computing 7(1), 1985: Borůvka, Jarník, Kruskal ve Prim'in çalışmalarını ve problemin erken tarihini karşılaştırmalı olarak inceler.
  • R. E. Tarjan, “Efficiency of a Good But Not Linear Set Union Algorithm”, Journal of the ACM 22(2), 1975: yol kısaltma ve rütbeye göre birleştirmenin ters Ackermann sınırını ilk kez kanıtlayan makale.

4.3.15Kaynaklar

  1. O. Borůvka, “O jistém problému minimálním”, Práce Moravské přírodovědecké společnosti 3, 1926.
  2. T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein, Introduction to Algorithms, 4. baskı, MIT Press, 2022 (Bölüm 19: ayrık kümeler, Bölüm 21: MST).
  3. R. L. Graham, P. Hell, “On the History of the Minimum Spanning Tree Problem”, Annals of the History of Computing 7(1), 43–57, 1985.
  4. J. B. Kruskal, “On the Shortest Spanning Subtree of a Graph and the Traveling Salesman Problem”, Proceedings of the American Mathematical Society 7(1), 1956.
  5. R. C. Prim, “Shortest Connection Networks and Some Generalizations”, Bell System Technical Journal 36(6), 1957.
  6. R. Sedgewick, K. Wayne, Algorithms, 4. baskı, Addison-Wesley, 2011 (Bölüm 4.3: Minimum Spanning Trees).
  7. R. E. Tarjan, “Efficiency of a Good But Not Linear Set Union Algorithm”, Journal of the ACM 22(2), 1975.

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