Tuyển sinh 🎓 Mình nhận đào tạo lập trình giải thuật thi đấu: thi vào lớp 10 chuyên tin · thi học sinh giỏi tin học cấp tỉnh/thành · thi vào đội tuyển quốc gia · Olympic tin học sinh viên / ICPC · phỏng vấn thuật toán. Bấm vào đây để xem thông tin khóa học → Xem khóa học
Thi thử miễn phí
🏆 Thi thử HSG Tin học tuần này trên ClueOJ
Mở công khai cho mọi người · đề do thầy Nguyễn Việt Nam Sơn biên soạn
Lịch thi, đề cũ & lời giải →

Guồng nước (Đề tự luyện HSG Tin quốc gia – Bài 1) – Đếm cách chọn K điểm trên vòng tròn với khoảng cách trong [A, B]: chia kẹo Euler, bao hàm – loại trừ, đếm kép

Nguồn: Đề tự luyện HSG Tin học quốc gia – Bài 1 (đề tự soạn)Độ khó: KháTổ hợpChia kẹo EulerBao hàm – loại trừĐếm képNghịch đảo modulo

Đề bài

Guồng nước

Vành guồng nước có $N$ khe cách đều, đánh số $0, 1, \ldots, N - 1$ theo chiều kim đồng hồ (sau khe $N - 1$ lại là khe $0$). Thầy Sơn gắn $K$ ống tre giống hệt nhau vào $K$ khe khác nhau. Khoảng cách từ một ống tới ống kế tiếp là số bước khe đi theo chiều kim đồng hồ để gặp ống gần nhất. Cách gắn đạt nếu cả $K$ khoảng cách đều nằm trong đoạn $[A, B]$.

Hai cách khác nhau nếu tập khe khác nhau (xoay đi một góc vẫn tính là khác). Đếm số cách gắn đạt, lấy dư cho $10^9 + 7$.

Dữ liệu vào: một dòng $N, K, A, B$ ($2 \le K \le N \le 2 \cdot 10^6$; $1 \le A \le B \le N$).

Kết quả: số cách gắn đạt, mod $10^9 + 7$.

  • Subtask 1 (20%): $N \le 20$.
  • Subtask 2 (30%): $N \le 3000$.
  • Subtask 3 (20%): $B = N$.
  • Subtask 4 (30%): không có ràng buộc gì thêm.
Input
6 2 2 4
Output
9
Giải thích

Có $9$ cách: $\{0, 2\}, \{1, 3\}, \{2, 4\}, \{3, 5\}, \{0, 4\}, \{1, 5\}$ (hai khoảng cách $2$ và $4$) và $\{0, 3\}, \{1, 4\}, \{2, 5\}$ (hai khoảng cách đều là $3$). Cách $\{0, 1\}$ không đạt vì có các khoảng cách $1$ và $5$.

Dành cho học viênNộp bài trên ClueOJ

Bạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì dùng link này nhé. Bộ test có chia subtask và có cả những test "bẫy", nên code đúng ý tưởng nhưng sai chi tiết nhỏ cũng sẽ bị trừ điểm đó.

Nộp bài Guồng nước

Lời mở đầu

Bài này là một bài đếm thuần tuý: không cần cấu trúc dữ liệu nào, chỉ cần nhìn bài toán dưới đúng "góc" để đưa về những công thức tổ hợp quen thuộc. Trước hết, hãy đổi cách mô tả một cách gắn ống:

  • Nếu ta biết một khe có ống (gọi là khe xuất phát) và dãy $K$ khoảng cách $(g_1, g_2, \ldots, g_K)$ đi lần lượt theo chiều kim đồng hồ, ta dựng lại được toàn bộ tập khe: $s,\ s + g_1,\ s + g_1 + g_2, \ldots$ (lấy mod $N$).
  • Đi hết $K$ khoảng cách thì quay về đúng ống ban đầu, nên luôn có $g_1 + g_2 + \cdots + g_K = N$.
  • Điều kiện "đạt" chỉ nói về các khoảng cách: $A \le g_i \le B$ với mọi $i$.
g1=3 g2=2 g3=4 g4=3 0 1 2 3 4 5 6 7 8 9 10 11 Tập khe {0, 3, 5, 9} trên guồng N = 12 khe ⇔ dãy khoảng cách (g1, g2, g3, g4) = (3, 2, 4, 3) kèm khe xuất phát 0 Mọi dãy khoảng cách đều có tổng đúng bằng N = 12. Điều kiện "đạt": A ≤ gi ≤ B với mọi i. Cùng tập khe đó, nếu xuất phát từ khe 3 ta được (2, 4, 3, 3), từ khe 5: (4, 3, 3, 2), từ khe 9: (3, 3, 2, 4) – K cách đọc.

Một tập khe tương ứng với một dãy khoảng cách có tổng $N$ – nhưng tuỳ ta chọn ống nào làm điểm xuất phát mà có $K$ cách "đọc" dãy đó.

Vậy việc đếm số dãy $(g_1, \ldots, g_K)$ với tổng $N$ và mỗi số trong $[A, B]$ là bài toán "chia kẹo" quen thuộc. Cái khó (cũng là nút thắt của bài) nằm ở chỗ một tập khe lại ứng với nhiều dãy khác nhau, tuỳ điểm xuất phát. Ta sẽ đi từ cách vét cạn tới công thức $O(N)$:

Cách 1Vét mọi tập khe – O(2^N · N) · 20%

Với $N \le 20$, có $2^{20} \approx 10^6$ tập khe. Duyệt mọi số nhị phân mask có đúng $K$ bit $1$; lấy ra các khe được chọn theo thứ tự tăng dần $p_0 < p_1 < \cdots < p_{K-1}$. Các khoảng cách là $p_{i+1} - p_i$, riêng khoảng cuối đi vòng qua khe $0$: $p_0 + N - p_{K-1}$. Kiểm tra cả $K$ khoảng cách nằm trong $[A, B]$.

Đừng quên khoảng cách "vòng qua khe 0" – thiếu nó thì ví dụ của đề vẫn ra $9$ (rất dễ bị lừa!) nhưng các test khác sẽ sai.

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

int main() {
    int n, k, A, B;
    cin >> n >> k >> A >> B;
    long long cnt = 0;
    for (int mask = 0; mask < (1 << n); mask++) {        // thu moi tap khe
        if (__builtin_popcount(mask) != k) continue;
        vector<int> p;                                     // cac khe duoc chon, tang dan
        for (int i = 0; i < n; i++)
            if (mask >> i & 1) p.push_back(i);
        bool ok = true;
        for (int i = 0; i < k; i++) {
            int g = (i + 1 < k) ? p[i + 1] - p[i] : p[0] + n - p[i];   // khoang toi ong ke tiep
            if (g < A || g > B) { ok = false; break; }
        }
        if (ok) cnt++;
    }
    cout << cnt % 1000000007 << "\n";
    return 0;
}
import sys

def main():
    n, k, A, B = map(int, sys.stdin.read().split())
    cnt = 0
    for mask in range(1 << n):                      # thu moi tap khe
        if bin(mask).count("1") != k:
            continue
        p = [i for i in range(n) if mask >> i & 1]   # cac khe duoc chon, tang dan
        ok = True
        for i in range(k):
            g = p[i + 1] - p[i] if i + 1 < k else p[0] + n - p[i]   # khoang toi ong ke tiep
            if g < A or g > B:
                ok = False
                break
        if ok:
            cnt += 1
    print(cnt % 1000000007)

main()
Không gian$O(N)$
Thời gian$O(2^N \cdot N)$

Cách 2Cắt vòng tại khe 0 + QHĐ chia khoảng – O(N · K) · 50%

Vòng tròn khó đếm vì "không có điểm đầu". Ta tự tạo điểm đầu bằng cách nhìn vào khe 0. Với một cách gắn bất kỳ, gọi $p$ là khe nhỏ nhất có ống, $q$ là khe lớn nhất có ống. Khoảng cách từ ống $q$ tới ống $p$ (đi vòng qua khe $N - 1$ rồi khe $0$) là $g = p + N - q$ – ta gọi nó là khoảng vắt qua khe 0.

  • Biết $g$, ống $p$ có thể nằm ở bất kỳ khe nào trong $0, 1, \ldots, g - 1$ (khi đó $q = p + N - g$ vẫn hợp lệ): có đúng $g$ vị trí.
  • $K - 1$ khoảng cách còn lại nằm gọn trong đoạn từ $p$ tới $q$, nên chúng có tổng $q - p = N - g$, mỗi số trong $[A, B]$.

Gọi $dp_t[s]$ là số dãy $t$ số, mỗi số trong $[A, B]$, có tổng $s$. Khi đó $$\text{đáp án} = \sum_{g = A}^{\min(B, N)} g \cdot dp_{K-1}[N - g].$$ Tính $dp$ theo từng lớp $t$: $dp_t[s] = \sum_{x = A}^{B} dp_{t-1}[s - x]$ – một tổng trên đoạn liên tiếp, nên dùng mảng cộng dồn của lớp trước để tính mỗi ô trong $O(1)$. Có $K - 1$ lớp, mỗi lớp $O(N)$, tổng $O(NK) \le 9 \cdot 10^6$ khi $N \le 3000$.

Chạy tay ví dụ $N = 6, K = 2, A = 2, B = 4$: chỉ cần một lớp, $dp_1[s] = 1$ khi $s \in [2, 4]$.

$g$ (khoảng vắt qua khe 0)Số vị trí của $p$$dp_1[6 - g]$Đóng gópCác tập khe
22$dp_1[4] = 1$2$\{0, 4\}, \{1, 5\}$
33$dp_1[3] = 1$3$\{0, 3\}, \{1, 4\}, \{2, 5\}$
44$dp_1[2] = 1$4$\{0, 2\}, \{1, 3\}, \{2, 4\}, \{3, 5\}$

Tổng $2 + 3 + 4 = 9$ – đúng đáp án. Mỗi tập khe có đúng một khoảng vắt qua khe 0 nên không bị đếm trùng.

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll MOD = 1000000007;

int main() {
    int n, k, A, B;
    cin >> n >> k >> A >> B;
    // dp[s] = so cach viet s thanh tong cua (so khoang da xet) so, moi so trong [A, B]
    vector<ll> dp(n + 1, 0), pre(n + 2), nd(n + 1);
    dp[0] = 1;
    for (int t = 1; t <= k - 1; t++) {                 // them lan luot k - 1 khoang
        pre[0] = 0;
        for (int s = 0; s <= n; s++) pre[s + 1] = (pre[s] + dp[s]) % MOD;
        for (int s = 0; s <= n; s++) {
            int hi = s - A, lo = max(0, s - B);        // dp moi[s] = tong dp[s - g], g trong [A, B]
            nd[s] = (hi < lo) ? 0 : (pre[hi + 1] - pre[lo] + MOD) % MOD;
        }
        swap(dp, nd);
    }
    ll ans = 0;
    for (int g = A; g <= B && g <= n; g++)             // g = khoang "vat qua" khe 0, co g vi tri
        ans = (ans + (ll)g * dp[n - g]) % MOD;
    cout << ans << "\n";
    return 0;
}
import sys
MOD = 1000000007

def main():
    n, k, A, B = map(int, sys.stdin.read().split())
    # dp[s] = so cach viet s thanh tong cua (so khoang da xet) so, moi so trong [A, B]
    dp = [0] * (n + 1)
    dp[0] = 1
    for t in range(k - 1):                       # them lan luot k - 1 khoang
        pre = [0] * (n + 2)
        for s in range(n + 1):
            pre[s + 1] = (pre[s] + dp[s]) % MOD
        nd = [0] * (n + 1)
        for s in range(A, n + 1):                # dp moi[s] = tong dp[s - g], g trong [A, B]
            lo = s - B if s - B > 0 else 0
            nd[s] = (pre[s - A + 1] - pre[lo]) % MOD
        dp = nd
    ans = 0
    for g in range(A, min(B, n) + 1):            # g = khoang "vat qua" khe 0, co g vi tri
        ans = (ans + g * dp[n - g]) % MOD
    print(ans)

main()
Không gian$O(N)$
Thời gian$O(N \cdot K)$

Cách 3Chia kẹo Euler + đếm kép (khi B = N) – O(N) · 70%

Khi $B = N$, cận trên không còn tác dụng (một khoảng cách không thể vượt $N$). Ta làm hai bước.

Bước 1 – đếm dãy khoảng cách (chia kẹo Euler). Gọi $comp$ là số dãy $(g_1, \ldots, g_K)$ với $g_i \ge A$ và tổng bằng $N$. Đặt $h_i = g_i - A \ge 0$ thì $h_1 + \cdots + h_K = N - KA = m$. Số nghiệm nguyên không âm của phương trình này là bài "chia $m$ cái kẹo cho $K$ em" (xếp $m$ cái kẹo và $K - 1$ vách ngăn thành một hàng): $$comp = \binom{m + K - 1}{K - 1} \quad (\text{và } comp = 0 \text{ nếu } m < 0).$$

Bước 2 – từ dãy về tập khe (đếm kép). Xét các cặp (tập khe đạt, một ống được đánh dấu trong tập). Đếm số cặp theo hai cách:

  • Mỗi tập khe đạt có $K$ ống, nên có $K$ cách đánh dấu: số cặp $= K \cdot \text{đáp án}$.
  • Mỗi cặp tương ứng đúng một bộ (khe của ống được đánh dấu $s$, dãy khoảng cách đọc từ ống đó). Ngược lại, mọi bộ ($s \in [0, N)$, một dãy hợp lệ) đều dựng được một tập khe đạt với ống đánh dấu ở $s$: số cặp $= N \cdot comp$.

Vậy $K \cdot \text{đáp án} = N \cdot comp$, tức là $$\text{đáp án} = \frac{N \cdot comp}{K}.$$ Thử với ví dụ: $m = 6 - 2 \cdot 2 = 2$, $comp = \binom{3}{1} = 3$ (ba dãy $(2, 4), (3, 3), (4, 2)$), đáp án $= 6 \cdot 3 / 2 = 9$. Bấm "Tiếp" ở mô phỏng dưới đây để thấy mỗi tập khe xuất hiện đúng $K$ lần khi ta thử mọi điểm xuất phát:

Ví dụ nhanh:

Chia cho $K$ khi đang làm việc theo modulo. $comp$ đã được lấy dư cho $p = 10^9 + 7$, nên không được lấy $(N \cdot comp \bmod p)$ rồi chia nguyên cho $K$ – kết quả gần như luôn sai (nhưng với ví dụ nhỏ thì vẫn đúng!). Vì $p$ là số nguyên tố và $K < p$, ta nhân với nghịch đảo modulo $K^{-1} \equiv K^{p-2} \pmod p$ (định lý Fermat nhỏ), tính bằng lũy thừa nhanh. Giai thừa tới $N$ tính một lần trong $O(N)$.

Code dưới đây dùng công thức khi $B \ge N$ và quay về Cách 2 cho các trường hợp còn lại.

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll MOD = 1000000007;

ll power(ll a, ll e) {
    ll r = 1; a %= MOD;
    while (e > 0) {
        if (e & 1) r = r * a % MOD;
        a = a * a % MOD; e >>= 1;
    }
    return r;
}

ll slowDP(int n, int k, int A, int B) {                // Cach 2: cat vong tai khe 0, O(N * K)
    vector<ll> dp(n + 1, 0), pre(n + 2), nd(n + 1);
    dp[0] = 1;
    for (int t = 1; t <= k - 1; t++) {
        pre[0] = 0;
        for (int s = 0; s <= n; s++) pre[s + 1] = (pre[s] + dp[s]) % MOD;
        for (int s = 0; s <= n; s++) {
            int hi = s - A, lo = max(0, s - B);
            nd[s] = (hi < lo) ? 0 : (pre[hi + 1] - pre[lo] + MOD) % MOD;
        }
        swap(dp, nd);
    }
    ll ans = 0;
    for (int g = A; g <= B && g <= n; g++) ans = (ans + (ll)g * dp[n - g]) % MOD;
    return ans;
}

int main() {
    ll n, k, A, B;
    cin >> n >> k >> A >> B;
    if (B < n) { cout << slowDP(n, k, A, B) << "\n"; return 0; }
    // B >= n: khong co can tren. comp = C(n - kA + k - 1, k - 1)
    ll m = n - k * A;
    if (m < 0) { cout << 0 << "\n"; return 0; }
    vector<ll> f(n + 1);
    f[0] = 1;
    for (ll i = 1; i <= n; i++) f[i] = f[i - 1] * i % MOD;
    ll a = m + k - 1, b = k - 1;
    ll comp = f[a] * power(f[b], MOD - 2) % MOD * power(f[a - b], MOD - 2) % MOD;
    ll ans = n % MOD * comp % MOD * power(k, MOD - 2) % MOD;   // dem kep: n * comp / k
    cout << ans << "\n";
    return 0;
}
import sys
MOD = 1000000007

def slow_dp(n, k, A, B):                         # Cach 2: cat vong tai khe 0, O(N * K)
    dp = [0] * (n + 1)
    dp[0] = 1
    for t in range(k - 1):
        pre = [0] * (n + 2)
        for s in range(n + 1):
            pre[s + 1] = (pre[s] + dp[s]) % MOD
        nd = [0] * (n + 1)
        for s in range(A, n + 1):
            lo = s - B if s - B > 0 else 0
            nd[s] = (pre[s - A + 1] - pre[lo]) % MOD
        dp = nd
    ans = 0
    for g in range(A, min(B, n) + 1):
        ans = (ans + g * dp[n - g]) % MOD
    return ans

def main():
    n, k, A, B = map(int, sys.stdin.read().split())
    if B < n:
        print(slow_dp(n, k, A, B)); return
    # B >= n: khong co can tren. comp = C(n - kA + k - 1, k - 1)
    m = n - k * A
    if m < 0:
        print(0); return
    f = [1] * (n + 1)
    for i in range(1, n + 1):
        f[i] = f[i - 1] * i % MOD
    a, b = m + k - 1, k - 1
    comp = f[a] * pow(f[b], MOD - 2, MOD) % MOD * pow(f[a - b], MOD - 2, MOD) % MOD
    print(n % MOD * comp % MOD * pow(k, MOD - 2, MOD) % MOD)   # dem kep: n * comp / k

main()
Không gian$O(N)$
Thời gian$O(N + \log p)$ khi $B = N$

Cách 4Thêm bao hàm – loại trừ cho cận trên B – O(N) · 100%

Phép đếm kép ở Cách 3 không hề dùng tới $B$ – nó đúng với mọi điều kiện chỉ phụ thuộc vào dãy khoảng cách. Vậy chỉ còn phải đếm $$comp = \#\{(g_1, \ldots, g_K) : g_1 + \cdots + g_K = N,\ A \le g_i \le B\}.$$ Vẫn đặt $h_i = g_i - A$, ta cần $h_1 + \cdots + h_K = m = N - KA$ với $0 \le h_i \le B - A$. Gọi một khoảng là "quá dài" nếu $h_i \ge d$ với $d = B - A + 1$.

  • Nếu ép một tập $J$ gồm $j$ khoảng phải quá dài: đặt $h_i' = h_i - d$ với $i \in J$, ta lại được bài chia kẹo không cận trên với tổng $m - jd$, có $\binom{m - jd + K - 1}{K - 1}$ nghiệm.
  • Có $\binom{K}{j}$ cách chọn tập $J$. Theo nguyên lý bao hàm – loại trừ, số dãy không có khoảng nào quá dài là $$comp = \sum_{j \ge 0} (-1)^j \binom{K}{j} \binom{m - jd + K - 1}{K - 1}.$$
  • Chỉ cần $j \le K$ và $jd \le m$ (số hạng còn lại bằng $0$), nên có không quá $\min(K, m/d) + 1 \le N$ số hạng, mỗi số hạng $O(1)$ nhờ bảng giai thừa và nghịch đảo giai thừa.

Ví dụ $N = 8, K = 3, A = 2, B = 3$: $m = 2$, $d = 2$. Số hạng $j = 0$: $\binom{3}{0}\binom{4}{2} = 6$ (mọi cách chia không để ý cận trên); $j = 1$: $\binom{3}{1}\binom{2}{2} = 3$ (một khoảng bị ép dài $\ge 4$). Vậy $comp = 6 - 3 = 3$ – ba dãy $(2, 3, 3), (3, 2, 3), (3, 3, 2)$ – và đáp án $= 8 \cdot 3 / 3 = 8$. Em hãy thử với các bộ số khác trong mô phỏng:

Ví dụ nhanh:

Ghép lại: tính giai thừa $f[i]$ và nghịch đảo giai thừa $inv[i]$ tới $N$ (chỉ một lần lũy thừa nhanh cho $inv[N]$, rồi lùi $inv[i - 1] = inv[i] \cdot i$), cộng/trừ các số hạng bao hàm – loại trừ, cuối cùng nhân $N$ và $K^{-1}$. Mọi chỉ số tổ hợp đều không vượt $N - 1$ vì $m + K - 1 \le N - 1$.

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll MOD = 1000000007;

ll power(ll a, ll e) {
    ll r = 1; a %= MOD;
    while (e > 0) {
        if (e & 1) r = r * a % MOD;
        a = a * a % MOD; e >>= 1;
    }
    return r;
}

int main() {
    ll n, k, A, B;
    cin >> n >> k >> A >> B;
    ll m = n - k * A;                      // phan du sau khi moi khoang lay A
    if (m < 0) { cout << 0 << "\n"; return 0; }
    vector<ll> f(n + 1), inv(n + 1);       // giai thua va nghich dao giai thua
    f[0] = 1;
    for (ll i = 1; i <= n; i++) f[i] = f[i - 1] * i % MOD;
    inv[n] = power(f[n], MOD - 2);
    for (ll i = n; i >= 1; i--) inv[i - 1] = inv[i] * i % MOD;
    auto C = [&](ll a, ll b) -> ll {
        if (b < 0 || a < b) return 0;
        return f[a] * inv[b] % MOD * inv[a - b] % MOD;
    };
    ll d = B - A + 1, comp = 0;            // comp = so cach chia n thanh k khoang trong [A, B]
    for (ll j = 0; j <= k && j * d <= m; j++) {
        ll t = C(k, j) * C(m - j * d + k - 1, k - 1) % MOD;
        if (j % 2 == 0) comp = (comp + t) % MOD;
        else comp = (comp - t + MOD) % MOD;
    }
    ll ans = n % MOD * comp % MOD * power(k, MOD - 2) % MOD;   // dem kep: n * comp / k
    cout << ans << "\n";
    return 0;
}
import sys
MOD = 1000000007

def main():
    n, k, A, B = map(int, sys.stdin.buffer.read().split())
    m = n - k * A                          # phan du sau khi moi khoang lay A
    if m < 0:
        print(0); return
    f = [1] * (n + 1)                      # giai thua va nghich dao giai thua
    for i in range(1, n + 1):
        f[i] = f[i - 1] * i % MOD
    inv = [1] * (n + 1)
    inv[n] = pow(f[n], MOD - 2, MOD)
    for i in range(n, 0, -1):
        inv[i - 1] = inv[i] * i % MOD
    d = B - A + 1
    comp = 0                               # so cach chia n thanh k khoang trong [A, B]
    j = 0
    while j <= k and j * d <= m:
        a = m - j * d + k - 1              # C(k, j) * C(a, k - 1)
        t = f[k] * inv[j] % MOD * inv[k - j] % MOD * (f[a] * inv[k - 1] % MOD * inv[a - k + 1] % MOD) % MOD
        if j % 2 == 0:
            comp = (comp + t) % MOD
        else:
            comp = (comp - t) % MOD
        j += 1
    ans = n % MOD * comp % MOD * pow(k, MOD - 2, MOD) % MOD   # dem kep: n * comp / k
    print(ans)

main()
Không gian$O(N)$
Thời gian$O(N + \log p)$
Những lỗi hay gặp
(1) Quên khoảng cách vòng qua khe $0$ (coi các khe nằm trên một đường thẳng); (2) quên cận trên $B$ – chỉ dùng một tổ hợp chia kẹo; (3) lấy $N \cdot comp \bmod p$ rồi mới chia nguyên cho $K$; (4) khi trừ số hạng lẻ quên cộng thêm $p$, in ra số âm. Cả bốn lỗi này đều vẫn cho đúng ví dụ của đề!

Chạy tay từng dòng lời giải chuẩn với dữ liệu nhỏ ($N \le 12$) để thấy rõ bảng giai thừa, nghịch đảo và từng số hạng bao hàm – loại trừ:

C++▶ Chạy tay từng dòng codeTheo dõi giai thừa, nghịch đảo giai thừa, các số hạng bao hàm – loại trừ (N ≤ 12).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3typedef long long ll;
4const ll MOD = 1000000007;
5
6ll power(ll a, ll e) {
7 ll r = 1; a %= MOD;
8 while (e > 0) {
9 if (e & 1) r = r * a % MOD;
10 a = a * a % MOD; e >>= 1;
11 }
12 return r;
13}
14
15int main() {
16 ll n, k, A, B;
17 cin >> n >> k >> A >> B;
18 ll m = n - k * A; // phan du sau khi moi khoang lay A
19 if (m < 0) { cout << 0 << "\n"; return 0; }
20 vector<ll> f(n + 1), inv(n + 1); // giai thua va nghich dao giai thua
21 f[0] = 1;
22 for (ll i = 1; i <= n; i++) f[i] = f[i - 1] * i % MOD;
23 inv[n] = power(f[n], MOD - 2);
24 for (ll i = n; i >= 1; i--) inv[i - 1] = inv[i] * i % MOD;
25 auto C = [&](ll a, ll b) -> ll {
26 if (b < 0 || a < b) return 0;
27 return f[a] * inv[b] % MOD * inv[a - b] % MOD;
28 };
29 ll d = B - A + 1, comp = 0; // comp = so cach chia n thanh k khoang trong [A, B]
30 for (ll j = 0; j <= k && j * d <= m; j++) {
31 ll t = C(k, j) * C(m - j * d + k - 1, k - 1) % MOD;
32 if (j % 2 == 0) comp = (comp + t) % MOD;
33 else comp = (comp - t + MOD) % MOD;
34 }
35 ll ans = n % MOD * comp % MOD * power(k, MOD - 2) % MOD; // dem kep: n * comp / k
36 cout << ans << "\n";
37 return 0;
38}

Bộ test chấm "bắt" những lỗi nào?

Cách làmSub 1 (10 test)Sub 2 (9 test)Sub 3 (8 test)Sub 4 (11 test)Điểm
Cách 1 – vét mọi tập kheĐúngSai/TLESai/TLESai/TLE20
Cách 2 – cắt vòng tại khe 0 + QHĐ O(NK)ĐúngĐúngTLETLE50
Cách 3 – chia kẹo + đếm kép (B = N)ĐúngĐúngĐúngTLE70
Cách 4 – thêm bao hàm – loại trừĐúngĐúngĐúngĐúng100
Sai: lấy mod rồi chia nguyên cho K10/102/91/81/1131,9
Sai: quên +MOD khi trừ (in số âm)10/105/98/88/1178,5
Sai: quên cận trên B2/102/98/81/1133,4
Sai: quên khoảng vòng qua khe 02/102/91/81/1115,9

Ghi chú của mình: các cách C++ ở trên đã được chấm thử với đúng bộ test của bài: bộ test có 38 test (10 + 9 + 8 + 11), mỗi cách đạt đúng số điểm công bố, còn bốn lời giải sai bị bắt như bảng trên. Lời giải chuẩn sol.cpp chạy test lâu nhất chỉ khoảng 0,05 giây; bản Python sol.py cho cùng đáp án trên mọi test (trên CPython khoảng 0,6 giây, các em vẫn nên chọn PyPy 3 khi nộp). Ở subtask 3 và 4, $N \approx 2 \cdot 10^6$ và $K \ge 5000$ nên QHĐ $O(NK)$ của Cách 2 phải làm hơn $10^{10}$ phép tính – chạy quá 4 giây mỗi test. "TLE" nghĩa là chạy quá 3 lần giới hạn thời gian. Lỗi quên cộng MOD chỉ lộ ở những test có số hạng trừ lớn hơn tổng đang có, nên nó vẫn "ăn" trọn subtask 1 và 3 – đừng để mất điểm oan như vậy nhé!

Rất vui khi anh em nào đã đọc đến những dòng cuối cùng này. Anh em đọc chắc cũng mỏi mắt luôn chứ 😅 vậy anh em nghĩ người viết ra hết đống ở trên còn thế nào nữa 🤪. Vậy nên đừng tiếc gì cho mình một lời bình luận hoặc chia sẻ bài viết để động viên mình nè. Chúc anh em học tốt và gặt hái nhiều thành công 😇

Tự học thuật toán: khó ở đâu?
Đọc xong một bài giải là bước đầu. Phần lớn học sinh tự học sẽ gặp những trở ngại sau:
  • Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
  • Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
  • Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
  • Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
  • Dễ nản giữa chừng khi không có ai theo sát.
Học cùng thầy Nam Sơn, các trở ngại đó được gỡ bỏ:
  • Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
  • Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
  • Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
  • Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.

Nhận xét