Kirjoittaja: Antti Laaksonen
Kirjoitettu: 31.08.2007 – 31.08.2007
Tagit: koodi näytille, vinkki
Leveyssuuntainen ja syvyyssuuntainen haku ovat keskeisiä tapoja verkkojen läpikäyntiin. Tässä koodivinkissä aiheeseen tutustutaan hieman tavallisesta poikkeavasti labyrintin avulla. Labyrintin sisältä löytää ulos sekä leveyssuuntaisella että syvyyssuuntaisella haulla, mutta välillä erot algoritmien tehokkuudessa ja muistinkäytössä ovat suuria.
Syvyyssuuntainen haku on ehkä luonnollisin tapa labyrintin tutkimiseen. Siinä labyrintissa tunkeudutaan eteenpäin niin syvälle kuin päästään ja jos joudutaan umpikujaan, palataan takaisin niin kauan, kunnes ollaan jälleen tienhaarassa, josta pystyy jatkamaan eteenpäin. Toisin sanoen reitin haarautuessa jokainen haara käsitellään yksi kerrallaan kokonaan, ja vasta kun kaikki haarat on tutkittu, palataan tarvittaessa takaisin johonkin aiempaan haaraan.
Seuraavissa kuvissa näkyy syvyyssuuntaisen haun kaksi vaihetta. Aloitusruutu on D2, josta lähdetään ensin oikealle. Tämä reitti johtaa kuitenkin umpikujaan, jonne juuri ollaan joutumassa ensimmäisessä kuvassa. Ennen toista kuvaa on peruutettu takaisin alkuun ja käyty vielä toisella harharetkellä ruudussa F8. Nyt labyrintin uloskäynti alkaa vihdoin häämöttää.
Kaikkia reitin varrelle jääneitä ruutuja säilytetään taulukossa, jonka nimi on pino, koska vain taulukon loppuun tehdään lisäyksiä ja poistoja. Aina lähtöruudussa poikettaessa taulukko on hetkellisesti tyhjä. Lisäksi karttaan merkitään, missä ruuduissa on jo käyty, jotta samaan paikkaan ei mennä uudestaan. Syvyyssuuntainen haku toteutetaan usein rekursiivisella funktiolla, jolloin reitin kirjanpito hieman helpottuu.
Syvyyssuuntaisen haun heikkous on, että jos haku sattuu lähtemään väärään suuntaan, se voi tuhlata hyvin paljon aikaa labyrintin umpikujissa. Kaikkein ikävintä on, jos lähtöpaikka on uloskäynnin vieressä, mutta haku valitsee väärän suunnan. Tällöin joudutaan käymään koko labyrintti läpi, ennen kuin lopulta palataan lähtöpaikalle ja löydetään ulos. Tämän ongelman ratkaisuna on leveyssuuntainen haku.
Leveyssuuntainen haku etsii labyrintin reittejä järjestyksessä niiden pituuden mukaan. Ensin haku etsii kaikki reitit, joissa siirrytään yksi askel johonkin suuntaan. Jos tämä ei johda ulospääsyyn, haku laajentaa reitteihin, joissa liikutaan kaksi askelta, sitten kolme askelta jne. Haun toimintaa voi ajatella niin, että aina kun reitti haarautuu, jokaista haaraa lähtee tutkimaan rinnakkain erillinen haku.
Seuraavissa kuvissa lähtöpaikka on ruudussa B6. Heti aluksi haku jakaantuu kahdeksi hauksi, joista toinen lähtee vasemmalle ja toinen oikealle. Toisessa kuvassa haku on ehtinyt hieman pidemmälle ja hakuja on jo kolme. Nyt alun perin vasemmalle lähtenyt haku on jo melkein päässyt ulos. Muut haut joutuvat tosin umpikujaan, mutta niiden kohtalo ei haittaa.
Leveyssuuntainen haku toteutetaan taulukolla, jonka nimi on jono, koska paikkoja lisätään taulukon loppuun ja paikkoja poistetaan taulukon alusta. Käsittelyyn otetaan kulloinkin taulukon alussa oleva ruutu, ja jos siitä pääsee ruutuihin, joita ei vielä ole tutkittu, nämä ruudut lisätään taulukon loppuun. Näin yhtään uutta ruutua ei aleta käsitellä, ennen kuin kaikki vanhat ruudut on käsitelty.
Leveyssuuntainen haku löytää varmasti lyhyimmän reitin ulos, kun taas syvyyssuuntainen haku saattaa päätyä pitempään reittiin, jos on useita eripituisia reittejä. Toisaalta leveyssuuntaisella haulla ei voi koskaan käydä hyvä onni: jos labyrintti haarautuu kymmeneen yhtä pitkään käytävään, joista vain yksi johtaa ulos, leveyssuuntainen haku tutkii järjestelmällisesti kaikki käytävät kokonaan.
Tässä esitetyillä hakumenetelmillä on valtavasti sovelluskohteita, koska hyvin monenlaisia ongelmia voi ajatella labyrintteina. Kun ongelmaa ratkaistessa pitää valita monesta vaihtoehdosta, tilanne vastaa labyrintin haaraa, ja kun valintaketju johtaa ongelman ratkaisuun, labyrintista päästään ulos. Usein tosin labyrinttia ei saa valmiina, vaan ohjelma muodostaa sitä pikku hiljaa, ja labyrintti jää ehkä keskeneräiseksi.
' Leveyssuuntainen ja syvyyssuuntainen haku
' Antti Laaksonen, 2007
SCREEN 1
DIM kartta%(0 TO 10, 0 TO 10)
DIM nimet$(1 TO 9, 1 TO 9)
DIM muisti%(1 TO 100, 1 TO 2)
' laby$ = "B"
' haku$ = "L"
' piirto$ = "N"
' GOTO jatko
CLS
PRINT "Valitse labyrintti:"
PRINT
PRINT " A = Labyrintti A"
PRINT " B = Labyrintti B"
PRINT " C = Labyrintti C"
DO
laby$ = UCASE$(INKEY$)
LOOP UNTIL laby$ = "A" OR laby$ = "B" OR laby$ = "C"
CLS
PRINT "Valitse hakutapa:"
PRINT
PRINT " L = leveyssuuntainen haku"
PRINT " S = syvyyssuuntainen haku"
DO
haku$ = UCASE$(INKEY$)
LOOP UNTIL haku$ = "L" OR haku$ = "S"
CLS
PRINT "Valitse piirtotapa:"
PRINT
PRINT " N = nopea piirto"
PRINT " H = hidas piirto"
PRINT " A = askellus"
DO
piirto$ = UCASE$(INKEY$)
LOOP UNTIL piirto$ = "N" OR piirto$ = "H" OR piirto$ = "A"
jatko:
SELECT CASE laby$
CASE "A"
RESTORE labya
CASE "B"
RESTORE labyb
CASE "C"
RESTORE labyc
END SELECT
CLS
FOR i% = 0 TO 9
FOR j% = 0 TO 9
LINE (j% * 16 + 3, i% * 16 + 3)-STEP(16, 16), , B
NEXT
NEXT
FOR i% = 1 TO 9
LOCATE 2, i% * 2 + 2: PRINT CHR$(48 + i%)
LOCATE i% * 2 + 2, 2: PRINT CHR$(64 + i%)
NEXT
FOR i% = 1 TO 9
READ rivi$
FOR j% = 1 TO 9
SELECT CASE MID$(rivi$, j%, 1)
CASE " "
kartta%(j%, i%) = 1
CASE "#"
kartta%(j%, i%) = 0
LINE (j% * 16 + 5, i% * 16 + 5)-STEP(12, 12), , BF
CASE "*"
nx% = j%: ny% = i%
LOCATE i% * 2 + 2, j% * 2 + 2: PRINT CHR$(1)
END SELECT
nimet$(j%, i%) = CHR$(64 + i%) + CHR$(48 + j%)
NEXT
NEXT
FOR i% = 1 TO 80
LOCATE (i% - 1) MOD 20 + 2, 24 + ((i% - 1) \ 20) * 4
PRINT "--"
NEXT
LOCATE 22: PRINT STRING$(40, "-")
IF haku$ = "S" THEN
GOSUB syvyys
ELSEIF haku$ = "L" THEN
GOSUB leveys
ELSE
GOSUB loppu
END IF
leveys:
muisti%(1, 1) = nx%
muisti%(1, 2) = ny%
alku% = 1: loppu% = 1
LOCATE (loppu% - 1) MOD 20 + 2, 24 + ((loppu% - 1) \ 20) * 4
PRINT nimet$(muisti%(loppu%, 1), muisti%(loppu%, 2))
loppu% = 2
DO WHILE alku% < loppu% AND INKEY$ <> CHR$(27)
ux% = muisti%(alku%, 1)
uy% = muisti%(alku%, 2)
viesti$ = "Jatketaan jonon alusta ruudusta " + nimet$(ux%, uy%)
GOSUB odota
LOCATE (alku% - 1) MOD 20 + 2, 24 + ((alku% - 1) \ 20) * 4
PRINT "xx"
alku% = alku% + 1
IF ux% = 1 OR uy% = 1 OR ux% = 9 OR uy% = 9 THEN
viesti$ = "Vihdoin löytyi reitti ulos!"
GOSUB odota
EXIT DO
END IF
vloppu% = loppu%
IF kartta%(ux% + 1, uy%) = 1 THEN
muisti%(loppu%, 1) = ux% + 1
muisti%(loppu%, 2) = uy%
kartta%(ux% + 1, uy%) = 2
LOCATE (loppu% - 1) MOD 20 + 2, 24 + ((loppu% - 1) \ 20) * 4
PRINT nimet$(muisti%(loppu%, 1), muisti%(loppu%, 2))
LOCATE uy% * 2 + 2, ux% * 2 + 2: PRINT "x"
LOCATE uy% * 2 + 2, (ux% + 1) * 2 + 2: PRINT CHR$(1)
loppu% = loppu% + 1
viesti$ = "Lisätään jonoon vapaa ruutu " + nimet$(ux% + 1, uy%)
GOSUB odota
END IF
IF kartta%(ux%, uy% + 1) = 1 THEN
muisti%(loppu%, 1) = ux%
muisti%(loppu%, 2) = uy% + 1
kartta%(ux%, uy% + 1) = 2
LOCATE (loppu% - 1) MOD 20 + 2, 24 + ((loppu% - 1) \ 20) * 4
PRINT nimet$(muisti%(loppu%, 1), muisti%(loppu%, 2))
LOCATE uy% * 2 + 2, ux% * 2 + 2: PRINT "x"
LOCATE (uy% + 1) * 2 + 2, ux% * 2 + 2: PRINT CHR$(1)
loppu% = loppu% + 1
viesti$ = "Lisätään jonoon vapaa ruutu " + nimet$(ux%, uy% + 1)
GOSUB odota
END IF
IF kartta%(ux% - 1, uy%) = 1 THEN
muisti%(loppu%, 1) = ux% - 1
muisti%(loppu%, 2) = uy%
kartta%(ux% - 1, uy%) = 2
LOCATE (loppu% - 1) MOD 20 + 2, 24 + ((loppu% - 1) \ 20) * 4
PRINT nimet$(muisti%(loppu%, 1), muisti%(loppu%, 2))
LOCATE uy% * 2 + 2, ux% * 2 + 2: PRINT "x"
LOCATE uy% * 2 + 2, (ux% - 1) * 2 + 2: PRINT CHR$(1)
loppu% = loppu% + 1
viesti$ = "Lisätään jonoon vapaa ruutu " + nimet$(ux% - 1, uy%)
GOSUB odota
END IF
IF kartta%(ux%, uy% - 1) = 1 THEN
muisti%(loppu%, 1) = ux%
muisti%(loppu%, 2) = uy% - 1
kartta%(ux%, uy% - 1) = 2
LOCATE (loppu% - 1) MOD 20 + 2, 24 + ((loppu% - 1) \ 20) * 4
PRINT nimet$(muisti%(loppu%, 1), muisti%(loppu%, 2))
LOCATE uy% * 2 + 2, ux% * 2 + 2: PRINT "x"
LOCATE (uy% - 1) * 2 + 2, ux% * 2 + 2: PRINT CHR$(1)
loppu% = loppu% + 1
viesti$ = "Lisätään jonoon vapaa ruutu " + nimet$(ux%, uy% - 1)
GOSUB odota
END IF
IF vloppu% = loppu% THEN
viesti$ = "Reitti päättyi umpikujaan ruudussa " + nimet$(ux%, uy%)
GOSUB odota
END IF
LOOP
GOSUB loppu
syvyys:
muisti%(1, 1) = nx%
muisti%(1, 2) = ny%
ylin% = 1
LOCATE (ylin% - 1) MOD 20 + 2, 24 + ((ylin% - 1) \ 20) * 4
PRINT nimet$(muisti%(ylin%, 1), muisti%(ylin%, 2))
ux% = nx%: uy% = ny%
DO WHILE ylin% > 0 AND INKEY$ <> CHR$(27)
IF kartta%(ux%, uy%) = 2 THEN
LOCATE uy% * 2 + 2, ux% * 2 + 2: PRINT "x"
ELSE
LOCATE uy% * 2 + 2, ux% * 2 + 2: PRINT " "
END IF
ux% = muisti%(ylin%, 1)
uy% = muisti%(ylin%, 2)
viesti$ = "Liikutaan pinon ylimpään ruutuun " + nimet$(ux%, uy%)
kartta%(ux%, uy%) = 2
LOCATE uy% * 2 + 2, ux% * 2 + 2: PRINT CHR$(1)
GOSUB odota
IF ux% = 1 OR uy% = 1 OR ux% = 9 OR uy% = 9 THEN
viesti$ = "Vihdoin löytyi reitti ulos!"
GOSUB odota
EXIT DO
END IF
LOCATE (ylin% - 1) MOD 20 + 2, 23 + ((ylin% - 1) \ 20) * 4
PRINT " "
poisto% = 0
IF kartta%(ux% + 1, uy%) = 1 THEN
ylin% = ylin% + 1
muisti%(ylin%, 1) = ux% + 1
muisti%(ylin%, 2) = uy%
viesti$ = "Lisätään pinoon vapaa ruutu " + nimet$(ux% + 1, uy%)
ELSEIF kartta%(ux%, uy% + 1) = 1 THEN
ylin% = ylin% + 1
muisti%(ylin%, 1) = ux%
muisti%(ylin%, 2) = uy% + 1
viesti$ = "Lisätään pinoon vapaa ruutu " + nimet$(ux%, uy% + 1)
ELSEIF kartta%(ux% - 1, uy%) = 1 THEN
ylin% = ylin% + 1
muisti%(ylin%, 1) = ux% - 1
muisti%(ylin%, 2) = uy%
viesti$ = "Lisätään pinoon vapaa ruutu " + nimet$(ux% - 1, uy%)
ELSEIF kartta%(ux%, uy% - 1) = 1 THEN
ylin% = ylin% + 1
muisti%(ylin%, 1) = ux%
muisti%(ylin%, 2) = uy% - 1
viesti$ = "Lisätään pinoon vapaa ruutu " + nimet$(ux%, uy% - 1)
ELSE
ylin% = ylin% - 1
poisto% = 1
viesti$ = "Poistetaan pinosta käsitelty ruutu " + nimet$(ux%, uy%)
END IF
IF poisto% = 0 THEN
LOCATE (ylin% - 1) MOD 20 + 2, 24 + ((ylin% - 1) \ 20) * 4
PRINT nimet$(muisti%(ylin%, 1), muisti%(ylin%, 2))
ELSE
LOCATE ylin% MOD 20 + 2, 24 + (ylin% \ 20) * 4
PRINT "xx"
END IF
GOSUB odota
LOOP
loppu:
SLEEP
SCREEN 0
END
odota:
LOCATE 23: PRINT LEFT$(viesti$ + SPACE$(40), 40)
IF piirto$ = "N" THEN
a! = TIMER
WHILE TIMER - a! < .2: WEND
ELSEIF piirto$ = "H" THEN
a! = TIMER
WHILE TIMER - a! < .8: WEND
ELSEIF piirto$ = "A" THEN
WHILE INKEY$ = "": WEND
END IF
RETURN
labya:
DATA "#########"
DATA "# #"
DATA "# ##### #"
DATA "#* # #"
DATA "### # # #"
DATA "# # # # #"
DATA "# # # ###"
DATA "# # "
DATA "#########"
labyb:
DATA "#########"
DATA "# #"
DATA "# ## ## #"
DATA "# ## ## #"
DATA "# * #"
DATA "# ## ## #"
DATA "# ## ## #"
DATA "# ## #"
DATA "####### #"
labyc:
DATA "#########"
DATA " * #"
DATA "####### #"
DATA "# #"
DATA "### ### #"
DATA "# # # # #"
DATA "# # # # #"
DATA "# # #"
DATA "#########"Saisiko EXE:ä? Basic:ini reistailee :( Sekoittaa kaiken sekavaksi...
Toki, ohjelman voi noutaa käännettynä osoitteesta:
http://koti.mbnet.fi/pllk/muut/LABY2.EXE
Varmaan kelpo vinkki, mutta en tiedä onko QBasic mukavin kieli algoritmien esittelemiseen.
Antti kirjoitti:
Labyrintin sisältä löytää ulos sekä leveyssuuntaisella että syvyyssuuntaisella haulla, mutta välillä erot algoritmien tehokkuudessa ja muistinkäytössä ovat suuria.
Luin jutun läpi ja odotin saavani tähän jotain tarkennusta. Äkkiseltään tuntuisi että keskimäärin kumpikin on yhtä tehokas. Eli olisi lähinnä tuurista kiinni kumpi on tehokkaampi. Onko tästä jotain tarkempaa tietoa?
Leveyshaun hyviä puolia ovat, että se löytää varmasti lyhimmän reitin (ja kaikki alireitit matkan varrella käsiteltyihin ruutuihin ovat myös lyhimpiä) eikä joudu koluamaan pitkiä umpikujia. Tosiaankin riippuu hyvin paljon labyrintin rakenteesta ja siitä, mihin suuntaan syvyyshaku sattuu kulloinkin menemään, kumpi algoritmi lopulta on yksittäisessä tapauksessa tehokkaampi.
Tämä labyrinttiesimerkki vääristää tilannetta muistinkäytön suhteen, koska labyrintti on jo valmiina muistissa ja molemmat algoritmit voivat tehdä siihen merkintöjä varaamatta lisää muistia. Mutta jos hakuverkkoa muodostetaan vasta ohjelman suorituksen aikana, leveyshaku tarvitsee paljon lisämuistia, koska se joutuu pitämään kirjaa kaikista solmuista, joissa on jo käyty. Syvyyshaun taas täytyy tallentaa muistiin vain yksi reitti kerrallaan. Toisaalta syvyyshaku voi joutua todella pahasti harhateille, jos se lähtee tutkimaan valtavaa verkkoa (joka ei edes ehkä kokonaisuudessaan mahdu muistiin) väärästä suunnasta.
Syvyyshaun vähäinen muistinkäyttö ja leveyshaun lyhimmän reitin löytäminen voidaan jotenkuten yhdistää suorittamalla peräkkäisiä syvyyshakuja, joissa reitin pituus on rajoitettu ja suurinta pituutta kasvatetaan hakujen välillä. Tällöin syvyyshaku saadaan pidettyä aisoissa, mutta muistia ei kulu sen enempää kuin tavallisessa syvyyshaussakaan. Tämä ratkaisu on selvästi leveyshakua hitaampi, koska syvyyshakuja tehdään yleensä useita, mutta näin vältetään leveyshaun muistinkulutusongelmat.
Tuossa syvyyshaun syvyyden rajoittamisessa tulee vaan ongelma, jos se oikea reitti on pidempi kuin sallittu maksimipituus. Tällöin jouduttaisiin epäonnistumisen jälkeen kasvattamaan maksimisyvyyttä ja tekemään kaikki alusta.
Niin siinä valitettavasti käy. Mutta jos tavallinen syvyyshaku tai leveyshaku ei löydä ratkaisua lainkaan, suurenkin aikalisän voi hyväksyä rajoitetussa syvyyshaussa.
No, wikipedian mukaan tuo rajoitetun haun aikalisä ei ole kovinkaan merkittävä leveyshakuun verrattuna. Mitä isompi verkon haarautumiskerroin on sitä vähemmän tuo asteittainen syventäminen vaikuttaa testattavien solmujen määränä suhteessa leveyshakuun.
http://en.wikipedia.org/wiki/IDA*
Lisäksi en olisi yhtään yllättynyt, vaikka IDA* olisi nopeampi kuin leveyshaku myös tilanteissa, joissa verkko ja leveyshaun rakenteet mahtuvat kokonaan muistiin. Yleensä nimittäin ohjelmien muistinkäsittelyssä kuluu eniten aikaa ja IDA* pienimuistinkulutus vastaa leveyshaun suuri, voivat hyvinkin kallistaa nopeusedun IDA*:n puolelle. Vaikka muistia ei ladattaisikaan dynaamisesti, tulee muistinkäytön nopeutee ottaa huomioon myös kuinka hyvin algoritmin rakenteet sopivat eri välimuisteihin, mistä syystä usein vähemmän muistia käyttävä algoritmi on nopeampi ellei muistinkäytöllä saavuteta algoritmille parempaa aikakompleksisuutta.