Dynamisk programmering Undervises av Stein Krogdahl 5. september 2012 Dagens stoff er hentet fra kapittel 9 i læreboka, samt kapittel 20.5 (som vi «hoppet over» sist) Kapittel 9 er lagt ut på undervisningsplanen. Disse foilene ble opprinnelig laget av Petter Kristiansen (nå gruppelærer), og de fulgte starten av kapittelet nøye. Jeg har laget en noe annen start på dem som jeg synes blir enklere å forstå (men det kan jo være smak og behag!) Jeg skal skrive noe om forskjellen på bokas og disse foilenes innledning, og legge det ut på undervisningsplanen.
Dynamisk programmering Dynamisk programmering ble formalisert av Richard Bellmann (RAND Corporation) på 50-tallet. Programmering i betydningen planlegge, ta beslutninger. (Har ikke noe med kode eller å skrive kode å gjøre.) Dynamisk for å indikere at det er en stegvis prosess. Men også et pynteord.
Hva kan løses med Dynamisk Programmering? Dynamisk programmering brukes typisk til å løse optimaliseringsproblemer. (Problemer hvor det kan være mange mulige («feasible») løsninger, og hvor vi ønsker å finne den beste vi ønsker å optimere verdien av en angitt objektiv-funksjon (eller objekt-funksjon?). Problemet må være slik at hver instans naturlig har en størrelse (som for O- notasjon), gjerne slik at de minste instansene har størrelse 1 eller 0. Videre må man kunne løse en instans (et problem) av størrelse n om man vet løsningen på et antall mindre instanser. Disse må være lette å sette opp ut fra det opprinnelige problemet, og alle er altså mindre enn n. De instansene vi på denne måten setter opp, kaller vi «del-problemer» av det store problemet. Mengden av delproblemer vi må løse for å løse det store problemet må være grei og sette opp, og være polynomisk i antall. Disse delproblemene settes typisk opp i en slags tabell, matrise, e.l. Løsningen gjøres så ved å beregne alle de aktuelle delproblemene, i rekkefølge fra de minste til det store problemet selv. De minste delproblemene må være greie å løse direkte
Overlappende delproblemer En dynamisk programmerings-algoritme starter med å løse de minste delproblemene, og setter sammen disse løsningene til løsninger på større og større problemer. Løsningene lagres etter hvert i en passelig array, matrise e.l. Dynamisk programmering er spesielt nyttig om samme delproblem skal brukes flere ganger for forskjellige større delproblemer. Siden løsningene på delproblemene lagres i en tabell, og slipper vi å løse delproblemer flere ganger. Under er størrelsen på en instans C(i,j) lik i - j
Polynomisk antall delproblemer En algoritmes effektivitet er direkte avhengig av antall delproblemer som løses: er det få (polynomisk), blir algoritmen effektiv, er det mange (eksponensielt) blir algoritmen ikke praktisk nyttig. C(1,n) Ikke egnet om det genereres et eksponensielt antall delproblemer
«Divide and conquer» vs. dynamisk programmering Divide and conquer (f.eks Quicksort) Top down (rekursivt kall) Best egnet når delproblemene er uavhengige av hverandre. Da re-beregnes ikke løsninger på delproblemer. Kun relevante delproblemer løses. Dynamisk programmering Gjøres i utganspunktet nedenfra-opp, altså slik at de enkleste delproblemene løses først, og svarene blir satt inn i en tabell. Egner seg spesielt når delproblemer overlapper. Svaret på hvert delproblem beregnes bare en gang, og siden kan svarene hentes i tabellen Og altså: Vi løser delproblemer i en slik rekkefølge at når vi trenger svaret på et bestemt (mindre) delproblem så er det allerede løst og svaret er satt inn i tabellen. «Top-Down» dynamisk programmering (Memoisering) En ulempe med metoden over er at vi beregner løsninger på alle delproblemer, selv de vi ikke vil trenge alle i den aktuelle beregningen. Vi kan i stedet gjøre beregningen (rekursivt) ovenfra-ned, og sette svaret inn i den samme tabellen. Må da ha en spesialverdi: «ikke beregnet». Litt mer om dette på slutten (eller hvertfall på gruppene).
Eksempel 1: Strenger som likner (kap. 20.5) Slike problemer er veldig aktuelle bl.a. i gen-forskning En streng P er en k-approksimasjon av en streng T dersom T kan konverteres til P ved å utføre maksimalt k av følgende operasjoner: Substitusjon Et symbol i T byttes ut med et annet. Tillegg Et nytt symbol legges til et sted i T. Sletting Et symbol slettes fra T. Edit distance, ED(P,T ), mellom to strenger T og P er det minste antall slike operasjoner som trengs for å konvertere T til P (eller P til T!). Eks. logarithm alogarithm algarithm algorithm (+a, -o, a/o) T P
Strenger som likner Gitt to strenger T og P, vi ønsker å finne edit distance mellom disse. Vi bruker en matrise D som under, og tar sikte på fylle den slik at: D [i, j ] = ED( P [1: i ], T [1: j ] ). Vi tenker oss at P ligger nedover langs venstre kant av matrisen, og at T ligger langs overkanten av matrisen. Vi ser på P opp til indeks i og T opp til indeks j. T P 0 1 j -1 j 0 0 1 j -1 j 1 1... i -1 i -1 i i? De «minste» problemene er langs øverste og venstre kant, der P eller T er tomme. Hvorfor kan vi initialisere disse som over? Hvordan kan vi finne svaret i én rute ut fra ruter for mindre problemer?
Strenger som likner Gitt to strenger T og P, ønsker vi å finne edit distance mellom disse. Vi ønsker D [i, j ] = ED( P [1: i ], T [1: j ] ). Dermed vil svaret for hele T og P til slutt bli stående i D[m.n] Vi deler i følgende delproblemer: Tilfelle 1: Hvis P [ i ] = T [ j ]. så er D [ i, j ] = D [ i -1, j -1]. Se figur under: 1 i = a P[1: i ] 1 j a T[1: j ]
Strenger som ligner Tilfelle 2: Strenger som likner: Om T [ j ] ikke er lik P [ i ] Substitusjon sette T [ j ] = P [ i ] 1 i 1 j a n n e P[1: i ] a n n f T[1: j ] Tillegg i T legge til symbol i T i pos T [ j ] sletting av P [ i ] 1 i a n n e P[1: i ] 1 j a n n T[1: j ] Sletting i T sletting symbol i T på pos T [ j ] 1 i 1 j a n n P[1: i ] a n n e T[1: j ] Regner ut D [ i, j ] ved å finne D [ i -1, j -1] (edit distance for det grå området), og legger til 1 for substitusjonen. Å legge til en e i T er det samme som å slette en i P. Regner ut D [ i, j ] ved å finne D [ i -1, j ] (edit distance for det grå området), og legger til 1 for slettingen i P. Regner ut D [ i, j ] ved å finne D [ i, j -1] (edit distance for det grå området), og legger til 1 for slettingen i T.
Strenger som likner Formelen (rekursiv definisjon) for D[ i, j ] vil altså være som følger: 0 1 j -1 j 0 0 1 j -1 j 1 1... i -1 i -1 i i
Strenger som likner Formelen (rekursiv definisjon) for D[ i, j ] vil altså være som følger: Løsningen, edit distance mellom strengene, finnes i D[m,n] (P er av lengde m og T av lengde n).
Strenger som likner Formelen (rekursiv definisjon) for D[ i, j ] vil altså være som følger: 0 1 j -1 j 0 0 1 j -1 j 1 1... i -1 i -1 i i +1
Strenger som likner function EditDistance ( P [1:n ], T [1:m ] ) i 0 j 0 for i 0 to n do D[ i, 0 ] i for j 1 to m do D[ 0, j ] j for i 1 to n do for j 1 to m do If P [ i ] = T [ j ] then D[ i, j ] D[ i -1, j - 1 ] else D[ i, j ] min { D[i -1, j - 1] +1, D[i -1, j ] +1, D[i, j - 1] +1 } endif endfor endfor return( D[ n, m ] ) end EditDistance
Strenger som likner Eks. T D a n e 0 1 2 3 j 0 0 1 2 3 del T del T a 1 1 0 1 2 P ins T n 2 2 1 0 1 n 3 3 2 1 1 e 4 4 3 2 1 i
Strenger som likner Eks. T D a n e 0 1 2 3 j 0 0 1 2 3 del T del T a 1 1 0 1 2 P ins T n 2 2 1 0 1 n 3 3 2 1 1 e 4 4 3 2 1 i
Eksempel 2: Optimal matrisemultiplikasjon Vi er gitt en sekvens M 0, M 1,, M n -1 av matriser og ønsker å beregne produktet M 0 M 1 M n -1. Merk at «formatene» her må være riktig hele veien. Radlengden i f.eks. M 1 må være lik kolonnelengden i M 2 Matrisemultiplikasjon er assosiativ, altså: (A B) C = A (B C) (men ikke symmetrisk, siden A B generelt er forskjellig fra B A ) Dermed kan man utføre multiplikasjonen i forskjellige rekkefølger. Med fire matriser kan det gjøres på følgende måter: (M 0 (M 1 (M 2 M 3 ))) (M 0 ((M 1 M 2 ) M 3 )) ((M 0 M 1 ) (M 2 M 3 )) ((M 0 (M 1 M 2 )) M 3 ) (((M 0 M 1 ) M 2 ) M 3 ) Kostnaden (antall enkelt-multiplikasjoner) kan variere veldig mellom de ulike måtene å sette parenteser på. Vi ønsker å finnne den med færrest mulig.
Optimal matrisemultiplikasjon Gitt to matriser, der antall rader (= kolonnelengden) står først A = p q matrise, B = q r matrise. Kostnaden (antall skalare multiplikasjoner) ved å beregne A B er p q r (A B) er en p r matrise. Eks. Beregn A B C, hvor A er en 10 100 matrise, B er en 100 5 matrise, og C er en 5 50 matrise. Å beregne D = (A B) koster 5,000 og gir en 10 5 matrise. Å beregne D C koster 2,500. Total kostnad for (A B) C blir 7,500. Å beregne E = (B C) koster 25,000 og gir en 100 50 matrise. Å beregne A E koster 50,000. Total kostnad for A (B C) blir 75,000.
Optimal matrisemultiplikasjon Gitt en sekvens av matriser M 0, M 1,, M n -1, ønsker vi å beregne produktet på billigst mulig måte vi vil finne optimal parentes-struktur. En parenterisering av sekvensen er en oppdeling i to delsekvenser, som hver for seg må parenteriseres: (M 0 M 1 M k ) (M k + 1 M k + 2 M n-1 ) Vi må prøve alle mulige k for å finne hvor det er best å dele sekvensen. Hvert delingspunkt gir opphav til to delproblemer parenterisering av venstre og høyre delsekvens. Består sekvensen bare av to matriser, er det bare én mulig parenterisering Har vi en optimal parenterisering av en sekvens av matriser, så må parenteriseringen av hver delsekvens også være optimal. Ellers kan vi jo bare bytte ut den som ikke var optimal med en bedre en.
Optimal matrisemultiplikasjon La d 0, d 1,, d n være dimensjonene til sekvensen av matriser M 0, M 1,, M n-1, slik at matrise M i har dimensjon d i d i+1. La m i,j være kostnaden av en optimal parenterisering av M i, M i+1,, M j Formelen (rekursiv definisjon) for m i,j vil være som følger: m i, j { m + m + d d d }, for alle 0 i < j n 1 = min i, k k, j i k + 1 j + 1 i k< j = 0, for alle 0 i n m i, i 1 Kostnaden av å utføre hele multiplikasjonen med den optimale parenteriseringen vil da være m 0,n -1
Optimal matrisemultiplikasjon d 30 35 15 5 10 20 25 Eksempel 0 Matrisen m 1 Andre indeks j 2 3 4 5 15,125 0 11,875 10,500 9,375 7,125 5,375 Første indeks i 7,875 4,375 2,500 3,500 15,750 2,625 750 1,000 5,000 1 2 3 m 1,4 = min(d 1 d 2 d 5 + m(1,1) + m(2,4), d 1 d 3 d 5 + m(1,2) + m(3,4), d 1 d 4 d 5 + m(1,3) + m(4,4)) 4 = min(35 15 20 + 0 + 2,500, 35 5 20 + 2,625 + 1,000, 35 10 20 + 4,375 + 0) = min(13000, 7125, 11375) 5 =7125 0 0 0 0 0 0
0 m 1 2 3 Optimal matrisemultiplikasjon M 1 M 2 M 3 M 4 5 = M 1 (M 2 M 3 M 4 ) 4 = (M 1 M 2 ) (M 3 M 4 ) c = (M 3 1 M 2 M 3 ) M 4 2 2 1 4 5 15,125 9,375 7,125 5,375 7,875 4,375 2,500 3,500 15,750 2,625 750 1,000 5,000 0 0 0 0 0 0 0 11,875 10,500 1 2 3 4 5 0 Registrering av hvor optimalt delepunkt ligger i M 1 M 2 M 3 M 4. Det ligger etter M 2 1 2 3 4
Optimal matrisemultiplikasjon c 3 4 5 0 2 2 2 1 2 2 2 2 2 3 1 0 2 2 4 4 5 0 0 1 2 3 4 m 4 15,125 1 3 11,875 10,500 2 2 9,375 7,125 5,375 3 1 7,875 4,375 2,500 3,500 4 0 15,750 2,625 750 1,000 5,000 0 0 0 0 0 0 5
Optimal matrisemultiplikasjon c 1 5 0 4 2 1 3 2 2 2 2 2 2 2 3 0 2 2 4 0 1 2 3 4 4 M 0 M 1 M 2 M 3 M 4 M 5 = (M 0 M 1 M 2 ) (M 3 M 4 M 5 ) = ((M 0 ) (M 1 M 2 )) ((M 3 M 4 ) (M 5 ))
Optimal matrisemultiplikasjon function OptimalParens( d[0 : n 1] ) for i 0 to n-1 do m[i, i] 0 for diag 1 to n 1 do // hjelpevariabel for å fylle ut tabellen nivå for nivå for i 0 to n 1 diag do j i + diag m[i, j] for k i to j 1 do q m[i, k] + m[k + 1, j] + d[i] d[k + 1] d[j + 1] if q < m[i, j] then m[i, j] q c[i,j] k endif return m[0, n 1] end OptimalParens
Eksempel 3: Optimale søketrær 0 Pat 1 Joe Ray 2 Ann 3
Optimale søketrær 0 Pat 1 Joe Ray 2 Ann 3 Ann Joe Pat Ray p 0 p 1 p 2 p 3 q 0 q 1 q 2 q 3 q 4 3 3 3 2 2 1 2 2 2
Optimale søketrær 0 Pat 1 Joe Ray 2 Ann 3 Sum av p-er og q-er er 1 Ann Joe Pat Ray p 0 p 1 p 2 p 3 q 0 q 1 q 2 q 3 q 4 3 3 3 2 2 1 2 2 2 Gjennomsnittlig søketid: 3p 0 + 2p 1 + 1p 2 + 2p 3 + 3q 0 + 3q 1 + 2q 2 + 2q 3 + 2q 4
Optimale søketrær For generelle søketrær får vi følgende formel for gjennomsnittlig antall sammenlikninger som gjøres: T et tre med n nøkler (søkeord lagret i interne noder) K 0,, K n -1. n +1 bladnoder tilsvarer intervaller I 0,, I n mellom nøklene. sannsynlighetsvektorer p og q for nøklene og intervallene mellom dem. d i er nivået til nøkkel K i, e i er nivået til bladnode som korresponderer med I i. n 1 A ( T, n, p, q) = p ( d + 1) + i i i = 0 i = 0 n q i e i
Optimale søketrær Hvis p i -ene er like og q i -ene er like, vil det komplette binære søketreet være det optimale. Men noen ord kan være oftere søkt etter enn andre (p i -ene ulike), derfor kan det lønne seg med skjeve trær og deltrær, for å få ord som det ofte blir søkt etter så høyt som mulig opp i treet. Vi ønsker å finne det optimale binære søketreet T over alle mulige, gitt søkesannsynlighetene (p i -ene og q i -ene). Dvs treet som minimerer gjennomsnittlig antall sammenlikninger A(T,n,p,q): min A( T, n, P, Q) T p i -ene og q i -ene er i utgangspunktet sannsynligheter (tall i intervallet [0,1], som summerer til 1), men vi kan slakke på kravet og anta at de er positive reelle tall, det er uansett bare tall som sammenliknes. n 1 La σ ( p, q) = + n p i q i i = 0 i = 0
Optimale søketrær Et binært tre T for nøklene K 0, K n-1 består av en rot med nøkkel K i, og to deltrær L og R. K i K 0,, K i -1 p 0,, p i -1 q 0,, q i K i+1,, K n -1 p i+1,, p n -1 p i+1,, q n Vi må prøve alle mulige røtter K i for å finne hvor det er best å dele treet. Hver mulige rot gir opphav til to delproblemer optimalisering av de korresponderende venstre og høyre deltrærne. Gjennomsnittlig antall operasjoner for treet T er gitt ved: A( T, n, p, q) A( L, i, p,..., p, q,..., q ) + A( R, n i +, p,..., p, q,..., q ) + σ ( p, q) = 0 i 1 0 i 1 i + 1 n 1 i + 1 For enkelthets skyld skriver vi A(T) = A(L) + A(R) + σ(p,q) n
Optimale søketrær La T ij være et søketre for nøklene K i,, K j, 0 i, j n 1. (T ij er det tomme treet om j < i.) La σ ( i, j) = j k = i j + 1 p k + q k k = i Formelen (rekursiv definisjon) for A(T i,j ) vil være som følger: A( Tij ) = min{ A( Ti, k 1) + A( Tk + 1, j )} + σ ( i, j) i k j A(T ii ) = 0 + 0 + σ(i,i) Kostnaden av det optimale treet finner vi i A(T 0,n -1 ).
Optimale søketrær En algoritme kan nå relativt enkelt lages ved å fylle ut en tabell med verdiene for A(T ij ) som definert av formelen (samme måte som for matrisemultiplikasjon): A( Tij ) = min{ A( Ti, k 1) + A( Tk + 1, j )} + σ ( i, j) i k j A( T ii ) = 0 + 0 + σ ( i, j) I tillegg til verdien A(T ij ) må også en representasjon av de aktuelle trærne vedlikeholdes, slik at vi finner selve treet.
Dynamisk programmering: Fyller ut ulike typer tabeller nedenfra-opp
Dynamisk Programmering Utregning Dynamisk programmering er i realiteten en beregning av en verdi bottomup. Vi har fylt ut tabeller, meget regelmessig, men egentlig kan alle beregninger som gjøres bottom-up i en rettet asyklisk graf (DAG) sees på som dynamisk programmering. (Trær er spesialtilfeller av DAG.)
Dynamisk Programmering Utregning Dynamisk programmering er i realiteten en beregning av en verdi bottomup. Vi har fylt ut tabeller, meget regelmessig, men egentlig kan alle beregninger som gjøres bottom-up i en rettet asyklisk graf (DAG) sees på som dynamisk programmering. (Trær er spesialtilfeller av DAG.)
Dynamisk Programmering, og litt om «memoisering» «Top-Down» dynamisk programmering (Memoisering) En ulempe med metoden over er at vi beregner løsninger på alle delproblemer, selv de vi ikke vil trenge alle i den aktuelle beregningen. Vi kan i stedet gjøre beregningen (rekursivt) ovenfra-ned, og sette svaret inn i den samme tabellen. Må da ha en ekstra spesialverdi: «ikke beregnet» i tabellelementene (f.eks. er -1 OK i de fleste tilfelle).
eks. Strenger som likner Memoisering T D a n e 0 1 2 3 j 0 0 1 2 3 del T del T a 1 1 0 1 2 P ins T n 2 2 1 0 1 n 3 3 2 1 1 e 4 4 3 2 1 i
Strenger som likner eks. T D a n e 0 1 2 3 j 0 0 1 2 3 del T del T a 1 1 0 1 2 P ins T n 2 2 1 0 1 n 3 3 2 1 1 e 4 4 3 2 1 i