Tilbake
5.2

5.2 BFS og DFS — traversering

Bredde-først (BFS) og dybde-først (DFS) traversering, DFS-full over alle komponenter, og det faste skillet DFS fra én node O(|E|) vs. DFS-full O(|V|+|E|).

55 min
9 oppgaver
BFSDFStraversering
Din fremgang i kapitlet
0 / 9 oppgaver

Forkunnskaper

- kap. 5.1 — hele kapitlet. Du trenger nabolister,
grad, sti, sykel og sammenhengende komponent, og du trenger vanen med å
oppgi hvilken representasjon du antar.
- kap. 1.2 — løkketelling. Kjøretidene her kommer av å
telle hvor mange ganger hver node og hver kant behandles.

To resultater fra kap. 5.1 brukes så tett at det er
verdt å ha dem foran seg:

- Å gå gjennom hele grafen fra nabolister koster O(V+E)O(|V| + |E|) — leddet
V|V| er der fordi du må innom hver node, også de uten naboer.
- Å ramse opp naboene til én node koster O(grad(v))O(\text{grad}(v)) med naboliste,
men O(V)O(|V|) med nabomatrise. Derfor forutsetter alle kjøretidene i dette
kapitlet nabolister, og derfor sier vi det høyt hver gang.

Notasjons- og pseudokodeliste

Løkke 1 — bredde-først: køen og lagene (ca. 15 min)

En beskjed skal ut i et nettverk av kolleger. Du sender den til alle du
kjenner direkte. De sender den videre til alle de kjenner, som ikke
allerede har fått den. Og så videre.

Etter første runde har alle som er ett ledd unna, fått beskjeden. Etter andre
runde alle som er to ledd unna. Rundene er lag, og de kommer i rekkefølge
— det er hele ideen bak bredde-først-søk.

For at det skal virke mekanisk, trenger du to ting. Du trenger et sted å
legge dem som har fått beskjeden, men ikke har sendt den videre ennå — det er
køen. Og du trenger å huske hvem som allerede har fått den — det er
merket v.besokt. Uten merket sender kollegene beskjeden fram og tilbake
til hverandre for alltid, og programmet ditt stopper aldri.

Traversering

Å besøke nodene i en graf systematisk, slik at hver node som kan nås fra
startnoden, besøkes nøyaktig én gang.

De to traverseringene i pensum er bredde-først (BFS) og dybde-først
(DFS). De besøker nøyaktig de samme nodene — det er bare rekkefølgen som
skiller dem, og hvilke ekstra opplysninger man får på kjøpet.

Merket `v.besokt`

Flagget som sier at noden allerede er sett, slik at den ikke behandles igjen.

Uten det går enhver traversering av en graf med en sykel i uendelig løkke:
A sender til B, B sender tilbake til A, A sender til B igjen. Merket er også
det som gjør at hver kant behandles et konstant antall ganger, og dermed det
som gir kjøretiden O(V+E)O(|V| + |E|). Glemt merke er derfor både en
korrekthetsfeil og en kjøretidsfeil på samme tid.

Kø (først inn, først ut)

Samlingen bredde-først bruker: Ko.settInn(v) legger bakerst,
Ko.taUt() tar ut forrest. Begge er O(1)O(1).

Rekkefølgen «først inn, først ut» er nøyaktig det som gjør at nodene kommer
ut lagvis: alle på avstand 1 er lagt inn før noen på avstand 2, og køen
bevarer den rekkefølgen. Bytt køen mot en stakk, og du får dybde-først i
stedet — samme algoritme, annen samling, helt annen rekkefølge.

Bredde-først-søk (BFS)

Traverseringen som besøker nodene i økende avstand fra startnoden, målt i
antall kanter, ved hjelp av en kø.

Kjøretid: O(V+E)O(|V| + |E|) med nabolister.
Gir på kjøpet: korteste vei fra startnoden til alle noder den når, målt i
antall kanter, i en uvektet graf.
Krever: et besokt-merke per node, og en kø.

📜Pseudokode-kontrakt: `BFS`
Antagelser om representasjon. Grafen G = (V, E) er gitt som
nabolister; naboene til en node u kan ramses opp i O(grad(u))O(\text{grad}(u)).
Hver node v har feltene v.besokt og v.avstand. Køen støtter
settInn og taUt i O(1)O(1).

Prebetingelse: s er en node i V.
Postbetingelse: hver node som kan nås fra s, har besokt = sant og
avstand lik antall kanter på den korteste veien fra s. Noder som ikke kan
nås, har besokt = usant og avstand = uendelig.

Procedure BFS(G, s)
  Input:  graf G = (V, E) som nabolister, startnode s
  Output: hver node som kan naas fra s er merket, med avstand i antall kanter
  for hver node v i V:
      v.besokt = usant
      v.avstand = uendelig
  s.besokt = sant
  s.avstand = 0
  Ko = tom ko
  Ko.settInn(s)
  while Ko er ikke tom:
      u = Ko.taUt()
      for hver nabo v av u:
          if v.besokt er usant:
              v.besokt = sant
              v.avstand = u.avstand + 1
              Ko.settInn(v)

Invarianten i én setning: køen inneholder til enhver tid bare noder fra
høyst to nabolag — først alle på avstand dd, så alle på avstand d+1d+1 — og
derfor tas nodene ut i ikke-synkende avstand fra s.

Detaljen som er lett å bomme på: noden merkes besokt når den legges
inn
i køen, ikke når den tas ut. Merker du den først ved uttak, kan den
samme noden legges inn flere ganger — og da er kjøretiden ikke lenger
O(V+E)O(|V| + |E|).

Kjøretid: O(V+E)O(|V| + |E|). Initialiseringsløkka er O(V)O(|V|); hver node
legges inn og tas ut av køen nøyaktig én gang, og for hver node ramses
naboene opp én gang, som til sammen er O(E)O(|E|) over hele grafen.

✏️Eksempel 1: Bredde-først med full kø-tilstand

Vannverket fra kap. 5.1 har åtte pumpestasjoner
(grafen U1). Kjør BFS fra A og før køen steg for steg. Oppgi
besøksrekkefølgen og hvilket lag hver stasjon havner i.

A: B, C
B: A, D
C: A, D, E
D: B, C, F
E: C, F
F: D, E, G
G: F, H
H: G

Sporingstavlen. Naboene tas i alfabetisk rekkefølge, så sporingen er
entydig. «Legges i køen» er de naboene som ikke allerede var merket.

StegTas ut av køenDybdeLegges i køenKøen etter stegetBesøkt
start---AA
1A0B, CB, CA, B, C
2B1DC, DA, B, C, D
3C1ED, EA, B, C, D, E
4D2FE, FA, B, C, D, E, F
5E2ingenFA, B, C, D, E, F
6F3GGA, B, C, D, E, F, G
7G4HHA, B, C, D, E, F, G, H
8H5ingen(tom)A, B, C, D, E, F, G, H

Sluttsvaret — dette er det du leverer:
besoeksrekkefolge: A -> B -> C -> D -> E -> F -> G -> H
lag 0: A
lag 1: B, C
lag 2: D, E
lag 3: F
lag 4: G
lag 5: H
Alle åtte nodene ble nådd, og ingen ble utelatt.
Se på steg 5. E tas ut, og ingenting legges inn — F var allerede merket
i steg 4, da D la den inn. Det er merket som gjør at F ikke havner i køen
to ganger, og det er nøyaktig derfor kjøretiden holder seg lineær.
Se på lagene. H ligger i lag 5, altså fem kanter fra A. Det er ikke
bare rekkefølgen bredde-først gir deg — det er den korteste avstanden
målt i kanter, og du har den gratis i v.avstand.
Fellenote. Fella her er å tro at rekkefølgen nodene legges inn i

køen er svaret. Besøksrekkefølgen er rekkefølgen de tas ut, og de to

faller bare sammen fordi køen er «først inn, først ut». Bytter du til en

stakk, faller de fra hverandre umiddelbart.

📝Oppgave 1

(Innstegsoppgave, sjanger E — håndkjøring, altså at du utfører operasjonen
steg for steg og oppgir sluttilstanden.) Kjør BFS på den samme grafen
U1, men start i F i stedet for A.

A: B, C
B: A, D
C: A, D, E
D: B, C, F
E: C, F
F: D, E, G
G: F, H
H: G

a) Før sporingstavlen med køen etter hvert steg.
b) Oppgi besøksrekkefølgen og lagene.
c) Hvor mange kanter er det fra F til A?

📜Bredde-først gir korteste vei i antall kanter

I en uvektet graf gir BFS(G, s) for hver node vv den korteste veien
fra ss til vv målt i antall kanter. Verdien står i v.avstand.

Grunnen i én setning: køen tømmes lagvis, så en node kan ikke tas ut før
alle noder med lavere avstand er tatt ut — og den får derfor avstanden til
den første noden som nådde den.

De to grensene du må kjenne, og som spørres om:

- Bare i uvektet graf. Har kantene vekter, er det ikke lenger antall
kanter som er interessant, og bredde-først kan gi feil svar: en vei med to
kanter kan være lengre enn en vei med fem. Vektet korteste vei krever andre
verktøy, og de kommer i Del 6.
- Dybde-først gjør det ikke. Dybde-først finner en sti, men ikke
nødvendigvis den korteste — den går så dypt den kan før den snur, og kan
komme fram til en node langs en lang omvei. Å hevde at dybde-først gir
korteste vei, er et fast trekkpunkt.

BFS-lag

Mengden av noder som ligger nøyaktig dd kanter fra startnoden. Lag 0 er
startnoden alene, lag 1 er dens naboer, lag 2 er naboenes naboer som ikke
allerede er sett, og så videre.

Lagene er ikke en egenskap ved grafen alene, men ved grafen og
startnoden: bytter du start, blir lagene helt andre. Kommer en oppgave med
«innen kk ledd» eller «lagvis utsending», er lagene svaret.

✏️Eksempel 2: Lagene i rutenettet

De ni fuktsensorene i veksthuset (grafen U8 fra
kap. 5.1) står i et rutenett på tre ganger tre. Kjør
BFS fra A, og oppgi lagene. Hvor mange kanter er det fra A til I?

A: B, D
B: A, C, E
C: B, F
D: A, E, G
E: B, D, F, H
F: C, E, I
G: D, H
H: E, G, I
I: F, H

Sporingstavlen:

StegTas ut av køenDybdeLegges i køenKøen etter stegetBesøkt
start---AA
1A0B, DB, DA, B, D
2B1C, ED, C, EA, B, C, D, E
3D1GC, E, GA, B, C, D, E, G
4C2FE, G, FA, B, C, D, E, F, G
5E2HG, F, HA, B, C, D, E, F, G, H
6G2ingenF, HA, B, C, D, E, F, G, H
7F3IH, IA, B, C, D, E, F, G, H, I
8H3ingenIA, B, C, D, E, F, G, H, I
9I4ingen(tom)A, B, C, D, E, F, G, H, I

Sluttsvaret:
besoeksrekkefolge: A -> B -> D -> C -> E -> G -> F -> H -> I
lag 0: A
lag 1: B, D
lag 2: C, E, G
lag 3: F, H
lag 4: I
Det er fire kanter fra A til I, og det er nøyaktig det figuren over
viser.
Se på steg 6. G tas ut og legger ingenting inn: naboene D og H er
allerede merket — D fra starten, H i steg 5 av E. Det er verdt å legge
merke til at H fikk lag 3 fra E, ikke fra G, selv om G også er nabo. Den
første noden som når H, bestemmer avstanden, og fordi køen tømmes
lagvis, er den første også den nærmeste.
Se på lagstørrelsene: 1, 2, 3, 2, 1. I et rutenett vokser lagene til
midten og krymper etterpå. Det er den samme formen som gjør bredde-først

til riktig verktøy for «hvor mange er innen kk ledd?»-oppgaver.

Fellenote. Fella her er felle #6 i bokas feilregister i sin

kjøretidsform: å tro at fordi vi startet i én node, koster kjøringen
O(E)O(|E|). Denne prosedyren initialiserer alle nodene først, og da er

kjøretiden O(V+E)O(|V| + |E|) uansett hvor få noder som faktisk nås.

📝Oppgave 2
Sjanger H

Skriv en prosedyre AntallInnenLag(G, s, k) som teller
hvor mange noder som ligger høyst kk kanter fra s. Oppgi antagelser og
kjøretid.

Du skal ikke finne på en ny traversering — bygg på BFS.

Løkke 2 — dybde-først med rekursjon (ca. 13 min)

— naturlig pausepunkt —

Bytt ut beskjeden i nettverket med en person som utforsker et tunnelsystem.
Hun går inn i den første sidegangen hun ser, og i den første sidegangen der
igjen, og fortsetter så langt hun kommer. Først når hun står i en blindvei,
snur hun og går tilbake til forrige veikryss for å prøve neste sidegang.

Det er dybde-først-søk. Der bredde-først brer seg utover i ringer, borer
dybde-først seg ned.

Og legg merke til hva «gå tilbake til forrige veikryss» er: det er nøyaktig
det et rekursivt kall gjør når det returnerer. Derfor er dybde-først
kortest og klarest skrevet rekursivt — og rekursjon er pensum i IN2010, så
det er formen du skal bruke. Den iterative varianten med en eksplisitt stakk
kommer i neste løkke, og den gjør det samme.

Dybde-først-søk (DFS)

Traverseringen som følger én vei så langt den kan før den snur, og først da
prøver neste ubesøkte nabo.

Kjøretid: O(E)O(|E|) fra én node; O(V+E)O(|V| + |E|) når den kjøres over
hele grafen (DFSFull).
Gir på kjøpet: rekkefølgen kallene returnerer i (ferdigrekkefølgen),
som blir avgjørende i kap. 5.4.
Gir IKKE: korteste vei. Det er bredde-først som gjør det.

`DFSVisit`

Den rekursive kjernen i dybde-først: merk noden, og kall deg selv på hver
ubesøkte nabo.

Kallet DFSVisit(G, u) merker alt som kan nås fra u, og ingenting annet.
Kjøretiden fra én node er O(E)O(|E|) — prosedyren har ingen løkke over alle
noder, så arbeidet følger kantene den faktisk går langs. Skal hele grafen
dekkes, må den kalles fra DFSFull, og da blir det O(V+E)O(|V| + |E|).

📜Pseudokode-kontrakt: `DFSVisit`
Antagelser om representasjon. Grafen G = (V, E) er gitt som
nabolister; hver node v har feltet v.besokt. Naboene til u kan ramses
opp i O(grad(u))O(\text{grad}(u)).

Prebetingelse: u.besokt er usant, og alle noder har fått besokt
satt (det gjøres av den som kaller — se DFSFull under).
Postbetingelse: hver node som kan nås fra u gjennom umerkede noder,
har besokt = sant. Ingen andre noder er endret.

Procedure DFSVisit(G, u)
  Input:  graf G = (V, E) som nabolister, node u med u.besokt = usant
  Output: alle noder som kan naas fra u er merket besokt
  u.besokt = sant
  for hver nabo v av u:
      if v.besokt er usant:
          DFSVisit(G, v)

Grunnideen i én setning: merket settes før de rekursive kallene, så
en node aldri kan bli besøkt to ganger, og traverseringen kan ikke gå i ring
selv om grafen har sykler.

Rekursjonen har en kostnad du skal nevne: kallstakken blir like dyp som
den lengste veien traverseringen følger, altså O(V)O(|V|) i verste fall. På en
graf som er en lang kjede, ligger alle nodene på stakken samtidig.

Kjøretid: O(E)O(|E|) fra én node. Prosedyren har ingen løkke over VV;
arbeidet er å gå langs kantene, og hver kant ses høyst to ganger — én gang
fra hver ende. Merk godt: dette er kjøretiden for ett kall fra én
node. Skal hele grafen dekkes, er svaret et annet — se DFSFull.

✏️Eksempel 3: Dybde-først med kallstakken ført steg for steg

Kjør rekursivt dybde-først fra A i vannverksgrafen U1, og før
kallstakken etter hvert kall og hver retur. Oppgi besøksrekkefølgen og
ferdigrekkefølgen.

A: B, C
B: A, D
C: A, D, E
D: B, C, F
E: C, F
F: D, E, G
G: F, H
H: G

«Kallstakken» er de DFSVisit-kallene som er i gang samtidig. Naboene tas
i alfabetisk rekkefølge.

StegHandlingKallstakken etter stegetBesøkt
1kall DFSVisit(A)AA
2kall DFSVisit(B)A, BA, B
3ser på naboen A fra B — allerede besøkt, hopper overA, BA, B
4kall DFSVisit(D)A, B, DA, B, D
5ser på naboen B fra D — allerede besøkt, hopper overA, B, DA, B, D
6kall DFSVisit(C)A, B, D, CA, B, C, D
7ser på naboen A fra C — allerede besøkt, hopper overA, B, D, CA, B, C, D
8ser på naboen D fra C — allerede besøkt, hopper overA, B, D, CA, B, C, D
9kall DFSVisit(E)A, B, D, C, EA, B, C, D, E
10ser på naboen C fra E — allerede besøkt, hopper overA, B, D, C, EA, B, C, D, E
11kall DFSVisit(F)A, B, D, C, E, FA, B, C, D, E, F
12ser på naboen D fra F — allerede besøkt, hopper overA, B, D, C, E, FA, B, C, D, E, F
13ser på naboen E fra F — allerede besøkt, hopper overA, B, D, C, E, FA, B, C, D, E, F
14kall DFSVisit(G)A, B, D, C, E, F, GA, B, C, D, E, F, G
15ser på naboen F fra G — allerede besøkt, hopper overA, B, D, C, E, F, GA, B, C, D, E, F, G
16kall DFSVisit(H)A, B, D, C, E, F, G, HA, B, C, D, E, F, G, H
17ser på naboen G fra H — allerede besøkt, hopper overA, B, D, C, E, F, G, HA, B, C, D, E, F, G, H
18DFSVisit(H) returnererA, B, D, C, E, F, GA, B, C, D, E, F, G, H
19DFSVisit(G) returnererA, B, D, C, E, FA, B, C, D, E, F, G, H
20DFSVisit(F) returnererA, B, D, C, EA, B, C, D, E, F, G, H
21DFSVisit(E) returnererA, B, D, CA, B, C, D, E, F, G, H
22DFSVisit(C) returnererA, B, DA, B, C, D, E, F, G, H
23ser på naboen F fra D — allerede besøkt, hopper overA, B, DA, B, C, D, E, F, G, H
24DFSVisit(D) returnererA, BA, B, C, D, E, F, G, H
25DFSVisit(B) returnererAA, B, C, D, E, F, G, H
26ser på naboen C fra A — allerede besøkt, hopper overAA, B, C, D, E, F, G, H
27DFSVisit(A) returnerer(tom)A, B, C, D, E, F, G, H

Sluttsvaret:
besoeksrekkefolge (naar noden foerst naas): A -> B -> D -> C -> E -> F -> G -> H
ferdigrekkefolge (naar kallet returnerer):  H -> G -> F -> E -> C -> D -> B -> A
maks dybde paa kallstakken: 8
Sammenlign med eksempel 1, som var bredde-først på nøyaktig den samme
grafen fra nøyaktig den samme noden. Der ble rekkefølgen
A, B, C, D, E, F, G, H; her blir den A, B, D, C, E, F, G, H. Forskjellen
er C og D som har byttet plass — og den kommer av at dybde-først følger B
videre til D i stedet for å gå tilbake til A for å hente C.
Se på steg 23. Vi er tilbake i D etter at hele grenen under C er
ferdig, og D har fortsatt en nabo igjen å se på: F. Den er besøkt for
lengst — via C og E. Det er dette som gjør at kanter kan bli sett fra
begge ender, og det er derfor kjøretidsargumentet sier «hver kant høyst to
ganger».
Se på maks dybde: 8. Alle åtte nodene lå på kallstakken samtidig i
steg 16. Det er kostnaden ved rekursjon, og på en graf som er en lang
kjede er den O(V)O(|V|).
Fellenote. Fella her er å forveksle besøksrekkefølgen med
ferdigrekkefølgen. De er ikke omvendte av hverandre — se selv: den ene
slutter på H, den andre begynner på H, men midtpartiene stemmer ikke
overens. Ferdigrekkefølgen blir avgjørende i
kap. 5.4, så vend deg til å føre begge.

📝Oppgave 3
Sjanger E

Kjør rekursivt dybde-først fra A i veksthusgrafen U8, og før
kallstakken.

A: B, D
B: A, C, E
C: B, F
D: A, E, G
E: B, D, F, H
F: C, E, I
G: D, H
H: E, G, I
I: F, H

a) Oppgi besøksrekkefølgen.
b) Oppgi ferdigrekkefølgen.
c) Hva er den største dybden kallstakken når?
d) Sammenlign med bredde-først på samme graf fra samme node (eksempel 2).
Hvilken node havner lengst unna sin plass?

Løkke 3 — den samme traverseringen uten rekursjon (ca. 8 min)

Kallstakken i eksempel 3 var ikke magi. Den var en stakk: kall legges
øverst, returer tas av øverst. Bytter du den ut med en stakk du styrer selv,
får du nøyaktig den samme traverseringen uten et eneste rekursivt kall.

Det er verdt å kunne begge formene. Den rekursive er kortere og er den du bør
skrive på eksamen; den iterative viser hva som faktisk skjer, og er svaret
hvis en oppgave uttrykkelig ber om en variant uten rekursjon.

Stakk (sist inn, først ut)

Samlingen dybde-først bruker: Stakk.leggPa(v) legger øverst,
Stakk.taAv() tar av øverst. Begge er O(1)O(1).

Forskjellen fra en kø er hele forskjellen mellom dybde-først og
bredde-først. Stakken gir deg alltid den sist oppdagede noden tilbake, og
det er nøyaktig «gå videre innover før du snur». Kallstakken i et rekursivt
program er den samme datastrukturen, bare styrt av kjøresystemet.

Iterativ dybde-først med eksplisitt stakk

Varianten som erstatter rekursjonen med en stakk du styrer selv.

To detaljer skiller den fra den rekursive: naboene må legges på i omvendt
rekkefølge for at den første naboen skal behandles først, og en node kan
ligge på stakken flere ganger samtidig. Derfor må besokt sjekkes én
gang til når noden tas av. Kjøretiden er den samme, O(V+E)O(|V| + |E|) når alle
noder initialiseres, og fordelen er at kallstakken ikke kan renne over på en
svært dyp graf.

📜Den iterative varianten av dybde-først
Antagelser om representasjon. Som for DFSVisit: nabolister, og
v.besokt per node. Stakken har leggPa og taAv i O(1)O(1).

Prebetingelse: s er en node i V.
Postbetingelse: de samme nodene er merket som etter DFSVisit(G, s), og
de er besøkt i den samme rekkefølgen.

Procedure DFSIterativ(G, s)
  Input:  graf G = (V, E) som nabolister, startnode s
  Output: alle noder som kan naas fra s er merket besokt
  for hver node v i V:
      v.besokt = usant
  Stakk = tom stakk
  Stakk.leggPa(s)
  while Stakk er ikke tom:
      u = Stakk.taAv()
      if u.besokt er usant:
          u.besokt = sant
          for hver nabo v av u i omvendt rekkefolge:
              if v.besokt er usant:
                  Stakk.leggPa(v)

Grunnideen i én setning: stakken gir alltid tilbake den sist oppdagede
noden, og det er nøyaktig det et rekursivt kall gjør — bare at her ser du
stakken.

Hvorfor if u.besokt er usant gjentas ved uttak: en node kan bli lagt på
stakken av flere naboer før den blir tatt av. Uten sjekken ville den blitt
besøkt to ganger. Dette er den eneste virkelige forskjellen mot den rekursive
formen, og den er en fast kilde til feil.

Kjøretid: O(V+E)O(|V| + |E|). Initialiseringen er O(V)O(|V|), og hver kant kan
føre til høyst én legging på stakken fra hver ende, altså O(E)O(|E|) leggingar
og uttak til sammen.

✏️Eksempel 4: Samme rekkefølge, men med stakken synlig

Kjør DFSIterativ fra A i U1 — den samme grafen og den samme
startnoden som i eksempel 3 — og før stakken etter hvert steg. Blir
besøksrekkefølgen den samme som med rekursjon?

A: B, C
B: A, D
C: A, D, E
D: B, C, F
E: C, F
F: D, E, G
G: F, H
H: G

Naboene legges på i omvendt rekkefølge, slik at den første naboen tas først.
Stakken skrives fra bunnen mot toppen, så det siste navnet er øverst.

StegHandlingStakken etter stegetBesøkt
start-A(ingen)
1besøk A; legg på stakken: B, CC, BA
2besøk B; legg på stakken: DC, DA, B
3besøk D; legg på stakken: C, FC, F, CA, B, D
4besøk C; legg på stakken: EC, F, EA, B, C, D
5besøk E; legg på stakken: FC, F, FA, B, C, D, E
6besøk F; legg på stakken: GC, F, GA, B, C, D, E, F
7besøk G; legg på stakken: HC, F, HA, B, C, D, E, F, G
8besøk H; legg på stakken: ingenC, FA, B, C, D, E, F, G, H
9F tas av stakken, men er alt besøkt — hopp overCA, B, C, D, E, F, G, H
10C tas av stakken, men er alt besøkt — hopp over(tom)A, B, C, D, E, F, G, H

Sluttsvaret:
besoeksrekkefolge: A -> B -> D -> C -> E -> F -> G -> H
Ja — nøyaktig den samme som i eksempel 3. De to formene er samme
algoritme.
Se på steg 3. Stakken er C, F, C — noden C ligger der to ganger.
Den ble lagt på av A i steg 1 og av D i steg 3, og ingen av gangene var
den besøkt ennå. I den rekursive formen kan dette ikke skje, fordi
sjekken gjøres rett før kallet.
Se på steg 9 og 10. Der høster vi konsekvensen: to noder tas av
stakken og forkastes fordi de allerede er besøkt. Det er nøyaktig det
if u.besokt er usant ved uttak er til for. Fjerner du den linja, blir C
besøkt to ganger, og en algoritme som teller eller summerer noe per node,

gir feil svar.

Fellenote. Fella her er å legge naboene på i vanlig rekkefølge og

regne med samme resultat som med rekursjon. Da tas siste nabo først, og
besøksrekkefølgen blir en annen — fortsatt et gyldig dybde-først-søk, men

ikke det samme som fasiten hvis oppgaven har oppgitt en naborekkefølge.

📝Oppgave 4
Sjanger H

Om den iterative varianten av dybde-først:

a) Hvorfor må naboene legges på stakken i omvendt rekkefølge?
b) Hvorfor sjekkes besokt én gang til når noden tas av stakken, når den
allerede ble sjekket før den ble lagt på?
c) En medstudent foreslår å droppe merkingen helt og i stedet sjekke om
noden allerede ligger på stakken. Hva går galt?
d) Hva er kjøretiden, og hvorfor er den den samme som for den rekursive
formen?

Løkke 4 — hele grafen, og det faste kjøretidsskillet (ca. 10 min)

— naturlig pausepunkt —

Både BFS og DFSVisit starter i én node og merker det de kan nå derfra.
Men i kap. 5.1 så du en graf med tre atskilte biter, og
en node uten en eneste kant. En traversering fra A ville aldri sett dem.

Skal hele grafen dekkes, må du derfor gå over alle nodene og starte en ny
traversering hver gang du finner en som ikke er merket. Det er DFSFull.

Og det er her det faste trekkpunktet i faget ligger. De to prosedyrene har
forskjellig kjøretid, og forskjellen er ikke pedantisk — den er
eksplisitt påpekt i sensorenes veiledninger, og den er grunnen til at feilen
har fått et eget nummer i bokas feilregister.

`DFSFull`

Løkka som kaller DFSVisit fra hver node som ennå ikke er merket, slik at
alle noder blir besøkt — også de som ligger i andre biter av grafen, og
de som ikke har en eneste kant.

Kjøretid: O(V+E)O(|V| + |E|). Leddet V|V| kommer fra de to løkkene over alle
nodene: én for å nullstille merkene, én for å starte traverseringene. Uten
DFSFull dekker dybde-først bare den biten startnoden ligger i.

📜Pseudokode-kontrakt: `DFSFull`
Antagelser om representasjon. Nabolister; v.besokt per node. DFSVisit
er som i kontrakten over.

Prebetingelse: ingen.
Postbetingelse: hver node i V har besokt = sant, og hver node er
besøkt nøyaktig én gang.

Procedure DFSFull(G)
  Input:  graf G = (V, E) som nabolister
  Output: hver node i G er besokt noeyaktig en gang
  for hver node v i V:
      v.besokt = usant
  for hver node v i V:
      if v.besokt er usant:
          DFSVisit(G, v)

Grunnideen i én setning: hvert kall til DFSVisit fra den ytre løkka
merker nøyaktig én sammenhengende bit av grafen, og løkka fortsetter til
ingen umerkede noder er igjen.

Kjøretid: O(V+E)O(|V| + |E|). De to løkkene over VV gir V|V|-leddet, og
alle DFSVisit-kallene til sammen ser hver kant høyst to ganger, som gir
E|E|-leddet. Merk at det ytre if gjør at hver node behandles én gang, ikke
én gang per kall.

Legg merke til én ting til: hver gang den ytre løkka faktisk starter et
kall, har den funnet en ny bit av grafen. Å telle hvor mange ganger det skjer
er derfor å telle bitene — men den oppgaven hører til
kap. 5.3, og der skrives den ut i sin helhet.

📜Det faste skillet: dybde-først fra én node mot dybde-først over hele grafen
Dette er kapitlets viktigste enkeltresultat, og det du oftest får trekk
for.

ProsedyreHva den gjørKjøretid
DFSVisit(G, u)merker det som kan nås fra én node uO(E)O(|E|)
DFSFull(G)merker alle noder, i alle biter av grafenO(V+E)O(|V| + |E|)
BFS(G, s)merker det som kan nås fra s, med avstander, og initialiserer alle noderO(V+E)O(|V| + |E|)

Hvorfor forskjellen er reell. DFSVisit inneholder ingen løkke over
VV. Den gjør bare arbeid der den faktisk går, og arbeidet følger kantene.
DFSFull gjør to ting i tillegg: den nullstiller merket på hver node, og
den prøver å starte fra hver node. Begge løkkene er O(V)O(|V|), og de

kjøres uansett hvor få kanter grafen har.
Det skarpeste eksempelet: en graf med en million noder og null kanter.
Her er E=0|E| = 0. DFSVisit fra én node gjør nesten ingenting. DFSFull
bruker likevel en million steg, fordi den må innom hver eneste node. Uten
V|V|-leddet ville påstanden vært at hele arbeidet er gratis.

Fellenote. Dette er felle #6 i bokas feilregister: å oppgi O(E)O(|E|)
for en algoritme som i virkeligheten går over alle nodene, eller
O(V+E)O(|V| + |E|) for et enkelt DFSVisit-kall. Kontrollen som tar to
sekunder:
har algoritmen din en løkke for hver node v i V? Da er V|V|

med i kjøretiden. Har den det ikke, skal V|V| ikke stå der.
Og motsatt vei, som er like viktig: å skrive O(V+E)O(|V| + |E|) overalt «for
sikkerhets skyld» er ikke gratis. Kjøretiden skal matche algoritmen du
faktisk skrev.

Dybde-først fra én node — kjøretiden
O(E)O(|E|). DFSVisit(G, u) har ingen løkke over alle nodene; arbeidet er å
følge kantene, og hver kant ses høyst to ganger.

Dette gjelder ett kall fra én node, med merkene allerede initialisert
av den som kaller. Å oppgi O(V+E)O(|V| + |E|) her er felle #6 i den ene
retningen.

Dybde-først over hele grafen — kjøretiden
O(V+E)O(|V| + |E|). DFSFull(G) har to løkker over alle nodene — én for å
nullstille merkene og én for å starte traverseringer — og de kjøres uansett
hvor få kanter grafen har.

Å oppgi O(E)O(|E|) her er felle #6 i den andre retningen, og det er den
varianten sensor ser oftest. Kontrollen: finnes det en løkke over VV i
koden din, skal V|V| stå i kjøretiden.

Bredde-først — kjøretiden
O(V+E)O(|V| + |E|) med nabolister. Initialiseringen av alle noder er O(V)O(|V|),
hver node legges inn og tas ut av køen nøyaktig én gang, og naboene ramses
opp én gang per node, som til sammen er O(E)O(|E|).

Med nabomatrise i stedet blir den O(V2)O(|V|^2), fordi hver naboiterasjon da
koster O(V)O(|V|). Det er derfor antagelsen om representasjon må stå i
besvarelsen: den samme algoritmen har to ulike kjøretider.

📝Oppgave 5
Sjanger F og…

En graf har V=1000000|V| = 1\,000\,000 noder og
E=12|E| = 12 kanter. Alle kantene ligger mellom de tolv første nodene; resten av
grafen er isolerte noder uten kanter.

a) Omtrent hvor mange steg bruker DFSVisit(G, A), der A er en av de tolv
nodene med kanter?
b) Omtrent hvor mange steg bruker DFSFull(G)?
c) Skriv opp kjøretiden for hver av dem, og forklar hvilket ledd som
dominerer.
d) En besvarelse oppgir O(E)O(|E|) for DFSFull. Hvor galt er det her?

📝Oppgave 6
Eksamensnivå, sjanger H

Et tunnelsystem under et fjellanlegg er kartlagt
som en urettet graf: nodene er kryss, kantene er tunneler. Skriv en algoritme
som avgjør om det finnes en vei fra kryss s til kryss t. Oppgi kjøretid.

Deretter: en kollega foreslår i stedet å prøve alle mulige veier fra s og se
om noen ender i t. Hva er galt med det forslaget?

Løkke 5 — samme graf, to rekkefølger (ca. 9 min)

Til slutt skal de to settes ved siden av hverandre på nøyaktig den samme
grafen, fra nøyaktig den samme noden. Det er den formen spørsmålet kommer i
på eksamen: «oppgi besøksrekkefølgen for bredde-først og for dybde-først».

Poenget er ikke å pugge to rekkefølger, men å se hvorfor de skiller lag:
køen tvinger fram lagene, stakken tvinger fram dybden.

✏️Eksempel 5: Begge traverseringene på grafen i figuren

Grafen i figuren over har seks noder og sju kanter. Kjør både bredde-først
og rekursivt dybde-først fra A, og forklar hvor de to skiller lag.

A: B, C
B: A, D, E
C: A, E
D: B, F
E: B, C, F
F: D, E

Bredde-først fra A:

StegTas ut av køenDybdeLegges i køenKøen etter stegetBesøkt
start---AA
1A0B, CB, CA, B, C
2B1D, EC, D, EA, B, C, D, E
3C1ingenD, EA, B, C, D, E
4D2FE, FA, B, C, D, E, F
5E2ingenFA, B, C, D, E, F
6F3ingen(tom)A, B, C, D, E, F

besoeksrekkefolge: A -> B -> C -> D -> E -> F
lag 0: A
lag 1: B, C
lag 2: D, E
lag 3: F
Rekursivt dybde-først fra A:
StegHandlingKallstakken etter stegetBesøkt
1kall DFSVisit(A)AA
2kall DFSVisit(B)A, BA, B
3ser på naboen A fra B — allerede besøkt, hopper overA, BA, B
4kall DFSVisit(D)A, B, DA, B, D
5ser på naboen B fra D — allerede besøkt, hopper overA, B, DA, B, D
6kall DFSVisit(F)A, B, D, FA, B, D, F
7ser på naboen D fra F — allerede besøkt, hopper overA, B, D, FA, B, D, F
8kall DFSVisit(E)A, B, D, F, EA, B, D, E, F
9ser på naboen B fra E — allerede besøkt, hopper overA, B, D, F, EA, B, D, E, F
10kall DFSVisit(C)A, B, D, F, E, CA, B, C, D, E, F
11ser på naboen A fra C — allerede besøkt, hopper overA, B, D, F, E, CA, B, C, D, E, F
12ser på naboen E fra C — allerede besøkt, hopper overA, B, D, F, E, CA, B, C, D, E, F
13DFSVisit(C) returnererA, B, D, F, EA, B, C, D, E, F
14ser på naboen F fra E — allerede besøkt, hopper overA, B, D, F, EA, B, C, D, E, F
15DFSVisit(E) returnererA, B, D, FA, B, C, D, E, F
16DFSVisit(F) returnererA, B, DA, B, C, D, E, F
17DFSVisit(D) returnererA, BA, B, C, D, E, F
18ser på naboen E fra B — allerede besøkt, hopper overA, BA, B, C, D, E, F
19DFSVisit(B) returnererAA, B, C, D, E, F
20ser på naboen C fra A — allerede besøkt, hopper overAA, B, C, D, E, F
21DFSVisit(A) returnerer(tom)A, B, C, D, E, F

besoeksrekkefolge (naar noden foerst naas): A -> B -> D -> F -> E -> C
ferdigrekkefolge (naar kallet returnerer):  C -> E -> F -> D -> B -> A
maks dybde paa kallstakken: 6
Hvor de skiller lag: i det andre steget. Begge har akkurat besøkt A og
B. Bredde-først har C liggende i køen fra første steg, og køen gir den ut
før noe som ble oppdaget senere — så C blir nummer tre. Dybde-først går
rett videre fra B til D, og kommer ikke tilbake til C før alt annet er

utforsket — så C blir nummer seks, altså sist.

Legg merke til avstandene. Bredde-først forteller deg at F ligger tre

kanter fra A. Dybde-først besøkte F som nummer tre i rekkefølgen, men det
sier ingenting om avstand — den kom dit langs A -> B -> D -> F, som
tilfeldigvis er tre kanter, men i en annen graf kunne den kommet dit langs
en lang omvei. Rekkefølgen i dybde-først er ikke avstand.
Fellenote. Fella her er å bruke dybde-først når oppgaven ber om
korteste vei. Det er bredde-først som gir korteste vei, og bare målt i
antall kanter i en uvektet graf.

📝Oppgave 7
Sjanger E

Kjør den iterative varianten av dybde-først fra A på den
samme grafen som i eksempel 5, og før stakken.

A: B, C
B: A, D, E
C: A, E
D: B, F
E: B, C, F
F: D, E

a) Sett opp sporingstavlen.
b) Blir besøksrekkefølgen den samme som med rekursjon?
c) Hvor mange ganger tas en node av stakken uten å bli besøkt, og hvorfor?

📝Oppgave 8
Eksamensnivå, sjanger H

En kommune skal varsle innbyggere ved et
vannbrudd. Ledningsnettet er en urettet, uvektet graf der nodene er kummer og
kantene er ledninger. Bruddet skjer i kum s.

Skriv en algoritme som for hver kum oppgir hvor mange ledninger den ligger
unna s, og som melder fra om kummer som ikke kan nås i det hele tatt. Oppgi
antagelser og kjøretid, og begrunn hvorfor kjøretiden er den lavest mulige.

📝Oppgave 9
Eksamensnivå, sjanger H

En kollega har levert denne prosedyren, som skal
besøke alle nodene i en graf:

Procedure BesokAlt(G)
  Input:  graf G = (V, E) som nabolister
  Output: alle noder besokt
  for hver node v i V:
      DFSVisit(G, v)
  Kjoeretid: O(|E|)

der DFSVisit er som i kapitlets kontrakt.

a) Finn alle feilene.
b) Vis konkret hva som går galt på grafen med nabolistene
A: B og B: A.
c) Skriv en korrekt versjon med riktig kjøretid.

Begrepsbank

Begrepsbanken er flashcard-/repetisjonsstoff — den gjentar det du nettopp har
lest. Hopp trygt over ved førstegangslesing; tidsanslaget for kapitlet gjelder
kjernestoffet.

Besøksrekkefølge

Rekkefølgen nodene først nås i, altså rekkefølgen de merkes besokt i.

For bredde-først er dette rekkefølgen nodene tas ut av køen; for dybde-først
er det rekkefølgen kallene startes i. Det er dette svaret en
håndkjøringsoppgave som regel ber om — les oppgaveteksten nøye, for den kan
også be om ferdigrekkefølgen.

Ferdigrekkefølge

Rekkefølgen de rekursive DFSVisit-kallene returnerer i, altså
rekkefølgen nodene blir helt ferdige i.

Den er ikke det motsatte av besøksrekkefølgen — sammenlign de to i eksempel
3, så ser du at bare endene stemmer. Ferdigrekkefølgen er ikke pynt: den er
selve grunnlaget for algoritmen i kap. 5.4, så venn deg
til å føre begge kolonnene når du håndkjører.

Rekursjonsdybden i dybde-først

Hvor mange DFSVisit-kall som er aktive samtidig, altså hvor høy
kallstakken blir. Den er O(V)O(|V|) i verste fall.

På en graf som er en lang kjede, ligger alle nodene på stakken samtidig — i
eksempel 3 nådde dybden 8 av 8 noder. Det er den ene praktiske grunnen til å
velge den iterative varianten: en eksplisitt stakk kan bli like stor, men den
sprenger ikke kjøresystemets kallstakk.

Dybde-først gir en sti, ikke den korteste

Dybde-først finner en vei fra startnoden til hver node den når, men ikke
nødvendigvis den korteste — den går så dypt den kan før den snur, og kan
derfor nå en node langs en lang omvei.

Trenger du korteste vei målt i antall kanter, er svaret bredde-først, og bare
i en uvektet graf. Å hevde det motsatte er et fast trekkpunkt, og det er lett
å gjøre fordi rekkefølgen dybde-først besøker nodene i, ser ut som om den
betyr noe. Den gjør ikke det.

Uendelig løkke uten merking

En traversering uten besokt-merke stopper aldri i en graf med en sykel: A
sender til B, B sender tilbake til A, og de to holder på i det uendelige.

Merket er derfor ikke en optimalisering, men en betingelse for at algoritmen
i det hele tatt terminerer. Det er også det som gjør at hver node behandles
én gang og hver kant et konstant antall ganger, altså det som gir kjøretiden
O(V+E)O(|V| + |E|).

Repetisjon — kapitlet på ett kort

Dette kapitlet er skrevet av Anthropics toppmodeller (Claude Opus og Claude Fable) og er foreløpig ikke manuelt gjennomgått — kvalitetskontrollen gjøres av uavhengige KI-agenter, og innmeldte feil rettes fortløpende. Funnet en feil? Meld fra, så retter vi den. Les mer om hvordan innholdet lages.

Skolesaga er en uavhengig læringsressurs og er ikke tilknyttet eller godkjent av Universitetet i Oslo. Dette er ikke offisielt studiemateriell. Les mer.