BFS ile ağırlıksız graflarda en kısa yolu bulmayı öğrendin: Katman katman ilerlemek yeterliydi, çünkü her kenar aynı maliyeti taşıyordu. Gerçek dünyada ise durum böyle değildir. Şehirler arası yollar farklı uzunluktadır, ağ bağlantıları farklı gecikmelere sahiptir, uçuşlar farklı fiyatlarla satılır.
Kenarlar ağırlıklı olduğunda BFS'in garantisi çöker: İki kısa kenarla ulaşılan bir düğüm, tek uzun kenarla ulaşılandan daha yakın olabilir. Bu derste ağırlıklı graflarda en kısa yol bulan üç temel algoritmayı öğreneceksin. Her birinin kendi kullanım alanı ve kendi sınırları var.
Dijkstra en hızlısıdır ama negatif ağırlıkla çalışmaz. Bellman-Ford daha yavaştır ama negatif ağırlıkları kaldırır ve negatif döngüleri tespit eder. Floyd-Warshall tüm düğüm çiftleri arasındaki mesafeleri tek seferde hesaplar. Hepsinin temelinde aynı basit işlem durur: gevşetme.
Gevşetme (relaxation)
Tüm en kısa yol algoritmalarının çekirdeği tek bir satırdır:
Elimizde her düğüm için "şimdiye kadar bulunan en kısa mesafe" tahmini var.
u → v kenarı, ağırlığı w:
eğer dist[u] + w < dist[v] ise
dist[v] = dist[u] + w ← daha kısa bir yol bulundu!
parent[v] = u ← yolu geri kurmak için
Buna "kenarı gevşetmek" denir.
Başlangıçta tüm mesafeler sonsuz, kaynak sıfırdır.Algoritmalar arasındaki fark, kenarların hangi sırayla gevşetildiğidir. Dijkstra en yakın düğümden başlar, Bellman-Ford tüm kenarları tekrar tekrar gevşetir, Floyd-Warshall ara düğümler üzerinden gider.
package main
import "fmt"
const inf = 1 << 60
type Edge struct {
From, To string
Weight int
}
func main() {
// Gevşetmenin nasıl çalıştığını elle izleyelim
dist := map[string]int{"A": 0, "B": inf, "C": inf, "D": inf}
parent := map[string]string{}
edges := []Edge{
{"A", "B", 4},
{"A", "C", 2},
{"C", "B", 1},
{"B", "D", 5},
{"C", "D", 8},
}
show := func(step string) {
fmt.Printf("%-24s", step)
for _, n := range []string{"A", "B", "C", "D"} {
if dist[n] >= inf {
fmt.Printf("%6s", "∞")
continue
}
fmt.Printf("%6d", dist[n])
}
fmt.Println()
}
fmt.Printf("%-24s%6s%6s%6s%6s\n", "adım", "A", "B", "C", "D")
show("başlangıç")
for _, e := range edges {
if dist[e.From] >= inf {
continue
}
if dist[e.From]+e.Weight < dist[e.To] {
dist[e.To] = dist[e.From] + e.Weight
parent[e.To] = e.From
show(fmt.Sprintf("%s→%s (%d) gevşetildi", e.From, e.To, e.Weight))
continue
}
show(fmt.Sprintf("%s→%s (%d) gevşemedi", e.From, e.To, e.Weight))
}
fmt.Println()
fmt.Println("A'dan mesafeler:", dist)
fmt.Println("ebeveynler:", parent)
fmt.Println()
fmt.Println("Dikkat: A→B doğrudan 4, ama A→C→B yolu 2+1=3 → daha kısa!")
fmt.Println("Gevşetme bunu otomatik yakalar.")
}adım A B C D başlangıç 0 ∞ ∞ ∞ A→B (4) gevşetildi 0 4 ∞ ∞ A→C (2) gevşetildi 0 4 2 ∞ C→B (1) gevşetildi 0 3 2 ∞ B→D (5) gevşetildi 0 3 2 8 C→D (8) gevşemedi 0 3 2 8 A'dan mesafeler: map[A:0 B:3 C:2 D:8] ebeveynler: map[B:C C:A D:B] Dikkat: A→B doğrudan 4, ama A→C→B yolu 2+1=3 → daha kısa! Gevşetme bunu otomatik yakalar.
Dijkstra algoritması
Dijkstra'nın fikri açgözlüdür: Henüz kesinleşmemiş düğümler arasından en yakın olanı seç, kesinleştir, komşularını gevşet. Bir düğüm kesinleştiğinde onun mesafesi bir daha değişmez.
A --4-- B
| ╲ |
2 1 5
| ╲ |
C --8-- D
adım 1: A kesinleşti (0). Komşuları gevşet: B=4, C=2
adım 2: en yakın kesinleşmemiş: C (2). Komşuları: B=min(4, 2+1)=3, D=2+8=10
adım 3: en yakın: B (3). Komşusu: D=min(10, 3+5)=8
adım 4: D kesinleşti (8)
Sonuç: A→C→B→D = 2+1+5 = 8Neden açgözlü seçim doğrudur? Çünkü tüm ağırlıklar negatif olmadığı için, en yakın kesinleşmemiş düğüme daha kısa bir yol bulmak imkânsızdır — herhangi bir başka yol, ondan daha uzak bir düğümden geçmek zorunda kalırdı ve bu, mesafeyi ancak artırabilir.
Aşağıdaki görselleştirmede Dijkstra'nın adım adım hangi düğümü kesinleştirdiğini 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(from, to string, w int) {
g[from] = append(g[from], Edge{to, w})
g[to] = append(g[to], Edge{from, w}) // yönsüz
slices.SortFunc(g[from], func(a, b Edge) int { return strings.Compare(a.To, b.To) })
slices.SortFunc(g[to], func(a, b Edge) int { return strings.Compare(a.To, b.To) })
}
func (g Graph) Nodes() []string { return slices.Sorted(maps.Keys(g)) }
// Öncelik kuyruğu: container/heap arayüzü
type item struct {
node string
dist int
}
type pq []item
func (q pq) Len() int { return len(q) }
func (q pq) Less(i, j int) bool {
if q[i].dist != q[j].dist {
return q[i].dist < q[j].dist
}
return q[i].node < q[j].node // deterministik
}
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 { old := *q; it := old[len(old)-1]; *q = old[:len(old)-1]; return it }
// dijkstra: O((V + E) log V)
func (g Graph) dijkstra(source string) (map[string]int, map[string]string) {
dist := map[string]int{}
parent := map[string]string{}
for _, n := range g.Nodes() {
dist[n] = inf
}
dist[source] = 0
q := &pq{{source, 0}}
heap.Init(q)
done := map[string]bool{}
for q.Len() > 0 {
cur := heap.Pop(q).(item)
if done[cur.node] {
continue // eski (bayat) kayıt: atla
}
done[cur.node] = true
for _, e := range g[cur.node] {
if done[e.To] {
continue
}
if newDist := dist[cur.node] + e.Weight; newDist < dist[e.To] {
dist[e.To] = newDist
parent[e.To] = cur.node
heap.Push(q, item{e.To, newDist})
}
}
}
return dist, parent
}
// path: hedefe giden yolu geri kurar
func path(parent map[string]string, source, target string) ([]string, bool) {
if source == target {
return []string{source}, true
}
if _, ok := parent[target]; !ok {
return nil, false
}
var out []string
for n := target; n != ""; n = parent[n] {
out = append(out, n)
if n == source {
break
}
}
slices.Reverse(out)
if out[0] != source {
return nil, false
}
return out, true
}
func main() {
// Şehirler arası mesafeler (km)
g := Graph{}
roads := []struct {
a, b string
km int
}{
{"Ankara", "Konya", 258},
{"Ankara", "Eskişehir", 233},
{"Eskişehir", "Bursa", 154},
{"Konya", "Antalya", 310},
{"Eskişehir", "Afyon", 144},
{"Afyon", "Antalya", 291},
{"Afyon", "İzmir", 327},
{"Bursa", "İzmir", 328},
}
for _, r := range roads {
g.AddEdge(r.a, r.b, r.km)
}
g["Van"] = nil // bağlantısı olmayan şehir
dist, parent := g.dijkstra("Ankara")
fmt.Println("Ankara'dan mesafeler:")
for _, n := range g.Nodes() {
if dist[n] >= inf {
fmt.Printf(" %-12s ulaşılamaz\n", n)
continue
}
p, _ := path(parent, "Ankara", n)
fmt.Printf(" %-12s %5d km %s\n", n, dist[n], strings.Join(p, " → "))
}
fmt.Println()
fmt.Println("Ankara → Antalya karşılaştırması:")
fmt.Println(" Konya üzerinden: 258 + 310 =", 258+310, "km")
fmt.Println(" Afyon üzerinden: 233 + 144 + 291 =", 233+144+291, "km")
fmt.Println(" Dijkstra'nın bulduğu:", dist["Antalya"], "km")
}Ankara'dan mesafeler: Afyon 377 km Ankara → Eskişehir → Afyon Ankara 0 km Ankara Antalya 568 km Ankara → Konya → Antalya Bursa 387 km Ankara → Eskişehir → Bursa Eskişehir 233 km Ankara → Eskişehir Konya 258 km Ankara → Konya Van ulaşılamaz İzmir 704 km Ankara → Eskişehir → Afyon → İzmir Ankara → Antalya karşılaştırması: Konya üzerinden: 258 + 310 = 568 km Afyon üzerinden: 233 + 144 + 291 = 668 km Dijkstra'nın bulduğu: 568 km
Negatif ağırlıklar ve Bellman-Ford
Dijkstra neden negatif ağırlıklarla çalışmaz? Çünkü açgözlü varsayımı bozulur: Bir düğümü kesinleştirdikten sonra, negatif bir kenar üzerinden ona daha kısa bir yol bulunabilir.
A --1--> B
| |
4 -5
| ↓
+------> C
Dijkstra: A(0) → B'yi kesinleştir (1) → C'yi kesinleştir (4)
Ama A→B→C = 1 + (-5) = -4 daha kısa!
B zaten kesinleşmişti, tekrar bakılmadı → YANLIŞ CEVAP
Bellman-Ford: tüm kenarları V-1 kez gevşetir, bu yüzden yakalar.Bellman-Ford'un fikri kaba ama sağlamdır: Tüm kenarları V−1 kez gevşet. Neden V−1? Çünkü en kısa yol en fazla V−1 kenar içerebilir (daha fazlası döngü demektir) ve her tur en az bir kenarı kesinleştirir.
package main
import (
"fmt"
"maps"
"slices"
"strings"
)
const inf = 1 << 60
type Edge struct {
From, To string
Weight int
}
type Digraph struct {
edges []Edge
nodes map[string]bool
}
func NewDigraph() *Digraph {
return &Digraph{nodes: map[string]bool{}}
}
func (g *Digraph) AddEdge(from, to string, w int) {
g.nodes[from], g.nodes[to] = true, true
g.edges = append(g.edges, Edge{from, to, w})
}
func (g *Digraph) Nodes() []string { return slices.Sorted(maps.Keys(g.nodes)) }
// bellmanFord: negatif ağırlıkları destekler — O(V × E)
func (g *Digraph) bellmanFord(source string) (map[string]int, map[string]string, bool, []string) {
dist := map[string]int{}
parent := map[string]string{}
for _, n := range g.Nodes() {
dist[n] = inf
}
dist[source] = 0
n := len(g.nodes)
// V-1 tur: her turda tüm kenarları gevşet
for range n - 1 {
changed := false
for _, e := range g.edges {
if dist[e.From] >= inf {
continue
}
if dist[e.From]+e.Weight < dist[e.To] {
dist[e.To] = dist[e.From] + e.Weight
parent[e.To] = e.From
changed = true
}
}
if !changed {
break // erken çıkış: değişiklik kalmadı
}
}
// V. tur: hâlâ gevşeme varsa NEGATİF DÖNGÜ var
for _, e := range g.edges {
if dist[e.From] >= inf {
continue
}
if dist[e.From]+e.Weight < dist[e.To] {
// Döngüyü bul: e.To'dan geriye n kez git
cycle := findNegativeCycle(parent, e.To, n)
return dist, parent, false, cycle
}
}
return dist, parent, true, nil
}
func findNegativeCycle(parent map[string]string, start string, n int) []string {
// Döngüye kesin girmek için n adım geri git
node := start
for range n {
if p, ok := parent[node]; ok {
node = p
}
}
// Döngüyü topla
var cycle []string
seen := map[string]bool{}
for cur := node; !seen[cur]; cur = parent[cur] {
seen[cur] = true
cycle = append(cycle, cur)
}
cycle = append(cycle, node)
slices.Reverse(cycle)
return cycle
}
func main() {
// Negatif ağırlıklı graf (Dijkstra burada yanlış sonuç verir)
g := NewDigraph()
g.AddEdge("A", "B", 1)
g.AddEdge("A", "C", 4)
g.AddEdge("B", "C", -5)
g.AddEdge("C", "D", 2)
dist, parent, ok, _ := g.bellmanFord("A")
fmt.Println("negatif ağırlıklı graf (A→B=1, A→C=4, B→C=-5, C→D=2):")
fmt.Println(" negatif döngü yok:", ok)
for _, n := range g.Nodes() {
if dist[n] >= inf {
fmt.Printf(" %s: ulaşılamaz\n", n)
continue
}
fmt.Printf(" %s: %d\n", n, dist[n])
}
fmt.Println(" A→C: doğrudan 4 yerine A→B→C = 1-5 =", dist["C"])
_ = parent
// Negatif döngü
fmt.Println()
cyclic := NewDigraph()
cyclic.AddEdge("A", "B", 1)
cyclic.AddEdge("B", "C", -3)
cyclic.AddEdge("C", "B", 1)
cyclic.AddEdge("C", "D", 2)
_, _, ok2, cycle := cyclic.bellmanFord("A")
fmt.Println("negatif döngülü graf (B→C=-3, C→B=1 → toplam -2):")
fmt.Println(" negatif döngü yok mu:", ok2)
if !ok2 {
fmt.Println(" bulunan döngü:", strings.Join(cycle, " → "))
fmt.Println(" bu döngüde dolaşmak mesafeyi sonsuza kadar azaltır")
}
// Arbitraj tespiti: para birimi kurları
fmt.Println()
fmt.Println("UYGULAMA: döviz arbitrajı")
fmt.Println("Kurların logaritmasının negatifini ağırlık yaparsan,")
fmt.Println("negatif döngü = kâr getiren çevrim zinciri demektir.")
}negatif ağırlıklı graf (A→B=1, A→C=4, B→C=-5, C→D=2): negatif döngü yok: true A: 0 B: 1 C: -4 D: -2 A→C: doğrudan 4 yerine A→B→C = 1-5 = -4 negatif döngülü graf (B→C=-3, C→B=1 → toplam -2): negatif döngü yok mu: false bulunan döngü: C → B → C bu döngüde dolaşmak mesafeyi sonsuza kadar azaltır UYGULAMA: döviz arbitrajı Kurların logaritmasının negatifini ağırlık yaparsan, negatif döngü = kâr getiren çevrim zinciri demektir.
Bellman-Ford'un Dijkstra'ya göre iki üstünlüğü var: Negatif ağırlıkları kaldırır ve negatif döngü tespiti yapar. İkincisi çok değerli bir yetenektir; negatif döngü varsa en kısa yol tanımsızdır (döngüde sonsuza kadar dolaşarak mesafeyi azaltabilirsin) ve bunu bilmek gerekir.
Negatif döngü tespitinin en ilginç uygulaması döviz arbitrajıdır: Kurları uygun biçimde dönüştürdüğünde, negatif bir döngü "bir para biriminden başlayıp zincir hâlinde çevirerek daha fazlasıyla dönmek" anlamına gelir.
Floyd-Warshall: tüm çiftler
Bazen tek bir kaynaktan değil, her düğümden her düğüme mesafeyi bilmek gerekir. Dijkstra'yı her düğümden çalıştırabilirsin, ama Floyd-Warshall bunu çok daha basit bir kodla yapar: üç iç içe döngü.
Fikir: "k düğümünü ara nokta olarak kullanmak yolu kısaltır mı?"
for k in tüm düğümler:
for i in tüm düğümler:
for j in tüm düğümler:
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
k döngüsünün EN DIŞTA olması kritiktir: Her k turunda,
"yalnızca ilk k düğümü ara nokta olarak kullanan" en kısa yollar kesinleşir.package main
import (
"fmt"
"maps"
"slices"
"strings"
)
const inf = 1 << 40 // toplamada taşmaması için makul bir değer
type Digraph struct {
edges map[string]map[string]int
nodes map[string]bool
}
func NewDigraph() *Digraph {
return &Digraph{edges: map[string]map[string]int{}, nodes: map[string]bool{}}
}
func (g *Digraph) AddEdge(from, to string, w int) {
g.nodes[from], g.nodes[to] = true, true
if g.edges[from] == nil {
g.edges[from] = map[string]int{}
}
g.edges[from][to] = w
}
func (g *Digraph) Nodes() []string { return slices.Sorted(maps.Keys(g.nodes)) }
// floydWarshall: tüm çiftler arası en kısa yol — O(V³)
func (g *Digraph) floydWarshall() (map[string]map[string]int, map[string]map[string]string, bool) {
nodes := g.Nodes()
dist := map[string]map[string]int{}
next := map[string]map[string]string{}
for _, i := range nodes {
dist[i] = map[string]int{}
next[i] = map[string]string{}
for _, j := range nodes {
switch {
case i == j:
dist[i][j] = 0
default:
dist[i][j] = inf
}
}
for to, w := range g.edges[i] {
dist[i][to] = w
next[i][to] = to
}
}
// Üç iç içe döngü: k EN DIŞTA olmalı
for _, k := range nodes {
for _, i := range nodes {
for _, j := range nodes {
if dist[i][k] >= inf || dist[k][j] >= inf {
continue
}
if through := dist[i][k] + dist[k][j]; through < dist[i][j] {
dist[i][j] = through
next[i][j] = next[i][k]
}
}
}
}
// Köşegen negatifse negatif döngü var
for _, i := range nodes {
if dist[i][i] < 0 {
return dist, next, false
}
}
return dist, next, true
}
func reconstruct(next map[string]map[string]string, from, to string) []string {
if next[from][to] == "" {
return nil
}
path := []string{from}
for cur := from; cur != to; cur = next[cur][to] {
path = append(path, next[cur][to])
}
return path
}
func main() {
g := NewDigraph()
links := []struct {
a, b string
ms int
}{
{"sunucu1", "sunucu2", 10},
{"sunucu2", "sunucu3", 15},
{"sunucu1", "sunucu3", 40},
{"sunucu3", "sunucu4", 5},
{"sunucu2", "sunucu4", 30},
{"sunucu4", "sunucu1", 20},
}
for _, l := range links {
g.AddEdge(l.a, l.b, l.ms)
}
dist, next, ok := g.floydWarshall()
nodes := g.Nodes()
fmt.Println("ağ gecikmeleri (ms):")
for _, l := range links {
fmt.Printf(" %s → %s: %d ms\n", l.a, l.b, l.ms)
}
fmt.Println()
fmt.Println("tüm çiftler arası en kısa gecikme:")
fmt.Printf("%-10s", "")
for _, j := range nodes {
fmt.Printf("%10s", j)
}
fmt.Println()
for _, i := range nodes {
fmt.Printf("%-10s", i)
for _, j := range nodes {
if dist[i][j] >= inf {
fmt.Printf("%10s", "∞")
continue
}
fmt.Printf("%10d", dist[i][j])
}
fmt.Println()
}
fmt.Println()
fmt.Println("negatif döngü yok:", ok)
fmt.Println()
fmt.Println("örnek yollar:")
for _, pair := range [][2]string{
{"sunucu1", "sunucu4"},
{"sunucu4", "sunucu3"},
{"sunucu1", "sunucu3"},
} {
p := reconstruct(next, pair[0], pair[1])
fmt.Printf(" %s → %s (%d ms): %s\n",
pair[0], pair[1], dist[pair[0]][pair[1]], strings.Join(p, " → "))
}
fmt.Println()
fmt.Println("sunucu1 → sunucu3: doğrudan 40 ms, sunucu2 üzerinden",
10+15, "ms → Floyd-Warshall kısa olanı buldu")
}ağ gecikmeleri (ms):
sunucu1 → sunucu2: 10 ms
sunucu2 → sunucu3: 15 ms
sunucu1 → sunucu3: 40 ms
sunucu3 → sunucu4: 5 ms
sunucu2 → sunucu4: 30 ms
sunucu4 → sunucu1: 20 ms
tüm çiftler arası en kısa gecikme:
sunucu1 sunucu2 sunucu3 sunucu4
sunucu1 0 10 25 30
sunucu2 40 0 15 20
sunucu3 25 35 0 5
sunucu4 20 30 45 0
negatif döngü yok: true
örnek yollar:
sunucu1 → sunucu4 (30 ms): sunucu1 → sunucu2 → sunucu3 → sunucu4
sunucu4 → sunucu3 (45 ms): sunucu4 → sunucu1 → sunucu2 → sunucu3
sunucu1 → sunucu3 (25 ms): sunucu1 → sunucu2 → sunucu3
sunucu1 → sunucu3: doğrudan 40 ms, sunucu2 üzerinden 25 ms → Floyd-Warshall kısa olanı bulduFloyd-Warshall'ın kodu şaşırtıcı derecede kısadır ama k döngüsünün en dışta olması zorunludur. Sırayı değiştirirsen algoritma yanlış çalışır: k en dışta olduğunda, o turun sonunda "yalnızca ilk k düğümü ara nokta olarak kullanan" en kısa yollar kesinleşir ve bu, tümevarımın temelini oluşturur.
Algoritma ayrıca bedavaya iki şey verir: Köşegendeki bir değer negatifse negatif döngü var demektir, ve dist[i][j] sonsuzsa i'den j'ye hiç yol yoktur (ulaşılabilirlik matrisi).
0-1 BFS
Özel bir durum: Kenar ağırlıkları yalnızca 0 veya 1 olabiliyorsa, Dijkstra'nın öncelik kuyruğuna ihtiyaç yoktur. Çift uçlu kuyruk yeterlidir ve karmaşıklık O(V + E)'ye iner.
package main
import (
"fmt"
"maps"
"slices"
"strings"
)
const inf = 1 << 60
type Edge struct {
To string
Weight int // yalnızca 0 veya 1
}
type Graph map[string][]Edge
func (g Graph) AddEdge(from, to string, w int) {
g[from] = append(g[from], Edge{to, w})
g[to] = append(g[to], Edge{from, w})
slices.SortFunc(g[from], func(a, b Edge) int { return strings.Compare(a.To, b.To) })
slices.SortFunc(g[to], func(a, b Edge) int { return strings.Compare(a.To, b.To) })
}
func (g Graph) Nodes() []string { return slices.Sorted(maps.Keys(g)) }
// zeroOneBFS: 0/1 ağırlıklı graflarda en kısa yol — O(V + E)
func (g Graph) zeroOneBFS(source string) map[string]int {
dist := map[string]int{}
for _, n := range g.Nodes() {
dist[n] = inf
}
dist[source] = 0
deque := []string{source}
for len(deque) > 0 {
cur := deque[0]
deque = deque[1:]
for _, e := range g[cur] {
newDist := dist[cur] + e.Weight
if newDist >= dist[e.To] {
continue
}
dist[e.To] = newDist
if e.Weight == 0 {
deque = append([]string{e.To}, deque...) // 0 ağırlık: ÖNE ekle
} else {
deque = append(deque, e.To) // 1 ağırlık: ARKAYA ekle
}
}
}
return dist
}
func main() {
// Labirent: bazı geçitler ücretsiz (0), bazıları kapı açmayı gerektiriyor (1)
g := Graph{}
passages := []struct {
a, b string
cost int
}{
{"giriş", "hol", 0},
{"hol", "salon", 0},
{"salon", "mutfak", 1}, // kapı
{"hol", "koridor", 1}, // kapı
{"koridor", "mutfak", 0},
{"mutfak", "bahçe", 1}, // kapı
{"salon", "balkon", 0},
}
for _, p := range passages {
g.AddEdge(p.a, p.b, p.cost)
}
fmt.Println("geçitler (0 = açık, 1 = kapı açmak gerekir):")
for _, p := range passages {
fmt.Printf(" %-10s ↔ %-10s maliyet %d\n", p.a, p.b, p.cost)
}
dist := g.zeroOneBFS("giriş")
fmt.Println()
fmt.Println("girişten en az kaç kapı açarak ulaşılır:")
for _, n := range g.Nodes() {
if dist[n] >= inf {
fmt.Printf(" %-10s ulaşılamaz\n", n)
continue
}
fmt.Printf(" %-10s %d kapı\n", n, dist[n])
}
fmt.Println()
fmt.Println("Neden çift uçlu kuyruk yeterli?")
fmt.Println("0 ağırlıklı kenar mesafeyi artırmadığı için düğüm ÖNE eklenir;")
fmt.Println("1 ağırlıklı kenar mesafeyi bir artırdığı için ARKAYA eklenir.")
fmt.Println("Böylece kuyruk her zaman sıralı kalır: öncelik kuyruğuna gerek yok.")
}geçitler (0 = açık, 1 = kapı açmak gerekir): giriş ↔ hol maliyet 0 hol ↔ salon maliyet 0 salon ↔ mutfak maliyet 1 hol ↔ koridor maliyet 1 koridor ↔ mutfak maliyet 0 mutfak ↔ bahçe maliyet 1 salon ↔ balkon maliyet 0 girişten en az kaç kapı açarak ulaşılır: bahçe 2 kapı balkon 0 kapı giriş 0 kapı hol 0 kapı koridor 1 kapı mutfak 1 kapı salon 0 kapı Neden çift uçlu kuyruk yeterli? 0 ağırlıklı kenar mesafeyi artırmadığı için düğüm ÖNE eklenir; 1 ağırlıklı kenar mesafeyi bir artırdığı için ARKAYA eklenir. Böylece kuyruk her zaman sıralı kalır: öncelik kuyruğuna gerek yok.
0-1 BFS, BFS ile Dijkstra arasındaki köprüdür. BFS'te tüm ağırlıklar 1'dir ve kuyruk doğal olarak sıralı kalır. Ağırlıklar serbest olduğunda öncelik kuyruğu gerekir. Ağırlıklar yalnızca 0 ve 1 ise, çift uçlu kuyruk sıralılığı logaritmik maliyet ödemeden korur.
Hangi algoritma ne zaman?
Üç algoritma arasındaki seçim, grafın özelliklerine ve neye ihtiyaç duyduğuna bağlıdır.
Karar rehberi:
Tüm kenarlar eşit ağırlıklıysa BFS kullan. En hızlısı ve en basiti; öncelik kuyruğu kurmak gereksiz maliyet olur.
Ağırlıklar 0 ve 1 ise 0-1 BFS kullan. Dijkstra'nın logaritmik çarpanından kurtulursun.
Ağırlıklar negatif değilse Dijkstra kullan. Pratikte en yaygın durumdur: mesafeler, süreler, maliyetler negatif olmaz.
Negatif ağırlık varsa Bellman-Ford kullan. Ayrıca negatif döngü tespiti gerekiyorsa tek seçenektir.
Tüm çiftler gerekiyorsa graf boyutuna bak. Düğüm sayısı küçükse (birkaç yüz) Floyd-Warshall'ın sadeliği kazanır. Graf büyük ve seyrekse, her düğümden Dijkstra çalıştırmak O(V·(V+E)·log V) verir ve V³'ten daha iyi olabilir.
Bir de sık karşılaşılan bir tuzak: Dijkstra'yı negatif ağırlıklı grafta çalıştırmak hata vermez. Sessizce yanlış cevap döndürür. Bu yüzden veri kaynağını bilmiyorsan, ağırlıkların negatif olup olmadığını kontrol etmek ya da doğrudan Bellman-Ford kullanmak daha güvenlidir.
Gerçek sistemlerde en kısa yol
Navigasyon uygulamalarının milyonlarca yol kesişimi içeren haritalarda saniyenin altında rota bulmasını, bu derste gördüğün algoritmalar tek başına açıklamaz. Gerçek sistemler aynı temeli kullanır ama üzerine birkaç katman ekler; bunları bilmek teoriyle pratik arasındaki mesafeyi görmeni sağlar.
Çift yönlü arama. Hem başlangıçtan hem hedeften aynı anda arama başlatmak, keşfedilen alanı belirgin biçimde küçültür. Tek yönlü arama, hedefe olan mesafe kadar yarıçapı olan bir daireyi tarar; iki yönlü arama, yarıçapı yarı olan iki daireyi tarar ve bunların toplam alanı çok daha küçüktür.
Sezgisel yönlendirme. Dijkstra hedefi bilmez, her yöne eşit ilgi gösterir. Hedefin nerede olduğuna dair bir tahmin — örneğin kuş uçuşu mesafe — aramayı doğru yöne yönlendirebilir. Bu fikri kullanan algoritma, gerçek maliyet ile tahmini kalan maliyeti toplayarak öncelik belirler; tahmin gerçek mesafeyi asla aşmıyorsa sonuç hâlâ optimaldir.
Ön işleme ve hiyerarşi. Yol ağları nadiren değişir, bu yüzden önceden hesaplama yapmak mümkündür. Otoyollar üzerinden geçen uzun mesafe rotaları önceden hesaplanıp saklanır; sorgu anında yalnızca başlangıç ve hedefin yakınındaki yerel yollar taranır. Bu yaklaşım, kıtalar arası rota sorgularını milisaniyelere indirir.
Zaman bağımlı ağırlıklar. Trafik yoğunluğu saate göre değişir; bir kenarın maliyeti sabit değildir. Bu durumda ağırlık bir fonksiyon hâline gelir ve algoritma "şu saatte bu kenara girersem ne kadar sürer" sorusunu sorar. Temel gevşetme mantığı değişmez, yalnızca ağırlık hesabı zenginleşir.
Çoklu ölçüt. Gerçek kullanıcılar yalnızca süreyi değil, yakıt maliyetini, yol ücretlerini ve konforu da önemser. Bu ölçütleri tek bir ağırlıkta birleştirmek pratikte en yaygın çözümdür; her ölçüte bir katsayı verilir ve kullanıcı tercihlerine göre ayarlanır.
Ortak ders şudur: Temel algoritmalar değişmez, ama gerçek sistemlerde onların etrafına problem-özel katmanlar örülür. Temeli sağlam anlamak, bu katmanları tasarlayabilmenin ön koşuludur.
Sık yapılan hatalar
- Negatif ağırlıklı grafta Dijkstra kullanmak. Hata vermez, yanlış sonuç verir.
- Öncelik kuyruğunda bayat kayıtları atlamamak. Kesinleşmiş düğümü tekrar işlemek yanlış sonuçlara yol açabilir.
- Floyd-Warshall'da
kdöngüsünü en dışta yazmamak. Sıra değişirse algoritma yanlış çalışır. - Sonsuz değeri çok büyük seçmek. İki sonsuzu toplamak taşmaya yol açar; toplama yapmadan önce kontrol et.
- Bellman-Ford'da V. turu atlamak. Negatif döngü tespiti tam olarak o turda yapılır.
- Yolu geri kurmak için ebeveyn kaydı tutmamak. Mesafe tek başına genelde yetmez.
- Ulaşılamaz düğümleri ele almamak. Mesafesi sonsuz kalan düğümler için yol yoktur.
Alıştırmalar
Uçuş bağlantıları ve fiyatları verilmiş. En ucuz yolu ve en az aktarmalı yolu ayrı ayrı bul. İkisinin farklı olabileceğini göster.
İpucu
En ucuz yol için Dijkstra (ağırlık = fiyat), en az aktarma için BFS (ağırlık = 1) kullan. Aynı graf, iki farklı ölçüt.
Çözümü göster
package main
import (
"container/heap"
"fmt"
"maps"
"slices"
"strings"
)
const inf = 1 << 60
type Flight struct {
To string
Price int
}
type Network map[string][]Flight
func (n Network) Add(from, to string, price int) {
n[from] = append(n[from], Flight{to, price})
n[to] = append(n[to], Flight{from, price})
slices.SortFunc(n[from], func(a, b Flight) int { return strings.Compare(a.To, b.To) })
slices.SortFunc(n[to], func(a, b Flight) int { return strings.Compare(a.To, b.To) })
}
func (n Network) Cities() []string { return slices.Sorted(maps.Keys(n)) }
type item struct {
city string
cost int
}
type pq []item
func (q pq) Len() int { return len(q) }
func (q pq) Less(i, j int) bool {
if q[i].cost != q[j].cost {
return q[i].cost < q[j].cost
}
return q[i].city < q[j].city
}
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 }
// cheapest: Dijkstra ile en ucuz yol
func (n Network) cheapest(from, to string) (int, []string) {
dist := map[string]int{}
parent := map[string]string{}
for _, c := range n.Cities() {
dist[c] = inf
}
dist[from] = 0
q := &pq{{from, 0}}
done := map[string]bool{}
for q.Len() > 0 {
cur := heap.Pop(q).(item)
if done[cur.city] {
continue
}
done[cur.city] = true
for _, f := range n[cur.city] {
if nd := dist[cur.city] + f.Price; nd < dist[f.To] {
dist[f.To] = nd
parent[f.To] = cur.city
heap.Push(q, item{f.To, nd})
}
}
}
return dist[to], buildPath(parent, from, to)
}
// fewestStops: BFS ile en az aktarma
func (n Network) fewestStops(from, to string) (int, []string) {
parent := map[string]string{}
visited := map[string]bool{from: true}
queue := []string{from}
stops := map[string]int{from: 0}
for len(queue) > 0 {
cur := queue[0]
queue = queue[1:]
for _, f := range n[cur] {
if visited[f.To] {
continue
}
visited[f.To] = true
parent[f.To] = cur
stops[f.To] = stops[cur] + 1
queue = append(queue, f.To)
}
}
if !visited[to] {
return -1, nil
}
return stops[to], buildPath(parent, from, to)
}
func buildPath(parent map[string]string, from, to string) []string {
if from == to {
return []string{from}
}
if _, ok := parent[to]; !ok {
return nil
}
var path []string
for c := to; c != ""; c = parent[c] {
path = append(path, c)
if c == from {
break
}
}
slices.Reverse(path)
return path
}
func pathCost(n Network, path []string) int {
total := 0
for i := 0; i+1 < len(path); i++ {
for _, f := range n[path[i]] {
if f.To == path[i+1] {
total += f.Price
break
}
}
}
return total
}
func main() {
net := Network{}
net.Add("İstanbul", "Ankara", 600)
net.Add("Ankara", "İzmir", 500)
net.Add("İstanbul", "İzmir", 1400)
net.Add("İzmir", "Antalya", 400)
net.Add("İstanbul", "Antalya", 1600)
net.Add("Ankara", "Antalya", 900)
routes := [][2]string{
{"İstanbul", "İzmir"},
{"İstanbul", "Antalya"},
{"Ankara", "Antalya"},
}
fmt.Println("uçuş fiyatları:")
for _, c := range net.Cities() {
for _, f := range net[c] {
if c < f.To {
fmt.Printf(" %-10s ↔ %-10s %5d TL\n", c, f.To, f.Price)
}
}
}
fmt.Println()
for _, r := range routes {
cost, cheapPath := net.cheapest(r[0], r[1])
stops, fewPath := net.fewestStops(r[0], r[1])
fewCost := pathCost(net, fewPath)
fmt.Printf("%s → %s\n", r[0], r[1])
fmt.Printf(" en ucuz : %5d TL, %d aktarma %s\n",
cost, len(cheapPath)-2, strings.Join(cheapPath, " → "))
fmt.Printf(" en az akt: %5d TL, %d aktarma %s\n",
fewCost, stops-1, strings.Join(fewPath, " → "))
if cost != fewCost {
fmt.Printf(" → farklı! en ucuz olmak için %d TL tasarruf, %d aktarma fazla\n",
fewCost-cost, (len(cheapPath)-2)-(stops-1))
}
fmt.Println()
}
}uçuş fiyatları: Ankara ↔ Antalya 900 TL Ankara ↔ İstanbul 600 TL Ankara ↔ İzmir 500 TL Antalya ↔ İstanbul 1600 TL Antalya ↔ İzmir 400 TL İstanbul ↔ İzmir 1400 TL İstanbul → İzmir en ucuz : 1100 TL, 1 aktarma İstanbul → Ankara → İzmir en az akt: 1400 TL, 0 aktarma İstanbul → İzmir → farklı! en ucuz olmak için 300 TL tasarruf, 1 aktarma fazla İstanbul → Antalya en ucuz : 1500 TL, 1 aktarma İstanbul → Ankara → Antalya en az akt: 1600 TL, 0 aktarma İstanbul → Antalya → farklı! en ucuz olmak için 100 TL tasarruf, 1 aktarma fazla Ankara → Antalya en ucuz : 900 TL, 0 aktarma Ankara → Antalya en az akt: 900 TL, 0 aktarma Ankara → Antalya
Aynı graf üzerinde iki farklı ölçüt, iki farklı cevap verir. Bu, en kısa yol problemlerinde "en kısa"nın ne anlama geldiğini baştan tanımlamanın önemini gösteriyor: mesafe, süre, ücret, aktarma sayısı, konfor — her biri farklı bir ağırlık fonksiyonu ve farklı bir sonuçtur.
Gerçek uçuş arama motorları bu ölçütleri birleştirir: Ağırlığı "fiyat + aktarma başına ceza" gibi bir formülle hesaplarlar. Ölçütü değiştirmek algoritmayı değiştirmez, yalnızca kenar ağırlıklarını.
Ağ bağlantılarının güvenilirlik oranları (0 ile 1 arasında) verilmiş. Bir yolun güvenilirliği, üzerindeki bağlantıların çarpımıdır. En güvenilir yolu bul.
İpucu
Çarpımı maksimize etmek, logaritmaların toplamını maksimize etmekle aynıdır. Negatif logaritma alırsan problem en kısa yol problemine dönüşür ve Dijkstra uygulanabilir.
Çözümü göster
package main
import (
"container/heap"
"fmt"
"maps"
"math"
"slices"
"strings"
)
type Link struct {
To string
Reliability float64 // 0..1
}
type Network map[string][]Link
func (n Network) Add(a, b string, r float64) {
n[a] = append(n[a], Link{b, r})
n[b] = append(n[b], Link{a, r})
slices.SortFunc(n[a], func(x, y Link) int { return strings.Compare(x.To, y.To) })
slices.SortFunc(n[b], func(x, y Link) int { return strings.Compare(x.To, y.To) })
}
func (n Network) Nodes() []string { return slices.Sorted(maps.Keys(n)) }
type item struct {
node string
cost float64
}
type pq []item
func (q pq) Len() int { return len(q) }
func (q pq) Less(i, j int) bool {
if q[i].cost != q[j].cost {
return q[i].cost < q[j].cost
}
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 }
// mostReliable: -log(güvenilirlik) ağırlığıyla Dijkstra
func (n Network) mostReliable(source string) (map[string]float64, map[string]string) {
// cost = -ln(reliability); toplamı minimize etmek çarpımı maksimize eder
cost := map[string]float64{}
parent := map[string]string{}
for _, node := range n.Nodes() {
cost[node] = math.Inf(1)
}
cost[source] = 0
q := &pq{{source, 0}}
done := map[string]bool{}
for q.Len() > 0 {
cur := heap.Pop(q).(item)
if done[cur.node] {
continue
}
done[cur.node] = true
for _, l := range n[cur.node] {
if l.Reliability <= 0 {
continue
}
w := -math.Log(l.Reliability)
if nc := cost[cur.node] + w; nc < cost[l.To] {
cost[l.To] = nc
parent[l.To] = cur.node
heap.Push(q, item{l.To, nc})
}
}
}
// Maliyetleri güvenilirliğe geri çevir
reliability := map[string]float64{}
for node, c := range cost {
if math.IsInf(c, 1) {
reliability[node] = 0
continue
}
reliability[node] = math.Exp(-c)
}
return reliability, parent
}
func buildPath(parent map[string]string, from, to string) []string {
if from == to {
return []string{from}
}
if _, ok := parent[to]; !ok {
return nil
}
var path []string
for c := to; c != ""; c = parent[c] {
path = append(path, c)
if c == from {
break
}
}
slices.Reverse(path)
return path
}
func pathReliability(n Network, path []string) float64 {
if len(path) < 2 {
return 1
}
product := 1.0
for i := 0; i+1 < len(path); i++ {
for _, l := range n[path[i]] {
if l.To == path[i+1] {
product *= l.Reliability
break
}
}
}
return product
}
func main() {
net := Network{}
net.Add("A", "B", 0.9)
net.Add("B", "D", 0.9)
net.Add("A", "C", 0.95)
net.Add("C", "D", 0.8)
net.Add("A", "D", 0.7)
net.Add("D", "E", 0.99)
fmt.Println("bağlantı güvenilirlikleri:")
for _, node := range net.Nodes() {
for _, l := range net[node] {
if node < l.To {
fmt.Printf(" %s ↔ %s: %.2f\n", node, l.To, l.Reliability)
}
}
}
rel, parent := net.mostReliable("A")
fmt.Println()
fmt.Println("A'dan en güvenilir yollar:")
for _, node := range net.Nodes() {
p := buildPath(parent, "A", node)
if p == nil && node != "A" {
fmt.Printf(" %s: ulaşılamaz\n", node)
continue
}
fmt.Printf(" %s: %.4f %s\n", node, rel[node], strings.Join(p, " → "))
}
fmt.Println()
fmt.Println("A → D için seçenekler:")
options := [][]string{
{"A", "D"},
{"A", "B", "D"},
{"A", "C", "D"},
}
for _, o := range options {
fmt.Printf(" %-16s güvenilirlik: %.4f\n",
strings.Join(o, " → "), pathReliability(net, o))
}
fmt.Printf("Dijkstra'nın bulduğu: %.4f\n", rel["D"])
}bağlantı güvenilirlikleri: A ↔ B: 0.90 A ↔ C: 0.95 A ↔ D: 0.70 B ↔ D: 0.90 C ↔ D: 0.80 D ↔ E: 0.99 A'dan en güvenilir yollar: A: 1.0000 A B: 0.9000 A → B C: 0.9500 A → C D: 0.8100 A → B → D E: 0.8019 A → B → D → E A → D için seçenekler: A → D güvenilirlik: 0.7000 A → B → D güvenilirlik: 0.8100 A → C → D güvenilirlik: 0.7600 Dijkstra'nın bulduğu: 0.8100
Bu problem, problem dönüştürme tekniğinin güzel bir örneğidir. Çarpımı maksimize etmek, ilk bakışta en kısa yol problemine benzemez. Ama logaritmanın temel özelliği — çarpımı toplama çevirmesi — problemi tanıdığı bir biçime sokar. Negatif logaritma almak da maksimizasyonu minimizasyona çevirir.
Güvenilirlikler 1'den küçük olduğu için logaritmaları negatiftir; negatifini aldığımızda pozitif ağırlıklar elde ederiz ve Dijkstra'nın ön koşulu sağlanır. Güvenilirlik 1'den büyük olabilseydi ağırlıklar negatif olurdu ve Bellman-Ford gerekirdi.
Aynı dönüşüm pek çok yerde işe yarar: olasılık çarpımları, büyüme oranları, bileşik faiz hesapları. "Çarpım" gördüğün her yerde logaritmayı düşün.
İki düğüm arasındaki en kısa yolu değil, k'ıncı en kısa yolu bul (tekrarlı düğümlere izin verilerek). Dijkstra'yı değiştirerek çöz: Her düğüme k kez ulaşmaya izin ver.
İpucu
Normal Dijkstra her düğümü bir kez kesinleştirir. K'ıncı yol için her düğüme kaç kez ulaşıldığını say ve k'ya kadar izin ver. Hedefe k'ıncı ulaşma, k'ıncı en kısa yoldur.
Çözümü göster
package main
import (
"container/heap"
"fmt"
"maps"
"slices"
"strings"
)
type Edge struct {
To string
Weight int
}
type Graph map[string][]Edge
func (g Graph) AddEdge(from, to string, w int) {
g[from] = append(g[from], Edge{to, w})
slices.SortFunc(g[from], func(a, b Edge) int { return strings.Compare(a.To, b.To) })
if _, ok := g[to]; !ok {
g[to] = nil
}
}
func (g Graph) Nodes() []string { return slices.Sorted(maps.Keys(g)) }
type state struct {
node string
dist int
path []string
}
type pq []state
func (q pq) Len() int { return len(q) }
func (q pq) Less(i, j int) bool {
if q[i].dist != q[j].dist {
return q[i].dist < q[j].dist
}
return strings.Join(q[i].path, "") < strings.Join(q[j].path, "")
}
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.(state)) }
func (q *pq) Pop() any { o := *q; s := o[len(o)-1]; *q = o[:len(o)-1]; return s }
// kShortestPaths: ilk k en kısa yolu bulur
func (g Graph) kShortestPaths(source, target string, k int) []state {
count := map[string]int{}
var results []state
q := &pq{{source, 0, []string{source}}}
heap.Init(q)
for q.Len() > 0 && count[target] < k {
cur := heap.Pop(q).(state)
count[cur.node]++
if cur.node == target {
results = append(results, cur)
continue
}
// Her düğüme en fazla k kez uğra
if count[cur.node] > k {
continue
}
for _, e := range g[cur.node] {
newPath := slices.Clone(cur.path)
newPath = append(newPath, e.To)
heap.Push(q, state{e.To, cur.dist + e.Weight, newPath})
}
}
return results
}
func main() {
g := Graph{}
edges := []struct {
a, b string
w int
}{
{"A", "B", 1},
{"A", "C", 5},
{"B", "C", 2},
{"B", "D", 7},
{"C", "D", 1},
{"A", "D", 10},
}
for _, e := range edges {
g.AddEdge(e.a, e.b, e.w)
}
fmt.Println("kenarlar (yönlü):")
for _, e := range edges {
fmt.Printf(" %s → %s: %d\n", e.a, e.b, e.w)
}
fmt.Println()
paths := g.kShortestPaths("A", "D", 4)
fmt.Println("A → D için ilk 4 en kısa yol:")
for i, p := range paths {
fmt.Printf(" %d. uzunluk %2d: %s\n", i+1, p.dist, strings.Join(p.path, " → "))
}
fmt.Println()
// Tek yol olan durum
simple := Graph{}
simple.AddEdge("X", "Y", 3)
single := simple.kShortestPaths("X", "Y", 3)
fmt.Println("X → Y (tek yol var, 3 istendi):")
for i, p := range single {
fmt.Printf(" %d. uzunluk %d: %s\n", i+1, p.dist, strings.Join(p.path, " → "))
}
fmt.Println(" bulunan yol sayısı:", len(single))
fmt.Println()
// Ulaşılamaz hedef
disconnected := Graph{}
disconnected.AddEdge("P", "Q", 1)
disconnected.AddEdge("R", "S", 1)
none := disconnected.kShortestPaths("P", "S", 2)
fmt.Println("P → S (bağlantı yok):", len(none), "yol bulundu")
fmt.Println()
fmt.Println("Normal Dijkstra her düğümü BİR kez kesinleştirir.")
fmt.Println("K'ıncı yol için her düğüme k kez uğramaya izin verilir.")
fmt.Println("Hedefe i'inci ulaşma, i'inci en kısa yoldur.")
}kenarlar (yönlü): A → B: 1 A → C: 5 B → C: 2 B → D: 7 C → D: 1 A → D: 10 A → D için ilk 4 en kısa yol: 1. uzunluk 4: A → B → C → D 2. uzunluk 6: A → C → D 3. uzunluk 8: A → B → D 4. uzunluk 10: A → D X → Y (tek yol var, 3 istendi): 1. uzunluk 3: X → Y bulunan yol sayısı: 1 P → S (bağlantı yok): 0 yol bulundu Normal Dijkstra her düğümü BİR kez kesinleştirir. K'ıncı yol için her düğüme k kez uğramaya izin verilir. Hedefe i'inci ulaşma, i'inci en kısa yoldur.
Bu algoritma, Dijkstra'nın küçük bir genellemesidir ve durum uzayını genişletme tekniğine örnektir: Durum artık yalnızca "hangi düğümdeyim" değil, "hangi düğümde ve buraya kaçıncı kez geldim" bilgisidir.
Aynı teknik pek çok problemde işe yarar. "En fazla k yakıt ikmaliyle en kısa yol" probleminde durum (düğüm, kalan yakıt) olur. "En fazla k aktarmayla en ucuz uçuş" probleminde (şehir, aktarma sayısı). Grafı bu şekilde genişletmek, ek kısıtları algoritmayı değiştirmeden ele almanın standart yoludur.
Dikkat edilecek nokta bellek kullanımıdır: Bu uygulamada her durum kendi yolunu saklıyor ve bu O(k × V × yol uzunluğu) belleğe yol açıyor. Yalnızca mesafeler gerekiyorsa yolları saklamamak, belleği belirgin biçimde düşürür.
Kısa sınav
Gevşetme (relaxation) işlemi nedir?
Dijkstra algoritması negatif ağırlıklı graflarda neden çalışmaz?
Bellman-Ford kaç tur kenar gevşetmesi yapar ve neden?
Floyd-Warshall'da k döngüsünün en dışta olması neden zorunludur?
0-1 BFS neden öncelik kuyruğuna ihtiyaç duymaz?
Tüm çiftler arası en kısa yol için hangisi daha uygundur?
Özet
- Tüm en kısa yol algoritmalarının çekirdeği gevşetmedir: daha kısa bir yol bulunduğunda mesafe tahmini güncellenir.
- Dijkstra açgözlüdür: En yakın kesinleşmemiş düğümü seçer ve komşularını gevşetir; negatif ağırlıklarla çalışmaz.
- Öncelik kuyruğunda bayat kayıtları atlamak gerekir;
container/heapile uygulama standarttır. - Bellman-Ford tüm kenarları V−1 kez gevşetir; negatif ağırlıkları kaldırır ve negatif döngü tespit eder.
- Floyd-Warshall üç iç içe döngüyle tüm çiftler arası mesafeleri hesaplar;
kdöngüsü en dışta olmalıdır. - 0-1 BFS, ağırlıklar yalnızca 0 ve 1 olduğunda çift uçlu kuyrukla O(V+E) çözüm verir.
- Yolu geri kurmak için her gevşetmede ebeveyn kaydı tutulur.
- Ağırlıklar eşitse BFS, 0/1 ise 0-1 BFS, negatif değilse Dijkstra, negatifse Bellman-Ford, tüm çiftler için Floyd-Warshall.
- Dijkstra'yı negatif ağırlıkla çalıştırmak hata vermez, sessizce yanlış sonuç verir.