En graf 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å.
| Representasjon | Plass | Kant finnes? | Naboer til |
|---|---|---|---|
| Naboliste | |||
| Nabomatrise |
For tynne grafer () er nabolista standardvalget, og det er den som gir den fine grensa 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 , så alle naboene til , 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 før noe på avstand — og det er nettopp derfor
dist[] ender opp med korteste vei målt i antall kanter.
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) Interaktiv visualisering krever JavaScript.
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 når vi går inn i visit(u), og en ferdigtid når vi
forlater den. Disse tidene bærer overraskende mye struktur.
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 Interaktiv visualisering krever JavaScript.
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: blir en etterkommer av i DFS-treet hvis og bare hvis det på tidspunktet finnes en sti fra til 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 , kan vi klassifisere den ut fra fargen til :
| Kanttype | Når DFS ser den | Betydning |
|---|---|---|
| Trekant | er hvit (uoppdaget) | fører til en ny node i DFS-treet |
| Tilbakekant | er grå (på stakken) | peker mot en ane — avslører en sykel |
| Foroverkant | er svart, etterkommer | snarvei til en allerede ferdig etterkommer |
| Krysskant | er svart, ikke i slekt | mellom to ferdige greiner |
Topologisk sortering
I en rettet asyklisk graf (DAG) er en topologisk sortering en lineær rekkefølge av nodene slik at hver kant 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 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å for å få ferdigtidene, og én gang på den transponerte grafen (alle kanter snudd), der vi starter nye trær i synkende ferdigtid. Hvert tre i andre runde er én komponent. Hele jobben er fortsatt .
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 , faller Dijkstras algoritme rett ut av samme mønster (se korteste vei). Det er denne abstraksjonen som binder grafalgoritmene sammen.