A Toom-Cook algoritmus
Itt megtanuljuk az oszd meg és uralkodj algoritmus zseniális alkalmazását lineáris algebrával kombinálva nagy számok gyors szorzására. És bevezetés a big-O jelölésbe!
Ez az eredmény valójában annyira ellentmond az intuíciónak, hogy a nagy matematikus Kolmogorov azt feltételezte, hogy ilyen gyors algoritmus nem létezik.
0. rész: A hosszú szorzás lassú szorzás. (És bevezetés a Big-O jelölésbe)
Mindannyian emlékszünk arra, hogy az iskolában tanultunk szorozni. Sőt, be kell vallanom valamit. Ne feledjétek, ez egy ítélkezésmentes zóna 😉
Az iskolában “vizes játék” volt, amikor túlságosan esett az eső ahhoz, hogy a játszótéren játsszunk. Míg a többi (normális/szórakozni szerető/pozitív jelzőt választó) gyerek játszott és hülyéskedett, én néha fogtam egy papírlapot és összeszoroztam nagy számokat, csak úgy a kedvemért. A számolás puszta öröme! Még szorzási versenyünk is volt, a “Szuper 144″, ahol egy 12-szer 12-es számokból (1-től 12-ig) álló, összekevert rácsot kellett a lehető leggyorsabban összeszoroznunk. Ezt vallásos módon gyakoroltam, olyannyira, hogy saját, előre elkészített gyakorló lapjaim voltak, amelyeket lefénymásoltam, és váltakozva használtam. Végül 2 perc 1 másodpercre csökkentettem az időmet, aminél a firkálás fizikai határát elértem.”
A szorzás eme megszállottsága ellenére soha nem jutott eszembe, hogy ezt jobban is meg lehetne csinálni. Nagyon sok órát töltöttem szorzással, de soha nem próbáltam javítani az algoritmuson.
Gondoljunk csak a 103 szorzására 222-vel.
Kiszámoljuk 2szer 3szor 1 +2szer 0szor 10 + 2szor 1szer 100 + 20szor 3szor 1 + 20szor 0szor 10 + 20szor 1szer 100 = 22800
Az n jegyű számok szorzásához általában O(n²) művelet szükséges. A “nagy-O” jelölés azt jelenti, hogy ha a műveletek számát az n függvényében ábrázolnánk, akkor valójában az n² kifejezés számít. Az alakja lényegében kvadratikus.
Ma teljesítjük gyermekkori álmainkat, és továbbfejlesztjük ezt az algoritmust. De előbb! Big-O vár ránk.
Big-O
Nézzük meg az alábbi két gráfot
Kérdezz meg egy matematikust, hogy “x² és x²+x^(1/3) ugyanaz?”, és valószínűleg belehánynak a kávéscsészéjükbe, sírni kezdenek, és motyognak valamit a topológiáról. Legalábbis én (először) így reagáltam, pedig alig tudok valamit a topológiáról.
Oké. Ezek nem ugyanazok. De a mi számítógépünket nem érdekli a topológia. A számítógépek teljesítménye annyi mindenben különbözik, amit nem is tudok leírni, mert semmit sem tudok a számítógépekről.
De egy gőzhenger finomhangolásának semmi értelme. Ha két függvény a bemenetük nagy értékei esetén ugyanolyan nagyságrendben növekszik, akkor gyakorlati szempontból ez tartalmazza a legtöbb releváns információt.
A fenti grafikonon azt látjuk, hogy a két függvény nagy bemenetek esetén eléggé hasonlóan néz ki. De ugyanígy az x² és a 2x² is hasonló – az utóbbi csak kétszer akkora, mint az előbbi. Az x = 100-nál az egyik 10 000, a másik pedig 20 000. Ez azt jelenti, hogy ha az egyiket ki tudjuk számítani, akkor valószínűleg a másikat is ki tudjuk számítani. Míg egy exponenciálisan növekvő függvény, mondjuk 10^x, x = 100-nál már jóval nagyobb, mint az atomok száma a világegyetemben. (10¹⁰⁰ >>>>> 10⁸⁰, ami egy becslés, az Eddington-szám, az univerzumban lévő atomok számára a guglizásom* szerint).
*Tényleg a DuckDuckGo-t használtam.
Egész számokat polinomként írni egyszerre nagyon természetes és nagyon természetellenes. Az 1342 = 1000 + 300 + 40 + 2 írása nagyon természetes. Az 1342 = x³+3x²+4x + 2 írása, ahol x = 10, kissé furcsa.
Általában tegyük fel, hogy két nagy számot akarunk szorozni, b bázisban írva, egyenként n számjeggyel. Mindkét n számjegyű számot polinomként írjuk fel. Ha az n számjegyű számot r darabra osztjuk, akkor ez a polinom n/r tagból áll.
Legyen például b = 10, r = 3, és a számunk 142,122,912
Ezt átalakíthatjuk
Ez nagyon szépen működik, ha r = 3, és a 10-es bázison írt számunknak 9 számjegye volt. Ha r nem osztja tökéletesen az n-t, akkor egyszerűen hozzáadhatunk néhány nullát az elejére. Ezt megint egy példával lehet a legjobban szemléltetni:
27134 = 27134 + 0*10⁵ = 027134, amit úgy ábrázolunk, hogy
Miért lehet ez hasznos? A P polinom együtthatóinak megtalálásához, ha a polinom N rendű, akkor N+1 ponton történő mintavételezéssel meghatározhatjuk az együtthatóit.
Ha például az x⁵ + 3x³ +4x-2 polinomot, amely 5 rendű, 6 pontban mintavételezzük, akkor ki tudjuk számolni a teljes polinomot.
Intuitív módon egy 5 rendű polinomnak 6 együtthatója van: 1, x, x, x², x³, x⁴, x⁵ együtthatója. Adva 6 értéket és 6 ismeretlent, meg tudjuk állapítani az értékeket.
(Ha ismered a lineáris algebrát, akkor az x különböző hatványait lineárisan független vektorokként kezelheted, készíthetsz egy mátrixot az információ ábrázolására, invertálhatod a mátrixot, és voilá)
Polinomok mintavétele az együtthatók meghatározásához (választható)
Itt egy kicsit mélyebben megvizsgáljuk a polinom mintavételezésének logikáját az együtthatók levezetéséhez. Nyugodtan ugorjunk a következő szakaszra.
Azt fogjuk megmutatni, hogy ha két N fokú polinom N+1 ponton megegyezik, akkor azonosnak kell lenniük. Vagyis N+1 pont egyértelműen meghatározza az N rendű polinomot.
Nézzünk egy 2-es rendű polinomot. Ez az alakja P(z) = az² +bz + c. Az algebra alaptétele alapján – szégyentelenül, dugó ide, több hónapot töltöttem azzal, hogy összeraktam ennek a bizonyítását, majd megírtam, itt – ez a polinom faktorizálható. Ez azt jelenti, hogy felírhatjuk A(z-r)(z-w)
Megjegyezzük, hogy z = r-nél és z = w-nél a polinom értéke 0. Azt mondjuk, hogy w és r a polinom gyökei.
Most megmutatjuk, hogy P(z)-nek nem lehet kettőnél több gyöke. Tegyük fel, hogy három gyöke van, ezeket nevezzük w, r, s. Ezután faktorizáljuk a polinomot. P(z) = A(z-r)(z-w). Akkor P(s) = A(s-r)(s-w). Ez nem egyenlő 0-val, mivel a nem nulla számok szorzása nem nulla számot ad. Ez ellentmondás, mert az eredeti feltételezésünk az volt, hogy s a P polinom gyöke.
Ez az érvelés lényegében azonos módon alkalmazható egy N rendű polinomra.
Tegyük fel, hogy két N rendű P és Q polinom N+1 pontban megegyezik. Akkor, ha különböznek, akkor P-Q olyan N-rendű polinom, amely N+1 pontban 0. Az előzőek szerint ez lehetetlen, mivel egy N rendű polinomnak legfeljebb N gyöke van. Tehát a feltételezésünk téves lehetett, és ha P és Q N+1 pontban megegyezik, akkor ugyanaz a polinom.
A látás hit: Egy kidolgozott példa
Nézzünk egy igazán nagy számot, amelyet p-nek nevezünk. 10-es bázisban p 24 számjegy hosszú (n=24), és 4 darabra osztjuk (r=4). n/r = 24/4 = 6
p = 292,103,243,859,143,157,364,152.
Az ezt reprezentáló polinomot pedig nevezzük P-nek,
P(x) = 292,103 x³ + 243,859 x² +143,157 x+ 364,152
mivel P(10⁶) = p.
Mivel ennek a polinomnak a foka 3, az összes együtthatót ki tudjuk számolni 4 helyen történő mintavételből. Nézzük meg, mi történik, ha néhány helyen mintavételezünk.
- P(0) = 364,152
- P(1) = 1,043,271
- P(-1) = 172,751
- P(2) = 3,962,726
Feltűnő, hogy ezek mind 6 vagy 7 számjegyűek. Ehhez képest a p-ben eredetileg 24 számjegy található.
Szorozzuk be p-t q-val. Legyen q = 124,153,913,241,143,634,899,130, és
Q(x) = 124,153 x³ + 913,241 x²+143,634 x + 899,130
A t=pq kiszámítása szörnyű lehet. De ahelyett, hogy ezt közvetlenül tennénk, megpróbáljuk kiszámítani t polinomiális reprezentációját, amit T-vel jelölünk. Mivel P egy r-1=3 rendű polinom, Q pedig egy r-1=3 rendű polinom, T egy 2r-2=6 rendű polinom.
T(10⁶) = t = pq
T(10⁶) = P(10⁶)Q(10⁶)
A trükk az, hogy a T együtthatóit fogjuk kiszámítani, nem pedig közvetlenül összeszorozzuk p és q együtthatóit. Ezután a T(1⁰⁶) kiszámítása egyszerű, mert az 1⁰⁶-val való szorzáshoz csak 6 nullát ragasztunk hátra. Az összes rész összegzése is könnyű, mert az összeadás egy nagyon alacsony költségű művelet a számítógépek számára.
A T együtthatóinak kiszámításához 2r-1 = 7 ponton kell mintavételeznünk, mert ez egy 6 rendű polinom. Valószínűleg kis egész számokat akarunk választani, ezért
- T(0)=P(0)Q(0)= 364,152*899,130
- T(1)=P(1)Q(1)= 1,043,271*2,080,158
- T(2)=P(2)Q(2)= 3,962,726*5,832,586
- T(-1)=P(-1)Q(-1)= 172751*1544584
- T(-2)=P(-2)Q(-2)= -1,283,550*3,271,602
- T(3)=P(-3)Q(-3)= -5,757,369*5,335,266
Megjegyezzük a P(-3), Q(-3), P(2), Q(2) stb. méretét…. Ezek mind olyan számok, amelyek megközelítőleg n/r = 24/4 = 6 számjegyűek. Némelyik 6 számjegyű, némelyik 7 számjegyű. Ahogy n egyre nagyobb lesz, ez a közelítés egyre ésszerűbbé válik.
Ezidáig a problémát a következő lépésekre, illetve algoritmusra redukáltuk:
(1) Először kiszámítjuk P(0), Q(0), P(1), Q(1), stb (alacsony költségű művelet)
(2) Hogy megkapjuk T(x) 2r-1= 7 értékét, most 2r-1 n/r számjegyű közelítő méretű számot kell megszoroznunk. Nevezetesen ki kell számolnunk P(0)Q(0), P(1)Q(1), P(-1)Q(-1), stb. (Nagy költségű művelet)
(3) Rekonstruáljuk a T(x) értéket az adatpontjainkból. (Alacsony költségű művelet: de majd később kerül szóba).
(4) A rekonstruált T(x)-ünkkel értékeljük ki T(10⁶) ebben az esetben
Ez szépen elvezet a megközelítés futási idejének elemzéséhez.
Futási idő
A fentiekből kiderült, hogy az (1) és (3) alacsony költségű műveleteket ideiglenesen figyelmen kívül hagyva, két n számjegyű szám Toom algoritmusával történő szorzásának költségei a következőknek engedelmeskednek:
Ezt láttuk a kidolgozott példában. Minden P(0)Q(0), P(1)Q(1) stb. szorzása T(n/r)-be kerül, mert két körülbelül n/r hosszúságú számot szorzunk. Ezt 2r-1-szer kell megtennünk, mivel T(x) – lévén 2r-2 rendű polinom – 2r-1 helyen akarunk mintát venni.
Milyen gyors ez? Egy ilyen egyenletet explicit módon is meg tudunk oldani.
Most már csak néhány kusza algebra
Mert a mátrixszorzás (“újra összerakás”) folyamatának volt még néhány költsége, és az összeadásból, nem igazán mondhatjuk, hogy a megoldásunk a pontos futási idő, így ehelyett arra következtetünk, hogy megtaláltuk a futási idő nagy-O-ját
Optimalizálás r
Ez a futási idő még mindig r-től függ. Javítsuk ki az r-t, és kész is vagyunk. De kíváncsiságunkat még nem elégítettük ki!
Amint n egyre nagyobb lesz, szeretnénk egy megközelítőleg optimális értéket találni az r megválasztásához. Vagyis olyan r értéket akarunk választani, amely n különböző értékeire változik.
A legfontosabb dolog itt az, hogy r változása esetén némi figyelmet kell fordítanunk az újra összerakási költségre – az ezt a műveletet végrehajtó mátrix költsége O(r²). Amikor r fix volt, erre nem kellett figyelnünk, mert ahogy n nagyra nőtt r fixálása mellett, az n-t tartalmazó kifejezések voltak azok, amelyek meghatározták a növekedést, hogy mennyi számítás szükséges. Mivel az r optimális választása az n növekedésével egyre nagyobb lesz, ezt már nem hagyhatjuk figyelmen kívül.
Ez a különbségtétel döntő fontosságú. Elemzésünk kezdetben r egy értékét választotta, talán 3-at, talán 3 milliárdot, majd a big-O méretét nézte, ahogy n tetszőlegesen nagyra nőtt. Most megnézzük a viselkedést, ahogy n tetszőlegesen nagyra nő, és r az n-nel együtt nő valamilyen általunk meghatározott ütemben. Ez a szemléletváltás az O(r²)-t változónak tekinti, míg amikor r-t fixen tartottuk, az O(r²) egy konstans volt, bár nem ismertük.
Az előző elemzésünket tehát frissítjük:
ahol az O(r²) az újra összerakási mátrix költségét jelenti. Most az n minden értékéhez olyan r értéket akarunk választani, amely megközelítőleg minimalizálja ezt a kifejezést. A minimalizáláshoz először megpróbáljuk differenciálni r tekintetében. Ez a kissé förtelmesnek tűnő kifejezést
Normális esetben a minimumpont megtalálásához a deriváltat 0-val egyenlővé tesszük. De ennek a kifejezésnek nincs szép minimuma. Ehelyett egy “elég jó” megoldást keresünk. Vendégértékként r = r^(sqrt(logN)) értéket használunk. Grafikusan ez elég közel van a minimumhoz. (Az alábbi ábrán az 1/1000 faktor azért van, hogy a grafikon ésszerű méretarányban is látható legyen)
A lenti grafikonon a T(N) függvényünk egy konstanssal skálázva, piros színnel látható. A zöld vonal az x = 2^sqrt(logN) értéknél van, ami a vendégértékünk volt. Az “x” az “r” helyét veszi át, és mindhárom képen N különböző értékét használjuk.