Tämän ja seuraavan viikon tehtävien aiheena on dynaaminen ohjelmointi. Jos et tunne tekniikkaa ennestään, sopiva johdatus aiheeseen on Tirakirjan luku 9.
Merkkijonojen laskeminen
Tyypillinen dynaamisen ohjelmoinnin sovellus on laskea, monellako merkin pituisella merkkijonolla (tai alkion kokoisella taulukolla) on haluttu ominaisuus. Tällöin ongelman voi koettaa esittää rekursiivisena funktiona , joka antaa ratkaisun tapauksessa .
Tarkastellaan esimerkkinä tehtävää, jossa haluamme laskea -pituiset bittijonot, joissa ei ole missään kohdassa kolmea samaa bittiä peräkkäin. Merkitään :llä tällaisten bittijonojen määrää. Esimerkiksi , koska mahdolliset bittijonot ovat 001, 010, 011, 100, 101 ja 110.
Funktion pohjatapaukset ovat ja . Tämän jälkeen suurempia tapauksia voi laskea kaavalla . Tässä on ideana laskea yhteen tapaukset, joissa bittijonon lopussa on tai samaa bittiä. Voimme toteuttaa laskennan tehokkaasti dynaamisella ohjelmoinnilla näin:
f[1] = 2; f[2] = 4; for (int i = 3; i <= n; i++) { f[i] = f[i-1]+f[i-2]; } cout << f[n]; // vastaus tapauksessa n
Laskeminen modulossa
Usein dynaamisessa ohjelmoinnissa vastaus voi olla suuri eikä mahdu int
- tai long
-tyyppiin. Tällöin tehtävässä voi olla maininta, että vastaus riittää laskea "modulo ", jossa on jokin luku, eli vastauksen jakojäännös :llä.
Vastauksen voi laskea modulossa toteuttamalla koodi muuten samalla tavalla kuin ennenkin, mutta jokaisessa laskutoimituksessa mukana on modulo. Esimerkiksi yllä olevaa koodia riittäisi muuttaa näin:
f[1] = 2; f[2] = 4; for (int i = 3; i <= n; i++) { f[i] = (f[i-1]+f[i-2])%M; } cout << f[n]; // vastaus tapauksessa n
Huomaa, että jos laskennan aikana on vähennyslasku, tuloksena oleva modulo voi olla negatiivinen, mikä ei ole toivottavaa. Tämän voi estää lisäämällä lukuun , jos se on pienempi kuin :
int x = (a-b)%M; if (x < 0) x += M;
Ruudukon käsittely
Toinen tavallinen dynaamisen ohjelmoinnin käyttökohde on, että haluamme laskea jotain ruudukkoon liittyvää. Tällöin voimme koettaa muotoilla tehtävän funktiona , missä on ruudukon kohta. Voimme käyttää dynaamista ohjelmointia, jos tapauksen ratkaisun saa laskettua tapauksista , missä ja .
Yksi tyypillinen tilanne on, että haluamme laskea reittejä, jotka lähtevät ruudukon vasemmasta yläkulmasta ja joissa saa kulkea vain askelia oikealle ja alaspäin. Jälkimmäisen ehdon ansiosta voimme käyttää dynaamista ohjelmointia, koska reitillä olevilla ruuduilla on selkeä järjestys.
Esimerkiksi jos ruudussa on luku , voimme laskea rekursiolla mikä on suurin lukujen summa reitillä, joka alkaa vasemmasta yläkulmasta ja päättyy kohtaan . Ideana on valita vasemmalta tai ylhäältä tuleva reitti niin, että summa on mahdollisimman suuri.
Voimme laskea funktion arvot kaikkiin kohtiin seuraavasti:
for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { f[i][j] = a[i][j]+max(f[i-1][j],f[i][j-1]); } }
Huomaa, että taulukko on tarkoituksella -indeksoitu, koska silloin reunoilla ruudukon ulkopuolella olevissa kohdissa on oletuksena arvo .