Burnside và Polya (Đếm với đối xứng)
Bổ đề Burnside và Định lý Polya giải bài toán đếm số cấu hình phân biệt khi đã tính đến đối xứng: hai cấu hình được coi là giống nhau nếu có thể biến cái này thành cái kia bằng một phép biến đổi trong nhóm đối xứng (xoay, lật, hoán vị nhãn...). Ví dụ kinh điển: đếm số vòng cổ hạt với màu, trong đó xoay vòng đi một góc không tạo ra vòng mới.
Cách ngây thơ là sinh ra tất cả cấu hình rồi gom nhóm các cấu hình tương đương — vừa tốn bộ nhớ vừa chậm theo cấp số nhân. Burnside biến bài toán thành một tổng chạy trên các phép biến đổi của nhóm (thường chỉ vài hoặc vài chục phép, hoặc phép xoay), đưa độ phức tạp về đa thức. Với vòng cổ, công thức rút gọn còn thay vì .
Ý tưởng / Trực giác
Gọi là tập tất cả cấu hình (kể cả các cấu hình tương đương nhau), và là nhóm các phép biến đổi (ví dụ nhóm xoay ). Hai cấu hình "giống nhau" khi có một biến cái này thành cái kia. Tập các cấu hình giống nhau gọi là một quỹ đạo (orbit). Câu hỏi đếm-có-đối-xứng chính là: có bao nhiêu quỹ đạo?
Bổ đề Burnside cho công thức:
trong đó = số cấu hình bất động (fixed) dưới , tức tác động vào nó mà nó không đổi.
Vì sao công thức này đúng? Đếm theo hai cách số cặp thoả (phép giữ nguyên cấu hình ):
- Gom theo : tổng .
- Gom theo : với mỗi , số giữ nguyên nó là kích thước nhóm con ổn định (stabilizer) . Theo định lý quỹ đạo–ổn định, . Cộng trên cả một quỹ đạo gồm phần tử, mỗi phần tử đóng góp , nên một quỹ đạo đóng góp đúng . Vậy .
Cho hai cách bằng nhau rồi chia cho , ta được công thức. Trực giác: trung bình số điểm bất động trên toàn nhóm chính bằng số quỹ đạo.
Trực giác cho phần trong tô màu. Một phép là một hoán vị các vị trí. Hoán vị này phân rã thành các chu trình (cycle). Cấu hình bất động dưới khi và chỉ khi mọi vị trí trong cùng một chu trình có cùng màu (vì luân chuyển chúng cho nhau). Mỗi chu trình tự do chọn 1 trong màu, nên nếu có chu trình thì . Đây là cầu nối tới Polya.
Với phép xoay vị trí trên vòng hạt, hoán vị vị trí có đúng chu trình, mỗi chu trình dài . Do đó:
Ví dụ chạy tay
Đếm vòng cổ hạt, màu, xét nhóm xoay xoay 0, 1, 2, 3 vị trí.
Đánh số vị trí 0 1 2 3. Phép xoay đưa vị trí . Ta phân rã từng phép thành chu trình và đếm :
j=0 (đơn vị): 0->0 1->1 2->2 3->3
chu trình: (0)(1)(2)(3) c = gcd(4,0)=4 -> 3^4 = 81
[mỗi vị trí tự do]
j=1 (xoay 1): 0->1->2->3->0
chu trình: (0 1 2 3) c = gcd(4,1)=1 -> 3^1 = 3
[cả 4 hạt phải cùng màu]
j=2 (xoay 2): 0->2->0 1->3->1
chu trình: (0 2)(1 3) c = gcd(4,2)=2 -> 3^2 = 9
[hạt đối nhau cùng màu]
j=3 (xoay 3): 0->3->2->1->0
chu trình: (0 3 2 1) c = gcd(4,3)=1 -> 3^1 = 3
Bảng tổng kết (ô đang cộng dồn đánh dấu <--):
| Phép | số chu trình | ||
|---|---|---|---|
| 0 | 4 | 4 | 81 |
| 1 | 1 | 1 | 3 |
| 2 | 2 | 2 | 9 |
| 3 | 1 | 1 | 3 |
| tổng | 96 |
Áp dụng Burnside:
Khớp với đáp án mẫu của bài cntnecklace (input 4 3 → 24). Lưu ý tổng luôn chia hết cho trên số nguyên — đây là tính chất bắt buộc của Burnside, hữu ích để kiểm tra cài đặt khi chưa lấy modulo.
Cài đặt
Khung chung cho số học modulo (luỹ thừa nhanh + nghịch đảo qua định lý Fermat nhỏ, áp dụng khi nguyên tố):
#include <bits/stdc++.h>
using namespace std;
const long long MOD = 1e9 + 7;
// Luỹ thừa nhanh: a^e mod MOD, O(log e)
long long power(long long a, long long e, long long mod) {
a %= mod; if (a < 0) a += mod; // chuẩn hoá cơ số về [0, mod)
long long r = 1;
while (e > 0) {
if (e & 1) r = r * a % mod;
a = a * a % mod;
e >>= 1;
}
return r;
}
// Nghịch đảo modulo: a^(MOD-2) mod MOD, đúng khi MOD nguyên tố và gcd(a, MOD) = 1
long long inv(long long a, long long mod) { return power(a, mod - 2, mod); }
Đếm vòng cổ dưới nhóm xoay (bài cntnecklace), trực tiếp theo công thức :
// Đếm số vòng cổ n hạt, m màu, phân biệt khi xoay. O(n log n).
long long count_necklace(long long n, long long m) {
long long total = 0;
for (long long j = 0; j < n; j++) {
long long g = __gcd(n, j); // chú ý: gcd(n, 0) = n, đúng cho phép đơn vị
total = (total + power(m, g, MOD)) % MOD;
}
// chia cho n trong modulo = nhân nghịch đảo của n
return total % MOD * inv(n % MOD, MOD) % MOD;
}
Tối ưu xuống bằng cách gom các theo giá trị . Số có (với ) đúng bằng (Euler totient):
long long count_necklace_fast(long long n, long long m) {
long long total = 0;
for (long long d = 1; d * d <= n; d++) {
if (n % d) continue;
long long d2 = n / d;
// ước d -> có phi(n/d) phép xoay cho gcd = d
total = (total + (long long)phi(n / d) % MOD * power(m, d, MOD)) % MOD;
if (d != d2) // ước còn lại n/d, tránh đếm đôi khi d == n/d
total = (total + (long long)phi(d) % MOD * power(m, d2, MOD)) % MOD;
}
return total % MOD * inv(n % MOD, MOD) % MOD;
}
Cấu hình tổng quát qua Polya — cycle index. Khi biết hoán vị vị trí của từng , đếm số chu trình rồi cộng :
// perms[g] = hoán vị vị trí của phép thứ g (perms[g][i] = ảnh của vị trí i)
long long polya_count(const vector<vector<int>>& perms, long long m) {
int N = perms[0].size();
long long total = 0;
for (const auto& p : perms) {
vector<char> seen(N, 0);
long long cyc = 0;
for (int i = 0; i < N; i++) if (!seen[i]) { // đếm số chu trình
cyc++;
for (int j = i; !seen[j]; j = p[j]) seen[j] = 1;
}
total = (total + power(m, cyc, MOD)) % MOD; // mỗi chu trình tự do 1 màu
}
return total % MOD * inv((long long)perms.size() % MOD, MOD) % MOD;
}
Độ phức tạp
- Vòng cổ trực tiếp
count_necklace: vòng lặp phép xoay, mỗi phép mộtpowervà một__gcd, tổng thời gian, bộ nhớ. Đủ cho . - Bản tối ưu
count_necklace_fast: duyệt ước qua , mỗi ước mộtpowervà mộtphi. Nếu sàng/đặt totient sẵn thì gần ; bộ nhớ (hoặc để phân tích thừa số cho ). - Polya tổng quát
polya_count: mỗi phép duyệt toàn bộ vị trí một lần để đếm chu trình () cộng mộtpower. Với phép: thời gian, bộ nhớ cho mảngseen. Đây là nút thắt khi lớn (vd thì không liệt kê nổi — phải dùng công thức đóng cho cycle index thay vì duyệt).
Lý do nhanh: thay vì cấu hình, ta chỉ chạy trên các phép biến đổi của nhóm; mỗi phép quy về một bài đếm chu trình rẻ tiền.
⚠️ Lỗi thường gặp
- Tràn số khi nhân/luỹ thừa. rất lớn; phải lấy modulo sau mỗi phép nhân và dùng
long long. Trongpower, bỏ quên% modở bướca = a * asẽ trànlong longngay khi . - Chia cho trực tiếp. Trong số học modulo không được viết
total / n. Phải nhân nghịch đảoinv(n). Quên điều này cho kết quả sai hoàn toàn dù tổng đúng. - Nghịch đảo khi MOD không nguyên tố / không nguyên tố cùng nhau.
invqua Fermat chỉ đúng khi nguyên tố và . Nếu là bội của MOD (hiếm nhưng có thể với lớn) thì nghịch đảo không tồn tại — khi đó tổng (chưa modulo) vẫn chia hết cho trên , nên hãy chia trước khi lấy modulo bằng số học lớn, hoặc dùng MOD khác. - cho phép đơn vị. Phép xoay có (mọi vị trí là chu trình riêng → ).
__gcd(n, 0)trả về là đúng; đừng "sửa" thành 1 — nhiều cài đặt cũ épj==0 ? n : jlà thừa và dễ gây nhầm. - Quên một loại phép trong nhóm. Với nhóm hai mặt (dihedral) phải cộng cả phép xoay và phép lật, rồi chia cho . Tô màu mặt khối lập phương phải đủ 24 phép quay (1 đơn vị + 6 + 3 + 8 + 6). Thiếu một lớp phép → đáp án sai.
- Lật của đa giác chẵn vs lẻ khác nhau. Với : lẻ thì mỗi trục lật qua 1 đỉnh và 1 cạnh giữa, fix ; chẵn có hai loại trục (qua 2 đỉnh → , qua 2 cạnh → ). Dùng nhầm công thức cho parity sai là bẫy phổ biến.
- Modulo âm. Sau các phép trừ (vd trong biến thể trọng số), kết quả có thể âm; luôn chuẩn hoá
(x % MOD + MOD) % MOD.
Biến thể / Mở rộng
- Nhóm hai mặt (vòng cổ có lật, bracelet): gồm xoay + lật. Phần xoay như trên; phần lật phụ thuộc parity của như mục lỗi.
- Tô màu vật thể 3D: nhóm quay khối lập phương , khối tứ diện ... mỗi lớp phép quay có số chu trình riêng trên mặt/đỉnh/cạnh.
- Polya có trọng số: thay vì mỗi màu đếm như nhau, gán biến cho từng màu và thay vào cycle index để đếm số cấu hình có chính xác số lượng mỗi màu cho trước.
- Đếm đồ thị/cây không nhãn: nhóm là tác động lên các cặp đỉnh; cycle index của tác động trên cạnh cho công thức đếm đồ thị không đẳng cấu.
- Kiến thức nền hữu ích: nghịch đảo modulo, hàm Euler — xem thêm trang Toán học.
Bài tập luyện
- Đếm hoán vị phân biệt (cntperms) — (Amateur) làm quen số học modulo, giai thừa và nghịch đảo modulo — bộ công cụ bắt buộc trước khi chia cho trong Burnside.
- Số vòng hoán vị (permrounds) — (Intermediate) phân rã hoán vị thành chu trình và lấy LCM độ dài; đúng cấu trúc chu trình quyết định số điểm bất động trong Burnside/Polya.
- Đếm cặp nguyên tố cùng nhau (cntcopair) — (Advanced) rèn kỹ thuật đếm theo và sàng/totient — chính là cơ chế gom giúp tối ưu công thức vòng cổ.
- Đếm vòng cổ (cntnecklace) — (Advanced) áp dụng trực tiếp Burnside: đếm vòng hạt màu phân biệt khi xoay, đúng công thức với .