go
Algoritmalar dersleri
Algoritmalar/Graf Algoritmaları

Topolojik Sıralama

Yönlü döngüsüz graflar, Kahn algoritması ve bağımlılık çözümleme.

Ders 13 / 1825 dkOrta
Bu derste öğreneceklerin
  • Yönlü döngüsüz graflar (DAG)
  • Kahn algoritması (giriş derecesi)
  • DFS tabanlı topolojik sıralama
  • Yönlü grafta döngü tespiti (üç renk)
  • Ders planlama problemi
  • Derleme ve bağımlılık sıralaması

Bir yazılım projesini derlemek istiyorsun ama modüller birbirine bağımlı: A modülü B'ye, B de C'ye ihtiyaç duyuyor. Hangi sırayla derleyeceksin? Ya da bir üniversitede ders seçimi yapıyorsun; bazı dersler ön koşul gerektiriyor. Hangi sırayla alacaksın?

Bu soruların ortak yapısı şudur: Bazı işler diğerlerinden önce yapılmak zorunda ve bu kısıtları bozmayan bir sıralama arıyorsun. Bu sıralamaya topolojik sıralama denir. Bağımlılıkları yönlü kenarlar olarak modellersen, problem bir graf problemine dönüşür.

Kritik bir ön koşul vardır: Graf döngü içermemelidir. A, B'ye bağımlıysa ve B de A'ya bağımlıysa hiçbir geçerli sıralama yoktur. Bu yüzden topolojik sıralama yalnızca yönlü döngüsüz graflar (DAG — Directed Acyclic Graph) için tanımlıdır. Bu derste iki farklı topolojik sıralama algoritmasını, yönlü graflarda döngü tespitini ve gerçek uygulamalarını öğreneceksin.

Yönlü döngüsüz graflar

Ders bağımlılıkları (ok: ön koşul → ders)

   Matematik ──► Algoritmalar ──► Yapay Zekâ
       │              │
       ▼              ▼
   İstatistik ──► Makine Öğrenmesi

Geçerli bir sıra:
  Matematik, Algoritmalar, İstatistik, Makine Öğrenmesi, Yapay Zekâ
  Matematik, İstatistik, Algoritmalar, Yapay Zekâ, Makine Öğrenmesi
  ...

Birden çok geçerli sıra olabilir! Topolojik sıralama TEK değildir.

DAG'ların iki temel özelliği vardır. Birincisi, en az bir kaynak düğümü vardır: hiç gelen kenarı olmayan bir düğüm. (Olmasaydı, her düğümün bir öncesi olurdu ve geriye doğru sonsuza kadar gidebilirdik — bu ancak döngü varsa mümkündür.) İkincisi, en az bir bitiş düğümü vardır: hiç giden kenarı olmayan.

Bu birinci özellik, birinci algoritmanın temelini oluşturur.

Aşağıdaki görselleştirmede bir bağımlılık grafını topolojik olarak sıralayabilir, hangi düğümün ne zaman hazır hâle geldiğini izleyebilirsin:

Kahn algoritması

Fikir çok sezgiseldir: Bağımlılığı kalmamış bir işi al, yap, sil. Tekrarla.

Her düğümün GİRİŞ DERECESİ (kaç ön koşulu var) hesaplanır.

   A(0) ──► B(1) ──► D(2)
     │        │        ▲
     ▼        ▼        │
   C(1) ────────────────┘

adım 1: derecesi 0 olanlar: [A] → A'yı al, komşularının derecesini azalt
        B(0), C(0)
adım 2: [B, C] → B'yi al → D(1)
adım 3: [C] → C'yi al → D(0)
adım 4: [D] → D'yi al

sıra: A, B, C, D
main.go
package main

import (
	"fmt"
	"maps"
	"slices"
)

type DAG struct {
	adj   map[string][]string
	nodes map[string]bool
}

func NewDAG() *DAG {
	return &DAG{adj: map[string][]string{}, nodes: map[string]bool{}}
}

func (g *DAG) AddNode(n string) { g.nodes[n] = true }

// AddEdge: from → to (from, to'nun ön koşuludur)
func (g *DAG) AddEdge(from, to string) {
	g.AddNode(from)
	g.AddNode(to)
	g.adj[from] = append(g.adj[from], to)
	slices.Sort(g.adj[from])
}

func (g *DAG) Nodes() []string { return slices.Sorted(maps.Keys(g.nodes)) }

// inDegrees: her düğümün kaç gelen kenarı var
func (g *DAG) inDegrees() map[string]int {
	deg := map[string]int{}
	for _, n := range g.Nodes() {
		deg[n] = 0
	}
	for _, targets := range g.adj {
		for _, t := range targets {
			deg[t]++
		}
	}
	return deg
}

// kahn: giriş derecesi tabanlı topolojik sıralama — O(V + E)
func (g *DAG) kahn() ([]string, bool) {
	deg := g.inDegrees()

	// Derecesi sıfır olanlarla başla (alfabetik: deterministik çıktı)
	var ready []string
	for _, n := range g.Nodes() {
		if deg[n] == 0 {
			ready = append(ready, n)
		}
	}
	slices.Sort(ready)

	var order []string
	for len(ready) > 0 {
		cur := ready[0]
		ready = ready[1:]
		order = append(order, cur)

		for _, next := range g.adj[cur] {
			deg[next]--
			if deg[next] == 0 {
				ready = append(ready, next)
				slices.Sort(ready) // deterministik sıra
			}
		}
	}

	// Tüm düğümler sıralandıysa döngü yok
	return order, len(order) == len(g.nodes)
}

// levels: aynı anda yapılabilecek işleri gruplar
func (g *DAG) levels() ([][]string, bool) {
	deg := g.inDegrees()
	var current []string
	for _, n := range g.Nodes() {
		if deg[n] == 0 {
			current = append(current, n)
		}
	}

	var result [][]string
	processed := 0
	for len(current) > 0 {
		slices.Sort(current)
		result = append(result, current)
		processed += len(current)

		var next []string
		for _, node := range current {
			for _, t := range g.adj[node] {
				deg[t]--
				if deg[t] == 0 {
					next = append(next, t)
				}
			}
		}
		current = next
	}
	return result, processed == len(g.nodes)
}

func main() {
	g := NewDAG()
	deps := [][2]string{
		{"Matematik", "Algoritmalar"},
		{"Matematik", "İstatistik"},
		{"Algoritmalar", "Yapay Zekâ"},
		{"Algoritmalar", "Makine Öğrenmesi"},
		{"İstatistik", "Makine Öğrenmesi"},
	}
	for _, d := range deps {
		g.AddEdge(d[0], d[1])
	}

	fmt.Println("bağımlılıklar (ön koşul → ders):")
	for _, d := range deps {
		fmt.Printf("  %s%s\n", d[0], d[1])
	}

	fmt.Println()
	fmt.Println("giriş dereceleri:")
	deg := g.inDegrees()
	for _, n := range g.Nodes() {
		fmt.Printf("  %-20s %d\n", n, deg[n])
	}

	order, ok := g.kahn()
	fmt.Println()
	fmt.Println("topolojik sıra (döngü yok:", ok, "):")
	for i, n := range order {
		fmt.Printf("  %d. %s\n", i+1, n)
	}

	fmt.Println()
	fmt.Println("paralel yapılabilecek gruplar (dönem planı):")
	lvls, _ := g.levels()
	for i, level := range lvls {
		fmt.Printf("  dönem %d: %v\n", i+1, level)
	}
	fmt.Println("en az dönem sayısı:", len(lvls))
}
Çıktı
bağımlılıklar (ön koşul → ders):
  Matematik → Algoritmalar
  Matematik → İstatistik
  Algoritmalar → Yapay Zekâ
  Algoritmalar → Makine Öğrenmesi
  İstatistik → Makine Öğrenmesi

giriş dereceleri:
  Algoritmalar         1
  Makine Öğrenmesi     2
  Matematik            0
  Yapay Zekâ           1
  İstatistik           1

topolojik sıra (döngü yok: true ):
  1. Matematik
  2. Algoritmalar
  3. Yapay Zekâ
  4. İstatistik
  5. Makine Öğrenmesi

paralel yapılabilecek gruplar (dönem planı):
  dönem 1: [Matematik]
  dönem 2: [Algoritmalar İstatistik]
  dönem 3: [Makine Öğrenmesi Yapay Zekâ]
en az dönem sayısı: 3
ZamanO(V + E)AlanO(V)

Kahn algoritmasının iki güzel yan ürünü vardır. Birincisi, döngü tespiti bedavadır: Algoritma bittiğinde sıralanan düğüm sayısı toplam düğüm sayısından azsa, kalan düğümler bir döngü içindedir. İkincisi, levels fonksiyonunda gördüğün gibi, aynı anda yapılabilecek işleri gruplayabilirsin — bu, paralel derleme ve proje planlamanın temelidir.

DFS tabanlı topolojik sıralama

İkinci yaklaşım, DFS'in çıkış sırasını kullanır: Bir düğümün tüm ardıllarını işledikten sonra onu listenin başına ekle.

DFS'in çıkış sırası, topolojik sıranın TERSİDİR.

Neden? Bir düğüm, tüm ardılları işlendikten sonra çıkar.
Yani çıkış sırasında sonra çıkan, sıralamada önce gelir.

A → B → D
A → C → D

DFS(A): B'ye in → D'ye in → D çık → B çık → C'ye in → C çık → A çık
çıkış sırası: D, B, C, A
ters çevir:   A, C, B, D  ← topolojik sıra
main.go
package main

import (
	"fmt"
	"maps"
	"slices"
)

type DAG struct {
	adj   map[string][]string
	nodes map[string]bool
}

func NewDAG() *DAG {
	return &DAG{adj: map[string][]string{}, nodes: map[string]bool{}}
}

func (g *DAG) AddNode(n string) { g.nodes[n] = true }

func (g *DAG) AddEdge(from, to string) {
	g.AddNode(from)
	g.AddNode(to)
	g.adj[from] = append(g.adj[from], to)
	slices.Sort(g.adj[from])
}

func (g *DAG) Nodes() []string { return slices.Sorted(maps.Keys(g.nodes)) }

// topoDFS: DFS çıkış sırasının tersi — O(V + E)
func (g *DAG) topoDFS() ([]string, bool) {
	const (
		white = 0 // hiç ziyaret edilmedi
		gray  = 1 // ziyaret ediliyor (yığında)
		black = 2 // tamamlandı
	)
	color := map[string]int{}
	var order []string
	hasCycle := false

	var dfs func(string)
	dfs = func(node string) {
		if hasCycle {
			return
		}
		color[node] = gray

		for _, next := range g.adj[node] {
			switch color[next] {
			case gray:
				hasCycle = true // GRİ bir düğüme dönmek: döngü!
				return
			case white:
				dfs(next)
			}
		}
		color[node] = black
		order = append(order, node) // çıkışta ekle
	}

	for _, n := range g.Nodes() {
		if color[n] == white {
			dfs(n)
		}
	}
	if hasCycle {
		return nil, false
	}
	slices.Reverse(order) // çıkış sırasının tersi
	return order, true
}

// findCycle: döngü varsa yolunu döndürür
func (g *DAG) findCycle() []string {
	const (
		white = 0
		gray  = 1
		black = 2
	)
	color := map[string]int{}
	parent := map[string]string{}
	var cycle []string

	var dfs func(string) bool
	dfs = func(node string) bool {
		color[node] = gray
		for _, next := range g.adj[node] {
			switch color[next] {
			case gray:
				// Döngüyü geri kur
				cycle = []string{next}
				for n := node; n != next && n != ""; n = parent[n] {
					cycle = append(cycle, n)
				}
				slices.Reverse(cycle)
				cycle = append(cycle, next)
				return true
			case white:
				parent[next] = node
				if dfs(next) {
					return true
				}
			}
		}
		color[node] = black
		return false
	}

	for _, n := range g.Nodes() {
		if color[n] == white && dfs(n) {
			return cycle
		}
	}
	return nil
}

func main() {
	// Geçerli DAG
	g := NewDAG()
	for _, d := range [][2]string{
		{"A", "B"}, {"A", "C"}, {"B", "D"}, {"C", "D"}, {"D", "E"},
	} {
		g.AddEdge(d[0], d[1])
	}

	order, ok := g.topoDFS()
	fmt.Println("DAG için DFS tabanlı topolojik sıra:")
	fmt.Println("  ", order, "| geçerli:", ok)
	fmt.Println("  döngü:", g.findCycle())

	// Döngülü graf
	cyclic := NewDAG()
	for _, d := range [][2]string{
		{"A", "B"}, {"B", "C"}, {"C", "A"}, {"C", "D"},
	} {
		cyclic.AddEdge(d[0], d[1])
	}
	order2, ok2 := cyclic.topoDFS()
	fmt.Println()
	fmt.Println("döngülü graf:")
	fmt.Println("  topolojik sıra:", order2, "| geçerli:", ok2)
	fmt.Println("  bulunan döngü:", cyclic.findCycle())

	// Kendine döngü
	self := NewDAG()
	self.AddEdge("X", "X")
	fmt.Println()
	fmt.Println("kendine döngü:")
	_, ok3 := self.topoDFS()
	fmt.Println("  geçerli:", ok3, "| döngü:", self.findCycle())

	// Bağlantısız DAG
	forest := NewDAG()
	forest.AddEdge("A", "B")
	forest.AddEdge("C", "D")
	forest.AddNode("E")
	order4, ok4 := forest.topoDFS()
	fmt.Println()
	fmt.Println("bağlantısız DAG:")
	fmt.Println("  topolojik sıra:", order4, "| geçerli:", ok4)
}
Çıktı
DAG için DFS tabanlı topolojik sıra:
   [A C B D E] | geçerli: true
  döngü: []

döngülü graf:
  topolojik sıra: [] | geçerli: false
  bulunan döngü: [B C A A]

kendine döngü:
  geçerli: false | döngü: [X X]

bağlantısız DAG:
  topolojik sıra: [E C D A B] | geçerli: true

Üç renkli döngü tespiti

Yönlü graflarda döngü tespiti, yönsüz graflardan farklıdır. Yönsüz grafta "ziyaret edilmiş bir komşuya rastlamak" döngü demekti; yönlü grafta bu yetmez.

    A ──► B
    │     │
    ▼     ▼
    C ──► D

C'den D'ye giderken D zaten ziyaret edilmiş (B üzerinden).
Ama bu bir DÖNGÜ DEĞİL! Yalnızca iki farklı yol D'ye varıyor.

Döngü, ancak HÂLÂ İŞLENMEKTE OLAN (yığında olan) bir düğüme
dönüldüğünde vardır.

ÜÇ RENK:
  BEYAZ: hiç ziyaret edilmedi
  GRİ:   ziyaret ediliyor, hâlâ çağrı yığınında
  SİYAH: tamamlandı, yığından çıktı

GRİ bir düğüme rastlamak → DÖNGÜ
SİYAH bir düğüme rastlamak → sorun yok, zaten işlendi
main.go
package main

import (
	"fmt"
	"maps"
	"slices"
)

type Digraph struct {
	adj   map[string][]string
	nodes map[string]bool
}

func NewDigraph() *Digraph {
	return &Digraph{adj: map[string][]string{}, nodes: map[string]bool{}}
}

func (g *Digraph) AddEdge(from, to string) {
	g.nodes[from], g.nodes[to] = true, true
	g.adj[from] = append(g.adj[from], to)
	slices.Sort(g.adj[from])
}

func (g *Digraph) Nodes() []string { return slices.Sorted(maps.Keys(g.nodes)) }

type color int

const (
	white color = iota
	gray
	black
)

// analyze: üç renkli DFS ile döngü ve renk geçmişini raporlar
func (g *Digraph) analyze() (bool, []string) {
	colors := map[string]color{}
	var log []string

	var dfs func(string) bool
	dfs = func(node string) bool {
		colors[node] = gray
		log = append(log, fmt.Sprintf("%s → GRİ", node))

		for _, next := range g.adj[node] {
			switch colors[next] {
			case gray:
				log = append(log, fmt.Sprintf("%s%s: GRİ düğüme dönüş → DÖNGÜ", node, next))
				return true
			case black:
				log = append(log, fmt.Sprintf("%s%s: SİYAH, zaten işlenmiş (sorun yok)", node, next))
			case white:
				if dfs(next) {
					return true
				}
			}
		}
		colors[node] = black
		log = append(log, fmt.Sprintf("%s → SİYAH", node))
		return false
	}

	for _, n := range g.Nodes() {
		if colors[n] == white {
			if dfs(n) {
				return true, log
			}
		}
	}
	return false, log
}

func main() {
	// Elmas şekli: ziyaret edilmiş düğüme rastlanır ama döngü YOK
	diamond := NewDigraph()
	for _, e := range [][2]string{{"A", "B"}, {"A", "C"}, {"B", "D"}, {"C", "D"}} {
		diamond.AddEdge(e[0], e[1])
	}

	fmt.Println("ELMAS GRAF (A→B→D, A→C→D):")
	cyc, log := diamond.analyze()
	for _, l := range log {
		fmt.Println("  ", l)
	}
	fmt.Println("  döngü var mı:", cyc)

	// Gerçek döngü
	fmt.Println()
	cyclic := NewDigraph()
	for _, e := range [][2]string{{"A", "B"}, {"B", "C"}, {"C", "A"}} {
		cyclic.AddEdge(e[0], e[1])
	}
	fmt.Println("DÖNGÜLÜ GRAF (A→B→C→A):")
	cyc2, log2 := cyclic.analyze()
	for _, l := range log2 {
		fmt.Println("  ", l)
	}
	fmt.Println("  döngü var mı:", cyc2)

	fmt.Println()
	fmt.Println("ÖNEMLİ: Yönsüz grafta 'ziyaret edilmiş komşu' döngü demekti.")
	fmt.Println("Yönlü grafta yetmez; komşunun HÂLÂ YIĞINDA olması gerekir.")
}
Çıktı
ELMAS GRAF (A→B→D, A→C→D):
   A → GRİ
   B → GRİ
   D → GRİ
   D → SİYAH
   B → SİYAH
   C → GRİ
   C → D: SİYAH, zaten işlenmiş (sorun yok)
   C → SİYAH
   A → SİYAH
  döngü var mı: false

DÖNGÜLÜ GRAF (A→B→C→A):
   A → GRİ
   B → GRİ
   C → GRİ
   C → A: GRİ düğüme dönüş → DÖNGÜ
  döngü var mı: true

ÖNEMLİ: Yönsüz grafta 'ziyaret edilmiş komşu' döngü demekti.
Yönlü grafta yetmez; komşunun HÂLÂ YIĞINDA olması gerekir.
ZamanO(V + E)AlanO(V)

Üç renkli yaklaşım, yönlü graflarla ilgili pek çok algoritmanın temelini oluşturur. Kenarları sınıflandırmayı da sağlar: gri bir düğüme giden kenar geri kenar (döngü), siyaha giden kenar ileri veya çapraz kenardır. Bu sınıflandırma, güçlü bağlı bileşen bulma ve köprü tespiti algoritmalarında kullanılır.

Kahn mı DFS mi?

İki algoritma da O(V + E) çalışır ve aynı işi yapar. Aradaki farklar pratikte seçim yapmana yardımcı olur.

ÖlçütKahnDFS tabanlı
YapıKuyruk + giriş dereceleriÖzyineleme + renkler
Döngü tespitiSıralanan düğüm sayısı kontrol edilirGri düğüme rastlanır
Katman bilgisiDoğal olarak verirVermez
Sözlük sırasına göre en küçük sıraÖncelik kuyruğuyla kolayZor
Özyineleme derinliğiYokO(V) — derin graflarda risk
Artımlı kullanımKolay (dereceleri güncelle)Zor (baştan çalıştır)

Pratik öneri: Kahn algoritmasını varsayılan olarak kullan. Döngü tespiti daha nettir, katman bilgisi bedavadır, özyineleme derinliği sorunu yoktur ve paralel planlama gibi ek ihtiyaçları doğal karşılar. DFS tabanlı sürüm, zaten DFS yapıyorsan (örneğin aynı geçişte başka bilgi de topluyorsan) daha uygundur.

Uygulama: derleme ve bağımlılık çözümleme

Paket yöneticileri, derleme araçları ve görev çalıştırıcıları bu algoritmayı her gün kullanır.

main.go
package main

import (
	"fmt"
	"maps"
	"slices"
	"strings"
)

type BuildSystem struct {
	deps    map[string][]string // hedef → ön koşulları
	targets map[string]bool
}

func NewBuildSystem() *BuildSystem {
	return &BuildSystem{deps: map[string][]string{}, targets: map[string]bool{}}
}

// Require: target, prerequisites'e bağımlıdır
func (b *BuildSystem) Require(target string, prerequisites ...string) {
	b.targets[target] = true
	for _, p := range prerequisites {
		b.targets[p] = true
	}
	b.deps[target] = append(b.deps[target], prerequisites...)
	slices.Sort(b.deps[target])
}

func (b *BuildSystem) Targets() []string { return slices.Sorted(maps.Keys(b.targets)) }

// BuildOrder: Kahn ile derleme sırası
func (b *BuildSystem) BuildOrder() ([]string, []string, error) {
	// Ters graf: ön koşul → ona bağımlı olan
	forward := map[string][]string{}
	inDeg := map[string]int{}
	for _, t := range b.Targets() {
		inDeg[t] = 0
	}
	for target, prereqs := range b.deps {
		for _, p := range prereqs {
			forward[p] = append(forward[p], target)
			inDeg[target]++
		}
	}
	for k := range forward {
		slices.Sort(forward[k])
	}

	var ready []string
	for _, t := range b.Targets() {
		if inDeg[t] == 0 {
			ready = append(ready, t)
		}
	}
	slices.Sort(ready)

	var order []string
	for len(ready) > 0 {
		cur := ready[0]
		ready = ready[1:]
		order = append(order, cur)
		for _, next := range forward[cur] {
			inDeg[next]--
			if inDeg[next] == 0 {
				ready = append(ready, next)
				slices.Sort(ready)
			}
		}
	}

	if len(order) != len(b.targets) {
		// Döngüde kalan hedefleri bul
		var stuck []string
		inOrder := map[string]bool{}
		for _, o := range order {
			inOrder[o] = true
		}
		for _, t := range b.Targets() {
			if !inOrder[t] {
				stuck = append(stuck, t)
			}
		}
		return nil, stuck, fmt.Errorf("dairesel bağımlılık: %s", strings.Join(stuck, ", "))
	}

	// Paralel gruplar
	return order, b.parallelGroups(forward), nil
}

func (b *BuildSystem) parallelGroups(forward map[string][]string) []string {
	inDeg := map[string]int{}
	for _, t := range b.Targets() {
		inDeg[t] = 0
	}
	for _, prereqs := range b.deps {
		_ = prereqs
	}
	for target, prereqs := range b.deps {
		inDeg[target] = len(prereqs)
	}

	var current []string
	for _, t := range b.Targets() {
		if inDeg[t] == 0 {
			current = append(current, t)
		}
	}

	var groups []string
	round := 1
	for len(current) > 0 {
		slices.Sort(current)
		groups = append(groups, fmt.Sprintf("aşama %d: %s", round, strings.Join(current, ", ")))
		var next []string
		for _, node := range current {
			for _, t := range forward[node] {
				inDeg[t]--
				if inDeg[t] == 0 {
					next = append(next, t)
				}
			}
		}
		current = next
		round++
	}
	return groups
}

func main() {
	// Gerçekçi bir Go projesi bağımlılık grafı
	b := NewBuildSystem()
	b.Require("app", "handlers", "config")
	b.Require("handlers", "models", "validators")
	b.Require("models", "database")
	b.Require("validators", "models")
	b.Require("database", "config")
	b.Require("config")

	fmt.Println("bağımlılıklar:")
	for _, t := range b.Targets() {
		if len(b.deps[t]) == 0 {
			fmt.Printf("  %-12s (bağımlılık yok)\n", t)
			continue
		}
		fmt.Printf("  %-12s%s\n", t, strings.Join(b.deps[t], ", "))
	}

	order, groups, err := b.BuildOrder()
	if err != nil {
		fmt.Println("hata:", err)
		return
	}

	fmt.Println()
	fmt.Println("derleme sırası:")
	for i, t := range order {
		fmt.Printf("  %d. %s\n", i+1, t)
	}

	fmt.Println()
	fmt.Println("paralel derleme aşamaları:")
	for _, g := range groups {
		fmt.Println("  " + g)
	}

	// Dairesel bağımlılık
	fmt.Println()
	broken := NewBuildSystem()
	broken.Require("a", "b")
	broken.Require("b", "c")
	broken.Require("c", "a")
	broken.Require("d")

	_, stuck, err2 := broken.BuildOrder()
	fmt.Println("bozuk proje:")
	if err2 != nil {
		fmt.Println("  hata:", err2)
		fmt.Println("  döngüde kalan hedefler:", stuck)
	}
}
Çıktı
bağımlılıklar:
  app          ← config, handlers
  config       (bağımlılık yok)
  database     ← config
  handlers     ← models, validators
  models       ← database
  validators   ← models

derleme sırası:
  1. config
  2. database
  3. models
  4. validators
  5. handlers
  6. app

paralel derleme aşamaları:
  aşama 1: config
  aşama 2: database
  aşama 3: models
  aşama 4: validators
  aşama 5: handlers
  aşama 6: app

bozuk proje:
  hata: dairesel bağımlılık: a, b, c
  döngüde kalan hedefler: [a b c]

Bu örnekteki parallelGroups fonksiyonu, gerçek derleme araçlarının nasıl paralelleştirme yaptığını gösteriyor: Aynı aşamadaki hedefler birbirine bağımlı olmadığı için eşzamanlı derlenebilir. Aşama sayısı, paralel derlemenin alabileceği en kısa süreyi (kritik yol uzunluğunu) belirler.

Dairesel bağımlılık hatası da gerçek araçlarda gördüğün mesajın ta kendisidir. Go derleyicisi de içeri alma döngülerini tam olarak böyle tespit eder ve hangi paketlerin döngüde olduğunu bildirir.

Topolojik sıralamayı tanımak

Bir problemin topolojik sıralama gerektirdiğini fark etmek, çözümün tamamını verir. Tanıman gereken işaretler şunlardır.

"Şundan önce bunu yap" kısıtları. Problem metninde ön koşul, bağımlılık, öncelik ya da sıra kısıtı geçiyorsa, bunlar yönlü kenarlardır ve topolojik sıralama aranıyordur.

"Geçerli bir sıra var mı" soruları. Cevap, grafın döngü içerip içermediğine bağlıdır. Topolojik sıralama hem cevabı hem sıranın kendisini verir.

Katmanlı planlama. "En az kaç adımda tamamlanır", "aynı anda kaç iş yapılabilir" soruları Kahn algoritmasının katman çıktısıyla yanıtlanır.

DAG üzerinde dinamik programlama. Topolojik sıra, DAG üzerinde DP yapmak için doğru işleme sırasını verir: Bir düğümü işlemeden önce tüm öncüllerinin işlenmiş olduğu garanti edilir. En uzun yol, yol sayma ve DAG'da en kısa yol problemleri bu şekilde çözülür.

Döngü tespiti tek başına. Bazen sıralamaya ihtiyacın yoktur, yalnızca "dairesel bağımlılık var mı" sorusunu yanıtlaman gerekir. Her iki algoritma da bunu bedavaya verir.

Pratikte dikkat edilecek bir nokta: Topolojik sıra tek değildir. Birden çok geçerli sıra varsa ve sen belirli bir tanesini istiyorsan (örneğin sözlük sırasına göre en küçüğü), Kahn algoritmasındaki hazır düğüm kuyruğunu bir öncelik kuyruğuna çevirmen yeterlidir. Bu ders boyunca deterministik çıktı için hazır listeyi sıralı tuttuk; gerçek uygulamalarda bunun maliyeti genelde ihmal edilebilir.

Kısmi sıralar ve genel resim

Topolojik sıralamanın arkasında, matematikte kısmi sıra adı verilen bir kavram durur ve bu kavramı anlamak algoritmayı daha derin kavramanı sağlar.

Tam sıra, herhangi iki eleman arasında kesin bir karşılaştırma tanımlar: Sayılar arasında hangisinin büyük olduğu her zaman bellidir. Kısmi sıra ise bazı çiftler için sessiz kalır: Bir projede "tasarım, geliştirmeden önce gelmeli" kuralı vardır ama "belgeleme, test etmeden önce mi sonra mı" sorusunun cevabı yoktur — ikisi birbirinden bağımsızdır.

Topolojik sıralamanın yaptığı iş tam olarak şudur: Kısmi bir sırayı, onu bozmayan bir tam sıraya genişletmek. Bu yüzden sonuç tek değildir; kısıtlanmamış her çift için birden çok seçenek vardır. Matematikte bu işleme "doğrusal genişletme" denir ve her kısmi sıranın en az bir doğrusal genişletmesi olduğu kanıtlanmıştır — bu kanıt, DAG'ların her zaman topolojik sıralanabilmesinin gerekçesidir.

Bu bakış açısı pratik sorular da yanıtlar. "Kaç farklı geçerli sıra var?" sorusu, doğrusal genişletme sayısını sormaktır ve genel durumda bu sayıyı hesaplamak zor bir problemdir. "İki eleman arasında kesin bir sıra var mı?" sorusu, birinden diğerine yol olup olmadığını kontrol etmektir. "Sıralamayı tam olarak belirlemek için kaç kısıt eklemek gerekir?" sorusu ise belirsizlikleri ölçmektir — bu dersteki alfabe çözümleme alıştırmasında tam olarak bunu yaptık.

Aynı kavram sürüm numaralandırma, izin hiyerarşileri, tip sistemlerinde alt tip ilişkileri ve dağıtık sistemlerde olay sıralaması gibi çok farklı alanlarda karşına çıkar. Hepsinde ortak olan şey, elimizde kısmi bir bilgi bulunması ve onunla tutarlı bir düzen kurmamız gerektiğidir.

Sık yapılan hatalar

  • Döngü kontrolünü atlamak. Döngülü bir grafta algoritma sessizce eksik bir sıra döndürür; sıralanan düğüm sayısını kontrol et.
  • Yönlü grafta yönsüz döngü tespiti kullanmak. Ziyaret edilmiş düğüme rastlamak yönlü grafta döngü anlamına gelmez; üç renk gerekir.
  • Kenar yönünü ters kurmak. "A, B'ye bağımlı" ifadesini modellerken oku hangi yöne çizdiğine dikkat et; sonuç tersine döner.
  • Topolojik sıranın tek olduğunu sanmak. Birden çok geçerli sıra olabilir; testlerinde tek bir sıra beklemek kırılganlık üretir.
  • Yalıtılmış düğümleri kaydetmemek. Hiç kenarı olmayan hedefler, yalnızca kenarlardan kurulan grafta görünmez.
  • Derin graflarda özyinelemeli DFS kullanmak. Girdi kontrolsüzse Kahn algoritmasını tercih et.
  • Kendine döngüyü unutmak. Bir düğümün kendisine kenarı varsa geçerli sıra yoktur; üç renkli kontrol bunu yakalar.

Alıştırmalar

Alıştırma·Ders planlama
Kolay

Ders ön koşulları verilmiş. Tüm dersleri alabilmek için geçerli bir sıra bulunup bulunmadığını belirle; bulunuyorsa sırayı ve en az kaç dönem gerektiğini raporla.

İpucu

Kahn algoritmasının katman çıktısı doğrudan dönem sayısını verir: Aynı katmandaki dersler birbirine bağımlı değildir ve aynı dönemde alınabilir.

Çözümü göster
main.go
package main

import (
	"fmt"
	"maps"
	"slices"
	"strings"
)

type Curriculum struct {
	prereqs map[string][]string
	courses map[string]bool
}

func NewCurriculum() *Curriculum {
	return &Curriculum{prereqs: map[string][]string{}, courses: map[string]bool{}}
}

func (c *Curriculum) Add(course string, prereqs ...string) {
	c.courses[course] = true
	for _, p := range prereqs {
		c.courses[p] = true
	}
	c.prereqs[course] = append(c.prereqs[course], prereqs...)
}

func (c *Curriculum) Courses() []string { return slices.Sorted(maps.Keys(c.courses)) }

// Plan: dönem dönem ders planı
func (c *Curriculum) Plan() ([][]string, bool) {
	inDeg := map[string]int{}
	unlocks := map[string][]string{}
	for _, course := range c.Courses() {
		inDeg[course] = len(c.prereqs[course])
	}
	for course, ps := range c.prereqs {
		for _, p := range ps {
			unlocks[p] = append(unlocks[p], course)
		}
	}

	var current []string
	for _, course := range c.Courses() {
		if inDeg[course] == 0 {
			current = append(current, course)
		}
	}

	var semesters [][]string
	placed := 0
	for len(current) > 0 {
		slices.Sort(current)
		semesters = append(semesters, current)
		placed += len(current)

		var next []string
		for _, course := range current {
			for _, unlocked := range unlocks[course] {
				inDeg[unlocked]--
				if inDeg[unlocked] == 0 {
					next = append(next, unlocked)
				}
			}
		}
		current = next
	}
	return semesters, placed == len(c.courses)
}

func main() {
	c := NewCurriculum()
	c.Add("Programlamaya Giriş")
	c.Add("Veri Yapıları", "Programlamaya Giriş")
	c.Add("Algoritmalar", "Veri Yapıları")
	c.Add("Ayrık Matematik")
	c.Add("Veritabanı", "Veri Yapıları")
	c.Add("İşletim Sistemleri", "Programlamaya Giriş")
	c.Add("Yapay Zekâ", "Algoritmalar", "Ayrık Matematik")
	c.Add("Dağıtık Sistemler", "İşletim Sistemleri", "Veritabanı")

	fmt.Println("ön koşullar:")
	for _, course := range c.Courses() {
		if len(c.prereqs[course]) == 0 {
			fmt.Printf("  %-24s (ön koşul yok)\n", course)
			continue
		}
		fmt.Printf("  %-24s%s\n", course, strings.Join(c.prereqs[course], ", "))
	}

	semesters, ok := c.Plan()
	fmt.Println()
	fmt.Println("plan geçerli mi:", ok)
	if ok {
		for i, s := range semesters {
			fmt.Printf("  dönem %d: %s\n", i+1, strings.Join(s, ", "))
		}
		fmt.Println("en az dönem sayısı:", len(semesters))
	}

	// Dairesel ön koşul
	fmt.Println()
	broken := NewCurriculum()
	broken.Add("A", "B")
	broken.Add("B", "C")
	broken.Add("C", "A")
	_, ok2 := broken.Plan()
	fmt.Println("dairesel ön koşullu müfredat geçerli mi:", ok2)
}
Çıktı
ön koşullar:
  Algoritmalar             ← Veri Yapıları
  Ayrık Matematik          (ön koşul yok)
  Dağıtık Sistemler        ← İşletim Sistemleri, Veritabanı
  Programlamaya Giriş      (ön koşul yok)
  Veri Yapıları            ← Programlamaya Giriş
  Veritabanı               ← Veri Yapıları
  Yapay Zekâ               ← Algoritmalar, Ayrık Matematik
  İşletim Sistemleri       ← Programlamaya Giriş

plan geçerli mi: true
  dönem 1: Ayrık Matematik, Programlamaya Giriş
  dönem 2: Veri Yapıları, İşletim Sistemleri
  dönem 3: Algoritmalar, Veritabanı
  dönem 4: Dağıtık Sistemler, Yapay Zekâ
en az dönem sayısı: 4

dairesel ön koşullu müfredat geçerli mi: false
ZamanO(V + E)AlanO(V)

Dönem sayısı, grafın en uzun yol uzunluğuna eşittir ve bu değere kritik yol denir. Ne kadar paralel çalışırsan çalış, bu sayının altına inemezsin: Zincirin her halkası bir dönem alır. Proje yönetiminde aynı hesap, projenin en erken bitiş tarihini belirler.

Alıştırma·DAG'da en uzun yol
Orta

Bir DAG'da en uzun yolu bul. Genel graflarda bu problem NP-zordur, ama DAG'da topolojik sıra sayesinde doğrusal sürede çözülür. Yolun kendisini de döndür.

İpucu

Topolojik sırada ilerlerken her düğüm için "buraya kadarki en uzun yol" değerini güncelle. Topolojik sıra, bir düğüme geldiğinde tüm öncüllerinin hesaplanmış olmasını garanti eder.

Çözümü göster
main.go
package main

import (
	"fmt"
	"maps"
	"slices"
	"strings"
)

type Edge struct {
	To     string
	Weight int
}

type DAG struct {
	adj   map[string][]Edge
	nodes map[string]bool
}

func NewDAG() *DAG {
	return &DAG{adj: map[string][]Edge{}, nodes: map[string]bool{}}
}

func (g *DAG) AddEdge(from, to string, weight int) {
	g.nodes[from], g.nodes[to] = true, true
	g.adj[from] = append(g.adj[from], Edge{to, weight})
	slices.SortFunc(g.adj[from], func(a, b Edge) int { return strings.Compare(a.To, b.To) })
}

func (g *DAG) Nodes() []string { return slices.Sorted(maps.Keys(g.nodes)) }

func (g *DAG) topoOrder() ([]string, bool) {
	inDeg := map[string]int{}
	for _, n := range g.Nodes() {
		inDeg[n] = 0
	}
	for _, edges := range g.adj {
		for _, e := range edges {
			inDeg[e.To]++
		}
	}

	var ready []string
	for _, n := range g.Nodes() {
		if inDeg[n] == 0 {
			ready = append(ready, n)
		}
	}
	slices.Sort(ready)

	var order []string
	for len(ready) > 0 {
		cur := ready[0]
		ready = ready[1:]
		order = append(order, cur)
		for _, e := range g.adj[cur] {
			inDeg[e.To]--
			if inDeg[e.To] == 0 {
				ready = append(ready, e.To)
				slices.Sort(ready)
			}
		}
	}
	return order, len(order) == len(g.nodes)
}

// longestPath: DAG'da en uzun yol — O(V + E)
func (g *DAG) longestPath() (int, []string, bool) {
	order, ok := g.topoOrder()
	if !ok {
		return 0, nil, false
	}

	dist := map[string]int{}
	parent := map[string]string{}
	for _, n := range g.Nodes() {
		dist[n] = 0
	}

	// Topolojik sırada ilerle: öncüller kesin hesaplanmış
	for _, node := range order {
		for _, e := range g.adj[node] {
			if dist[node]+e.Weight > dist[e.To] {
				dist[e.To] = dist[node] + e.Weight
				parent[e.To] = node
			}
		}
	}

	// En uzun yolun bitiş düğümünü bul
	best, end := 0, ""
	for _, n := range g.Nodes() {
		if dist[n] >= best {
			best, end = dist[n], n
		}
	}

	var path []string
	for n := end; n != ""; n = parent[n] {
		path = append(path, n)
	}
	slices.Reverse(path)
	return best, path, true
}

// countPaths: iki düğüm arasındaki yol sayısı
func (g *DAG) countPaths(from, to string) int {
	order, ok := g.topoOrder()
	if !ok {
		return 0
	}
	count := map[string]int{from: 1}
	for _, node := range order {
		for _, e := range g.adj[node] {
			count[e.To] += count[node]
		}
	}
	return count[to]
}

func main() {
	// Proje görevleri ve süreleri
	g := NewDAG()
	tasks := []struct {
		from, to string
		days     int
	}{
		{"başla", "tasarım", 3},
		{"tasarım", "arayüz", 5},
		{"tasarım", "veritabanı", 4},
		{"arayüz", "entegrasyon", 2},
		{"veritabanı", "entegrasyon", 6},
		{"entegrasyon", "test", 4},
		{"test", "yayın", 1},
		{"başla", "belgeleme", 2},
		{"belgeleme", "yayın", 1},
	}
	for _, t := range tasks {
		g.AddEdge(t.from, t.to, t.days)
	}

	fmt.Println("görev süreleri (gün):")
	for _, t := range tasks {
		fmt.Printf("  %-14s%-14s %d gün\n", t.from, t.to, t.days)
	}

	order, _ := g.topoOrder()
	fmt.Println()
	fmt.Println("topolojik sıra:", order)

	length, path, ok := g.longestPath()
	fmt.Println()
	fmt.Println("kritik yol (en uzun süre):", length, "gün | geçerli:", ok)
	fmt.Println("  ", strings.Join(path, " → "))

	fmt.Println()
	fmt.Println("başla → yayın arasındaki yol sayısı:", g.countPaths("başla", "yayın"))

	// Döngülü graf
	cyclic := NewDAG()
	cyclic.AddEdge("A", "B", 1)
	cyclic.AddEdge("B", "A", 1)
	_, _, ok2 := cyclic.longestPath()
	fmt.Println()
	fmt.Println("döngülü grafta en uzun yol hesaplanabilir mi:", ok2)
	fmt.Println("(döngüde ağırlıklar sonsuza kadar birikeceği için tanımsızdır)")
}
Çıktı
görev süreleri (gün):
  başla          → tasarım        3 gün
  tasarım        → arayüz         5 gün
  tasarım        → veritabanı     4 gün
  arayüz         → entegrasyon    2 gün
  veritabanı     → entegrasyon    6 gün
  entegrasyon    → test           4 gün
  test           → yayın          1 gün
  başla          → belgeleme      2 gün
  belgeleme      → yayın          1 gün

topolojik sıra: [başla belgeleme tasarım arayüz veritabanı entegrasyon test yayın]

kritik yol (en uzun süre): 18 gün | geçerli: true
   başla → tasarım → veritabanı → entegrasyon → test → yayın

başla → yayın arasındaki yol sayısı: 3

döngülü grafta en uzun yol hesaplanabilir mi: false
(döngüde ağırlıklar sonsuza kadar birikeceği için tanımsızdır)
ZamanO(V + E)AlanO(V)

Genel graflarda en uzun yol bulmak NP-zordur — bilinen polinom zamanlı bir algoritma yoktur. DAG'da ise doğrusal sürede çözülür. Farkı yaratan şey topolojik sıradır: Bir düğüme geldiğinde tüm öncüllerinin cevabı zaten hesaplanmıştır, bu yüzden tek geçiş yeterlidir.

Bu, DAG üzerinde dinamik programlama kalıbının tipik örneğidir ve proje yönetiminde "kritik yol yöntemi" adıyla kullanılır: Projenin en kısa tamamlanma süresi, görev grafındaki en uzun yola eşittir. Kritik yol üzerindeki her gecikme projeyi doğrudan geciktirir; diğer yollardaki görevler ise bir miktar esnekliğe sahiptir.

Alıştırma·Alfabeyi çözümleme
Zor

Bilinmeyen bir alfabeye göre sıralanmış kelimeler verilmiş. Bu kelimelerden harflerin sırasını çıkar. Sıra belirsizse ya da çelişkiliyse bunu bildir.

İpucu

Ardışık iki kelimeyi karşılaştır; ilk farklı harf çifti bir sıra kısıtı verir. Bu kısıtlardan bir graf kur ve topolojik sıralama yap. Çelişki döngü demektir; belirsizlik ise birden çok geçerli sıra olması.

Çözümü göster
main.go
package main

import (
	"fmt"
	"slices"
)

// deriveAlphabet: sıralı kelimelerden harf sırasını çıkarır
func deriveAlphabet(words []string) (string, error) {
	// Tüm harfleri topla
	letters := map[rune]bool{}
	for _, w := range words {
		for _, r := range w {
			letters[r] = true
		}
	}

	adj := map[rune][]rune{}
	inDeg := map[rune]int{}
	for r := range letters {
		inDeg[r] = 0
	}

	// Ardışık kelime çiftlerinden kısıt çıkar
	for i := 0; i+1 < len(words); i++ {
		a, b := []rune(words[i]), []rune(words[i+1])
		minLen := min(len(a), len(b))

		found := false
		for j := range minLen {
			if a[j] != b[j] {
				// a[j] < b[j] kısıtı
				if !slices.Contains(adj[a[j]], b[j]) {
					adj[a[j]] = append(adj[a[j]], b[j])
					inDeg[b[j]]++
				}
				found = true
				break
			}
		}
		// Geçersiz durum: "abc" sonra "ab" gelemez
		if !found && len(a) > len(b) {
			return "", fmt.Errorf("geçersiz sıra: %q, %q'dan sonra gelemez", words[i+1], words[i])
		}
	}

	// Kahn algoritması
	var ready []rune
	for r := range letters {
		if inDeg[r] == 0 {
			ready = append(ready, r)
		}
	}
	slices.Sort(ready)

	var order []rune
	multiplePossible := false
	for len(ready) > 0 {
		if len(ready) > 1 {
			multiplePossible = true // birden çok seçenek: sıra belirsiz
		}
		cur := ready[0]
		ready = ready[1:]
		order = append(order, cur)

		next := slices.Clone(adj[cur])
		slices.Sort(next)
		for _, n := range next {
			inDeg[n]--
			if inDeg[n] == 0 {
				ready = append(ready, n)
				slices.Sort(ready)
			}
		}
	}

	if len(order) != len(letters) {
		return "", fmt.Errorf("çelişkili kısıtlar: harf sırasında döngü var")
	}
	result := string(order)
	if multiplePossible {
		return result, fmt.Errorf("sıra belirsiz (birden çok geçerli sıra var); biri: %q", result)
	}
	return result, nil
}

// verify: bulunan alfabeye göre kelimeler sıralı mı
func verify(words []string, alphabet string) bool {
	rank := map[rune]int{}
	for i, r := range alphabet {
		rank[r] = i
	}
	less := func(a, b string) bool {
		ra, rb := []rune(a), []rune(b)
		for i := 0; i < len(ra) && i < len(rb); i++ {
			if ra[i] != rb[i] {
				return rank[ra[i]] < rank[rb[i]]
			}
		}
		return len(ra) <= len(rb)
	}
	for i := 0; i+1 < len(words); i++ {
		if !less(words[i], words[i+1]) {
			return false
		}
	}
	return true
}

func main() {
	cases := [][]string{
		{"wrt", "wrf", "er", "ett", "rftt"},
		{"z", "x"},
		{"z", "x", "z"}, // çelişki
		{"abc", "ab"},   // geçersiz sıra
		{"ab", "ac", "bc"},
		{"kedi", "kera", "köpek"},
	}

	for _, words := range cases {
		fmt.Printf("kelimeler: %v\n", words)
		alphabet, err := deriveAlphabet(words)
		switch {
		case alphabet == "" && err != nil:
			fmt.Println("  hata:", err)
		case err != nil:
			fmt.Println("  uyarı:", err)
			fmt.Println("  doğrulama:", verify(words, alphabet))
		default:
			fmt.Printf("  alfabe: %q (tek geçerli sıra)\n", alphabet)
			fmt.Println("  doğrulama:", verify(words, alphabet))
		}
		fmt.Println()
	}

	fmt.Println("Kısıt çıkarma kuralı:")
	fmt.Println("Ardışık iki kelimenin İLK FARKLI harfi bir sıra kısıtı verir.")
	fmt.Println("Sonraki harfler hakkında hiçbir bilgi vermez!")
	fmt.Println()
	fmt.Println("Örnek: \"wrt\" < \"wrf\" → yalnızca t < f")
	fmt.Println("(w ve r eşit olduğu için onlar hakkında bilgi yok)")
}
Çıktı
kelimeler: [wrt wrf er ett rftt]
  alfabe: "wertf" (tek geçerli sıra)
  doğrulama: true

kelimeler: [z x]
  alfabe: "zx" (tek geçerli sıra)
  doğrulama: true

kelimeler: [z x z]
  hata: çelişkili kısıtlar: harf sırasında döngü var

kelimeler: [abc ab]
  hata: geçersiz sıra: "ab", "abc"'dan sonra gelemez

kelimeler: [ab ac bc]
  alfabe: "abc" (tek geçerli sıra)
  doğrulama: true

kelimeler: [kedi kera köpek]
  uyarı: sıra belirsiz (birden çok geçerli sıra var); biri: "adeikprö"
  doğrulama: true

Kısıt çıkarma kuralı:
Ardışık iki kelimenin İLK FARKLI harfi bir sıra kısıtı verir.
Sonraki harfler hakkında hiçbir bilgi vermez!

Örnek: "wrt" < "wrf" → yalnızca t < f
(w ve r eşit olduğu için onlar hakkında bilgi yok)
ZamanO(toplam karakter + alfabe²)AlanO(alfabe²)

Bu problem, topolojik sıralamanın gerçek bir keşif aracı olarak kullanılabileceğini gösteriyor. Verilen sıralı kelime listesi, harfler arasındaki kısmi sıra bilgisini içerir; topolojik sıralama bu kısmi bilgiyi tam bir sıraya çevirir.

Üç farklı sonuç durumu olduğuna dikkat et. Tek geçerli sıra varsa, algoritmanın her adımında hazır listede tek eleman bulunur. Birden çok geçerli sıra varsa, bazı adımlarda hazır listede birden fazla eleman olur — bu, o harfler arasında hiçbir kısıt bulunmadığı anlamına gelir. Çelişki varsa graf döngü içerir ve hiçbir geçerli sıra yoktur.

En sık yapılan hata, ardışık kelimelerden yalnızca ilk farklı harfin bilgi verdiğini gözden kaçırmaktır. Sonraki harfler hakkında hiçbir şey söylenemez; onlar için kısıt eklemek yanlış sonuçlar üretir.

Kısa sınav

Kısa sınav

Topolojik sıralama hangi graf türü için tanımlıdır?

Kahn algoritması hangi bilgiyi kullanır?

Kahn algoritmasında döngü nasıl tespit edilir?

DFS tabanlı topolojik sıralama nasıl elde edilir?

Yönlü grafta döngü tespitinde üç renk neden gereklidir?

Bir DAG'ın topolojik sıralaması kaç farklı olabilir?

Özet

  • Topolojik sıralama, yönlü kenarların belirttiği "önce-sonra" kısıtlarını bozmayan bir düğüm sıralamasıdır.
  • Yalnızca yönlü döngüsüz graflar (DAG) için tanımlıdır; döngü varsa geçerli sıra yoktur.
  • Kahn algoritması giriş derecelerini kullanır: derecesi sıfır olanı al, komşularının derecesini azalt.
  • Kahn algoritması döngü tespitini ve katman bilgisini bedavaya verir; katman sayısı kritik yol uzunluğudur.
  • DFS tabanlı sürüm, çıkış sırasının tersini alır ve üç renkli işaretlemeyle döngüyü yakalar.
  • Yönlü grafta döngü, yalnızca gri (hâlâ yığında olan) bir düğüme dönüldüğünde vardır.
  • Topolojik sıra tek değildir; kısıtlanmamış düğümlerin sırası serbesttir.
  • Topolojik sıra, DAG üzerinde dinamik programlama için doğru işleme sırasını verir: en uzun yol, yol sayma, kritik yol.
  • Derleme sistemleri, paket yöneticileri, ders planlama ve görev çizelgeleme bu algoritmanın günlük uygulamalarıdır.
Bu dersi bitirdin mi?
İlerlemen bu tarayıcıda saklanır.