私はCを習得しようとしていて、本当に大きな数字(つまり、100桁、1000桁など)を処理できないことに遭遇しました。これを行うライブラリが存在することは承知していますが、自分で実装したいと考えています。
私は、誰かが任意精度の算術の非常に詳細で馬鹿げた説明を持っているか提供できるかどうか知りたいだけです。
回答:
数値を小さな部分として扱うには、適切なストレージとアルゴリズムの問題です。int
が0から99までしか使用できないコンパイラーがあり、999999までの数値を処理したいとします(単純にするために、ここでは正の数値のみを考慮します)。
そのためには、各数値に3を付けint
、小学校で学んだ(本来持っているはずの)同じルールを使用して、加算、減算、その他の基本的な演算を行います。
任意精度のライブラリでは、メモリが保持できるものだけで、数値を表すために使用される基本型の数に固定の制限はありません。
追加例123456 + 78
::
12 34 56
78
-- -- --
12 35 34
最下位から作業する:
これは実際、CPU内のビットレベルで加算が一般的に機能する方法です。
減算は似ています(キャリーの代わりに基本タイプの減算と借用を使用)、乗算は加算の繰り返し(非常に遅い)またはクロス積(高速)で実行でき、除算はより複雑ですが、数値のシフトと減算によって実行できます関与している(子供の頃に学んだであろう長い分裂)。
私は実際に、二乗したときに整数にフィットできる最大の10のべき乗を使用してこの種のことを行うライブラリを作成しました(2つint
のを乗算するときのオーバーフローを防ぐために、16ビットint
が0から99に制限されているなど)二乗したときに9,801(<32,768)int
を生成するか、0〜9,999を使用して32ビットで99,980,001(<2,147,483,648)を生成し、アルゴリズムを大幅に簡略化します。
注意すべきいくつかのトリック。
1 /数値を加算または乗算するときは、必要な最大スペースを事前に割り当て、それが多すぎる場合は後で減らします。たとえば、2つの100桁(数字はint
)を追加しても、101桁を超えることはありません。12桁の数値に3桁の数値を掛けても、15桁を超えることはありません(桁数を追加します)。
2 /速度を上げるために、絶対に必要な場合にのみ、数値を正規化(必要なストレージを削減)してください。私のライブラリではこれを個別の呼び出しとして使用しているため、ユーザーは速度とストレージの問題を選択できます。
3 /正数と負数の加算は減算であり、負数の減算は、同等の正数を加算することと同じです。符号を調整した後、addメソッドとsubtractメソッドで互いに呼び出して、コードをかなり節約できます。
4 /常に次のような数値になるので、小さい数値から大きい数値を減算することは避けてください。
10
11-
-- -- -- --
99 99 99 99 (and you still have a borrow).
代わりに、11から10を引き、それを否定します。
11
10-
--
1 (then negate to get -1).
これは、私がこれをしなければならなかったライブラリの1つからのコメント(テキストに変換)です。残念ながら、コード自体は著作権で保護されていますが、4つの基本的な操作を処理するのに十分な情報を取得できる場合があります。以下では、-a
および-b
は負の数を表し、a
およびb
はゼロまたは正の数であると仮定します。
さらに、符号が異なる場合は、否定の使用減算:
-a + b becomes b - a
a + -b becomes a - b
以下のために減算、符号が異なる場合は、否定の使用追加:
a - -b becomes a + b
-a - b becomes -(a + b)
また、大きな数から小さな数を差し引くことを確実にするための特別な処理:
small - big becomes -(big - small)
乗算は、次のようにエントリレベルの計算を使用します。
475(a) x 32(b) = 475 x (30 + 2)
= 475 x 30 + 475 x 2
= 4750 x 3 + 475 x 2
= 4750 + 4750 + 4750 + 475 + 475
これを実現する方法には、32の各桁を一度に1つずつ(逆方向に)抽出し、次にaddを使用して結果(最初はゼロ)に追加する値を計算します。
ShiftLeft
また、ShiftRight
演算を使用して、a LongInt
をラップ値(「実際の」数学の場合は10)ですばやく乗算または除算します。上記の例では、475をゼロに2回(32の最後の桁)を加算して950を取得しています(結果= 0 + 950 = 950)。
次に、シフト475を左に移動して4750を取得し、右シフト32を取得して3を取得します。4750をゼロに3回追加して14250を取得し、950の結果に追加して15200を取得します。
左シフト4750は47500を取得し、右シフト3は0を取得します。右シフト32は現在ゼロであるため、終了し、実際には475 x 32は15200になります。
除算もトリッキーですが、初期の算術に基づいています(「ガジンタ」メソッドの「入り」を意味します)。の次の長い除算を検討してください12345 / 27
。
457
+-------
27 | 12345 27 is larger than 1 or 12 so we first use 123.
108 27 goes into 123 4 times, 4 x 27 = 108, 123 - 108 = 15.
---
154 Bring down 4.
135 27 goes into 154 5 times, 5 x 27 = 135, 154 - 135 = 19.
---
195 Bring down 5.
189 27 goes into 195 7 times, 7 x 27 = 189, 195 - 189 = 6.
---
6 Nothing more to bring down, so stop.
したがって、12345 / 27
ある457
残り6
。確認:
457 x 27 + 6
= 12339 + 6
= 12345
これは、ドローダウン変数(最初はゼロ)を使用して、12345のセグメントを27以上になるまで一度に1つずつ下げることで実装されます。
次に、27未満になるまで27を単純に減算します。減算の数は、一番上の行に追加されたセグメントです。
停止するセグメントがなくなると、結果が得られます。
これらはかなり基本的なアルゴリズムであることを覚えておいてください。数値が特に大きくなる場合、複雑な算術を実行するより良い方法があります。あなたは、GNU Multiple Precision Arithmetic Libraryのようなものを調べることができます-これは、私の独自のライブラリよりも実質的に優れており、高速です。
それはメモリが不足すると単純に終了するという点でかなり不幸な機能を備えています(私の意見では、汎用ライブラリにとってかなり致命的な欠陥です)が、それを見渡すことができれば、それはかなりうまくいきます。
ライセンスの理由で(または、アプリケーションが明らかな理由で終了しないようにするため)使用できない場合は、少なくとも、独自のコードに統合するためのアルゴリズムをそこから取得できます。
私はまた、MPIR(GMPの分岐)でのやりがいは、潜在的な変更についての議論の影響を受けやすいことも発見しました-彼らはより開発者にやさしい束のようです。
ホイールを再発明することは、個人の啓蒙と学習に非常に適していますが、それも非常に大きな作業です。私はあなたをその重要な演習と私自身が行った演説として思いとどまらせたくはありませんが、大きなパッケージが対処する作業には微妙で複雑な問題があることに注意してください。
たとえば、乗算。単純に、「スクールボーイ」メソッドを考えるかもしれません。つまり、1つの数値を他の数値の上に書き、学校で学んだように長い乗算を行います。例:
123
x 34
-----
492
+ 3690
---------
4182
ただし、この方法は非常に低速です(O(n ^ 2)、nは桁数)。代わりに、最近のbignumパッケージは、離散フーリエ変換または数値変換のいずれかを使用して、これを本質的にO(n ln(n))演算に変換します。
そして、これは整数のためだけです。数値のある種の実数表現(log、sqrt、expなど)でより複雑な関数に入ると、状況はさらに複雑になります。
理論的な背景が必要な場合は、Yapの本の「アルゴリズム代数の基本的な問題」の最初の章を読むことを強くお勧めします。すでに述べたように、gmp bignumライブラリーは優れたライブラリーです。実数のために、私はmpfrを使用し、それを好んだ。
車輪を作り直さないでください。四角になる場合があります。
試行およびテストされた、GNU MPなどのサードパーティライブラリを使用します。
abort()
に割り当ての失敗を要求します。割り当ての失敗は、特定の非常に大規模な計算で発生する可能性があります。これはライブラリにとって許容できない動作であり、独自の任意精度のコードを作成するのに十分な理由があります。
鉛筆と紙で行うのと基本的に同じ方法で行います...
malloc
realloc
、必要に応じおよびれます通常、計算の基本単位として使用します
アーキテクチャに応じて。
2進数または10進数のベースの選択は、最大のスペース効率、人間の可読性、およびチップ上での2進化10進数(BCD)数学サポートの有無の要望に依存します。
あなたは高校の数学でそれを行うことができます。実際にはより高度なアルゴリズムが使用されています。たとえば、1024バイトの数値を2つ追加するには、次のようにします。
unsigned char first[1024], second[1024], result[1025];
unsigned char carry = 0;
unsigned int sum = 0;
for(size_t i = 0; i < 1024; i++)
{
sum = first[i] + second[i] + carry;
carry = sum - 255;
}
one place
最大値を処理するために追加する場合は、結果を大きくする必要があります。これを見てください:
9
+
9
----
18
TTMathは、学習したい場合に最適なライブラリです。C ++を使用して構築されています。上記の例はばかげたものですが、これは一般的に加算と減算がどのように行われるかです!
この主題についての適切な参照は、数学演算の計算の複雑さです。これは、実装する各操作に必要な容量を示します。たとえば、2つのN-digit
数値がある場合2N digits
、乗算の結果を保存する必要があります。
ミッチは言った、はるかに実装するための簡単な作業ではありません!C ++を知っている場合は、TTMathをご覧になることをお勧めします。
究極のリファレンスの1つ(IMHO)は、KnuthのTAOCP Volume IIです。数値を表すための多くのアルゴリズムとこれらの表現に対する算術演算について説明しています。
@Book{Knuth:taocp:2,
author = {Knuth, Donald E.},
title = {The Art of Computer Programming},
volume = {2: Seminumerical Algorithms, second edition},
year = {1981},
publisher = {\Range{Addison}{Wesley}},
isbn = {0-201-03822-6},
}
あなたが大きな整数コードを自分で書きたいと仮定すると、これは驚くほど簡単で、最近それを行った人として話されます(MATLABでですが)。
個々の10進数を2進数として保存しました。これにより、多くの操作、特に出力が簡単になります。必要以上に多くのストレージを必要としますが、ここではメモリが安価であり、ベクトルのペアを効率的にたたみ込みできれば、乗算が非常に効率的になります。または、いくつかの10進数をdoubleに格納できますが、乗算を行うためのたたみ込みにより、非常に大きな数値で数値の問題が発生する可能性があることに注意してください。
符号ビットを個別に保管します。
2つの数値の加算は、主に数字を加算することであり、次に各ステップでキャリーをチェックします。
1組の数値の乗算は、少なくともタップで高速たたみ込みコードを使用している場合は、たたみ込みとそれに続くキャリーステップとして実行するのが最適です。
数値を個々の10進数の文字列として格納する場合でも、除算(mod / rem opsも)を実行して、結果として一度に約13の10進数を得ることができます。これは、一度に1桁の10進数のみを処理する除算よりもはるかに効率的です。
整数の整数乗を計算するには、指数のバイナリ表現を計算します。次に、必要に応じて、二乗演算を繰り返してパワーを計算します。
多くの操作(因数分解、素数性テストなど)は、powermod操作の恩恵を受けます。つまり、mod(a ^ p、N)を計算するときは、pがバイナリ形式で表現されている指数の各ステップで結果mod Nを減らします。最初にa ^ pを計算せず、次にmod Nを削減しようとします。
PHPで行った簡単な(素朴な)例を次に示します。
「加算」と「乗算」を実装し、それを指数の例に使用しました。
http://adevsoft.com/simple-php-arbitrary-precision-integer-big-num-example/
コードスニップ
// Add two big integers
function ba($a, $b)
{
if( $a === "0" ) return $b;
else if( $b === "0") return $a;
$aa = str_split(strrev(strlen($a)>1?ltrim($a,"0"):$a), 9);
$bb = str_split(strrev(strlen($b)>1?ltrim($b,"0"):$b), 9);
$rr = Array();
$maxC = max(Array(count($aa), count($bb)));
$aa = array_pad(array_map("strrev", $aa),$maxC+1,"0");
$bb = array_pad(array_map("strrev", $bb),$maxC+1,"0");
for( $i=0; $i<=$maxC; $i++ )
{
$t = str_pad((string) ($aa[$i] + $bb[$i]), 9, "0", STR_PAD_LEFT);
if( strlen($t) > 9 )
{
$aa[$i+1] = ba($aa[$i+1], substr($t,0,1));
$t = substr($t, 1);
}
array_unshift($rr, $t);
}
return implode($rr);
}