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|).
Grafrepresentasjon og traversering står i alle sju settene i arkivet (7 av
7, altså 100 %), og de to algoritmene i dette kapitlet — bredde-først og
dybde-først — er byggeklossene i så godt som hver eneste grafoppgave i resten
av boka. Lærer du dem ordentlig nå, får du igjen for det seks kapitler på
rad.
Kapitlet trener to sjangre:
- Sjanger H — grafalgoritmer i pseudokode, altså at du skriver algoritmen
selv og oppgir kjøretiden. Denne sjangeren er på hvert eneste sett, med
2–3 oppgaver, og det er her karakteren avgjøres.
- Sjanger F — matrise- og tabellavkryssing, altså at du krysser av
hvilken kjøretid som hører til hvilken variant.
Ett punkt veier tyngre enn alt annet i dette kapitlet, og det er et
eksplisitt trekkpunkt i sensorenes veiledninger: dybde-først-søk fra én
node er , mens dybde-først over hele grafen er .
Å blande de to er felle #6 i bokas feilregister — den mest lønnsomme
enkeltfeilen å kvitte seg med i hele Del 5. Den har sin egen boks lenger
nede, og den kommer igjen i oppgavene.
Det andre faste trekkpunktet: dybde-først gir deg en sti, men ikke den
korteste. Det er bredde-først som gjør det, og bare i en uvektet graf.
Prioritet: høyeste.
Slik er kapitlet lagt opp (55 min): fem løkker med hvert sitt tidsanslag,
og pausepunkter underveis. Traverseringene håndkjøres først og skrives i
pseudokode etterpå — i den rekkefølgen, fordi du skriver bedre pseudokode når
du har ført køen med blyant først.
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 — leddet
er der fordi du må innom hver node, også de uten naboer.
- Å ramse opp naboene til én node koster med naboliste,
men med nabomatrise. Derfor forutsetter alle kjøretidene i dette
kapitlet nabolister, og derfor sier vi det høyt hver gang.
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.
Å 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.
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 . Glemt merke er derfor både en
korrekthetsfeil og en kjøretidsfeil på samme tid.
Samlingen bredde-først bruker: Ko.settInn(v) legger bakerst,Ko.taUt() tar ut forrest. Begge er .
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.
Traverseringen som besøker nodene i økende avstand fra startnoden, målt i
antall kanter, ved hjelp av en kø.
Kjøretid: 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ø.
G = (V, E) er gitt somnabolister; naboene til en node
u kan ramses opp i .Hver node
v har feltene v.besokt og v.avstand. Køen støttersettInn og taUt i .Prebetingelse: s er en node i V.
Postbetingelse: hver node som kan nås fra s, har besokt = sant ogavstand 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 , så alle på avstand — 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
.
Kjøretid: . Initialiseringsløkka er ; 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 over hele grafen.
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: Gentydig. «Legges i køen» er de naboene som ikke allerede var merket.
| Steg | Tas ut av køen | Dybde | Legges i køen | Køen etter steget | Besøkt |
|---|---|---|---|---|---|
| start | - | - | - | A | A |
| 1 | A | 0 | B, C | B, C | A, B, C |
| 2 | B | 1 | D | C, D | A, B, C, D |
| 3 | C | 1 | E | D, E | A, B, C, D, E |
| 4 | D | 2 | F | E, F | A, B, C, D, E, F |
| 5 | E | 2 | ingen | F | A, B, C, D, E, F |
| 6 | F | 3 | G | G | A, B, C, D, E, F, G |
| 7 | G | 4 | H | H | A, B, C, D, E, F, G, H |
| 8 | H | 5 | ingen | (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: HAlle å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.
(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: Ga) 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?
I en uvektet graf gir BFS(G, s) for hver node den korteste veien
fra til 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.
Mengden av noder som ligger nøyaktig 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 ledd» eller «lagvis utsending», er lagene svaret.
De ni fuktsensorene i veksthuset (grafen U8 fra
kap. 5.1) står i et rutenett på tre ganger tre. KjørBFS 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| Steg | Tas ut av køen | Dybde | Legges i køen | Køen etter steget | Besøkt |
|---|---|---|---|---|---|
| start | - | - | - | A | A |
| 1 | A | 0 | B, D | B, D | A, B, D |
| 2 | B | 1 | C, E | D, C, E | A, B, C, D, E |
| 3 | D | 1 | G | C, E, G | A, B, C, D, E, G |
| 4 | C | 2 | F | E, G, F | A, B, C, D, E, F, G |
| 5 | E | 2 | H | G, F, H | A, B, C, D, E, F, G, H |
| 6 | G | 2 | ingen | F, H | A, B, C, D, E, F, G, H |
| 7 | F | 3 | I | H, I | A, B, C, D, E, F, G, H, I |
| 8 | H | 3 | ingen | I | A, B, C, D, E, F, G, H, I |
| 9 | I | 4 | ingen | (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: IDet er fire kanter fra A til I, og det er nøyaktig det figuren overviser.
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 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
. Denne prosedyren initialiserer alle nodene først, og da er
kjøretiden uansett hvor få noder som faktisk nås.
Skriv en prosedyre AntallInnenLag(G, s, k) som teller
hvor mange noder som ligger høyst 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.
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: fra én node; 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.
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 — 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 .
G = (V, E) er gitt somnabolister; hver node
v har feltet v.besokt. Naboene til u kan ramsesopp i .
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å i verste fall. På en
graf som er en lang kjede, ligger alle nodene på stakken samtidig.
Kjøretid: fra én node. Prosedyren har ingen løkke over ;
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.
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.
| Steg | Handling | Kallstakken etter steget | Besøkt |
|---|---|---|---|
| 1 | kall DFSVisit(A) | A | A |
| 2 | kall DFSVisit(B) | A, B | A, B |
| 3 | ser på naboen A fra B — allerede besøkt, hopper over | A, B | A, B |
| 4 | kall DFSVisit(D) | A, B, D | A, B, D |
| 5 | ser på naboen B fra D — allerede besøkt, hopper over | A, B, D | A, B, D |
| 6 | kall DFSVisit(C) | A, B, D, C | A, B, C, D |
| 7 | ser på naboen A fra C — allerede besøkt, hopper over | A, B, D, C | A, B, C, D |
| 8 | ser på naboen D fra C — allerede besøkt, hopper over | A, B, D, C | A, B, C, D |
| 9 | kall DFSVisit(E) | A, B, D, C, E | A, B, C, D, E |
| 10 | ser på naboen C fra E — allerede besøkt, hopper over | A, B, D, C, E | A, B, C, D, E |
| 11 | kall DFSVisit(F) | A, B, D, C, E, F | A, B, C, D, E, F |
| 12 | ser på naboen D fra F — allerede besøkt, hopper over | A, B, D, C, E, F | A, B, C, D, E, F |
| 13 | ser på naboen E fra F — allerede besøkt, hopper over | A, B, D, C, E, F | A, B, C, D, E, F |
| 14 | kall DFSVisit(G) | A, B, D, C, E, F, G | A, B, C, D, E, F, G |
| 15 | ser på naboen F fra G — allerede besøkt, hopper over | A, B, D, C, E, F, G | A, B, C, D, E, F, G |
| 16 | kall DFSVisit(H) | A, B, D, C, E, F, G, H | A, B, C, D, E, F, G, H |
| 17 | ser på naboen G fra H — allerede besøkt, hopper over | A, B, D, C, E, F, G, H | A, B, C, D, E, F, G, H |
| 18 | DFSVisit(H) returnerer | A, B, D, C, E, F, G | A, B, C, D, E, F, G, H |
| 19 | DFSVisit(G) returnerer | A, B, D, C, E, F | A, B, C, D, E, F, G, H |
| 20 | DFSVisit(F) returnerer | A, B, D, C, E | A, B, C, D, E, F, G, H |
| 21 | DFSVisit(E) returnerer | A, B, D, C | A, B, C, D, E, F, G, H |
| 22 | DFSVisit(C) returnerer | A, B, D | A, B, C, D, E, F, G, H |
| 23 | ser på naboen F fra D — allerede besøkt, hopper over | A, B, D | A, B, C, D, E, F, G, H |
| 24 | DFSVisit(D) returnerer | A, B | A, B, C, D, E, F, G, H |
| 25 | DFSVisit(B) returnerer | A | A, B, C, D, E, F, G, H |
| 26 | ser på naboen C fra A — allerede besøkt, hopper over | A | A, B, C, D, E, F, G, H |
| 27 | DFSVisit(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: 8Sammenlign med eksempel 1, som var bredde-først på nøyaktig den sammegrafen 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 .
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.
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, Ha) 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.
Samlingen dybde-først bruker: Stakk.leggPa(v) legger øverst,Stakk.taAv() tar av øverst. Begge er .
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.
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, når alle
noder initialiseres, og fordelen er at kallstakken ikke kan renne over på en
svært dyp graf.
DFSVisit: nabolister, ogv.besokt per node. Stakken har leggPa og taAv i .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: . Initialiseringen er , og hver kant kan
føre til høyst én legging på stakken fra hver ende, altså leggingar
og uttak til sammen.
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: GNaboene 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.
| Steg | Handling | Stakken etter steget | Besøkt |
|---|---|---|---|
| start | - | A | (ingen) |
| 1 | besøk A; legg på stakken: B, C | C, B | A |
| 2 | besøk B; legg på stakken: D | C, D | A, B |
| 3 | besøk D; legg på stakken: C, F | C, F, C | A, B, D |
| 4 | besøk C; legg på stakken: E | C, F, E | A, B, C, D |
| 5 | besøk E; legg på stakken: F | C, F, F | A, B, C, D, E |
| 6 | besøk F; legg på stakken: G | C, F, G | A, B, C, D, E, F |
| 7 | besøk G; legg på stakken: H | C, F, H | A, B, C, D, E, F, G |
| 8 | besøk H; legg på stakken: ingen | C, F | A, B, C, D, E, F, G, H |
| 9 | F tas av stakken, men er alt besøkt — hopp over | C | A, B, C, D, E, F, G, H |
| 10 | C 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 -> HJa — nøyaktig den samme som i eksempel 3. De to formene er sammealgoritme.
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 Cbesø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.
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.
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: . Leddet kommer fra de to løkkene over alle
nodene: én for å nullstille merkene, én for å starte traverseringene. UtenDFSFull dekker dybde-først bare den biten startnoden ligger i.
v.besokt per node. DFSVisiter 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: . De to løkkene over gir -leddet, og
alle DFSVisit-kallene til sammen ser hver kant høyst to ganger, som gir
-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.
for.
| Prosedyre | Hva den gjør | Kjøretid |
|---|---|---|
DFSVisit(G, u) | merker det som kan nås fra én node u | |
DFSFull(G) | merker alle noder, i alle biter av grafen | |
BFS(G, s) | merker det som kan nås fra s, med avstander, og initialiserer alle noder |
Hvorfor forskjellen er reell.
DFSVisit inneholder ingen løkke over. 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, ogden prøver å starte fra hver node. Begge løkkene er , og de
kjøres uansett hvor få kanter grafen har.
Det skarpeste eksempelet: en graf med en million noder og null kanter.
Her er . DFSVisit fra én node gjør nesten ingenting. DFSFull
bruker likevel en million steg, fordi den må innom hver eneste node. Uten
-leddet ville påstanden vært at hele arbeidet er gratis.
Fellenote. Dette er felle #6 i bokas feilregister: å oppgi
for en algoritme som i virkeligheten går over alle nodene, eller
for et enkelt DFSVisit-kall. Kontrollen som tar to
sekunder: har algoritmen din en løkke for hver node v i V? Da er
med i kjøretiden. Har den det ikke, skal ikke stå der.
Og motsatt vei, som er like viktig: å skrive overalt «for
sikkerhets skyld» er ikke gratis. Kjøretiden skal matche algoritmen du
faktisk skrev.
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 her er felle #6 i den ene
retningen.
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 her er felle #6 i den andre retningen, og det er den
varianten sensor ser oftest. Kontrollen: finnes det en løkke over i
koden din, skal stå i kjøretiden.
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 .
Med nabomatrise i stedet blir den , fordi hver naboiterasjon da
koster . Det er derfor antagelsen om representasjon må stå i
besvarelsen: den samme algoritmen har to ulike kjøretider.
En graf har noder og
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 for DFSFull. Hvor galt er det her?
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.
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| Steg | Tas ut av køen | Dybde | Legges i køen | Køen etter steget | Besøkt |
|---|---|---|---|---|---|
| start | - | - | - | A | A |
| 1 | A | 0 | B, C | B, C | A, B, C |
| 2 | B | 1 | D, E | C, D, E | A, B, C, D, E |
| 3 | C | 1 | ingen | D, E | A, B, C, D, E |
| 4 | D | 2 | F | E, F | A, B, C, D, E, F |
| 5 | E | 2 | ingen | F | A, B, C, D, E, F |
| 6 | F | 3 | ingen | (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: FRekursivt dybde-først fra A:| Steg | Handling | Kallstakken etter steget | Besøkt |
|---|---|---|---|
| 1 | kall DFSVisit(A) | A | A |
| 2 | kall DFSVisit(B) | A, B | A, B |
| 3 | ser på naboen A fra B — allerede besøkt, hopper over | A, B | A, B |
| 4 | kall DFSVisit(D) | A, B, D | A, B, D |
| 5 | ser på naboen B fra D — allerede besøkt, hopper over | A, B, D | A, B, D |
| 6 | kall DFSVisit(F) | A, B, D, F | A, B, D, F |
| 7 | ser på naboen D fra F — allerede besøkt, hopper over | A, B, D, F | A, B, D, F |
| 8 | kall DFSVisit(E) | A, B, D, F, E | A, B, D, E, F |
| 9 | ser på naboen B fra E — allerede besøkt, hopper over | A, B, D, F, E | A, B, D, E, F |
| 10 | kall DFSVisit(C) | A, B, D, F, E, C | A, B, C, D, E, F |
| 11 | ser på naboen A fra C — allerede besøkt, hopper over | A, B, D, F, E, C | A, B, C, D, E, F |
| 12 | ser på naboen E fra C — allerede besøkt, hopper over | A, B, D, F, E, C | A, B, C, D, E, F |
| 13 | DFSVisit(C) returnerer | A, B, D, F, E | A, B, C, D, E, F |
| 14 | ser på naboen F fra E — allerede besøkt, hopper over | A, B, D, F, E | A, B, C, D, E, F |
| 15 | DFSVisit(E) returnerer | A, B, D, F | A, B, C, D, E, F |
| 16 | DFSVisit(F) returnerer | A, B, D | A, B, C, D, E, F |
| 17 | DFSVisit(D) returnerer | A, B | A, B, C, D, E, F |
| 18 | ser på naboen E fra B — allerede besøkt, hopper over | A, B | A, B, C, D, E, F |
| 19 | DFSVisit(B) returnerer | A | A, B, C, D, E, F |
| 20 | ser på naboen C fra A — allerede besøkt, hopper over | A | A, B, C, D, E, F |
| 21 | DFSVisit(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: 6Hvor de skiller lag: i det andre steget. Begge har akkurat besøkt A ogB. 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.
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, Ea) 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?
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.
Den første er felle #6 i bokas feilregister, og den er den dyreste i hele
Del 5.
- Å forveksle dybde-først fra én node med dybde-først over hele grafen.
DFSVisit(G, u) er ; DFSFull(G) er .
Kontrollen: står det for hver node v i V i pseudokoden din? Da skal
med i kjøretiden. Står det ikke, skal det ikke.
- Å tro at dybde-først gir korteste vei. Den gir en vei. Korteste vei
målt i antall kanter får du av bredde-først, og bare i en uvektet graf.
Har kantene vekter, duger ingen av dem, og du må til Del 6.
- Å glemme besokt-merket. I en graf med en sykel går traverseringen da
i uendelig løkke. Merket er både det som gjør algoritmen korrekt og det som
gjør den lineær.
- Å merke noden ved uttak fra køen i stedet for ved innlegging. Da kan
den samme noden legges inn flere ganger, og bredde-først er ikke lenger
.
- Å droppe besokt-sjekken ved uttak i den iterative varianten. Der er
duplikater på stakken helt normalt, og uten sjekken blir noder besøkt to
ganger.
- Å oppgi kjøretid uten å si hvilken representasjon du antar. Med
nabomatrise er både bredde-først og dybde-først , ikke
. Kjøretiden er ikke en egenskap ved algoritmen alene.
Og den gjennomgående: å oppgi en kjøretid som ikke matcher pseudokoden du
faktisk skrev. Les koden din én gang til med kjøretiden i hånden, og tell
løkkene. Å bruke uten å definere det er dessuten felle #10 — i grafer
har du to størrelser, og , og begge skal navngis.
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 nabolisteneA: 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.
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.
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.
Hvor mange DFSVisit-kall som er aktive samtidig, altså hvor høy
kallstakken blir. Den er 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 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.
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
.
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.