Arbitrary-Precision Arithmetic
TL;DR: Template at the end...
Definition
Arbitrary-Precision Arithmetic, also known as bignum arithmetic, uses certain algorithmic structures to support operations on integers larger than those supported by the language's built-in integer types (numbers exceeding the language's native integer range).
Introduction
Arbitrary-precision arithmetic involves many small details and requires careful implementation.
So today, let's implement a simple calculator together.
Task
Input: An expression of the form a <op> b.
aandbare non-negative decimal integers with length not exceeding \(1000\);<op>is a character (+,-,*, or/) representing the operation.- There is a single space between the integer and the operator.
Output: The result of the operation.
- For
+,-, and*operations, output one line representing the result; - For the
/operation, output two lines representing the quotient and remainder, respectively. - It is guaranteed that all results are non-negative integers.
Storage
In typical implementations, arbitrary-precision numbers are represented as strings, where each character represents a decimal digit. Therefore, arbitrary-precision numerical computation can be seen as a special kind of string processing.
When reading a string, the most significant digit is at the beginning of the string (the position with a smaller index). However, by convention, the position with the smallest index stores the least significant digit of the number, meaning the string is stored in reverse. The reason for this is that the length of the number may change, but we want digits of the same place value to remain aligned (e.g., all ones digits at index [0], all tens digits at index [1], etc.). Additionally, addition, subtraction, and multiplication operations generally start from the ones digit (recall elementary school column arithmetic), which provides ample justification for "reverse storage".
We will adhere to this convention hereafter. Define a constant LEN = 1004 to represent the maximum length accommodated by the program.
Thus, it's straightforward to write the code for reading an arbitrary-precision number:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
Output is also in reverse order of storage. Since we don't want to output leading zeros, we start from the most significant digit and search downward for the first non-zero digit, then output from there. The termination condition is i >= 1 instead of i >= 0 because we still want to output a single character 0 when the entire number equals \(0\).
1 2 3 4 5 6 7 | |
Putting it together, we have a complete repeater program.
copycat.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | |
Arithmetic Operations
The difficulty of the four basic operations varies. The simplest are arbitrary-precision addition and subtraction, followed by arbitrary-precision–single-precision (ordinary int) multiplication and arbitrary-precision–arbitrary-precision multiplication, with arbitrary-precision–arbitrary-precision division being the most complex.
We will implement all required functions in this order.
Addition
Arbitrary-precision addition is essentially column addition.
That is, starting from the least significant digit, add the digits at corresponding positions of the two addends and determine whether the sum reaches or exceeds \(10\). If it does, handle the carry: increment the result at the next higher position by \(1\), and reduce the current digit's result by \(10\).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
Combining this with the previous part yields an addition calculator.
adder.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | |
Subtraction
Arbitrary-precision subtraction is essentially column subtraction.
Subtract digit by digit starting from the ones place, borrowing \(1\) from the next higher digit when a negative result occurs. The overall approach is exactly the same as addition.
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Replacing add() with sub() in the previous program yields a subtraction calculator.
subtractor.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | |
Try it: input 1 2—output /9999999, huh, why is this OI Wiki giving me fake code...
Actually, the above code only handles the case where the minuend \(a\) is greater than or equal to the subtrahend \(b\). Handling the case where the minuend is smaller than the subtrahend, i.e., \(a<b\), is simple.
\(a-b=-(b-a)\)
To compute \(b-a\), since \(b>a\), we can call the sub function from the above code as sub(b,a,c). To obtain \(a-b\), simply prepend a negative sign to the result.
Multiplication
Arbitrary-Precision–Single-Precision
Arbitrary-precision multiplication, that is, column... wait a minute!
First, consider a simple case: one of the multipliers is an ordinary int. Is there a simple way to handle this?
An intuitive approach is to directly multiply each digit of \(a\) by \(b\). Numerically, this method is correct, but it does not conform to decimal representation, so it needs to be reorganized into a normal form.
The reorganization method also processes carries digit by digit from the least significant digit upward. However, the carry here can be very large, even much larger than \(9\), because each digit after multiplication can reach the magnitude of \(9b\). Therefore, the carry can no longer be simply handled by a \(-10\) operation; instead, it must be computed using the quotient and remainder of division by \(10\). See the code comments for details, or refer to the diagram below illustrating the process of multiplying the arbitrary-precision number \(1337\) by the single-precision number \(42\).

Of course, for this reason, this method requires special attention to the range of the multiplier \(b\). If it is on the same order of magnitude as \(10^9\) (or the upper bound of the corresponding integer type), then arbitrary-precision–single-precision multiplication should be used with caution.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
Arbitrary-Precision–Arbitrary-Precision
If both multipliers are arbitrary-precision, then column multiplication can shine again.
Recall each step of column multiplication: it actually computes the sum of several terms \(a \times b_i \times 10^i\). For example, to compute \(1337 \times 42\), we compute \(1337 \times 2 \times 10^0 + 1337 \times 4 \times 10^1\).
Thus, we can decompose \(b\) into all its digits, each of which is a single-precision number, multiply them separately by \(a\), shift them left to their respective positions, and then add them to get the answer. Of course, carries must also be handled uniformly at the end, as in the previous example.

Note that this process is not exactly the same as column multiplication. Our algorithm does not carry during each multiplication step; instead, it retains all results in their corresponding positions and processes carries uniformly at the end, but this does not affect the result.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Division
One implementation of arbitrary-precision division is long division.
Long division can actually be seen as a process of successive subtractions. For example, the calculation of the tens digit of the quotient in the diagram above can be understood as: after subtracting \(12\) from \(45\) three times, the result becomes less than \(12\) and cannot be subtracted further, so this digit is \(3\).
To reduce redundant computations, we first obtain the length \(l_a\) of the dividend and the length \(l_b\) of the divisor, and start computing the quotient from index \(l_a - l_b\), from the most significant digit to the least significant digit. This aligns the highest digit of the first multiplication with the highest digit of the dividend, just like in manual calculation.
The reference implementation provides a function greater_eq() to determine whether the dividend, with its lowest digit at index last_dg, can subtract the divisor while remaining non-negative. Then, for each digit of the quotient, it repeatedly calls greater_eq(), and when it holds, uses arbitrary-precision subtraction to subtract the divisor from the remainder, simulating the long division process.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | |
Introductory Section Complete!
Combining the implementations of the four arithmetic operations described above completes the calculator program mentioned at the beginning.
calculator.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | |
Digit-Packing Arbitrary-Precision Arithmetic
Introduction
In general arbitrary-precision addition, subtraction, and multiplication, we split the numbers involved into individual digits for computation.
For example, when computing \(8192\times 42\), if we follow the arbitrary-precision–arbitrary-precision multiplication method, we are actually computing \((8000+100+90+2)\times(40+2)\).
When the number of digits is large, the number of split digits also increases, and the efficiency of arbitrary-precision operations decreases.
Is there a way to optimize this?
Note that the method of splitting digits does not affect the final result, so we can merge several digits together.
Process
Using the same example, if we split every two digits into one number, we can split it into \((8100+92)\times 42\).
Such splitting does not affect the final result, but because the number of split digits is reduced, computational efficiency improves.
Understanding this process from the perspective of numeral systems, we perform operations in a larger base (splitting every two digits above can be considered as operating in base \(100\)) to reduce the number of digits involved in operations, thereby improving efficiency.
This is the idea behind digit-packing arbitrary-precision arithmetic.
Below we provide a reference implementation of digit-packing arbitrary-precision addition to further illustrate its implementation:
Reference Implementation of Digit-Packing Arbitrary-Precision Addition
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Efficient Long Division with Digit-Packing
When using digit-packing, if trial division is still performed using the method described earlier, the number of trial divisions will be very high, resulting in a large constant factor. For example, in base 10,000, each digit requires an average of 5000 trial divisions, which is an unacceptable constant. Therefore, we need a more efficient trial division method.
We can use double as an intermediary. Suppose the dividend has 4 digits: \(a_4,a_3,a_2,a_1\), and the divisor has 3 digits: \(b_3,b_2,b_1\). Then we only need to trial one digit of the quotient: using base \(base\), estimate the quotient with the expression \(\dfrac{a_4 base + a_3}{b_3 + b_2 base^{-1} + (b_1+1)base^{-2}}\). For multiple digits, it's the single-digit method plus a loop. Since the divisor uses 3-digit precision to estimate the quotient, it ensures that the estimated quotient \(q'\) and the actual quotient \(q\) satisfy \(q-1 \le q' \le q\), so each digit requires at most two trial divisions in the worst case. However, this requires that \(base^3\) fits within the effective precision of double, i.e., \(base^3 < 2^{53}\), so when using this method, it is recommended not to exceed base 32768; otherwise, errors due to insufficient precision can easily lead to mistakes.
Additionally, since the estimated quotient is always less than or equal to the actual quotient, there is room for further optimization. In most cases, each digit is estimated only once. When estimating the next digit, the estimated quotient might be greater than or equal to the base due to errors from the previous digit, but that's fine as long as we perform a unified carry at the end. For example, suppose the base is 10, and we compute \(395081/9876\). The trial division steps are as follows:
- First trial division yields \(3950/988=3\), so \(395081-(9876 \times 3 \times 10^1) = 98801\). An error occurs here, but we ignore it and continue.
- Continue trial division on the remainder 98801: \(9880/988=10\), so \(98801-(9876 \times 10 \times 10^0) = 41\), which is the final remainder.
- Sum the results of the trial division process and handle carries: \(3 \times 10^1 + 10 \times 10^0 = 40\), which is the exact quotient.
Although the method seems simple, implementation is tricky. Therefore, we provide a reference implementation that has been verified multiple times to be correct. Important details are also noted in the comments.
Reference Implementation of Efficient Long Division with Digit-Packing
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | |
Karatsuba Multiplication
Let the number of digits of an arbitrary-precision number be \(n\). Then arbitrary-precision–arbitrary-precision column multiplication takes \(O(n^2)\) time. This section introduces a more time-efficient algorithm proposed by the former Soviet (Russian) mathematician Anatoly Karatsuba, which is a divide-and-conquer algorithm.
Consider two decimal integers \(x\) and \(y\), each containing \(n\) digits (leading zeros allowed). Choose any \(0 < m < n\), and let
where \(x_0, y_0, z_0, z_1 < 10^m\). Then
Observe that
so to compute \(z_1\), we only need to compute \((x_1 + x_0) \cdot (y_1 + y_0)\) and then subtract \(z_0\) and \(z_2\).
The above equation is actually the core of Karatsuba's algorithm. It transforms a multiplication problem of length \(n\) into three smaller subproblems. If we let \(m = \left\lceil \dfrac n 2 \right\rceil\), and denote the time taken by Karatsuba's algorithm to multiply two \(n\)-digit integers as \(T(n)\), then \(T(n) = 3 \cdot T \left(\left\lceil \dfrac n 2 \right\rceil\right) + O(n)\). By the master theorem, \(T(n) = \Theta(n^{\log_2 3}) \approx \Theta(n^{1.585})\).
The entire process can be implemented recursively. For clarity, the code below implements polynomial multiplication via Karatsuba's algorithm and finally handles all carry issues.
karatsuba_mulc.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | |
About new and delete
See Memory Pool.
However, such an implementation has a problem: in base \(b\), each coefficient of the polynomial can reach the magnitude of \(n \cdot b^2\), which may cause integer overflow in digit-packing arbitrary-precision implementations. If carries are handled during polynomial multiplication, then \(x_1 + x_0\) and \(y_1 + y_0\) can reach \(2 \cdot b^m\), adding an extra digit (if using \(x_1 - x_0\), negative numbers must be handled specially). Therefore, the implementation method should be chosen based on the actual application scenario.
Efficient Large Integer Multiplication Based on Polynomials
If the data scale reaches \(10^{10^5}\) or larger, ordinary arbitrary-precision multiplication may time out. This section introduces a method to optimize such multiplication using polynomials.
For an \(n\)-digit decimal integer \(a\), it can be viewed as a polynomial \(A=a_{0} 10^0+a_{1} 10^1+\cdots+a_{n-1} 10^{n-1}\) where each coefficient is an integer not exceeding \(10\). Thus, we transform integer multiplication into polynomial multiplication.
Ordinary polynomial multiplication still takes \(O(n^2)\) time, but it can be optimized using algorithms from the polynomial section, such as Fast Fourier Transform (FFT) and Number Theoretic Transform (NTT). The optimized time complexity is \(O(n\log n)\).
Encapsulated Classes
Here is an encapsulated arbitrary-precision integer class, and here is an ultra-minimal implementation class supporting dynamic length and four arithmetic operations.
Here is another template
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | |
Exercises
- NOIP 2012 King's Game
- SPOJ - Fast Multiplication
- SPOJ - GCD2
- UVa - Division
- UVa - Fibonacci Freeze
- Codeforces - Notepad
References and Links
本页面最近更新:,更新历史
发现错误?想一起完善? 在 GitHub 上编辑此页!
本页面贡献者:OI-wiki
本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用