İçeriğe geç
Educora
Üniversite25 dk50 / 59

Graflar ve graf algoritmaları

Grafların gösterimini, genişlik öncelikli (BFS) ve derinlik öncelikli (DFS) aramayı, Dijkstra algoritmasını, topolojik sıralamayı ve minimum kapsayan ağacı (Kruskal) adım adım öğren.

Kendini test et
Bu derste öğreneceklerin
  • Bir grafı komşuluk matrisi ya da komşuluk listeleriyle göstermek ve seçimi gerekçelendirmek
  • BFS, DFS ve Dijkstra algoritmasını elle izlemek
  • Topolojik sıra ve minimum kapsayan ağaç bulmak

Bir navigasyon uygulaması Bakü'den Şeki'ye en kısa yolu bir saniyede bulur, bir sosyal ağ “tanıyor olabileceğin kişileri” önerir, bir üniversite de dersleri kimse “Programlama”dan önce “Algoritmalar”a yazılmasın diye sıralar. Üç farklı problem, tek model: graf. Graf, nesneleri (köşeleri) ve aralarındaki bağlantıları (kenarları) tanımlar ve bilgisayar biliminin en çok kullanılan modellerinden biridir.

Graflar ve gösterimleri

Tanım
Graf G = (V, E)

Bir V köşeler kümesi ve bir E kenarlar (köşe çiftleri) kümesi. Kenarlar yönsüz (arkadaşlık) ya da yönlü (A → B, “takip ediyor”) olabilir; her kenarın bir ağırlığı (uzaklık, maliyet, süre) varsa graf ağırlıklıdır. Bir köşenin derecesi, ona değen kenarların sayısıdır.

∑ deg(v) = 2|E|, |E| ≤ |V| · (|V| − 1) / 2∑ deg(v) = 2|E|, |E| ≤ |V| · (|V| − 1) / 2
burada:
  • deg(v)v köşesinin derecesi
  • |V|, |E|köşe ve kenar sayıları

“El sıkışma önermesi”: her yönsüz kenar derece toplamına iki uçta birer olmak üzere 2 ekler. İkinci eşitsizlik, basit yönsüz bir grafta en fazla kenar sayısıdır.

ÖzellikKomşuluk matrisiKomşuluk listeleri
bellekO(V²)O(V + E)
(u, v) kenarı var mı?O(1)O(deg u)
u'nun tüm komşularını gezmekO(V)O(deg u)
ne zaman seçiliryoğun graf (E ≈ V²)seyrek graf (yollar, sosyal ağlar)

Ders boyunca tek bir örnek graf kullanacağız. Altı köşe A–F ve ağırlıklı yönsüz kenarlar: A–B 4, A–C 2, B–C 1, B–D 5, C–D 8, C–E 10, D–E 2, D–F 6, E–F 3. Ağırlıkları kilometre olarak düşün.

Genişlik öncelikli (BFS) ve derinlik öncelikli (DFS) arama

BFS başlangıç köşesinden bir dalga gibi yayılır: önce tüm komşular, sonra komşuların komşuları. Bir kuyruk kullanır ve ağırlıksız bir grafta kenar sayısı bakımından en kısa yolları verir. DFS ise bir yol boyunca olabildiğince derine iner, çıkmaza gelince geri döner; yığıtla (ya da özyinelemeyle) çalışır ve bağlılık, döngü bulma ve topolojik sıralama için kullanılır. İkisi de her köşeye ve her kenara bir kez bakar: O(V + E).

Örnek 1: F'den BFS ve DFS

Ağırlıkları dikkate almadan örnek grafta F'den BFS ve DFS uygula. Komşuları alfabetik sırayla al: A: B, C; B: A, C, D; C: A, B, D, E; D: B, C, E, F; E: C, D, F; F: D, E.

Çözümü göster
BFS (kuyruk): [F] → F'yi al, D ve E'yi ekle (uzaklık 1) → D'yi al, B ve C'yi ekle (uzaklık 2) → E'nin yeni komşusu yok → B'yi al, A'yı ekle (uzaklık 3).
Düzeyler: F | D, E | B, C | A.
DFS (özyineleme): F → D (F'nin ilk komşusu) → B (D'nin ilk ziyaret edilmemiş komşusu) → A → C (A'nın komşusu) → E (C'nin ziyaret edilmemiş komşusu).
Sıra: F, D, B, A, C, E; DFS A'ya 4 adımda ulaştı, BFS ise en kısa yolu (3 kenar) buldu.
Python
from collections import deque

graph = {'A': ['B', 'C'], 'B': ['A', 'C', 'D'], 'C': ['A', 'B', 'D', 'E'],
         'D': ['B', 'C', 'E', 'F'], 'E': ['C', 'D', 'F'], 'F': ['D', 'E']}

def bfs(start):
    dist = {start: 0}
    queue = deque([start])
    while queue:
        v = queue.popleft()
        for w in graph[v]:
            if w not in dist:
                dist[w] = dist[v] + 1
                queue.append(w)
    return dist

def dfs(v, seen):
    seen.append(v)
    for w in graph[v]:
        if w not in seen:
            dfs(w, seen)
    return seen

print('BFS from F:', bfs('F'))
print('DFS from F:', dfs('F', []))
▸ Beklenen çıktı
BFS from F: {'F': 0, 'D': 1, 'E': 1, 'B': 2, 'C': 2, 'A': 3}
DFS from F: ['F', 'D', 'B', 'A', 'C', 'E']
Komşuluk listeleri bir sözlükte saklanır. BFS uzaklıkları köşeleri keşfettiği sırayla verir; DFS sırası elle yaptığımız izlemeyle aynıdır.

Dijkstra algoritması: ağırlıklı en kısa yollar

Kenarların ağırlığı olunca BFS işe yaramaz: üç kısa kenar tek bir uzun kenardan ucuz olabilir. Dijkstra algoritması (negatif olmayan ağırlıklar için) her adımda henüz kesinleşmemiş köşeler arasından uzaklığı en küçük olanı seçer (öncelik kuyruğu), onu kesinleştirir ve komşularının uzaklıklarını gevşetme (relaxation) ile günceller.

dist[v] ← min(dist[v], dist[u] + w(u, v)); T = O((V + E) · log V)
burada:
  • dist[v]kaynaktan v'ye şimdiye kadar bulunan en kısa uzaklık
  • w(u, v)u–v kenarının ağırlığı (≥ 0)
  • Tikili yığınla çalışma süresi
Adım: kesinleşenABCDEF
0: başlangıç0∞∞∞∞∞
1: A (0)042∞∞∞
2: C (2)0321012∞
3: B (3)032812∞
4: D (8)03281014
5: E (10)03281013
6: F (13)03281013
A'dan Dijkstra izlemesi (her adımdan sonraki dist değerleri). 2. adımda B 4'ten 3'e düşer (A → C → B); 4. ve 5. adımlarda E ve F iyileşir.
Örnek 2: yolu geri kurmak

Tabloyu kullanarak A'dan F'ye en kısa yolu ve uzunluğunu bul.

Çözümü göster
Her güncellemede “nereden geldiğimiz” (prev) saklanır: F en son E'den (10 + 3), E D'den (8 + 2), D B'den (3 + 5), B C'den (2 + 1), C de A'dan (0 + 2) güncellendi.
Sondan geriye: F ← E ← D ← B ← C ← A.
Yol: A → C → B → D → E → F, uzunluk 2 + 1 + 5 + 2 + 3 = 13.
Dikkat: ilk bakışta akla gelen A → B → D → F yolu 4 + 5 + 6 = 15'tir.
Python
import heapq

edges = [('A', 'B', 4), ('A', 'C', 2), ('B', 'C', 1), ('B', 'D', 5), ('C', 'D', 8),
         ('C', 'E', 10), ('D', 'E', 2), ('D', 'F', 6), ('E', 'F', 3)]
adj = {}
for u, v, w in edges:
    adj.setdefault(u, []).append((v, w))
    adj.setdefault(v, []).append((u, w))

def dijkstra(src):
    dist, prev, pq = {src: 0}, {}, [(0, src)]
    while pq:
        d, u = heapq.heappop(pq)
        if d > dist[u]:
            continue
        for v, w in adj[u]:
            if d + w < dist.get(v, float('inf')):
                dist[v], prev[v] = d + w, u
                heapq.heappush(pq, (d + w, v))
    return dist, prev

dist, prev = dijkstra('A')
path = ['F']
while path[-1] != 'A':
    path.append(prev[path[-1]])
print(dict(sorted(dist.items())))
print(' -> '.join(reversed(path)), '=', dist['F'])
▸ Beklenen çıktı
{'A': 0, 'B': 3, 'C': 2, 'D': 8, 'E': 10, 'F': 13}
A -> C -> B -> D -> E -> F = 13
Öncelik kuyruğu rolünü heapq üstlenir. Bir köşe kuyruğa birkaç kez girebilir; eskimiş kayıtları d > dist[u] koşulu atlar (“tembel silme”).

Topolojik sıralama ve minimum kapsayan ağaç

Topolojik sıralama, yönlü döngüsüz bir grafın (DAG) köşelerini her u → v kenarı için u, v'den önce gelecek biçimde dizer: ders önkoşulları, program modüllerinin derlenme sırası, Excel'de hücrelerin yeniden hesaplanma sırası. Kahn algoritması: iç derecesi 0 olan köşeleri kuyruğa koy, birini çıkar, kenarlarını sil ve iç derecesi 0'a düşen köşeleri kuyruğa ekle. V'den az köşe çıkarsa grafta döngü vardır. Süre: O(V + E).

Python
from collections import deque

courses = {'Programming': ['DataStructures', 'Databases'], 'Math': ['Algorithms'],
           'DataStructures': ['Algorithms'], 'Databases': ['AI'],
           'Algorithms': ['AI'], 'AI': []}

def topo_sort(g):
    indeg = {v: 0 for v in g}
    for v in g:
        for w in g[v]:
            indeg[w] += 1
    queue = deque(v for v in g if indeg[v] == 0)
    order = []
    while queue:
        v = queue.popleft()
        order.append(v)
        for w in g[v]:
            indeg[w] -= 1
            if indeg[w] == 0:
                queue.append(w)
    return order if len(order) == len(g) else None

print(topo_sort(courses))
print(topo_sort({'X': ['Y'], 'Y': ['Z'], 'Z': ['X']}))
▸ Beklenen çıktı
['Programming', 'Math', 'DataStructures', 'Databases', 'Algorithms', 'AI']
None
Ders grafı için geçerli bir sıra elde edilir; X → Y → Z → X döngüsü olan graf için None döner: böyle önkoşullarla hiçbir derse başlanamaz.
Tanım
Minimum kapsayan ağaç (MST)

Bağlı ağırlıklı bir grafın tüm köşelerini birleştiren, döngü içermeyen ve toplam ağırlığı olabilecek en küçük olan kenar kümesi. Her zaman tam V − 1 kenarı vardır. Kullanımlar: köyleri en az kabloyla bağlamak, elektrik ve su şebekesi tasarımı, kümeleme.

Örnek 3: Kruskal algoritması

Örnek grafın minimum kapsayan ağacını Kruskal algoritmasıyla bul: kenarları artan ağırlıkla al ve döngü oluşturmayanları seç.

Çözümü göster
Sıra: B–C 1, A–C 2, D–E 2, E–F 3, A–B 4, B–D 5, D–F 6, C–D 8, C–E 10.
B–C 1 ✓ → {B, C}
A–C 2 ✓ → {A, B, C}
D–E 2 ✓ → {D, E}
E–F 3 ✓ → {D, E, F}
A–B 4 ✗ — A ve B zaten aynı bileşende (A–C–B döngüsü kapanırdı)
B–D 5 ✓ → tüm köşeler birleşti; 5 = V − 1 kenar, dur.
MST: {B–C, A–C, D–E, E–F, B–D}, toplam ağırlık 1 + 2 + 2 + 3 + 5 = 13.
Python
edges = [('A', 'B', 4), ('A', 'C', 2), ('B', 'C', 1), ('B', 'D', 5), ('C', 'D', 8),
         ('C', 'E', 10), ('D', 'E', 2), ('D', 'F', 6), ('E', 'F', 3)]
parent = {v: v for v in 'ABCDEF'}

def find(v):
    while parent[v] != v:
        v = parent[v]
    return v

mst = []
for u, v, w in sorted(edges, key=lambda e: e[2]):
    ru, rv = find(u), find(v)
    if ru != rv:
        parent[ru] = rv
        mst.append((u, v, w))
print(mst)
print('total weight:', sum(w for _, _, w in mst))
▸ Beklenen çıktı
[('B', 'C', 1), ('A', 'C', 2), ('D', 'E', 2), ('E', 'F', 3), ('B', 'D', 5)]
total weight: 13
“Döngü oluşturur mu?” sorusunu birleştir–bul (union–find) yapısı yanıtlar: find bir bileşenin “temsilcisini” bulur; iki köşenin temsilcisi aynıysa kenar bir döngü kapatır. Kruskal'ın süresini sıralama belirler: O(E log E). Prim algoritması ise ağacı bir köşeden büyütür (Dijkstra'ya benzer, öncelik kuyruğuyla).

Önemli noktalar

  • Graf G = (V, E)'dir; seyrek graflar için komşuluk listesi (O(V + E)), yoğun graflar için matris (O(V²)).
  • BFS kuyrukla düzey düzey ilerler ve ağırlıksız en kısa yolları verir; DFS yığıtla derine iner; ikisi de O(V + E)'dir.
  • Dijkstra negatif olmayan ağırlıklarla, öncelik kuyruğu ve gevşetmeyle O((V + E) log V) sürede çalışır.
  • Topolojik sıra yalnızca DAG'de vardır; Kahn algoritması döngüleri de saptar.
  • MST'nin V − 1 kenarı vardır; Kruskal kenarları ağırlığa göre sıralar ve union–find ile döngülerden kaçınır.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
Bir yol ağında V = 10⁴ kavşak ve E = 3 · 10⁴ yol var. Komşuluk matrisi kaç hücre gerektirir?