Đi trên lưới từ góc tới góc , 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ả đườ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 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ó nào ở đúng vị trí : gọi là tập hoán vị giữ nguyên ; một giao tập có phần tử, nên
Cũng có truy hồi gọn: quà của người 1 tới người ( cách). Nếu đưa lại quà cho 1 thì còn 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 , còn cách. Vậy , , .
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 để xem.
Hỏi nhanh
Có bao nhiêu hoán vị của không có điểm cố định?
, . Kiểm bằng bao hàm loại trừ: .
Ví dụHoán vị không điểm cố định
Đề. 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 .
Lời giải. Truy hồi với , , đú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à , . Mỗi vòng tính rồi dời hai biến. tới nhân một số dưới vẫn vừa long long.
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 vòng không chạy và in : một em thì không cách nào để em đó nhận quà người khác.
#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
0
Độ phức tạp: .
IINguyên lý phản xạ
Đường đi trên lưới gồm bước lên, bước xuống, bắt đầu ở độ cao . Đếm những đường không xuống dưới 0: lấy tổng trừ các đường chạm . Lật phần đường trước lần chạm đầu tiên qua đường cho một song ánh với các đường bất kỳ từ tới cùng điểm cuối; số đường đó là . Vậy
Với , , đây là số Catalan : số dãy ngoặc đúng dài .
Trong hình, đường xanh có chạm 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ừ .
Ví dụHoàn thiện tiền tố ngoặc
Đề. Độ dài và một tiền tố ngoặc dài . Đếm số cách nối thêm để được dãy ngoặc đúng dài , theo mô-đun .
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 , đã dùng op ngoặc mở và cl ngoặc đóng. Phần nối thêm cần đúng bước lên và bước xuống, xuất phát từ độ cao và không xuống dưới 0. Công thức phản xạ cho .
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.
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. lẻ, hay tiền tố dùng quá ngoặc một loại.
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 ngoài , nên khi (không đường nào chạm được) số hạng thứ hai tự bằng 0. Phép trừ cộng thêm MOD.
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: , tiền tố (, có , , ; .
#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
(
1
Độ phức tạp: .
IIIÔ cấm đầu tiên
Đường đi đơn điệu (chỉ xuống, sang phải) từ tới có cách. Có ô cấm thì bao hàm loại trừ trên mọi tập ô là , 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 , thêm đích làm ô cuối, gọi là số đường từ gốc tới ô không qua ô cấm nào trước đó:
Mỗi đường xấu tới có đúng một ô cấm đầu tiên , nên không bị trừ hai lần. Tổng .
Hỏi nhanh
Lưới , ô cấm . Có bao nhiêu đường từ tới ?
Tổng có đường; các đường qua : 1 đường tới nó nhân 1 đường đi tiếp. Còn đường, đi xuống trước.
Ví dụĐường đi tránh ô cấm
Đề. Lưới (), ô đen. Đếm đường đi xuống, sang phải từ tới không qua ô đen, theo mô-đun .
Lời giải. Sắp các ô đen theo và thêm đích làm ô cuối. là số đường từ tới ô không đi qua ô đen nào trước khi tới . Lấy mọi đường tới rồi trừ các đường xấu, phân loại theo ô đen đầu tiên mà đường chạm: có cách tới sạch, rồi cách đi tiếp tới . 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 ô (cả hai tọa độ không lớn hơn) luôn đứng trước , 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 , đủ cho .
sort(p, p + n);
p[n] = {h, w};
Bước 2: trừ đường xấu. Chỉ trừ những ô nằm ở góc trên trái của ; ô khác không thể nằm trên đường đơn điệu tới .
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;
}
#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
1
Độ phức tạp: 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 (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:
Vòng cổ hạt, màu, nhóm gồm phép quay: phép quay bước chia các hạt thành chu trình, mỗi chu trình phải cùng màu, nên cố định cách tô. Đáp án ; gom theo ước của được .
Trong hình, các hạt cùng màu là cùng chu trình của phép quay bước; bên phải là số cách tô bất động cho từng .
Ví dụVòng màu dưới phép quay
Đề. Vòng cổ hạt, màu; hai vòng như nhau nếu quay được thành nhau. Đếm số vòng theo mô-đun .
Lời giải. Bổ đề Burnside với nhóm phép quay: phép quay bước có chu trình hạt, mỗi chu trình một màu, nên giữ nguyên cách tô. Số vòng khác nhau là trung bình các số đó, . Chia cho là nhân nghịch đảo của (theo Fermat, nên có nghịch đảo).
pw[i] tính sẵn tới . là phép quay không làm gì, __gcd(0, n) = n, giữ nguyên cả cách tô.
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 ở trên thì nhanh hơn, nhưng ở đây chưa cần.
#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
1
Độ phức tạp: .
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 ; hàm
Ctrả 0 khi hay . - “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,ifaccủa bài 0.9.5 để tính theo modulo trong . - Hoán vị không điểm cố định .
- Không xuống dưới 0 từ độ cao : .
- Burnside: số lớp bằng trung bình số cấu hình bất động; vòng cổ .
Bài tập
Bài tập ngoài
Đọc thêm
- Binomial Coefficientscp-algorithms
- The Inclusion-Exclusion Principlecp-algorithms
- Burnside's lemma / Pólya enumeration theoremcp-algorithms