Kierros 6: Dynaaminen ohjelmointi ja ahneet algoritmit

Samankaltaiset tiedostot
CS-A1140 Tietorakenteet ja algoritmit

Luku 6. Dynaaminen ohjelmointi. 6.1 Funktion muisti

A ja B pelaavat sarjan pelejä. Sarjan voittaja on se, joka ensin voittaa n peliä.

Tietorakenteet ja algoritmit

Kierros 4: Binäärihakupuut

Algoritmit 2. Luento 14 Ke Timo Männikkö

Algoritmien suunnittelu ja analyysi (kevät 2004) 1. välikoe, ratkaisuja

Johdatus diskreettiin matematiikkaan Harjoitus 5, Ratkaise rekursioyhtälö

f(n) = Ω(g(n)) jos ja vain jos g(n) = O(f(n))

4.3. Matemaattinen induktio

Algoritmit 1. Luento 13 Ti Timo Männikkö

Tietorakenteet ja algoritmit - syksy

Luku 8. Aluekyselyt. 8.1 Summataulukko

Algoritmit 1. Luento 13 Ma Timo Männikkö

Algoritmit 2. Luento 8 To Timo Männikkö

Tietorakenteet ja algoritmit Johdanto Lauri Malmi / Ari Korhonen

Algoritmit 2. Luento 9 Ti Timo Männikkö

Algoritmi on periaatteellisella tasolla seuraava:

V. V. Vazirani: Approximation Algorithms, luvut 3-4 Matti Kääriäinen

Algoritmit 2. Luento 13 Ti Timo Männikkö

ja λ 2 = 2x 1r 0 x 2 + 2x 1r 0 x 2

CS-A1140 Tietorakenteet ja algoritmit

Algoritmit 2. Luento 13 Ti Timo Männikkö

AVL-puut. eräs tapa tasapainottaa binäärihakupuu siten, että korkeus on O(log n) kun puussa on n avainta

Vaihtoehtoinen tapa määritellä funktioita f : N R on

Algoritmit 2. Luento 10 To Timo Männikkö

Esimerkkejä polynomisista ja ei-polynomisista ongelmista

Rekursio. Funktio f : N R määritellään yleensä antamalla lauseke funktion arvolle f (n). Vaihtoehtoinen tapa määritellä funktioita f : N R on

Algoritmit 1. Luento 12 Ti Timo Männikkö

58131 Tietorakenteet ja algoritmit (syksy 2015)

Algoritmit 1. Luento 11 Ti Timo Männikkö

Algoritmit 2. Luento 2 To Timo Männikkö

Vasen johto S AB ab ab esittää jäsennyspuun kasvattamista vasemmalta alkaen:

Algoritmit 2. Luento 3 Ti Timo Männikkö

Algoritmit 2. Luento 6 To Timo Männikkö

811312A Tietorakenteet ja algoritmit , Harjoitus 2 ratkaisu

(p j b (i, j) + p i b (j, i)) (p j b (i, j) + p i (1 b (i, j)) p i. tähän. Palaamme sanakirjaongelmaan vielä tasoitetun analyysin yhteydessä.

Algoritmit 2. Luento 7 Ti Timo Männikkö

811312A Tietorakenteet ja algoritmit Kertausta kurssin alkuosasta

1.4 Funktioiden kertaluokat

Hakupuut. tässä luvussa tarkastelemme puita tiedon tallennusrakenteina

Tietorakenteet, laskuharjoitus 1,

Algoritmit 1. Luento 12 Ke Timo Männikkö

Algoritmit 2. Luento 4 To Timo Männikkö

811312A Tietorakenteet ja algoritmit, , Harjoitus 7, ratkaisu

811312A Tietorakenteet ja algoritmit II Perustietorakenteet

Nopea kertolasku, Karatsuban algoritmi

Algoritmit 1. Luento 3 Ti Timo Männikkö

= 5! 2 2!3! = = 10. Edelleen tästä joukosta voidaan valita kolme särmää yhteensä = 10! 3 3!7! = = 120

4 Tehokkuus ja algoritmien suunnittelu

Algoritmit 2. Luento 3 Ti Timo Männikkö

58131 Tietorakenteet ja algoritmit (kevät 2016) Ensimmäinen välikoe, malliratkaisut

Tietorakenteet, laskuharjoitus 3, ratkaisuja

Valitaan alkio x 1 A B ja merkitään A 1 = A { x 1 }. Perinnöllisyyden nojalla A 1 I.

2. Seuraavassa kuvassa on verkon solmujen topologinen järjestys: x t v q z u s y w r. Kuva 1: Tehtävän 2 solmut järjestettynä topologisesti.

ALGORITMIT 1 DEMOVASTAUKSET KEVÄT 2012

SAT-ongelman rajoitetut muodot

815338A Ohjelmointikielten periaatteet Harjoitus 6 Vastaukset

Diskreetin matematiikan perusteet Laskuharjoitus 2 / vko 9

811312A Tietorakenteet ja algoritmit, VI Algoritmien suunnitteluparadigmoja

Esimerkkejä vaativuusluokista

Epädeterministisen Turingin koneen N laskentaa syötteellä x on usein hyödyllistä ajatella laskentapuuna

Ohjelmoinnin peruskurssien laaja oppimäärä

Graafit ja verkot. Joukko solmuja ja joukko järjestämättömiä solmupareja. eli haaroja. Joukko solmuja ja joukko järjestettyjä solmupareja eli kaaria

Verkon värittämistä hajautetuilla algoritmeilla

Algoritmit 2. Luento 2 Ke Timo Männikkö

Tietorakenteet ja algoritmit syksy Laskuharjoitus 1

Olkoon seuraavaksi G 2 sellainen tasan n solmua sisältävä suunnattu verkko,

Algoritmit 2. Luento 4 Ke Timo Männikkö

5 Kertaluokkamerkinnät

Algoritmit 1. Luento 10 Ke Timo Männikkö

Kierros 2: Järjestämisalgoritmeja

Tietorakenteet, laskuharjoitus 7, ratkaisuja

Ohjelmoinnin peruskurssien laaja oppimäärä

Ohjelmoinnin peruskurssien laaja oppimäärä

Kierros 1: Algoritmianalyysin ja tietorakenteiden perusteita

Miten osoitetaan joukot samoiksi?

Tietojenkäsittelyteorian alkeet, osa 2

Johdatus matemaattiseen päättelyyn

Algoritmit 1. Luento 2 Ke Timo Männikkö

Algoritmit 1. Luento 5 Ti Timo Männikkö

TIEA341 Funktio-ohjelmointi 1, kevät 2008

isomeerejä yhteensä yhdeksän kappaletta.

7.4 Sormenjälkitekniikka

811120P Diskreetit rakenteet

Diskreetin matematiikan perusteet Esimerkkiratkaisut 3 / vko 10

Sekvenssien segmentointi. Merkkijonoja. Sekvenssien segmentointi. ja dynaaminen ohjelmointi

verkkojen G ja H välinen isomorfismi. Nyt kuvaus f on bijektio, joka säilyttää kyseisissä verkoissa esiintyvät särmät, joten pari

Algoritmit 1. Luento 7 Ti Timo Männikkö

Ei-yhteydettömät kielet [Sipser luku 2.3]

Kääreluokat (oppikirjan luku 9.4) (Wrapper-classes)

Algoritmit 1. Demot Timo Männikkö

MS-A0402 Diskreetin matematiikan perusteet

Lausekielinen ohjelmointi II Ensimmäinen harjoitustyö

Algoritmit 1. Demot Timo Männikkö

Tietorakenteet ja algoritmit

Rekursiiviset palautukset [HMU 9.3.1]

Oikeasta tosi-epätosi -väittämästä saa pisteen, ja hyvästä perustelusta toisen.

811312A Tietorakenteet ja algoritmit Kertausta kurssin alkuosasta

TKT20001 Tietorakenteet ja algoritmit Erilliskoe , malliratkaisut (Jyrki Kivinen)

Transkriptio:

Kierros 6: Dynaaminen ohjelmointi ja ahneet algoritmit Tommi Junttila Aalto University School of Science Department of Computer Science CS-A1140 Data Structures and Algorithms Autumn 2017 Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 1 / 50

Materiaalia kirjassa Introduction to Algorithms, 3rd ed. (online via Aalto lib): Dynaaminen ohjelmointi: kappaleet 15.0, 15.1 ja 15.4 Ahneiden algoritmien perusteet: kappaleet 16.1 ja 16.2 Materiaalia muualla ja linkkejä: wikipedia-artikkeli dynaamisesta ohjelmoinnista wikipedia-artikkeli ahneista algoritmeista MIT Open Courseware video (videon toisen puoliskon lyhimpien polkujen algoritmeihin palataan myöhemmin kurssilla) Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 2 / 50

Dynaaminen ohjelmointi Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 3 / 50

Dynaaminen ohjelmointi on yleinen algoritmien suunnitteluperiaate Se soveltuu monien optimointiongelmien ratkaisemiseen; näissä on tavoitteena löytää paras ratkaisu ongelmaan Lyhyesti ja karkeasti kuvattuna se on täydellinen rekursiivinen haku osaongelmien ratkaisujen tallentamisella täydennettynä Termi dynaaminen ohjelmointi johtuu historiallisista syistä, lue lisää esim. tästä wikipedia-artikkelista Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 4 / 50

Esimerkki I: Fibonaccin luvut Aloitetaan erittäin yksinkertaisella esimerkillä (tuttu jo edellisiltä kursseilta) Fibonaccin lukujono F n = 0,1,1,2,3,5,8,... määritellään rekursiivisesti kaavoilla F 0 = 0, F 1 = 1 ja F n = F n 1 + F n 2 for n 2 Lukujen laskeminen helppoa rekursiivisesti def f i b ( n : I n t ) : B i g I n t = { r e q u i r e ( n >= 0) i f ( n == 0) 0 else i f ( n == 1) 1 else f i b ( n 1) + f i b ( n 2) Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 5 / 50

Kutsupuu laskettaessa arvoa fib(6): fib(6) fib(5) fib(4) fib(4) fib(3) fib(3) fib(2) fib(3) fib(2) fib(2) fib(1) fib(2) fib(1) fib(1) fib(0) fib(2) fib(1) fib(1) fib(0) fib(1) fib(0) fib(1) fib(0) fib(1) fib(0) Moni funktiokutsu esiintyy useasti kutsupuussa Toisin sanoen sama aliongelma fib(i) ratkaistaan monta kertaa Tämä heijastuu ajoaikaan: scala> measurecputime { f i b (45) res2 : ( BigInt, Double ) = (1134903170,49.203652944) Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 6 / 50

Mikä on tämän rekursiivisen algoritmin ajoaika? Rekursioyhtälö jos oletetaan lukujen yhteenlaskun olevan vakioaikaista: T (n) = T (n 2) + T (n 1) +O(1) ja T (1) = T (0) = O(1) Saadaan helposti alarajoja: T (n) F n = ϕn ( 1 ϕ )n 5 = ϕn 5 ( ϕ 1 )n 5 = Ω(ϕ n ), missä ϕ = 1.6180339887... on kultainen leikkaus T (n) 2T (n 2) +O(1) ja siten T (n) = Ω(2 n/2 ) Algoritmin ajoaika on vähintään eksponentiaalinen Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 7 / 50

Tarkennetaan vielä hiukan analyysiä Itse asiassa lukujen yhteenlasku tässä ei ole vakioaikaista koska lukujen F n binääriesityksen pituus kasvaa lineaarisesti suhteessa arvoon n termin O(1) rekursioyhtälössä pitäisi olla Ω(n) Huomataan myös: n on funktion argumentin arvo, ei sen binääriesityksen pituus Jos funktiota fib kutsutaan m-bittisellä arvona n, niin ajoaika on Ω(ϕ 2m ) suhteessa syötteen kokoon m Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 8 / 50

Fibonaccin luvut dynaamisella ohjelmoinnilla Sen sijaan, että laskettaisiin arvoja fib(i) uudelleen ja uudelleen, tallennetaan jo lasketut arvot assosiatiiviseen kuvaukseen ja käytetään näitä aina kun mahdollista (engl. memoization) def f i b ( n : I n t ) : B i g I n t = { r e q u i r e ( n >= 0) val m = scala. c o l l e c t i o n. mutable.map[ I n t, B i g I n t ] ( ) def i n n e r ( i : I n t ) : B i g I n t = m. getorelseupdate ( i, { i f ( i == 0) 0 else i f ( i == 1) 1 else i n n e r ( i 1) + i n n e r ( i 2) ) i n n e r ( n ) Ajoaika parantuu huomattavasti scala> measurecputime { f i b (1000) res14 : ( BigInt, Double ) = (<A number with 209 d i g i t s >,0.001016623) Tämä osatulosten tallentaminen on dynaamisen ohjelmoinnin avainkomponentti Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 9 / 50

Kutsupuu arvolle fib(6) osatulosten tallentamisella: fib(6) fib(5) fib(4) fib(4) fib(3) fib(3) fib(2) fib(3) fib(2) fib(2) fib(1) fib(2) fib(1) fib(1) fib(0) fib(2) fib(1) fib(1) fib(0) fib(1) fib(0) fib(1) fib(0) fib(1) fib(0) Vihreät solmut kuvaavat kutsuja, joiden arvo löytyy jo tallennettuna ja joiden alikutsupuita (punaisella) ei siis käydä läpi lainkaan Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 10 / 50

Uuden version ajoaika? Erilaisten kutsukertojen lukumäärä kerrottuna kussakin kuluvalla ajalla Oletetaan vakioaikainen lukujen yhteenlasku ja haku/päivitys-operaatiot kuvauksilla 1 : n O(1) = O(n) Tässä analyysissa ei tarvittu rekursioyhtälöitä Otettaessa huomioon kahden Θ(n)-bittisen luvun yhteenlaskuun kuluva lineaarinen aika saadaan rekursioyhtälö Muistin käyttö? Θ(n 2 ) T (n) = T (n 1) + Θ(n) = Θ(n 2 ) 1 Kuten ollaan aiemmin nähty, haku/päivitys-operaatiot ovat kuvauksille Scalan Map-piirrettä käyttäessä itse asiassa O(logn) tai keskimäärin O(1), mutta tässä tapauksessa taulukoiden avulla päästäisiin vakioaikaan. Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 11 / 50

Rekursiivinen verio ei ole häntärekursiivinen ja voi siis kuluttaa kaiken pinomuistin ( stack overflow error ) Sama laskenta voidaan tehdä helposti myös iteratiivisesti alhaalta ylös (engl. bottom-up), alkaen arvosta fib(0) ja tallentaen tulokset taulukkoon def f i b ( n : I n t ) : B i g I n t = { r e q u i r e ( n >= 0) val m = new Array [ B i g I n t ] ( n+1 max 2) m( 0 ) = 0 m( 1 ) = 1 for ( i < 2 to n ) m( i ) = m( i 2) + m( i 1) m( n ) scala> measurecputime { f i b (1000) res17 : ( BigInt, Double ) = (<A number with 209 d i g i t s >,7.05329E 4) Taulukoille haku- ja päivitysoperaatiot ovat vakioaikaisia Muistin käyttö? Θ(n 2 ) Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 12 / 50

Itse asiassa tarvitsee muistaa vain kaksi edellistä arvoa saadaan muistin käyttöä pienemmäksi Aloitetaan arvoista F 0 ja F 1 ja jatketaan arvoon F n def f i b ( n : I n t ) : B i g I n t = { r e q u i r e ( n >= 0) i f ( n == 0) return 0 var fprev = B i g I n t ( 0 ) var f = B i g I n t ( 1 ) for ( i < 1 u n t i l n ) { val fnew = fprev + f fprev = f f = fnew return f scala> measurecputime { f i b (1000) res48 : ( BigInt, Double ) = (<A number with 209 d i g i t s >,6.0929E 4) Muistin käyttö? Θ(n) Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 13 / 50

Sama funktionaalisella tyylillä def f i b ( n : I n t ) : B i g I n t = { r e q u i r e ( n >= 0) i f ( n == 0) 0 else (1 u n t i l n ). f o l d L e f t [ ( B i g I n t, B i g I n t ) ] ( 0, 1 ) ( {case ( ( fprev, f ), i ) => ( f, fprev+ f ) ). 2 Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 14 / 50

Ongelman ratkaiseminen dynaamisella ohjelmoinnilla 1 Määritellään (optimaalinen) ratkaisu (optimaalisten) osaratkaisujen avulla Tuloksena (optimaalisen) ratkaisun rekursiivinen määritelmä Fibonaccin lukujen esimerkissä ei ole optimaalisuuden vaatimusta ja rekursiivinen määritelmä oli jo valmiiksi annettuna 2 Tarkastellaan, esiintyvätkö osaratkaisut useaan kertaan rekursiivisessa kutsupuussa 3 Jos esiintyvät, käytetään osaratkaisujen tallentamista tai tehdään iteratiivinen versio, joka tuottaa osaratkaisut taulukkoon 2 2 Jos eivät esiinny, niin osaongelmat eivät limity ja dynaaminen ohjelmointi ei toimi. Joskus voi olla mahdollista tehdä ongelmalle toisenlainen rekursiivinen määritelmä, jossa osaongelmat limittyvät. Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 15 / 50

Esimerkki II: Tangon leikkaaminen Nyt rekursiivista määritelmää ei ole annettu valmiina vaan se joudutaan keksimään Tarkastellaan seuraavaa optimointiongelmaa: Annettuna materiaalitanko, jonka pituus on n yksikköä sekä taulukko (p 1,...,p n ) hintoja, jossa jokainen hinta p i määrää rahasumman, joka saadaan myymällä i yksikön mittainen pala tangosta. Tangon leikkaaminen paloihin on ilmaista. Mikä on paras tapa leikata tanko paloihin niin, että saadaan paras kokonaistuotto? Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 16 / 50

Esimerkki Jos tangon pituus alussa on 4 ja hinnat ovat 2 rahayksikköä tangonpaloille, joiden pituus on 1, 4 rahayksikköä tangonpaloille, joiden pituus on 2, 7 rahayksikköä tangonpaloille, joiden pituus on 3 ja 8 rahayksikköä tangonpaloille, joiden pituus on 4. Tuottavin tapa leikata tanko paloihin on tehdä yksi 1 yksikön pala ja yksi 3 yksikön pala. Näiden myyminen tuottaa 9 rahayksikköä. Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 17 / 50

Optimaalisen ratkaisun rekursiivinen määrittely Kuinka leika n yksikön tanko paloihin parhaalla tavalla? Olkoon maksimituotto s(n) s(n) on suurin seuraavista: Leikataan pois 1 yksikön pala ja sen jälkeen leikataan jäljelle jäävä n 1 mittainen tanko parhaalla tavalla tuotto on p 1 + s(n 1) Leikataan pois 2 yksikön pala ja sen jälkeen leikataan jäljelle jäävä n 2 mittainen tanko parhaalla tavalla tuotto on p 2 + s(n 2)... Leikataan pois n 1 yksikön pala ja sen jälkeen leikataan jäljelle jäävä 1 mittainen tanko parhaalla tavalla tuotto on p n 1 + s(1) Ei tehdä leikkauksia, tuotto on p n s(n) on siis s(n) = max i=1,...,n (p i + s(n i)) missä s(0) = 0, jotta saadaan viimeinen ei leikkauksia -tapaus hoidettua yhtenäisellä tavalla Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 18 / 50

Rekursiivisen määrittelyn avulla ongelman ratkaisevan ohjelman toteutus on suoraviivaista Scala-kielellä def cut ( n : I n t, p r i c e s : Array [ I n t ] ) = { r e q u i r e ( prices. length >= n+1) r e q u i r e ( p r i c e s ( 0 ) == 0 && p r i c e s. f o r a l l ( >= 0) ) def i n n e r ( k : I n t ) : Long = { i f ( k == 0) 0 else { var best : Long = 1 for ( i < 1 to k ) best = best max ( p r i c e s ( i ) + i n n e r ( k i ) ) best i n n e r ( n ) Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 19 / 50

Sama funktionaalisella tyylillä def cut ( n : I n t, p r i c e s : Array [ I n t ] ) = { r e q u i r e ( prices. length >= n+1) r e q u i r e ( p r i c e s ( 0 ) == 0 && p r i c e s. f o r a l l ( >= 0) ) def i n n e r ( k : I n t ) : Long = { i f ( k == 0) 0 else (1 to k ). i t e r a t o r.map( i => p r i c e s ( i ) + i n n e r ( k i ) ). max i n n e r ( n ) Miksi yllä on käytetty iteraattoria? Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 20 / 50

Kutsupuu kun n = 4 cut(4, prices) inner(4) inner(3) inner(2) inner(1) inner(0) inner(2) inner(1) inner(0) inner(1) inner(0) inner(0) inner(1) inner(0) inner(0) inner(0) inner(0) Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 21 / 50

Ajoaika? Kun tangon pituus on n, niin leikkauksia voidaan tehdä n 1 kohdasta on olemassa 2 n 1 tapaa leikata tanko koska algoritmi käy läpi jokaisen, sen ajoaika on Θ(2 n ) Voitaisiin parantaa rekursiivista ratkaisua ottamalla ensin kaikki yhden yksikön leikkaukset, sitten kahden yksikön jne. Tällä menettelyllä eri tapoja leikata tanko on sama määrä kuin tapoja osittaa luonnollinen luku n yhteenlaskettaviin. Tämä määrä kasvaa asymptoottisesti nopeammin kuin mikä tahansa muuttujan n polynomi (katso esim. partition function-wikipediasivu). Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 22 / 50

Dynaamisen ohjelmoinnin ratkaisu tallentamisella Koska osaongelmat esiintyvät useasti kutsupuussa, voidaan jälleen käyttää niiden ratkaisujen tallentamista def cut ( n : I n t, p r i c e s : Array [ I n t ] ) = { r e q u i r e ( prices. length >= n+1) r e q u i r e ( p r i c e s ( 0 ) == 0 && p r i c e s. f o r a l l ( >= 0) ) val m = scala. c o l l e c t i o n. mutable.map[ I n t, Long ] ( ) def i n n e r ( k : I n t ) : Long = m. getorelseupdate ( k, { i f ( k == 0) 0 else { var best : Long = 1 for ( i < 1 to k ) best = best max ( p r i c e s ( i ) + i n n e r ( k i ) ) best ) i n n e r ( n ) Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 23 / 50

Kutsupuu osaongelmien ratkaisujen tallentamisella kun n = 4 cut(4, prices) inner(4) inner(3) inner(2) inner(1) inner(0) inner(2) inner(1) inner(0) inner(1) inner(0) inner(0) inner(1) inner(0) inner(0) inner(0) inner(0) Jälleen vihreiden solmujen ratakaisut löytyvät tallennettuina ja niiden punaisia alipuita ei käydä ikinä läpi Ajoaika: T (0) = O(1) and T (n) = O(n) + T (n 1) = O(n 2 ) Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 24 / 50

Ratkaisun konstruointi Tangonleikkausongelmassa pelkkä maksimituotto ei välttämättä riitä vaan tahdotaan tietää myös kuinka se saadaan aikaiseksi eli millaisiin paloihin tanko tulee leikata sen aikaansaamiseksi Tämä tieto saadaan talteen yksinkertaisesti muistamalla jokaisella kutsulla inner(k) parhaan tuoton lisäksi myös se, miten se saatiin aikaiseksi 3 3 parhaita tapoja voi olle useita mutta pidetään muistissa vain yhtä koska vastaavia tapoja voi olla erittäin suuri määrä Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 25 / 50

Idea koodina def cut ( n : I n t, p r i c e s : Array [ I n t ] ) = { r e q u i r e ( prices. length >= n+1) r e q u i r e ( p r i c e s ( 0 ) == 0 && p r i c e s. f o r a l l ( >= 0) ) val m = scala. c o l l e c t i o n. mutable.map[ I n t, ( Long, L i s t [ I n t ] ) ] ( ) def i n n e r ( k : I n t ) : ( Long, L i s t [ I n t ] ) = m. getorelseupdate ( k, { i f ( k == 0) ( 0, N i l ) else { var best : Long = 1 var bestcuts : L i s t [ I n t ] = null for ( i < 1 to k ) { val ( subvalue, subcuts ) = i n n e r ( k i ) i f ( subvalue + p r i c e s ( i ) > best ) { best = subvalue + p rice s ( i ) bestcuts = i : : subcuts ( best, bestcuts ) ) i n n e r ( n ) Ajoaika on edelleen O(n 2 ) Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 26 / 50

Sama iteratiivisella alhaalta ylös -tyylillä ja taulukoilla: def cut ( n : I n t, p r i c e s : Array [ I n t ] ) = { r e q u i r e ( prices. length >= n+1) r e q u i r e ( p r i c e s ( 0 ) == 0 && p r i c e s. f o r a l l ( >= 0) ) val m = new Array [ ( Long, L i s t [ I n t ] ) ] ( n+1) m( 0 ) = ( 0, N i l ) / / The base case for ( k < 1 to n ) { var best : Long = 1 var bestcuts : L i s t [ I n t ] = null for ( i < 1 to k ) { val ( subvalue, subcuts ) = m( k i ) i f ( subvalue + p r i c e s ( i ) > best ) { best = subvalue + p rice s ( i ) bestcuts = i : : subcuts m( k ) = ( best, bestcuts ) m( n ) Muistin käyttö: Θ(n) Miksi parhaiden ratkaisujen tallentaminen ei nosta muistinkäyttöä neliölliseksi eli Θ(n 2 )? Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 27 / 50

Kertaus: milloin dynaaminen ohjelmointi toimii? Yleisesti ottaen, dynaamista ohjelmointia voidaan käyttää kun 1 (optimaalinen) ratkaisu voidaan määritellä käyttämällä pienempiä (optimaalisia) osaongelmien ratkaisuja ja 2 osaongelmat limittyvät eli saman osaongelman ratkaisua tarkastellaan useasti Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 28 / 50

Esimerkki III: Pisin yhteinen osamerkkijono Vielä yksi esimerkki... Tarkastellaan kahta merkkijonoa X = (x 1,...,x n ) ja Y = (y 1,...,y m ) Nämä voisivat olla vaikkapa DNA-sekvenssejä, tekstejä tms Tällaisten jonojen samankaltaisuudelle on monta erilaista määritelmää Tarkastellaan seuraavassa erästä: merkkijonojen X ja Y yhteinen osamerkkijono on merkkijono Z = (z 1,...,z k ), jonka merkit esiintyvät tässä järjestyksessä kummassakin merkkijonossa X ja Y pisin yhteinen osamerkkijono (engl. longest common subsequence, LCS) on yhteinen osamerkkijono, jolla on suurin mahdollinen pituus Mitä pidempiä pisimmät yhteiset osamerkkijonot ovat, sitä samankaltaisempia merkkijonot ovat Pisimmän yhteisen osamerkkijonon ongelma on: annettuna kaksi merkkijonoa, etsi yksi niiden pisimmistä yhteisistä osamerkkijonoista Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 29 / 50

Esimerkki Tarkastellaan merkkijonoja X = (A,C,G,T,A,T ) ja Y = (A,T,G,T,C,T ). Niiden (yksikäsitteinen) pisin yhteinen osamerkkijono on (A, G, T, T ). Esimerkki Pisimpiä yhteisiä osamerkkijonoja voi olla useita; merkkijonojen X = (C,C,C,T,T,T ) ja Y = (T,T,T,C,C,C) pisimmät yhteiset osamerkkijonot ovat (C,C,C) ja (T,T,T ) Yksinkertainen ratkaisu pisimpien yhteisten osamerkkojonojen löytämiselle olisi tuottaa ( pisin ensin -järjestyksessä) toisen merkkijonon kaikki osamerkkijonot ja tarkastaa, löytyvätkö ne toisesta merkkijonosta Tämä olisi tehotonta koska osamerkkijonoja on eksponentiaalinen määrä Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 30 / 50

Rekursiivinen määritelmä Jotta voitaisiin käyttää dynaamista ohjelmointia, määritellään optimiratkaisu pienempien optimaalisten osaratkaisujen avulla Merkitään merkkijonjen X ja Y alkuosia seuraavasti: X i = (x 1,...,x i ) kun 0 i n ja Y j = (y 1,...,y i ) kun 0 j m Idea: tarkastellaan merkkijonojen X n = (x 1,...,x n ) ja Y m = (y 1,...,y m ) viimeisiä merkkejä Jos x n = y m, niin saadan merkkijonjen X n ja Y m eräs LCS ottamalla merkkijonojen X n 1 ja Y m 1 mikä tahansa LCS ja lisäämällä sen loppuun merkki x n Jos x n y m ja merkkijonojen X n ja Y m LCS Z ei sisällä merkkiä x n viimeisempänä, niin Z on merkkijonojen X n 1 ja Y m LCS Jos x n y m ja merkkijonojen X n ja Y m LCS Z ei sisällä merkkiä y m viimeisempänä, niin Z on merkkijonojen X n ja Y m 1 LCS Perustapauksessa n = 0 tai m = 0; tällöin LCS on tyhjä merkkijono pituudeltaan 0 Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 31 / 50

Esimerkki Esitellään määritelmän eri tapauksia: 1 Jos X 6 = (A,C,G,T,A,T ) ja Y 5 = (A,G,C,C,T ), niin niiden pisimmät yhteiset osamerkkijonot ovat (A,C,T ) ja (A,G,T ). Nämä molemmat saadaa kahdesta merkkijonojen X 5 = (A,C,G,T,A) ja Y 4 = (A,G,C,C) pisimmästä yhteisestä osamerkkijonosta (A, C) ja (A, G) lisäämällä niiden loppuun merkki T 2 Jos X 5 = (A,G,C,A,C) ja Y 6 = (A,C,G,T,A,T ), niin niiden pisimmät osamerkkijonot ovat (A, C, A) ja (A, G, A). Kumpikaan ei sisällä merkkiä C viimeisenä merkkinä ja molemmat ovat merkkijonojen X 4 = (A,G,C,A) ja Y 6 = (A,C,G,T,A,T ) pisimmät yhteiset osamerkkijonot 3 Kolmas tapaus on symmetrinen edellisen tapauksen kanssa Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 32 / 50

Jälleen kerran toteutuksen tekeminen rekursiivisen määrittelyn avulla on helppoa: def l c s ( a : String, b : S t r i n g ) = { def i n n e r ( n : I n t, m: I n t ) : S t r i n g = { i f ( n < 0) else i f (m < 0) else i f ( a ( n ) == b (m) ) i n n e r ( n 1, m 1) + a ( n ) else Seq ( i n n e r ( n 1, m), i n n e r ( n, m 1) ). maxby(. length ) i n n e r ( a. length 1, b. length 1) Huom: indeksointi alkaa indeksistä 0, ei 1 kuten matemaattisessa määritelmässä Ajoaika on jälleen eksponentiaalinen pahimmassa tapauksessa... mutta aika hyvä joillekin syötteille, esim. lcs( abcab, cab ). Miksi? Merkkien lisääminen merkkijonojen loppuun ei myöskään ole tehokasta eli myös tässä suhteessa ensimmäinen toteutus jättää parantamisen varaa Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 33 / 50

Eräs kutsupuu lcs('abac','acab') inner(3,3) inner(2,3) inner(3,2) inner(1,3) inner(2,2) inner(2,2) inner(3,1) inner(0,2) inner(1,1) inner(1,1) inner(2,0) inner(-1,1) inner(0,1) inner(1,0) inner(0,1) inner(1,0) inner(1,-1) inner(-1,1) inner(0,0) inner(0,0) inner(1,-1) inner(-1,1) inner(0,0) inner(0,0) inner(1,-1) inner(-1,-1) inner(-1,-1) inner(-1,-1) inner(-1,-1) Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 34 / 50

Toinen kutsupuu lcs('ccc','bbb') inner(2,2) inner(1,2) inner(2,1) inner(0,2) inner(1,1) inner(1,1) inner(2,0) inner(-1,2) inner(0,1) inner(0,1) inner(1,0) inner(0,1) inner(1,0) inner(1,0) inner(2,-1) inner(-1,1) inner(0,0) inner(-1,1) inner(0,0) inner(0,0) inner(1,-1) inner(-1,1) inner(0,0) inner(0,0) inner(1,-1) inner(0,0) inner(1,-1) inner(-1,0) inner(0,-1) inner(-1,0) inner(0,-1) inner(-1,0) inner(0,-1) inner(-1,0) inner(0,-1) inner(-1,0) inner(0,-1) inner(-1,0) inner(0,-1) Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 35 / 50

Osatulosten tallentaminen (vain pituus, ei osamerkkijonoa) def l c s ( a : String, b : S t r i n g ) = { val mem = scala. c o l l e c t i o n. mutable. HashMap [ ( I n t, I n t ), I n t ] ( ) def i n n e r ( n : I n t, m: I n t ) : I n t = mem. getorelseupdate ( ( n, m), { i f ( n < 0) 0 else i f (m < 0) 0 else i f ( a ( n ) == b (m) ) i n n e r ( n 1, m 1) + 1 else Seq ( i n n e r ( n 1, m), i n n e r ( n, m 1) ). max ) i n n e r ( a. length 1, b. length 1) Perustele seuraava väite: Algoritmin ajoaika on O(nm), missä n on merkkijonon a pituus ja m merkkijonon b Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 36 / 50

Osatulosten tallentaminen ja parhaan osamerkkijonon konstruointi def l c s ( a : String, b : S t r i n g ) = { val mem = scala. c o l l e c t i o n. mutable. HashMap [ ( I n t, I n t ), I n t ] ( ) def i n n e r ( n : I n t, m: I n t ) : I n t = mem. getorelseupdate ( ( n, m), { i f ( n < 0) 0 else i f (m < 0) 0 else i f ( a ( n ) == b (m) ) i n n e r ( n 1, m 1)+1 else Seq ( i n n e r ( n 1, m), i n n e r ( n, m 1) ). max ) i n n e r ( a. length 1, b. length 1) def r e c o n s t r u c t ( n : I n t, m: I n t ) : L i s t [ Char ] = { i f ( n < 0) N i l else i f (m < 0) N i l else i f ( a ( n ) == b (m) ) a ( n ) : : r e c o n s t r u c t ( n 1, m 1) else i f (mem( ( n, m) ) == mem( n 1, m) ) r e c o n s t r u c t ( n 1, m) else reconstruct ( n,m 1) r e c o n s t r u c t ( a. length 1, b. length 1). reverse. mkstring ( ) Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 37 / 50

Iteratiivinen versio Rekursiivisten toteutusten suurin haitta, myös osatulosten tallentamisen yhteydessä, on suurten rekursiosyvyyksien aiheuttama pinomuistin loppuminen ( stack overflow error ) järjestelmissä, joissa rekursiopinolle on varattu vain kiinteä määrä muistia Esimerkiksi edellä annetun Scala-ohjelman ajaminen tuhansien merkkien pituisilla merkkijonoilla, joilla on pitkä LCS, saa aikaan pinomuistin loppumisen tyypillisesti konfiguroidussa Java-virtuaalikoneessa Esitellään siis iteratiivinen versio, joka tallentaa merkkijonojen X i ja Y j pisimmän yhteisen osamerkkijon pituuden 2-uloitteisen taulukon table alkioon table[i,j] ja työskentelee alhaalta ylöspäin alkaen perustapauksista i = 0 ja j = 0 sekä laskien arvot table[i-1,j], table[i,j-1] ja table[i-1,j-1] ennen arvoa table[i,j] lopputulos on alkiossa table(n,m) Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 38 / 50

def l c s ( x : String, y : S t r i n g ) : S t r i n g = { val ( n, m) = ( x. length, y. length ) val t a b l e = Array. t a b u l a t e [ I n t ] ( n+1,m+1) ({ case ( i, j ) => 0) for ( i < 1 to n ; j < 1 to m) { i f ( x ( i 1) == y ( j 1) ) t a b l e ( i ) ( j ) = t a b l e ( i 1) ( j 1) + 1 else i f ( t a b l e ( i 1) ( j ) >= t a b l e ( i ) ( j 1) ) t a b l e ( i ) ( j ) = t a b l e ( i 1) ( j ) else t a b l e ( i ) ( j ) = t a b l e ( i ) ( j 1) / / Reconstruct a s o l u t i o n var i = n var j = m var s o l u t i o n : L i s t [ Char ] = N i l while ( i > 0 && j > 0) { i f ( x ( i 1) == y ( j 1) ) { s o l u t i o n = x ( i 1) : : s o l u t i o n i = 1 j = 1 else i f ( t a b l e ( i 1) ( j ) >= t a b l e ( i ) ( j 1) ) i = 1 else j = 1 s o l u t i o n. mkstring ( ) Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 39 / 50

Muodostetaan taulukko merkkijonoille X = (A,G,C,T,C,T ) ja Y = (A,C,G,T,A,T ) A G C T C T 0 1 2 3 4 5 6 A 0 1 C 2 G 3 T 4 A 5 T 6 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 1 1 2 2 2 2 0 1 2 2 2 2 2 0 1 2 2 3 3 3 0 1 2 2 3 3 3 0 1 2 2 3 3 4 Nuolet kuvaavat suuntaa/suuntia, jotka tuottavat pisimmän yhteisen osamerkkijonon Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 40 / 50

Ahneet algoritmit Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 41 / 50

Edellisissä esimerkeissä dynaamisen ohjelmoinnin piti tutkia monia osaongelmia: minkä kokoinen pala leikataan ensin tangosta? kaksi tapausta kun x n y m pisimmän yhteisen osamerkkijonon ongelmassa Joskus on mahdollista valita ahneesti vain yksi tapaus ja täten käytännössä redusoida ongelma yhdeksi pienemmäksi osaongelmaksi Tarkastellaan seuraavassa yhtä yksinkertaista ahnetta algoritmia Verkkoja käsittelevällä kierroksella nähdään muita ahneita algoritmeja kun tarkastellaan pienimmän virittäjäpuun ongelmaa Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 42 / 50

Esimerkki IV: Toimintojen valinta -ongelma Annettuna äärellinen joukko toimintoja A = {a 1,...,a n Jokaiseen toimintoon a i on liitetty aloitusaika s i ja lopetusaika f i siten, että s i < f i Toiminto suoritetaan puoliavoimena ajanjaksona [s i,f i ) Kaksi toimintoa, a i ja a j kun i j, ovat yhteensopivia jos niitä ei suoriteta yhtä aikaa eli jos joko f i s j tai f j s i Tehtävänä on löytää (jokin) maksimaalinen osajoukko A A siten, että kaikki toiminnot osajoukossa ovat keskenään yhteensopivia Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 43 / 50

Esimerkki Tarkastellaan seuraavaa toimintojen joukkoa A = {a 1,...,a 11, missä i 1 2 3 4 5 6 7 8 9 10 11 s i 1 3 0 5 3 5 6 8 8 2 12 f i 4 5 6 7 9 9 10 11 12 14 16 Eräs maksimaalinen keskenään yhteensopivien toimintojen joukko on {a 2,a 4,a 9,a 11. a 4 a 5 a 6 a 10 a 3 a 9 a 1 a 2 a 7 a 8 a 11 Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 44 / 50

Dynaamiseen ohjelmointiin perustuva ratkaisu Olkoon toimintojen joukko A = {a 1,...,a n Olkoon A l,u = {a i A l s i f i u niiden toimintojen joukko, joiden suoritus (i) alkaa ajanhetkenä l tai myöhemmin ja (ii) loppuu ennen ajanhetkeä u Tällöin A = A L,U, missä L = min i {1,...,n s i ja U = max i {1,...,n f i Jokaisella (osa)ongelmalla A l,u on seuraava optimaalisen osaongelman ominaisuus: Olkoon a i A l,u mikä tahansa toiminto Olkoot A l,s i ja A f i,u mitä tahansa maksimaalisia keskenään yhteensopivia toimintojen joukkoja osaongelmilla A l,si and A fi,u Tällöin A l,u at i = A l,s i {a i A f i,u on (eräs) sellainen suurin keskenään yhteensopivien toimintojen osajoukko (osa)ongelmalle A l,u, joka sisältää toiminnon a i Saadaan siis osaongelman A l,u maksimaalinen keskenään yhteensopivien toimintojen osajoukko A l,u ottamalla suurin joukosta {A l,u at j a j A l,u Ratkaisu alkuperäiseen ongelmaan on A L,U Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 45 / 50

On saatu rekursiivinen määritelmä ongelmalle ja tästä saadaan dynaamisen ohjelmoinnin algoritmi ongelmalle Huomaa, että riittää tarkastella (n + 1) (n + 1) kappaletta osaongelmia A l,u Täten dynaamisen ohjelmoinnin algoritmi saadaan helposti toimimaan polynomisessa ajassa Tämä ei päde suoraviivaiselle ratkaisulle, jossa listataan kaikki joukon A osajoukot, tarkastetaan jokaisen keskinäinen yhteensopivuus ja muistetaan suurin löydetty keskenään yhteensopiva osajoukko. Koska osajoukkoja on eksponentiaalinen määrä, myös tämän ratkaisun ajoaika olisi eksponentiaalinen. Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 46 / 50

Ahne algoritmi ongelmalle Toimintojen valinta -ongelmalla on eräs ominaisuus, jota voidaan käyttää vielä yksinkertaisemman ja nopeamman algoritmin tekemiseen Ei nimittäin tarvitse tarkastella kaikkia kulloisen osajoukon toimintoja vaan riittää valita toiminto, joka päättyy mahdollisimman aikaisin ja tämän jälkeen tarkastella osaongelmaa, josta tämä toiminto ja kaikki sen kanssa epäyhteensopivat toiminnot on poistettu Tämä optimointi perustuu havaintoon, että jos on olemassa maksimaalinen keskenään yhteensopivien toimintojen osajoukko, joka ei sisällä aikaisimmin päättyvää toimintoa a i, niin voidaan korvata osajoukon toinen toiminto toiminnolla a i ja saada aikaan yhtä suuri maksimaalinen keskenään yhteensopivien toimintojen joukko Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 47 / 50

Teoreema Olkoot A A maksimaalinen keskenään yhteensopivien toimintojen osajoukko, a i aikaisimman lopetusajan omaava toiminto joukossa A ja a j aikaisimman lopetusajan omaava toiminto joukossa A. Nyt (A \ {a j ) {a i on myös maksimaalinen keskenään yhteensopivien toimintojen osajoukko. Todistus Jos i = j, niin kaikki on kunnossa. Jos i j, niin täytyy päteä, että f i f j. Koska a j on aikaisimman lopetusajan omaava toiminto joukossa A, A ei sisällä yhtään a k, k j, jolle s k f i. Täten voidaan korvata toiminto a j toiminnolla a i joukossa A ja saadaan yhtä suuri maksimaalinen keskenään yhteensopivien toimintojen osajoukko. Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 48 / 50

Esimerkki Tarkastellaan taas toimintojen joukkoa A = {a 1,...,a 11, missä i 1 2 3 4 5 6 7 8 9 10 11 s i 1 3 0 5 3 5 6 8 8 2 12 f i 4 5 6 7 9 9 10 11 12 14 16 Eräs maksimaalinen ratkaisu on {a 2,a 4,a 9,a 11 Tämä ei sisällä joukon A aikaisimmin päättyvää toimintoa a 1 Vaihdetaan a 1 joukon aikaisimmin päättyvän toiminnon a 2 tilalle ja saadaan uusi maksimaalinen ratkaisu {a 1,a 4,a 9,a 11 {a 4,a 9,a 11 on karsitun osaongelman {a 4,a 6,a 7,a 8,a 9,a 11, josta siis poistettu a 1 ja kaikki sen kanssa epäyhteensopivat toiminnot, eräs maksimaalinen ratkaisu a 4 a 5 a 6 a 10 a 3 a 9 a 1 a 2 a 7 a 8 a 11 Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 49 / 50

Ahneen algoritmin Scala-toteutus def a c t i v i t y S e l e c t o r ( acts : Seq [ ( I n t, I n t ) ] ) = { val sorted = acts. sortby (. 2 ) val s o l = scala. c o l l e c t i o n. mutable. A r r a y B u f f e r [ ( I n t, I n t ) ] ( ) var time = I n t. MinValue for ( act < sorted ) { i f ( act. 1 >= time ) { s o l += act time = act. 2 s o l. t o L i s t Esimerkkiongelman ratkaiseminen sillä: scala> val acts = L i s t ( ( 1, 4 ), ( 3, 5 ), ( 0, 6 ), ( 5, 7 ), ( 3, 9 ), ( 5, 9 ), ( 6, 1 0 ), ( 8, 1 1 ), ( 8, 1 2 ), ( 2, 1 4 ),(12,16) ) acts : L i s t [ ( I n t, I n t ) ] = L i s t ( ( 1, 4 ), ( 3, 5 ), ( 0, 6 ), ( 5, 7 ), ( 3, 9 ), ( 5, 9 ), (6,10), (8,11), (8,12), (2,14), (12,16) ) scala> a c t i v i t y S e l e c t o r ( acts ) res0 : L i s t [ ( I n t, I n t ) ] = L i s t ( ( 1, 4 ), ( 5, 7 ), (8,11), (12,16) ) Tommi Junttila (Aalto University) Kierros 6 CS-A1140 / Autumn 2017 50 / 50