Skip to Content

Principio de inclusión-exclusión

Recursos

Recursos
FuenteRecursoNotas
cp-algoThe Inclusion-Exclusion Principle Artículo bien cubierto
CFInclusion-Exclusion Principle Buena explicación
WikipediaInclusion-exclusion principleWiki

Introducción

El principio de inclusión-exclusión se relaciona con hallar el tamaño de la unión de algunos conjuntos.

Verbalmente se puede enunciar de la siguiente forma:

Sumar los tamaños de los conjuntos por separado, restar los tamaños de todas las intersecciones por pares de los conjuntos, volver a sumar los tamaños de las intersecciones de ternas de conjuntos, restar el tamaño de las cuádruplas de conjuntos, …

La identidad matemática de lo anterior es:

i=1nAi=i=1nAi1i<jnAiAj+1i<j<knAiAjAk+(1)n1A1An \left| \bigcup_{i=1}^n A_i \right| = \sum_{i=1}^n|A_i| - \sum_{1\leq i<j\leq n} |A_i \cap A_j| + \sum _{1\leq i<j<k\leq n}|A_i \cap A_j \cap A_k| - \cdots + (-1)^{n-1} | A_1 \cap \cdots \cap A_n |

Escrita en forma compacta:

i=1nAi=0J{1,2,...,n}(1)J1jJAj \bigg|\bigcup_{i=1}^nA_i \bigg|= \sum_{0 \neq J \in \{1, 2,...,n\} } (-1)^{|J|-1} \bigg| \bigcap_{j \in J} A_j \bigg|

Función de Möbius

La función de Möbius  es una función multiplicativa que resulta útil al tratar la técnica de inclusión-exclusión y problemas relacionados con divisores. Toma valores en {1,0,1}\{-1, 0, 1\} según la factorización del número.

μ(n)={1si n es 1,0si n tiene un factor primo al cuadrado,(1)ksi n es producto de k factores primos distintos. \mu(n)=\begin{cases} 1 & \text{si $n$ es $1$},\\ 0 & \text{si $n$ tiene un factor primo al cuadrado},\\ (-1)^k & \text{si $n$ es producto de $k$ factores primos distintos}. \end{cases}

Abajo se pueden ver los primeros 1919 valores de μ(n)\mu(n):

n12345678910111213141516171819
μ(n)\mu(n)1-1-10-11-1001-10-1110-10-1

Veamos cómo se puede precomputar la función de Möbius con una criba ligeramente modificada.

mobius[1] = -1; for (int i = 1; i < VALMAX; i++) { if (mobius[i]) { mobius[i] = -mobius[i]; for (int j = 2 * i; j < VALMAX; j += i) { mobius[j] += mobius[i]; } } }

Aplicaciones

SQFREE

HechoFuenteNombreDificultadTagsSolución
SPOJSQFREE - Square-free integersNormalPIE, Divisorsen el módulo

Explicación

Una aplicación perfecta del principio de inclusión-exclusión y de la función de Möbius. En este caso particular el conjunto AiA_i — mencionado antes en la sección del tutorial — denota cuántos números son divisibles por i2i^2 y se nos pide hallar i=1nAi\bigg| \bigcup_{i=1}^{\sqrt{n}} A_i \bigg|. El arreglo de Möbius precomputado indica si hay que sumar o restar AiA_i.

Implementación

Complejidad temporal: O(VlogV+Tn)\mathcal{O}(V \log V + T \cdot \sqrt{n}), donde V=1e7V = 1e7

#include <iostream> #include <vector> using namespace std; const int VALMAX = 1e7; int mobius[VALMAX]; int main() { int test_num; cin >> test_num; mobius[1] = -1; for (int i = 1; i < VALMAX; i++) { if (mobius[i]) { mobius[i] = -mobius[i]; for (int j = 2 * i; j < VALMAX; j += i) { mobius[j] += mobius[i]; } } } for (int t = 0; t < test_num; t++) { long long n; long long ans = 0; cin >> n; for (int i = 1; 1LL * i * i <= n; i++) { ans += mobius[i] * n / ((long long)i * i); } cout << ans << '\n'; } }

Cowpatibility

HechoFuenteNombreDificultadTagsSolución
GoldCowpatibilityNormalPIE, BitsetSolución

Explicación

En este caso particular el conjunto AiA_i — mencionado antes en la sección del tutorial — denota cuántos pares de vacas tienen al menos ii sabores de helado en común. Del número total de pares restamos la unión de AiA_i. La respuesta global es:

n(n1)2i=15Ai \frac{n \cdot(n-1)}{2}- \bigg| \bigcup_{i=1}^{5} A_i \bigg|

Implementación

Complejidad temporal: O(NlogN)\mathcal{O}(N \log N)

#include <bits/stdc++.h> using namespace std; int main() { ifstream in("cowpatibility.in"); int n; in >> n; map<vector<int>, int> subsets; for (int i = 1; i <= n; i++) { vector<int> v(5); for (int &num : v) { in >> num; } sort(v.begin(), v.end()); for (int mask = 1; mask < (1 << 5); mask++) { vector<int> subset; for (int i = 0; i < 5; i++) { if ((1 << i) & mask) { subset.push_back(v[i]); } } subsets[subset]++; } } long long ans = (long long)n * (n - 1) / 2; for (const auto &[subset, freq] : subsets) { ans -= ((int)subset.size() % 2 == 1 ? 1LL : -1LL) * freq * (freq - 1) / 2; } ofstream("cowpatibility.out") << ans << endl; }
from collections import defaultdict with open("cowpatibility.in", "r") as read: n = int(read.readline().strip()) subsets = defaultdict(int) for _ in range(n): flavors = list(map(int, read.readline().strip().split())) flavors.sort() for mask in range(1, 1 << 5): subset = tuple(flavors[i] for i in range(5) if (1 << i) & mask) subsets[subset] += 1 ans = n * (n - 1) // 2 for subset, freq in subsets.items(): if len(subset) % 2 == 1: ans -= freq * (freq - 1) // 2 else: ans += freq * (freq - 1) // 2 print(ans, file=open("cowpatibility.out", "w"))

La cantidad de strings que coinciden con un cierto patrón

HechoFuenteNombreDificultadTagsSolución
TopCoderSetOfPatternsDifícilPIE, Strings, Patternsen el módulo

Explicación 1

Un enfoque de programación dinámica con máscaras de bits se vería así:

dp[i][mask]=la cantidad de strings de longitud i que coinciden con todos los patrones del conjunto, pero con ninguˊn otro patroˊn. dp[i][mask] = \text{la cantidad de strings de longitud i que coinciden con todos los patrones del conjunto, pero con ningún otro patrón. } La recurrencia es:

dp[i][mask&j]=dp[i1][j] donde j es un conjunto de patrones que coinciden con el caraˊcter c en la posicioˊn i dp[i][mask \& j]=dp[i-1][j]\text{ donde j es un conjunto de patrones que coinciden con el carácter c en la posición i}

El siguiente código ilustra esto:

Implementación

Complejidad temporal: O(MN2N)\mathcal{O}(M \cdot N \cdot 2^N), donde MM es el tamaño de cada string en patterns y NN es el tamaño de patterns.

const int MOD = 1000003; int howMany(vector<string> patterns, int k) { int n = patterns.size(); int m = patterns[0].size(); vector<vector<int>> dp(50, vector<int>(1 << n)); for (int i = 0; i < m; i++) { for (char c = 'a'; c <= 'z'; c++) { int mask = 0; for (int j = 0; j < n; j++) { if (patterns[j][i] == c || patterns[j][i] == '?') { mask |= (1 << j); } } if (i == 0) { dp[i][mask]++; } else { for (int j = 0; j < (1 << n); j++) { dp[i][j & mask] = (dp[i][j & mask] + dp[i - 1][j]) % MOD; } } } } int ans = 0; for (int mask = 0; mask < (1 << n); mask++) { if (__builtin_popcount(mask) == k) { ans = (ans + dp[m - 1][mask]) % MOD; } } return ans; }

Explicación 2

El problema también se puede resolver usando el principio de inclusión-exclusión.

Una observación importante es que podemos contar fácilmente los strings que satisfacen algunos patrones específicos. Simplemente iteramos por las posiciones de todos los patrones. Si todos los patrones contienen ?? entonces podemos usar cualquier letra de aa a zz, lo que nos da 2626 soluciones; en caso contrario solo podemos poner la letra fija contenida por un patrón. La respuesta es el producto.

Iterar sobre subconjuntos — denotados por AA — de patrones formados por exactamente kk strings. Para este subconjunto específico contar la cantidad de strings que solo pueden coincidir con todos los patrones del subconjunto AA. Aplicar el principio de inclusión-exclusión sobre todos los superconjuntos BB tales que ABA \subset B.

solve(A)=BA(1)Bkf(B) solve(A) = \sum_{B \supseteq A} (-1)^{|B|-k} \cdot f(B)

f(B)f(B) denota la cantidad de strings que coinciden con al menos el conjunto BB

La respuesta global es:

ans=A:A=ksolve(A) ans = \sum_{A:|A|=k} solve(A)

Implementación

Complejidad temporal: O((Nk)2NkMN)\mathcal{O}(\binom{N}{k} \cdot 2^{N - k} \cdot M \cdot N)

const int MOD = 1000003; int howMany(vector<string> patterns, int k) { int n = patterns.size(); int m = patterns[0].size(); int ans = 0; for (int mask = 0; mask < (1 << n); mask++) { // subsets with exactly k patterns matters if (__builtin_popcount(mask) != k) { continue; } // iterate over all superset of current subset mask for (int supermask = mask; supermask < (1 << n); supermask++) { if ((mask & supermask) != mask) { continue; } int sign = ((__builtin_popcount(supermask) - k) & 1 ? -1 : 1); int freq = 1; // checks how many valid strings satisfy the supermask for (int i = 0; i < m; i++) { bool flag = true; char last_letter = '?'; for (int j = 0; j < n; j++) { if (((1 << j) & supermask) == 0) { continue; } // check for conflicts if the pattern specifies a letter not '?' if (patterns[j][i] != '?') { if (last_letter == '?') { last_letter = patterns[j][i]; } else if (patterns[j][i] != last_letter) { // conflict! two patterns require different letters here flag = false; break; } } } if (!flag) { freq = 0; } else if (last_letter == '?') { freq = (freq * 26) % MOD; } } ans = (ans + sign * freq) % MOD; ans = (ans + MOD) % MOD; } } return ans; }
HechoFuenteNombreDificultadTagsSolución
SPOJMOMOS - FEASTOFPIGSFácilPIE, Divisors, SieveSolución
SPOJKPRIMESB - Almost Prime Numbers AgainNormalPIE, DivisorsSolución
SPOJMSKYCODE - Sky CodeNormalPIE, DivisorsSolución
CSESCounting ReordersNormalPIESolución
CSESCounting Coprime PairsNormalPIE, DivisorsSolución
CSESGrid CompletionNormalPIESolución