Chương 7·Bài 7.3

Advanced Math

Chia 10 viên kẹo giống nhau cho 4 bạn, bạn nào cũng được nhận 0 hoặc nhiều hơn. Có bao nhiêu cách? Liệt kê thì mệt, nhưng đặt 3 vách ngăn vào giữa dãy kẹo là ra ngay (133)\binom{13}{3}.

Các bài đếm khó thường lắp từ vài viên gạch quen thuộc như vậy: tổ hợp theo mô-đun (giai thừa và nghịch đảo tính sẵn), bao hàm loại trừ (đếm cái dễ rồi trừ cái vi phạm), nguyên lý phản xạ cho đường đi không vượt một đường thẳng, đếm theo vi phạm đầu tiên cho lưới có ô cấm, và bổ đề Burnside khi hai cấu hình được coi là một nếu biến đổi được thành nhau. Việc chính là đưa đề về một trong các khuôn này.

Tổ hợp theo mô-đun

Theo mô-đun nguyên tố pp, tính sẵn fac[i] =i!= i! và ifac[i] =(i!)−1= (i!)^{-1}: lấy ifac[n] bằng lũy thừa p−2p - 2, rồi ifac[i-1] = ifac[i] * i. Khi đó (nk)=n!⋅(k!)−1⋅((n−k)!)−1\binom{n}{k} = n! \cdot (k!)^{-1} \cdot ((n-k)!)^{-1} trong O(1)O(1).

Cách lùi dần này đáng dùng vì rẻ hơn hẳn việc lấy nghịch đảo từng số:

Dựng bảng giai thừa và nghịch đảo giai thừa tới hai triệu.lũy thừa nhanh từng số188 ms · gấp 8 lầnmột lũy thừa, rồi lùi dần23 ms
Dựng bảng giai thừa và nghịch đảo giai thừa tới hai triệu, hai cách.

Một lũy thừa rồi lùi dần nhanh hơn lũy thừa từng số khoảng 8 lần. Quy tắc: chỉ lấy nghịch đảo bằng Fermat một lần, các số còn lại suy ra bằng phép nhân.

Hai công thức dùng nhiều nhất:

  • Hoán vị đa tập: xâu dài nn có c1,c2,…c_1, c_2, \dots ký tự mỗi loại có n!c1! c2!⋯\frac{n!}{c_1! \, c_2! \cdots} cách sắp.
  • Chia kẹo: chia mm vật giống nhau cho nn người (được nhận 0) có (n+m−1m)\binom{n + m - 1}{m} cách, bằng số cách đặt n−1n - 1 vách ngăn vào giữa mm vật.

Ví dụHoán vị của đa tập ký tự

Đề. Xâu chữ thường dài tới 10610^6. Đếm số xâu khác nhau tạo được bằng cách sắp lại mọi ký tự, theo mô-đun 109+710^9 + 7.

Lời giải. Nếu mọi ký tự đều khác nhau thì có n!n! cách sắp. Chữ cc xuất hiện kck_c lần thì mỗi xâu bị đếm lặp kc!k_c! lần do đổi chỗ các bản của cc với nhau. Vậy đáp án là n!∏ckc!\dfrac{n!}{\prod_c k_c!}; phép chia theo mô-đun nguyên tố là nhân nghịch đảo.

Bước 1: bảng giai thừa và nghịch đảo. ifac[n] tính bằng Fermat, (n!)p−2(n!)^{p-2}; các ifac nhỏ hơn suy ra lùi bằng ifac[i - 1] = ifac[i] * i, vì 1(i−1)!=ii!\frac{1}{(i-1)!} = \frac{i}{i!}. Cả bảng chỉ tốn một lần lũy thừa.

C++
fac[0] = 1;
for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % MOD;
ifac[n] = power(fac[n], MOD - 2);
for (int i = n; i >= 1; i--) ifac[i - 1] = ifac[i] * i % MOD;

Bước 2: chia cho các kc!k_c!. Chữ không xuất hiện có ifac[0] = 1, không làm đổi kết quả.

C++
ll ans = fac[n];
for (int c = 0; c < 26; c++) ans = ans * ifac[cnt[c]] % MOD;
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll MOD = 1e9 + 7;
const int N = 1e6 + 5;
ll fac[N], ifac[N];

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

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    string s;
    cin >> s;
    int n = s.size();
    fac[0] = 1;
    for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % MOD;
    ifac[n] = power(fac[n], MOD - 2);
    for (int i = n; i >= 1; i--) ifac[i - 1] = ifac[i] * i % MOD;
    int cnt[26] = {};
    for (char c : s) cnt[c - 'a']++;
    ll ans = fac[n];
    for (int c = 0; c < 26; c++) ans = ans * ifac[cnt[c]] % MOD;
    cout << ans << '\n';
}
aab

Độ phức tạp: O(n+log⁡p)O(n + \log p).

Không điểm cố định

Số hoán vị không có ii nào ở đúng vị trí ii: gọi AiA_i là tập hoán vị giữ nguyên ii; một giao kk tập có (n−k)!(n - k)! phần tử, nên

Dn=∑k=0n(−1)k(nk)(n−k)!.D_n = \sum_{k=0}^{n} (-1)^k \binom{n}{k} (n - k)! .

Cũng có truy hồi gọn: quà của người 1 tới người j≠1j \ne 1 (n−1n - 1 cách). Nếu jj đưa lại quà cho 1 thì còn Dn−2D_{n-2} cách cho những người khác. Nếu không thì coi “không nhận quà của 1” như “không nhận quà của mình” cho jj, còn Dn−1D_{n-1} cách. Vậy Dn=(n−1)(Dn−1+Dn−2)D_n = (n - 1)(D_{n-1} + D_{n-2}), D1=0D_1 = 0, D2=1D_2 = 1.

Hình dưới liệt kê mọi hoán vị không điểm cố định, chia theo việc người nhận quà của 1 có trả lại cho 1 hay không. Kéo nn để xem.

Hoán vị không điểm cố định.

Hỏi nhanh

Có bao nhiêu hoán vị của {1,2,3,4}\{1, 2, 3, 4\} không có điểm cố định?

Ví dụHoán vị không điểm cố định

Đề. n≤106n \le 10^6 em, mỗi em một món quà. Đếm cách phát sao cho không em nào nhận lại quà của mình, theo mô-đun 109+710^9 + 7.

Lời giải. Truy hồi Dn=(n−1)(Dn−1+Dn−2)D_n = (n - 1)(D_{n-1} + D_{n-2}) với D0=1D_0 = 1, D1=0D_1 = 0, đúng như lập luận ở trên. Chỉ cần hai giá trị trước, nên giữ hai biến thay cho cả mảng.

a, b lần lượt là Di−2D_{i-2}, Di−1D_{i-1}. Mỗi vòng tính DiD_i rồi dời hai biến. (i−1)(i - 1) tới 10610^6 nhân một số dưới 109+710^9 + 7 vẫn vừa long long.

C++
ll a = 1, b = 0;
for (ll i = 2; i <= n; i++) {
    ll c = (i - 1) * ((a + b) % MOD) % MOD;
    a = b, b = c;
}

Với n=1n = 1 vòng không chạy và in D1=0D_1 = 0: một em thì không cách nào để em đó nhận quà người khác.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll MOD = 1e9 + 7;

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    ll n;
    cin >> n;
    ll a = 1, b = 0;
    for (ll i = 2; i <= n; i++) {
        ll c = (i - 1) * ((a + b) % MOD) % MOD;
        a = b, b = c;
    }
    cout << b << '\n';
}
1

Độ phức tạp: O(n)O(n).

Nguyên lý phản xạ

Đường đi trên lưới gồm aa bước lên, bb bước xuống, bắt đầu ở độ cao h≥0h \ge 0. Đếm những đường không xuống dưới 0: lấy tổng (a+ba)\binom{a + b}{a} trừ các đường chạm −1-1. Lật phần đường trước lần chạm −1-1 đầu tiên qua đường y=−1y = -1 cho một song ánh với các đường bất kỳ từ −h−2-h - 2 tới cùng điểm cuối; số đường đó là (a+ba+h+1)\binom{a + b}{a + h + 1}. Vậy

(a+ba)−(a+ba+h+1).\binom{a + b}{a} - \binom{a + b}{a + h + 1}.

Với h=0h = 0, a=b=n/2a = b = n/2, đây là số Catalan 1n/2+1(nn/2)\frac{1}{n/2 + 1}\binom{n}{n/2}: số dãy ngoặc đúng dài nn.

Trong hình, đường xanh có chạm −1-1 thì phần trước lần chạm đầu được lật (cam): mỗi đường xấu ứng đúng một đường xuất phát từ −h−2-h - 2.

Lật phần đường trước lần chạm đầu.

Ví dụHoàn thiện tiền tố ngoặc

Đề. Độ dài n≤106n \le 10^6 và một tiền tố ngoặc dài kk. Đếm số cách nối thêm để được dãy ngoặc đúng dài nn, theo mô-đun 109+710^9 + 7.

Lời giải. Coi ngoặc mở là bước lên, ngoặc đóng là bước xuống. Dãy ngoặc đúng là đường đi không xuống dưới 0 và kết thúc ở 0. Tiền tố cho trước đưa ta tới độ cao hh, đã dùng op ngoặc mở và cl ngoặc đóng. Phần nối thêm cần đúng a=n/2−opa = n/2 - op bước lên và b=n/2−clb = n/2 - cl bước xuống, xuất phát từ độ cao hh và không xuống dưới 0. Công thức phản xạ cho (a+ba)−(a+ba+h+1)\binom{a + b}{a} - \binom{a + b}{a + h + 1}.

Bước 1: đọc tiền tố. Theo dõi độ cao h; xuống dưới 0 ở bất kỳ lúc nào thì in 0 ngay.

C++
for (char c : s) {
    if (c == '(') h++, op++;
    else h--;
    if (h < 0) {
        cout << 0 << '\n';
        return 0;
    }
}

Bước 2: các trường hợp vô nghiệm. nn lẻ, hay tiền tố dùng quá n/2n/2 ngoặc một loại.

C++
if (n % 2 || op > n / 2 || cl > n / 2) {
    cout << 0 << '\n';
    return 0;
}

Bước 3: công thức phản xạ. C(n, k) trả 0 khi kk ngoài [0,n][0, n], nên khi a+h+1>a+ba + h + 1 > a + b (không đường nào chạm −1-1 được) số hạng thứ hai tự bằng 0. Phép trừ cộng thêm MOD.

C++
ll a = n / 2 - op, b = n / 2 - cl;
cout << (C(a + b, a) - C(a + b, a + h + 1) + MOD) % MOD << '\n';

Kiểm với mẫu: n=2n = 2, tiền tố (, có h=1h = 1, a=0a = 0, b=1b = 1; (10)−(12)=1\binom{1}{0} - \binom{1}{2} = 1.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll MOD = 1e9 + 7;
const int N = 1e6 + 5;
ll fac[N], ifac[N];

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

ll C(ll n, ll k) {
    if (k < 0 || k > n) return 0;
    return fac[n] * ifac[k] % MOD * ifac[n - k] % MOD;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    ll n;
    string s;
    cin >> n >> s;
    fac[0] = 1;
    for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % MOD;
    ifac[n] = power(fac[n], MOD - 2);
    for (int i = n; i >= 1; i--) ifac[i - 1] = ifac[i] * i % MOD;
    ll h = 0, op = 0;
    for (char c : s) {
        if (c == '(') h++, op++;
        else h--;
        if (h < 0) {
            cout << 0 << '\n';
            return 0;
        }
    }
    ll cl = s.size() - op;
    if (n % 2 || op > n / 2 || cl > n / 2) {
        cout << 0 << '\n';
        return 0;
    }
    ll a = n / 2 - op, b = n / 2 - cl;
    cout << (C(a + b, a) - C(a + b, a + h + 1) + MOD) % MOD << '\n';
}
2
(

Độ phức tạp: O(n)O(n).

Ô cấm đầu tiên

Đường đi đơn điệu (chỉ xuống, sang phải) từ (1,1)(1,1) tới (r,c)(r, c) có (r+c−2r−1)\binom{r + c - 2}{r - 1} cách. Có nn ô cấm thì bao hàm loại trừ trên mọi tập ô là 2n2^n, nhưng chỉ cần phân loại theo ô cấm đầu tiên mà đường chạm: sắp các ô cấm theo (r,c)(r, c), thêm đích làm ô cuối, gọi dpidp_i là số đường từ gốc tới ô ii không qua ô cấm nào trước đó:

dpi=(ri+ci−2ri−1)−∑j: rj≤ri, cj≤ci, j≠idpj((ri−rj)+(ci−cj)ri−rj).dp_i = \binom{r_i + c_i - 2}{r_i - 1} - \sum_{j:\, r_j \le r_i,\, c_j \le c_i,\, j \ne i} dp_j \binom{(r_i - r_j) + (c_i - c_j)}{r_i - r_j}.

Mỗi đường xấu tới ii có đúng một ô cấm đầu tiên jj, nên không bị trừ hai lần. Tổng O(n2)O(n^2).

Hỏi nhanh

Lưới 2×22 \times 2, ô cấm (1,2)(1, 2). Có bao nhiêu đường từ (1,1)(1,1) tới (2,2)(2,2)?

Ví dụĐường đi tránh ô cấm

Đề. Lưới h×wh \times w (≤105\le 10^5), n≤2000n \le 2000 ô đen. Đếm đường đi xuống, sang phải từ (1,1)(1,1) tới (h,w)(h, w) không qua ô đen, theo mô-đun 109+710^9 + 7.

Lời giải. Sắp các ô đen theo (r,c)(r, c) và thêm đích (h,w)(h, w) làm ô cuối. dpidp_i là số đường từ (1,1)(1, 1) tới ô ii không đi qua ô đen nào trước khi tới ii. Lấy mọi đường tới ii rồi trừ các đường xấu, phân loại theo ô đen đầu tiên jj mà đường chạm: có dpjdp_j cách tới jj sạch, rồi (Δr+ΔcΔr)\binom{\Delta r + \Delta c}{\Delta r} cách đi tiếp tới ii. Mỗi đường xấu có đúng một ô đen đầu tiên nên bị trừ đúng một lần.

Bước 1: sắp. Ô có thể đi trước ô ii (cả hai tọa độ không lớn hơn) luôn đứng trước ii, nên khi tính dp[i] mọi dp[j] cần dùng đã xong. Bảng giai thừa dựng tới 2⋅1052 \cdot 10^5, đủ cho (h+w−2h−1)\binom{h + w - 2}{h - 1}.

C++
sort(p, p + n);
p[n] = {h, w};

Bước 2: trừ đường xấu. Chỉ trừ những ô jj nằm ở góc trên trái của ii; ô khác không thể nằm trên đường đơn điệu tới ii.

C++
dp[i] = C(r + c - 2, r - 1);
for (int j = 0; j < i; j++)
    if (p[j].first <= r && p[j].second <= c) {
        int dr = r - p[j].first, dc = c - p[j].second;
        dp[i] = (dp[i] - dp[j] * C(dr + dc, dr) % MOD + MOD) % MOD;
    }
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll MOD = 1e9 + 7;
const int N = 2e5 + 5;
ll fac[N], ifac[N], dp[2005];
pair<int, int> p[2005];

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

ll C(int n, int k) {
    if (k < 0 || k > n) return 0;
    return fac[n] * ifac[k] % MOD * ifac[n - k] % MOD;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    int h, w, n;
    cin >> h >> w >> n;
    fac[0] = 1;
    for (int i = 1; i < N; i++) fac[i] = fac[i - 1] * i % MOD;
    ifac[N - 1] = power(fac[N - 1], MOD - 2);
    for (int i = N - 1; i >= 1; i--) ifac[i - 1] = ifac[i] * i % MOD;
    for (int i = 0; i < n; i++) cin >> p[i].first >> p[i].second;
    sort(p, p + n);
    p[n] = {h, w};
    for (int i = 0; i <= n; i++) {
        int r = p[i].first, c = p[i].second;
        dp[i] = C(r + c - 2, r - 1);
        for (int j = 0; j < i; j++)
            if (p[j].first <= r && p[j].second <= c) {
                int dr = r - p[j].first, dc = c - p[j].second;
                dp[i] = (dp[i] - dp[j] * C(dr + dc, dr) % MOD + MOD) % MOD;
            }
    }
    cout << dp[n] << '\n';
}
2 2 1
1 2

Độ phức tạp: O(n2)=4⋅106O(n^2) = 4 \cdot 10^6 phép.

Bổ đề Burnside

Khi hai cấu hình được coi là một nếu một phép biến đổi trong nhóm GG (như các phép quay) đưa cái này thành cái kia, số lớp tương đương là trung bình số cấu hình bất động qua các phép biến đổi:

1∣G∣∑g∈G∣Fix⁡(g)∣.\frac{1}{|G|} \sum_{g \in G} |\operatorname{Fix}(g)|.

Vòng cổ nn hạt, mm màu, nhóm gồm nn phép quay: phép quay kk bước chia các hạt thành gcd⁡(k,n)\gcd(k, n) chu trình, mỗi chu trình phải cùng màu, nên cố định mgcd⁡(k,n)m^{\gcd(k, n)} cách tô. Đáp án 1n∑k=0n−1mgcd⁡(k,n)\frac{1}{n} \sum_{k=0}^{n-1} m^{\gcd(k, n)}; gom theo ước dd của nn được 1n∑d∣nφ(n/d) md\frac{1}{n} \sum_{d \mid n} \varphi(n/d)\, m^d.

Trong hình, các hạt cùng màu là cùng chu trình của phép quay kk bước; bên phải là số cách tô bất động cho từng kk.

Chu trình của phép quay.

Ví dụVòng màu dưới phép quay

Đề. Vòng cổ n≤106n \le 10^6 hạt, m≤106m \le 10^6 màu; hai vòng như nhau nếu quay được thành nhau. Đếm số vòng theo mô-đun 109+710^9 + 7.

Lời giải. Bổ đề Burnside với nhóm nn phép quay: phép quay kk bước có gcd⁡(k,n)\gcd(k, n) chu trình hạt, mỗi chu trình một màu, nên giữ nguyên mgcd⁡(k,n)m^{\gcd(k, n)} cách tô. Số vòng khác nhau là trung bình các số đó, 1n∑k=0n−1mgcd⁡(k,n)\frac{1}{n} \sum_{k=0}^{n-1} m^{\gcd(k, n)}. Chia cho nn là nhân nghịch đảo của nn (theo Fermat, n<pn < p nên có nghịch đảo).

pw[i] =mi= m^i tính sẵn tới nn. k=0k = 0 là phép quay không làm gì, __gcd(0, n) = n, giữ nguyên cả mnm^n cách tô.

C++
for (ll k = 0; k < n; k++) sum = (sum + pw[__gcd(k, n)]) % MOD;
cout << sum * power(n, MOD - 2) % MOD << '\n';

Gom theo ước như công thức với φ\varphi ở trên thì nhanh hơn, nhưng ở đây chưa cần.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll MOD = 1e9 + 7;

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

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    ll n, m;
    cin >> n >> m;
    vector<ll> pw(n + 1);
    pw[0] = 1;
    for (int i = 1; i <= n; i++) pw[i] = pw[i - 1] * m % MOD;
    ll sum = 0;
    for (ll k = 0; k < n; k++) sum = (sum + pw[__gcd(k, n)]) % MOD;
    cout << sum * power(n, MOD - 2) % MOD << '\n';
}
1 1

Độ phức tạp: O(nlog⁡n)O(n \log n).

Khi đi thi

  • Tính sẵn giai thừa và nghịch đảo giai thừa tới cận trên của mọi tham số trong (⋅⋅)\binom{\cdot}{\cdot}; hàm C trả 0 khi k<0k < 0 hay k>nk > n.
  • “Không có cái nào…”: bao hàm loại trừ. Nhiều ô cấm có thứ tự: đếm theo vi phạm đầu tiên. Đường đi không vượt đường thẳng: phản xạ.
  • “Coi là một nếu quay, lật được”: Burnside, đếm số chu trình của từng phép biến đổi.

Tóm tắt

  • (nk)\binom{n}{k} trong O(1)O(1) nhờ fac, ifac; hoán vị đa tập n!/∏ci!n!/\prod c_i!; chia kẹo (n+m−1m)\binom{n + m - 1}{m}.
  • Hoán vị không điểm cố định Dn=(n−1)(Dn−1+Dn−2)D_n = (n - 1)(D_{n-1} + D_{n-2}).
  • Không xuống dưới 0 từ độ cao hh: (a+ba)−(a+ba+h+1)\binom{a+b}{a} - \binom{a+b}{a+h+1}.
  • Burnside: số lớp bằng trung bình số cấu hình bất động; vòng cổ 1n∑mgcd⁡(k,n)\frac1n \sum m^{\gcd(k,n)}.

Bài tập

Đọc thêm