Ana içeriğe atla
OR Araçları

Rehber

En Kısa Yol: Dijkstra ve Bellman-Ford

Yönlü ağırlıklı ağlarda en kısa yolu bulmak: Dijkstra (≥ 0 ağırlık, binary heap) ve Bellman-Ford (negatif ağırlık + çevrim tespiti). Sayısal örnekler ve karmaşıklık karşılaştırması.

· 11 dk okuma

İlgili araç

En Kısa Yol Çözücü (Dijkstra · Bellman-Ford)

Yönlü ağırlıklı ağda kaynaktan (s) tüm düğümlere ya da seçilen hedefe (t) minimum toplam ağırlıklı yolu Dijkstra (binary heap, ≥ 0 ağırlık) ya da Bellman-Ford (negatif ağırlık + negatif çevrim tespiti) ile tarayıcıda anlık bul. Algoritma seçimi otomatik: negatif kenar varsa Bellman-Ford. Her düğüm için uzaklık + predecessor; hedef verilirse adım adım yol rekonstrüksiyonu.

Aracı aç →

En kısa yol problemi, graf algoritmalarının temel taşıdır. GPS navigasyon ekranınızda gördüğünüz rota, internet trafiğinizin geçtiği yönlendirici sırası, hatta bir oyundaki düşman karakterinin size doğru ilerlemesi — hepsi arkasında bu klasik problemi taşır. Bu rehberde iki kanonik algoritmayı inceliyoruz: Dijkstra (1959) ve Bellman-Ford (1956).

Problemin tanımı

Yönlü ağırlıklı bir graf G=(V,E,w)G = (V, E, w):

  • V düğüm kümesi (örn. şehirler, yönlendiriciler, oyun karoları)
  • E yönlü kenar kümesi
  • w : E → ℝ ağırlık fonksiyonu (mesafe, süre, maliyet — negatif olabilir)

Bir yol p=(v0,v1,,vk)p = (v_0, v_1, \ldots, v_k) ardışık kenarlardan oluşur; yolun ağırlığı w(p)=i=0k1w(vi,vi+1)w(p) = \sum_{i=0}^{k-1} w(v_i, v_{i+1}).

Tek-kaynak en kısa yol (SSSP): verilen ss kaynağından her vVv \in V düğümüne olan minimum yol ağırlığını δ(s,v)\delta(s, v) bul.

Tek-kaynak tek-hedef: SSSP’nin özel hâli — yalnızca sts \to t.

Gevşetme — algoritmaların kalbi

Her iki algoritma da aynı temel adımı tekrarlar: kenar gevşetme (relaxation).

Relax(u, v, w):
    if dist[v] > dist[u] + w(u, v):
        dist[v] ← dist[u] + w(u, v)
        pred[v] ← u

Gevşetme yakınsadığında — yani hiçbir kenar daha fazla iyileşemediğinde — üçgen eşitsizliği her kenarda sağlanır:

δ(s,v)δ(s,u)+w(u,v)\delta(s, v) \le \delta(s, u) + w(u, v)

Bu eşitsizlik en kısa yolun karakterizasyonudur. İki algoritma kenarları farklı bir sırayla gevşetir; farkları işte burada.

Dijkstra algoritması (≥ 0 ağırlık)

Açgözlü prensip: her adımda bilinen en yakın ulaşılmamış düğümü seç.

function Dijkstra(G, s):
    dist[v] ← +∞ for v ∈ V; dist[s] ← 0
    pq ← {(0, s)}                      # min-heap
    while pq is not empty:
        (d, u) ← pq.pop()
        if d > dist[u]: continue       # stale (lazy delete)
        for each (u, v) ∈ E:
            if Relax(u, v, w):
                pq.push((dist[v], v))
    return dist, pred

Karmaşıklık binary heap ile O((V + E) log V). Pratikte 100K düğümlü ağlar saniyenin altında çözülür.

Neden ≥ 0 zorunlu?

Açgözlü mantık şu fikre dayanır: “u’yu kapattığım anda, δ(s,u)\delta(s, u) kesinleşmiştir; başka hiçbir yol u’yu daha kısaya getiremez.” Bu varsayım yalnızca ağırlıklar non-negatif iken doğrudur. Negatif bir kenar varsa, u kapandıktan sonra başka bir komşunun negatif kenarla u’ya daha kısa bir alternatif yaratması mümkün olur — ama Dijkstra kapanan düğüme bir daha bakmaz.

Somut karşı örnek (FAQ’da):

KenarAğırlık
A → C1
A → B4
B → C−10
  • Doğru cevap: ABC=4+(10)=6A \to B \to C = 4 + (-10) = -6
  • Dijkstra: önce C (dist=1) settled; B sonra gelir ama B → C bakılmaz; cevap 6-6 yerine 11

Sayısal örnek — CLRS 24.3

Cormen ve arkadaşlarının ders kitabı örneği:

KenarAğırlık
A → B10
A → D5
B → C1
B → D2
D → B3
D → C9
D → E2
C → E4
E → A7
E → C6

Kaynak A. Dijkstra adım adım:

Popdist[A]dist[B]dist[C]dist[D]dist[E]
— (init)0
A (0)0105
D (5)081457
E (7)081357
B (8)08957
C (9)08957

Nihai uzaklıklar: δ=(0,8,9,5,7)\delta = (0, 8, 9, 5, 7). Predecessor zinciriyle örn. ACA \to C yolu: ADBCA \to D \to B \to C (toplam 9).

Bellman-Ford algoritması (negatif ağırlık dahil)

Düz mantık: tüm kenarları V1V - 1 kez sırayla gevşet.

function BellmanFord(G, s):
    dist[v] ← +∞ for v ∈ V; dist[s] ← 0
    for i = 1 to |V| − 1:
        changed ← false
        for each (u, v, w) ∈ E:
            if Relax(u, v, w): changed ← true
        if not changed: break          # erken çıkış
    # Negatif çevrim kontrolü:
    for each (u, v, w) ∈ E:
        if dist[u] + w < dist[v]:
            return "negatif çevrim"
    return dist, pred

Karmaşıklık O(V · E). Doğruluk argümanı: kk‘inci pass’ten sonra en fazla kk kenar içeren tüm en kısa yollar bulunmuş olur (induction). En kısa yollar negatif çevrim yokken en fazla V1V - 1 kenar uzunluğunda, yani V1V - 1 pass yeter. VV‘inci pass’te hâlâ değişim olursa, demek ki VV kenar uzunluğunda bir yol daha kısa — bu ancak negatif çevrim ile mümkündür.

Sayısal örnek — CLRS 24.1

Kaynak s; ağırlıklar:

KenarAğırlıkKenarAğırlık
s → t6x → t−2
s → y7y → x−3
t → x5y → z9
t → y8z → s2
t → z−4z → x7

Bellman-Ford’un nihai uzaklıkları: δ(s,t)=2\delta(s,t) = 2, δ(s,x)=4\delta(s,x) = 4, δ(s,y)=7\delta(s,y) = 7, δ(s,z)=2\delta(s,z) = -2. Negatif çevrim yok — algoritma geçerli cevap verir.

Negatif çevrim örneği

s → a (1)
a → b (2)
b → a (−4)
b → t (1)

abaa \to b \to a çevrim toplamı 2-2 < 0 — sonsuza kadar uzaklığı düşürebilir. Bellman-Ford bunu V=4V = 4‘üncü pass’te tespit eder ve “erişilebilir negatif çevrim” raporlar.

Algoritmaların karşılaştırması

ÖzellikDijkstraBellman-Ford
Negatif ağırlıkHayırEvet
Negatif çevrim tespiti— (ön koşul ihlali)Evet
KarmaşıklıkO((V + E) log V)O(V · E)
Veri yapısıMin-heapDüz kenar listesi
Pratik 10K düğüm< 50 ms≈ 1-5 s
Doğruluk koşuluTüm w ≥ 0Negatif çevrim yok

Karar ağacı: ağırlıklar non-negatif mi? → Dijkstra. Negatif olabilir mi? → Bellman-Ford. Tüm-çiftler (APSP) mi? → Floyd-Warshall O(V³) ya da Johnson O(V · E + V² log V).

Uzantılar ve genellemeler

  • A* (1968) — heuristic ile Dijkstra. Hedef yönüne doğru aramayı yönlendirerek pratik 10-100x hızlanma; yol-bulma (gridler, oyun yapay zekâsı, OSRM gibi rota motorları) standardı. Heuristic admissible olduğu sürece optimum.
  • SPFA (Shortest Path Faster Algorithm) — Bellman-Ford’un kuyruk tabanlı pratik varyantı. Worst case yine O(V · E) ama tipik graflarda çok daha hızlı. Min-Cost Flow algoritmamızın da iç motoru.
  • Floyd-Warshall — tüm düğüm çiftleri arası en kısa yollar; O(V³) dinamik programlama, ufak/orta yoğun ağlarda zarif çözüm.
  • Bidirectional search — kaynak ve hedeften aynı anda iki Dijkstra başlat; ortada buluşunca dur. Yaklaşık √n kat hızlanma.
  • Contraction Hierarchies — yol planlama motorlarının (OSRM, GraphHopper) ön-işleme tekniği; sorgu O(log V) seviyesine iner.

Klasik uygulamalar

AlanEn kısa yol nasıl çıkar?
GPS navigasyonŞehir düğüm, yol kenar, ağırlık = süre/mesafe; A* veya CH
İnternet yönlendirmeOSPF/IS-IS link-state protokolleri Dijkstra kullanır
Telekom kapasite planlamaBağlantı maliyeti = ağırlık; yedeklilik için k-en kısa yol
Oyun yapay zekâsıKaro grafı + manhattan/euclid heuristic = A*
Arbitraj tespitiKur grafında w=log(rate)w = -\log(\text{rate}); negatif çevrim = kâr fırsatı
Ulaştırma probleminde MODIReduced cost grafında en negatif kenar = girer hücre
Min-Cost Flow (SSP)Residüel grafta tekrarlı en kısa yol arama
Sözlük benzeri aramaLevenshtein grafında en az düzenleme uzaklığı

Bu araçta kullandığımız çözüm

Aracımız iki algoritmayı da yerel olarak (tarayıcıda) çalıştırır:

  • Dijkstra: binary heap + lazy delete. Settled küme tutulmaz; pop edilen düğüm dist’i güncel değilse atlanır. V500V \le 500, E2000E \le 2000 sınırı eğitim/orta ölçek için yeterli.
  • Bellman-Ford: V1V - 1 pass + erken çıkış (bir pass’te değişim yoksa yakınsadı). VV‘inci pass değişim getirirse negatif çevrim raporlanır; çevrim üzerindeki bir düğüm örneği döndürülür (predecessor zincirini VV kez geri izleyerek elde edilir).
  • Otomatik seçim: “auto” modu — herhangi bir negatif kenar varsa Bellman-Ford, yoksa Dijkstra. Dijkstra negatif ağırlıkla zorlanırsa hata döner (sessizce yanlış cevap yerine).

Sınırlar

  • Tek emtia (commodity), tek kaynak. Çok-kaynak gerekirse süper-kaynak ekleyin (kapasiteden bağımsız 0 ağırlıklı kenarlarla).
  • Çevrim/zaman bağımlı ağırlıklar yok (dinamik / zaman-genişletilmiş graf).
  • Stokastik en kısa yol (rastgele ağırlık) bu modelde yok — beklenen değer için MDP / değer iterasyonu gerekir.
  • kk-en kısa yol (Yen / Eppstein) bu sürümde yok; tek en kısa yol + alternatif rotalar için ayrı bir backlog item’ı.

Bu rehberin tamamladığı Maks-Akış / Min-Cut ve Min-Maliyetli Akış rehberleriyle birlikte graf optimizasyonunun klasik üçlüsünü tamamlar — en kısa yol Min-Cost Flow’un (SSP) iç motorudur, dolayısıyla ikisini birlikte düşünmek doğaldır.

Sıkça sorulanlar

En kısa yol problemi tam olarak nedir?
Yönlü ağırlıklı bir grafda (V düğüm, E kenar, ağırlık w: E → ℝ) belirli bir kaynak s ile diğer düğümler arasındaki minimum toplam ağırlıklı yolu bulmak. Tek-kaynak tek-hedef (s → t), tek-kaynak tüm-düğüm (SSSP) veya tüm çiftler (APSP) varyantları vardır. Ağırlık olarak mesafe, süre, maliyet, enerji ya da olasılığın negatif logaritması kullanılabilir. Klasik uygulamalar: GPS navigasyon, internet yönlendirme (OSPF, IS-IS), oyunlarda yapay zekâ (A*), telefon şebekesi planlama, ulaştırma probleminde alternatif maliyet hesabı.
Dijkstra algoritması nasıl çalışır?
Greedy (açgözlü) bir yaklaşım: bilinen en yakın düğümü seç, oradan komşu düğümlere doğru gevşetme (relaxation) yap, tekrarla. Adım adım: (1) kaynak için dist[s] = 0, diğerleri için Infinity. (2) Min-öncelik kuyruğundan (binary heap) en küçük dist'li düğüm u'yu çıkar. (3) Her komşu v için dist[v] > dist[u] + w(u,v) ise dist[v]'yi gevşet. (4) Kuyruk boşalana kadar tekrarla. Binary heap ile karmaşıklık O((V + E) log V); Fibonacci heap ile O(E + V log V) teorik olarak daha iyidir ama pratikte ek sabit faktörler nedeniyle binary heap daha hızlıdır. Kritik kısıt: tüm ağırlıklar negatif olmamalı — aksi hâlde algoritma yanlış sonuç verebilir, çünkü 'bir kez kapatılan' düğümü sonradan kısaltma şansını gözden kaçırır.
Bellman-Ford ne zaman gerekir, Dijkstra'dan ne farklıdır?
Bellman-Ford negatif ağırlıklı kenarlarla çalışır ve negatif çevrimleri tespit eder. Algoritma daha sade ama yavaş: V−1 kez tüm kenarlar üzerinde dolaş, her kenarı gevşet — toplam O(V·E). V'inci pass'te hâlâ değişim olursa erişilebilir bir negatif çevrim vardır (sonsuz şekilde dönerek uzaklığı −∞'a düşürebilirsiniz, yani en kısa yol tanımsız). Ne zaman kullanılır: (1) ağırlıklar negatif olabilir (örn. ulaştırma indirgemesinde bir kenarın 'kazanç' anlamı varsa), (2) negatif çevrim olabilir mi tespit edilmesi isteniyor (arbitraj algılama, döviz/kripto arası çevrim kazançları), (3) Johnson algoritması (APSP) bir alt rutini olarak. Olumsuz yan: O(V·E) — Dijkstra'dan log V katı yavaş, büyük ağlarda fark hissedilir.
'Gevşetme (relaxation)' tam olarak ne demek?
Bir kenarı gevşetme = mevcut dist[v] tahminini, dist[u] + w(u,v) değeriyle karşılaştırıp daha küçükse güncellemek. Bu hem Dijkstra'nın hem Bellman-Ford'un atom adımıdır. Sezgisel olarak: 'şu an v'ye olan en kısa uzaklık tahminim X. Ama u'dan geçerek dist[u] + w(u,v) ile daha kısaya ulaşabiliyorsam tahminimi düşürürüm (predecessor = u olarak kaydederim).' İki algoritma kenarları farklı sırayla gevşetir: Dijkstra her zaman en küçük dist'li ulaşılmamış düğümü seçer (heap'ten); Bellman-Ford her pass'te kenar listesini bütün olarak tarar (sıra önemli değil). Doğruluk her iki durumda da 'gevşetme yakınsadığında her kenar (u,v) için dist[v] ≤ dist[u] + w(u,v) — yani üçgen eşitsizliği' kuralının sağlanmasından gelir.
Negatif çevrim tam olarak ne anlama gelir?
Toplam ağırlığı sıfırın altında kalan bir yönlü çevrim. Örneğin a → b ağırlık 2, b → a ağırlık −3 ise a → b → a çevrim toplamı −1; bu çevrimi sonsuz kez gezerek herhangi bir düğüme olan 'uzaklığınızı' istediğiniz kadar azaltabilirsiniz. Bu yüzden en kısa yol tanımsız hâle gelir (matematiksel olarak −∞). Bellman-Ford bunu V'inci pass'te bir değişiklik gözlemleyerek tespit eder. Çevrim erişilemez bir bileşendeyse (kaynaktan ulaşılamıyorsa) algoritma onu raporlamaz çünkü problem hâlâ tanımlı: erişilebilir düğümler için cevap var. Pratik anlamı önemli: arbitraj tespiti tam olarak negatif çevrim aramaktır — log(kur) ağırlıklı bir grafta negatif çevrim = riskten arınmış kâr fırsatı.
Dijkstra negatif ağırlıkta neden bozulur, somut örnek?
Klasik karşı örnek: A → B ağırlık 1, A → C ağırlık 3, B → C ağırlık −5. Tüm ağırlıklar non-negatif olmadığı için Dijkstra bu grafda yanlıştır. Algoritma: dist = {A:0, B:1, C:3}. A'dan başla; min B (dist=1) → çıkar, B'yi 'kapat'; B → C gevşet: dist[C] = 1 + (−5) = −4. Şimdi C en küçük (−4) → C'yi çıkar. Algoritma bitti — dist[C] = −4 doğru cevap (A → B → C). Bu örnekte tesadüfen çalıştı, çünkü gevşetme C kapatılmadan oldu. Patolojik durum: A → C ağırlık 1, A → B ağırlık 4, B → C ağırlık −10 olsa. Dijkstra önce C'yi (dist=1) çıkarıp 'kapatır' (settled), sonra B'yi gevşetir ama B → C kenarına bakmaz çünkü C zaten settled. Doğru cevap A → B → C = −6 ama Dijkstra dist[C] = 1 raporlar. Bu yüzden negatif ağırlık varsa Bellman-Ford.
A*, Dijkstra'dan ne kadar daha hızlı?
A* (1968, Hart-Nilsson-Raphael) Dijkstra'nın heuristic'li bir genişletmesi: priority key olarak dist[u] yerine dist[u] + h(u) — h, u'dan hedefe tahmini kalan uzaklık. h **admissible** olmalı (asla gerçek uzaklığı aşmamalı; örn. Öklid uzaklığı ya da Manhattan). Doğru h ile A* doğru cevabı verir ama Dijkstra'dan çok daha az düğüm açar — pratikte yol-bulmada (gridler, navigasyon) 10-100x hızlanma sağlar. h(u) = 0 alınca tam olarak Dijkstra'ya dönüşür. Sınırı: h iyi bir alt sınır değilse A* çok dağılır (gevşek heuristic) ya da yanlış cevap verir (tutarsız heuristic). Bu aracımız klasik Dijkstra çözüyor; A* için spesifik bir koordinat uzayı ve mesafe fonksiyonu gerektiğinden ayrı bir tasarım gerekir.