go
Algoritmalar dersleri
Algoritmalar/Graf Algoritmaları

En Kısa Yol Algoritmaları

Dijkstra, Bellman-Ford ve Floyd-Warshall.

Ders 14 / 1840 dkİleri
Bu derste öğreneceklerin
  • Ağırlıklı graflar ve gevşetme (relaxation)
  • Dijkstra algoritması ve container/heap
  • Yolu geri oluşturma
  • Negatif ağırlıklar ve Bellman-Ford
  • Negatif döngü tespiti
  • Floyd-Warshall: tüm çiftler arası en kısa yol
  • 0-1 BFS
  • Hangi algoritma ne zaman?

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.

main.go
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.")
}
Çıktı
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 = 8

Neden 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:

main.go
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")
}
Çıktı
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
ZamanO((V + E) log V)AlanO(V)

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.

main.go
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.")
}
Çıktı
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.
ZamanO(V × E)AlanO(V)

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.
main.go
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")
}
Çıktı
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ı buldu
ZamanO(V³)AlanO(V²)

Floyd-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.

main.go
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.")
}
Çıktı
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.
ZamanO(V + E)AlanO(V)

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.

ÖlçütBFS0-1 BFSDijkstraBellman-FordFloyd-Warshall
AğırlıklarHepsi eşit0 veya 1Negatif olmayanNegatif olabilirNegatif olabilir
KarmaşıklıkO(V+E)O(V+E)O((V+E) log V)O(V·E)O(V³)
Kaynak sayısıTekTekTekTekTüm çiftler
Negatif döngü tespitiHayırEvetEvet
Uygulama zorluğuKolayKolayOrtaKolayÇok kolay

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 k dö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

Alıştırma·En az aktarmalı uçuş
Kolay

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
main.go
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()
	}
}
Çıktı
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
ZamanO((V+E) log V) Dijkstra, O(V+E) BFSAlanO(V)

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ı.

Alıştırma·En güvenilir yol
Orta

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
main.go
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"])
}
Çıktı
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
ZamanO((V+E) log V)AlanO(V)

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.

Alıştırma·K'ıncı en kısa yol
Zor

İ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
main.go
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.")
}
Çıktı
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.
ZamanO(k × E × log(k × V))AlanO(k × V)

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

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/heap ile 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; k dö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.
Bu dersi bitirdin mi?
İlerlemen bu tarayıcıda saklanır.