TDT4120 Algoritmer og datastrukturer Interaktiv pensumguide · H2026
Fremgang
0/14
08
Grafer

Traversering av grafer

Kjernepensum ~45 min lesing · Traversering · BFS · DFS

En graf G=(V,E)G = (V, E) er nodemengde pluss kanter. Nesten alt vi gjør med grafer starter med å utforske dem systematisk: oppdage hver node og hver kant nøyaktig så mange ganger vi trenger. Traversering er ryggraden under korteste vei, spenntrær, topologisk sortering og mye mer — så det lønner seg å ha de to grunnmønstrene, bredde-først og dybde-først, helt i fingrene.

Grafrepresentasjoner

To representasjoner dominerer, og valget styrer kjøretiden til alt vi bygger oppå.

RepresentasjonPlassKant (u,v)(u,v) finnes?Naboer til uu
NabolisteΘ(V+E)\Theta(V + E)O(degu)O(\deg u)Θ(degu)\Theta(\deg u)
NabomatriseΘ(V2)\Theta(V^2)Θ(1)\Theta(1)Θ(V)\Theta(V)
Naboliste er kompakt for tynne grafer og lar oss gå gjennom naboene i grad-tid; matrise svarer på kant-spørringer i konstant tid, men koster Theta(V2)\\Theta(V^2) plass.

For tynne grafer (EV2E \ll V^2) er nabolista standardvalget, og det er den som gir den fine grensa O(V+E)O(V+E) for både BFS og DFS: hver node behandles én gang og hver kant følges et konstant antall ganger.

Bredde-først-søk

BFS utforsker grafen lagvis: først kilden ss, så alle naboene til ss, så alle deres uoppdagede naboer, og så videre. En FIFO-kø holder fronten av oppdagede-men-ikke-ferdige noder. Fordi køen er først-inn-først-ut, behandles alltid alt på avstand kk før noe på avstand k+1k+1 — og det er nettopp derfor dist[] ender opp med korteste vei målt i antall kanter.

bfs.py
def bfs(G, s):
  for u in G.V:
      dist[u] = INF
  dist[s] = 0
  Q = [s]
  while Q:
      u = Q.pop(0)
      for v in G.adj[u]:
          if dist[v] != INF:
              continue
          dist[v] = dist[u] + 1
          Q.append(v)
Bredde-først-søk

Interaktiv visualisering krever JavaScript.

0 / 0

Kilden er fiolett; fronten (køen) er oransje, ferdige noder grønne. Sub-tallet under hver node er dist[] = korteste antall kanter fra s. Tre-kantene (grønne) danner BFS-treet. Spill av eller stega med piltastene.

Dybde-først-søk

DFS gjør det motsatte av BFS: i stedet for å vifte utover, dykker det så dypt det kan langs én gren, og rygger først tilbake når grenen er kjørt tørr. Fronten er her rekursjonsstakken (LIFO). Hver node får to tidsstempler: en oppdagelsestid d[u]d[u] når vi går inn i visit(u), og en ferdigtid f[u]f[u] når vi forlater den. Disse tidene bærer overraskende mye struktur.

dfs.py
def dfs(adj, source):
  d, f = {}, {}            # oppdagelses- og ferdigtid
  time = 0

  def visit(u):
      nonlocal time
      time += 1; d[u] = time
      for v in adj[u]:
          if v not in d:
              visit(v)
      time += 1; f[u] = time

  visit(source)
  return d, f
Dybde-først-søk

Interaktiv visualisering krever JavaScript.

0 / 0

Den aktive noden (toppen av stakken) er rød/accent, resten av stakken oransje, ferdige noder grønne. Sub-merket d/f er oppdagelses- og ferdigtid. Grønne kanter er trekanter; røde stiplede er tilbakekanter til en node som fortsatt ligger på stakken (en sykel).

Et nært beslektet resultat er hvit-sti-teoremet: vv blir en etterkommer av uu i DFS-treet hvis og bare hvis det på tidspunktet d[u]d[u] finnes en sti fra uu til vv som bare går gjennom hvite (uoppdagede) noder. Sammen forklarer disse to hvorfor DFS-trærne fanger nøyaktig nåbarheten i grafen.

Kantklassifisering

Når DFS følger en kant (u,v)(u, v), kan vi klassifisere den ut fra fargen til vv:

KanttypeNår DFS ser denBetydning
Trekantvv er hvit (uoppdaget)fører til en ny node i DFS-treet
Tilbakekantvv er grå (på stakken)peker mot en ane — avslører en sykel
Foroverkantvv er svart, etterkommersnarvei til en allerede ferdig etterkommer
Krysskantvv er svart, ikke i slektmellom to ferdige greiner
Kantklassifisering i en rettet graf. En graf er asyklisk nøyaktig når DFS ikke finner noen tilbakekant.

Topologisk sortering

I en rettet asyklisk graf (DAG) er en topologisk sortering en lineær rekkefølge av nodene slik at hver kant (u,v)(u, v) peker framover. Algoritmen er forbløffende enkel: kjør DFS, og legg hver node fremst i en liste når den blir ferdig. Synkende ferdigtid f[u]f[u] gir nettopp en gyldig topologisk rekkefølge.

Sterkt sammenhengende komponenter

I en rettet graf er en sterkt sammenhengende komponent en maksimal mengde noder der alle kan nå alle. Standardalgoritmen kjører DFS to ganger: én gang på GG for å få ferdigtidene, og én gang på den transponerte grafen GG^\top (alle kanter snudd), der vi starter nye trær i synkende ferdigtid. Hvert tre i andre runde er én komponent. Hele jobben er fortsatt O(V+E)O(V + E).

Ett mønster, mange algoritmer

BFS og DFS skiller seg bare i hvilken datastruktur fronten er: en FIFO-kø gir BFS, en LIFO-stakk gir DFS. Bytter vi fronten ut med en vilkårlig prioritetskø — ta alltid ut det «beste» elementet etter en nøkkel — får vi et generelt traverseringsskjelett. Velger vi nøkkelen til å være avstandsestimatet d[v]d[v], faller Dijkstras algoritme rett ut av samme mønster (se korteste vei). Det er denne abstraksjonen som binder graf­algoritmene sammen.

Sjekk deg selv

Hvorfor finner BFS korteste veier i uvektede grafer?