Wiki › Thuật toán › Toán học › Nguyên lý bao hàm loại trừ

Nguyên lý bao hàm loại trừ

huunguyen huunguyen Updated Tháng 9. 30, 2026 Đọc 19 phút 5 lượt xem

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 A1,…,Ak (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ả 2 lẫn 3" dễ đếm hơn "chia hết cho 2 hoặc 3". Với k tập nhỏ, duyệt 2k tập con bằng bitmask cho lời giải O(2k·k), 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 1018 phần tử).

Trong cả trang, k 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 |A|+|B| thì phần giao A∩B bị đếm hai lần, nên trừ đi một lần:

|A∪B|=|A|+|B|−|A∩B|.

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 3 lần rồi trừ 3 lần, tức là đang bị đếm 0 lần. Phải cộng lại:

|A∪B∪C|=|A|+|B|+|C|−|A∩B|−|A∩C|−|B∩C|+|A∩B∩C|.

  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 k tập A1,…,Ak và với mỗi tập con khác rỗng S⊆{1,…,k}, đặt AS=⋂i∈SAi:

|A1∪…∪Ak|=∑∅≠S(−1)|S|+1|AS|.

Trong thực tế, dạng hay dùng hơn là đếm phần tử của tập vũ trụ U không thuộc tập nào (quy ước A∅=U):

|U⧵(A1∪…∪Ak)|=∑S⊆{1..k}(−1)|S||AS|.

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 Ai là tập vi phạm điều kiện thứ i. Việc đếm "vi phạm đồng thời một nhóm điều kiện S" 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ử x thuộc đúng t tập. Ở dạng "không thuộc tập nào", x góp vào số hạng của S khi và chỉ khi S là tập con của t tập chứa x. Có (tj) cách chọn S gồm j tập như vậy, nên tổng đóng góp của x là

∑j=0t(−1)j(tj)=(1−1)t={1t=00t≥1.

Đây là khai triển nhị thức Newton của (1−1)t. Phần tử không thuộc tập nào (t=0) chỉ nằm trong số hạng S=∅ nên được đếm đúng 1 lần; mọi phần tử "xấu" bị triệt tiêu hoàn toàn. Dạng hợp chỉ là |U| trừ đi dạng này.

Ví dụ chạy tay

Đếm số x∈[1,30] không chia hết cho 2, 3, 5. Gọi Ap là tập các số chia hết cho p. Vì 2,3,5 nguyên tố cùng nhau, "chia hết cho mọi số trong S" tương đương "chia hết cho tích của chúng", nên |AS|=⌊30/∏p∈Sp⌋.

Duyệt 23=8 mặt nạ bit (bit 0 ứng với 2, bit 1 với 3, bit 2 với 5):

mask S tích ⌊30/tích⌋ dấu đóng góp
000 ∅ 1 30 + +30
001 {2} 2 15 − −15
010 {3} 3 10 − −10
100 {5} 5 6 − −6
011 {2,3} 6 5 + +5
101 {2,5} 10 3 + +3
110 {3,5} 15 2 + +2
111 {2,3,5} 30 1 − −1

Tổng: 30−31+10−1=8. Kiểm tra: đó là 1,7,11,13,17,19,23,29.

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ố x∈[1,n] 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 n tới 1018, nên dừng sớm khi tích vượt n (khi đó ⌊n/tích⌋=0).

#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 n−countFree(n). Muốn đếm trên đoạn [L,R], lấy countFree(R)−countFree(L−1).

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: O(2k·k). Có 2k tập con, mỗi tập con tính tích trong O(k). Có thể giảm còn O(2k) 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ớ: O(k) với cách tính lại tích, O(2k) nếu lưu giá trị cho mọi mặt nạ.
  • Giới hạn thực tế: 2k chỉ chấp nhận được khi k≲20 đến 25. May mắn là số lượng ước nguyên tố phân biệt của một số ≤1018 không quá 15 (tích 15 số nguyên tố đầu tiên 2·3⋯47≈6,1·1017, nhân thêm 53 là vượt 1018), và của một số ≤106 không quá 7, nên bài "đếm số nguyên tố cùng nhau với p" luôn chạy nhanh.

⚠️ Lỗi thường gặp

  • Dừng ở giao đôi một. Triệu chứng: đúng với k=2, sai khi k≥3. 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 |S|. 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ụ 4 và 6), "chia hết cho cả hai" là chia hết cho lcm(4,6)=12, không phải 24. 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 lcm thay cho tích (và vẫn kiểm tra tràn).
  • Tràn số khi nhân tích. Với n≤1018, tích vài số nguyên tố lớn vượt long long. Kiểm tra prod > n / p trước khi nhân; khi vượt thì số hạng bằng 0, 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 S=∅ mang dấu + và dấu là (−1)|S|; dạng "hợp" bỏ S=∅ và dấu là (−1)|S|+1. Trộn hai dạng làm lệch đúng |U| hoặc đổi dấu toàn bộ. Tự kiểm tra bằng một input tay nhỏ như n=30 ở trên.
  • Modulo âm. Khi đáp án lấy theo modulo, (s - term) % MOD có 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 12=2·2·3 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.
  • k quá lớn. Với k=40 thì 240 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 |S| (khi |AS| chỉ phụ thuộc |S|, 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 2k phần tử quá lớn. Cách lưu giá trị cho mọi mặt nạ tốn 2k ô nhớ nhân kích thước mỗi ô. Ví dụ mảng Rect (bốn long long, 32 byte) ở mục giao hình chữ nhật: k=16 tốn 2 MB, nhưng k=20 đã tốn 32 MB. Khi k 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 O(k) bộ nhớ.

Biến thể / Mở rộng

Hoán vị không điểm cố định (derangement)

Đếm số hoán vị p của {1,…,n} mà pi≠i với mọi i. Gọi Ai là tập hoán vị có pi=i. Cố định một tập S gồm j vị trí thì các vị trí còn lại hoán vị tùy ý: |AS|=(n−j)!, chỉ phụ thuộc j=|S|. Gộp (nj) tập con cùng kích thước:

Dn=∑j=0n(−1)j(nj)(n−j)!=n!∑j=0n(−1)jj!.

Nhờ gộp theo kích thước, 2n số hạng thu về n+1 số hạng, nên tính một giá trị Dn mất O(n) (sau khi có sẵn giai thừa, xem Tổ hợp). Nếu cần cả bảng D0..Dn mà gọi công thức này cho từng m thì tốn O(n2); thay vào đó dùng công thức truy hồi tương đương Dn=(n−1)(Dn−1+Dn−2), cả bảng trong O(n). Dãy đầu: D0..D7=1,0,1,2,9,44,265,1854. Tỷ lệ Dn/n!→1/e≈0,3679.

// 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ụ m vị trí "nguy hiểm" trong q vị trí trống, các giá trị còn thiếu cũng có q giá trị). Khi đó đáp án là ∑j=0m(−1)j(mj)(q−j)!.

Từ "ít nhất" sang "đúng r" (nghịch đảo nhị thức)

Nhiều bài hỏi số đối tượng có đúng r tính chất (trong k tính chất). Đặt

gj=∑|S|=j|AS|

(tổng theo mọi nhóm j tính chất, mỗi đối tượng có t tính chất được đếm (tj) lần, không phải "số đối tượng có ít nhất j tính chất"). Nếu et là số đối tượng có đúng t tính chất thì gj=∑t≥j(tj)et, và nghịch đảo nhị thức cho

er=∑j≥r(−1)j−r(jr)gj.

Với r=0 đây chính là công thức "không thuộc tập nào". Ví dụ với hoán vị n=6 và tính chất "pi=i": gj=(6j)(6−j)! cho g=[720,720,360,120,30,6,1], và công thức trên cho số hoán vị có đúng r đ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: er=(6r)D6−r, ví dụ e2=15·9=135.)

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 f(v) = số đối tượng có đặc trưng đúng bằng v (ví dụ OR của tập được chọn bằng đúng v), nhưng chỉ dễ tính

g(m)=∑v⊆mf(v)

(đặc trưng là tập con của m). Khi đó bao hàm loại trừ cho

f(v)=∑m⊆v(−1)|v|−|m|g(m).

Tính trực tiếp là O(3B); 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 f(v) có trong O(B·2B). Ví dụ: đếm, với mọi v, số tập con khác rỗng của dãy a có OR đúng bằng v. OR là tập con của m khi và chỉ khi từng phần tử là tập con của m, nên g(m)=2cnt(m)−1 với cnt(m) là số ai⊆m.

// 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 a=[1,2,3,1], B=2:

 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ố 1: {1},{1′},{1,1′}, đúng 3.

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 0.

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ó gcd=1 bằng ∑dμ(d)·(số cặp cùng chia hết cho d). Hàm μ(d) chính là hệ số bao hàm loại trừ (−1)|S| trên tập ước nguyên tố của d (và bằng 0 khi d 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 k 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 k 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: [maxx1,minx2]×[maxy1,miny2], rỗng khi đầu lớn hơn cuối. Vậy mỗi số hạng |AS| tính trong O(1): số ô là (x2−x1+1)(y2−y1+1). 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 (x2−x1)(y2−y1) và coi giao rỗng khi x1≥x2 hoặc y1≥y2.

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ụ: A=[1,3]×[1,3], B=[2,4]×[2,4], C=[3,5]×[1,2] (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 O(1), tổng O(2k) thời gian và O(2k) bộ nhớ. Khi k 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 x1+…+xn=s với 0≤xi≤fi. Không có chặn trên thì đáp án là (s+n−1n−1) (chia kẹo Euler). Gọi Ai là tập nghiệm có xi>fi; đặt xi′=xi−(fi+1) thì |AS|=(s−∑i∈S(fi+1)+n−1n−1) (bằng 0 nếu tổng bị âm). Duyệt 2n tập con là xong. Khi s rất lớn, tổ hợp (Nn−1) với n nhỏ tính trực tiếp bằng tích n−1 thừa số nhân nghịch đảo của (n−1)!, 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ó gcd=1 bằng cách đếm số phần tử chia hết cho từng d rồi cộng trừ theo μ(d): 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ố y≤M nguyên tố cùng nhau với p bằng bao hàm loại trừ trên tối đa 7 ước nguyên tố của p, rồi tìm kiếm nhị phân phần tử thứ k.
  • 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 25−1 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ể: 220 tập hộp bị vượt, mỗi tập một tổ hợp (Nn−1) với N tới 1014.
Đề 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 1" 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 5) biến tổng khó thành vài tổng "chia hết cho d" 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 5 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 v" khó đếm, nhưng "OR là tập con bit của m" thì dễ; tìm cách đi từ cái dễ sang cái khó cho mọi v cùng lúc.
  • Thiết bị thông minh (voi23_sdev) (Kỳ cựu) VOI 2023: riêng subtask N≤15 có thể làm bằng bao hàm loại trừ trên 2N tập hình chữ nhật (giao vẫn là hình chữ nhật, số điểm dưới đường x+y=D có công thức đóng); lời giải đầy đủ cần kỹ thuật khác.
Trang này có hữu ích không? Đăng nhập để trả lời

Bình luận

Không có bình luận tại thời điểm này.

gnatmake 12.2.0 a68g 3.1.2 nasm 2.16.1 as_x64 2.47 awk 1.3.4 gcc 16.2.0 dotnet 10.0.402 g++ 16.2.0 g++-themis 16.2.0 g++17 16.2.0 g++20 16.2.0 g++23 16.2.0 clang++ 22.1.8 dmd 2.113.0 dart 3.13.6 gforth 0.7.3 gfortran 15.3.0 go 1.27.3 groovyc 5.1.3 javac 26.0.2.1 node 26.12.1 julia 1.13.2 kotlinc 2.4.21 lean 4.35.0 sbcl 2.2.9 lua 5.5.1 nim 2.2.12 fpc 3.2.2 fpc-themis 3.2.2 perl 5.42.3 php 8.5.12 pike 8.0 swipl 9.0.4 pypy3 7.3.23 python3 3.15.1 racket 8.7 ruby 4.0.8 rustc 1.99.1 csc 5.3.0 ctoj-scratch 0.0.1 sed 4.9 swiftc 6.4.1 tclsh 8.6 bun 1.4.4 deno 2.9.8 v 0.5.2 zig 0.17.0