Khoá học C++ cơ bản
Bài học: Xử Lý Số Nguyên Lớn
🏆 +100 XP tiềm năng Thoát

Xử Lý Số Nguyên Lớn

2 khối nội dung 45 phút học tập MỞ BẢNG TRẮNG

Xử Lý Số Nguyên Lớn

Nội dung bài học

1. Bản chất vấn đề & trực giác thuật toán (the core problem & intuition)

Trong ngôn ngữ lập trình C++, kiểu dữ liệu số nguyên có kích thước lớn nhất được hỗ trợ phần cứng là unsigned long long (64-bit, tối đa xấp xỉ $1.84 \times 10^{19}$) hoặc phần mở rộng GCC __int128 (128-bit, tối đa xấp xỉ $3.4 \times 10^{38}$).

Tuy nhiên, trong các bài toán thực tế và đề thi học sinh giỏi (như tính $100!$, tính số Fibonacci thứ $1000$, hoặc tính $2^{10000}$ mà không lấy dư modulo), kết quả có thể dài hàng nghìn đến hàng chục nghìn chữ số. Vì C++ không có sẵn kiểu dữ liệu BigInteger như Python hay Java, lập trình viên thi đấu C++ bắt buộc phải tự mô phỏng các phép tính số học đặt tính rồi tính như toán tiểu học trên mảng ký tự (string) hoặc mảng số nguyên (vector<int>).

Big integer hay modular arithmetic: Chọn vũ khí nào?

Phân định lựa chọn giải thuật: Modulo vs Big Integer

Đề bài yêu cầu Quy mô kết quả Vũ khí tối ưu Kỹ thuật cốt lõi
Tính $A^B \pmod M$ ($B \le 10^{18}$) $\le M$ Modulo Lũy thừa nhị phân $\mathcal{O}(\log B)$
Tính $\frac{A}{B} \pmod M$ $\le M$ Modulo Nghịch đảo Modulo $A \times B^{-1}$
Tính $F_{10^6} \pmod M$ $\le M$ Modulo Nhân ma trận nhị phân $\mathcal{O}(\log N)$
Tính chính xác $2^{10000}$ $\approx 3011$ chữ số Big Integer Lũy thừa nhị phân trên BigInt
Tính chính xác $1000!$ $2568$ chữ số Big Integer Nhân BigInt $\times$ int liên tiếp
Tính chính xác số Fibonacci $F_{1000}$ $209$ chữ số Big Integer Cộng BigInt + BigInt quy hoạch động
Số có $10^5$ chữ số nhưng chỉ cần $\% M$ $\le M$ Modulo Vòng lặp Horner: cur = (cur * 10 + d) % M

2. Mô phỏng từng bước (visual step-by-step simulation)

Ví dụ 1: Mô phỏng phép cộng số lớn $A = 9876$ và $B = 543$

  • Quy tắc: Đảo ngược chuỗi để chữ số hàng đơn vị nằm ở chỉ số 0.
  • $A' = [6, 7, 8, 9]$, $B' = [3, 4, 5]$.
Vị trí hàng ($i$) Cặp chữ số $(A'_i, B'_i)$ Biến nhớ vào Phép tính tổng Ghi nhận & Nhớ mới
0 (Hàng đơn vị) $(6, 3)$ $0$ $6 + 3 + 0 = 9$ Ghi $9$, nhớ $0$
1 (Hàng chục) $(7, 4)$ $0$ $7 + 4 + 0 = 11$ Ghi $1$, nhớ $1$
2 (Hàng trăm) $(8, 5)$ $1$ $8 + 5 + 1 = 14$ Ghi $4$, nhớ $1$
3 (Hàng nghìn) $(9, 0)$ $1$ $9 + 0 + 1 = 10$ Ghi $0$, nhớ $1$
Dư cuối $1$ $\text{carry} = 1$ Ghi $1$, nhớ $0$
  • Kết quả đảo ngược: $[9, 1, 4, 0, 1] \implies \mathbf{10419}$.

Ví dụ 2: Mô phỏng phép nhân số lớn $A = 48$ với số nhỏ $b = 7$

  • $A' = [8, 4]$.
  • Bước 0 ($i = 0$): $8 \times 7 + 0 = 56 \implies$ Ghi $6$, carry $= 5$.
  • Bước 1 ($i = 1$): $4 \times 7 + 5 = 33 \implies$ Ghi $3$, carry $= 3$.
  • Dư cuối: Ghi carry $= 3$.
  • Kết quả đảo ngược: $[6, 3, 3] \implies \mathbf{336}$.

3. Lý thuyết cốt lõi & bất biến thuật toán (core invariants)

3.1. Mô hình biểu diễn số lớn & little-endian

  • Biểu diễn Little-Endian: Lưu các chữ số theo thứ tự từ hàng thấp đến hàng cao (chữ số hàng đơn vị nằm ở chỉ số 0).
  • Ưu điểm cốt lõi: Hàng đơn vị nằm ở index = 0, nên khi cộng, trừ hoặc nhân ta có thể xử lý trực tiếp từ hàng thấp lên hàng cao và truyền biến nhớ carry/borrow sang phần tử kế tiếp ($a[0] \to a[1] \to a[2] \dots$). Ngoài ra, chữ số mới ở cuối có thể được thêm bằng push_back() với chi phí amortized $\mathcal{O}(1)$.
  • Biểu diễn Base 10 vs Base $10^9$:
  • Base 10 (string / vector<int>): Mỗi phần tử lưu 1 chữ số thập phân ($0 \dots 9$).
  • Base $10^9$ (vector<int> / vector<long long>): Nhóm các cụm 9 chữ số từ phải sang trái.
  • Cấu trúc dữ liệu: Mỗi chunk lưu kiểu int ($0 \dots 999,999,999$); phép nhân giữa 2 chunks lưu kiểu long long (vì $(10^9 - 1) \times (10^9 - 1) \approx 10^{18} < 2^{63}-1$).
  • Ví dụ: Số $1234567890123456789$ được tách thành: $$\text{chunks} = [23456789, 123456789, 1]$$ $$\text{Giá trị} = 23456789 + 123456789 \times 10^9 + 1 \times (10^9)^2$$

3.2. Bảng tổng hợp các phép toán số nguyên lớn ($\mathcal{O}(L^2)$)

Phép toán Bản chất thuật toán Độ phức tạp thời gian Lưu ý quan trọng
So sánh ($A, B$) So sánh độ dài trước, sau đó so sánh từ điển $\mathcal{O}(\max(L_A, L_B))$ Xóa sạch số 0 ở đầu trước khi so sánh
Cộng ($A + B$) Mô phỏng cộng từng hàng kèm biến nhớ carry $\mathcal{O}(\max(L_A, L_B))$ Xử lý carry còn dư sau khi hết chữ số
Trừ ($A - B$) Mô phỏng trừ có mượn borrow ($A \ge B$) $\mathcal{O}(L_A)$ Xóa sạch số $0$ vô nghĩa ở đầu (leading zeros)
Nhân nhỏ ($A \times b$) Nhân từng chữ số của $A$ với số nguyên $b$ $\mathcal{O}(L_A)$ Biến carry có thể vượt quá $10$, cần kiểu long long
Nhân lớn ($A \times B$) Tích lũy $C[i + j] += A[i] \times B[j]$ rồi normalize $\mathcal{O}(L_A \times L_B)$ Khởi tạo mảng $L_A + L_B$ (áp dụng cho $L \le 5000$)
Chia nhỏ ($A / b, A \% b$) Chia từ hàng cao nhất xuống hàng đơn vị $\mathcal{O}(L_A)$ Biến tích lũy cur = cur * 10 + A[i]

3.3. Thuật toán chia số lớn cho số nhỏ & bất biến horner

Khi chia số lớn $A$ cho số nguyên $b$ ($1 \le b \le 10^9$), ta duyệt từ chữ số hàng cao nhất xuống hàng đơn vị:

string divSmall(string a, long long b) {
    string res = "";
    long long cur = 0;
    for (char c : a) {
        cur = cur * 10 + (c - '0');
        int digit = cur / b;
        res.push_back(char('0' + digit));
        cur %= b; // cur luôn là số dư hiện tại
    }
    // Xóa số 0 vô nghĩa ở đầu
    int pos = 0;
    while (pos + 1 < (int)res.size() && res[pos] == '0') pos++;
    return res.substr(pos);
}

Ghi chú:

BẤT BIẾN TOÁN HỌC CỦA PHÉP CHIA TỪNG BƯỚC:

Vì trước mỗi bước lặp ta luôn duy trì số dư $0 \le cur < b$, nên sau khi nhận thêm một chữ số mới $cur = cur \times 10 + \text{digit}$, giá trị luôn thỏa mãn $cur < 10b$. Do đó thương tại mỗi bước digit = cur / b chắc chắn luôn nằm trong khoảng $[0, 9]$ (là một chữ số thập phân hợp lệ duy nhất).

3.4. Tối ưu hóa base $10^9$ (chunking optimization)

  • Thay vì thực hiện phép nhân trên từng chữ số đơn lẻ (Base 10 có $L$ chữ số), ta nén số lớn thành $\frac{L}{9}$ chunks trong Base $10^9$.
  • Đánh giá hiệu năng: Số lượng cặp chunk cần nhân giảm xấp xỉ $\left(\frac{L}{9}\right) \times \left(\frac{L}{9}\right) = \frac{L^2}{81}$ (giảm khoảng 81 lần về số lượng phép nhân chunk). Tốc độ thực tế tăng vọt từ hàng chục lần giúp vượt qua các bài toán $N \le 10^5$.

4. Các bẫy lỗi lập trình kinh điển (bug traps)

  1. Quên xóa số 0 vô nghĩa ở đầu (Leading Zeros): * Sau phép trừ (ví dụ $1000 - 999 = 0001$), nếu không xóa số 0 thì chuỗi sẽ in ra 0001. * Cách xử lý: while (res.size() > 1 && res.back() == '0') res.pop_back();.

  2. Không xét trường hợp số $0$: * Phép nhân $A \times 0$ phải trả về "0", không được trả về rỗng "".

  3. Biến carry trong phép nhân số nhỏ có thể rất lớn: * Trong phép nhân $A \times b$ với $b = 10^9$, carry sau mỗi bước có thể lên tới $10^9$, do đó kiểu dữ liệu của carry bắt buộc phải là long long.

5. Mẫu cài đặt chuẩn thi đấu (competitive templates)

# include <bits/stdc++.h>
using namespace std;

// Hàm xóa số 0 vô nghĩa ở đầu chuỗi đảo ngược
void removeLeadingZeros(string &s) {
    while (s.size() > 1 && s.back() == '0') {

        s.pop_back();
    }
}

// Phép cộng 2 số nguyên lớn không âm (A + B)
string addBig(string a, string b) {
    reverse(a.begin(), a.end());
    reverse(b.begin(), b.end());

    string res = "";
    int carry = 0;
    int n = max(a.size(), b.size());

    for (int i = 0; i < n || carry; ++i) {
        int sum = carry;
        if (i < (int)a.size()) sum += a[i] - '0';
        if (i < (int)b.size()) sum += b[i] - '0';
        res.push_back((sum % 10) + '0');
        carry = sum / 10;
    }

    reverse(res.begin(), res.end());
    return res;
}

// Phép trừ 2 số nguyên lớn không âm (A - B với A >= B)
string subBig(string a, string b) {
    reverse(a.begin(), a.end());
    reverse(b.begin(), b.end());

    string res = "";
    int borrow = 0;

    for (int i = 0; i < (int)a.size(); ++i) {
        int diff = (a[i] - '0') - borrow;
        if (i < (int)b.size()) diff -= (b[i] - '0');
        if (diff < 0) {
            diff += 10;
            borrow = 1;
        } else {
            borrow = 0;
        }
        res.push_back(diff + '0');
    }

    removeLeadingZeros(res);
    reverse(res.begin(), res.end());
    return res;
}

// Phép nhân 2 số nguyên lớn chuẩn mực và an toàn (A * B)
string mulBig(string a, string b) {
    if (a == "0" || b == "0") return "0";

    reverse(a.begin(), a.end());
    reverse(b.begin(), b.end());

    int n = a.size(), m = b.size();
    vector<int> c(n + m, 0);

    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < m; ++j) {
            c[i + j] += (a[i] - '0') * (b[j] - '0');
        }
    }

    // Normalize: Đẩy biến nhớ carry sang các ô kế tiếp
    for (int i = 0; i + 1 < n + m; ++i) {
        c[i + 1] += c[i] / 10;
        c[i] %= 10;
    }

    while (c.size() > 1 && c.back() == 0) {

        c.pop_back();
    }

    string res = "";
    for (int i = (int)c.size() - 1; i >= 0; --i) {
        res.push_back(c[i] + '0');
    }

    return res;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    string a, b;
    if (!(cin >> a >> b)) return 0;

    cout << "A + B = " << addBig(a, b) << "\n";
    cout << "A * B = " << mulBig(a, b) << "\n";
    return 0;
}
1
A
B
C
D
2
A
B
C
D
3
A
B
C
D
4
A
B
C
D
5
A
B
C
D
6
A
B
C
D
7
A
B
C
D
8
A
B
C
D
9
A
B
C
D
10
A
B
C
D

Bài Tập Thực Hành & Rèn Luyện

16 Bài tập thực hành

Nhấn vào nút "Làm bài" bên dưới để mở giao diện làm bài trực tuyến (Online Judge) và thực hiện viết mã nguồn cho bài tập này.

So Sánh Hai Số Nguyên Lớn
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Cộng Hai Số Nguyên Lớn
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Trừ Hai Số Nguyên Lớn (A >= B)
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Trừ Hai Số Lớn Tổng Quát (Có Thể Âm)
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Nhân Số Lớn Với Số Nhỏ
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Nhân Hai Số Nguyên Lớn
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Chia Số Lớn Cho Số Nhỏ (Lấy Thương)
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Chia Lấy Dư Số Lớn Cho Số Nhỏ
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Tính Giai Thừa Số Lớn (N!)
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Lũy Thừa Số Lớn Chính Xác (A^B)
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Số Fibonacci Lớn Thứ N
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Tổng Các Chữ Số Của N!
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Chia Hai Số Nguyên Lớn (A / B)
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Căn Bậc Hai Số Nguyên Lớn
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Ước Chung Lớn Nhất Số Lớn
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Số Lớn Cực Hạn: Tổ Hợp C(N, K) Chính Xác
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài