Chương 7·Bài 7.3.2

Lattice Path Counting

Đi trên lưới từ góc (0,0)(0, 0) tới góc (n,n)(n, n), mỗi bước sang phải hoặc lên trên một ô, và không bao giờ đi lên phía trên đường chéo. Có bao nhiêu đường như vậy? Liệt kê thì mệt, nhưng lấy cả (2nn)\binom{2n}{n} đường trừ đi số đường vi phạm, mà số đường vi phạm đếm được bằng một phép phản xạ, là ra ngay.

Các bài đếm khó thường lắp từ vài viên gạch quen thuộc như vậy, trên nền các công thức tổ hợp và cách tính (nk)\binom{n}{k} theo modulo của bài 0.9.5: 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.

IKhô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).

IINguyê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).

IIIÔ 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.

IVBổ đề 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

  • Mọi lời giải dùng sẵn fac, ifac của bài 0.9.5 để tính (nk)\binom{n}{k} theo modulo trong O(1)O(1).
  • 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

Bài tập ngoài

Đọc thêm