Dynamisk programmering (DP) er en designmetode for optimeringsproblemer der den rekursive løsningen løser de samme delinstansene om og om igjen. I stedet for å regne dem ut på nytt løser vi hver delinstans én gang og slår opp svaret senere. Naiv rekursjon kan være eksponentiell; DP gjør den polynomisk ved å bytte gjentatt regning mot litt minne.
To forutsetninger: delstruktur og overlapp
DP er riktig verktøy nøyaktig når problemet har to egenskaper samtidig.
Det er nyttig å se dette som en delinstansgraf: én node per distinkt delinstans, med en kant fra en delinstans til hver delinstans den avhenger av. Overlapp betyr at flere veier ender i samme node. Optimal delstruktur betyr at en nodes verdi kan regnes ut fra verdiene til etterfølgerne. Kjøretiden til DP-en er da ganske enkelt: antall noder arbeidet per node.
Memoisering vs. iterasjon
Det finnes to måter å fylle inn delinstansgrafen på — de regner ut nøyaktig de samme verdiene, men i motsatt rekkefølge.
Top-down (memoisert)
Skriv den naturlige rekursjonen, men lagre hvert svar i en tabell første gang det regnes ut, og returner det lagrede svaret ved senere kall. Bekvemt, og regner bare ut de delinstansene som faktisk trengs. Bærer rekursjonens stakk-kostnad.
Bottom-up (iterativ)
Sorter delinstansene topologisk etter avhengighet og fyll tabellen med løkker fra de minste opp. Ingen rekursjon, ofte lavere konstantfaktor, og lett å resonnere om plassbruken — men du må selv finne en gyldig utfyllingsrekkefølge.
Begge gir samme asymptotiske kjøretid. Verdien vi lagrer er som regel bare den optimale tallverdien; for å få selve løsningen lagrer vi i tillegg hvilket valg som var optimalt i hver celle, og rekonstruerer ved å følge valgene bakover fra det ferdige svaret.
Oppskrift: slik setter du opp en DP
-
Karakteriser strukturen
Vis at en optimal løsning er bygd av optimale delløsninger — finn det første valget som splitter problemet i mindre, uavhengige delinstanser.
-
Definer verdien rekursivt
Skriv som et maksimum/minimum over de mulige første valgene, uttrykt ved av mindre delinstanser. Husk grunntilfellene.
-
Regn ut nedenfra (eller memoiser)
Fyll en tabell over alle distinkte delinstanser i en rekkefølge som respekterer avhengighetene — bottom-up med løkker, eller top-down med memoisering.
-
Rekonstruer løsningen
Lagre hvilket valg som ga optimum i hver celle, og følg valgene bakover fra svaret for å sette sammen den faktiske løsningen.
Stavkapping
I stavkapping har vi en stav av lengde og en prisliste , og skal kappe den i biter som maksimerer total pris. Det første valget er lengden på den første biten; resten er en stav av lengde , som vi løser optimalt på samme måte. Det gir rekurrensen
Det er bare distinkte delinstanser , og hver tar arbeid, så bottom-up-DP-en er — mot eksponentiell naiv rekursjon. Med prislisten fyller vi tabellen nedenfra:
| Optimalt første kutt | ||
|---|---|---|
| — | ||
Lengste felles delsekvens (LCS)
LCS finner den lengste sekvensen som forekommer (ikke nødvendigvis sammenhengende) i begge to strenger og . Sammenlign de siste tegnene: er de like, hører tegnet med i LCS-en, og vi går videre på begge prefiksene. Er de ulike, er svaret det beste av å droppe det siste tegnet i eller i :
Tabellen har celler og hver fylles i , så LCS er . Lagrer vi hvilken av de tre reglene som ga hver celle, kan vi rekonstruere selve delsekvensen ved å følge pilene fra tilbake til hjørnet.
Binært ryggsekkproblem
Det binære ryggsekkproblemet (appendiks E) gir gjenstander med vekt og verdi og en sekk med kapasitet . Hver gjenstand tas helt eller ikke i det hele tatt (derav «binært»). Vi maksimerer verdien innenfor kapasiteten. Det første valget er om gjenstand er med:
der den andre grenen bare gjelder når . Tabellen har celler, hver fylt i , så DP-en er .