DD2458, Problemlösning och programmering under press Föreläsning 8: Aritmetik och stora heltal Datum: 2007-11-06 Skribent(er): Martin Tittenberger, Patrik Lilja Föreläsare: Per Austrin Denna föreläsning handlade om beräkningar med flyttal och stora heltal. 1 Flyttalsaritmetik Låt oss betrakta två exempel där flyttal inte beter sig som man tror. Exempel 1: Avrundning double x = 0.29; int y = (int) (100 * x); printf("%d", y); Utdata från programmet ovan blir 28 istället för 29. Exempel 2: Jämförelse for (double x = 0.0; x!= 1.0; x += 0.1) printf("hello World\n"); Loopen kommer att köras oändligt många gånger eftersom termineringsvillkoret aldrig uppfylls. 1.1 Flyttalsrepresentation Orsaken till att de märkliga fenomenen uppstår i exemplen ovan är att man i allmänhet inte kan representera reella tal exakt med ändligt mycket minne. I exemplen används primitiva datatyper vars precision är begränsad. Vi skall titta närmare på hur flyttal representeras i minnet. 1.1.1 IEEE Floating Point Representation Ett flyttal x representeras på följande sätt enligt IEEE standarden: x = S M 2 E, där: S = Teckenbit {0, 1} M = Mantissa (siffrorna i talet) E = Exponent (storleken på talet) 1
2 DD2458 Popup HT 2007 Typ Teckenbit Mantissa Exponent Storlek float 1 24 8 32 double 1 53 11 64 long double 1 1 113 15 128 2 Tabell 1: Antal bitar för S, M och E för olika flyttalstyper. 1.1.2 Flyttalstyper Som man kan notera i tabell 1 är summan av antalet bitar för de respektive typerna ett större än storleken (totala antalet bitar). Detta beror på att flyttal lagras i en normaliserad form där den mest signifikanta biten i mantissan alltid är satt. Därför behöver inte denna bit lagras. Om den mest signifikanta biten inte skulle vara satt så kan man istället representera talet som S (2 M) 2 E 1. 1.1.3 En närmare titt på double Ett flyttal x av typen double representeras på följande sätt: x =( 1) S (1 + M 2 52 ) 2 E 1023 där S {0, 1}, 0 M<2 52, 1 E<2 11 1. Värdena E =0och E =2 11 1 är reserverade för speciella tal. En double där E =0representerar talet: ( 1) s (M 2 52 ) 2 1023 Notera att om M =0får vi precis 0.Endouble där E =2 11 1 kan representera oändligheten eller NaN 3,setabell2. Exponent Mantissa Betydelse 0 0 ±0 (beror på S) 0 0 Ej normaliserat tal 2047 0 ± (beror på S) 2047 0 NaN (Not a Number) Tabell 2: Reserverade tal. För att erhålla samt NaN i C++ utan att få varken kompileringsvarningar eller fel så behöver man först inkludera <numeric> och sedan anropa funktionerna: std::numeric_limits<double>::infinity() std::numeric_limits<double>::quiet_nan() I Java är motsvarande värden definierade som konstanter i klassen Double och kan kommas åt på följande sätt: Double.NEGATIVE_INFINITY Double.POSITIVE_INFINITY Double.NaN 1 Datatypen finns ej i Java. 2 Storleken varierar mellan 80 och 128 bitar beroende på arkitektur. 3 Resultatet av att någon operation misslyckades. Ex. 1, 0.0/0.0,
Aritmetik och stora heltal 3 1.2 Varför blir det fel? Vi skall nu redogöra mer ingående för varför de märkliga fenomenen uppstod i de två första exemplen samt presentera ett par olika lösningsförslag. Exempel 1 Talet 0.29 kan inte representerasexakt med en double. Datornlagrardå istället den double som ligger närmast 0.29, vilken råkar vara 0x128F 5C28F 5C28F 2 54 och som har det ungefärliga värdet 0.28999999999999998. När talet sedan multipliceras med 100 och trunkeras till ett heltal erhåller vi resultatet 28. Lösning Läs in talet som <int>.<int>. Kan dock bli knepigt att kunna skilja på tal som t.ex. 5.1 och 5.01. Avrunda till närmsta heltal. y = (int) (x * 100 + 0.5) Detta fungerar så länge x inte är för stort (men i sådant fall så behöver man ändå en större datatyp för y). Exempel 2 Talet 0.1 kan inte heller representeras exakt med en double. Men det kan däremot 1. Detta betyder att efter 10 iterationer i for-loopen så kommer inte variabeln x att anta exakt värdet 1 (men nästan). Termineringskriteriet kommer alltså aldrig att uppfyllas. Lösning Använd ej strikt jämförelse (==) för att kontrollera likhet mellan två flyttal a och b, tya och b är sällan exakt lika. I detta fall lämpar det sig bättre att undersöka om talen är väldigt nära varandra. Två metoder man kan tillämpa för att jämföra flyttal på är: Absolutfel: Vi definierar a och b som lika i absolut bemärkelse: och a är mindre än b: a abs = b om a b <ɛ a abs <bom a abs b och a<b Relativfel: Vi definierar a och b som lika i relativ bemärkelse: a rel = b om a b <ɛ ( a + b ) och a är mindre än b: a rel <bom a rel b och a<b
4 DD2458 Popup HT 2007 Absolutfel fungerar dåligt för mycket stora tal eftersom differensen mellan talen kan vara stor trots att den relativt sett är obetydlig. Relativfel fungerar dåligt för små tal av motsvarande skäl. Vill man vara helt säker så kan man använda båda metoder. Då gäller: a komb = b om a abs = b eller a rel = b a komb < b om a komb b och a<b Vilket värde man skall välja på ɛ varierar från fall till fall, men ett värde mellan 10 7 och 10 13 räcker för de flesta tillämpningar. 1.3 float vs. double I de flesta tillämpningar där man behöver använda flyttal så bör man välja double. Det ger betydligt högre precision än float och har väsentligen samma prestanda. float kan dock vara användbar om man har ont om minne, eller om avrundningsfel inte har någon betydelse, som t.ex. i datorgrafiksammanhang. Om man behöver godtycklig precision så kan man använda sig av biblioteket GMP 4 för C++ eller klassen BigInteger ijava. 1.4 Exempel på när flyttal är att föredra framför heltal. Finn y = 4 x, 1 x 10 40 givet att y är ett heltal. Observera att x är för stort för att kunna lagras i en heltalstyp. Lösning 1. Läs in x som en double för erhålla approximationen X x (1 ± ɛ) där ɛ 10 15. 2. Låt Y = pow(x, 0.25) vara en approximation av y. 3. Låt svaret vara Y avrundat till närmaste heltal. 1.4.1 Analys av precision Låt oss analysera hur stort fel Y kan ha. Y = pow(x, 0.25) (1 ± ɛ) X 1 4 =(1± ɛ) e ln(x)/4 (1 ± ɛ) e ln(x) 4 (1±ɛ) ±ɛ ln(y) =(1± ɛ) y e (1 ± ɛ) (1 ± ɛ ln(y)) y y ± y ɛ (1 ± ln(y)) 4 GNU Multiple Precision Arithmetic Library, http://gmplib.org/
Aritmetik och stora heltal 5 I tredje sista steget utnyttjade vi likheten ln(x) 4 =ln(y), och i näst sista steget utnyttjade vi att e ±δ 1 ± δ, för små värden på δ. Vi får slutligen ett fel i vår beräkning som är Y y y ɛ (1 + ln(y)) 2 10 4 0.5, vilket är vad som krävs för att få korrekt svar efter avrundning. 1.5 Mer information Mer utförlig information om flyttalshantering finns att läsa i följande artikel: http://docs.sun.com/source/806-3568/ncg_goldberg.html 2 Heltalsaritmetik 2.1 Floor och ceil Givet x R definieras: x = floor(x) som det största heltal mindre än eller lika med x. x = ceil(x) som det minsta heltal större än eller lika med x. x x x 5 5 6 5.3 5 6 0.7 1 0 Tabell 3: Exempel på floor och ceil 2.1.1 Operationer Givet x R och n, m Z gäller följande: 2.1.2 Att beräkna n m och n m x = n n x<n+1 x = n n 1 <x n x = x x = x x ± n = x ±n x ± n = x ±n n n + m 1 = för m>0 m m Det enklaste sättet att beräkna n m och n m ärattförstberäkna n m som en double och sedan använda sig av floor och ceil. Detta kommer att fungera så länge inte talen blir väldigt stora då vi riskerar att få avrundningsfel. Dessa beräkningar kan göras på ett mer precist sätt. Idéen är att använda identiteterna ovan till att reducera alla fall till n m med n, m > 0, vilket ju precis motsvaras av heltalsdivision.
6 DD2458 Popup HT 2007 Algoritm 1: Beräkning av n m,därn, m Z Floor-Fraction(n, m) (1) if m<0 (2) return Floor-Fraction( n, m) (3) else if n<0 (4) return Ceil-Fraction( n, m) (5) else (6) return n/m Algoritm 2: Beräkning av n m,därn, m Z Ceil-Fraction(n, m) (1) if m>0 (2) return Floor-Fraction(n + m 1, m) (3) else (4) return Floor-Fraction( n m 1, m) 2.1.3 Exempel på tillämpning Kattisproblemet Rare Easy Problem 5 kan lösas på ett enkelt sätt med hjälp av floor och ceil. Problemet går ut på att hitta N givet K = N M, därm är N med sista siffran borttagen. Notera att M = N/10. Vi vill alltså bestämma N så att N N/10 = K. K = N N/10 = N + N/10 = 9N/10 K 1 < 9N/10 K 10(K 1) < 9N 10K 10(K 1) + 1 9N 10K 10K/9 1 N 10K/9 10K/9 1 N 10K/9 Där vi använder definitionen av ceil, från 2.1.1, vid övergången från likhet till olikhet. Om K ärdelbartmed9 så kommer vi att få lösningarna 10K/9 och 10K/9 1, annars kommer lösningen att vara 10K/9. 2.2 Stora heltal Ibland räcker inte de primitiva datatyperna till. Exempelvis vore kryptosystem som RSA enkla att knäcka om man bara använde sig av 64-bitars nycklar. I verkliga tillämpningar använder man sig typiskt av färdiga bibliotek som GMP eller 5 http://kattis.csc.kth.se/problem?id=easy
Aritmetik och stora heltal 7 BigInteger. Här ska vi dock titta på hur man själv kan skapa enkla motsvarigheter vilket man kan vara tvingad till i problemlösningssammanhang. 2.2.1 Representation Vi kan skriva ett tal x i någon bas b som: x = x n 1 b n 1 + x n 2 b n 2 + + x 2 b 2 + x 1 b + x 0 där 0 x i <b. Koefficienterna lagras exempelvis i en vektor. 2.2.2 Hur väljer vi b? b =2 k,t.ex.2 32 eller 2 64 Effektivt eftersom man utnyttjar minnesutrymmet väl vilket innebär att det blir få x i och därmed få beräkningar. Att det dessutom är datorns interna representation gör att division- och moduloberäkningarna i nedanstående algoritmer kan implementeras effektivt med shift- ochand-operationer. En nackdel är att en inläsning och utskrift i bas 10 blir jobbigt. b =10 Enkelt med inläsning, men ineffektivt eftersom b är så litet, vilket innebär att det blir det många x i och därmed görs fler beräkningar och det krävs mer minne. b =10 d Vid tillfällen då man behöver skriva ihop en lösning snabbt så är detta en bra kompromiss. In- och utläsning blir enkelt och samtidigt utnyttjas minnesutrymmet ganska väl. För att undvika overflow vid multiplikation så bör d väljas så att 10 2d = b 2 får plats i den primitiva datatyp man valt för vektorn [x i ]. Om man t.ex. använder en 64-bitars heltalstyp så är d =9ett bra val. 2.2.3 Operationer Vi antar att talen x, y och z är representerade som ovan i basen b. Vitänkerossatt x i =0om i> x, där x betecknar storleken på x:s vektor, samma sak gäller för y och z. Notera att dessa algoritmer är samma som man lärde sig i grundskolan. Algoritm 3: Addition, x + y Add(x, y) (1) carry 0 (2) n max( x, y ) (3) for i 0 to n 1 (4) tmp x i + y i + carry (5) z i tmp mod b (6) carry tmp/b (7) z n carry (8) return z
8 DD2458 Popup HT 2007 Algoritm 4: Subtraktion, x y Sub(x, y) (1) if x<y (2) return Sub(y,x) (3) borrow 0 (4) n max( x, y ) (5) for i 0 to n 1 (6) tmp x i y i borrow (7) z i tmp mod b (8) if tmp < 0 (9) borrow 1 (10) else (11) borrow 0 (12) return z Algoritm 5: Multiplikation, x y Mul(x, y) (1) z 0 (2) n x (3) m y (4) for i 0 to n 1 (5) for j 0 to m 1 (6) z i+j z i+j + x i y j (7) carry 0 (8) l z (9) for j 0 to l 1 (10) z j z j + carry (11) carry z j /b (12) z j = z j mod b (13) z l carry (14) return z För varje varv i loopen gäller att: z i+j = z i+j från föregående varv + x i y j + minnessiffror b 1+(b 1) 2 + b 1 = b 2 1 Det betyder att om vi väljer b så att b 2 1 ryms i den datatyp som används så löper vi ingen risk för overflow. Algoritmen har tidskomplexitet Θ(n 2 ), det finns dock betydligt snabbare algoritmer för multiplikation av stora tal. 6 6 Karatsuba Θ(n log 2 3 ), Fast-Fourier-Transform O(n log n log log n)
Aritmetik och stora heltal 9 Algoritm 6: Division (med små nämnare) Input: Heltal x och a, dära<b Output: Tupel ( x/a,x mod a) Div(x, a) (1) r 0 (2) n x (3) for i n 1 to 0 (4) tmp b r + x i (5) z i tmp/a (6) r tmp mod a (7) return (z,r) För att utföra division med stora nämnare krävs en betydligt krångligare algoritm. 2.2.4 Exponentiering Antag att vi vill beräkna x e för några heltal x och e. Den naiva ansatsen är att göra e stycken multiplikationer, vilket kan ta lång tid om e är stort. Det går att göra betydligt snabbare. Vi gör observationen att x e = x e mod 2 x 2 e 2 och utnyttjar det till att skapa en algoritm. Algoritm 7: Beräkning av x e genom upprepad kvadrering Square-and-Multiply(x, e) (1) z 1 (2) while e 0 (3) if e mod 2 = 1 (4) z z x (5) x x x (6) e e/2 (7) return z Eftersom e halveras i varje steg så kommer vi att göra O(log e) multiplikationer.