紙と鉛筆での掛け算
僕の愛用するVisual C++で扱える最大の整数はunsigned long long:符号なし64bit整数です。1bitつまり2進での1桁は10進では約0.3桁に相当するので、unsigned long longで表現できる最大の整数は10進で(64*0.3=)19桁、9桁どうしの掛け算まではオーバーフローせずに求めることができます。……さて、それ以上に大きな数千/数万桁の整数の掛け算を正しく行うにはどうしましょうね、と。
整数の掛け算を紙と鉛筆で行う"筆算"は小学校で習いました。計算機向けにちょっとアレンジしておさらいします。
4 5 6
× 7 8
-------------------------
32 40 48 (400 + 50 + 6)× 8
+ 28 35 42 (400 + 50 + 6)×70
-------------------------
28 67 82 48
3 5 5 6 8 繰り上がり
456×78を筆算で計算するには、まずそれぞれの数を桁ごとにバラし、総当たりで掛け合わせたのち各桁の和を求めます。出てきたコタエは 28 67 82 48、各桁が10未満となるよう、1の位から順に繰り上げを行うと 3 5 5 6 8、これが掛け算の結果です。
小学校の先生は僕たちにすばらしい知恵を授けてくれました。紙と鉛筆(そして時間と根気)さえあればどんなに大きな整数の掛け算だろうが、このやり方で答えが求まります。
筆算による掛け算を実装してみましょう、入力となる整数と返り値(結果)は10進表記での文字列:std::stringとします。
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
string onpaper_multiplies(const string& sa, const string& sb) {
int digits = (int)(sa.size() + sb.size());
vector<int> a(sa.size(),0);
vector<int> b(sb.size(),0);
vector<int> c(digits,0);
// 下位の桁から順にセット
auto char2int = [](char ch) { return ch - '0';};
transform(sa.rbegin(), sa.rend(), a.begin(), char2int);
transform(sb.rbegin(), sb.rend(), b.begin(), char2int);
// a,b各桁の全組み合わせで掛け算を行う
for ( size_t i = 0; i < a.size(); ++i ) {
for ( size_t j = 0; j < b.size(); ++j ) {
c[i+j] += a[i] * b[j];
}
}
// 桁あふれの繰り上げ
int carry = 0;
transform(c.begin(), c.end(), c.begin(),
[&](int n) { int t = n + carry; carry = t /10; return t % 10; });
// 文字列化(ゼロ・サプレスを行う)
string result;
result.reserve(digits);
bool zero = false;
for_each(c.rbegin(), c.rend(),
[&](int ch) { if ( ch != 0 && !zero ) { zero = true; }
if ( zero ) { result.push_back(ch+'0'); } });
return result;
}
#ifdef TRIAL
#include <iostream>
using namespace std;
int main() {
string sa = "456";
string sb = "78";
string sc = onpaper_multiplies(sa, sb);
cout << sa << " * " << sb << " = " << sc << endl;
}
#endif
正しい結果が得られたみたいだけど、念のためそこそこ大きな数を食わせて確認しておきます。.NET Framework: System.Numerics.BigInteger の助けを借りて2つの整数とその積を生成し、onpaper_multiplies での結果とを照合します。
#include <string>
#include <random>
#include <algorithm>
#include <msclr/marshal_cppstd.h>
using namespace System;
using namespace System::Numerics;
using namespace std;
using namespace msclr::interop;
int main(array<System::String ^> ^args) {
if ( args->Length < 2 ) {
Console::WriteLine(L"make_random_multiplies <digits> <rows>");
return 1;
}
int digits = Int32::Parse(args[0]); // digits桁の2整数とその積を
int rows = Int32::Parse(args[1]); // rows組出力する
mt19937 gen;
uniform_int_distribution<> dist(0,9);
auto random_digit = [&]() -> wchar_t { return dist(gen) + L'0'; };
wstring sa(digits, L'0');
wstring sb(digits, L'0');
for ( int i = 0; i < rows; ++i ) {
generate_n(begin(sa), digits, random_digit);
generate_n(begin(sb), digits, random_digit);
BigInteger a = BigInteger::Parse(marshal_as<String^>(sa));
BigInteger b = BigInteger::Parse(marshal_as<String^>(sb));
BigInteger c = BigInteger::Multiply(a, b);
Console::WriteLine(L"{0} {1} {2}", a.ToString(), b.ToString(), c.ToString());
}
return 0;
}
#include <iostream>
#include <string>
using namespace std;
// 所要時間の計測を行うよう、少し修正した
string onpaper_multiplies(const string& sa, const string& sb, float& duration);
int main() {
string sa, sb, expected, actual;
float elapsed;
float duration = 0.0f;
int times = 0;
while ( cin >> sa >> sb >> expected ) {
actual = onpaper_multiplies(sa, sb, elapsed);
duration += elapsed;
if ( expected != actual ) {
cerr << L"oops!" << endl;
return 1;
}
++times;
}
cerr << "ok. takes " << duration << "[ms] for " << times << " multiplies." << endl;
return 0;
}
1000桁の掛け算を一万回やって同じ結果が得られてますから、間違っちゃいないみたいです。
筆算アルゴリズムで書かれたonpaper_multipliesには1つ大きな問題があります、それは処理時間。
この計算法のキモとなるのは各桁総当たりの掛け算部分。見てのとおり二重のfor-loopですから、N桁どうしの掛け算に必要な時間はNの二乗に比例します。時間計算量:O(N^2) てことです。
Nが小さいうちはどってことないのですが、大きくなるにつれてその遅さに我慢できなくなります。マルチスレッド化したところで高々数倍、多少マシにはなりますが O(N^2)には歯が立ちません。
