Knihovna dlouhých čísel c. Podívejte se, co je „Dlouhá aritmetika“ v jiných slovnících

Je známo, že počítač může pracovat s čísly, jejichž počet bitů je omezený. Zpravidla jsme zvyklí pracovat s 32 a 64bitovými celými čísly, která v platformě .NET odpovídají typům Int32 (int) a Int64 (long), resp.

Ale co když potřebujete reprezentovat číslo, jako je například 29! = 8841761993739701954543616000000? Takové číslo se nevejde do 64bitového, tím méně do 32bitového datového typu. Právě pro práci s tak velkými čísly existuje dlouhá aritmetika.

Dlouhá aritmetika- V počítačová technologie operace (sčítání, násobení, odčítání, dělení, umocňování atd.) na číslech, jejichž bitová hloubka přesahuje délku strojového slova daného počítač. Tyto operace jsou realizovány nikoli hardwarově, ale softwarově, s využitím základního hardwaru pro práci s čísly nižších řádů.

Dlouhá aritmetika může být také považována za jednu ze sekcí programování olympiád, protože při řešení problémů často nestačí kapacita standardních typů k vyjádření konečného výsledku. Při výběru programovacího jazyka pro potřeby olympiády není nepodstatná vestavěná sada nástrojů (hotové knihovny, implementované třídy). Mnoho jazyků (Java, Ruby, Python) má vestavěnou podporu pro dlouhou aritmetiku, což může výrazně zkrátit čas potřebný k napsání programu.

Platforma .NET až do verze 4.0 neměla vestavěnou podporu pro práci s dlouhými čísly. Ve verzi 4 získal .NET nejen dlouhá čísla, ale i komplexní čísla. Tato funkce je dostupná prostřednictvím sestavy Systém.Čísla a typy BigInteger A Komplex definovaný v oboru názvů se stejným názvem jako název sestavení.

Je třeba říci, že struktura BigInteger měl mít, ale v té době ještě nebyl zcela připraven, jeho implementace nesplňovala všechny potřeby (mohou zde být zahrnuty i problémy s výkonem), proto bylo rozhodnuto odložit jeho vydání až na .NET 4.0.

V tomto článku bych se rád podíval na detaily implementace dlouhé aritmetiky od Microsoftu.

Obecné pojmy

Myšlenka reprezentace dlouhých čísel v paměti počítače je poměrně jednoduchá. Zvažte číslo 123456789 v desítkové soustavě čísel. Je zřejmé, že to může být reprezentováno následovně:

123456789 10 = 1*10 8 + 2*10 7 + 3*10 6 + 4*10 5 + 5*10 4 + 6*10 3 + 7*10 2 + 8*10 1 + 9*10 0

V obecný případ, libovolné číslo může být reprezentováno jako:

A = a n-1 β n-1 + a n-2 β n-2 +…+a 1 β + a 0
kde β je základ číselné soustavy, ve které číslo reprezentujeme, a koeficienty a i splňují dvojitou nerovnost 0 ≤ a i< β.

Zobrazení čísla připomíná zobrazení polynomu, jen místo x na příslušný stupeň máme základ β na požadovaný stupeň. Jak známo, je vhodné reprezentovat polynom a 0 + a 1 x + a 2 x 2 + … + a n x n jako pole, jehož prvky představují koeficienty a i a index i určuje odpovídající stupeň x. Stejným způsobem se ukládá dlouhé číslo; zbývá pouze rozhodnout o volbě základu β.

Například stejné číslo 123456789 může být reprezentováno v desetitisícové (β = 10 4) číselné soustavě takto:

123456789 10 = 1*(10 4) 2 + 2345*(10 4) 1 + 6789*(10 4) 0

Zastoupením čísla 123456789 v desetitisícové číselné soustavě získáme dvě výhody najednou: za prvé snížíme množství spotřebované paměti, protože místo pole 9 čísel stačí uložit pole 3 čísel (1 , 2345 a 6789), za druhé, výrazně Zkrátíme čas potřebný k provedení standardních operací s dlouhými čísly, protože zpracováváme 4 číslice čísla najednou. Obecně je počítač stejně rychlý při přidávání jednociferných a 32bitových čísel, takže byste toho měli využít.

Radix β obvykle závisí na maximální velikosti základního datového typu v počítači a vybírá se na základě následujících úvah:

  1. základ musí odpovídat jednomu ze základních datových typů;
  2. Báze by měla být co největší, aby se zmenšila velikost reprezentace dlouhých čísel a zvýšila rychlost operací s nimi, ale dostatečně malá, aby všechny operace s koeficienty používaly datový typ základ;
  3. pro usnadnění výstupu a ladění můžete zvolit β jako mocninu 10, β - mocnina dvě umožňuje provádět rychlé operace na nízké úrovni.
Je třeba také poznamenat, že znaménko čísla je zohledněno v samostatné proměnné, to znamená, že pole obsahuje modul dlouhého čísla a číslo je také uloženo zpětně. To bylo provedeno především pro pohodlí: není třeba zpracovávat zvláštním způsobem nulový / poslední prvek pole, který by mohl uložit znaménko čísla, a všechny operace se provádějí od nízkých po vysoké číslice.

BigInteger od společnosti Microsoft

Když se podíváte na strukturu BigInteger přes dekompilátor Reflektor nebo dotPeek, uvidíme následující pole:

Soukromé statické pouze pro čtení BigInteger s_bnMinInt = new BigInteger(-1, new uint( (uint) int.MinValue )); private static BigInteger s_bnOneInt = new BigInteger(1); private static BigInteger s_bnZeroInt = new BigInteger(0); private static BigInteger s_bnMinusOneInt = new BigInteger(-1); interní int _sign; interní uint_bits; private const int knMaskHighBit = -2147483648; private const uint kuMaskHighBit = 2147483648U; private const int kcbitUint = 32; private const int kcbitUlong = 64; private const int DecimalScaleFactorMask = 16711680; private const int DecimalSignMask = -2147483648;
Struktura obsahuje pouze dvě pole instance (_sign a _bits), zbývající pole jsou konstanty a pole statického čtení představující hodnoty struktury pro čísla -1, 0 a 1.

Můžeme předpokládat, že proměnná _sign ukládá znaménko čísla a pole _bits obsahuje koeficienty a i . Vzhledem k tomu, že pole _bits je typu uint, můžeme také předpokládat, že základ β je mocninou dvou 2 32 (protože uint je 32bitové číslo bez znaménka).

Pokusme se tedy naše domněnky potvrdit nebo vyvrátit.

Konstruktor, který bere int jako argument, vypadá takto:

Konstruktor přijímající int

public BigInteger(int value) (​if (value == int.MinValue) (​this = BigInteger.s_bnMinInt; ) else ( this._sign = hodnota; this._bits = (uint) null; ) )


Jeho implementace vám může říci něco více o účelu proměnné _sign. Jak vidíte, pokud se dlouhé číslo vejde do rozsahu int (od -2 31 do 2 31 -1), uloží se do proměnné _sign a pole _bits se nepoužije, rovná se null. Tato optimalizace by měla urychlit provoz typu BigInteger a také snížit množství spotřebované paměti, když číslo ve skutečnosti není velké.

Konstruktor, který bere uint jako argument, vypadá takto:

Konstruktér s uint

public BigInteger(hodnota uint) (​if (hodnota<= (uint) int.MaxValue) { this._sign = (int) value; this._bits = (uint) null; } else { this._sign = 1; this._bits = new uint; this._bits = value; } }


Podle toho, zda se číslo vejde do rozsahu int, se zapíše buď do proměnné _sign nebo do pole _bits.

Následující konstruktor, který přijímá 64bitové číslo se znaménkem (dlouhé), pomůže odpovědět na otázku o výběru základu číselné soustavy:

Konstruktor, který přijímá dlouhé

public BigInteger(long value) (​if ((long) int.MinValue<= value && value <= (long) int.MaxValue) { if (value == (long) int.MinValue) { this = BigInteger.s_bnMinInt; } else { this._sign = (int) value; this._bits = (uint) null; } } else { ulong num; if (value < 0L) { num = (ulong) -value; this._sign = -1; } else { num = (ulong) value; this._sign = 1; } this._bits = new uint; this._bits = (uint) num; this._bits = (uint) (num >> 32); } }


Pokud se číslo nevejde do rozsahu int, pak, jak vidíme, proměnná _sign obsahuje znaménko čísla (-1 pro záporné a 1 pro kladné) a pole _bits obsahuje stejné koeficienty a i a je vyplněno jako následuje:

This._bits = new uint; this._bits = (uint)num; this._bits = (uint) (num >> 32);
V tomto případě je 64bitové číslo num rozděleno na dvě 32bitová čísla: (uint)num a (uint)(num >> 32). První číslo představuje posledních 32 bitů num, zatímco druhé představuje prvních 32 bitů (posun doprava o n bitů je ekvivalentní celočíselnému dělení 2n).

Určíme, jak dlouhé bude číslo.MaxValue = 2 63 -1 = 9223372036854775807 bude uložena ve struktuře BigInteger . Chcete-li to provést, vydělte to 2 32:

Ve skutečnosti (uint)long.MaxValue = 4294967295, (uint)(long.MaxValue >> 32) = 2147483647.

Takže 9223372036854775807 = 2147483647*(2 32) 1 + 4294967295*(2 32) 0 a BigInteger
bude zastoupena dvojice:

_sign = 1
_bits = (4294967295, 2147483647) // zapamatujte si, že číslo je uloženo zpětně

Pro dlouhé číslo -1234567891011121314151617181920 máme:


To znamená, že číslo je rozšířeno na mocniny 2 32 takto:
1234567891011121314151617181920 = 15*(2 32) 3 + 2501550035*(2 32) 2 + 3243814879*(2 32) 1 + 4035623136*(2 32) 0

Prostředek, BigInteger bude zastoupena dvojice:

_sign = -1 // znak čísla
_bits = (4035623136, 3243814879, 2501550035, 15)

Číslo, které se vejde do rozsahu int, řekněme 17, bude uloženo takto:

_sign = 17
_bits = null

Zkoumání konstruktérů struktur BigInteger můžeme uzavřít:

  1. pokud se číslo vejde do rozsahu int, pak se uloží do proměnné _sign;
  2. pokud se číslo nevejde do rozsahu int, pak je jeho znaménko uloženo v proměnné _sign (-1 pro záporné a 1 pro kladné) a pole _bits obsahuje koeficienty a i rozšíření dlouhého čísla se základem 2 32 .
Základ β = 2 32 je dobrá volba, protože s mocninami dvou se snáze pracuje na nízké úrovni (násobení a dělení mocninou dvěma odpovídá bitovým posunům doleva a doprava), a až 32 bitů čísla budou zpracovány v době, která umožňuje poměrně rychlé provádění operací na nich.

Obecně platí, že struktura BigInteger je kompletní implementace dlouhé aritmetiky na platformě .NET. Microsoft se ho zároveň snažil co nejvíce přiblížit primitivním číselným typům: instance BigInteger lze použít stejně jako jakýkoli jiný celočíselný typ. BigInteger přetěžuje standardní numerické operátory pro provádění základních matematických operací, jako je sčítání, odčítání, dělení, násobení, odčítání, negace a jednočlenná negace. Můžete také použít standardní číselné operátory k vzájemnému porovnání dvou hodnot BigInteger. Stejně jako jiné celočíselné typy, BigInteger Podporuje bitové operátory And, Or, XOR, vlevo a vpravo.

U jazyků, které nepodporují uživatelem definované operátory, struktura BigInteger také poskytuje ekvivalentní metody provádět matematické operace. To platí pro metody sčítání, dělení, násobení, negace, odečítání a některé další. Microsoft udělal to samé při implementaci struktury Desetinný .

Mnoho členů struktury BigInteger odpovídají přímo členům jiných celočíselných typů. Kromě, BigInteger přidává prvky jako:

  • IsEven – určuje, zda je číslo sudé;
  • IsPowerOfTwo - určuje, zda je číslo mocninou dvou;
  • Znaménko - vrací hodnotu označující znaménko velkého celého čísla;
  • Abs - vrátí absolutní hodnotu čísla BigInteger;
  • DivRem - vrátí podíl a zbytek operace dělení;
  • GreatestCommonDivisor - vrátí největšího společného dělitele dvou čísel;
  • Log - vrací logaritmus zadaného čísla v číselné soustavě se zadaným základem;
  • Max/Min - vrací největší/nejmenší ze dvou čísel;
  • ModPow - provádí modulární dělení čísla umocněného na jiné číslo;
  • Pow - zvýší hodnotu BigInteger na danou mocninu.

Pár slov o BigInteger v Mono a Javě

Je třeba poznamenat, že Mono má také podporu pro dlouhou aritmetiku. Realizace konstrukce BigInteger v Mono není prakticky žádný rozdíl od implementace Microsoftu, kromě toho, že nemá optimalizaci pro čísla reprezentovaná typem int.

To znamená, že číslo 17 v Mono bude reprezentováno dvojicí:

_sign = 1 // znak čísla
_bits = (17)

Podobná implementace BigInteger zastoupené v Javě:

Veřejná třída BigInteger rozšiřuje Number implementuje Comparable ( int signum; int mag; private int bitCount = -1; private int bitLength = -1; private int nejnižšíSetBit = -2; private int firstNonzeroByteNum = -2; private int firstNonzeroIntNum = -2; private final static long LONG_MASK = 0xffffffffL; )
Protože Java nemá nepodepsané typy, pole mag je typu int. V souladu s tím bude reprezentace dlouhého čísla v Javě a .NET odlišné. V .NET bude reprezentace o něco efektivnější, protože typ uint pokrývá větší rozsah:

Konstruktor, který přijímá dlouhé

private BigInteger(long val) ( if (val< 0) { signum = -1; val = -val; } else { signum = 1; } int highWord = (int)(val >>> 32); if (highWord == 0) ( mag = new int; mag = (int)val; ) else ( mag = new int; mag = highWord; mag = (int)val; ) )


V Javě, stejně jako v Mono, neexistuje žádná optimalizace pro čísla reprezentovaná typem int.

Výkon BigInteger

Práce s dlouhým číslem BigInteger , musíte si pamatovat možné problémy související s výkonem. Například zdánlivě neškodný operátor ++ může mít významný dopad na výkon:

Délka int = 1000000; BigInteger num = BigInteger.Parse("12345678910111213141516171819"); for (int i = 2; i< length; i++) { if (IsPrime(i)) num++; } Console.WriteLine(num);
Ačkoli se zdá, že tento příklad mění hodnotu existujícího objektu, není tomu tak. Objekty BigInteger neměnný, což znamená, že interně společný jazykový modul runtime skutečně vytvoří nový objekt BigInteger a přiřadí mu hodnotu o jednu větší než předchozí.

V v tomto příkladu, můžete provést následující: provádět mezioperační operace pomocí běžných číselných typů a poté použít BigInteger :

Délka int = 1000000; BigInteger num = BigInteger.Parse("12345678910111213141516171819"); int temp = 0; for (int i = 2; i< length; i++) { if (IsPrime(i)) temp++; } num += temp; Console.WriteLine(num);
Ostatní číselné typy .NET Framework jsou také neměnné. Nicméně, protože typ BigInteger nemá žádnou horní ani dolní hranici, jeho hodnoty mohou narůst do velmi vysokých hodnot a mít měřitelný dopad na výkon.

Místo závěru

Shrneme-li, můžeme říci, že platforma .NET počínaje verzí 4 získala plnohodnotnou implementaci celé číslo dlouhá aritmetika. Možná pro úplné štěstí zbývá jen implementovat strukturu BigRational , který je v .NET BCL již nějakou dobu přítomen ve stavu beta. Přidat štítky

Dlouhá aritmetika

Dlouhá aritmetika je množina software(datové struktury a algoritmy), které umožňují pracovat s čísly mnohem větších velikostí, než umožňují standardní datové typy.

Typy celočíselné dlouhé aritmetiky

Obecně lze říci, že i pouze v úlohách olympiády je sada nástrojů poměrně velká, takže udělejme klasifikaci různé typy dlouhá aritmetika.

Klasická dlouhá aritmetika

Základní myšlenkou je, že číslo je uloženo jako pole jeho číslic.

Čísla lze použít z jedné či druhé číselné soustavy, obvykle se používá desítková číselná soustava a její mocniny (deset tisíc, miliarda), nebo se používá binární číselná soustava.

Operace s čísly v tomto typu dlouhé aritmetiky se provádějí pomocí „školních“ algoritmů pro sčítání, odčítání, násobení a dlouhé dělení. Jsou však pro ně také použitelné rychlé algoritmy násobení: Fast Fourier Transform a Karatsuba Algorithm.

Zde popisujeme práci pouze s nezápornými dlouhými čísly. Pro podporu záporných čísel je nutné zavést a podporovat další příznak pro „negativitu“ čísla nebo pracovat v doplňkových kódech.

Datová struktura

Dlouhá čísla budeme ukládat jako vektor čísel, kde každý prvek je jedna číslice čísla.

typedef vektor< int >lnum;

Pro zvýšení efektivity budeme pracovat v systému na bázi základní miliardy, tzn. Každý prvek vektoru neobsahuje jednu, ale všechny číslice najednou:

const int základ = 1000 * 1000 * 1000 ;

Číslice budou ve vektoru uloženy v takovém pořadí, aby na prvním místě byly nejméně významné číslice (tj. jedničky, desítky, stovky atd.).

Všechny operace budou navíc implementovány tak, že po provedení kterékoli z nich nebudou žádné úvodní nuly (tj. nuly navíc na začátku čísla) (samozřejmě za předpokladu, že před každou z nich nebudou žádné úvodní nuly). úkon). Je třeba poznamenat, že v prezentované implementaci jsou pro číslo nula správně podporovány dvě reprezentace: prázdný vektor číslic a vektor číslic obsahující jediný prvek - nulu.

Závěr

Nejjednodušší je vytisknout dlouhé číslo.

Nejprve jednoduše vytiskneme úplně poslední prvek vektoru (nebo , je-li vektor prázdný) a poté vytiskneme všechny zbývající prvky vektoru a doplníme je nulami před znaky:

printf ("%d" , a.prázdné () ? 0 : a.zpět () ) ; for (int i= (int ) a.velikost () - 2 ; i>= 0 ; -- i) printf ("%09d" , a[ i] ) ;

(zde je malý jemný bod: nezapomeňte si zapsat typové obsazení, protože jinak bude číslo bez znaménka, a pokud , pak při odečítání dojde k přetečení)

Čtení

Načteme řetězec do a poté jej převedeme na vektor:

for (int i= (int ) s.length () ; i> 0 ; i- = 9 ) jestliže (i< 9 ) a.push_back (atoi (s.substr (0 , i) .c_str () ) ) ; else a.push_back (atoi (s.substr (i- 9 , 9 ) .c_str () ) ) ;

Pokud místo toho použijete pole "s", kód bude ještě kompaktnější:

for (int i= (int ) strlen (s) ; i> 0 ; i- = 9 ) ( s[ i] = 0 ; a.push_back (atoi (i>= 9? s+ i- 9 : s) ); )

Pokud vstupní číslo již může obsahovat úvodní nuly, lze je po přečtení odstranit tímto způsobem:

while (a.size() >

Přidání

Přidá číslo k číslu a uloží výsledek do:

< max(a.size () ,b.size () ) || carry; ++ i) { if (i == a.size () ) a.push_back (0 ) ; a[ i] + = carry + (i < b.size () ? b[ i] : 0 ) ; carry = a[ i] >= základ; if (nesou) a[ i] - = základ; )

Odčítání

Odečte číslo () od čísla a uloží výsledek do:

int carry = 0 ; for (velikost_t i= 0 ; i< b.size () || carry; ++ i) { a[ i] - = carry + (i < b.size () ? b[ i] : 0 ) ; carry = a[ i] < 0 ; if (carry) a[ i] + = base; } while (a.size () >1 && a.back () == 0 ) a.pop_back () ;

Zde po provedení odečítání odstraníme úvodní nuly, abychom podpořili predikát, že žádné úvodní nuly nejsou.

Násobení dlouhým krátkým

Vynásobí long krátkým() a výsledek uloží do:

int carry = 0 ; for (velikost_t i= 0 ; i< a.size () || carry; ++ i) { if (i == a.size () ) a.push_back (0 ) ; long long cur = carry + a[ i] * 1ll * b; a[ i] = int (cur % base) ; carry = int (cur / base) ; } while (a.size () >1 && a.back () == 0 ) a.pop_back () ;

Zde po provedení dělení odstraníme úvodní nuly, abychom podpořili predikát, že žádné úvodní nuly nejsou.

(Poznámka: metoda další optimalizace. Pokud je rychlost operace extrémně důležitá, můžete zkusit nahradit dvě dělení jedním: počítejte pouze celočíselnou část dělení (v kódu je to proměnná) a poté z ní vypočítejte zbytek dělení (pomocí jednoho násobení úkon). Tato technika vám zpravidla umožňuje zrychlit kód, i když ne příliš výrazně.)

Násobení dvou dlouhých čísel

Vynásobí a uloží výsledek do:

Lnum c(a.size() + b.size()); for (velikost_t i= 0 ; i< a.size () ; ++ i) for (int j= 0 , carry= 0 ; j< (int ) b.size () || carry; ++ j) { long long cur = c[ i+ j] + a[ i] * 1ll * (j < (int ) b.size () ? b[ j] : 0 ) + carry; c[ i+ j] = int (cur % base) ; carry = int (cur / base) ; } while (c.size () >1 && c.back () == 0 ) c.pop_back () ;

Dělení dlouhé na krátké

Dělí dlouhé krátkým (), ukládá kvocient v , zbytek v:

int carry = 0 ; for (int i= (int ) a.size () - 1 ; i>= 0 ; -- i) ( long long cur = a[ i] + carry * 1ll * base; a[ i] = int (cur / b) ; carry = int (cur % b) ) while (a.velikost () > 1 && a.back () == 0 ) a.pop_back () ;

Dlouhá aritmetika v faktorizované formě

Smyslem je zde ukládat nikoli samotné číslo, ale jeho rozklad na rozklad, tzn. stupně každého prvočísla v něm obsaženého.

Tato metoda je také velmi jednoduchá na implementaci a je velmi snadné provádět operace násobení a dělení, ale není možné provádět sčítání nebo odčítání. Na druhou stranu tato metoda oproti „klasickému“ přístupu výrazně šetří paměť a umožňuje násobení a dělení mnohem (asymptoticky) rychleji.

Tato metoda se často používá, když je nutné dělit komplexním modulem: pak stačí uložit číslo ve formě mocnin prvočíselných dělitelů tohoto modulu a další číslo - zbytek stejného modulu.

Dlouhá aritmetika pomocí systému jednoduchých modulů (čínský teorém nebo Garnerovo schéma)

Myšlenka je taková, že se vybere určitý systém modulů (obvykle malých, které se vejdou do standardních datových typů) a počet se uloží jako vektor zbytků z jeho rozdělení každým z těchto modulů.

Jak uvádí čínská věta o zbytku, stačí to k jedinečnému uložení libovolného čísla v rozsahu od 0 do součinu těchto modulů mínus jedna. Zároveň existuje Garnerův algoritmus, který umožňuje tuto obnovu z modulární formy do obvyklé, „klasické“ podoby čísla.

Tato metoda tedy umožňuje úsporu paměti ve srovnání s "klasickou" dlouhou aritmetikou (i když v některých případech ne tak radikální jako metoda faktorizace). V modulární formě je navíc možné velmi rychle provádět sčítání, odčítání a násobení - to vše v asymptoticky krátkém čase, úměrném počtu modulů systému.

To vše je však za cenu velmi pracného překladu čísla z tohoto modulárního tvaru do běžného tvaru, který kromě značné časové náročnosti vyžaduje i implementaci „klasické“ dlouhé aritmetiky s násobením.

Kromě toho vyrábět divizečísla v takové reprezentaci pomocí systému jednoduchých modulů není možné.

Typy zlomkové dlouhé aritmetiky

Operace se zlomkovými čísly se v úlohách olympiády vyskytují mnohem méně často a práce s obrovskými zlomkovými čísly je mnohem obtížnější, takže v olympiádách se nachází pouze specifická podmnožina dlouhé zlomkové aritmetiky.

Dlouhá aritmetika v neredukovatelných zlomcích

Číslo je reprezentováno jako neredukovatelný zlomek , kde a jsou celá čísla. Pak lze všechny operace se zlomkovými čísly snadno zredukovat na operace s čitateli a jmenovateli těchto zlomků.

Obvykle je v tomto případě nutné pro uložení čitatele a jmenovatele použít také dlouhou aritmetiku, ale její nejjednodušší formou je „klasická“ dlouhá aritmetika, i když někdy stačí vestavěný 64bitový číselný typ.

Z pozice s plovoucí desetinnou čárkou se stane samostatný typ

Někdy problém vyžaduje provedení výpočtů s velmi velkými nebo velmi malými čísly, ale zároveň zabránit jejich přetečení. Je známo, že vestavěný typ bajtu umožňuje hodnoty exponentů v rozsahu , což někdy nemusí být dostatečné.

Technika je ve skutečnosti velmi jednoduchá - zavádí se další celočíselná proměnná, která je zodpovědná za exponent, a po provedení každé operace zlomkové číslo"normalizuje", tzn. se vrátí do segmentu zvýšením nebo snížením exponentu.

Při násobení nebo dělení dvou takových čísel musíte odpovídajícím způsobem přičíst nebo odečíst jejich exponenty. Při sčítání nebo odčítání by se před provedením této operace měla čísla zmenšit na jeden exponent, u kterého se jedno z nich vynásobí mocninou rozdílu exponentu.

Konečně je jasné, že není nutné volit za základ exponenciály. Na základě struktury vestavěných typů s plovoucí desetinnou čárkou se jeví jako nejvýhodnější nastavit základ rovný .

Při řešení určitého počtu problémů nastávají situace, kdy výsledky nebo mezivýpočty nelze uložit v žádném z platných typů programovacího jazyka. Poté se musíte obrátit na část programování nazvanou dlouhá aritmetika.

Pro řešení různé typy problémy dlouhé aritmetiky platí stejný princip. Každá číslice čísla je uložena v samostatném prvku pole. Pro usnadnění přidávání nové číslice jsou čísla uložena v obráceném pořadí a délka čísla může být uložena v samostatné proměnné nebo v nulovém prvku pole.

Problém se sčítáním

Algoritmus řešení

Problém se řeší stejným způsobem jako při přidávání do sloupce. Sčítání provádíme pro každou číslici zvlášť. Pokud je součet větší než 9, vezměte zbytek při dělení čísla 10 a nezapomeňte přidat počet desítek k další nejvyšší číslici

Programový kód

program A+B; var s1,s2:string; a,b:pole celého čísla; len,i,c:integer; f1,f2:text; begin Assign(f1,"INPUT.TXT"); Reset(f1); Assign(f2,"VYSTUP.TXT"); ReWrite(f2); c:=0; ReadLn(f1,s1); ReadLn(f1,s2); close(f1); len:=délka(s1); (rozdělení řetězců na prvky pole) for i:=1 to len do a:=Ord(s1[i])-48; len:=délka(s2); for i:=1 to len do b:=Ord(s2[i])-48; if délka(s1)>délka(s2) then len:=délka(s1) else len:=délka(s2); for i:=1 to len do begin c:=c+a[i]+b[i]; (proměnná c bude později použita pro přesun čísla na další řádek) a[i]:=c mod 10; (výsledek sčítání bude zapsán do pole a) c:=c div 10; konec;

jestliže c>0 pak begin len:=délka+1; a:=c; konec;

for i:=len downto 1 do (výsledek vypíše do souboru) Write(f2,a[i]); close(f2); konec.

Algoritmus řešení

Na rozdíl od sčítacího problému si nemůžeme vybrat, po jehož délce se budeme pohybovat, takže nejprve musíme zkontrolovat konečný výsledek pro jeho znaménko. To vás zbaví dalších potíží. Přímé nalezení rozdílu se neliší od nalezení rozdílu „odčítáním sloupců“.

Programový kód

program A-B; Funkce CompLong(s1,s2:řetězec):integer; (Funkce CompLong porovná dva řetězce jako velká čísla) var a,len1,len2,i:integer; b:boolean; begin a:=0; b:=true; len1:=délka(s1); len2:=délka(s2); if len1>len2 then begin a:= 1; b:=false; konec; pokud len1 Ord(s2[i])-48 pak začíná a:= 1; přestávka; konec;

pokud s1[i]

Programový kód

1) do len:=len-1; for i:=len downto 1 do (výsledek vypíše do souboru) Write(f2,a[i]); close(f2)end.

Problém násobení

program A*B; var f1,f2:text; s1:řetězec; b, i, c: celé číslo; a:array of integer; begin Assign(f1,"INPUT.TXT"); Reset(f1); Assign(f2,"VYSTUP.TXT"); ReWrite(f2); c:=0; ReadLn(f1,s1); ReadLn(f1,b); close(f1); a:=délka(s1); pro i:=1 do a:=Ord(s1[i])-48; for i:=1 do a do begin a[i]:=c+a[i]*b; c:=a[i] div 10; a[i]:=a[i] mod 10; konec;

Zatímco c>0 začněte a:=a+1; a[a]:=c mod 10; c:=c div 10; konec;

Dlouhá aritmetika if a[a]=0 then Write(f2,0) else for i:=a downto 1 do Write(f2,a[i]); close(f2); konec. Poznámka Na základě těchto programů lze vytvářet programy pro hledání faktoriálu, umocňování atd. umožňuje pracovat s velkými čísly. Je třeba také vzít v úvahu, že pro úsporu paměti lze do každé buňky pole uložit více než jednu číslici čísla.

Dlouhá aritmetika

  • - ve výpočetní technice operace s čísly, jejichž bitová hloubka přesahuje délku strojového slova daného počítače. Aritmetika velkých čísel je v podstatě sada algoritmů pro provádění základních operací (sčítání, násobení, umocňování...) s čísly, implementovaná nikoli hardwarově, ale softwarově, využívající základnější hardware pro práci s čísly nižších řádů. Speciální případ -
  • Kryptografie. Většina systémů šifrování dat, stejně jako systémy digitálního podpisu dat, používá celočíselnou aritmetiku modulo m, kde m je velmi velké kladné přirozené číslo, které nemusí být nutně prvočíslo. Přítomnost vyžadují například RSA, Rabin nebo El Gamal efektivní metody pro operace násobení a umocňování pro čísla řádu 10 309. Docela běžné programovací jazyky, jako je ISO C a JAVA, však umí pracovat pouze s čísly dané desetinné přesnosti. Konkrétně u C99 nepřesahuje maximální délka vestavěného datového typu long long číslo 10 19 . Některé další programovací jazyky, jako je Python, však mají vestavěné knihovny pro práci s velkými čísly.
  • Matematický a finanční software, který vyžaduje, aby výsledek výpočtu na počítači odpovídal do poslední číslice výsledku výpočtu na papíře. Konkrétně Windows kalkulačka (od Windows 95).
  • Čísla s pohyblivou řádovou čárkou. V počítačích jsou čísla s pohyblivou řádovou čárkou reprezentována ve tvaru n = *m*b x, kde n je samotné číslo, je znaménko čísla, m je mantisa, b je základ exponenciální funkce, x je exponent . Při zpracování vysoce přesných čísel může velikost mantisy překročit hardwarové limity. V těchto případech lze pro práci s mantisou použít algoritmy pro práci s velkými čísly.
  • Jedno z oblíbených témat sportovního programování. S rozšířením standardních knihoven pro práci s dlouhými čísly se postupně vytrácí.

Nezbytný hardware pro práci s dlouhou aritmetikou

Přísně vzato, k implementaci libovolné přesné aritmetiky vyžaduje procesor pouze nepřímé adresování; v aritmetice s pevnou přesností se dokonce obejdete i bez něj. Některé funkce procesoru však zrychlují dlouhou aritmetiku a zároveň zjednodušují jeho programování.

  • Nosit vlajku. Operace „sčítání/odečítání s přenosem“, „cyklický posun prostřednictvím přenosového bitu“.
  • Operace přístupu k paměti s automatickým přírůstkem a automatickým snižováním.

Implementace v programovacích jazycích

Jak bylo uvedeno výše, programovací jazyky mají vestavěné datové typy, jejichž velikost obecně nepřesahuje 64 bitů.
Desítková dlouhá aritmetika byla implementována v programovacím jazyce ALMIR-65 na počítači MIR-1 a v programovacím jazyce ANALYTIC na počítači MIR-2.
Pro práci s velkými čísly však existuje poměrně dost hotových optimalizovaných knihoven pro dlouhé aritmetiky.

  • LibTomMath

Algoritmy

Násobící algoritmy

Základna

Je to algoritmus založený na školní metodě „ve sloupci“. Trvá to O(N*M) čas, kde N, M jsou velikosti násobených čísel. Jeho algoritmus je podrobně popsán v knize. Oddíl 4.3.1.

Násobení Karatsuba

Tento algoritmus je také popsán v. Oddíl 4.3.3, část A. Tento algoritmus je nejjednodušší implementací myšlenky separace vstupních dat, která se stala základem pro algoritmy uvedené níže.

Toom 3-Way

Myšlenku jako první navrhl A.L. Toom in a spočívá v rozdělení vstupních dat (násobičů) na 3 stejné části (pro zjednodušení považujeme všechny části za stejné).
Podívejme se na tento algoritmus pomocí následujícího příkladu. Nechť jsou dána dvě čísla Y a X Nechť jsou definovány operace s čísly, jejichž velikost je rovna 1/3 velikosti původních čísel (viz obrázek). (předpokládáme také, že čísla zabírají stejnou paměť a jsou rozdělena přesně na 3 části, jinak obě čísla doplníme nulami na požadovanou velikost.

Poté probíhá mapování (parametrizace) těchto částí do polynomů 2. stupně.

Zaveďme podle definice takové, že hodnota polynomů je rovna vstupním číslům a . Pro bitovou reprezentaci čísel je toto číslo rovno 2 umocněné na mocninu rovnající se délce každé části v bitech.

Zavedeme také polynom:
(1).

Po určení prvků - koeficientů polynomu - budou použity k získání , protože . Velikost koeficientů je 2x větší (z paměti) než rozdělení nebo . Konečné číslo rovnající se součinu lze nalézt sečtením s posunem, jak je znázorněno na obrázku níže.

Koeficienty lze vypočítat následovně: a tak dále, ale to bude vyžadovat všech 9 násobení: pro i, j=0,1,2 a bude ekvivalentní jednoduchému násobení. Místo toho byl zvolen jiný přístup. se počítají v (1) při 5 bodech při různých .
Níže uvedená tabulka ukazuje hodnoty polynomů v rovnosti (1)





Parametr t=inf je podmíněný. Znamená to banální rovnost koeficientů pro , čímž získáme hodnotu okamžitě. Tento systém lineární vzhledem k 5 neznámým. Když se to vyřeší, dostaneme neznámé. Dále dostaneme hodnotu polynomu, jak je popsáno výše.

Toom 4-Way

Toom-algoritmus pro libovolný oddíl

Toomův algoritmus pro dělení vstupních čísel na n operandů je ekvivalentní výše popsanému. Obecně platí, že rozdělení dvou stejně dlouhých operandů na r částí vede k výpočtu hodnot při 2*r-1 bodech. Jako body pro řešení soustavy obvykle bereme 0, inf, +1, −1 a ±2^i, ±2^-i.

Násobení Fourierovou metodou

Tento násobící algoritmus se používá pro velká a velmi velká čísla. Popis tohoto algoritmu lze nalézt v různých zdrojích, zejména v sekci 4.3.3, část C, nebo v Lipsonově kapitole 9. Následuje popis algoritmu.
Tento typ algoritmu pro násobení dvou čísel v čase , kde je počet platných číslic v násobených číslech (za předpokladu, že jsou stejná), je připisován Cooletovi a Tukeymu - 1965. Existují také návrhy, že tento algoritmus byl znám již dříve, ale neměl velký význam, dokud nebyly vynalezeny první počítače. Pravděpodobní kandidáti na vynález tohoto algoritmu se také nazývají Runge a Konig - 1924, stejně jako Gauss - 1805.
Nechť existuje číslo, představme ho jako polynom, jak jsme to udělali dříve. Říkejme tomu polynom . Zavedeme také diskrétní Fourierovu transformaci polynomu jako vektor se souřadnicemi. Kde jsou definovány jako – komplexní odmocnina tého stupně 1, která se nerovná 1. Faktem je, že komplexní odmocnina z 1 je určena až do fázového faktoru, počet těchto kořenů je . Fourierova transformace se aplikuje za účelem nahrazení konvoluce koeficientů polynomů A a B součinem jejich Fourierových obrazů.

,
kde násobení F (A) * F (B) znamená skalární součin vektorů.
Předpokládejme, že n je mocnina dvou.
Hledání F(A) se provádí pomocí rekurzivní metody (rozděl a panuj). Cílem je rozdělit původní polynom na dva polynomy,


Pak .
Všimněte si, že mezi všemi čísly (0<= i < ), только различных. Поэтому, ДПФ и будут -элементными. А так как ДПФ A0 и A1 состоит из элементов, то комплексным корнем из 1 там будет корень степени .
Takže, kde 0<= k < n / 2 и
.
Použili jsme vlastnosti komplexních čísel: různé kořeny stupně n v celkovém n. .

Získáme rekurzivní algoritmus:
DFT jednoho prvku se rovná tomuto prvku
abychom našli DFT A, rozdělíme koeficienty A na sudé a liché, dostaneme dva polynomy A0 a A1, najdeme pro ně DFT, najdeme DFT A:


za 0<= k < n / 2.
Existuje důkaz následujícího tvrzení: Inverzní DFT se nalézá podobně jako dopředná DFT, kromě toho, že body symetrické kolem reálné osy jsou brány jako body, namísto těch, které se používají v dopředné DFT transformaci. Je také nutné vydělit všechny koeficienty polynomu n

Dělící algoritmy

Algoritmy umocňování

Algoritmy extrakce kořenů

Algoritmy převodu číselné soustavy

Jiné algoritmy

Literatura

  1. Donald E. Knuth, „The Art of Computer Programming“, svazek 2, „Seminumerical Algo-ritms“, 3. vydání, Addison-Wesley, 1998 The Art of Programming
  2. Dan Zuras, „On Squaring and Multiplying Large Integers“, ARITH-11: IEEE Symposium on Computer Arithmetic, 1993, str. 260 až 271.
  3. DAN SSSR 150 (1963), 496-498

Při řešení úloh se mnozí z nás museli potýkat s tím, že nám prostě nestačily typové rozměry na zdánlivě nejjednodušší operace: sčítání, odčítání a násobení. Všechny tyto operace jsou vám známé z prvních tříd. Co ale dělat, když je třeba jednu z těchto operací aplikovat na obrovská čísla, řekněme 1000 znaků nebo více... (jak jen vaše fantazie dovolí!). Tento článek pomůže vyřešit tento problém.

Zvažte každou aritmetickou operaci samostatně, nejprve napište implementační kód v C++. Nejprve musíte pochopit, že nekonečně dlouhé číslo lze pouze reprezentovat ve formě dynamického pole, což je to, co musíme udělat. Ale i když jsou čísla reprezentována jako dynamická pole, budou stále platit určitá omezení. Například délka takového čísla bude omezena velikostí paměti počítače. Měli byste také pochopit, že při použití operací sčítání a násobení bude výsledek zabírat více místa v paměti počítače než operandy.

Přidávání dlouhých čísel

Uvažujme aritmetickou operaci sčítání, používanou v dlouhé aritmetice. Algoritmus této jednoduché aritmetické operace je překvapivě jednoduchý. Vypadá to takto:

// určení délky součtového pole if (velikost_a > velikost_b) délka = velikost_a + 1; else délka = velikost_b + 1; for (int ix = 0; ix< length; ix++) { b += a; // суммируем последние разряды чисел b += (b / 10); // если есть разряд для переноса, переносим его в следующий разряд b %= 10; // если есть разряд для переноса он отсекается } if (b == 0) length--;

Hrdina příležitosti“ – tedy čísla, která budeme sčítat, jsou pravděpodobně zapsána v polích a a b. Je třeba vzít v úvahu, že se píší „zrcadlo“, to znamená, že první prvek pole odpovídá poslední číslici odpovídajícího čísla, druhý prvek předposlednímu atd. Délkové velikosti čísel jsou uloženy v proměnných size_a a size_b , ale můžete použít jakékoli jiné. Samozřejmě si myslíte: "Proč je zde příkaz výběru if, řádky 2 - 5?" nebo "K čemu je tady?" V tomto bloku kódu definujeme maximální délku čísla vyplývající ze součtu. Nejčastěji jsou totiž sečtená čísla různě dlouhá, jedno je větší, druhé menší a potřebujeme alokovat paměť tak, aby každé číslo sedělo.

Dále v algoritmu postupujeme tak, jak nás učili v hodinách matematiky: nejprve sečteme jednotlivé číslice, počínaje koncem, řádkem 9; výslednou částku vydělíme 10 a získáme celočíselnou část z dělení deseti, kterou ihned přičteme k další číslici, řádku 10. V řádku 11 odřízneme první číslici výsledného čísla, pokud samozřejmě existuje.
To je vše. Hlavní věc je nezapomenout, že číslo bude uloženo v poli b a mělo by být vydáno od konce.

Odečítání dlouhých čísel

Druhou nejčastěji používanou aritmetickou operací je odčítání. A tato část článku vám pomůže naučit se psát algoritmy pro odečítání velkých a obrovských čísel.Budeme předpokládat, že naše čísla jsou uložena v polích a a b, ma n - délky těchto čísel. Je třeba poznamenat, že čísla jsou psána „zrcadlově“ (viz výše). Samozřejmě, pokud víme, které číslo je větší, pak se úkol zjednoduší. Ale to možná nevíme. Pak bychom měli nejprve zjistit, které číslo je větší. To budeme potřebovat pro určení znaménka výsledného čísla, tedy pokud je první číslo menší než druhé, tak se v odpovědi objeví mínus. A tak začneme psát první část algoritmu, tedy určení většího čísla. Algoritmus vypadá takto:

Int k = 3; // pokud k == 3, pak jsou čísla stejně dlouhá délka = velikost_a; if (velikost_a > velikost_b) ( délka = velikost_a; k = 1; // pokud k == 1, pak první číslo je delší než druhé) else if (velikost_b > velikost_a) ( délka = velikost_b; k = 2; / / pokud k = = 2, což znamená, že druhé číslo je delší než první) else // pokud jsou čísla stejně dlouhá, pak je nutné porovnat jejich váhy pro (int ix = 0; ix< length;) // поразрядное сравнение весов чисел { if (a >b) // pokud je číslice prvního čísla větší ( k = 1; // znamená, že první číslo je delší než druhá zarážka; // opustí cyklus for) if(b > a) // pokud číslice druhé číslo je větší ( k = 2; // to znamená, že druhé číslo je delší než první přestávka; // opuštění cyklu for ) ) // konec for

Nyní vysvětlím, co bylo napsáno. Nejprve můžete vidět, že proměnná k má hodnotu 3. V této části algoritmu je proměnná k příznakem pro výsledek testu. Jsou-li čísla stejná, pak k zůstane rovno 3, pokud je první větší než druhé, pak k nabývá hodnotu 1, pokud je druhé větší než první, pak k nabývá hodnotu 2. Délka proměnná bude mít hodnotu délky většího čísla. Nyní přejdeme k odůvodnění výkonu tohoto algoritmu. Porovnání čísel probíhá ve dvou fázích. Nejprve porovnáme délky čísel: které číslo je delší, tím větší, řádky 1 - 11. Pokud jsou čísla stejně dlouhá, přejdeme k porovnávání číslic, řádky 13 - 26. Začneme porovnávat číslice v pořadí, počínaje nejvyšší, určíme tedy větší váhu čísla. To je podstata a složitost prvního dílu. Nyní přejdeme k druhé části algoritmu - odčítání. Vypadá to takto:

Int rozdíl (int *x, int *y, int *z, délka int) ( for (int ix = 0; ix< (length - 1); ix++) // проход по всем разрядам числа, начиная с последнего, не доходя до первого { if (ix < (length - 1)) // если текущий разряд чисел не первый { x--; // в следующуем разряде большего числа занимаем 1. z += 10 + x; // в ответ записываем сумму значения текущего разряда большего числа и 10-ти } else // если текущий разряд чисел - первый z += x; // в ответ суммируем значение текущего разряда большего числа z -= y; // вычитаем значение текущего разряда меньшего числа if (z / 10 >0) // pokud je hodnota v aktuálním bitu dvouciferná ( z++; // přesun o jedničku na nejvýznamnější bit z %= 10; // ořízne ji v aktuálním bitu) ) return 0; )

Pro samotné odčítání je vhodné napsat funkci, protože pak nebudeme muset psát dva algoritmy pro dva případy: když je první číslo větší než druhé, a naopak. V poli x obsahujeme větší číslo, v poli y menší číslo, v poli z výsledek. Algoritmus je poměrně jednoduchý: pro každou číslici přidáme 10, přičemž vezmeme v úvahu odečítání od nejvyšší číslice - 1. To se provádí za účelem zjednodušení odčítání číslic. Tato operace se provede pouze v případě, že dotyčná číslice není poslední v poli (první v čísle). Po odečtení číslic zkontrolujeme výsledné číslo v dané číslici v poli z. Odpověď bude zapsána do pole z a „zrcadlově“ (viz výše). Postup by se měl volat takto:

If (k == 1) rozdíl(a,b,c, délka); - pokud je první číslo větší než druhé, if (k == 2) rozdíl(b,a,c, délka); - pokud je druhé číslo větší než první.

Nyní bude odpověď uložena v poli c ve stejném „zrcadlovém“ pořadí. Tak jsme se naučili odečítat velká a obrovská čísla.

Násobení dlouhých čísel

Nyní přejdeme k poslednímu, ale neméně důležitému tématu našeho článku – práci. Tento algoritmus lze při řešení problémů najít častěji než předchozí dva. Vlastně méně demogické. Pojďme přímo k samotnému algoritmu. Předkládám vaší pozornosti algoritmus:

Délka = velikost_a + velikost_b + 1; for (int ix = 0; ix< size_a; ix++) for (int jx = 0; jx < size_b; jx++) c += a * b; for (int ix = 0; ix < length; ix++) { c += c / 10; c %= 10; } while (c == 0) length-- ;

Takto vypadá problémový algoritmus. Nyní zkusme „toto“ rozebrat, abychom přesněji pochopili, jak to funguje. Nejprve jsme měli dvě čísla v polích a a b, všechna ve stejném „zrcadlovém“ (viz výše) tvaru. Délky čísel jsou uloženy v proměnných size_a a size_b. Proměnná délka ukládá délku výsledného čísla. Bude se rovnat buď součtu délek původních čísel, nebo tomuto součtu zvýšenému o jednu. Protože ale neznáme přesnou délku výsledného čísla, vezmeme si větší délku, tedy druhou možnost. Nyní, po těchto jednoduchých výpočtech, začněme násobit čísla. Budeme je množit tak, jak nás to učili ve škole. K tomu spustíme dvě smyčky: jednu až do velikosti_a, druhou až do velikosti_b. Po těchto smyčkách můžete vidět další až do délky . díky němu při zápisu čísla do pole dostaneme v každé buňce pole jednu číslici výsledného čísla. Poslední smyčka je potřeba pro zjištění přesné délky výsledného čísla, protože námi předpokládaná délka může být větší než skutečná. Odpověď bude uložena v poli c, vše ve stejném „zrcadlovém“ tvaru.
To je celý algoritmus. Je srozumitelnější, když je implementován v programovacím jazyce, v našem případě je to C++.