BigNum

BigNum problems involve math (usually arithmetic) involving number that cannot be stored in 64-bits, the largest commonly available data size.

A lot of programming languages offer BigNum library, such as Java (BigInteger/BigDecimal). There are several different BigNum libraries available on the net under GNU license, such as GNU MP Bignum library, but they are more practical to the development field rather than a competition setting.

This article is an attempt to demonstrate various techniques involved in BigNum computation, feasible to programming competitions. There are three main focuses of the code: Versatility, in which the same code could be applied to many different contest tasks; Speed Optimization, in which algorithms are fast enough to handle the worst case scenario of 1000 digits accuracy; and Code Optimization, in which the length of the code is kept at a reasonable level so it could be produced [bugfree] in a reasonable time frame.

A final note that the code samples uses of the C++ Standard Template Library, in an effort to take advantage of its various functionalities not present in C.

Preamble The code below uses some macros to keep the code short and tidy. Here they are:


 * 1) define pb push_back           //pushes an element into a vector
 * 2) define sz size              //size of a vector
 * 3) define fe first               //first element of a pair container
 * 4) define se second              //second element of a pair container
 * 5) define len length           //length of a string

typedef unsigned int uint;     //32-bit unsigned int typedef long long ll;          //64-bit signed long long typedef unsigned long long ull; //64-bit unsigned long long

BigInteger

Internal Structure There are two ways to interally represent BigInteger. The first way is what we generally use: a character array (or string) where each element/character represents a base 10 digit of the number. However, this turns out to be very inefficient when the numbers get large. The second way is to use a different radix (the base of the number system). Since the computer do integer computations in minimal time, it is preferable to use a very large radix so that this fact could be taken advantage of. Although in theory any radix would work, there are, in general, two reasonable choices: a power of 2, or a power of 10. The former is used because small tricks using binary numbers can be used to speed things up in most algorithms. However, there is a small trade off since almost always one would want to convert to and from the decimal system.

BigIntegers can be signed or unsigned.

typedef deque ubigint;        //unsigned BigInt typedef pair bigint;  //signed BigInt typedef pair divtype; //type returned by division

So here, a radix of 232 is chosen for BigInt. A pair container is used for the signed version of BigInt. However, for the duration of the article, we will mostly discuss algorithms for the unsigned version. The signed versions are usually trivial extensions and so it will not be discussed in detail (for examples, check out the source code in the end of the tutorial).

Finally, it is worthwhile to note that the digits are stored such that the unit digit is in the beginning of the vector.

Addition/Subtraction For addition of unsigned BigInteger, simply line up the numbers starting from the units digit, and add the corresponding digits to yield the final result. Sometimes a carry is needed. for (i=0; i < v1.sz || i < v2.sz; i++){ v = (ull) (i < v1.sz ? v1[i] : 0) + (i < v2.sz ? v2[i] : 0) + carry; carry = v >= (1ULL << 32); ret.pb((uint) v); } if (carry) ret.pb(1); This is a $$O(n)$$ algorithm.

Multiplication This implmentation is the paper-and-pen method. It is split into two parts: Part 1: Multiply by an unsigned integer Similar to the addition, we take each digit of v1 and multiply by the integer v2. Carries are taken care of as appropriate, yielding an $$O(n)$$ algorithm.

for (i = 0; i < v1.sz; i++){ v = ((ull)v1[i]) * v2 + carry; carry = v >> 32; ret.pb(v); } if (carry>0) ret.pb(carry);

Part 2: Multiply by an unsigned BigInteger This algorithm simply invokes multiplication by an integer multiple times, yielding an $$O(n^2)$$ algorithm. for (i = 0; i < v2.sz; i++){ res = mul(v1, v2[i], i); ret = add(ret, res); }

Division Divide an unsigned BigInteger by an unsigned integer This is again an $$O(n)$$ algorithm. It takes each digit (from most significant digit first) and calculates the quotient digit and remainder. The remainder gets carried to the next step. Also removes the leading 0 if applicable.

for (i = v1.sz - 1; i >= 0; i--){ v = v * (1ULL << 32) + v1[i]; ret.fe.pf(v / v2); v %= v2; } ret.se = v; String-Internal Format This occurs mostly when inputs are read. Notice that $$10^9 < 2^{32} = 4294967296$$. So to do this, we group every nine digits together and compute: $$((v_k2^{32}+v_{k-1})2^{32}+v_{k-2})2^{32}+...+v_0 $$ Using the multiplication (by integer) algorithm shown above, we achive a $$O(n^2)$$ algorithm.

i = s.length % 9; for (i > 0; i--){ v = v * 10 + s[i]; } ret.se.pb(v); s = s.substr(s.length % 9); ubigint tmp(1); //creates a BigInt with value 1 while(s.len > 0){ v = 0; for (i = 0; i < 9; i++) v = v * 10 + s[i] - '0'; s = s.substr(9); ret.se = mul(ret.se, 1e9); tmp[0] = v;	ret.se = add(ret.se, tmp); }

Internal Format-String Almost always, it is necessary to output the resulting BigInt on to the screen, in decimal format. Hence a conversion is needed. We can extract the last 9 digit by computing $$v%10^9$$, using the division (by integer) method shown above. Dividing by $$10^9$$ and repeat the process yields the following $$O(n^2)$$ algorithm.

if (compareTo(v,zero) == 0) return "0"; ubigint zero; zero.pb(0); while(compareTo(v, zero) != 0){ p = div(v, 1e9); v = p.fe; for (i = 0; i < 9; i++) { ret += p.se % 10 + '0'; p.se /= 10; } } reverse(ret.begin, ret.end);

Finally, a note of caution that there is a problem with leading zeros, but it is not so hard to take care of it.