Hướng dẫn giải của Bài toán đếm 2


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, Minhsang1, asenen_kiet, nguyenbaanhkiet

Hiểu bài toán

Bài toán yêu cầu đếm số cặp số nguyên không âm (j, i) sao cho 0 <= j <= i < n và tổ hợp chập i của j (Ci^j) chia hết cho một số nguyên tố p. N (số lượng số nguyên) có thể lên tới 3 triệu, và p là một số nguyên tố nhỏ hơn 100. Việc tính trực tiếp Ci^j để kiểm tra tính chia hết là bất khả thi do độ lớn của số hạng tử và độ phức tạp. Ta cần một phương pháp dựa trên tính chất chia hết của số tổ hợp.

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

Cách Đếm loại trừ (Dựa trên Lucas Theorem)
#include <bits/stdc++.h>
using namespace std;
using int64 = long long;

// Hàm tính số cặp (j, i) sao cho C(i, j) KHÔNG chia hết cho p
// Dựa trên Lucas Theorem: C(i, j) chia hết cho p nếu và chỉ nếu
// trong ít nhất một cặp số hạng tử cơ số p (i_k, j_k), ta có j_k > i_k.
// Vậy, C(i, j) KHÔNG chia hết cho p khi j_k <= i_k với mọi k.
// Số cách chọn j_k <= i_k cho mỗi số hạng tử i_k là (i_k + 1).
// Do đó, tổng số cặp không chia hết là tổng của (i_k + 1) với mọi i_k.
int64 count_not_divisible(int n, int p) {
    int64 total = 0;
    for (int i = 0; i < n; ++i) {
        int64 count = 1;
        int temp = i;
        while (temp > 0) {
            count *= (temp % p + 1);
            temp /= p;
        }
        total += count;
    }
    return total;
}

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

    int n, p;
    cin >> n >> p;

    // Tổng số cặp (j, i) thỏa mãn 0 <= j <= i < n là tổng của (i + 1) từ i = 0 đến n-1
    // Cong thuc tong: n*(n+1)/2
    int64 total_pairs = 1LL * n * (n + 1) / 2;

    // Số cặp chia hết = Tổng số cặp - Số cặp không chia hết
    int64 divisible = total_pairs - count_not_divisible(n, p);

    cout << divisible << "\n";
    return 0;
}
  • Time Complexity: O(N * log_p(N))
  • Space Complexity: O(1)

Phương pháp này dựa trên Lucas Theorem, phát biểu rằng C(i, j) chia hết cho p khi và chỉ khi có ít nhất một chữ số cơ số p của j lớn hơn tương ứng của i. Ngược lại, C(i, j) không chia hết khi tất cả các chữ số cơ số p của j đều nhỏ hơn hoặc bằng tương ứng của i. Với một số i cố định, số lượng j thỏa mãn điều kiện này chính là tích của các (chữ số của i) + 1. Ví dụ, i = 12, p = 5, các chữ số là (2, 2). Số cách chọn j là (2+1)*(2+1) = 9. Ta duyệt qua tất cả i từ 0 đến n-1, tính số lượng j tương ứng và cộng dồn. Đây là số cặp KHÔNG chia hết. Lấy tổng số cặp ban đầu trừ đi số này ta được kết quả.

Cách Quy hoạch động trên chuỗi cơ số (Digit DP)
#include <bits/stdc++.h>
using namespace std;

using ll = long long;

int p;
vector<int> digits; // Biểu diễn n dưới cơ số p

// dp[pos][less_i][less_j]
// pos: vị trí chữ số đang xét (từ trái sang phải)
// less_i: true nếu prefix của i đã nhỏ hơn prefix của n
// less_j: true nếu prefix của j đã nhỏ hơn prefix của n
// Giá trị trả về: số cặp (j, i) thỏa mãn điều kiện j <= i và C(i, j) không chia hết cho p
ll dp[30][2][2];

ll dfs(int pos, bool less_i, bool less_j) {
    if (pos == (int)digits.size()) return 1; // Tìm thấy một cặp (j, i) hợp lệ

    if (dp[pos][less_i][less_j] != -1) return dp[pos][less_i][less_j];

    ll res = 0;
    int limit_i = less_i ? p - 1 : digits[pos];
    int limit_j = less_j ? p - 1 : digits[pos];

    for (int i_d = 0; i_d <= limit_i; i_d++) {
        for (int j_d = 0; j_d <= limit_j; j_d++) {
            // Điều kiện Lucas Theorem: j_d <= i_d
            if (j_d <= i_d) {
                bool new_less_i = less_i || (i_d < limit_i);
                bool new_less_j = less_j || (j_d < limit_j);
                res += dfs(pos + 1, new_less_i, new_less_j);
            }
        }
    }
    return dp[pos][less_i][less_j] = res;
}

ll solve(ll n, int p_val) {
    if (n == 0) return 0;
    p = p_val;
    digits.clear();
    // Chuyển n thành chuỗi cơ số p
    while (n > 0) {
        digits.push_back(n % p);
        n /= p;
    }
    reverse(digits.begin(), digits.end());

    memset(dp, -1, sizeof(dp));
    // Kết quả là tổng số cặp (j, i) <= n thỏa mãn Lucas
    return dfs(0, false, false);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, p;
    cin >> n >> p;

    ll total_pairs = 1LL * n * (n + 1) / 2;
    ll not_divisible = solve(n - 1, p); // Tính cho n-1

    cout << total_pairs - not_divisible << endl;
    return 0;
}
  • Time Complexity: O(log_p(N))
  • Space Complexity: O(log_p(N))

Đây là phương pháp tối ưu nhất, sử dụng kỹ thuật quy hoạch động trên chuỗi cơ số (Digit DP). Thay vì duyệt qua từng số i, ta duyệt qua các cấu trúc cơ số của i và j. Trạng thái DP bao gồm vị trí chữ số đang xét và thông tin xem các số này đã nhỏ hơn n hay chưa. Do điều kiện Lucas (jk <= ik) độc lập giữa các chữ số, ta có thể đếm số cách chọn các cặp chữ số thỏa mãn tại mỗi bước. Độ phức tạp chỉ phụ thuộc vào số lượng chữ số của n trong cơ số p (tối đa ~20 chữ số với p=2), giúp giải quyết bài toán cực kỳ nhanh.

Cách Brute Force (Thử tất cả)
// Pseudo code
long long count = 0;
for (int i = 0; i < n; i++) {
    for (int j = 0; j <= i; j++) {
        if (C(i, j) % p == 0) count++;
    }
}
// Phải dùng BigInteger hoặc Lucas để check C(i, j) % p == 0
  • Time Complexity: O(N^2 * log_p(N))
  • Space Complexity: O(1)

Phương pháp này duyệt qua tất cả các cặp (j, i) thỏa mãn 0 <= j <= i < n. Với mỗi cặp, nó tính giá trị C(i, j) và kiểm tra xem có chia hết cho p hay không. Do N có thể lên tới 3 triệu, số cặp (j, i) xấp xỉ 4.5 * 10^12, quá lớn để duyệt trong thời gian cho phép.

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

Cách tiếp cận Time Space Tên
1 O(N * log_p(N)) O(1) Đếm loại trừ (Dựa trên Lucas Theorem)
2 O(log_p(N)) O(log_p(N)) Quy hoạch động trên chuỗi cơ số (Digit DP)
3 O(N^2 * log_p(N)) O(1) Brute Force (Thử tất cả)

Bài học kinh nghiệm

  • Lucas Theorem: C(i, j) chia hết cho p (số nguyên tố) khi và chỉ khi có ít nhất một chữ số cơ số p của j lớn hơn tương ứng của i.
  • Bài toán quy về đếm số cặp (j, i) sao cho jk <= ik với mọi chữ số cơ số p. Số lượng j cho trước i là tích của (i_k + 1).
  • Digit DP cho phép đếm số cặp (j, i) <= N có đặc điểm cơ số nào đó mà không cần duyệt qua từng số, bằng cách duyệt qua các vị trí chữ số.

Lỗi thường gặp

  • Sử dụng công thức tính tổ hợp trực tiếp (ví dụ qua Pascal triangle)会导致 overflow và TLE do độ phức tạp quá cao.
  • Quên rằng p là số nguyên tố, dẫn đến không áp dụng được Lucas Theorem.
  • Lỗi off-by-one: N là số lượng số nguyên (từ 0), nhưng Digit DP thường làm việc với giá trị số (n-1).

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.