Hướng dẫn giải của Số dư


Chỉ dùng lời giải này khi không có ý tưởng, và đừng copy-paste code từ lời giải này. Hãy tôn trọng người ra đề và người viết lời giải.
Nộp một lời giải chính thức trước khi tự giải là một hành động có thể bị ban.

Lời giải này đang bị ẩn cho đến khi bạn chọn mở ra.

Chúng tôi khuyên bạn nên tự thử giải bài trước. Việc mở lời giải có thể làm lộ mất ý tưởng chính trước khi bạn có cơ hội tự giải.

Bạn phải đăng nhập để mở lời giải này.

Đăng nhập

Tác giả: Hiếu Nguyễn, honghanh1155, caothao2241, thien22

Hiểu bài toán

Cho ba số nguyên dương $x, n, m$. Gọi $d$ là số chữ số của $x$ trong hệ 10. Xét số $y$ được tạo thành bằng cách lặp lại chuỗi ký tự biểu diễn thập phân của $x$ đúng $n$ lần. Yêu cầu tìm số dư của $y$ khi chia cho $m$ (tức là $y \pmod m$).\nVí dụ: $x=12, n=3, m=8$. $y = 121212$. $121212 \pmod 8 = 4$.\nRàng buộc: $1 \le x, n, m \le 10^{18}$. Do $x$ có thể rất lớn, ta không thể nối chuỗi trực tiếp. Ta cần một phương pháp tính toán số học.

Các cách tiếp cận

Cách Quy hoạch động và Chia để trị (Tổng cấp số nhân)
#include <iostream>
#include <string>

using namespace std;
using ll = long long;

// Hàm nhân hai số lớn theo modulo, sử dụng __int128 để tránh tràn số
ll mul_mod(ll a, ll b, ll mod) {
    return (__int128)a * b % mod;
}

// Hàm tính lũy thừa theo modulo: (base^exp) % mod
ll pow_mod(ll base, ll exp, ll mod) {
    ll res = 1 % mod;
    base %= mod;
    while (exp > 0) {
        if (exp & 1) res = mul_mod(res, base, mod);
        base = mul_mod(base, base, mod);
        exp >>= 1;
    }
    return res;
}

// Hàm tính tổng cấp số nhân: S = 1 + r + r^2 + ... + r^{n-1} (mod mod)
// Sử dụng chia để trị (đệ quy)
ll geom_sum(ll r, ll n, ll mod) {
    if (n == 0) return 0;
    if (n == 1) return 1 % mod;

    // Trường hợp đặc biệt: r % mod == 1 thì tổng là n * 1 = n
    if (r % mod == 1) {
        return n % mod;
    }

    if (n % 2 == 0) {
        // Nửa đầu: S(n/2)
        // Nửa sau: r^{n/2} * S(n/2)
        // Tổng: S(n) = S(n/2) * (1 + r^{n/2})
        ll half_sum = geom_sum(r, n / 2, mod);
        ll r_pow_half = pow_mod(r, n / 2, mod);
        return mul_mod(half_sum, (1 + r_pow_half) % mod, mod);
    } else {
        // N = 2k + 1
        // S(2k+1) = S(2k) + r^{2k}
        //          = S(k) * (1 + r^k) + (r^k)^2
        ll half_sum = geom_sum(r, n / 2, mod);
        ll r_pow_half = pow_mod(r, n / 2, mod);
        ll term1 = mul_mod(half_sum, (1 + r_pow_half) % mod, mod);
        ll term2 = mul_mod(r_pow_half, r_pow_half, mod);
        return (term1 + term2) % mod;
    }
}

int main() {
    ll x, n, m;
    if (cin >> x >> n >> m) {
        // Bước 1: Tính d = số chữ số của x
        string s = to_string(x);
        ll d = s.length();

        // Bước 2: Tính base = 10^d % m
        ll base = pow_mod(10, d, m);

        // Bước 3: Tính tổng cấp số nhân S = 1 + base + ... + base^{n-1}
        ll S = geom_sum(base, n, m);

        // Bước 4: Tính kết quả cuối cùng
        // y = x * (10^{d(n-1)} + ... + 10^d + 1) = x * S
        ll ans = mul_mod(x % m, S, m);

        cout << ans << endl;
    }
    return 0;
}
  • Time Complexity: O(log(n) + log(m))
  • Space Complexity: O(log(n)) (do đệ quy)

Đây là phương pháp tối ưu được sử dụng trong các solution mẫu.

  1. Biểu diễn $y$ dưới dạng: $y = x \cdot (10^{d(n-1)} + 10^{d(n-2)} + \dots + 10^d + 1)$, với $d$ là số chữ số của $x$. Gọi $R = 10^d \pmod m$.\n2. Giá trị cần tìm là $(x \pmod m) \cdot \sum_{i=0}^{n-1} R^i \pmod m$.\n3. Bài toán quy về tính tổng $S = 1 + R + R^2 + \dots + R^{n-1} \pmod m$.\n4. Do $n$ lớn, không thể lặp. Dùng phương pháp chia để trị:\n - Nếu $n$ chẵn: $S(n) = S(n/2) \cdot (1 + R^{n/2})$.\n - Nếu $n$ lẻ: $S(n) = S(n-1) + R^{n-1}$. Hoặc biến thể $S(2k+1) = S(k)(1+R^k) + (R^k)^2$.\n5. Cần các hàm phụ trợ: nhân lớn lấy modulo (dùng __int128), lũy thừa lớn lấy modulo, và đệ quy tính tổng.
Cách Phương pháp quy hoạch động dạng kẽ (Binary Lifting / DP)
// Pseudocode logic
// Giả sử có hàm mul_mod(a, b, m) và pow_mod(base, exp, m)

struct State {
    ll val; // Giá trị phần số hiện tại modulo m
    ll len; // Chiều dài phần số hiện tại modulo phi(m) (hoặc chỉ đơn giản là độ dài chu kỳ)
};

int main() {
    ll x, n, m;
    cin >> x >> n >> m;

    string s = to_string(x);
    ll d = s.length();
    ll base = pow_mod(10, d, m); // 10^d % m

    // Tính tổng geometric series S = (base^n - 1) / (base - 1)
    // Tránh chia nếu base == 1
    ll S;
    if (base == 1) {
        S = n % m;
    } else {
        // Tính (base^n - 1) * inv(base - 1) mod m
        // Nếu m không phải số nguyên tố, cần dùng Bézout hoặc cách khác.
        // Thực tế, giải pháp chia để trị an toàn hơn với mọi m.
        // Tuy nhiên, nếu m là số nguyên tố, ta có thể dùng:
        // ll inv = pow_mod(base - 1, m - 2, m);
        // ll num = (pow_mod(base, n, m) - 1 + m) % m;
        // S = mul_mod(num, inv, m);
    }

    ll ans = mul_mod(x % m, S, m);
    cout << ans << endl;
}
  • Time Complexity: O(log(n))
  • Space Complexity: O(1)

Đây là một biến thể khác của phương pháp toán học. Thay vì dùng đệ quy chia để trị, ta có thể tính tổng cấp số nhân bằng công thức kín: $$S = \frac{R^n - 1}{R - 1}$$ Sau đó thực hiện phép chia trong modulo. Tuy nhiên, phép chia trong số học modulo chỉ thực hiện được khi $m$ là số nguyên tố và $R \not\equiv 1 \pmod m$. Vì ràng buộc $m \le 10^{18}$ không giới hạn $m$ là số nguyên tố, phương pháp chia để trị (Approach 1) là lựa chọn an toàn và phổ biến nhất. Approach này đề cập đến phương pháp quy hoạch động kết hợp với kĩ thuật lưu trữ các bước lũy thừa (tương tự Binary Lifting) để tính $R^n$ nhanh chóng.

Cách Mô phỏng lặp (Brute Force)
#include <iostream>
#include <string>

using namespace std;
using ll = long long;

int main() {
    ll x, n, m;
    cin >> x >> n >> m;

    string s = to_string(x);
    ll current = 0;

    // Lặp n lần
    for (ll i = 0; i < n; i++) {
        for (char c : s) {
            current = (current * 10 + (c - '0')) % m;
        }
    }

    cout << current << endl;
    return 0;
}
  • Time Complexity: O(n * d), với d là số chữ số của x
  • Space Complexity: O(1)

Đây là cách tiếp cận trực quan nhất, mô phỏng chính xác quá trình tạo số $y$. Ta duy trì một biến current lưu giá trị số đã xây dựng được tính đến thời điểm hiện tại theo modulo $m$. Với mỗi chữ số mới được thêm vào (từ việc lặp lại $x$), ta cập nhật current = (current * 10 + digit) % m. Phương pháp này chỉ khả thi khi $n$ nhỏ. Với $n$ lên tới $10^{18}$, độ phức tạp thời gian là $O(10^{18})$, chương trình sẽ chạy rất lâu và chắc chắn bị Timeout.

Phân tích độ phức tạp

Cách tiếp cận Time Space Tên
1 O(log(n) + log(m)) O(log(n)) (do đệ quy) Quy hoạch động và Chia để trị (Tổng cấp số nhân)
2 O(log(n)) O(1) Phương pháp quy hoạch động dạng kẽ (Binary Lifting / DP)
3 O(n * d), với d là số chữ số của x O(1) Mô phỏng lặp (Brute Force)

Bài học kinh nghiệm

  • Biến đổi bài toán thành bài toán tính tổng cấp số nhân: $y = x \cdot (1 + 10^d + 10^{2d} + \dots + 10^{(n-1)d})$.
  • Phép chia để trị (Divide and Conquer) là chìa khóa để tính tổng cấp số nhân có số lượng số hạng lớn ($n \approx 10^{18}$) trong thời gian logarithmic.
  • Cần sử dụng các kiểu dữ liệu lớn (như __int128 trong C++) hoặc các kỹ thuật nhân an toàn (nhân đôi và cộng) để tránh tràn số khi tính toán với $m \le 10^{18}$.

Lỗi thường gặp

  • Tràn số khi thực hiện phép nhân (a * b) % m nếu ab đều gần bằng m. Cần dùng __int128 hoặc hàm nhân an toàn.
  • Sai lệch khi tính lũy thừa hoặc tổng do không xử lý đúng các trường hợp cơ sở của đệ quy (ví dụ: n = 0, n = 1, hoặc base = 1).
  • Giả sử $m$ là số nguyên tố để dùng phép chia nhân (modular inverse). Với $m$ bất kỳ, phương pháp chia để trị tổng cấp số nhân là bắt buộc.

Bình luận

Please read the guidelines before commenting.


Không có bình luận tại thời điểm này.