SEN2212 · 11. hafta · Analyzing and implementing graph algorithms.

En kısa yol ve döngü tespiti: BFS, Dijkstra, Bellman-Ford

Ağırlıksız grafta BFS ile, pozitif ağırlıkta Dijkstra ile, negatif kenarda Bellman-Ford ile en kısa yol; yönlü ve yönsüz grafta döngü bulma. Sınavın tablo doldurma soruları buradan gelir.

shortest pathsingle-sourceunweighted graphweighted graphDijkstra's algorithmrelaxationknown / visitednegative edgeBellman-Fordnegative cyclecycle detectionback edgeDAG

Geçen hafta grafı nasıl saklayacağını ve BFS/DFS ile nasıl gezeceğini gördün. Bu hafta o gezintileri işe koşuyoruz: bir kaynaktan (single-source) her düğüme en kısa yol ve grafta döngü var mı soruları. Sınavda neredeyse kesin bir “Dijkstra tablosunu doldur” sorusu gelir. Yanında “Dijkstra burada neden yanlış?” ya da “bu grafta döngü var mı?” türü kısa sorular olur.

Altın kurallar:

  1. Ağırlık yoksa BFS yeter. En kısa yol = en az kenar sayısı = BFS katmanı.
  2. Ağırlık pozitifse Dijkstra. Her adımda bilinmeyenler içinde en küçük mesafeli düğümü seç, kesinleştir, kenarlarını gevşet.
  3. Negatif kenar varsa Bellman-Ford. Dijkstra sessizce yanlış cevap verebilir.
  4. Yol, prev dizisinden geriye okunur. Hedeften kaynağa yürü, sonra ters çevir.
  5. Yönlü grafta döngü = gri düğüme kenar. Yönsüzde = iki ucu aynı kümede olan kenar.

Ağırlıksız en kısa yol: BFS

Her kenarın maliyeti 1 ise en kısa yol, en az kenarlı yoldur. BFS düğümleri kaynaktan uzaklık sırasına göre (katman katman) çıkardığı için bir düğümü ilk keşfettiği an mesafesi kesindir. Kural: dist[w] = dist[v] + 1, prev[w] = v.

Örnek yönsüz graf (komşu listeleri artan sırada):

      0
     / \
    1   2
     \ /
      3
      |
      4
     / \
    5   6
     \ /
      7
kuyruk         çıkan  keşfedilen (dist)
[0]            0      1(1) 2(1)
[1,2]          1      3(2)
[2,3]          2      -    (3 zaten görüldü)
[3]            3      4(3)
[4]            4      5(4) 6(4)
[5,6]          5      7(5)
[6,7]          6      -
[7]            7      -
BFS dist: [0, 1, 1, 2, 3, 4, 4, 5]
BFS prev: [-1, 0, 0, 1, 3, 4, 4, 5]
BFS path 0->7: [0, 1, 3, 4, 5, 7]

Süre O(V+E)O(V + E): her düğüm kuyruğa bir kez girer, her kenar bir kez (yönsüzde iki kez) bakılır.

TUZAK · mesafeyi kuyruktan çıkarken değil, keşfederken yaz. Düğümü kuyruğa koyduğun anda dist ve prev ver ve “görüldü” say. Yoksa aynı düğüm kuyruğa iki kez girer.

Dijkstra algoritması

Kenarların ağırlığı var ve hepsi negatif değil. Fikir: kaynaktan en yakın düğüm, başka hiçbir düğüm üzerinden daha kısa yola sahip olamaz (her ek kenar maliyeti artırır). O yüzden en küçük geçici mesafeli düğümü kesinleştir (known) ve ondan çıkan kenarları gevşet (relax):

du>dv+w(v,u)  ⇒  du=dv+w(v,u),  prevu=vd_u > d_v + w(v,u) \;\Rightarrow\; d_u = d_v + w(v,u),\; prev_u = v

Adımlar:

  1. d[kaynak] = 0, diğerleri ∞. Hiçbiri bilinmiyor.
  2. Bilinmeyenler içinden d değeri en küçük olan v’yi seç. Eşitlikte küçük etiketi seç.
  3. v’yi bilinen yap. Bilinmeyen her komşu u için gevşet.
  4. Bilinmeyen kalmayana (ya da hepsi ∞ olana) kadar 2’ye dön.

Çözümlü örnek

G1 grafı (yönlü), komşu listesi gösterimiyle:

A: B(4) C(1)
B: D(5)
C: B(2) D(8) E(10)
D: E(2) F(6)
E: F(2)
F: -

Sınavda tablo şöyle doldurulur. Her satır bir seçim; * bilinen demek.

adım seç  A   B   C   D   E   F
 0    -   0   ∞   ∞   ∞   ∞   ∞
 1    A   0*  4   1   ∞   ∞   ∞
 2    C   0*  3   1*  9   11  ∞
 3    B   0*  3*  1*  8   11  ∞
 4    D   0*  3*  1*  8*  10  14
 5    E   0*  3*  1*  8*  10* 12
 6    F   0*  3*  1*  8*  10* 12*

Programın çıktısı:

Dijkstra order: A C B D E F
Dijkstra final: A=0 B=3 C=1 D=8 E=10 F=12
prev: A=- B=C C=A D=B E=D F=E
  path to F: [A, C, B, D, E, F] cost 12

Dikkat edilecek iki an: 2. adımda B’nin değeri 4’ten 3’e düştü (C üzerinden daha ucuz). 4. adımda E, 11’den 10’a düştü. Gevşetme budur: daha iyi bir yol bulunca değeri ve prev’i güncellersin.

İPUCU · tabloda prev sütunu da tut. Sınav “path from A to F” diye sorarsa her güncellemede kimin üzerinden geldiğini yazmış olman gerekir. Sonradan geri hesaplamak zaman kaybettirir.

Karmaşıklık

UygulamaEn küçüğü bulmaToplam
Dizi taramasıO(V)O(V), V kezO(V2)O(V^2)
Binary heap (PriorityQueue)O(log⁡V)O(\log V)O(Elog⁡V)O(E \log V)

Java’da PriorityQueue ile yazarken “decreaseKey” yoktur. Pratik çözüm: mesafe düşünce düğümü yeni değeriyle tekrar ekle, kuyruktan çıkan düğüm zaten biliniyorsa atla (lazy deletion).

PriorityQueue<int[]> pq = new PriorityQueue<>((x, y) -> x[1] - y[1]);
pq.add(new int[]{src, 0});
while (!pq.isEmpty()) {
    int[] top = pq.poll();
    int v = top[0];
    if (known[v]) continue;          // eski kopya
    known[v] = true;
    for (int[] e : adj.get(v)) {     // e = {komşu, ağırlık}
        int u = e[0], w = e[1];
        if (!known[u] && d[v] + w < d[u]) {
            d[u] = d[v] + w;
            prev[u] = v;
            pq.add(new int[]{u, d[u]});
        }
    }
}

Negatif kenar ve Bellman-Ford

Dijkstra’nın “en yakını kesinleştir” adımı, sonradan bulunan bir kenarın mesafeyi azaltamayacağına güvenir. Negatif kenar bu güveni bozar.

A --2--> B --1--> D
|        ^
5       -4
|        |
+-> C ---+
Dijkstra order: A B D C
Dijkstra final: A=0 B=2 C=5 D=3
BF round 1: A=0 B=1 C=5 D=2

Dijkstra B’yi 2 ile kesinleştirdi, D’yi 3 yaptı. C en son geldi; C→B kenarı B’yi 1’e indirebilirdi ama B kapandı. Gerçek cevap D = 2.

Bellman-Ford seçim yapmaz: her turda bütün kenarları sırayla gevşetir. En kısa yol en fazla V−1 kenar içerdiği için V−1 tur yeter. Süre O(V⋅E)O(V \cdot E), Dijkstra’dan yavaş ama negatif kenarla doğru çalışır.

Arrays.fill(d, INF);
d[src] = 0;
for (int round = 1; round <= V - 1; round++)
    for (Edge e : edges)
        if (d[e.from] != INF && d[e.from] + e.w < d[e.to])
            d[e.to] = d[e.from] + e.w;

Kenar sırası tur sayısını etkiler. e1 B→C 2, e2 A→B −1, e3 S→A 4, e4 S→B 5, e5 C→D 3, e6 A→C 6 sırasıyla, S’den:

BF round 1: S=0 A=4 B=5 C=10 D=inf
BF round 2: S=0 A=4 B=3 C=7 D=10
BF round 3: S=0 A=4 B=3 C=5 D=8
BF round 4: S=0 A=4 B=3 C=5 D=8 (no change)

Bir turda hiçbir şey değişmediyse erken durabilirsin. V−1 turdan sonra bir kenar hâlâ gevşiyorsa toplamı negatif bir döngü var demektir; o zaman “en kısa yol” tanımsızdır (döngüde dönüp durarak maliyet sonsuza kadar azalır). S→A 1, A→B 2, B→C −4, C→A 1 grafında A→B→C→A toplamı −1 ve program BF negative cycle: true basar.

TUZAK · “tüm ağırlıklara sabit ekle” hilesi çalışmaz. Negatif kenardan kurtulmak için her kenara 5 eklersen çok kenarlı yollar daha fazla ceza alır, en kısa yol değişir.

Döngü tespiti (cycle detection)

Yönlü graf: üç renkli DFS

  • Beyaz (0): hiç görülmedi.
  • Gri (1): DFS’e girildi, henüz bitmedi (özyineleme yığınında).
  • Siyah (2): tüm torunları bitti.

Gri bir düğüme giden kenar, şu anki yolun bir atasına geri dönüyor demek: geri kenar (back edge), yani döngü. Siyah düğüme giden kenar döngü değildir; o dal zaten bitmiş.

0 --> 1 --> 2
|     ^     |
v     |     v
4     +---- 3

DFS 0, 1, 2, 3 sırasıyla iner, dördü de gri. 3→1 gri düğüme gidiyor:

G2 directed cycle: true back edge 3->1
DAG directed cycle: false

Döngüsüz yönlü grafa DAG denir. Sadece DAG’ların topolojik sırası vardır; Kahn algoritmasında (geçen hafta) tüm düğümler çıkarılamıyorsa da döngü var demektir.

Yönsüz graf

Yönsüz grafta her kenar iki yönlü olduğu için “komşu zaten görülmüş” her zaman döngü değildir: geldiğin ebeveyn de görülmüştür. İki yol:

  • DFS + parent: görülmüş bir komşu, ebeveynin değilse döngü.
  • Union-find: kenarları tek tek ekle. İki uç zaten aynı kümedeyse bu kenar döngü kapatır. Kruskal (13. hafta) tam olarak bunu kullanır.
undirected: first cycle-closing edge 1-3

Hızlı kontrol: bağlantılı yönsüz grafta E≥VE \ge V ise mutlaka döngü vardır (ağaçta E=V−1E = V - 1).

Hangi algoritma ne zaman?

DurumAlgoritmaSüre
AğırlıksızBFSO(V+E)O(V+E)
Ağırlıklar ≥ 0Dijkstra (heap)O(Elog⁡V)O(E \log V)
Negatif kenar olabilirBellman-FordO(VE)O(VE)
DAG, ağırlık herhangiTopolojik sırayla gevşetmeO(V+E)O(V+E)
Yönlü döngü var mı3 renkli DFSO(V+E)O(V+E)
Yönsüz döngü var mıDFS + parent ya da union-findO(V+E)O(V+E)

Sık yapılan hatalar

Yanlış düşünceDoğrusu
Dijkstra’da sıradaki düğüm, son seçilenin komşusuBütün bilinmeyenler içindeki en küçük d seçilir
Bilinen düğümün değeri sonradan güncellenirDijkstra’da güncellenmez; değişiyorsa negatif kenar var
Az kenarlı yol = kısa yolSadece ağırlıksız grafta. Ağırlıklıda toplam ağırlık
Negatif kenarla Dijkstra da doğru çalışırYanlış cevap verebilir, Bellman-Ford kullan
Bellman-Ford V tur yaparV−1 tur yeter; V. tur sadece negatif döngü kontrolü
Yönlü grafta görülmüş düğüme kenar = döngüSadece GRİ düğüme kenar döngüdür
Yönsüz DFS’te ebeveyne dönüş döngüdürEbeveyn hariç tutulur
Yol, prev dizisini baştan okuyarak bulunurHedeften geriye izlenir, sonra ters çevrilir

Kendini test et

Sınavdaki gibi İngilizce sorular. Önce kâğıt kalemle çöz, sonra cevabı aç.

Soru 1kolay

The undirected graph has edges 0-1, 0-2, 1-3, 2-3, 3-4, 4-5, 4-6, 5-7, 6-7 (adjacency lists in ascending order). Run BFS from vertex 0. Give the shortest-path distance (number of edges) of every vertex 0..7.

Cevabı göster
0, 1, 1, 2, 3, 4, 4, 5

Ağırlıksız grafta en kısa yol = BFS katmanı. Kuyruk: 0 → (1, 2) mesafe 1 → 3 mesafe 2 (1 üzerinden) → 4 mesafe 3 → (5, 6) mesafe 4 → 7 mesafe 5. Dizi: [0, 1, 1, 2, 3, 4, 4, 5].

Soru 2orta

In the same BFS from 0, using the prev array (first discoverer wins), what is the shortest path reported from 0 to 7?

Cevabı göster
0 → 1 → 3 → 4 → 5 → 7

prev dizisi [-1, 0, 0, 1, 3, 4, 4, 5]. 7'den geriye: prev[7]=5, prev[5]=4, prev[4]=3, prev[3]=1, prev[1]=0. Ters çevir: 0, 1, 3, 4, 5, 7. 3'ü ilk keşfeden 1'dir çünkü 1 kuyruktan 2'den önce çıktı.

Soru 3orta

Directed graph G1: A→B 4, A→C 1, C→B 2, B→D 5, C→D 8, C→E 10, D→E 2, D→F 6, E→F 2. Run Dijkstra from A. Give the final distances of A, B, C, D, E, F.

Cevabı göster
A=0, B=3, C=1, D=8, E=10, F=12

Her adımda bilinmeyenler arasından en küçük mesafeyi seç, çıkan kenarlarını gevşet (relax):

pick A: B=4 C=1
pick C: B=min(4,1+2)=3 D=9 E=11
pick B: D=min(9,3+5)=8
pick D: E=min(11,8+2)=10 F=14
pick E: F=min(14,10+2)=12
pick F: bitti

Soru 4kolay

For the Dijkstra run on G1 from A, in which order do the vertices become known?

Cevabı göster
A, C, B, D, E, F

Seçim her zaman bilinmeyenler içindeki en küçük mesafe: A(0), C(1), B(3), D(8), E(10), F(12). Mesafeler artan sırada kesinleşir; Dijkstra'nın temel özelliği budur.

Soru 5orta

Using the Dijkstra result on G1, what is the shortest path from A to F and its cost? Is the direct-looking route A→B→D→F shorter?

Cevabı göster
A → C → B → D → E → F, cost 12; A→B→D→F costs 15

prev zinciri: F←E←D←B←C←A. Maliyet 1+2+5+2+2 = 12. A→B→D→F = 4+5+6 = 15. Daha az kenarlı yol daha kısa olmak zorunda değil; ağırlıklı grafta kenar sayısı değil toplam ağırlık sayılır.

Soru 6kolay

What is the running time of Dijkstra's algorithm (a) with a simple array scan to find the minimum, (b) with a binary heap priority queue? V = vertices, E = edges.

Cevabı göster
(a) O(V²) (b) O(E log V)

(a) V kez en küçüğü bulmak için V elemanı tara: O(V2)O(V^2), kenar gevşetmeleri toplam O(E)O(E). (b) Her gevşetme heap'e O(log⁡V)O(\log V) maliyetle ekleme yapar, toplam O(Elog⁡V)O(E \log V). Seyrek grafta (E ≈ V) heap, yoğun grafta (E ≈ V²) dizi daha iyidir.

Soru 7zor

Graph: A→B 2, A→C 5, C→B -4, B→D 1. Dijkstra from A (a known vertex is never updated again) reports dist(D) = ? What is the true shortest distance to D?

Cevabı göster
Dijkstra: 3, true: 2

Dijkstra sırası A, B, D, C. B mesafe 2 ile erken kesinleşir, D = 2+1 = 3 olur. C en son seçilir; C→B -4 ile B = 1 olabilirdi ama B zaten bilinen, güncellenmez. Doğru yol A→C→B→D = 5-4+1 = 2. Bellman-Ford ilk turda B=1, D=2 bulur. Ders: negatif kenar varsa Dijkstra'ya güvenme.

Soru 8zor

Bellman-Ford on vertices S, A, B, C, D from S. Edges are relaxed in this fixed order each round: e1 B→C 2, e2 A→B -1, e3 S→A 4, e4 S→B 5, e5 C→D 3, e6 A→C 6. Give dist after round 1 and after round 2.

Cevabı göster
Round 1: A=4 B=5 C=10 D=∞; Round 2: A=4 B=3 C=7 D=10

Tur 1: e1, e2 atlanır (B, A henüz ∞). e3 A=4, e4 B=5, e5 atlanır (C ∞), e6 C=4+6=10.
Tur 2: e1 C=5+2=7, e2 B=4-1=3, e5 D=7+3=10, e6 10 > 7 değişmez.
Tur 3'te C=3+2=5, D=8 olur; tur 4'te değişiklik yok. Final: A=4, B=3, C=5, D=8. Kenar sırası kötü olduğu için doğru cevap birkaç tur sürdü; en fazla V-1 = 4 tur gerekir.

Soru 9kolay

How many rounds does Bellman-Ford need in the worst case on a graph with V vertices, what is its running time, and how does it detect a negative cycle?

Cevabı göster
V − 1 rounds, O(V·E); one extra round that still improves a distance means a negative cycle

En kısa yol (döngüsüz) en fazla V-1 kenar içerir; her tur en az bir kenar daha doğru yerleştirir. Her tur E kenarı gezer: O(VE)O(VE). V-1 turdan sonra hâlâ bir kenar gevşiyorsa sonsuza kadar azalan bir döngü var: negatif döngü. Örnek: S→A 1, A→B 2, B→C -4, C→A 1 döngüsünün toplamı -1, kontrol turu true döndürür.

Soru 10orta

Directed graph: 0→1, 1→2, 2→3, 3→1, 0→4. Run DFS from 0 with white/gray/black colors (adjacency in ascending order). Does the graph have a cycle? Which edge is found as the back edge?

Cevabı göster
Yes, back edge 3→1

DFS 0'dan 1'e, 2'ye, 3'e iner; hepsi gri (yığında). 3→1 kenarı gri bir düğüme gidiyor: geri kenar (back edge), yani döngü 1→2→3→1. Siyah (bitmiş) düğüme giden kenar döngü demek değildir; sadece gri düğüme giden kenar döngüdür.

Soru 11orta

Undirected graph on 0..5. Edges are added in the order 0-1, 1-2, 3-4, 2-4, 4-5, 1-3, 0-5 using union-find. Which edge is the first one that closes a cycle?

Cevabı göster
1-3

0-1, 1-2 ile {0,1,2}; 3-4 ile {3,4}; 2-4 bunları birleştirir {0,1,2,3,4}; 4-5 ile 5 de katılır. 1-3 geldiğinde find(1) = find(3), iki uç aynı kümede: döngü. Yönsüz grafta kural: iki ucu zaten bağlı olan kenar döngü kapatır.

Soru 12zor

Write a method boolean hasCycle(List<List<Integer>> adj) that returns true if the directed graph (vertices 0..n-1) contains a cycle. Use DFS with three colors.

Cevabı göster
0 = white, 1 = gray (on stack), 2 = black; an edge to a gray vertex means a cycle

Gri = şu anki DFS yolunda. Gri düğüme dönmek geri kenardır. Bağlantısız grafta her beyaz düğümden ayrıca başla:

static boolean hasCycle(List<List<Integer>> adj) {
    int[] color = new int[adj.size()];
    for (int v = 0; v < adj.size(); v++)
        if (color[v] == 0 && dfs(adj, v, color)) return true;
    return false;
}
static boolean dfs(List<List<Integer>> adj, int v, int[] color) {
    color[v] = 1;                       // gray
    for (int w : adj.get(v)) {
        if (color[w] == 1) return true; // back edge
        if (color[w] == 0 && dfs(adj, w, color)) return true;
    }
    color[v] = 2;                       // black
    return false;
}
Süre O(V+E)O(V + E). 0→1→2→3→0 halkasında true, 0→1, 0→2, 1→3, 2→3, 3→4 DAG'ında false döner.

Doğrulama: Tüm izler (BFS mesafeleri, Dijkstra adım tabloları ve yolları, negatif kenarda Dijkstra hatası, Bellman-Ford turları, negatif döngü, renkli DFS ile geri kenar, union-find ile döngü kapatan kenar) verify/SEN2212/W11.java ile JDK'da çalıştırılıp çıktıdan birebir alındı (java W11.java).

Deftere not kartı

Ağırlıksız → BFS, dist[w] = dist[v] + 1, O(V+E) Pozitif ağırlık → Dijkstra: en küçük bilinmeyeni seç, kenarlarını gevşet relax: if d[v] + w < d[u] then d[u] = d[v] + w, prev[u] = v Dijkstra: dizi O(V²), binary heap O(E log V) Mesafeler ARTAN sırada kesinleşir; bilinen düğüm bir daha değişmez Negatif kenar → Dijkstra YANLIŞ olabilir → Bellman-Ford Bellman-Ford: V-1 tur, her tur tüm kenarlar, O(V·E) V. turda hâlâ değişiklik → negatif döngü Yol = prev zincirini hedeften geriye izle, ters çevir Yönlü döngü: DFS + 3 renk, GRİ düğüme kenar = back edge Yönsüz döngü: union-find, iki uç aynı kümede → döngü DAG'da topolojik sıra var; döngü varsa yok