Bir kasabada birkaç mahalleyi elektrik şebekesine bağlaman gerekiyor. Her mahalle çifti arasında kablo çekmenin bir maliyeti var. Amacın tüm mahallelerin elektriğe kavuşması ama toplam kablo maliyetinin en az olması.
Bu problem bir graf problemidir ve cevabı bir kapsayan ağaçtır: Tüm düğümleri birbirine bağlayan, döngü içermeyen bir kenar kümesi. "Minimum" kelimesi de ağırlıkların toplamının en az olmasını ifade eder. Böylece minimum kapsayan ağaç (MST — Minimum Spanning Tree) elde edilir.
Bu problem, açgözlü algoritmaların gerçekten optimal sonuç verdiği ender ve değerli durumlardan biridir. İki farklı açgözlü strateji — Kruskal ve Prim — farklı yollardan aynı optimal sonuca ulaşır. Bu derste ikisini de yazacak, doğruluklarının arkasındaki kesme özelliğini anlayacak ve hangi durumda hangisini seçeceğini öğreneceksin.
Kapsayan ağaç ve MST
Graf: Kapsayan ağaç örnekleri:
A --4-- B A --4-- B A B
| ╲ | | | |
1 3 5 1 5 1 ╲3 5
| ╲ | | | | ╲ |
C --2-- D C D C D
toplam: 4+1+5=10 toplam: 1+3+5=9
MST: en az toplam ağırlıklı kapsayan ağaç
A B
| |
1 ╲3 |5 → değil
| ╲ |
A --4-- B
|
1 → 1 + 2 + 3 = 6 ← MST
|
C --2-- D (ve B-D yerine A-D=3)Kapsayan ağacın üç tanımlayıcı özelliği vardır:
Tam olarak V−1 kenar içerir. Daha azı grafı bağlamaya yetmez, daha fazlası döngü oluşturur.
Döngü içermez. Bir döngüdeki herhangi bir kenarı çıkarmak bağlantıyı bozmaz; dolayısıyla döngü gereksiz maliyettir.
Tüm düğümlere ulaşır. Graf bağlantılı değilse kapsayan ağaç yoktur; her bileşen için ayrı bir ağaç elde edilir ve sonuç bir orman olur.
Aşağıdaki görselleştirmede Kruskal algoritmasının kenarları hangi sırayla seçtiğini izleyebilirsin:
Kesme özelliği
MST algoritmalarının doğruluğu tek bir teoreme dayanır ve bu teoremi anlamak, iki algoritmanın neden çalıştığını da açıklar.
KESME ÖZELLİĞİ (cut property):
Düğümleri iki gruba ayıran herhangi bir "kesme" düşün.
Bu kesmeyi geçen kenarlar arasındaki EN HAFİF kenar,
mutlaka bir MST'de bulunur.
grup 1 | grup 2
|
A -----|---- B ağırlık 5
| | |
C -----|---- D ağırlık 3 ← bu kesmenin en hafif kenarı
| → MST'de olmak ZORUNDA
İSPAT (değişim argümanı):
En hafif kenar e'yi içermeyen bir MST olduğunu varsay.
e'yi ağaca eklersen bir döngü oluşur.
Bu döngü kesmeyi en az iki kez geçer, yani başka bir kesme kenarı f vardır.
f'yi çıkarıp e'yi koyarsan ağaç hâlâ kapsayan olur ve ağırlık(e) ≤ ağırlık(f)
olduğu için toplam artmaz. → e içeren bir MST vardır. ∎Bu teoremin iki algoritmaya nasıl uygulandığını göreceksin: Kruskal her adımda global olarak en hafif güvenli kenarı seçer; Prim ise mevcut ağacın kesmesindeki en hafif kenarı seçer. İkisi de kesme özelliğinin bir uygulamasıdır, bu yüzden ikisi de optimaldir.
Kruskal algoritması
Fikir basittir: Kenarları ağırlığa göre sırala, döngü oluşturmayanları seç. Döngü kontrolü için ayrık küme yapısı (Union-Find) kullanılır.
kenarlar sıralı: (C,D,2) (A,C,1) → hayır, önce sırala:
(A,C,1) (C,D,2) (A,D,3) (A,B,4) (B,D,5)
(A,C,1): A ve C farklı kümelerde → SEÇ, birleştir {A,C}
(C,D,2): C ve D farklı kümelerde → SEÇ, birleştir {A,C,D}
(A,D,3): A ve D AYNI kümede → döngü oluşur, ATLA
(A,B,4): A ve B farklı → SEÇ, birleştir {A,B,C,D}
(B,D,5): aynı küme → ATLA
3 kenar seçildi (V-1 = 3) → MST tamamlandı, ağırlık 1+2+4 = 7package main
import (
"fmt"
"slices"
"strings"
)
type Edge struct {
From, To string
Weight int
}
// Union-Find: döngü tespiti için
type UnionFind struct {
parent map[string]string
size map[string]int
count int
}
func NewUF(nodes []string) *UnionFind {
uf := &UnionFind{parent: map[string]string{}, size: map[string]int{}, count: len(nodes)}
for _, n := range nodes {
uf.parent[n] = n
uf.size[n] = 1
}
return uf
}
func (u *UnionFind) Find(x string) string {
root := x
for u.parent[root] != root {
root = u.parent[root]
}
for u.parent[x] != root { // yol sıkıştırma
next := u.parent[x]
u.parent[x] = root
x = next
}
return root
}
func (u *UnionFind) Union(a, b string) bool {
ra, rb := u.Find(a), u.Find(b)
if ra == rb {
return false // aynı kümede: döngü oluşur
}
if u.size[ra] < u.size[rb] {
ra, rb = rb, ra
}
u.parent[rb] = ra
u.size[ra] += u.size[rb]
u.count--
return true
}
// kruskal: kenarları sırala, döngü oluşturmayanları seç — O(E log E)
func kruskal(nodes []string, edges []Edge) ([]Edge, []Edge, int, bool) {
sorted := slices.Clone(edges)
slices.SortFunc(sorted, func(a, b Edge) int {
if a.Weight != b.Weight {
return a.Weight - b.Weight
}
if a.From != b.From {
return strings.Compare(a.From, b.From)
}
return strings.Compare(a.To, b.To)
})
uf := NewUF(nodes)
var chosen, skipped []Edge
total := 0
for _, e := range sorted {
if uf.Union(e.From, e.To) {
chosen = append(chosen, e)
total += e.Weight
continue
}
skipped = append(skipped, e)
}
// Tüm düğümler tek bileşende mi?
return chosen, skipped, total, uf.count == 1
}
func main() {
nodes := []string{"A", "B", "C", "D", "E", "F"}
edges := []Edge{
{"A", "B", 4}, {"A", "C", 1}, {"B", "C", 3},
{"B", "D", 5}, {"C", "D", 2}, {"C", "E", 7},
{"D", "E", 6}, {"D", "F", 8}, {"E", "F", 3},
}
fmt.Println("kenarlar (ağırlığa göre sıralı):")
sorted := slices.Clone(edges)
slices.SortFunc(sorted, func(a, b Edge) int { return a.Weight - b.Weight })
for _, e := range sorted {
fmt.Printf(" %s-%s: %d\n", e.From, e.To, e.Weight)
}
chosen, skipped, total, connected := kruskal(nodes, edges)
fmt.Println()
fmt.Println("SEÇİLEN kenarlar (MST):")
for _, e := range chosen {
fmt.Printf(" %s-%s: %d\n", e.From, e.To, e.Weight)
}
fmt.Println(" toplam ağırlık:", total)
fmt.Println(" kenar sayısı:", len(chosen), "| beklenen (V-1):", len(nodes)-1)
fmt.Println()
fmt.Println("ATLANAN kenarlar (döngü oluştururdu):")
for _, e := range skipped {
fmt.Printf(" %s-%s: %d\n", e.From, e.To, e.Weight)
}
fmt.Println()
fmt.Println("graf bağlantılı mı:", connected)
// Bağlantısız graf: orman elde edilir
fmt.Println()
isolated := append(slices.Clone(nodes), "G")
_, _, total2, connected2 := kruskal(isolated, edges)
fmt.Println("G düğümü eklendi (kenarsız):")
fmt.Println(" toplam ağırlık:", total2, "| bağlantılı mı:", connected2)
fmt.Println(" sonuç bir AĞAÇ değil, ORMAN")
}kenarlar (ağırlığa göre sıralı): A-C: 1 C-D: 2 B-C: 3 E-F: 3 A-B: 4 B-D: 5 D-E: 6 C-E: 7 D-F: 8 SEÇİLEN kenarlar (MST): A-C: 1 C-D: 2 B-C: 3 E-F: 3 D-E: 6 toplam ağırlık: 15 kenar sayısı: 5 | beklenen (V-1): 5 ATLANAN kenarlar (döngü oluştururdu): A-B: 4 B-D: 5 C-E: 7 D-F: 8 graf bağlantılı mı: true G düğümü eklendi (kenarsız): toplam ağırlık: 15 | bağlantılı mı: false sonuç bir AĞAÇ değil, ORMAN
Kruskal'ın karmaşıklığının baskın terimi sıralamadan gelir. Union-Find işlemleri neredeyse sabit olduğu için toplam maliyete katkısı ihmal edilebilir. Bu, Union-Find'in ne kadar verimli olduğunun bir göstergesidir — algoritmanın kalbinde durmasına rağmen karmaşıklık analizinde görünmez.
Bir başka ayrıntı: Kenarlar zaten sıralı geliyorsa (veya sayma sıralaması kullanılabilecek küçük tamsayı ağırlıklar varsa) karmaşıklık neredeyse doğrusala iner.
Prim algoritması
Prim farklı bir yoldan gider: Tek bir düğümden başla, ağacı büyüterek genişlet. Her adımda, ağaca en yakın olan ağaç dışı düğümü ekle.
A'dan başla. Ağaç = {A}
adım 1: A'nın kenarları: (A,B,4) (A,C,1)
en hafif: (A,C,1) → C ekle. Ağaç = {A,C}
adım 2: sınır kenarları: (A,B,4) (C,B,3) (C,D,2) (C,E,7)
en hafif: (C,D,2) → D ekle. Ağaç = {A,C,D}
adım 3: sınır: (A,B,4) (C,B,3) (D,B,5) (C,E,7) (D,E,6) (D,F,8)
en hafif: (C,B,3) → B ekle
...
Her adımda ağacı çevreleyen KESMEnin en hafif kenarı seçilir.Aşağıdaki görselleştirmede Prim algoritmasının ağacı nasıl büyüttüğünü izleyebilirsin:
package main
import (
"container/heap"
"fmt"
"maps"
"slices"
"strings"
)
const inf = 1 << 60
type Edge struct {
To string
Weight int
}
type Graph map[string][]Edge
func (g Graph) AddEdge(a, b string, w int) {
g[a] = append(g[a], Edge{b, w})
g[b] = append(g[b], Edge{a, w})
slices.SortFunc(g[a], func(x, y Edge) int { return strings.Compare(x.To, y.To) })
slices.SortFunc(g[b], func(x, y Edge) int { return strings.Compare(x.To, y.To) })
}
func (g Graph) AddNode(n string) {
if _, ok := g[n]; !ok {
g[n] = nil
}
}
func (g Graph) Nodes() []string { return slices.Sorted(maps.Keys(g)) }
type item struct {
node string
weight int
from string
}
type pq []item
func (q pq) Len() int { return len(q) }
func (q pq) Less(i, j int) bool {
if q[i].weight != q[j].weight {
return q[i].weight < q[j].weight
}
return q[i].node < q[j].node
}
func (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i] }
func (q *pq) Push(x any) { *q = append(*q, x.(item)) }
func (q *pq) Pop() any { o := *q; it := o[len(o)-1]; *q = o[:len(o)-1]; return it }
type MSTEdge struct {
From, To string
Weight int
}
// prim: ağacı büyüterek MST kurar — O((V + E) log V)
func (g Graph) prim(start string) ([]MSTEdge, int, bool) {
inTree := map[string]bool{}
best := map[string]int{}
from := map[string]string{}
for _, n := range g.Nodes() {
best[n] = inf
}
best[start] = 0
q := &pq{{start, 0, ""}}
heap.Init(q)
var mst []MSTEdge
total := 0
for q.Len() > 0 {
cur := heap.Pop(q).(item)
if inTree[cur.node] {
continue // bayat kayıt
}
inTree[cur.node] = true
if cur.from != "" {
mst = append(mst, MSTEdge{cur.from, cur.node, cur.weight})
total += cur.weight
}
for _, e := range g[cur.node] {
if inTree[e.To] || e.Weight >= best[e.To] {
continue
}
best[e.To] = e.Weight
from[e.To] = cur.node
heap.Push(q, item{e.To, e.Weight, cur.node})
}
}
return mst, total, len(inTree) == len(g)
}
func main() {
g := Graph{}
edges := []struct {
a, b string
w int
}{
{"A", "B", 4}, {"A", "C", 1}, {"B", "C", 3},
{"B", "D", 5}, {"C", "D", 2}, {"C", "E", 7},
{"D", "E", 6}, {"D", "F", 8}, {"E", "F", 3},
}
for _, e := range edges {
g.AddEdge(e.a, e.b, e.w)
}
mst, total, connected := g.prim("A")
fmt.Println("Prim algoritması (A'dan başlayarak):")
for i, e := range mst {
fmt.Printf(" %d. %s-%s: %d\n", i+1, e.From, e.To, e.Weight)
}
fmt.Println(" toplam ağırlık:", total)
fmt.Println(" kenar sayısı:", len(mst), "| bağlantılı:", connected)
// Başlangıç düğümü sonucu değiştirmez
fmt.Println()
fmt.Println("farklı başlangıç düğümleriyle toplam ağırlık:")
for _, start := range g.Nodes() {
_, t, _ := g.prim(start)
fmt.Printf(" %s'den başlayınca: %d\n", start, t)
}
fmt.Println("→ MST ağırlığı başlangıç noktasından bağımsızdır")
// Bağlantısız graf
fmt.Println()
g.AddNode("İzole")
_, t2, c2 := g.prim("A")
fmt.Println("izole düğüm eklendi:")
fmt.Println(" toplam:", t2, "| tüm düğümlere ulaşıldı mı:", c2)
fmt.Println(" Prim yalnızca başlangıcın bileşenini kapsar")
}Prim algoritması (A'dan başlayarak): 1. A-C: 1 2. C-D: 2 3. C-B: 3 4. D-E: 6 5. E-F: 3 toplam ağırlık: 15 kenar sayısı: 5 | bağlantılı: true farklı başlangıç düğümleriyle toplam ağırlık: A'den başlayınca: 15 B'den başlayınca: 15 C'den başlayınca: 15 D'den başlayınca: 15 E'den başlayınca: 15 F'den başlayınca: 15 → MST ağırlığı başlangıç noktasından bağımsızdır izole düğüm eklendi: toplam: 15 | tüm düğümlere ulaşıldı mı: false Prim yalnızca başlangıcın bileşenini kapsar
Prim ile Dijkstra'nın kodları neredeyse aynıdır ve bu tesadüf değildir. Aradaki tek fark, öncelik kuyruğunda saklanan değerdir: Dijkstra kaynaktan toplam mesafeyi tutar, Prim ise ağaca olan tek kenar mesafesini. Yani Dijkstra'da dist[u] + w, Prim'de yalnızca w kullanılır. Bu küçük fark, iki tamamen farklı problemi çözer.
Bir başka önemli gözlem: MST'nin ağırlığı başlangıç düğümünden bağımsızdır. Seçilen kenarlar farklı olabilir (eşit ağırlıklı kenarlar varsa) ama toplam her zaman aynıdır.
Karşılaştırma
Karar rehberi şöyledir. Kenar listesi elindeyse ve graf seyrekse Kruskal. Kenarları sıralamak dışında pek iş yoktur ve Union-Find çok hızlıdır. Komşuluk listesi elindeyse ve graf yoğunsa Prim. Tüm kenarları sıralamak yerine yalnızca sınır kenarlarına bakar.
Bağlantısız graf söz konusuysa Kruskal. Her bileşen için ayrı ağaç kurar ve sonucu bir orman olarak verir. Prim yalnızca başlangıç düğümünün bileşenini kapsar; tüm bileşenleri istiyorsan her bileşen için ayrı çalıştırman gerekir.
Pratikte fark genelde küçüktür ve elindeki veri temsili hangisinin daha kolay olduğunu belirler.
Uygulama: ağ tasarımı
MST'nin klasik uygulaması, "her şeyi en az maliyetle bağla" problemidir.
package main
import (
"fmt"
"slices"
"strings"
)
type Cable struct {
A, B string
Cost int
}
type UnionFind struct {
parent map[string]string
size map[string]int
count int
}
func NewUF(nodes []string) *UnionFind {
uf := &UnionFind{parent: map[string]string{}, size: map[string]int{}, count: len(nodes)}
for _, n := range nodes {
uf.parent[n] = n
uf.size[n] = 1
}
return uf
}
func (u *UnionFind) Find(x string) string {
root := x
for u.parent[root] != root {
root = u.parent[root]
}
for u.parent[x] != root {
next := u.parent[x]
u.parent[x] = root
x = next
}
return root
}
func (u *UnionFind) Union(a, b string) bool {
ra, rb := u.Find(a), u.Find(b)
if ra == rb {
return false
}
if u.size[ra] < u.size[rb] {
ra, rb = rb, ra
}
u.parent[rb] = ra
u.size[ra] += u.size[rb]
u.count--
return true
}
// designNetwork: MST ile en ucuz bağlantı planı
func designNetwork(sites []string, cables []Cable) ([]Cable, int, int) {
sorted := slices.Clone(cables)
slices.SortFunc(sorted, func(a, b Cable) int {
if a.Cost != b.Cost {
return a.Cost - b.Cost
}
if a.A != b.A {
return strings.Compare(a.A, b.A)
}
return strings.Compare(a.B, b.B)
})
uf := NewUF(sites)
var chosen []Cable
total := 0
for _, c := range sorted {
if uf.Union(c.A, c.B) {
chosen = append(chosen, c)
total += c.Cost
}
}
return chosen, total, uf.count
}
// maxEdgeOnPath: MST üzerinde iki düğüm arasındaki en ağır kenar
// (darboğaz kapasitesi: bu değerden kalın kablo gerekmez)
func maxEdgeOnPath(mst []Cable, from, to string) (int, bool) {
adj := map[string][]Cable{}
for _, c := range mst {
adj[c.A] = append(adj[c.A], c)
adj[c.B] = append(adj[c.B], Cable{c.B, c.A, c.Cost})
}
visited := map[string]bool{from: true}
type state struct {
node string
maxEdge int
}
stack := []state{{from, 0}}
for len(stack) > 0 {
cur := stack[len(stack)-1]
stack = stack[:len(stack)-1]
if cur.node == to {
return cur.maxEdge, true
}
for _, c := range adj[cur.node] {
if visited[c.B] {
continue
}
visited[c.B] = true
stack = append(stack, state{c.B, max(cur.maxEdge, c.Cost)})
}
}
return 0, false
}
func main() {
sites := []string{"Merkez", "Kuzey", "Güney", "Doğu", "Batı", "Liman"}
cables := []Cable{
{"Merkez", "Kuzey", 12},
{"Merkez", "Güney", 8},
{"Merkez", "Doğu", 15},
{"Merkez", "Batı", 20},
{"Kuzey", "Doğu", 7},
{"Güney", "Batı", 9},
{"Doğu", "Liman", 11},
{"Batı", "Liman", 25},
{"Kuzey", "Batı", 30},
}
fmt.Println("olası kablo güzergâhları (maliyet birimi):")
for _, c := range cables {
fmt.Printf(" %-8s ↔ %-8s %4d\n", c.A, c.B, c.Cost)
}
chosen, total, components := designNetwork(sites, cables)
fmt.Println()
fmt.Println("SEÇİLEN kablolar:")
for _, c := range chosen {
fmt.Printf(" %-8s ↔ %-8s %4d\n", c.A, c.B, c.Cost)
}
fmt.Println()
fmt.Println("toplam maliyet:", total)
fmt.Println("tüm kabloları çekmenin maliyeti:", func() int {
t := 0
for _, c := range cables {
t += c.Cost
}
return t
}())
fmt.Println("kablo sayısı:", len(chosen), "/", len(cables))
fmt.Println("bağlı bileşen sayısı:", components, "(1 = hepsi bağlı)")
fmt.Println()
fmt.Println("MST üzerindeki darboğaz kapasiteleri:")
for _, pair := range [][2]string{
{"Merkez", "Liman"},
{"Kuzey", "Batı"},
{"Güney", "Doğu"},
} {
bottleneck, ok := maxEdgeOnPath(chosen, pair[0], pair[1])
if !ok {
fmt.Printf(" %s ↔ %s: yol yok\n", pair[0], pair[1])
continue
}
fmt.Printf(" %-8s ↔ %-8s en ağır kenar: %d\n", pair[0], pair[1], bottleneck)
}
fmt.Println()
fmt.Println("MST'nin bir özelliği: iki düğüm arasındaki yolun en ağır kenarı,")
fmt.Println("tüm olası yollar arasında EN KÜÇÜK olan değerdir (minimax yol).")
}olası kablo güzergâhları (maliyet birimi): Merkez ↔ Kuzey 12 Merkez ↔ Güney 8 Merkez ↔ Doğu 15 Merkez ↔ Batı 20 Kuzey ↔ Doğu 7 Güney ↔ Batı 9 Doğu ↔ Liman 11 Batı ↔ Liman 25 Kuzey ↔ Batı 30 SEÇİLEN kablolar: Kuzey ↔ Doğu 7 Merkez ↔ Güney 8 Güney ↔ Batı 9 Doğu ↔ Liman 11 Merkez ↔ Kuzey 12 toplam maliyet: 47 tüm kabloları çekmenin maliyeti: 137 kablo sayısı: 5 / 9 bağlı bileşen sayısı: 1 (1 = hepsi bağlı) MST üzerindeki darboğaz kapasiteleri: Merkez ↔ Liman en ağır kenar: 12 Kuzey ↔ Batı en ağır kenar: 12 Güney ↔ Doğu en ağır kenar: 12 MST'nin bir özelliği: iki düğüm arasındaki yolun en ağır kenarı, tüm olası yollar arasında EN KÜÇÜK olan değerdir (minimax yol).
MST'nin son bölümde gösterilen özelliği — minimax yol özelliği — az bilinen ama çok kullanışlıdır: MST üzerindeki iki düğüm arasındaki yolun en ağır kenarı, o iki düğüm arasındaki tüm yollar içinde en ağır kenarı en küçük olan yoldur. Bu, "en zayıf halkayı en güçlü tutmak" gerektiğinde işe yarar: ağ kapasitesi planlama, güvenilirlik analizi, en az yükseklik tırmanışı gibi problemler.
MST'yi tanımak ve sınırları
MST, çok özel bir problemi çözer ve benzer görünen ama farklı olan problemlerle karıştırılmaya müsaittir. Ayrımları bilmek doğru aracı seçmeni sağlar.
MST, en kısa yol ağacı DEĞİLDİR. MST toplam kenar ağırlığını minimize eder; bir düğümden diğerine giden yolun kısa olmasını garanti etmez. Dijkstra'nın ürettiği ağaç, kaynaktan her düğüme en kısa yolu verir ama toplam ağırlığı minimal olmayabilir. İki farklı optimallik ölçütü, iki farklı ağaç.
MST, gezgin satıcı problemini çözmez. "Tüm şehirleri gez ve başlangıca dön" problemi NP-zordur. MST bu problem için bir alt sınır ve yaklaşık çözüm sağlar (MST ağırlığının iki katı, en iyi turdan daha kötü olamaz) ama optimal turu vermez.
MST tek olmayabilir. Eşit ağırlıklı kenarlar varsa birden çok MST bulunabilir; hepsi aynı toplam ağırlığa sahiptir. Tüm kenar ağırlıkları birbirinden farklıysa MST tektir.
Maksimum kapsayan ağaç da aynı algoritmalarla bulunur. Ağırlıkların işaretini değiştirmek ya da sıralamayı ters çevirmek yeterlidir. "En güvenilir ağ" gibi problemlerde bu varyant kullanılır.
Yönlü graflarda MST tanımı farklıdır. Kruskal ve Prim yönsüz graflar içindir. Yönlü karşılığı "minimum kapsayan arborescence" olarak adlandırılır ve farklı algoritmalar gerektirir.
Ek kısıtlar problemi zorlaştırır. "Her düğümün derecesi en fazla k olsun" ya da "şu kenarlar mutlaka dâhil olsun" gibi kısıtlar eklendiğinde problem NP-zor hâle gelebilir. Açgözlü yaklaşımın işe yaraması, kısıtsız hâlin özel yapısı sayesindedir.
Sık yapılan hatalar
- MST'yi en kısa yol ağacıyla karıştırmak. İki farklı optimallik ölçütüdür; MST'de iki düğüm arası yol uzun olabilir.
- Kruskal'da bağlantılılık kontrolünü atlamak. Graf kopuksa sonuç ağaç değil ormandır; bileşen sayısını kontrol et.
- Prim'i bağlantısız grafta tek seferde çalıştırıp tamamlandığını sanmak. Yalnızca başlangıcın bileşenini kapsar.
- Eşit ağırlıklarda deterministik sıra tanımlamamak. Farklı ama eşit ağırlıklı MST'ler üretilir; testler kırılgan olur.
- Öncelik kuyruğunda bayat kayıtları atlamamak. Prim'de aynı düğüm birden çok kez kuyruğa girebilir.
- V−1 kenar kontrolünü yapmamak. Doğru kurulmuş bir MST tam olarak V−1 kenar içerir; farklıysa bir hata vardır.
- Yönlü graflarda bu algoritmaları kullanmak. Kruskal ve Prim yalnızca yönsüz graflar için geçerlidir.
Alıştırmalar
Verilen bir kenar kümesinin geçerli bir kapsayan ağaç olup olmadığını kontrol eden bir fonksiyon yaz: kenar sayısı V−1 mi, döngü içeriyor mu, tüm düğümlere ulaşıyor mu? Ayrıca ağırlığını Kruskal'ın bulduğuyla karşılaştır.
İpucu
Üç koşulu ayrı ayrı kontrol et. Union-Find kullanırsan döngü ve bağlantılılık kontrolü aynı geçişte yapılabilir.
Çözümü göster
package main
import (
"fmt"
"slices"
"strings"
)
type Edge struct {
From, To string
Weight int
}
type UnionFind struct {
parent map[string]string
count int
}
func NewUF(nodes []string) *UnionFind {
uf := &UnionFind{parent: map[string]string{}, count: len(nodes)}
for _, n := range nodes {
uf.parent[n] = n
}
return uf
}
func (u *UnionFind) Find(x string) string {
for u.parent[x] != x {
u.parent[x] = u.parent[u.parent[x]] // yol yarılama
x = u.parent[x]
}
return x
}
func (u *UnionFind) Union(a, b string) bool {
ra, rb := u.Find(a), u.Find(b)
if ra == rb {
return false
}
u.parent[rb] = ra
u.count--
return true
}
// validate: kenar kümesi geçerli bir kapsayan ağaç mı
func validate(nodes []string, candidate []Edge) (bool, []string) {
var problems []string
if len(candidate) != len(nodes)-1 {
problems = append(problems,
fmt.Sprintf("kenar sayısı %d, beklenen %d (V-1)", len(candidate), len(nodes)-1))
}
uf := NewUF(nodes)
for _, e := range candidate {
if !uf.Union(e.From, e.To) {
problems = append(problems,
fmt.Sprintf("%s-%s kenarı döngü oluşturuyor", e.From, e.To))
}
}
if uf.count > 1 {
problems = append(problems,
fmt.Sprintf("graf bağlantılı değil: %d bileşen", uf.count))
}
return len(problems) == 0, problems
}
func kruskal(nodes []string, edges []Edge) ([]Edge, int) {
sorted := slices.Clone(edges)
slices.SortFunc(sorted, func(a, b Edge) int {
if a.Weight != b.Weight {
return a.Weight - b.Weight
}
return strings.Compare(a.From+a.To, b.From+b.To)
})
uf := NewUF(nodes)
var chosen []Edge
total := 0
for _, e := range sorted {
if uf.Union(e.From, e.To) {
chosen = append(chosen, e)
total += e.Weight
}
}
return chosen, total
}
func weight(edges []Edge) int {
t := 0
for _, e := range edges {
t += e.Weight
}
return t
}
func main() {
nodes := []string{"A", "B", "C", "D"}
all := []Edge{
{"A", "B", 4}, {"A", "C", 1}, {"B", "C", 3},
{"B", "D", 5}, {"C", "D", 2},
}
mst, mstWeight := kruskal(nodes, all)
fmt.Println("Kruskal'ın MST'si:")
for _, e := range mst {
fmt.Printf(" %s-%s: %d\n", e.From, e.To, e.Weight)
}
fmt.Println(" ağırlık:", mstWeight)
candidates := []struct {
name string
edges []Edge
}{
{"MST'nin kendisi", mst},
{"geçerli ama optimal değil", []Edge{{"A", "B", 4}, {"B", "C", 3}, {"C", "D", 2}}},
{"döngü içeren", []Edge{{"A", "C", 1}, {"C", "D", 2}, {"A", "B", 4}, {"B", "C", 3}}},
{"eksik kenar", []Edge{{"A", "C", 1}, {"C", "D", 2}}},
{"kopuk", []Edge{{"A", "C", 1}, {"B", "D", 5}}},
}
fmt.Println()
for _, c := range candidates {
ok, problems := validate(nodes, c.edges)
w := weight(c.edges)
fmt.Printf("%s (%d kenar, ağırlık %d):\n", c.name, len(c.edges), w)
if ok {
optimal := "OPTIMAL"
if w > mstWeight {
optimal = fmt.Sprintf("optimal değil (+%d)", w-mstWeight)
}
fmt.Printf(" geçerli kapsayan ağaç, %s\n", optimal)
continue
}
fmt.Println(" GEÇERSİZ:")
for _, p := range problems {
fmt.Println(" -", p)
}
}
}Kruskal'ın MST'si: A-C: 1 C-D: 2 B-C: 3 ağırlık: 6 MST'nin kendisi (3 kenar, ağırlık 6): geçerli kapsayan ağaç, OPTIMAL geçerli ama optimal değil (3 kenar, ağırlık 9): geçerli kapsayan ağaç, optimal değil (+3) döngü içeren (4 kenar, ağırlık 10): GEÇERSİZ: - kenar sayısı 4, beklenen 3 (V-1) - B-C kenarı döngü oluşturuyor eksik kenar (2 kenar, ağırlık 3): GEÇERSİZ: - kenar sayısı 2, beklenen 3 (V-1) - graf bağlantılı değil: 2 bileşen kopuk (2 kenar, ağırlık 6): GEÇERSİZ: - kenar sayısı 2, beklenen 3 (V-1) - graf bağlantılı değil: 2 bileşen
Doğrulayıcı yazmak, algoritma geliştirirken çok değerli bir alışkanlıktır: Kendi MST uygulamanın çıktısını bağımsız bir kontrolle sınamak, sessiz hataları yakalar. İkinci adaydaki durum özellikle öğreticidir — geçerli bir kapsayan ağaç ama minimum değil. Geçerlilik ve optimallik iki ayrı özelliktir; her ikisini de kontrol etmek gerekir.
İki düğüm arasında, yol üzerindeki en ağır kenarı en küçük olan yolu bul. MST'nin minimax özelliğini kullanarak çöz ve kaba kuvvetle doğrula.
İpucu
MST üzerindeki yol, minimax yoldur. MST'yi kur, sonra iki düğüm arasındaki yolu DFS ile bul ve üzerindeki en ağır kenarı döndür.
Çözümü göster
package main
import (
"fmt"
"slices"
"strings"
)
type Edge struct {
From, To string
Weight int
}
type UnionFind struct {
parent map[string]string
}
func NewUF(nodes []string) *UnionFind {
uf := &UnionFind{parent: map[string]string{}}
for _, n := range nodes {
uf.parent[n] = n
}
return uf
}
func (u *UnionFind) Find(x string) string {
for u.parent[x] != x {
u.parent[x] = u.parent[u.parent[x]]
x = u.parent[x]
}
return x
}
func (u *UnionFind) Union(a, b string) bool {
ra, rb := u.Find(a), u.Find(b)
if ra == rb {
return false
}
u.parent[rb] = ra
return true
}
func kruskal(nodes []string, edges []Edge) []Edge {
sorted := slices.Clone(edges)
slices.SortFunc(sorted, func(a, b Edge) int {
if a.Weight != b.Weight {
return a.Weight - b.Weight
}
return strings.Compare(a.From+a.To, b.From+b.To)
})
uf := NewUF(nodes)
var mst []Edge
for _, e := range sorted {
if uf.Union(e.From, e.To) {
mst = append(mst, e)
}
}
return mst
}
// bottleneckViaMST: MST üzerindeki yolun en ağır kenarı
func bottleneckViaMST(nodes []string, edges []Edge, from, to string) (int, []string, bool) {
mst := kruskal(nodes, edges)
adj := map[string][]Edge{}
for _, e := range mst {
adj[e.From] = append(adj[e.From], e)
adj[e.To] = append(adj[e.To], Edge{e.To, e.From, e.Weight})
}
for k := range adj {
slices.SortFunc(adj[k], func(a, b Edge) int { return strings.Compare(a.To, b.To) })
}
visited := map[string]bool{from: true}
type state struct {
node string
maxW int
path []string
}
stack := []state{{from, 0, []string{from}}}
for len(stack) > 0 {
cur := stack[len(stack)-1]
stack = stack[:len(stack)-1]
if cur.node == to {
return cur.maxW, cur.path, true
}
for _, e := range adj[cur.node] {
if visited[e.To] {
continue
}
visited[e.To] = true
newPath := slices.Clone(cur.path)
newPath = append(newPath, e.To)
stack = append(stack, state{e.To, max(cur.maxW, e.Weight), newPath})
}
}
return 0, nil, false
}
// bruteForce: tüm yolları dene, en küçük darboğazı bul
func bruteForce(nodes []string, edges []Edge, from, to string) (int, bool) {
adj := map[string][]Edge{}
for _, e := range edges {
adj[e.From] = append(adj[e.From], e)
adj[e.To] = append(adj[e.To], Edge{e.To, e.From, e.Weight})
}
best := 1 << 60
found := false
visited := map[string]bool{}
var dfs func(node string, maxW int)
dfs = func(node string, maxW int) {
if node == to {
if maxW < best {
best = maxW
}
found = true
return
}
visited[node] = true
for _, e := range adj[node] {
if !visited[e.To] {
dfs(e.To, max(maxW, e.Weight))
}
}
visited[node] = false
}
dfs(from, 0)
return best, found
}
func main() {
nodes := []string{"A", "B", "C", "D", "E"}
edges := []Edge{
{"A", "B", 10}, {"A", "C", 3}, {"C", "B", 4},
{"B", "D", 8}, {"C", "D", 9}, {"D", "E", 2},
{"C", "E", 15},
}
fmt.Println("kenarlar:")
for _, e := range edges {
fmt.Printf(" %s-%s: %d\n", e.From, e.To, e.Weight)
}
mst := kruskal(nodes, edges)
fmt.Println()
fmt.Println("MST:")
for _, e := range mst {
fmt.Printf(" %s-%s: %d\n", e.From, e.To, e.Weight)
}
fmt.Println()
fmt.Printf("%-12s %12s %12s %8s %s\n", "çift", "MST yolu", "kaba kuvvet", "aynı", "yol")
for _, pair := range [][2]string{
{"A", "B"}, {"A", "D"}, {"A", "E"}, {"B", "E"}, {"C", "E"},
} {
viaM, path, ok1 := bottleneckViaMST(nodes, edges, pair[0], pair[1])
brute, ok2 := bruteForce(nodes, edges, pair[0], pair[1])
if !ok1 || !ok2 {
fmt.Printf("%-12s yol yok\n", pair[0]+"-"+pair[1])
continue
}
fmt.Printf("%-12s %12d %12d %8t %s\n",
pair[0]+"-"+pair[1], viaM, brute, viaM == brute, strings.Join(path, "→"))
}
fmt.Println()
fmt.Println("A → B için: doğrudan kenar 10, ama A→C→B yolunda en ağır kenar 4")
fmt.Println("MST'nin minimax özelliği bunu otomatik verir.")
}kenarlar: A-B: 10 A-C: 3 C-B: 4 B-D: 8 C-D: 9 D-E: 2 C-E: 15 MST: D-E: 2 A-C: 3 C-B: 4 B-D: 8 çift MST yolu kaba kuvvet aynı yol A-B 4 4 true A→C→B A-D 8 8 true A→C→B→D A-E 8 8 true A→C→B→D→E B-E 8 8 true B→D→E C-E 8 8 true C→B→D→E A → B için: doğrudan kenar 10, ama A→C→B yolunda en ağır kenar 4 MST'nin minimax özelliği bunu otomatik verir.
Minimax yol problemi, ilk bakışta en kısa yol probleminin bir varyantı gibi görünür ama farklı bir ölçüt kullanır: Toplamı değil maksimumu minimize eder. Bu, "zincirin en zayıf halkasını en güçlü tutmak" anlamına gelir.
MST'nin bu problemi çözmesi, kesme özelliğinin bir sonucudur. MST üzerinde iki düğüm arasındaki yolu düşün; o yoldaki en ağır kenarı daha hafif bir kenarla değiştirebilseydin, MST daha hafif olurdu — ki bu mümkün değildir. Bu yüzden MST yolu, en küçük darboğaza sahip yoldur.
Pratik uygulaması geniştir: ağ bağlantı kapasitesi planlama (en yavaş bağlantıyı en hızlı tutmak), su dağıtım şebekelerinde en dar borunun kapasitesi, dağ yürüyüşünde en yüksek tırmanışı en düşük tutan güzergâh.
Bir grafın ikinci en küçük kapsayan ağacını bul. Yani MST'den farklı, ama ağırlığı ona en yakın olan kapsayan ağacı. MST'nin her kenarını tek tek çıkarıp yeniden hesaplayarak çöz.
İpucu
İkinci en iyi MST, MST'den tam olarak bir kenar farkıyla elde edilir. MST'nin her kenarını sırayla yasaklayıp Kruskal'ı yeniden çalıştır; en küçük sonucu seç.
Çözümü göster
package main
import (
"fmt"
"slices"
"strings"
)
type Edge struct {
From, To string
Weight int
}
func (e Edge) String() string { return fmt.Sprintf("%s-%s(%d)", e.From, e.To, e.Weight) }
type UnionFind struct {
parent map[string]string
count int
}
func NewUF(nodes []string) *UnionFind {
uf := &UnionFind{parent: map[string]string{}, count: len(nodes)}
for _, n := range nodes {
uf.parent[n] = n
}
return uf
}
func (u *UnionFind) Find(x string) string {
for u.parent[x] != x {
u.parent[x] = u.parent[u.parent[x]]
x = u.parent[x]
}
return x
}
func (u *UnionFind) Union(a, b string) bool {
ra, rb := u.Find(a), u.Find(b)
if ra == rb {
return false
}
u.parent[rb] = ra
u.count--
return true
}
// kruskalExcluding: belirli bir kenarı hariç tutarak MST kurar
func kruskalExcluding(nodes []string, edges []Edge, exclude int) ([]Edge, int, bool) {
type indexed struct {
Edge
idx int
}
sorted := make([]indexed, 0, len(edges))
for i, e := range edges {
if i == exclude {
continue
}
sorted = append(sorted, indexed{e, i})
}
slices.SortFunc(sorted, func(a, b indexed) int {
if a.Weight != b.Weight {
return a.Weight - b.Weight
}
return strings.Compare(a.From+a.To, b.From+b.To)
})
uf := NewUF(nodes)
var chosen []Edge
total := 0
for _, e := range sorted {
if uf.Union(e.From, e.To) {
chosen = append(chosen, e.Edge)
total += e.Weight
}
}
return chosen, total, uf.count == 1
}
// mstIndices: MST'de kullanılan kenarların indekslerini döndürür
func mstIndices(nodes []string, edges []Edge) ([]int, int) {
type indexed struct {
Edge
idx int
}
sorted := make([]indexed, len(edges))
for i, e := range edges {
sorted[i] = indexed{e, i}
}
slices.SortFunc(sorted, func(a, b indexed) int {
if a.Weight != b.Weight {
return a.Weight - b.Weight
}
return strings.Compare(a.From+a.To, b.From+b.To)
})
uf := NewUF(nodes)
var used []int
total := 0
for _, e := range sorted {
if uf.Union(e.From, e.To) {
used = append(used, e.idx)
total += e.Weight
}
}
slices.Sort(used)
return used, total
}
// secondBestMST: MST kenarlarını tek tek yasaklayarak ikinci en iyiyi bulur
func secondBestMST(nodes []string, edges []Edge) ([]Edge, int, Edge, bool) {
mstIdx, mstWeight := mstIndices(nodes, edges)
bestWeight := 1 << 60
var bestTree []Edge
var removed Edge
found := false
for _, idx := range mstIdx {
tree, weight, connected := kruskalExcluding(nodes, edges, idx)
if !connected {
continue // bu kenar köprü: çıkarılırsa graf kopar
}
if weight < bestWeight {
bestWeight, bestTree, removed, found = weight, tree, edges[idx], true
}
}
_ = mstWeight
return bestTree, bestWeight, removed, found
}
func main() {
nodes := []string{"A", "B", "C", "D"}
edges := []Edge{
{"A", "B", 1}, {"B", "C", 2}, {"C", "D", 3},
{"A", "C", 4}, {"B", "D", 5}, {"A", "D", 6},
}
fmt.Println("kenarlar:")
for i, e := range edges {
fmt.Printf(" %d. %v\n", i, e)
}
mstIdx, mstWeight := mstIndices(nodes, edges)
fmt.Println()
fmt.Println("MST:")
for _, i := range mstIdx {
fmt.Printf(" %v\n", edges[i])
}
fmt.Println(" ağırlık:", mstWeight)
second, secondWeight, removed, ok := secondBestMST(nodes, edges)
fmt.Println()
if !ok {
fmt.Println("ikinci MST bulunamadı (graf çok kısıtlı)")
return
}
fmt.Println("İKİNCİ en iyi kapsayan ağaç:")
for _, e := range second {
fmt.Printf(" %v\n", e)
}
fmt.Println(" ağırlık:", secondWeight)
fmt.Println(" çıkarılan kenar:", removed)
fmt.Println(" fark:", secondWeight-mstWeight)
// Köprü kenarı olan graf
fmt.Println()
bridged := []string{"P", "Q", "R"}
bridgeEdges := []Edge{
{"P", "Q", 1}, {"Q", "R", 2},
}
_, w := mstIndices(bridged, bridgeEdges)
_, _, _, ok2 := secondBestMST(bridged, bridgeEdges)
fmt.Println("ağaç şeklindeki graf (MST ağırlığı", w, "):")
fmt.Println(" ikinci MST var mı:", ok2)
fmt.Println(" → her kenar köprü olduğu için başka kapsayan ağaç yok")
}kenarlar: 0. A-B(1) 1. B-C(2) 2. C-D(3) 3. A-C(4) 4. B-D(5) 5. A-D(6) MST: A-B(1) B-C(2) C-D(3) ağırlık: 6 İKİNCİ en iyi kapsayan ağaç: A-B(1) C-D(3) A-C(4) ağırlık: 8 çıkarılan kenar: B-C(2) fark: 2 ağaç şeklindeki graf (MST ağırlığı 3 ): ikinci MST var mı: false → her kenar köprü olduğu için başka kapsayan ağaç yok
Bu çözümün temelinde önemli bir gözlem var: İkinci en iyi MST, MST'den tam olarak bir kenar farkıyla elde edilir. Neden? Çünkü iki kenar farkı olan bir ağaç, tek kenar farkı olan bir ağaçtan daha ağır olmak zorundadır — aksi hâlde o ara ağaç MST olurdu.
Bu yüzden MST'nin her kenarını sırayla yasaklayıp yeniden hesaplamak yeterlidir. Karmaşıklık O(V × E log E) olur: V−1 kenar için Kruskal'ı yeniden çalıştırıyoruz.
Daha verimli bir çözüm de vardır: MST'ye eklenebilecek her kenar için, o kenarın MST'de oluşturacağı döngüdeki en ağır kenarı bulup değiştirmek. Önceden hesaplama ile bu, O(E log V) seviyesine iner. Ama kavramsal olarak aynı fikri uygular.
Köprü kenarı olan durumun ele alınmasına dikkat et: Bir kenarın çıkarılması grafı kopuk hâle getiriyorsa, o kenar zorunludur ve alternatif ağaç kurulamaz. Ağaç şeklindeki bir grafta tüm kenarlar köprü olduğu için ikinci MST hiç yoktur.
Kısa sınav
Bir kapsayan ağaç kaç kenar içerir?
Kesme özelliği (cut property) ne söyler?
Kruskal algoritması döngü tespitini nasıl yapar?
Prim ile Dijkstra arasındaki temel fark nedir?
MST, iki düğüm arasındaki en kısa yolu verir mi?
Bağlantısız bir grafta hangi algoritma tüm bileşenleri kapsar?
Özet
- Kapsayan ağaç, tüm düğümleri bağlayan ve döngü içermeyen V−1 kenarlık bir kümedir.
- MST, kapsayan ağaçlar arasında toplam ağırlığı en küçük olanıdır.
- Kesme özelliği MST algoritmalarının doğruluk temelidir: Herhangi bir kesmenin en hafif kenarı bir MST'de bulunur.
- Kruskal kenarları ağırlığa göre sıralar ve döngü oluşturmayanları seçer; Union-Find kullanır ve O(E log E) çalışır.
- Prim tek düğümden başlayıp ağacı büyütür; öncelik kuyruğu kullanır ve O((V+E) log V) çalışır.
- Prim'in kodu Dijkstra'ya çok benzer; tek fark kuyrukta tutulan değerdir.
- Seyrek graflarda ve kenar listesi temsilinde Kruskal, yoğun graflarda ve komşuluk listesinde Prim daha uygundur.
- Bağlantısız graflarda Kruskal orman üretir; Prim yalnızca bir bileşeni kapsar.
- MST, en kısa yol ağacı değildir ve gezgin satıcı problemini çözmez; ancak minimax yol özelliğini sağlar.