Xử lý số lớn trong C++
1. Khi nào cần xử lý số lớn?
Bài toán 1
Cho hai số nguyên dương \(A\) và \(B\) (A và B có không quá \(1000\) chữ số)
Yêu cầu: Tính \(A + B, A \; – \; B, A * B\)
Các bạn có thể làm thử bài tập này và chấm điểm lời giải của bạn tại đây
Bài toán 2
Cho số nguyên dương \(N (N \le 1000)\). Hãy tính \(N!\)
Đặt vấn đề
Nếu không quan tâm tới phạm vi giá trị của các biến trong 2 bài tập trên. Thì đây là những bài tập cực kỳ đơn giản. Tuy nhiên, nếu bạn code Pascal/C++ sẽ gặp phải hiện tượng lỗi tràn số do kiểu dữ liệu có size lớn nhất là long long cũng chỉ lưu được một số nguyên có \(18-19\) chữ số
Xử lý số lớn sẽ giúp các bạn làm những bài tập đơn giản này
Ý tưởng
Tính toán phép cộng, trừ, nhân, chia theo phong cách của học sinh tiểu học

Tổ chức dữ liệu
Coi mỗi một số nguyên là 1 vector. Mỗi chữ số của số nguyên đó là một phần tử.
Ví dụ: ta có số nguyên A = 123456789
- Số nguyên \(A\) khi biểu diễn dưới dạng vector: \(A = [9, 8, 7, 6, 5, 4, 3, 2, 1]\).
- Ở đây, ta biểu diễn số A theo trình tự đảo ngược là do các phép cộng, trừ, nhân ta đều cộng các số tương ứng với hàng đơn vị trước rồi mới dần dần tính đến hàng chục, hàng nghìn. Hơn nữa, khi cộng, trừ, nhân hai số với nhau có thể độ dài của kết quả sẽ dài hơn độ dài của hai số ban đầu, việc chèn thêm các số vào phần đầu của kết quả sẽ mất thời gian hơn (phép insert trong vector có độ phức tạp \(O(N)\)), nếu lưu theo thứ tự đảo ngược thì có nghĩa là ta chỉ cần thêm các số vào cuối (độ phức tạp \(O(1)\)).
Trong C++ ta có thể định nghĩa một kiểu dữ liệu dùng cho số lớn như sau
typedef vector<long long> bignum;
2. Phép cộng
\(A = [9, 8, 7, 6, 5, 4, 3, 2, 1]\)
\(B = [1, 7, 4, 3, 1, 8, 9]\)
Ý tưởng
Gọi \(C\) là kết quả, số lượng chữ số ở vector kết quả sẽ có ít nhất là \(k = max(A.size(), B.size())\)
Cộng lần lượt từ vị trí \(0\) đến vị trí \(k-1\), khi cộng đến vị trí \(i\) ta có:
Gọi \(mem\) là giá trị của phần nhớ tại bước cộng thứ \(i-1\), khởi tạo \(= 0\)
Tổng tại vị trí \(i\): \(sum = A[i] + B[i] + mem\). Giá trị của \(C[i] = sum \; \% \; 10\)
Giá trị của mem: \(mem = sum \; / \; 10\)
Test thử với hai số A,B bên trên:

Code phép cộng hai số lớn C++
#include<bits/stdc++.h> using namespace std; #define ll long long typedef vector<ll> bignum; bignum add(bignum a, bignum b) { bignum c; c.resize(max(a.size(), b.size())); //Resize để thêm 0 vào cuối cho những số có độ dài khác nhau a.resize(c.size()); b.resize(c.size()); ll mem = 0; for (int i = 0; i < c.size(); ++i){ ll sum = mem + a[i] + b[i]; c[i] = sum%10; mem = sum/10; } if(mem > 0) c.push_back(mem); return c; } bignum toBignum(string a){ bignum c; c.clear(); for(int i = a.size()-1; i >=0; i--) c.push_back(a[i] - 48); return c; } string toString(bignum a){ string s = ""; for(int i = a.size()-1; i >=0; i--) s += char(a[i]+48); return s; } int main() { string a,b; cin>>a>>b; bignum A = toBignum(a); bignum B = toBignum(b); bignum C = add(A,B); cout<<toString(C); }
3. Phép trừ
Giả thiết: \(A \geq B\).
\(A = [9, 8, 7, 6, 5, 4, 3, 2, 1]\)
\(B = [1, 7, 4, 3, 1, 8, 9]\)
Ý tưởng
- Gọi \(C\) là kết quả, số lượng chữ số ở vector kết quả sẽ có nhiều nhất là \(k = max(A.size(), B.size())\)
- Trừ lần lượt từ vị trí \(0\) đến vị trí \(k-1\), khi trừ đến vị trí \(i\) ta có:
- Gọi mem là giá trị của phần nhớ tại bước trừ thứ \(i-1\), khởi tạo \(= 0\)
- Hiệu tại vị trí \(i\): \(subtract = A[i] – B[i] – mem\)
- Nếu \(subtract < 0\) thì ta sẽ phải vay thêm \(10\): \(substract += 10\), \(mem = 1\); ngược lại thì \(mem = 0\).
- Giá trị của \(C[i] = subtract\)
Test thử với hai số A,B bên trên

Code phép trừ hai số lớn C++
bignum subtract(bignum a, bignum b) { bignum c; c.resize(max(a.size(), b.size())); //Resize để thêm 0 vào cuối cho những số có độ dài khác nhau a.resize(c.size()); b.resize(c.size()); ll mem = 0; for (int i = 0; i < c.size(); ++i){ ll sub = a[i] - b[i] - mem; if(sub < 0){ mem = 1; sub += 10; }else mem = 0; c[i] = sub; } //loại bỏ các số 0 ở đầu while(c.size()>1 && c[c.size()-1] == 0) c.pop_back(); return c; }
4. Phép nhân
\(A = [9, 8, 7, 6]\)
\(B = 19\)
Ý tưởng
- Gọi \(C\) là kết quả, số lượng chữ số ở vector kết quả sẽ có nhiều nhất là \(k = max(A.size(), B.size())\)
- Nhân lần lượt từ vị trí \(0\) đến vị trí \(k-1\), khi nhân đến vị trí \(i\) ta tiến hành cộng luôn các số để lấy kết quả:
- Gọi mem là giá trị của phần nhớ tại bước trừ thứ \(i-1\), khởi tạo \(= 0\)
- Tích tại vị trí \(i\): \(mul = A[i] * B[i] + mem\). Giá trị của \(C[i] = mul \; \% \; 10\)
- Giá trị của mem: \(mem = mul \; / \; 10\).
- Lưu ý: Vì giá trị của \(C[i] < 10\) nên giá trị của \(mul\) và \(mem\) luôn luôn nằm trong khoảng giá trị của kiểu dữ liệu long long
Test thử với hai số A,B bên trên
Ta thấy giá trị của nhớ lúc nhân xong tất cả các chữ số là 12, vậy chỉ cần thêm 12 vào cuối của vector kết quả là ta sẽ được tích của A và B
Giả thiết Nhân số lớn với một số nhỏ(kiểu long long)

Để nhân số lớn với số lớn, ta làm tương tự. Nhân số A với từng chữ số của B và cộng lại
Code phép nhân hai số lớn C++
bignum multiple(bignum a, ll b) { bignum c; c.resize(a.size()); ll mem = 0; for (int i = 0; i < c.size(); ++i){ ll mul = mem + a[i]*b; c[i] = mul%10; mem = mul/10; } while(mem>0){ c.push_back(mem%10); mem/=10; } return c; } bignum multipleBig(bignum a, bignum b) { bignum c,cnt; c.clear(); c.push_back(0); cnt.clear(); ll mem = 0; for (int i = 0; i < b.size(); ++i){ //nhân từng chữ số của b với a bignum tmp = multiple(a,b[i]); //Thêm 0 vào đầu tmp.insert(tmp.begin(),cnt.begin(), cnt.end()); //cộng dồn vào kết quả c = add(c, tmp); cnt.push_back(0); } return c; }
5. Phép chia
Giả thiết: Chia số lớn với một số nhỏ (kiểu long long)
\(A = [9, 8, 7, 6]\)
\(B = 19\)
Ý tưởng
- Gọi \(C\) là kết quả, số lượng chữ số ở vector kết quả sẽ có nhiều nhất là \(k = max(A.size(), B.size())\)
- Chia lần lượt từ vị trí \(k-1\) đến vị trí \(0\), ta có
- Gọi mem là giá trị của phần dư tại bước nhân trước, khởi tạo \(= 0\)
- Giá trị số chia tại vị trí \(i\): \(div = mem*10+A[i]\)
- Giá trị của \(C[i] = div/b\)
- Giá trị của \(mem\): \(mem = div % b\)
- Lưu ý: Vì giá trị của \(C[i] < 10\) nên giá trị của \(div\) và \(mem\) luôn luôn nằm trong khoảng giá trị của kiểu dữ liệu long long
Code mẫu phép chia số lớn với số nhỏ C++
// chia lấy phần nguyên bignum division(bignum a, ll b) { bignum c; c.resize(a.size()); ll mem = 0; for (int i = c.size()-1; i >=0; i--){ ll div = mem*10 + a[i]; c[i] = div/b; mem = div%b; } //loại bỏ các số 0 ở đầu while(c.size()>1 && c[c.size()-1] == 0) c.pop_back(); return c; } //chia lấy phần dư ll mod(bignum a, ll b) { ll mem = 0; for (int i = a.size()-1; i >=0; i--){ ll div = mem*10 + a[i]; mem = div%b; } return mem; }
6. Đánh giá độ phức tạp
Với \(M,N\) là độ dài của các số lớn
| Kỹ thuật | Độ phức tạp |
|---|---|
| Cộng hai số lớn | \(O(M+N)\) |
| Trừ hai số lớn | \(O(M+N)\) |
| Nhân số lớn với số nhỏ | \(O(M)\) |
| Nhân hai số lớn | \(O(M * N)\) |
| Chia lấy phần nguyên/dư số lớn với số nhỏ | \(O(M)\) |
Code mẫu toàn bộ các hàm trên tham khảo ở đây
Bạn có thể tham khảo để làm bài tập xử lý số lớn cơ bản ở đây
7. Cải tiến kỹ thuật xử lý số lớn
Ý tưởng
Thay vì mỗi phần tử của vector lưu một chữ số của số cần tính, thì mình lưu X số cho mỗi phần tử (với \(1 \le X \le 18\))
Giả thiết
Với \(X = 4\)
\(A = 123456789\) được biểu diễn thành: \(A = [6789, 2345, 1]\)
\(B = 9813471\) được biểu diễn thành: \(B = [3471, 981]\)
Cách giải
- Gọi \(C\) là kết quả, số lượng chữ số ở vector kết quả sẽ có nhiều nhất là \(k = max(A.size(), B.size())\)
- Cộng lần lượt từ vị trí \(0\) đến vị trí \(k-1\), khi cộng đến vị trí \(i\) ta có:
- Gọi \(mem\) là giá trị của phần nhớ tại bước cộng thứ \(i-1\), khởi tạo \(= 0\)
- Tổng tại vị trí \(i\): \(sum = A[i] + B[i] + mem\) Giá trị của \(C[i] = sum \; \% \; 10^4 \; (là \; 10^X)\)
- Giá trị của \(mem\): \(mem = sum / 10^4\)
- Lưu ý lúc chuyển từ bignum sang string: Nếu số lượng chữ số ở phần tử đó không đủ \(X\), phải thêm \(0\) vào đầu cho đủ (xem ví dụ dưới)
Test thử với hai số A,B bên trên

Code mẫu cộng hai số lớn cải tiến
bignum add(bignum a, bignum b) { bignum c; c.resize(max(a.size(), b.size())); a.resize(c.size()); b.resize(c.size()); ll mem = 0; for (int i = 0; i < c.size(); ++i){ ll sum = mem + a[i] + b[i]; c[i] = sum%base; mem = sum/base; } if(mem > 0) c.push_back(mem); return c; }
Bạn có thể tham khảo toàn bộ code xử lý số lớn cải tiến tại đây
Kết luận
- Vậy thời gian thực hiện phép xử lý số lớn ở kỹ thuật này sẽ nhanh gấp \(X\) lần so với kỹ thuật trước.
- Với phép cộng, trừ hai số lớn: có thể đặt \(X = 17\), do mình vẫn có thể cộng trừ hai số có \(17\) chữ số với nhau trong kiểu long long
- Với phép nhân, chia hai số lớn: có thể đặt \(X = 9\), do mình vẫn có thể nhân hai số có \(9\) chữ số với nhau (nhân lên sẽ ra kết quả khoảng \(10^{18}\)) trong kiểu long long
8. Luyện tập
Đây là một số bài tập luyện tập trong sách Competitive Programming Basic của Code Dream, đăng ký mua ngay để được luyện tập nhiều dạng bài về thuật toán khác, chấm bài online tại http://oj.codedream.edu.vn







