The Toom-Cook Algorithm
Här lär vi oss en genial tillämpning av en divide and conquer-algoritm i kombination med linjär algebra för att multiplicera stora tal snabbt. Och en introduktion till big-O-notationen!
Detta resultat är faktiskt så kontraintuitivt att den store matematikern Kolmogorov förmodade att en så snabb algoritm inte existerade.
Del 0: Lång multiplikation är långsam multiplikation. (Och en introduktion till Big-O Notation)
Vi minns alla att vi lärde oss att multiplicera i skolan. Jag har faktiskt en bekännelse att göra. Kom ihåg att detta är en zon utan domar 😉
I skolan hade vi ”våtlek” när det regnade för mycket för att leka på lekplatsen. Medan andra (normala/roliga/älskande/positiva-adjektiv-av-valet) barn lekte lekar och strulade runt, tog jag ibland ett pappersark och multiplicerade stora tal tillsammans, bara för skojs skull. Den rena glädjen att räkna! Vi hade till och med en multiplikationstävling, ”Super 144”, där vi var tvungna att multiplicera ett 12 gånger 12 rutnät av siffror (från 1 till 12) så snabbt som möjligt. Jag tränade detta religiöst, till den grad att jag hade mina egna förberedda övningsark som jag kopierade och sedan använde i tur och ordning. Så småningom fick jag ner min tid till 2 minuter och 1 sekund, då min fysiska gräns för klotter var nådd.
Trots denna besatthet av multiplikation föll det mig aldrig in att vi kunde göra det bättre. Jag tillbringade så många timmar med att multiplicera, men jag försökte aldrig förbättra algoritmen.
Tänk på att multiplicera 103 med 222.
Vi beräknar 2 gånger 3 gånger 1 + 2 gånger 0 gånger 10 + 2 gånger 1 gånger 100 + 20 gånger 3 gånger 1 + 20 gånger 0 gånger 10 + 20 gånger 1 gånger 100 = 22800
I allmänhet krävs det O(n²) operationer för att multiplicera till n siffror. Notationen ”big-O” innebär att om vi skulle grafera antalet operationer som en funktion av n, så är det n²-termen som verkligen är det viktiga. Formen är i huvudsak kvadratisk.
I dag ska vi uppfylla alla våra barndomsdrömmar och förbättra denna algoritm. Men först! Big-O väntar på oss.
Big-O
Se på de två graferna nedan
Fråga en matematiker: ”Är x² och x²+x^(1/3) samma sak?”, och han eller hon kommer förmodligen att spy i sin kaffekopp och börja gråta och mumla något om topologi. Det var åtminstone min reaktion (till en början), och jag vet knappast något om topologi.
Okej. De är inte samma sak. Men vår dator bryr sig inte om topologi. Datorer skiljer sig åt i prestanda på så många sätt som jag inte ens kan beskriva eftersom jag inte vet något om datorer.
Men det är ingen idé att finjustera en ångvält. Om två funktioner, för stora värden på deras indata, växer i samma storleksordning så innehåller det för praktiska ändamål den mesta relevanta informationen.
I grafen ovan ser vi att de två funktionerna ser ganska lika ut för stora indata. Men på samma sätt liknar x² och 2x² varandra – den senare är bara dubbelt så stor som den förra. Vid x = 100 är den ena 10 000 och den andra 20 000. Det betyder att om vi kan beräkna den ena kan vi förmodligen beräkna den andra. Medan en funktion som växer exponentiellt, säg 10^x, vid x = 100 redan är långt mycket större än antalet atomer i universum. (10¹⁰⁰ >>>>> 10⁸⁰ vilket är en uppskattning, Eddington-talet, på antalet atomer i universum enligt min Googling*).
*Jag använde faktiskt DuckDuckGo.
Skriva heltal som polynom är både mycket naturligt och mycket onaturligt. Att skriva 1342 = 1000 + 300 + 40 + 2 är mycket naturligt. Att skriva 1342 = x³+3x²+4x + 2, där x = 10, är något udda.
Genomgående antar vi att vi vill multiplicera två stora tal, skrivna i basen b, med n siffror vardera. Vi skriver varje n-siffrigt tal som ett polynom. Om vi delar upp det n-siffriga talet i r bitar har detta polynom n/r termer.
Till exempel, låt b = 10, r = 3 och vårt tal är 142 122 912
Vi kan omvandla detta till
Det här fungerar mycket bra när r = 3 och vårt tal, skrivet i bas 10, hade 9 siffror. Om r inte perfekt delar n kan vi bara lägga till några nollor längst fram. Återigen illustreras detta bäst med ett exempel.
27134 = 27134 + 0*10⁵ = 027134, vilket vi representerar som
Varför kan detta vara till hjälp? För att hitta koefficienterna till polynomet P, om polynomet är av ordning N, kan vi bestämma dess koefficienter genom att ta stickprov i N+1 punkter.
Till exempel, om polynomet x⁵ + 3x³ +4x-2, som är av ordning 5, provtas i 6 punkter kan vi sedan räkna ut hela polynomet.
Intuitivt har ett polynom av ordning 5 6 koefficienter: koefficienten för 1, x, x², x³, x⁴, x⁵. Givet 6 värden och 6 okända kan vi ta reda på värdena.
(Om du kan linjär algebra kan du behandla de olika potenserna av x som linjärt oberoende vektorer, göra en matris för att representera informationen, invertera matrisen och voila)
Sampling av polynom för att bestämma koefficienter (valfritt)
Här tittar vi lite djupare på logiken bakom att ta prover på ett polynom för att härleda dess koefficienter. Hoppa gärna över till nästa avsnitt.
Vad vi kommer att visa är att om två polynom av grad N stämmer överens på N+1 punkter så måste de vara likadana. D.v.s. N+1 punkter specificerar unikt ett polynom av grad N.
Tänk på ett polynom av grad 2. Detta är av formen P(z) = az² +bz + c. Genom algebrans fundamentala sats – skamlös, plug här, jag tillbringade flera månader med att sammanställa ett bevis för detta, och sedan skriva upp det, här – kan detta polynom vara faktoriserat. Det betyder att vi kan skriva det som A(z-r)(z-w)
Bemärk att vid z = r, och vid z = w, värderas polynomet till 0. Vi säger att w och r är rötter till polynomet.
Nu visar vi att P(z) inte kan ha mer än två rötter. Anta att det har tre rötter, som vi kallar w, r, s. Vi faktoriserar sedan polynomet. P(z) = A(z-r)(z-w). Då är P(s) = A(s-r)(s-w). Detta är inte lika med 0, eftersom multiplikation av tal som inte är noll ger ett tal som inte är noll. Detta är en motsägelse eftersom vårt ursprungliga antagande var att s var en rot i polynomet P.
Detta argument kan tillämpas på ett polynom av ordning N på ett i stort sett identiskt sätt.
Antag nu att två polynom P och Q av ordning N stämmer överens på N+1 punkter. Om de är olika är då P-Q ett polynom av ordning N som är 0 i N+1 punkter. Enligt det föregående är detta omöjligt, eftersom ett polynom av ordning N har högst N rötter. Vårt antagande måste alltså ha varit fel, och om P och Q stämmer överens i N+1 punkter är de samma polynom.
Seeing is believing: I bas 10 är p 24 siffror långt (n=24), och vi kommer att dela upp det i fyra delar (r=4). n/r = 24/4 = 6
p = 292 103 243 859 143 157 364 152.
Och låt det polynom som representerar det kallas P,
P(x) = 292,103 x³ + 243,859 x² +143,157 x+ 364,152
med P(10⁶) = p.
Då graden av detta polynom är 3 kan vi räkna ut alla koefficienter genom att ta stickprov på 4 ställen. Låt oss få en känsla för vad som händer när vi tar stickprov på några punkter.
- P(0) = 364 152
- P(1) = 1 043 271
- P(-1) = 172 751
- P(2) = 3 962 726
En slående sak är att dessa alla har 6 eller 7 siffror. Det kan jämföras med de 24 siffror som ursprungligen fanns i p.
Låt oss multiplicera p med q. Låt q = 124 153 913 241 143 634 899 130, och
Q(x) = 124 153 x³ + 913 241 x²+143 634 x + 899 130
Att räkna ut t=pq kan vara hemskt. Men i stället för att göra det direkt försöker vi beräkna polynomrepresentationen av t, som vi kallar T. Eftersom P är ett polynom av ordningen r-1=3 och Q är ett polynom av ordningen r-1=3, är T ett polynom av ordningen 2r-2=6.
T(10⁶) = t = pq
T(10⁶) = P(10⁶)Q(10⁶)
Tricket är att vi kommer att räkna ut T:s koefficienter i stället för att direkt multiplicera p och q tillsammans. Då är det lätt att beräkna T(1⁰⁶), eftersom vi för att multiplicera med 1⁰⁶ bara sätter 6 nollor på baksidan. Att summera alla delar är också enkelt eftersom addition är en mycket billig åtgärd för datorer att utföra.
För att beräkna T:s koefficienter måste vi ta stickprov i 2r-1 = 7 punkter, eftersom det är ett polynom av ordning 6. Vi vill förmodligen välja små heltal, så vi väljer
- 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
Notera storleken på P(-3), Q(-3), P(2), Q(2) etc… Dessa är alla tal med ungefär n/r = 24/4 = 6 siffror. En del är 6 siffror, en del är 7 siffror. När n blir större blir denna approximation mer och mer rimlig.
So långt har vi reducerat problemet till följande steg, eller algoritm:
(1) Beräkna först P(0), Q(0), P(1), Q(1), Q(1), etc. (lågkostnadsåtgärd)
(2) För att få fram våra 2r-1= 7 värden på T(x) måste vi nu multiplicera 2r-1 tal med en approximativ storlek n/r siffror långa. Vi måste nämligen beräkna P(0)Q(0), P(1)Q(1), P(-1)Q(-1) osv. (Åtgärd med hög kostnad)
(3) Rekonstruera T(x) från våra datapunkter. (Lågkostnadsåtgärd: men kommer att beaktas senare).
(4) Med vår rekonstruerade T(x), utvärdera T(10⁶) i detta fall
Detta leder på ett trevligt sätt till en analys av körtiden för denna metod.
Löptid
Från ovanstående har vi kommit fram till att, om man tillfälligt bortser från de lågkostnadsåtgärderna (1) och (3), kommer kostnaden för att multiplicera två n-siffriga tal med Tooms algoritm att lyda:
Det här är vad vi såg i det fungerande exemplet. Att multiplicera varje P(0)Q(0), P(1)Q(1) osv. kostar T(n/r) eftersom vi multiplicerar två tal med längden ungefär n/r. Vi måste göra detta 2r-1 gånger, eftersom vi vill ta ett urval av T(x) – som är ett polynom av ordningen 2r-2 – på 2r-1 ställen.
Hur snabbt är detta? Vi kan lösa denna typ av ekvation explicit.