Nguyên lý bao hàm loại trừ
Nguyên lý bao hàm loại trừ (inclusion-exclusion principle) là công cụ đếm số phần tử thuộc ít nhất một trong các tập (hoặc không thuộc tập nào) khi các tập giao nhau chằng chịt. Thay vì đếm trực tiếp hợp (thường rất khó), ta chỉ cần đếm các giao, vốn dễ hơn nhiều: "chia hết cho cả lẫn " dễ đếm hơn "chia hết cho hoặc ". Với tập nhỏ, duyệt tập con bằng bitmask cho lời giải , trong khi cách ngây thơ là duyệt từng phần tử của tập vũ trụ (có thể tới phần tử).
Trong cả trang, luôn là số tập (số tính chất) đang xét.
Ý tưởng / Trực giác
Từ hai tập đến ba tập
Với hai tập, cộng thì phần giao bị đếm hai lần, nên trừ đi một lần:
Với ba tập, sau khi cộng ba tập và trừ ba giao đôi một, phần tử nằm trong cả ba tập đã được cộng lần rồi trừ lần, tức là đang bị đếm lần. Phải cộng lại:
Số lần mỗi vùng được đếm qua từng bước (ba tập A, B, C)
vùng +đơn -đôi +ba tổng
chỉ A 1 0 0 1
A và B (không C) 2 -1 0 1
cả A, B, C 3 -3 +1 1
Công thức tổng quát
Với tập và với mỗi tập con khác rỗng , đặt :
Trong thực tế, dạng hay dùng hơn là đếm phần tử của tập vũ trụ không thuộc tập nào (quy ước ):
Mẹo nhận diện: đề hỏi "không có tính chất xấu nào" (không chia hết cho số nào, không có điểm cố định, không vượt giới hạn của hộp nào...) thì gọi là tập vi phạm điều kiện thứ . Việc đếm "vi phạm đồng thời một nhóm điều kiện " thường có công thức đóng.
Vì sao mỗi phần tử được đếm đúng một lần
Xét một phần tử thuộc đúng tập. Ở dạng "không thuộc tập nào", góp vào số hạng của khi và chỉ khi là tập con của tập chứa . Có cách chọn gồm tập như vậy, nên tổng đóng góp của là
Đây là khai triển nhị thức Newton của . Phần tử không thuộc tập nào () chỉ nằm trong số hạng nên được đếm đúng lần; mọi phần tử "xấu" bị triệt tiêu hoàn toàn. Dạng hợp chỉ là trừ đi dạng này.
Ví dụ chạy tay
Đếm số không chia hết cho , , . Gọi là tập các số chia hết cho . Vì nguyên tố cùng nhau, "chia hết cho mọi số trong " tương đương "chia hết cho tích của chúng", nên .
Duyệt mặt nạ bit (bit ứng với , bit với , bit với ):
| mask | tích | dấu | đóng góp | ||
|---|---|---|---|---|---|
000 |
1 | 30 | |||
001 |
2 | 15 | |||
010 |
3 | 10 | |||
100 |
5 | 6 | |||
011 |
6 | 5 | |||
101 |
10 | 3 | |||
110 |
15 | 2 | |||
111 |
30 | 1 |
Tổng: . Kiểm tra: đó là .
Theo dõi riêng hai số để thấy "đếm đúng một lần":
x = 6 (chia hết cho 2 và 3, t = 2)
có mặt ở: {} {2} {3} {2,3}
dấu: +1 -1 -1 +1 => tổng 0 (bị loại, đúng)
x = 30 (chia hết cho 2, 3, 5, t = 3)
có mặt ở: {} {2} {3} {5} {2,3} {2,5} {3,5} {2,3,5}
dấu: +1 -1 -1 -1 +1 +1 +1 -1 => tổng 0
x = 7 (t = 0)
có mặt ở: {} => tổng 1
Cài đặt
C++
Đếm số không chia hết cho bất kỳ số nguyên tố nào trong danh sách. Tích có thể tràn long long khi tới , nên dừng sớm khi tích vượt (khi đó ).
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// Count x in [1, n] divisible by none of the given primes.
ll countFree(ll n, const vector<ll>& primes) {
int k = primes.size();
ll result = 0;
for (int mask = 0; mask < (1 << k); mask++) {
ll prod = 1;
bool tooBig = false;
for (int i = 0; i < k; i++)
if (mask >> i & 1) {
if (prod > n / primes[i]) { tooBig = true; break; } // product exceeds n: term is 0
prod *= primes[i];
}
if (tooBig) continue;
ll term = n / prod; // numbers divisible by all chosen primes
if (__builtin_popcount(mask) % 2 == 0) result += term;
else result -= term;
}
return result;
}
int main() {
ll n; int k;
cin >> n >> k;
vector<ll> primes(k);
for (auto& p : primes) cin >> p;
cout << countFree(n, primes) << "\n"; // input "30 3 2 3 5" -> 8
}
Muốn đếm số chia hết cho ít nhất một số nguyên tố, lấy . Muốn đếm trên đoạn , lấy .
Python
Số nguyên Python không tràn nên không cần kiểm tra tích.
def count_free(n, primes):
"""Count x in [1, n] divisible by none of the primes."""
k = len(primes)
result = 0
for mask in range(1 << k):
prod = 1
for i in range(k):
if mask >> i & 1:
prod *= primes[i]
sign = -1 if bin(mask).count("1") % 2 else 1
result += sign * (n // prod) # Python ints never overflow
return result
n, k, *primes = map(int, open(0).read().split())
print(count_free(n, primes))
Độ phức tạp
- Thời gian: . Có tập con, mỗi tập con tính tích trong . Có thể giảm còn bằng cách lưu giá trị của từng mặt nạ và suy ra
val[mask]từval[mask & (mask - 1)](bỏ bit thấp nhất), như trong mục giao hình chữ nhật bên dưới. - Bộ nhớ: với cách tính lại tích, nếu lưu giá trị cho mọi mặt nạ.
- Giới hạn thực tế: chỉ chấp nhận được khi đến . May mắn là số lượng ước nguyên tố phân biệt của một số không quá (tích số nguyên tố đầu tiên , nhân thêm là vượt ), và của một số không quá , nên bài "đếm số nguyên tố cùng nhau với " luôn chạy nhanh.
⚠️ Lỗi thường gặp
- Dừng ở giao đôi một. Triệu chứng: đúng với , sai khi . Công thức phải đi qua mọi kích thước tập con, dấu đổi luân phiên theo . Duyệt bitmask thì không bao giờ quên tầng nào.
- Dùng tích thay cho BCNN. Nếu các số không đôi một nguyên tố cùng nhau (ví dụ và ), "chia hết cho cả hai" là chia hết cho , không phải . Cách tránh: phân tích ra thừa số nguyên tố phân biệt trước, hoặc tính
lcmthay cho tích (và vẫn kiểm tra tràn). - Tràn số khi nhân tích. Với , tích vài số nguyên tố lớn vượt
long long. Kiểm traprod > n / ptrước khi nhân; khi vượt thì số hạng bằng , bỏ qua. - Sai dấu hoặc lẫn hai dạng công thức. Dạng "không thuộc tập nào" có số hạng mang dấu và dấu là ; dạng "hợp" bỏ và dấu là . Trộn hai dạng làm lệch đúng hoặc đổi dấu toàn bộ. Tự kiểm tra bằng một input tay nhỏ như ở trên.
- Modulo âm. Khi đáp án lấy theo modulo,
(s - term) % MODcó thể âm trong C++. Viết(s - term % MOD + MOD) % MOD. - Trùng phần tử trong danh sách. Nếu danh sách số nguyên tố có lặp (ví dụ đọc thừa số của mà không khử trùng), mỗi số nguyên tố bị coi là hai tập khác nhau và kết quả sai. Khử trùng trước khi duyệt.
- quá lớn. Với thì tập con là không khả thi. Khi đó cần gộp các tập con có cùng số hạng: theo kích thước (khi chỉ phụ thuộc , như derangement), theo ước số (hàm Möbius), hoặc theo mặt nạ giá trị (SOS DP). Xem mục Biến thể.
- Lưu mảng phần tử quá lớn. Cách lưu giá trị cho mọi mặt nạ tốn ô nhớ nhân kích thước mỗi ô. Ví dụ mảng
Rect(bốnlong long, byte) ở mục giao hình chữ nhật: tốn MB, nhưng đã tốn MB. Khi lớn, duyệt tập con bằng đệ quy (DFS thêm dần từng tập, giữ giao hiện tại trên ngăn xếp) chỉ tốn bộ nhớ.
Biến thể / Mở rộng
Hoán vị không điểm cố định (derangement)
Đếm số hoán vị của mà với mọi . Gọi là tập hoán vị có . Cố định một tập gồm vị trí thì các vị trí còn lại hoán vị tùy ý: , chỉ phụ thuộc . Gộp tập con cùng kích thước:
Nhờ gộp theo kích thước, số hạng thu về số hạng, nên tính một giá trị mất (sau khi có sẵn giai thừa, xem Tổ hợp). Nếu cần cả bảng mà gọi công thức này cho từng thì tốn ; thay vào đó dùng công thức truy hồi tương đương , cả bảng trong . Dãy đầu: . Tỷ lệ .
// One D_n mod MOD by inclusion-exclusion, O(n); needs fact[], C(a, b).
ll derange(int n) {
ll d = 0;
for (int j = 0; j <= n; j++) {
ll term = C(n, j) * fact[n - j] % MOD; // sum of |A_S| over all S with |S| = j
d = (j % 2 == 0) ? (d + term) % MOD : (d - term + MOD) % MOD;
}
return d;
}
// Whole table D_0..D_n in O(n) via the recurrence.
D[0] = 1;
if (n >= 1) D[1] = 0;
for (int m = 2; m <= n; m++) D[m] = (ll)(m - 1) * ((D[m - 1] + D[m - 2]) % MOD) % MOD;
Biến thể hay gặp: chỉ một số vị trí bị cấm (ví dụ vị trí "nguy hiểm" trong vị trí trống, các giá trị còn thiếu cũng có giá trị). Khi đó đáp án là .
Từ "ít nhất" sang "đúng " (nghịch đảo nhị thức)
Nhiều bài hỏi số đối tượng có đúng tính chất (trong tính chất). Đặt
(tổng theo mọi nhóm tính chất, mỗi đối tượng có tính chất được đếm lần, không phải "số đối tượng có ít nhất tính chất"). Nếu là số đối tượng có đúng tính chất thì , và nghịch đảo nhị thức cho
Với đây chính là công thức "không thuộc tập nào". Ví dụ với hoán vị và tính chất "": cho , và công thức trên cho số hoán vị có đúng điểm cố định:
r 0 1 2 3 4 5 6
e_r 265 264 135 40 15 0 1 (tổng = 720 = 6!)
(Kiểm chứng: , ví dụ .)
Bao hàm loại trừ trên tập con: SOS DP và nghịch đảo Möbius
Khi các "tập" là các bit của một mặt nạ, bao hàm loại trừ trở thành phép biến đổi trên dàn tập con. Giả sử ta cần = số đối tượng có đặc trưng đúng bằng (ví dụ OR của tập được chọn bằng đúng ), nhưng chỉ dễ tính
(đặc trưng là tập con của ). Khi đó bao hàm loại trừ cho
Tính trực tiếp là ; dùng quy hoạch động tổng trên tập con (SOS DP) với phép trừ thay cho phép cộng thì mọi có trong . Ví dụ: đếm, với mọi , số tập con khác rỗng của dãy có OR đúng bằng . OR là tập con của khi và chỉ khi từng phần tử là tập con của , nên với là số .
// cnt has size 2^B, cnt[x] = how many a_i equal x. pw[i] = 2^i mod MOD.
for (int b = 0; b < B; b++) // zeta: cnt[m] = #a_i that are submasks of m
for (int m = 0; m < (1 << B); m++)
if (m >> b & 1) cnt[m] += cnt[m ^ (1 << b)];
vector<ll> f(1 << B);
for (int m = 0; m < (1 << B); m++) f[m] = (pw[cnt[m]] - 1 + MOD) % MOD; // g(m)
for (int b = 0; b < B; b++) // Mobius: undo the zeta transform
for (int m = 0; m < (1 << B); m++)
if (m >> b & 1) f[m] = (f[m] - f[m ^ (1 << b)] + MOD) % MOD;
// now f[v] = number of non-empty subsets with OR exactly v
Chạy tay với , :
m (nhị phân) 00 01 10 11
cnt(m) 0 2 1 4 (a_i là tập con của m)
g(m)=2^cnt-1 0 3 1 15
f(v) 0 3 1 11 (11 = 15 - 3 - 1 + 0)
Kiểm tra: tập con có OR bằng 01 chỉ gồm các số : , đúng .
Dạng đối xứng với AND (đếm siêu tập thay cho tập con) xuất hiện trong bài Jzzhu và những con số (jzzhunum): đếm nhóm có AND bằng .
Tương tự, với điều kiện chia hết thay cho tập con bit, ta được nghịch đảo Möbius trên ước: số cặp có bằng . Hàm chính là hệ số bao hàm loại trừ trên tập ước nguyên tố của (và bằng khi có ước chính phương). Xem Hàm nhân tính.
Giao các hình chữ nhật (bài hình học)
Đếm số ô lưới nằm trong hợp của hình chữ nhật song song trục (mỗi hình cho bằng chỉ số ô, hai biên đều bao gồm), với nhỏ. Giao của các hình chữ nhật song song trục vẫn là một hình chữ nhật: , rỗng khi đầu lớn hơn cuối. Vậy mỗi số hạng tính trong : số ô là . Nếu cần tổng giá trị ô thay cho số ô, thay công thức này bằng tổng tiền tố hai chiều. Với hình chữ nhật tọa độ thực (tính diện tích), cách làm giữ nguyên, chỉ đổi số hạng thành và coi giao rỗng khi hoặc .
struct Rect { ll x1, y1, x2, y2; }; // cells x1..x2, y1..y2 (inclusive)
// Number of grid cells covered by at least one rectangle.
// Keep k small (e.g. k <= 16): inter[] holds 2^k Rects of 32 bytes (k = 20 needs 32 MB).
ll unionCells(const vector<Rect>& r) {
int k = r.size();
vector<Rect> inter(1 << k); // inter[mask] = intersection of rectangles in mask
ll total = 0;
for (int mask = 1; mask < (1 << k); mask++) {
int low = __builtin_ctz(mask); // lowest chosen rectangle
int rest = mask & (mask - 1); // mask without it (already computed)
if (rest == 0) inter[mask] = r[low];
else {
Rect a = inter[rest], b = r[low];
inter[mask] = {max(a.x1, b.x1), max(a.y1, b.y1), min(a.x2, b.x2), min(a.y2, b.y2)};
}
Rect c = inter[mask];
if (c.x1 > c.x2 || c.y1 > c.y2) continue; // empty intersection
ll cells = (c.x2 - c.x1 + 1) * (c.y2 - c.y1 + 1); // inclusive cell count
total += (__builtin_popcount(mask) & 1) ? cells : -cells;
}
return total;
}
Ví dụ: , , (tọa độ ô).
y
4 . B B B . |A| = 9, |B| = 9, |C| = 6
3 A AB AB B . |A∩B| = 4, |A∩C| = 2, |B∩C| = 2
2 A AB ABC BC C |A∩B∩C| = 1 (ô x = 3, y = 2)
1 A A AC C C
1 2 3 4 5 x hợp = 24 - 8 + 1 = 17 ô
Nhờ lấy giao tăng dần theo mask & (mask - 1), mỗi mặt nạ chỉ tốn , tổng thời gian và bộ nhớ. Khi lớn (hàng nghìn hình) thì bao hàm loại trừ không dùng được nữa; chuyển sang đường quét (sweep line) với cây phân đoạn.
Bao hàm loại trừ cho bài chia kẹo có chặn trên
Đếm số nghiệm nguyên với . Không có chặn trên thì đáp án là (chia kẹo Euler). Gọi là tập nghiệm có ; đặt thì (bằng nếu tổng bị âm). Duyệt tập con là xong. Khi rất lớn, tổ hợp với nhỏ tính trực tiếp bằng tích thừa số nhân nghịch đảo của , hoặc bằng định lý Lucas khi modulo nhỏ.
Bài tập luyện
Luyện kỹ thuật
- Đếm cặp nguyên tố cùng nhau (cntcopair) (Nâng cao) đếm cặp có bằng cách đếm số phần tử chia hết cho từng rồi cộng trừ theo : bao hàm loại trừ trên các ước nguyên tố, gộp theo ước số.
- Hoán vị không điểm cố định (permnofp) (Kỳ cựu) biến thể của derangement: chỉ những vị trí trống mà giá trị cùng chỉ số cũng đang thiếu mới có thể thành điểm cố định, bao hàm loại trừ theo số vị trí "nguy hiểm" bị vi phạm.
- Dãy số nguyên tố cùng nhau (kthcopr) (Kỳ cựu) đếm số nguyên tố cùng nhau với bằng bao hàm loại trừ trên tối đa ước nguyên tố của , rồi tìm kiếm nhị phân phần tử thứ .
- Tính tương thích bò (cowcompat) (Kỳ cựu) bitmask trên tập thuộc tính: với mỗi con bò, duyệt tập con khác rỗng các vị kem, dùng bảng băm đếm số bò chứa tập con đó; cộng trừ theo kích thước tập con ra số cặp có chung ít nhất một vị, lấy phần bù.
- Devu và những bông hoa (devuflower) (Chuyên gia) bài chia kẹo có chặn trên ở mục Biến thể: tập hộp bị vượt, mỗi tập một tổ hợp với tới .
Đề VOI
- Đường đi trên lưới (voi06_path) (Nâng cao) VOI 2006: khung chính là quy hoạch động đếm đường theo ô; bao hàm loại trừ chỉ là bước tăng tốc trong công thức quy hoạch động đó: điều kiện "ước chung lớn hơn " nghĩa là có chung ít nhất một ước nguyên tố, nên bao hàm loại trừ trên tập ước nguyên tố (tối đa ) biến tổng khó thành vài tổng "chia hết cho " dễ duy trì.
- Phần thưởng (voi18_bonus) (Nâng cao) VOI 2018: mỗi phương án là hợp của tối đa hình vuông; hãy nghĩ xem giao của các hình vuông có hình dạng gì và tính tổng trên nó nhanh thế nào.
- Phép toán OR (voi21_or) (Nâng cao) VOI 2021: "OR đúng bằng " khó đếm, nhưng "OR là tập con bit của " thì dễ; tìm cách đi từ cái dễ sang cái khó cho mọi cùng lúc.
- Thiết bị thông minh (voi23_sdev) (Kỳ cựu) VOI 2023: riêng subtask có thể làm bằng bao hàm loại trừ trên tập hình chữ nhật (giao vẫn là hình chữ nhật, số điểm dưới đường có công thức đóng); lời giải đầy đủ cần kỹ thuật khác.
Bình luận