derégvoltam
Na de, láttam ezt a postot nemrég:
be is nekróztam egy kommentet, most pedig megoldjuk rendesen :) közben megmutatom az egyik kedvenc ágyúmat.
A kérdés egyébként ez volt: egy n fokos lépcsőn akarunk felmenni, hányféleképp tehetjük ezt meg, ha egyszerre egy, két vagy három fokot tudunk lépni?
Oldjuk meg ezt a feladat-sablont.
Amit mindenki tud remélhetőleg: ha erre rekurziót írunk, akkor
f(1) = 1, f(2) = 2, f(3) = 4 (ezeket akár kézzel) és f(n+3) = f(n) + f(n+1) + f(n+2).
Persze ezt nem implementáljuk így, mivel mindig több kisebb argumentummal hívjuk újra, ez így n-ben exponenciális időt igényelne, ehelyett rekurzió-memorizálunk, vagy eleve bottom-up dinamikus programozunk:
T[1] = 1; T[2] = 2; T[3] = 4;
for( int i = 4; i <= n; i++ ) T[i] = T[i-1] + T[i-2] + T[i-3];
return T[n];
Mondjuk szerintem memoizationt spec itt használni hülyeség, hiszen pontosan tudjuk, melyik cellákra van szükség a számításhoz: mindre. Tehát kiszámítjuk mindet, nem ifezünk, hogy volt-e már egy hívás vagy sem.
Mivel mindig csak az utolsó három cellát használjuk, elég csak azt a hármat észben tartani és ciklikusan tölteni a 3-elemű tömböt modulo 3:
Mivel mindig csak az utolsó három cellát használjuk, elég csak azt a hármat észben tartani és ciklikusan tölteni a 3-elemű tömböt modulo 3:
T[1] = 1; T[2] = 2; T[0] = 4;
for( int i = 4; i <= n; i++ ) T[ i%3 ] += T[ (i-1)%3 ] + T[ (i-2) % 3 ];
return T[ n % 3 ];
észre is vehetjük persze, hogy mindenképp a tömbösszeget rakjuk bele, kevesebb if:
T[1] = 1; T[2] = 2; T[0] = 4;
for( int i = 4; i <= n; i++ ) T[ i%3 ] = T[ 0 ] + T[ 1 ] + T[ 2 ];
return T[ n % 3 ];
Lineáris idő, konstans tár, örülünk, így a fenti post. Örülünk?
frászt.
nincs ez még kész.
Egyrészt ha a tömb típusa int, akkor ez a három lépésig visszamenő rekurzió kábé 1.83n-nel arányos (lásd pl itt: http:://mathworld.wolfram.com/TribonacciNumber.html ), ami n=37-nél kicsúszik a 32 bitből, n=75-nél pedig a 64 bitből is, szóval akkor már inkább használjunk precomputed táblát, és ne zavarjon már az a néhány byte tárterület... (ok, nyilván azt dinamikusan számoljuk ki, de ne hülyéskedjünk ezzel a száz helyett csak négy inttel, tegyük el mindet tömbbe és ha jön a query, konstans idő visszaadni, kezét csonkolom.)
A kis n-ek kérdését lezongoráztuk. De mi van, ha n=100-ra kéne az érték? Akkor már akármekkora egészekkel kéne aritmetikát csinálni, ahol meg nem lehet azt mondani, hogy konstans idő alatt végzünk az összeadással pl. Versenyeken általában nem ilyen formában történik a kérdés, hanem kicsit komolyabban:
"Határozzuk meg a fenti T[n] értékét n~1.000.000.000.000 -re, modulo 1.000.007!"
értem? az eredmény befér intbe, a lineáris idő meg nem jó nekünk, ha n 64-bites. Ha látsz egy ilyet, el kéne kezdeni gondolkodni :D
úgyhogy ennél általánosabban fogjuk megoldani a kérdést, O(logn) idő és konstans tár mellett bármi hasonlót, modulóval vagy anélkül (anélkül persze nem feltétlen lesz O(logn), inkább O(log2n)).
k-lépéses lineáris rekurzió egy olyan f függvény, aminek megadjuk az f(1), ..., f(k) értékét, és a1,...,ak együtthatókat, ezek mondjuk valami valós számok; továbbá, minden n>=0-ra
f(n+k+1) = a1*f(n+1) + a2*f(n+2) + ... + ak*f(n+k).
Ilyen a fenti is, háromlépéses, f(1)=1, f(2)=2, f(3)=4 és a1=a2=a3=1 választásával, de a Fibonacci is, f(1)=f(2)=1, a1=a2=1 választással kétlépéses.
Kérdés az n. tag értéke modulo something, gyorsan, tegnapra, nagy n-re.
Írjunk fel egy k*k-s (ha a sorozat adott, ez konstans méretű) A mátrixot, mégpedig: az utolsó oszlopba fentről lefelé írjuk az a1,...,ak együtthatókat, a főátló ALÁ közvetlenül egyeseket, minden máshova 0 kerül.
Írjunk fel egy k*k-s (ha a sorozat adott, ez konstans méretű) A mátrixot, mégpedig: az utolsó oszlopba fentről lefelé írjuk az a1,...,ak együtthatókat, a főátló ALÁ közvetlenül egyeseket, minden máshova 0 kerül.
Ez ennél a feladatnál pl.
(0 0 1)
(1 0 1)
(0 1 1)
lesz, a Fibonacci esetében pedig
(0 1)
(1 1).
de miért? Mondjuk van egy (n1, n2, n3, ..., nk) vektorunk. Mit kapunk, ha beszorozzuk a fenti A mátrixszal? az első elem helyére kerül a második (ld mátrix első oszlop), a második helyére a harmadik stb; az utolsó oszlopot kivéve a mátrix egy shift left mátrix. Az utolsó elem helyére mi kerül?
a1*n1 + a2*n2 + ... + ak*nk.
Tehát ha a vektorunkban megvan az utolsó k eleme a sorozatnak, akkor a mátrixszal való szorzás eggyel tovább húzza az ablakot. Emiatt ha inicializáljuk a kezdővektort a v = (f(1), f(2), ..., f(k)) értékekkel és az n+k. elemet kell kiszámolnunk, akkor csak a vAn szorzat (ez egy vektor ugye, n-szer kell rászorozzunk A-val a v vektorra, hogy bekerüljön az f(n+k) az utsó pozícióba) utolsó koordinátáját kell visszaadjuk.
Ha modulo kell számolni, akkor úgy.
Elsőre ez nem jár előnnyel, sőt, hiszen most nem O(n) darab számot kell összeadni, hanem O(n) vektor-mátrix szorzatot kiszámolni, kinek jó ez?
nekünk!
mert tudunk iterált mátrixhatványozni: An kiszámításához kiszámoljuk négyzetreemelgetéssel A, A2, A4, A8, ..., A2logn mátrixokat és ami kell belőlük (hatványozás-azonosság: ahol n-ben 1-es van, azoknak megfelelőt szorozzuk), azokat szorozzuk össze:
T pow( T a, unsigned n ){
T ret; //inicializáljuk a szorzás egységelemére, mátrix esetében tehát egységmátrixra
while( n ){
if( n % 2 ) ret *= a;
n /= 2;
a *= a;
}
return ret;
}
kész is. ha modulo kell, akkor persze a mátrix osztályban úgy csináljuk a szorzás implementációját, hogy minden alapműveletet eleve modulo csináljon. Ez mennyi? O(logn) mátrixszorzás, konstans (k3) művelet per mátrixszorzás.
és akkor
Matrix A; //ezt a mátrixot inicializáljuk az aktuális sorozat együtthatóitól függően;
Vector init; //ezt a vektort inicializáljuk az aktuális sorozat kezdő k tagjától függően;
scanf("%d",&n);
return (init * pow( A, n))[k-1]; //értelmes overridingokat feltételezve
Ha tehát az n nagy lehet, ezt inkább így csináljuk.
A "mekkora számít nagynak" kérdésre: pl k=3 esetén két mátrix szorzása 45 művelet, az akkor cca 90logn lépés, plusz az apró, szemben az eredeti 2n művelettel. Ha pl n úgy kábé 300, akkor már a mátrixozás gyorsabb (főleg ha inkább egy konkrét 3x3-as memóriaterületen megy a számolás és nem pl új objektumokat hozogatunk létre nyakló nélkül, legyen bármi is az a nyakló. Párhuzamosítani is lehet.)
Ha meg kisebb, mint 300, én precomputed tábláznék. Nyilván kombinálnám a kettőt: kiszámolnám előre egy tömbbe az első mondjuk száz elemet, a többit mátrixokkal, ha kérdezik.
(Amúgy van komplikált hatványozós zárt képlet is a wolframos linken, azzal csak egy a baj: nincs az a lebegőpontosság, ami jó lenne ehhez a feladathoz, túl sok értékes jegyet kell megtartsunk..)
most már ki tudjuk bármilyen lineáris rekurzió általános, sokadik tagját gyorsan számolni.
örülünk, Vincent?
A "mekkora számít nagynak" kérdésre: pl k=3 esetén két mátrix szorzása 45 művelet, az akkor cca 90logn lépés, plusz az apró, szemben az eredeti 2n művelettel. Ha pl n úgy kábé 300, akkor már a mátrixozás gyorsabb (főleg ha inkább egy konkrét 3x3-as memóriaterületen megy a számolás és nem pl új objektumokat hozogatunk létre nyakló nélkül, legyen bármi is az a nyakló. Párhuzamosítani is lehet.)
Ha meg kisebb, mint 300, én precomputed tábláznék. Nyilván kombinálnám a kettőt: kiszámolnám előre egy tömbbe az első mondjuk száz elemet, a többit mátrixokkal, ha kérdezik.
(Amúgy van komplikált hatványozós zárt képlet is a wolframos linken, azzal csak egy a baj: nincs az a lebegőpontosság, ami jó lenne ehhez a feladathoz, túl sok értékes jegyet kell megtartsunk..)
most már ki tudjuk bármilyen lineáris rekurzió általános, sokadik tagját gyorsan számolni.
örülünk, Vincent?
