Karatsuba-algoritmus

Próbáljuk meg megoldani a feladatot csak a következő segédanyagok felhasználásával: Haskell könyvtárainak dokumentációja, Hoogle, a tárgy honlapja és a BE-AD rendszerbe feltöltött beadandók.

A feladatok egymásra épülnek, ezért érdemes ezeket a megadásuk sorrendjében megoldani, de legalább megérteni az aktuális feladatot megelőző feladatokat! A függvények definíciójában lehet, sőt javasolt is alkalmazni a korábban definiált függvényeket (függetlenül attól, hogy sikerült-e azokat megadni). Beadni viszont a teljes megoldást kell, vagyis az összes függvény definícióját egyszerre!

Tekintve, hogy a tesztesetek, bár odafigyelés mellett íródnak, nem fedik le minden esetben a függvény teljes működését, határozottan javasolt még külön próbálgatni a megoldásokat beadás előtt!

A feladat összefoglaló leírása

A Karatsuba-algoritmus egy gyors szorzást megvalósító algoritmus. Az algoritmus két, n számjegyből álló szám szorzatát állítja elő hatékonyan. Ilyenkor ugyanis a szorzat legfeljebb nlog23 ≈ n1, 585 számjegy szorzásából áll elő, ellentétben a hagyományos szorzás algoritmusával, amely n2 számjegy szorzásából határozható meg.

Például a Karatsuba-algoritmus két, 1024 számjegyből álló szám szorzatát 310 = 59049 lépésből tudja előállítani, ellentétben a hagyományos algoritmus négyzetes, azaz (210)2 = 1,048,576 darab szorzásával.

A számok, amelyekkel a feladatban foglalkozunk, tetszőleges alapú számrendszerben adottak. Az egyszerűség kedvéért a számrendszer alapszámát és az számrendszerhez tartozó “számjegyeket” is egész számokkal adjuk meg.

A számokat listákkal ábrázoljuk, ahol a hatványok együtthatói helyiérték szerinti növekvő sorrendben adottak (vagyis a lista elején szerepel a legkisebb helyiérték, a végén pedig a legnagyobb).

Például a 2015-ös egész szám 10-es számrendszerbeli ábrázolása [5,1,0,2], azaz 5 * 100 + 1 * 101 + 0 * 102 + 2 * 103. Ugyanez az érték 8-as számrendszerben felírva [7,3,7,3], azaz 7 * 80 + 3 * 81 + 7 * 82 + 3 * 83.

A függvények jobb olvashatóságának kedvéért a következő típusszinonimákat vezetjük be:

type Base   = Int
type BigNum = [Int]

ahol:

Konverziók (segédfüggvények a teszteléshez)

Adjuk meg azt a függvényt, amely a megadott számrendszerben ábrázol egy egész számot!

Megjegyzések:

toBigNum :: Base -> Integer -> BigNum

Test>
[0] :: BigNum
Test>
[0, 0, 1, 0, 0, 1, 1] :: BigNum
Test>
[4, 0, 6, 1] :: BigNum
Test>
[3, 0, 2, 5, 1, 2, 6, 4, 6, 7, 2, 6, 1] :: BigNum
Test>
[1, 4, 4, 0, 7, 1, 4, 3, 6, 1, 2, 2, 0, 2, 2, 4, 0, 6, 3, 0, 0, 5, 3, 7, 2, 6, 7, 4, 6, 0, 0, 5, 6, 1, 0, 7, 6, 1, 1, 1] :: BigNum
Test>
⊥₁ :: BigNum
⊥₁: toBigNum: improper arguments: -10 2
CallStack (from HasCallStack):
  error, called at ./Karatsuba.lhs:97:18 in main:Karatsuba
Test>
⊥₁ :: BigNum
⊥₁: toBigNum: improper arguments: 10 1
CallStack (from HasCallStack):
  error, called at ./Karatsuba.lhs:97:18 in main:Karatsuba
Test>
⊥₁ :: BigNum
⊥₁: toBigNum: improper arguments: -10 0
CallStack (from HasCallStack):
  error, called at ./Karatsuba.lhs:97:18 in main:Karatsuba

Adjuk meg azt a függvényt, amely egy BigNum típusú számot egész számmá alakít át!

fromBigNum :: Base -> BigNum -> Integer

Test>
100 :: Integer
Test>
0 :: Integer
Test>
100121 :: Integer
Test>
191751059232884086668491363525390625 :: Integer

Listák kiegészítése

Adjuk meg azt a magasabbrendű függvényt, amely segítségével egy sorozatot ki tudunk egészíteni megadott hosszúságúra, amikor arra szükség van!

pad :: ([a] -> [a] -> [a]) -> a -> Int -> [a] -> [a]

Test>
[0, 0, 0, 1, 1, 1, 1] :: [Integer]
Test>
[1, 1, 1, 1, 1, 1, 1, 1] :: [Integer]
Test>
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] :: [Integer]
Test>
[5, 5, 5, 1, 1, 1, 5, 5] :: [Integer]

A fenti függvény segítségével megadjuk a jobbról és balról kiegészítő függvényeket, amelyekre a későbbiekben egyébként szükségünk is lesz.

padLeft :: a -> Int -> [a] -> [a]

Test>
[0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] :: [Integer]
Test>
[1, 2, 3, 4, 5] :: [Integer]
Test>
[1, 1, 1, 1, 2, 3, 4, 5] :: [Integer]
padRight :: a -> Int -> [a] -> [a]

Test>
[1, 2, 3, 4, 5, 6, 7, 8, 9, 9] :: [Integer]
Test>
[1, 9, 9, 9, 9, 9, 9, 9, 9, 9] :: [Integer]

Két szám azonos méretűre alakítása

Adjuk meg azt az operátort, amely normalizál két számot, azaz azonos hosszúságúra alakít!

Megjegyzés: Értelemszerűen, az eredeti számok jelentését meg kell őrizni, így a hosszabb lista lesz a kiindulási alap!

(<=>) :: BigNum -> BigNum -> (BigNum, BigNum)

Test>
([], []) :: (BigNum, BigNum)
Test>
([0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) :: (BigNum, BigNum)
Test>
([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) :: (BigNum, BigNum)
Test>
([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [1, 2, 3, 4, 5, 0, 0, 0, 0, 0]) :: (BigNum, BigNum)
Test>
([1, 2, 3, 4, 5, 0, 0, 0, 0, 0], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) :: (BigNum, BigNum)
Test>
([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) :: (BigNum, BigNum)

Számok összeadása

Definiáljuk a számok összeadását! Az első paraméter a számrendszer alapszáma, a rákövetkező két paraméter pedig az összeadandó számokat adja meg.

FONTOS! A művelet eredményét az adott reprezentációban kell előállítani! Az operandusok egész számmá alakítása és a művelet elvégzése után a szám visszaalakítását végző megoldás NEM fogadható el!

Megjegyzések:

addBigNums :: Base -> BigNum -> BigNum -> BigNum

Test>
101 :: Integer
Test>
111 :: Integer
Test>
100 :: Integer

Számok összegzése

Korábban definiáltuk két szám összeadását. Definiáljuk ennek segítségével a nagy számok listájának összegzését! Ekkor lényegében az előbbi műveletet kell listákra kiterjesztenünk.

sumBigNums :: Base -> [BigNum] -> BigNum

Test>
[] :: BigNum
Test>
[0, 0, 0, 2, 1, 3] :: BigNum
Test>
10250 :: Integer

Számok különbsége

Az összeadás mintájára most definiáljuk a számok különbség képzését! Amennyiben a kivonás eredménye negatív, akkor ezt az információt a legnagyobb helyiérték együtthatójában őrizzük meg!

FONTOS! A művelet eredményét az adott reprezentációban kell előállítani! Az operandusok egész számmá alakítása és a művelet elvégzése után a szám visszaalakítását végző megoldás NEM fogadható el!

subBigNums :: Base -> BigNum -> BigNum -> BigNum

Test>
[1, 0, 0] :: BigNum
Test>
[4, 4, 1] :: BigNum
Test>
[0, 1, 0, 1, 1, 0, 1] :: BigNum
Test>
0 :: Integer

Számok listájának különbsége

Az előbb megadott függvény segítségével adjuk meg a számok listájának különbség képzését!

diffBigNums :: Base -> [BigNum] -> BigNum

Test>
[1, 0, 1, 0, 0] :: BigNum
Test>
5 :: Integer
Test>
[0, 1, 0, 0, 0] :: BigNum
Test>
2 :: Integer
Test>
[0, 0, 1, 1, 0, 1, 0, 0, 0] :: BigNum

Szám szorzása skalár értékkel (előkészítés)

Adjuk meg azt a függvényt, amely meghatározza egy i szám b alapú egész logaritmusát (j) és az ennek megfelelő b hatványát! A függvény eredménye egy rendezett pár, amelynek:

Megjegyzések:

logPowerBase :: Base -> Int -> (Int, Int)

Test>
(6, 64) :: (Int, Int)
Test>
(20, 1152921504606846976) :: (Int, Int)
Test>
⊥₁ :: (Int, Int)
⊥₁: logPowerBase: improper arguments
CallStack (from HasCallStack):
  error, called at ./Karatsuba.lhs:303:26 in main:Karatsuba

Adjuk meg azt a függvényt, amely meghatározza egy szám adott alapszámra vett hatványösszegre való bontását! A függvény a hatványösszegben használt hatványkitevők listáját adja meg!

Megjegyzések:

Például:

powersOf :: Base -> Int -> [Int]

Test>
[2, 1, 1, 1, 1, 0, 0, 0, 0] :: [Int]
Test>
[9, 8, 7, 6, 5, 3] :: [Int]
Test>
[18, 18, 18, 18, 18, 18, 18, 18, 18, 17, 17, 16, 16, 15, 15, 15, 14, 14, 14, 13, 13, 13, 13, 13, 13, 13, 12, 12, 10, 10, 10, 9, 9, 9, 9, 9, 9, 8, 8, 8, 8, 8, 8, 8, 8, 7, 7, 7, 7, 7, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0] :: [Int]

Szám szorzása skalár értékkel

Adjuk meg a skalárszorzás műveletét a korábban definiált függvények segítségével!

FONTOS! A művelet eredményét az adott reprezentációban kell előállítani! Az operandusok egész számmá alakítása és a művelet elvégzése után a szám visszaalakítását végző megoldás NEM fogadható el!

Megjegyzések:

Ennek megfelelően a skalárszorzás lépései:

  1. A szorzót felbontjuk az alapszám hatványainak összegévé, de ebből csak a kitevők szükségesek számunkra. Használjuk a korábban definiált powersOf függvényt!

  2. A hatványkitevőket a megjegyzés első pontjában ismertetett módon fogjuk felhasználni, azaz minden egyes hatványkitevő és az eredeti szám esetén a hatványkitevőnek megfelelő mértékben a kisebb helyiértéktől eltoljuk az eredeti számot.

  3. n hatványkitevő esetén n darab számunk keletkezik ezzel a módszerrel, amelyeket végül összegezni kell.

Példaul 2-es alapban a 11 skalár és a [1,0,1] BigNum esetén ez a következőképpen fog történni:

multBigNum :: Base -> Int -> BigNum -> BigNum

Test>
[4, 3, 7, 2] :: BigNum
Test>
1500 :: Integer
Test>
113868789652928492486967681984 :: Integer

Számok szorzata

Definiáljuk két szám szorzatát! A művelet felfogható úgy, hogy egy skalársorozattal szorozzuk végig ugyanazt a nagy számot, majd összegezzük a szorzások eredményeit. A szorzások során fontos, hogy melyik helyiértékkel szorzunk éppen, hiszen vegyük észre, a helyiértékektől függ a “számjegy” tényleges értéke!

Például (8-as számrendszerben):

    [2,1,7]
  * [1,2,1]
------------------
    [2,1,7]           (1 * [2,1,7])
      [4,2,6,1]       (2 * [2,1,7])
        [2,1,7]       (1 * [2,1,7])
------------------
    [2,5,3,0,1,1]
multBigNums :: Base -> BigNum -> BigNum -> BigNum

Test>
[0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1] :: BigNum
Test>
[0, 6, 7, 7] :: BigNum
Test>
4080 :: Integer
Test>
4080 :: Integer

Egy újabb típusszinonimát vezetünk be az algoritmus finomhangolásához, amely valójában egy egész érték lesz:

type Granularity = Int

Karatsuba művelete

A függvény paraméterei a következők:

Az algoritmus lépései:

  1. Megnézzük, hogy a számok valamelyike kellően kicsi-e, azaz a listák valamelyikének hossza kisebb vagy egyenlő-e a második paraméter értékénél.

    • Ha igen, akkor a korábban megírt szorzás műveletével összeszorozzuk és kész az eredmény.

    • Ha nem, akkor továbblépünk a következő pontra.

  2. Normalizáljuk mindkét számot, hogy azonos méreten legyenek ábrázolva.

  3. Mindkét számot körülbelül a felénél ketté vágjuk (low1, high1) és (low2, high2) listákra, ahol a lowi a neki megfelelő szám kisebb helyiértékeit, a highi pedig annak nagyobb helyi értékeit tartalmazza. Amennyiben a szám páratlan számjegyből áll, akkor a lowi rész tartalmazzon több számjegyet. Például: a [1,2,3] szám esetén a low legyen [1,2] és a high pedig [3].

  4. Három különböző számolás szükséges a szorzat előállításához:

    • A z0 a Karatsuba-szorzás low1 és low2 részlistákon vett (rekurzív) eredménye lesz.

    • A z1 a Karatsuba-szorzás (low1 + high1) és (low2 + high2) összegeken vett (rekurzív) eredménye lesz.

    • A z2 a Karatsuba-szorzás high1 és high2 részlistákon vett (rekurzív) eredménye lesz.

  5. A két szám szorzata végül a z2 * b(2 * m2) + (z1 − z2 − z0) * bm2 + z0 képlet alapján határozható meg, ahol:

    • A z0, z1, z2 értékek az előbbi pontban megadottak alapján kiszámításra kerülnek.

    • b a számrendszer alapszáma.

    • m2 pedig a paraméterül kapott (normalizált) listák hosszának (m) felének felső egészrésze, azaz m / 2⌉.

    • A képletben a z2 * b(2 * m2) és a (z1 − z2 − z0) * bm2 kifejezések kiértékelésénél ne a multBigNum műveletet alkalmazzuk, hanem az alacsonyabb helyiérték szerinti eltolás műveletét (padLeft). Ugyanis az alapszám Int típusú, így a bi hatvány könnyedén túlcsordulhat!

karatsuba :: Base -> Granularity -> BigNum -> BigNum -> BigNum

Test>
10000 :: Integer
Test>
10022012 :: Integer
Test>
15241578780673678546105778296296299281054720515622620750190521 :: Integer
Test>
[9, 4, 2, 1, 0, 5, 2, 3, 2, 4, 8, 7, 7, 0, 9, 6, 9, 3, 7, 4, 8, 5, 1, 6, 4, 3, 2, 0, 3, 7, 1, 9, 5, 0, 7, 0, 5, 8] :: BigNum