Big numbers

From PEGWiki
Revision as of 15:13, 28 September 2010 by Brian (Talk | contribs)

Jump to: navigation, search

Big numbers, known colloquially as bignums (adjectival form bignum, as in bignum arithmetic) are integers whose range exceeds those of machine registers. For example, most modern processors possess 64-bit registers which can be used to store integers up to 264-1. It is usually possible to add, subtract, multiply, or divide such integers in a single machine instruction. However, such machines possess no native implementation of arithmetic on numbers larger than this, nor any native means of representing them. In some applications, it might be necessary to work with numbers with hundreds or even thousands of digits.

Of course, humans have no problems with working with numbers greater than 264-1, other than the fact that it becomes increasingly tedious as the numbers grow larger; we just write them out and use the same algorithms we use on smaller numbers: add column-by-column and carry, and so on. This turns out to be the key to working with bignums in computer programs too. When we write out a number in decimal representation, we are essentially expressing it as a string or array of digits and the algorithms we use to perform arithmetic on them, which entail examining the digits in a particular order, may be considered as loops.

Digital representation

As alluded to above, bignums in computer programs are represented using a radix system. This means that a bignum is stored in a base n representation, where the choice of n is based on the application. Precisely, the number N = a_0 + a_1 n + a_2 n^2 + ... + a_k n^k is stored by giving the values a_0, a_1, a_2, ..., a_k, which we probably want to store as an array. Here are a few common possibilities:

  • If we let the radix be 10:
  • ASCII representation: the number is represented by a string which literally contains the number's digits as characters: 265252859812191058636308480000000 would be represented ['2','6','5',...]
  • BCD representation: an array of digits (not their ASCII values), hence [2,6,5,...]
  • If we let the radix be 109, we can store nine digits in each array entry. For example, 30! could be stored [265252,859812191,058636308,480000000]. Note that we must take care to group digits starting from the ones' place and moving left, instead of starting at the most significant digit and moving right, to avoid complicating the code and eroding performance. This is because adding and subtracting bignums requires that they be aligned at the decimal point (not at the most significant digit), so it is convenient when we guarantee that this is never found within an entry, otherwise shifting would be necessary.
  • If we let the radix be 232, we use a sequence of 32-bit unsigned integer typed variables to store the bignum. We know 26525285981219105863630848000000010 = D13F6370F96865DF5DD5400000016. Again, we align at the ones' place, giving [00000D13,F6370F96,865DF5DD,54000000]. (Note that even though the hex representation is given here, internally each element of the array should be stored simply as an integer type.)

Fixed versus dynamic bignums

There are, in principle, two kinds of bignum implementation. Suppose we know in advance the maximum size of the integers we might be working with. For example, in TREE1@SPOJ, we are asked to report the number of permutations which satisfy a certain property. There are only up to 30 elements, so we know that the answer will not exceed 30! = 265252859812191058636308480000000, which has 33 digits. It is not terribly difficult to implement the solution in such a way that no intermediate variable is ever larger than this. So we could, for example, use a string of length 33 to store all integers used in the computation of the answer (where numbers with fewer than 33 digits are padded with zeroes on the left), and treat all numbers as though they had 33 digits. This is probably the easiest type of bignum to implement.

On the other hand, sometimes it is not so easy to determine in advance the size of the numbers we might be working with, or a problem might have bundled test cases and a strict time limit, forcing the programmer to make the small cases run more quickly than the large ones. When this occurs it is a better idea to use dynamic bignums, which can expand or shrink according to their length. Dynamic bignums are trickier to code than fixed ones. Because extensible array data structures such as C++'s std::vector often support efficient insertion at the back but unacceptable linear-time performance on insertion at the front, it is advisable to store dynamic bignums in little-endian format or backward (see next section)