Principiul Includerii și Excluderii (PInEx)
PInEx e o tehnică de numărare care rezolvă
probleme de forma: “câte elemente satisfac cel puțin
una dintre condițiile
P1, P2, ..., Pn?”
Ideea intuitivă: când adunăm mărimile mulțimilor, dublăm elementele din intersecții. Trebuie să scădem ce am adunat în plus. Dar scăzând perechile, scădem prea mult din tripletele comune. Trebuie să le adăugăm înapoi. Și așa mai departe.
Intuiția cu diagrame Venn
2 mulțimi
┌──────────────┐
│ A │
│ ┌─────────┼─────┐
│ │ A ∩ B │ │
│ │ │ B │
└────┼─────────┘ │
│ │
└───────────────┘
Dacă adunăm |A| + |B|, elementul din
A ∩ B a fost numărat de două ori.
Trebuie să-l scădem o dată.
|A ∪ B| = |A| + |B| - |A ∩ B|
|A| = 10
|B| = 8
|A ∩ B| = 3
|A ∪ B| = 10 + 8 - 3 = 15
3 mulțimi
Adunăm |A| + |B| + |C|. Fiecare element din:
- O singură mulțime → numărat 1 dată ✓
- Exact 2 mulțimi (A∩B, A∩C, B∩C fără A∩B∩C) → numărat 2 ori ✗ (trebuie 1)
- Toate 3 (A∩B∩C) → numărat 3 ori ✗ (trebuie 1)
Scădem |A∩B| + |A∩C| + |B∩C|. Acum:
- Elemente exact în 2: numărat 2-1 = 1 dată ✓
- Elemente în toate 3: numărat 3-3 = 0 dată ✗ (trebuie 1)
Adăugăm |A∩B∩C|:
- Elemente în toate 3: numărat 0+1 = 1 dată ✓
|A ∪ B ∪ C| = |A| + |B| + |C| - |A∩B| - |A∩C| - |B∩C| + |A∩B∩C|
Tabel cu câte ori e numărat fiecare element
Pentru 3 mulțimi:
| Element e în… | +|A| +|B| +|C| | -|A∩B|… | +|A∩B∩C| | Total |
|---|---|---|---|---|
| O singură mulțime (A) | 1 | 0 | 0 | 1 |
| Exact 2 mulțimi (A,B) | 2 | -1 | 0 | 1 |
| Toate 3 (A,B,C) | 3 | -3 | +1 | 1 |
Fiecare element e numărat exact o dată - exact cât trebuie pentru uniune.
Formula generală
Pentru n mulțimi:
|A₁ ∪ A₂ ∪ … ∪ Aₙ| = Σ|Aᵢ| - Σ|Aᵢ ∩ Aⱼ| + Σ|Aᵢ ∩ Aⱼ ∩ Aₖ| - … ± |A₁ ∩ … ∩ Aₙ|
Compact:
|A₁ ∪ ... ∪ Aₙ| = Σ (-1)^(|S|+1) * |∩(i ∈ S) Aᵢ|
unde suma e peste toate submulțimile nevide
S ale lui {1, 2, ..., n}.
Semnul depinde de câte mulțimi intersectăm:
| Nr. mulțimi în intersecție | Semn |
|---|---|
| 1 | + |
| 2 | - |
| 3 | + |
| 4 | - |
| … | alternat |
Semnul e (-1)^(k+1) pentru k
mulțimi.
De ce funcționează? (demonstrația prin binom)
Demonstrația formală a PInEx
Fie x un element care aparține exact
k mulțimi (k >= 1). Cât contribuie
el la formula PInEx?
- E numărat de
kori înΣ|Aᵢ|(la celekmulțimi din care face parte) - E numărat de
C(k, 2)ori înΣ|Aᵢ ∩ Aⱼ|(la fiecare pereche de mulțimi din care face parte) - În general, la sumele de intersecții de
jmulțimi, apare deC(k, j)ori
Semnul pentru intersecții de j mulțimi e
(-1)^(j+1). Deci contribuția totală:
Σ (j=1..k) (-1)^(j+1) * C(k, j)
= -Σ (j=1..k) (-1)^j * C(k, j)
= -(Σ (j=0..k) (-1)^j * C(k, j) - 1)
= -(0 - 1) // binomul lui Newton: (1-1)^k = 0
= 1
Deci orice element din cel puțin o mulțime contribuie exact 1 la formula.
Problema clasică 1: numere divizibile
Enunț: Câte numere de la 1 la
N sunt divizibile cu cel puțin
unul dintre p1, p2, ..., pk (prime
distincte)?
Soluția directă (fără PInEx)
Complicată - trebuie să tratăm suprapunerile manual. Pentru
k prime, devine exponențial de greu.
Cu PInEx
Fie Aᵢ = mulțimea numerelor de la 1 la N
divizibile cu pᵢ.
Observații cheie:
|Aᵢ|= câte numere de la 1 la N sunt divizibile cu pᵢ =N / pᵢ(diviziune întreagă)|Aᵢ ∩ Aⱼ|= câte numere sunt divizibile cu amândouă = divizibile cupᵢ * pⱼ(pentru prime distincte) =N / (pᵢ * pⱼ)- În general, intersecția de
jmulțimi = divizibile cu produsul =N / (pᵢ₁ * pᵢ₂ * ... * pᵢⱼ)
Formula:
Răspuns = ΣN/pᵢ - ΣN/(pᵢ*pⱼ) + ΣN/(pᵢ*pⱼ*pₖ) - ...
Exemplu: N = 30, prime {2, 3, 5}
Numere de la 1 la 30 divizibile cu 2: {2, 4, 6, ..., 30} → 15 numere
Numere divizibile cu 3: {3, 6, 9, ..., 30} → 10 numere
Numere divizibile cu 5: {5, 10, 15, 20, 25, 30} → 6 numere
Numere divizibile cu 6 (=2*3): {6, 12, 18, 24, 30} → 5 numere
Numere divizibile cu 10 (=2*5): {10, 20, 30} → 3 numere
Numere divizibile cu 15 (=3*5): {15, 30} → 2 numere
Numere divizibile cu 30 (=2*3*5): {30} → 1 număr
PInEx:
Răspuns = 15 + 10 + 6 - 5 - 3 - 2 + 1 = 22
22 numere de la 1 la 30 sunt divizibile cu 2, 3 sau 5.
Verificare: numere de la 1 la 30 care nu
sunt divizibile cu niciun prim din {2,3,5}:
{1, 7, 11, 13, 17, 19, 23, 29} → 8 numere. Deci
30 - 8 = 22 ✓
Implementare cu bitmask
Pentru k mulțimi, iterăm prin toate cele
2^k - 1 submulțimi nevide reprezentate ca
bitmask:
#include <fstream>
using namespace std;
ifstream fin("date.in");
ofstream fout("date.out");
int p[21]; // pana la 20 de numere prime
int k;
long long N;
int main()
{
fin >> N >> k;
for (int i = 0; i < k; i++)
fin >> p[i];
long long rez = 0;
// Iteram prin toate submultimile nevide (mask = 1..2^k - 1)
for (int mask = 1; mask < (1 << k); mask++) {
long long produs = 1;
int nrBiti = 0;
bool overflow = false;
for (int i = 0; i < k; i++) {
if (mask & (1 << i)) {
// Verificam overflow inainte sa inmultim
if (produs > N / p[i]) { overflow = true; break; }
produs *= p[i];
nrBiti++;
}
}
if (overflow || produs > N) continue;
if (nrBiti & 1)
rez += N / produs; // semn pozitiv pentru submultimi de dimensiune impara
else
rez -= N / produs; // semn negativ pentru pare
}
fout << rez;
return 0;
}date.in:
30 3
2 3 5date.out:
22Pas cu pas: N=30, primele {2, 3, 5}
Iterăm prin toate cele 2^3 - 1 = 7 submulțimi
nevide:
| Mask | Biți setați | Elemente selectate | Produs | Nr biți | Semn | Contribuție |
|---|---|---|---|---|---|---|
001 |
bit 0 | {2} | 2 | 1 | + |
+30/2 = +15 |
010 |
bit 1 | {3} | 3 | 1 | + |
+30/3 = +10 |
011 |
bits 0,1 | {2, 3} | 6 | 2 | - |
-30/6 = -5 |
100 |
bit 2 | {5} | 5 | 1 | + |
+30/5 = +6 |
101 |
bits 0,2 | {2, 5} | 10 | 2 | - |
-30/10 = -3 |
110 |
bits 1,2 | {3, 5} | 15 | 2 | - |
-30/15 = -2 |
111 |
toți | {2, 3, 5} | 30 | 3 | + |
+30/30 = +1 |
Total:
15 + 10 - 5 + 6 - 3 - 2 + 1 = 22 ✓
Vizualizare: elementele acoperite
Să vedem concret ce acoperă fiecare mulțime în [1..30]:
A (div 2): 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30
B (div 3): 3, 6, 9, 12, 15, 18, 21, 24, 27, 30
C (div 5): 5, 10, 15, 20, 25, 30
Elemente în mai multe:
- 6 în A și B
- 10 în A și C
- 12 în A și B
- 15 în B și C
- 18 în A și B
- 20 în A și C
- 24 în A și B
- 25 în C
- 30 în TOATE 3
Fără PInEx, dacă adunăm |A|+|B|+|C| = 15+10+6 = 31, dar 30 are doar 22 numere divizibile - am numărat de prea multe ori.
Aplicație: funcția lui Euler (phi)
φ(n) = câte numere de la 1 la n sunt
prime cu n (cmmdc = 1).
Derivare prin PInEx
Fie n = p₁^a₁ * p₂^a₂ * ... * pₖ^aₖ
descompunerea în factori primi.
Un număr x ≤ n e prim cu n ⟺ nu e
divizibil cu niciun pᵢ.
Câte NU sunt prime cu n? = câte sunt
divizibile cu cel puțin un pᵢ. PInEx pe
A₁, ..., Aₖ unde
Aᵢ = {x : pᵢ | x}.
Aplicând PInEx și simplificând:
φ(n) = n - |A₁ ∪ ... ∪ Aₖ| = n * (1 - 1/p₁) * (1 - 1/p₂) * ... * (1 - 1/pₖ)
Cod
long long phi(long long n) {
long long rez = n;
for (long long p = 2; p * p <= n; p++) {
if (n % p == 0) {
while (n % p == 0) n /= p;
rez -= rez / p;
}
}
if (n > 1) rez -= rez / n;
return rez;
}Exemplu:
φ(30) = 30 * (1 - 1/2) * (1 - 1/3) * (1 - 1/5) = 30 * 1/2 * 2/3 * 4/5 = 8.
Într-adevăr, numerele din {1,…,30} prime cu 30 sunt: 1, 7, 11, 13, 17, 19, 23, 29 → 8 numere ✓
Aplicație: numere coprime cu N
Câte numere de la 1 la L sunt
coprime cu N (cmmdc = 1)?
Ideea
Fie p₁, p₂, ..., pₖ factorii primi distincți ai
lui N.
x e coprim cu N ⟺ niciunul dintre
pᵢ nu divide x.
Câte sunt coprime cu N = L - |divizibile cu cel puțin un pᵢ|
Cu PInEx pe factorii primi ai lui N.
long long coprime(long long L, long long N) {
vector<long long> primes;
long long n = N;
for (long long p = 2; p * p <= n; p++)
if (n % p == 0) {
primes.push_back(p);
while (n % p == 0) n /= p;
}
if (n > 1) primes.push_back(n);
int k = primes.size();
long long rez = 0;
for (int mask = 0; mask < (1 << k); mask++) {
long long produs = 1;
int bits = __builtin_popcount(mask);
for (int i = 0; i < k; i++)
if (mask & (1 << i))
produs *= primes[i];
if (bits & 1) rez -= L / produs;
else rez += L / produs;
}
return rez;
}Aici pornim cu mask = 0 (mulțimea vidă, produs =
1, contribuie +L) și alternăm semnul.
Când să folosești PInEx
Semne că problema cere PInEx:
1. “Cel puțin una dintre condiții”
“Câte numere au cel puțin un divizor din {2, 3, 5}?” → PInEx.
3. Suprapunere între mulțimi
Dacă ai de numărat elemente care satisfac o condiție, dar condiția se poate “suprapune”, PInEx elimină dublele numărări.
4. Număr mic de mulțimi (k ≤ 20)
Complexitatea e O(2^k * k). Pentru
k > 20 devine prea lent - ai nevoie de altă
abordare.
5. Complementul e mai ușor
Uneori “cel puțin” e greu direct, dar “niciuna” e ușor (sau invers). PInEx convertește între ele.
Complexitate
| Număr mulțimi (k) | Iterații (bitmask) |
|---|---|
| 10 | ~10,000 |
| 15 | ~500,000 |
| 20 | ~20 milioane |
| 25 | ~800 milioane (prea lent) |
Pentru k > 25, caută:
- Simplificare a structurii (poate unele intersecții sunt egale)
- Algoritm diferit (funcție Möbius, programare dinamică)
Greșeli frecvente
1. Confuzia semnului
Pentru uniune (cel puțin una):
if (nrBiti & 1)
rez += N / produs; // + pentru impar
else
rez -= N / produs; // - pentru parSemnul e (-1)^(k+1) unde
k = nr biți:
- k=1 →
+ - k=2 →
- - k=3 →
+ - …
Pentru complement (niciuna), începe cu
mulțimea vidă și inversează: k=0 → +, k=1 →
-, etc.
2. Overflow la produs
long long produs = 1;
for (...) {
produs *= p[i]; // poate depasi long long!
if (produs > N) break;
}Mai bine:
if (produs > N / p[i]) { overflow = true; break; }
produs *= p[i];Asta verifică overflow-ul înainte să înmulțim.
3. Uitarea mulțimii vide pentru complement
Pentru niciuna dintre condiții, trebuie să
includem și mask = 0:
for (int mask = 0; mask < (1 << k); mask++) {
// mask = 0 → universul intreg, semn +
}Pentru cel puțin una, pornim de la
mask = 1:
for (int mask = 1; mask < (1 << k); mask++) { ... }4. Confuzia += cu
-=
Scrie un comentariu sau folosește sgn:
int sgn = (nrBiti & 1) ? 1 : -1;
rez += sgn * (N / produs);Mai puține bug-uri.
5. Ignorarea primelor duplicate
Pentru problema cu divizibili, primele trebuie să fie distincte (altfel intersecția nu e produsul).
Dacă ai numere oarecare (nu prime), folosește cmmmc (LCM) în loc de produs:
lcm(a, b) = a * b / gcd(a, b)Recapitulare diagramă
┌─────────────────────────┐
│ Problemă combinatorială │
│ "câte elemente au..." │
└──────────┬──────────────┘
│
┌────────────┴────────────┐
│ │
"cel puțin una" "niciuna"
│ │
┌────────┴────────┐ ┌────────┴────────┐
│ PInEx direct │ │ PInEx pe │
│ mask = 1..2^k-1 │ │ complement │
│ semn (-1)^(k+1) │ │ mask = 0..2^k-1 │
│ │ │ semn (-1)^k │
└─────────────────┘ └─────────────────┘
Ce să reții
- PInEx numără elementele care satisfac cel puțin una dintre condițiile date.
- Formula:
|A₁ ∪ ... ∪ Aₙ| = Σ (-1)^(|S|+1) * |∩S|peste submulțimi nevide. - Semnul alternează: +, -, +, -, …
- Implementare cu bitmask în
O(2^k * k). - Pentru
k > 20-25, caută altă metodă. - Aplicații clasice:
- Numere divizibile cu cel puțin un prim → PInEx direct
- Funcția lui Euler
φ(n)→ PInEx pe factorii primi - Derangements
D(n) = n! * Σ(-1)^j/j! - Funcții surjective
- Numere coprime cu N
- Fii atent la overflow, semn și includerea/excluderea mulțimii vide.
- PInEx e fundament pentru funcția Möbius (generalizare).