Nakon nešto duže pauze uglavnom prouzrokovane ispitnim rokom, vraćamo se programiranju i matematici…
Rekurentne formule predstavljaju poseban oblik zapisivanja vrednosti nekog niza. Jedna od najpoznatijih ovakvih relacija je svakako Fibonačijev niz, u kome vrednost nekog člana niza zavisi od vrednosti prethodna dva.
Ukoliko u programu želimo da izračunamo neki član ovog (ili bilo kog drugog) niza, potrebno je da krenemo od vrednosti koje su nam date i iterativno dođemo do željenog člana, pritom prolazeći kroz sve njegove prethodnike. Naravno, moguće je i rešavanje relacije tako da formula zavisi samo od datih vrednosti i N, ali to bi bilo previše komplikovano za implementaciju.
Iako je ovaj pristup krajnje jednostavan, u nekim slučajevima nije moguće primeniti ga usled problema sa efikasnošću. Linearne homogene rekurentne formule, koje ću obraditi u ovom tekstu, mogu biti vremenski zahtevne ukoliko se traži član se velikim indeksom. Ukoliko svaki član zavisi od 10 prethodnih, a njegov indeks je 100.000.000, ukupno će biti potrebno milijardu sabiranja, što je apsolutno nepotrebno. Postoji i komplikovaniji, ali daleko brži pristup.
Inspiracija za ovaj tekst je nastala tokom rešavanja jednog od zadataka sa ovogodišnjeg BubbleCup-a koji je zahtevao efikasan pristup rekurentnim formulama — Archer’s Travel. Kako indeks člana ide čak do jedne milijarde, jednostavno rešenje nije moglo da se izvrši u okviru vremenskog ograničenja. Srećom, pa postoji rešenje sa logaritamskom složenošću.
Neka nam je data sledeća rekurentna formula:
Xn zavisi od prethodnih k članova, a vrednosti ai su poznati koeficijenti rekurentne formule. Ovu jednačinu možemo zapisati i na sledeći način upotrebom matrica:
Naravno, kada se ove dve matrice pomnože, rezultat je isti kao u prethodnoj jednačini. Za sledeći korak nama nije dovoljno samo xn, već nam treba i njegovih k-1 prethodnika. Ali zašto ne bismo i njih uvrstili u levu stranu jednakosti i dobili opšti iterativni korak?
Sada nam ostaje samo da formiramo kvadratnu matricu A tako da osim xn dobijemo i ostale potrebne članove. Tačnije, prvi red matrice A ćemo popuniti koeficijentima rekurentne formule, a ostatak ćemo ispuniti nulama, uz jedinice po dijagonali (ali ne dijagonali matrice), radi dobijanja željenog proizvoda.
Pošto smo dobili opšti oblik za računanje matrice, možemo ga rekurzivno primeniti da bismo došli do poznatih vrednosti članova niza.
Odlično! Sveli smo računanje člana xn na samo jedno množenje. Avaj, An-k+1 nam nije poznato, i računanje ove matrice se svodi na još n-k+1 množenja…
Da li je to slučaj? Naravno da nije. Sada nailazimo na korak koji predstavlja najbitniji deo ove “optimizacije” — računanje stepena nekog broja (odnosno matrice, u našem slučaju) se može izvršiti u logaritamskom vremenu.
Iako je množenje matrica nešto kompleksniji posao, ušteda je i dalje jako velika – najjednostavniji algoritam za množenje matrica je složenosti k3, gde je k dimenzija kvadratne matrice.
Za kraj, sledi najjednostavniji algoritam za računanje vrednosti nekog člana rekuretne formule implementiran u Javi. Dalje optimizacije su moguće (izbacivanje rekurzije, efikasnije množenje matrica), ali nemaju ni približno veliki uticaj kao što ima uvođenje matrica i njihovog stepenovanja u algoritam.
// Brojevi se cuvaju u BigDecimal jer mogu postati jako veliki // Smatra se da je indeks prvog clana niza nula // Pocetni clanovi se prosledjuju redom x(0), x(1), ... // Koeficijenti se prosledjuju obrnutim redosledom - prvo ide // koeficijent uz x(i), pa uz x(i-1) itd. static BigDecimal izracunaj(int indeksClana, double[] pocetniClanovi, double[] koeficijenti) { int k = pocetniClanovi.length; // Ukoliko prosledjeni nizovi nisu validni if (k != koeficijenti.length || k < 1) return null; // Ukoliko se trazi clan koji je vec poznat if (indeksClana < k) return new BigDecimal(pocetniClanovi[indeksClana]); // Sve je u redu, formiramo matricu BigDecimal[][] matrica = new BigDecimal[k][k]; for (int i = 0; i < k; i++) matrica[0][i] = new BigDecimal(koeficijenti[i]); for (int i = 1; i < k; i++) for (int j = 0; j < k; j++) if (i - 1 == j) matrica[i][j] = new BigDecimal(1); else matrica[i][j] = new BigDecimal(0); // Stepenujemo matricu - A^(indeks-k+1) matrica = stepenujRekurzivno(indeksClana - k + 1, matrica, k); // Racunamo x(indeks) BigDecimal rezultat = new BigDecimal(0); for (int i = 0; i < k; i++) rezultat = rezultat.add(matrica[0][i].multiply(new BigDecimal(pocetniClanovi[k - i - 1]))); return rezultat; } static BigDecimal[][] stepenujRekurzivno(int stepen, BigDecimal[][] matrica, int dimenzije) { if (stepen == 1) return matrica; BigDecimal[][] tempMatrica = stepenujRekurzivno(stepen / 2, matrica, dimenzije); // Kvadriramo dobijenu matricu BigDecimal[][] novaMatrica = new BigDecimal[dimenzije][dimenzije]; for (int i = 0; i < dimenzije; i++) for (int j = 0; j < dimenzije; j++) { novaMatrica[i][j] = new BigDecimal(0); for (int t = 0; t < dimenzije; t++) { novaMatrica[i][j] = novaMatrica[i][j].add(tempMatrica[i][t].multiply(tempMatrica[t][j])); } } // Mnozimo jos jednom ako je stepen neparan if (stepen % 2 != 0) { BigDecimal[][] novaTempMatrica = new BigDecimal[dimenzije][dimenzije]; for (int i = 0; i < dimenzije; i++) for (int j = 0; j < dimenzije; j++) { novaTempMatrica[i][j] = new BigDecimal(0); for (int t = 0; t < dimenzije; t++) novaTempMatrica[i][j] = novaTempMatrica[i][j].add(matrica[i][t].multiply(novaMatrica[t][j])); } novaMatrica = novaTempMatrica; } // Vracamo matricu return novaMatrica; }