4.2 Tre-algoritmer i pseudokode — beskjæring og lineær rekursjon
De faste Del 2-treoppgavene — InRange (beskåret in-order), diameter (én DFS), LCA og checkBST — der lavere kjøretid gir mer poeng.
Tre-algoritmer i pseudokode har vært på 6 av 7 sett i arkivet (86 %), og
de er alltid på Del 2 — den delen av settet der du skriver algoritmen selv
og der karakteren avgjøres.
Sjangeren har et navn i denne boka: sjanger G — tre-algoritmer i
pseudokode, altså at du får et binært søketre og en oppgave, og skal levere
en algoritme med oppgitt kjøretid. Fire oppgaver går igjen, og alle fire står
i dette kapitlet: skriv ut verdiene i et intervall, finn den lengste stien i
treet, finn nærmeste felles forfar til to noder, og avgjør om et tre i det
hele tatt er et gyldig søketre.
Her slår poengtrappen inn for fullt. Sensorveiledningene sier det rett ut:
lavere kjøretid gir mer poeng på samme oppgave. På hver av de fire
oppgavene finnes det en opplagt løsning som besøker hele treet, og en litt
smartere som ikke gjør det — og forskjellen er verdt flere poeng hver gang:
| Oppgave | Den opplagte løsningen | Den som gir full pott |
|---|---|---|
| verdier i et intervall | full traversering, | beskåret, |
| lengste sti (diameter) | høyde per node, | én gjennomgang, |
| gyldig søketre? | minste og største per node, | intervall nedover, |
Prioritet: høyeste. Dette er stoffet som skiller en C fra en A i Del 4 —
og C er en god og vanlig karakter, så les dette som en mulighet, ikke som et
krav om plettfrihet.
Slik er kapitlet lagt opp (55 min): fire løkker med tidsanslag, én per
algoritme, og et kort avsnitt til slutt om spørsmålet «kan dette gjøres
raskere?» — som du bør stille deg selv på hver eneste Del 2-oppgave.
Forkunnskaper
- kap. 4.1 — BST-egenskapen, at in-order gir sortert
utskrift, og at alle veier ned gjennom treet koster . Alt i dette
kapitlet hviler på de tre tingene.
- kap. 1.2 — å lese kjøretid ut av kode. Her teller du
ikke løkker, men hvor mange noder algoritmen besøker, og prinsippet er
det samme.
Algoritmene her er rekursive, og rekursjon er pensum i IN2010 — du skal
bruke den fritt. Er den fersk:
- Rekursjon — spore og skrive — å følge et rekursivt
kall steg for steg, og å skrive et selv.
- Nøstede lister og ordbøker — strukturer som inneholder
strukturer, som er akkurat det et subtre er.
Løkke 1 — å hoppe over det du vet du ikke trenger (ca. 15 min)
En værstasjon lagrer observasjoner i et binært søketre nøklet på klokkeslett.
Du skal hente ut alt som ble målt mellom klokka 09 og klokka 11. Treet har en
million observasjoner; intervallet inneholder kanskje tolv.
Den opplagte løsningen er å gå gjennom hele treet med InOrder og skrive ut
dem som ligger innenfor. Den virker, og den er — en million steg for
tolv treff.
Men søketreet vet noe du ikke utnytter. Står du i en node med verdien 14, og
øvre grense er 11, så er hele høyre subtre større enn 14 og dermed større
enn 11. Ikke én eneste verdi der inne kan være et treff. Du trenger ikke å se
på dem — du kan beskjære grenen.
Det er hele ideen, og det er den som skiller full pott fra halv pott på denne
oppgaven. Boka kaller den motsatte feilen — å traversere alt når beskjæring
var mulig — for felle #1 i feilregisteret.
Å la være å gå ned i et subtre fordi ordningen i treet allerede garanterer at
det ikke kan inneholde noe du er ute etter.
I et søketre er garantien BST-egenskapen: er noden større enn øvre grense, er
hele høyre subtre også det. Beskjæring er den enkleste måten å komme fra
til på, og den er nøyaktig det poengtrappen belønner.
v.x, v.left og v.right; et tomt subtre er nil. Grensene a og b erav samme type som verdiene, og begge er inklusive.
Prebetingelse: treet oppfyller BST-egenskapen, og a er ikke større ennb.
Postbetingelse: nøyaktig de verdiene som ligger mellom a og b er
skrevet ut, i stigende rekkefølge, og treet er uendret.
Procedure InRange(v, a, b)
Input: rota v i et BST (nil hvis subtreet er tomt), nedre grense a, oevre grense b
Output: alle verdier x i subtreet med a <= x <= b, skrevet ut i stigende rekkefoelge
if v er nil:
return
if a < v.x:
InRange(v.left, a, b)
if a <= v.x and v.x <= b:
skriv ut v.x
if v.x < b:
InRange(v.right, a, b)Grunnideen i én setning: dette er in-order-traverseringen fra
kap. 4.1 med to vakter satt foran de rekursive kallene —
venstre besøkes bare hvis det kan finnes noe stort nok der, høyre bare hvis det
kan finnes noe lite nok.
De to vaktene er hele forskjellen, og de er verdt å lese én gang til:
- if a < v.x — er noden allerede mindre enn eller lik nedre grense, er alt i
venstre subtre for lite. Hopp over.
- if v.x < b — er noden allerede større enn eller lik øvre grense, er alt i
høyre subtre for stort. Hopp over.
Kjøretid: i et balansert tre, der er antall noder og
er antall verdier som faktisk skrives ut. Begrunnelsen er todelt:
algoritmen følger to søkeveier ned gjennom treet, én for hver grense, og det
koster ; alt annet den besøker, er noder som enten skrives ut
eller ligger mellom de to søkeveiene, og det er av dem. Generelt skrives
kjøretiden , siden er i et degenerert tre.
Spesialtilfellet du bør nevne: er a lik b, er høyst 1, og
kjøretiden blir . Da er InRange blitt et vanlig søk.
Poengtrapp-notat. Den naive løsningen — full InOrder med enif-test på hver verdi — er og gir typisk omtrent halv uttelling. Den
er ikke gal, den er bare dyrere enn nødvendig, og det er nettopp det
poengtrappen straffer.
Et binært søketre er bygget ved å sette inn 50, 30, 70, 20, 40, 60 og 80. KjørInRange med intervallet . Hvilke noder besøkes, hvilke skrives ut,
og hva sparer du sammenlignet med en full in-order-traversering?
50
__________/ \__________
30 70
____/ \____ ____/ \____
20 40 60 80Sporingen:
- noder BESOEKT av beskåret InRange (6 stk): 50, 30, 20, 40, 70, 60
- noder skrevet ut (sortert): 30, 40, 50, 60
- noder en NAIV full in-order ville besøkt: 7 stk
- beskjæringer:
- høyre subtre til 70 (alt der er >= 70 > 60 = b)
Hva som skjedde, node for node. I rota 50 er a < v.x sant (25 er mindre
enn 50), så venstre besøkes; 50 ligger i intervallet og skrives ut; ogv.x < b er sant (50 er mindre enn 60), så høyre besøkes også. I noden 70 erv.x < b usant — 70 er ikke mindre enn 60 — og høyre subtre, altså 80,
beskjæres.
Sluttsvaret: 30, 40, 50, 60 — i stigende rekkefølge, slik in-order-formen
garanterer.
Besparelsen er beskjeden her, og det er poenget med å vise et lite tre: 6
besøk mot 7. I et tre med en million noder og fire treff ville den naive
løsningen brukt en million steg, mens den beskårne bruker omtrent 20 pluss
treffene. Forskjellen er ikke en konstant — den er mot
.
Fellenote. Fella her er felle #1 — å kjøre en full traversering når
beskjæring var mulig. Den koster typisk halve poengsummen på oppgaven, og den
er lett å gjøre, fordi den naive løsningen både er kortere å skrive og
fullstendig korrekt.
(Innstegsoppgave, sjanger G — tre-algoritmer i pseudokode, altså at du skriver
algoritmen selv og oppgir kjøretiden.) Du står i en node v under kjøring avInRange(v, a, b).
a) Under hvilken betingelse går du ned i venstre subtre, og hvorfor er
det trygt å hoppe over det ellers?
b) Under hvilken betingelse går du ned i høyre subtre?
c) Hvilken traverseringsrekkefølge sikrer at utskriften blir sortert?
Et binært søketre er bygget ved å sette inn 42, 17, 63, 8, 25, 55,
71, 4, 12, 20 og 33 i denne rekkefølgen. Kjør InRange med intervallet
.
a) Hvilke noder besøkes, og hvilke skrives ut?
b) Hvilke subtrær beskjæres, og hvilken av de to vaktene sørger for hver
av dem?
c) Hvor mange noder ville en full in-order-traversering besøkt?
Et binært søketre er bygget ved å sette inn 30, 15, 45, 10, 20,
40, 60, 5, 12, 18 og 25 i denne rekkefølgen. Kjør InRange med intervallet
— altså med a lik b.
a) Hvilke noder besøkes, og hva skrives ut?
b) Hvilke tre subtrær beskjæres?
c) Hva blir kjøretiden når a er lik b, og hva har algoritmen egentlig
blitt til?
Løkke 2 — diameter: én gjennomgang i stedet for n (ca. 15 min)
— naturlig pausepunkt —
Et fibernett er lagt ut som et tre, med en sentral node og grener som deler
seg videre. Spørsmålet fra driftsavdelingen er enkelt: hvor langt er det
mellom de to punktene som ligger lengst fra hverandre? Det tallet kalles
treets diameter, og det måles i antall kanter på den lengste stien.
To ting gjør oppgaven til en klassiker på Del 2.
Den første: stien går ikke nødvendigvis gjennom rota. Den kan ligge helt
inne i ett av subtrærne, langt fra toppen. Enhver løsning som bare regner ut
«venstre høyde pluss høyre høyde» i rota, er gal.
Den andre: den nærliggende måten å fikse det på er å gjøre nettopp den
regningen i hver node, og ta den største. Det er riktig — og det er
, fordi hver høydeberegning i seg selv er . Boka kaller det
felle #2: kvadratisk der lineær var mulig.
Den lineære løsningen bruker et triks du kommer til å se igjen i graf-delen:
la den rekursive funksjonen returnere det ene svaret og oppdatere det andre
underveis. Én gjennomgang, to svar.
Lengden på den lengste stien mellom to noder i treet, målt i antall kanter.
Stien går ikke nødvendigvis gjennom rota. For hver node er den lengste
stien som har som toppunkt, lik , der og er
høydene til de to subtrærne. Diameteren er den største av disse verdiene over
alle noder.
Høyden til et subtre er antall kanter på den lengste stien ned fra roten i
subtreet. En bladnode har høyde , og et tomt subtre får høyde .
Konvensjonen er ikke pynt: den er det som gjør at formelen
stemmer også når en node mangler et barn. En node med bare ett barn som er
bladnode får , som er riktig — det er én kant ned til
barnet.
v.x, v.leftog
v.right; nil for tomt subtre. Treet trenger ikke å være et søketre —diameter handler bare om formen. Vi antar at
maks er en variabel beggeprosedyrene ser.
Prebetingelse: ingen, ut over at rot peker på et tre eller er nil.
Postbetingelse: returverdien er antall kanter på den lengste stien i treet;
treet er uendret.
Procedure Diameter(rot)
Input: rota i et binaert tre (nil hvis treet er tomt)
Output: lengden paa den lengste stien i treet, maalt i kanter
maks = 0
Hoyde(rot)
return maks
Procedure Hoyde(v)
Input: noden v (kan vaere nil)
Output: hoeyden til subtreet med rot v, maalt i kanter; -1 for tomt subtre
if v er nil:
return -1
hL = Hoyde(v.left)
hR = Hoyde(v.right)
if hL + hR + 2 > maks:
maks = hL + hR + 2
return 1 + storste av hL og hRGrunnideen i én setning: Hoyde regner ut det den er bedt om og noterer
samtidig, gratis, den lengste stien som passerer gjennom noden den står i — så
når rekursjonen er ferdig, er alle noder prøvd som toppunkt.
Rekkefølgen er post-order: begge barna er ferdige før noden selv gjør noe.
Det er nettopp derfor hL og hR er tilgjengelige når maks skal oppdateres.
Kjøretid: , der er antall noder. Hver node får nøyaktig ettHoyde-kall, og arbeidet i kallet er to sammenligninger og en addisjon utenom
rekursjonen — altså konstant per node.
Rekursjonen har én kostnad til, og den skal du nevne én gang i besvarelsen:
kallstakken blir like dyp som treet, altså i minne. På et balansert tre
er det og helt ufarlig; på et skjevt tre er , og da
bruker algoritmen minne. Det er et fint delpoeng å ta med, og det er den
eneste prisen rekursjonen har her.
Poengtrapp-notat. Varianten som kaller Hoyde på nytt i hver node er
og gir tydelig mindre uttelling enn denne, selv om den gir riktig
svar. På en diameteroppgave har trappen vært eksplisitt i veiledningen: den
lineære løsningen gir full pott, den kvadratiske noe under to tredjedeler.
Regn ut diameteren til det binære søketreet som er bygget av 50, 30, 70, 20,
40, 60 og 80. Vis hva Hoyde returnerer i hver node, og hvordan maks
utvikler seg.
50
__________/ \__________
30 70
____/ \____ ____/ \____
20 40 60 80Radene står i post-order, altså i den rekkefølgen Hoyde faktisk blir
ferdig med nodene:
| Node (post-order) | hL | hR | hL+hR+2 | maks hittil |
|---|---|---|---|---|
| 20 | -1 | -1 | 0 | 0 |
| 40 | -1 | -1 | 0 | 0 |
| 30 | 0 | 0 | 2 | 2 |
| 60 | -1 | -1 | 0 | 2 |
| 80 | -1 | -1 | 0 | 2 |
| 70 | 0 | 0 | 2 | 2 |
| 50 | 1 | 1 | 4 | 4 |
- diameter (antall KANTER på lengste sti): 4
Les tabellen nedenfra og opp. Bladnodene 20, 40, 60 og 80 har to tomme
subtrær, altså , og kandidatverdien blir —
riktig, for stien fra en bladnode til seg selv har null kanter. I noden 30 er
begge barna bladnoder med høyde 0, så kandidaten er : stien
20-30-40 har to kanter. I rota er begge subtrærne høyde 1, og kandidaten blir
— stien 20-30-50-70-60, som har fire kanter.
Svaret er 4. Her lå maksimum i rota, men det er ikke noe algoritmen kunne
visst på forhånd: hver eneste node ble prøvd som toppunkt, og det er hele
grunnen til at maks er en variabel og ikke bare returverdien fra rota.
Én gjennomgang ga to svar. Hoyde returnerte høyden, som forelderen
trengte, og oppdaterte maks på veien. Hadde vi i stedet kalt Hoyde på nytt
fra hver node, ville de sju kallene kostet hver, og totalen blitt
.
Fellenote. Fella her er felle #2 — kvadratisk der lineær var mulig.
Den nest vanligste er å glemme -konvensjonen for tomme subtrær og regne
bladnoder som høyde 1; da blir alle kandidatverdiene to for store.
Et binært søketre er bygget ved å sette inn 8, 3, 10, 1, 6, 14, 4,
7 og 13 i denne rekkefølgen.
a) Sett opp post-order-tabellen med hL, hR, hL+hR+2 og maks hittil,
og oppgi diameteren.
b) Hvilken node ga den største kandidatverdien, og hvilken sti er det?
c) Node 10 har hL = -1. Hva betyr det, og hvorfor er kandidatverdien i
den noden likevel meningsfull?
Et binært søketre er bygget ved å sette inn 1, 2, 3,
4 og 5 i denne rekkefølgen.
a) Sett opp post-order-tabellen og oppgi diameteren.
b) Hvor dyp blir kallstakken når Diameter kjører på dette treet, og hva
er det uttrykt i ?
c) En medstudent foreslår å regne ut diameteren ved å kalle Hoyde på
venstre og høyre subtre i hver node og ta den største summen. Gir det
riktig svar, og hva blir kjøretiden?
Løkke 3 — nærmeste felles forfar (ca. 10 min)
To filer ligger et sted i et mappetre. Hvilken mappe er den nærmeste som
inneholder begge? Det er spørsmålet LCA stiller — forkortelsen står for
«lowest common ancestor», på norsk nærmeste felles forfar.
I et vilkårlig tre må du løse det med forelderpekere: løft den dypeste noden
opp til begge er like dypt nede, og gå så oppover i takt til de møtes.
I et søketre er det mye enklere, og det er søketre-varianten som går igjen
på eksamen. Der bærer verdiene selv informasjonen du trenger: står du i en
node der begge verdiene er mindre, må begge ligge til venstre. Er begge større,
ligger begge til høyre. Og i det øyeblikket den ene er på hver side — eller
noden er en av dem — har du funnet møtepunktet.
v.x,v.left og v.right. Begge verdiene x og y finnes i treet, og x er ikkestørre enn
y — er du usikker på rekkefølgen, bytt om på dem først.Prebetingelse: treet oppfyller BST-egenskapen.
Postbetingelse: returverdien er den dypeste noden som har både x og y i
sitt subtre; treet er uendret.
Procedure LCA(v, x, y)
Input: rota v i et BST, to verdier x og y som finnes i treet, med x <= y
Output: noden som er naermeste felles forfar til x og y
while v er ikke nil:
if y < v.x:
v = v.left
else if v.x < x:
v = v.right
else:
return v
return nilGrunnideen i én setning: så lenge begge verdiene ligger på samme side av
noden, kan ikke noden være den nærmeste felles forfaren — og første gang de
ikke gjør det, er den det.
Merk at else-grenen også dekker tilfellet der v.x er lik x eller y. En
node regnes som sin egen forfar, så hvis den ene verdien ligger i noden og den
andre under den, er noden svaret.
Kjøretid: , der er høyden. Løkka går ett nivå ned per runde og
aldri opp igjen. I et balansert tre er det ; på et skjevt tre
.
Varianten for et generelt tre — der verdiene ikke er ordnet og du i stedet
har v.parent og v.depth — går slik:
Procedure LCAGenerelt(u, w)
Input: to noder u og w i et tre der hver node har parent og depth
Output: naermeste felles forfar til u og w
while u.depth > w.depth:
u = u.parent
while w.depth > u.depth:
w = w.parent
while u er ikke lik w:
u = u.parent
w = w.parent
return uOgså : først løftes den dypeste opp til lik dybde, så går begge oppover i
takt. Oppgi hvilken av de to du har antatt — sensor binder seg ikke til
representasjonen, men du må si hva du forutsetter.
Finn nærmeste felles forfar til parene (20, 40), (20, 80), (60, 80) og (30, 30)
i søketreet bygget av 50, 30, 70, 20, 40, 60 og 80.
50
__________/ \__________
30 70
____/ \____ ____/ \____
20 40 60 80| x | y | Vei fra rota | LCA |
|---|---|---|---|
| 20 | 40 | 50 -> 30 | 30 |
| 20 | 80 | 50 | 50 |
| 60 | 80 | 50 -> 70 | 70 |
| 30 | 30 | 50 -> 30 | 30 |
Rad for rad. For (20, 40): i rota er
y lik 40, som er mindre enn 50, såbegge ligger til venstre og vi går dit. I noden 30 er 20 mindre og 40 større —
de skiller lag — så 30 er svaret.
For (20, 80) skiller de lag med en gang: 20 hører hjemme til venstre for 50 og
80 til høyre, så rota selv er nærmeste felles forfar. Én sammenligning.
For (30, 30), altså samme verdi to ganger, stopper løkka i noden 30 fordi
verken y < v.x eller v.x < x er sann. En node er sin egen forfar, og svaret
er noden selv.
Merk hva som ikke skjer: algoritmen leter aldri etter x og y hver for
seg. Den følger én sti, og den stopper i det øyeblikket den ser at verdiene må
skille lag. Det er derfor kjøretiden er og ikke to ganger — ikke
at det ville endret orden, men fordi det viser hva BST-egenskapen faktisk gir
deg gratis.
Bruk søketreet som er bygget ved å sette inn 42, 17, 63, 8, 25,
55, 71, 4, 12, 20 og 33.
a) Finn nærmeste felles forfar til parene (4, 12), (20, 33), (12, 55) og
(55, 71). Oppgi veien fra rota i hvert tilfelle.
b) Hvilket av de fire parene stoppet raskest, og hvorfor?
c) Hvorfor er kjøretiden og ikke ?
Løkke 4 — er dette i det hele tatt et søketre? (ca. 12 min)
En datafil er importert fra et gammelt system, og strukturen påstås å være et
binært søketre. Før noe annet kjøres på den, må påstanden sjekkes.
Den første ideen de fleste får, er å gå gjennom hver node og sjekke at venstre
barn er mindre og høyre barn er større. Den testen er ikke god nok, og
grunnen er ordet «alle» i BST-egenskapen: kravet gjelder hele subtreet, ikke
bare barna. En node kan være større enn sin forelder og likevel ligge i
venstre subtre til noe høyere opp.
Neste idé pleier å være: finn største verdi i venstre subtre og minste i
høyre, for hver node. Den er riktig, men den er — igjen felle
#2, en -jobb gjort i hver av noder.
Den lineære løsningen snur problemet: i stedet for å hente informasjon opp
fra subtrærne, sender du et krav nedover. Hver node får med seg intervallet
den må ligge i, og strammer det inn for barna sine.
v.x, v.leftog
v.right; nil for tomt subtre. Verdiene er unike og kan sammenlignes. Viantar at det finnes verdier som er mindre og større enn alt i treet, skrevet
-uendelig og +uendelig.Prebetingelse: ingen — det er nettopp gyldigheten som skal avgjøres.
Postbetingelse: returverdien er sant hvis og bare hvis subtreet under v
er et gyldig BST med alle verdier strengt inne i intervallet; treet er uendret.
Procedure CheckBST(v, low, high)
Input: noden v, og intervallet (low, high) som alle verdier i subtreet maa ligge i
Output: sant hvis subtreet med rot v er et gyldig BST innenfor intervallet
if v er nil:
return sant
if v.x <= low or v.x >= high:
return usant
return CheckBST(v.left, low, v.x) and CheckBST(v.right, v.x, high)Kalles på hele treet som CheckBST(rot, -uendelig, +uendelig).
Invarianten i én setning: når kallet gjøres på en node, inneholder
intervallet (low, high) nøyaktig de verdiene som er lovlige der ut fra alle
forfedrene — så en node som bryter mot noe langt oppe i treet, blir tatt her
og ikke først senere.
Se hvordan intervallet strammes: går du til venstre, blir noden det nye
taket; går du til høyre, blir den det nye gulvet. Det er hele mekanismen.
Kjøretid: , der er antall noder. Hver node får ett kall, og
arbeidet per kall er to sammenligninger. I praksis stopper algoritmen ved
første brudd, men verste tilfelle — et gyldig tre — krever at alle noder
sjekkes.
Alternativet som også er : kjør InOrder og sjekk at utskriften er
strengt stigende. Den er like god, og den er lettere å forklare i naturlig
språk — noe som gir full uttelling så lenge forklaringen er entydig.
Poengtrapp-notat. Å hente Min og Maks i hvert subtre for hver node gir
riktig svar i og dermed klart mindre uttelling. Å bare sammenligne
hver node med sine to barn er gal og gir tilsvarende lite, uansett hvor
pent den er skrevet.
Kjør CheckBST på treet bygget av 50, 30, 70, 20, 40, 60 og 80. Kjør den
deretter på det samme treet der noden 40 er byttet ut med 55, og forklar
hvorfor en test som bare sammenligner hver node med sine barn, ikke fanger
feilen.
50
__________/ \__________
30 70
____/ \____ ____/ \____
20 40 60 80| Node | low | high | Status |
|---|---|---|---|
| 50 | -uendelig | +uendelig | OK |
| 30 | -uendelig | 50 | OK |
| 20 | -uendelig | 30 | OK |
| 40 | 30 | 50 | OK |
| 70 | 50 | +uendelig | OK |
| 60 | 50 | 70 | OK |
| 80 | 70 | +uendelig | OK |
- gyldig BST: JA
- in-order: 20, 30, 40, 50, 60, 70, 80 (sortert)
Det brutte treet. Samme tre, men noden 40 er endret til 55. Lokalt ser det
riktig ut (
55 > 30), men 55 ligger i VENSTRE subtre til rota 50. 50
__________/ \__________
30 70
____/ \____ ____/ \____
20 55 60 80| Node | low | high | Status |
|---|---|---|---|
| 50 | -uendelig | +uendelig | OK |
| 30 | -uendelig | 50 | OK |
| 20 | -uendelig | 30 | OK |
| 55 | 30 | 50 | BRUDD |
- gyldig BST: NEI
- in-order: 20, 30, 55, 50, 60, 70, 80 (IKKE sortert)
Hvorfor barn-mot-forelder-testen svikter. Noden 55 er større enn sin
forelder 30 og ligger til høyre for den. Ser du bare på det ene forholdet, er
alt i orden. Kravet den bryter, kommer fra rota: alt i venstre subtre til
50 må være mindre enn 50, og 55 er ikke det.
Intervallmetoden fanger det umiddelbart, fordi kravet fra rota fortsatt ligger
i high når vi kommer ned til 55: intervallet er , og 55 er utenfor.
Legg merke til at algoritmen stoppet etter fire noder — den trengte ikke å se
på høyresiden i det hele tatt.
Den andre kontrollen sier det samme: in-order-utskriften er 20, 30, 55, 50,
60, 70, 80, og der kommer 50 etter 55. Ikke sortert, altså ikke et søketre.
Fellenote. Fella her er felle #9 — å sjekke en lokal, heap-aktig
relasjon mellom forelder og barn i stedet for BST-egenskapens krav om at
alle verdier i subtreet skal ligge på riktig side.
Et binært tre har rot 20 med barna 10 og 30. Noden
10 har barna 5 og 15; noden 30 har barna 12 og 35.
a) Sett opp low/high-tabellen som CheckBST genererer, og oppgi hvor
det første bruddet skjer.
b) Vis at in-order-metoden gir samme konklusjon.
c) Hvorfor er intervallmetoden , mens metoden med Min og Maks per
node er ?
De to første er de dyreste i hele Del 2, og de er begge feil som gir riktig
svar — det er derfor de er så lette å gjøre.
- Full traversering der beskjæring var mulig. Felle #1. Å skrive
InOrder med en if-test på hver verdi løser intervalloppgaven i ,
og koster typisk halve poengsummen. Kontrollen: kan ordningen i treet
utelukke en gren før du går ned i den? I et søketre er svaret som regel ja.
- Kvadratisk der lineær var mulig. Felle #2. Diameter med et nytt
Hoyde-kall i hver node, eller checkBST med Min og Maks per node.
Kontrollen: gjør du en jobb som selv koster , inne i noe som
besøker alle noder? Da er du på , og det finnes nesten alltid en
måte å samle informasjonen på i én gjennomgang.
- Å anta at den lengste stien går gjennom rota. Den kan ligge helt inne i
ett subtre. Derfor holdes maks som en variabel som oppdateres i hver node,
ikke som returverdien fra rota.
- Å glemme at tomt subtre har høyde . Med feil konvensjon blir alle
kandidatverdiene systematisk to for store, og diameteren blir gal på hvert
eneste tre.
- Å sjekke BST-egenskapen bare mot barna. Felle #9 i praksis: kravet
gjelder alle verdier i subtreet, ikke bare de to nærmeste. En node kan
se helt riktig ut lokalt og likevel bryte et krav fra rota.
- Å oppgi kjøretid uten å definere . Felle #10, og et eksplisitt
trekk. Skriv «der er antall noder i treet» og «der er antall
treff» — det tar fem sekunder og er et sikkert delpoeng.
- Å skrive uklar eller altfor lang pseudokode. Felle #12. En setning
sensor ikke forstår etter to gjennomlesninger, blir ignorert. Kravet er
entydighet, ikke syntaks — og en klar forklaring i naturlig språk kan gi like
mye som kode.
Kan dette gjøres raskere? (ca. 3 min)
Still deg dette spørsmålet på hver eneste Del 2-oppgave, før du begynner å
skrive. Det er ikke en høflighetsfrase: poengtrappen er en eksplisitt del av
sensurgrunnlaget, og på de fire oppgavene i dette kapitlet er svaret ja hver
gang.
| Oppgave | Naiv løsning | Kjøretid | Bedre løsning | Kjøretid |
|---|---|---|---|---|
| verdier i | full in-order med test | beskåret in-order | ||
| diameter | Hoyde i hver node | én post-order med global maks | ||
| nærmeste felles forfar | søk etter hver verdi og sammenlign stiene | intervall-sammenligning nedover | ||
| gyldig søketre? | Min/Maks per node | intervall nedover, eller in-order |
Legg merke til LCA-raden. Der er begge løsningene , og den naive er
ikke dårligere asymptotisk. Det er en nyttig påminnelse: poengtrappen belønner
lavere orden, ikke elegant kode. Er ordenen den samme, er du i mål — men
si det, så sensor ser at du har vurdert det.
Og for de tre andre radene: forskjellen er ikke kosmetisk. Med en million
noder er omtrent en billion steg og omtrent en million. Det er
forskjellen mellom et program som svarer og et som ikke gjør det.
Skriv en prosedyre som teller hvor mange verdier
i et binært søketre som ligger i intervallet , så effektivt som mulig.
Prosedyren skal returnere tallet, ikke skrive noe ut.
Oppgi antagelser om representasjon, algoritmen, og kjøretiden med definert.
Si også hva en naiv løsning ville kostet.
Du skal avgjøre om et binært tre er et gyldig
søketre. To løsninger er foreslått: intervallmetoden CheckBST, og «kjør
in-order og sjekk at utskriften er strengt stigende».
a) Sammenlign de to på kjøretid og minnebruk, og definér .
b) Er det noen situasjon der den ene er å foretrekke?
c) Konkludér: hvilken ville du levert på eksamen?
Begrepsbank
Begrepsbanken er flashcard-/repetisjonsstoff — den gjentar det du nettopp har
lest. Hopp trygt over ved førstegangslesing; tidsanslaget for kapitlet gjelder
kjernestoffet.
Skriver ut alle verdier i intervallet i stigende rekkefølge, og går
bare ned i en gren når ordningen tillater at det kan finnes treff der.
Kjøretid i et balansert tre, generelt , der er
antall treff.
De to vaktene er if a < v.x foran venstre kall og if v.x < b foran høyre.
Uten dem er algoritmen en vanlig in-order og koster .
Returnerer antall kanter på den lengste stien i treet. Kjøretid ,
minnebruk for kallstakken.
Virkemåten: én post-order-rekursjon som returnerer subtre-høyden og
oppdaterer en global maks med i hver node. Én gjennomgang,
to svar.
Lengden på den lengste stien som har noden v som toppunkt: ned i venstre
subtre, gjennom v, og ned i høyre.
De to ekstra kantene er kantene fra v til hvert av barna. Formelen stemmer
også når et barn mangler, fordi et tomt subtre har høyde .
Finner nærmeste felles forfar til to verdier ved å gå én sti nedover fra rota:
er begge mindre enn noden, gå til venstre; er begge større, gå til høyre;
ellers er noden svaret. Kjøretid .
En node regnes som sin egen forfar. Jo lenger fra hverandre verdiene er, desto
raskere skiller de lag — og desto kortere blir søket.
Uten ordning i verdiene trenger du v.parent og v.depth: løft den dypeste
noden opp til begge er like dypt nede, og gå så oppover i takt til de er samme
node. Kjøretid .
Oppgi hvilken av de to variantene du har antatt. Sensor binder seg ikke til
representasjonen, men antagelsen må stå i besvarelsen.
Avgjør om et tre er et gyldig binært søketre ved å sende et intervall
nedover: går du til venstre blir noden det nye taket, går du til høyre blir den
det nye gulvet. Kjøretid .
Kalles som CheckBST(rot, -uendelig, +uendelig). Metoden fanger brudd som
stammer fra en forfar langt oppe i treet — nettopp de en test mot barna alene
ikke ser.
Alternativet med samme kjøretid: traverser in-order og sjekk at hver verdi
er strengt større enn den forrige. Like god, og lettere å forklare i én setning
naturlig språk — men sammenlign underveis, ellers bruker du ekstra minne
på å lagre hele utskriften først.
En rekursiv tre-algoritme bruker minne, fordi kallstakken blir like dyp
som treet.
På et balansert tre er det og ufarlig. På et degenerert tre er
, og da bruker algoritmen minne selv om kjøretiden er
uendret. Å nevne dette er et sikkert delpoeng i en Del 2-besvarelse.
Sensurregelen «lavere kjøretidskompleksitet er mer poenggivende» — på samme
oppgave gir full pott, mindre, og minst.
Den er ofte skrevet ut eksplisitt i veiledningen. Å velge riktig algoritme er
derfor å velge poeng, og formuleringen «så effektivt som mulig» i en
oppgavetekst er signalet om at trappen gjelder.
Å gå gjennom hele treet når ordningen kunne ha utelukket grener. TypiskInOrder med en if-test i stedet for beskåret InRange.
Løsningen er korrekt, men i stedet for , og koster
typisk halve poengsummen. Kontrollen: kan ordningen utelukke en gren før du går
ned i den?
Å gjøre en jobb som selv koster , i hver av noder. Diameter med et
nytt høydekall per node, eller checkBST med Min og Maks per node.
Begge gir riktig svar i . Kuren er den samme begge steder: samle
informasjonen i én gjennomgang, enten ved å returnere den oppover eller
sende et krav nedover.
Del 2-sjangeren der du får et binært søketre og skal skrive algoritmen selv:InRange, diameter, LCA eller checkBST. Har vært på 6 av 7 sett (86 %).
Full uttelling krever fire ledd: problemet navngitt, antagelser om
representasjon oppgitt, algoritmen (pseudokode eller klar naturlig-språk-
forklaring), og kjøretid som matcher koden med definert.
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.