Skip to Content

Combinatoria

Si nunca se ha encontrado combinatoria, AoPS es un buen lugar para empezar.

Recursos
FuenteRecursoNotas
AoPSAlcumus

problemas de práctica; poner el foco en Counting and Probability para los temas del módulo

AoPSIntro to Counting and Probability

buen libro

Recursos

Recursos
FuenteRecursoNotas
CPH22 - Combinatorics

este módulo se basa en esto

cp-algoCombinatorics
HEBasics of Combinatorics

enseña combinatoria fundamental con un problema de práctica al final

AoPSIntroductory Combinatorics

enseña conceptos básicos de combinatoria

AoPSIntermediate Combinatorics

enseña conceptos más avanzados de combinatoria

CFInclusion-Exclusion Principle

un buen blog sobre el principio de inclusión-exclusión

Si se prefiere ver videos, estas son algunas opciones:

Recursos
FuenteRecursoNotas
YouTubeDeep Dive into Combinatorics

playlist de mathemaniac

YouTubeMIT 6.042J

clases 16-23

YouTubeSums and Expected Value #1

video de Errichto sobre valor esperado y sumas de subconjuntos

Coeficientes binomiales

HechoFuenteNombreDificultadTagsSolución
CSESBinomial CoefficientsFácilen el módulo

El coeficiente binomial (nk)\binom{n}{k} (se pronuncia ”nn en kk” o a veces se escribe como nCk{}_nC_k) representa el número de formas de elegir un subconjunto de kk elementos de un conjunto de nn elementos. Por ejemplo, (42)=6\binom{4}{2} = 6, porque el conjunto {1,2,3,4}\{1,2,3,4\} tiene 66 subconjuntos de 22 elementos:

{1,2},{1,3},{1,4},{2,3},{2,4},{3,4} \{1, 2\}, \{1, 3\}, \{1, 4\}, \{2, 3\}, \{2, 4\}, \{3, 4\}

Hay dos formas de calcular coeficientes binomiales:

Método 1: Triángulo de Pascal (programación dinámica) - O(n2)\mathcal{O}(n^2)

Los coeficientes binomiales se pueden calcular de forma recursiva como sigue:

(nk)=(n1k1)+(n1k) \binom{n}{k} = \binom{n - 1}{k - 1} + \binom{n - 1}{k}

La intuición detrás de esto es fijar un elemento xx en el conjunto y elegir k1k − 1 elementos de n1n − 1 elementos si xx está incluido en el conjunto, o elegir kk elementos de n1n − 1 elementos, en caso contrario.

Los casos base de la recursión son:

(n0)=(nn)=1 \binom{n}{0} = \binom{n}{n} = 1

porque siempre hay exactamente una forma de construir un subconjunto vacío y un subconjunto que contiene todos los elementos.

Esta fórmula recursiva se conoce comúnmente como el triángulo de Pascal .

Una implementación naive de esto usaría una fórmula recursiva, como abajo:

/** @return nCk mod p using naive recursion */ int binomial(int n, int k, int p) { if (k == 0 || k == n) { return 1; } return (binomial(n - 1, k - 1, p) + binomial(n - 1, k, p)) % p; }
/** @return nCk mod p using naive recursion */ public static int binomial(int n, int k, int p) { if (k == 0 || k == n) { return 1; } return (binomial(n - 1, k - 1, p) + binomial(n - 1, k, p)) % p; }
def binomial(n: int, k: int, p: int) -> int: """:return: nCk mod p using naive recursion""" if k == 0 or k == n: return 1 return (binomial(n - 1, k - 1, p) + binomial(n - 1, k, p)) % p

Además, podemos optimizar esto de O(2n)\mathcal{O}(2^n) a O(n2)\mathcal{O}(n^2) usando programación dinámica (DP) cacheando los valores de binomiales más pequeños para evitar recalcular los mismos valores una y otra vez. El código de abajo muestra una implementación bottom-up de esto.

/** @return nCk mod p using dynamic programming */ int binomial(int n, int k, int p) { // dp[i][j] stores iCj vector<vector<int>> dp(n + 1, vector<int>(k + 1, 0)); // base cases described above for (int i = 0; i <= n; i++) { /* * i choose 0 is always 1 since there is exactly one way * to choose 0 elements from a set of i elements * (don't choose anything) */ dp[i][0] = 1; /* * i choose i is always 1 since there is exactly one way * to choose i elements from a set of i elements * (choose every element in the set) */ if (i <= k) { dp[i][i] = 1; } } for (int i = 0; i <= n; i++) { for (int j = 1; j <= min(i, k); j++) { if (i != j) { // skips over the base cases // uses the recurrence relation above dp[i][j] = (dp[i - 1][j - 1] + dp[i - 1][j]) % p; } } } return dp[n][k]; // returns nCk modulo p }
/** @return nCk mod p using dynamic programming */ public static int binomial(int n, int k, int p) { // dp[i][j] stores iCj int[][] dp = new int[n + 1][k + 1]; // base cases described above for (int i = 0; i <= n; i++) { /* * i choose 0 is always 1 since there is exactly one way * to choose 0 elements from a set of i elements * (don't choose anything) */ dp[i][0] = 1; /* * i choose i is always 1 since there is exactly one way * to choose i elements from a set of i elements * (choose every element in the set) */ if (i <= k) { dp[i][i] = 1; } } for (int i = 0; i <= n; i++) { for (int j = 1; j <= Math.min(i, k); j++) { if (i != j) { // skips over the base cases // uses the recurrence relation above dp[i][j] = (dp[i - 1][j - 1] + dp[i - 1][j]) % p; } } } return dp[n][k]; // returns nCk modulo p }
def binomial(n: int, k, p): """:return: nCk mod p using dynamic programming""" # dp[i][j] stores iCj dp = [[0] * (k + 1) for _ in range(n + 1)] # base cases described above for i in range(n + 1): """ i choose 0 is always 1 since there is exactly one way to choose 0 elements from a set of i elements (don't choose anything """ dp[i][0] = 1 """ i choose i is always 1 since there is exactly one way to choose i elements from a set of i elements (choose every element in the set) """ if i <= k: dp[i][i] = 1 for i in range(n + 1): for j in range(1, min(i, k) + 1): if i != j: # skips over the base cases # uses the recurrence relation above dp[i][j] = (dp[i - 1][j - 1] + dp[i - 1][j]) % p return dp[n][k] # returns nCk modulo p

Método 2: Definición factorial (inversos modulares) - O(n+logMOD)\mathcal{O}(n + \log MOD)

Definir n!n! como n×(n1)×(n2)×1n \times (n - 1) \times (n - 2) \times \ldots 1. n!n! representa el número de permutaciones de un conjunto de nn elementos. Ver este artículo de AoPS  para más detalles.

Otra forma de calcular coeficientes binomiales es la siguiente:

(nk)=n!k!(nk)! \binom{n}{k} = \frac{n!}{k!(n-k)!}

Recordemos que (nk)\binom{n}{k} también representa el número de formas de elegir kk elementos de un conjunto de nn elementos. Una estrategia para obtener todas esas combinaciones es recorrer todas las permutaciones posibles de los nn elementos, y solo tomar los primeros kk elementos de cada permutación. Hay n!n! formas de hacerlo. Sin embargo, nótese que el orden de los elementos dentro y fuera del subconjunto no importa, así que el resultado se divide por k!k! y (nk)!(n − k)!.

Como estos coeficientes binomiales son grandes, los problemas típicamente exigen imprimir la respuesta módulo un primo grande pp como 109+710^9 + 7. Por suerte, podemos usar inversos modulares para dividir n!n! por k!k! y (nk)!(n - k)! módulo pp para cualquier primo pp. Computar factoriales inversos online puede consumir mucho tiempo. En su lugar, podemos precomputar todos los factoriales en tiempo O(n)\mathcal{O}(n) y los factoriales inversos en O(n+logMOD)\mathcal{O}(n + \log MOD). Primero, computamos el inverso modular del factorial más grande usando exponenciación binaria. Para el resto, usamos el hecho de que (n!)1(n!)1×(n+1)1×(n+1)((n+1)!)1×(n+1)(n!)^{-1} \equiv (n!)^{-1}\times (n+1)^{-1} \times (n+1) \equiv ((n+1)!)^{-1}\times (n+1). Ver el código de abajo para la implementación.

const int MAXN = 1e6; long long fac[MAXN + 1]; long long inv[MAXN + 1]; /** @return x^n modulo m in O(log p) time. */ long long exp(long long x, long long n, long long m) { x %= m; // note: m * m must be less than 2^63 to avoid ll overflow long long res = 1; while (n > 0) { if (n % 2 == 1) { res = res * x % m; } x = x * x % m; n /= 2; } return res; } /** Precomputes n! from 0 to MAXN. */ void factorial(long long p) { fac[0] = 1; for (int i = 1; i <= MAXN; i++) { fac[i] = fac[i - 1] * i % p; } } /** * Precomputes all modular inverse factorials * from 0 to MAXN in O(n + log p) time */ void inverses(long long p) { inv[MAXN] = exp(fac[MAXN], p - 2, p); for (int i = MAXN; i >= 1; i--) { inv[i - 1] = inv[i] * i % p; } } /** @return nCr mod p */ long long choose(long long n, long long r, long long p) { return fac[n] * inv[r] % p * inv[n - r] % p; }
import java.util.*; public class BinomialCoefficients { private static final int MAXN = (int)1e6; private static long[] fac = new long[MAXN + 1]; private static long[] inv = new long[MAXN + 1]; /** @return x^n modulo m in O(log p) time. */ private static long exp(long x, long n, long m) { x %= m; // note: m * m must be less than 2^63 to avoid ll overflow long res = 1; while (n > 0) { if (n % 2 == 1) { res = res * x % m; } x = x * x % m; n /= 2; } return res; } /** Precomputes n! from 0 to MAXN with a certain modulo. */ private static void factorial(long p) { fac[0] = 1; for (int i = 1; i <= MAXN; i++) { fac[i] = fac[i - 1] * i % p; } } /** * Precomputes all modular inverse factorials * from 0 to MAXN in O(n + log p) time */ private static void inverses(long p) { inv[MAXN] = exp(fac[MAXN], p - 2, p); for (int i = MAXN; i >= 1; i--) { inv[i - 1] = inv[i] * i % p; } } /** @return nCr mod p */ private static long choose(long n, long r, long p) { return fac[(int)n] * inv[(int)r] % p * inv[(int)(n - r)] % p; } }
MAXN = 10**6 fac = [0] * (MAXN + 1) inv = [0] * (MAXN + 1) def exp(x: int, n: int, m: int) -> int: """:return: x^n modulo m in O(log p) time.""" x %= m # note: m * m must be less than 2^63 to avoid ll overflow res = 1 while n > 0: if n % 2 == 1: res = (res * x) % m x = (x * x) % m n //= 2 return res def factorial(p: int): """Precomputes n! from 0 to MAXN.""" global fac fac[0] = 1 for i in range(1, MAXN + 1): fac[i] = (fac[i - 1] * i) % p def inverses(p: int): """ Precomputes all modular inverse factorials from 0 to MAXN in O(n + log p) time """ global inv inv[MAXN] = exp(fac[MAXN], p - 2, p) for i in range(MAXN, 0, -1): inv[i - 1] = (inv[i] * i) % p def choose(n: int, r: int, p: int): """:return: nCr mod p""" return fac[n] * inv[r] % p * inv[n - r] % p

Solución - Binomial Coefficients

El primer método para calcular factoriales binomiales es demasiado lento para este problema, ya que las restricciones de aa y bb son (1ba106)(1 \leq b \leq a \leq 10^6) (recordemos que la primera implementación corre en complejidad temporal O(n2)\mathcal{O}(n^2)). Sin embargo, podemos usar el segundo método para responder cada una de las nn consultas en tiempo constante precomputando factoriales y sus inversos modulares.

#include <iostream> using namespace std; using ll = long long; const int MAXN = 1e6; const int MOD = 1e9 + 7; ll fac[MAXN + 1]; ll inv[MAXN + 1]; // BeginCodeSnip{Counting functions} ll exp(ll x, ll n, ll m) { x %= m; ll res = 1; while (n > 0) { if (n % 2 == 1) { res = res * x % m; } x = x * x % m; n /= 2; } return res; } void factorial() { fac[0] = 1; for (int i = 1; i <= MAXN; i++) { fac[i] = fac[i - 1] * i % MOD; } } void inverses() { inv[MAXN] = exp(fac[MAXN], MOD - 2, MOD); for (int i = MAXN; i >= 1; i--) { inv[i - 1] = inv[i] * i % MOD; } } ll choose(int n, int r) { return fac[n] * inv[r] % MOD * inv[n - r] % MOD; } // EndCodeSnip int main() { factorial(); inverses(); int n; cin >> n; for (int i = 0; i < n; i++) { int a, b; cin >> a >> b; cout << choose(a, b) << '\n'; } }
import java.io.*; import java.util.*; public class BinomialCoeffs { private static final int MAXN = (int)1e6; private static final int MOD = (int)1e9 + 7; private static long[] fac = new long[MAXN + 1]; private static long[] inv = new long[MAXN + 1]; public static void main(String[] args) { factorial(); inverses(); Kattio io = new Kattio(); int n = io.nextInt(); for (int i = 0; i < n; i++) { int a = io.nextInt(); int b = io.nextInt(); System.out.println(choose(a, b)); } } // BeginCodeSnip{Counting Functions} private static long exp(long x, long n, long m) { x %= m; long res = 1; while (n > 0) { if (n % 2 == 1) { res = (res * x) % m; } x = (x * x) % m; n /= 2; } return res; } private static void factorial() { fac[0] = 1; for (int i = 1; i <= MAXN; i++) { fac[i] = (fac[i - 1] * i) % MOD; } } private static void inverses() { inv[MAXN] = exp(fac[MAXN], MOD - 2, MOD); for (int i = MAXN; i >= 1; i--) { inv[i - 1] = (inv[i] * i) % MOD; } } private static long choose(int n, int r) { return (((fac[n] * inv[r]) % MOD) * inv[n - r]) % MOD; } // EndCodeSnip // CodeSnip{Kattio} }
MAXN = 10**6 MOD = 10**9 + 7 fac = [0] * (MAXN + 1) inv = [0] * (MAXN + 1) # BeginCodeSnip{Counting Functions} def exp(x: int, n: int, m: int) -> int: x %= m res = 1 while n > 0: if n % 2 == 1: res = res * x % m x = x * x % m n //= 2 return res def factorial(): fac[0] = 1 for i in range(1, MAXN + 1): fac[i] = fac[i - 1] * i % MOD def inverses(): inv[MAXN] = exp(fac[MAXN], MOD - 2, MOD) for i in range(MAXN, 0, -1): inv[i - 1] = inv[i] * i % MOD def choose(n: int, r: int): return fac[n] * inv[r] % MOD * inv[n - r] % MOD # EndCodeSnip factorial() inverses() n = int(input()) for _ in range(n): a, b = map(int, input().split()) print(choose(a, b))

Desarreglos

HechoFuenteNombreDificultadTagsSolución
YSMontmort NumbersFácilen el módulo

El número de desarreglos (derangements) de nn números, expresado como !n!n, es el número de permutaciones tales que ningún elemento aparece en su posición original. De forma informal, es el número de formas en que nn sombreros se pueden devolver a nn personas de modo que ninguna persona reciba su propio sombrero.

Método 1: Principio de inclusión-exclusión

Supongamos que tenemos eventos E1,E2,,EnE_1, E_2, \dots, E_n, donde el evento EiE_i corresponde a que la persona ii reciba su propio sombrero. Quisiéramos calcular n!E1E2Enn! - \lvert E_1 \cup E_2 \cup \dots \cup E_n \rvert.

Restamos de n!n! el número de formas en que ocurre cada evento; es decir, consideramos la cantidad n!E1E2Enn! - \lvert E_1 \rvert - \lvert E_2 \rvert - \dots - \lvert E_n \rvert. Esto subcuenta, porque estamos restando demasiadas veces los casos en los que ocurre más de un evento. Específicamente, para una permutación donde ocurren al menos dos eventos, subcontamos en uno. Así, sumamos de vuelta el número de formas en que ocurren dos eventos. Podemos continuar este proceso para cada tamaño de subconjuntos de índices. La expresión queda ahora de la forma:

n!E1E2En=k=1n(1)k(nuˊmero de permutaciones con k puntos fijos) n! - \lvert E_1 \cup E_2 \cup \dots \cup E_n \rvert = \sum_{k = 1}^n (-1)^k \cdot (\text{número de permutaciones con $k$ puntos fijos})

Para un tamaño de conjunto kk, el número de permutaciones con al menos kk índices se puede computar eligiendo un conjunto de tamaño kk que quedan fijos, y permutando los otros índices. En términos matemáticos:

(nk)(nk)!=n!k!(nk)!(nk)!=n!k! {n \choose k}(n-k)! = \frac{n!}{k!(n-k)!}(n-k)! = \frac{n!}{k!}

Así, el problema pasa a ser computar

n!k=0n(1)kk! n!\sum_{k=0}^n\frac{(-1)^k}{k!}

lo cual se puede hacer en tiempo lineal.

#include <atcoder/modint> #include <bits/stdc++.h> using namespace std; using mint = atcoder::modint; int main() { int n, m; cin >> n >> m; mint::set_mod(m); mint c = 1; for (int i = 1; i <= n; i++) { c = (c * i) + (i % 2 == 1 ? -1 : 1); cout << c.val() << ' '; } cout << endl; }
import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int n = scanner.nextInt(); int m = scanner.nextInt(); long c = 1; for (int i = 1; i <= n; i++) { c = (c * i) + (i % 2 == 1 ? -1 : 1); c %= m; c += m; c %= m; System.out.print(c + " "); } System.out.println(); } }
n, m = map(int, input().split()) c = 1 for i in range(1, n + 1): c = (c * i) + (-1 if i % 2 == 1 else 1) c %= m c += m c %= m print(c, end=" ") print()

Método 2: Programación dinámica

Supongamos que la persona 1 recibió el sombrero de la persona ii. Hay dos casos:

  1. Si la persona ii recibe el sombrero de la persona 1, entonces el problema se reduce a un subproblema de tamaño n2n - 2. Hay n1n - 1 posibilidades para ii en este caso, así que sumamos a la respuesta actual (n1)!(n2)(n - 1)\cdot !(n - 2).
  2. Si la persona ii no recibe el sombrero de la persona 1, entonces podemos reasignar el sombrero de la persona 1 para que sea el sombrero de la persona ii (si recibieron el sombrero de la persona 1, esto se convertiría en el primer caso). Así, esto se vuelve un subproblema de tamaño n1n - 1, y hay n1n - 1 formas de elegir ii.

Así, tenemos

!n=(n1)(!(n2)+!(n1)) !n = (n - 1)(!(n - 2) + !(n - 1))

que se puede computar en tiempo lineal con DP. Los casos base son que !0=1!0 = 1 y !1=0!1 = 0.

#include <atcoder/modint> #include <bits/stdc++.h> using namespace std; using mint = atcoder::modint; int main() { int n, m; cin >> n >> m; mint::set_mod(m); mint a = 1, b = 0; cout << 0 << ' '; for (int i = 2; i <= n; i++) { mint c = (i - 1) * (a + b); cout << c.val() << ' '; a = b; b = c; } cout << endl; }
import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int n = scanner.nextInt(); int m = scanner.nextInt(); int a = 1; int b = 0; System.out.print("0 "); for (int i = 2; i <= n; i++) { int c = (int)(((long)(i - 1) * (a + b)) % m); System.out.print(c + " "); a = b; b = c; } System.out.println(); } }
n, m = map(int, input().split()) a, b = 1, 0 print(0, end=" ") for i in range(2, n + 1): c = (i - 1) * (a + b) % m print(c, end=" ") a, b = b, c print()

Estrellas y barras

Recursos
FuenteRecursoNotas
cp-algoStars and bars
MediumThe Stars and Bars Formula

Artículo bien documentado.

Estrellas y barras  es un método útil en combinatoria que consiste en agrupar objetos indistinguibles en cajas distinguibles. El número de formas de poner nn objetos indistinguibles en kk cajas distinguibles es:

(n+k1n)=(n+k1k1) \binom{n+k-1}{n}=\binom{n+k-1}{k-1}

El segundo coeficiente binomial de arriba se puede derivar de la propiedad de los coeficientes binomiales: (nk)=(nnk)\binom{n}{k}=\binom{n}{n-k}.

Veamos un ejemplo particular para n=3n=3 y k=2k=2 que tiene 44 posibilidades. Como el nombre indica, la visualización suele hacerse con estrellas separadas en grupos por barras:

||\bigstar \bigstar \bigstar \\ |\bigstar |\bigstar \bigstar \\ |\bigstar \bigstar| \bigstar \\ \bigstar \bigstar \bigstar || \\

Como probablemente se habrá notado, puede haber cajas vacías: cuando ponemos todas las estrellas en la primera o en la segunda caja. Puede haber casos en los que todas las cajas deban ser no vacías. En ese caso, el número de formas de poner nn objetos indistinguibles en kk cajas distinguibles no vacías es: (n1k1)\binom{n-1}{k-1}

HechoFuenteNombreDificultadTagsSolución
SPOJMARBLES - MarblesFácilen el módulo

Explicación

Para este problema deberíamos pensar al revés: digamos que los kk colores de los que elegimos son de hecho cajas y, en lugar de elegir nn canicas, las ponemos en las cajas respectivas. El problema tiene la restricción de que debemos tomar al menos una canica de cada tipo, lo que en nuestra nueva perspectiva significa que todas las cajas deben ser no vacías. Así, la respuesta se obtiene con la segunda fórmula: (n1k1)\binom{n-1}{k-1}

Implementación

Complejidad temporal: O(TK)\mathcal{O}(T \cdot K)

#include <iostream> /** @return n choose k, computed naively since the problem has no overflow */ long long comb(int n, int k) { if (k > n - k) { k = n - k; } long long ret = 1; for (int i = 0; i < k; i++) { // this is done instead of *= for divisibility issues ret = ret * (n - i) / (i + 1); } return ret; } int main() { int test_num; std::cin >> test_num; for (int t = 0; t < test_num; t++) { int marble_num; int color_num; std::cin >> marble_num >> color_num; std::cout << comb(marble_num - 1, color_num - 1) << '\n'; } }
from math import comb ans = [] for _ in range(int(input())): marble_num, color_num = [int(i) for i in input().split()] ans.append(str(comb(marble_num - 1, color_num - 1))) print("\n".join(ans))

Valor esperado

Recursos
FuenteRecursoNotas
BrilliantExpected Value

muchos ejemplos de matemática pura de valor esperado + explicaciones

BrilliantLinearity of Expectation

muchos ejemplos de matemática pura de linealidad de la esperanza + explicaciones

CFExpected Value

un buen blog sobre el valor esperado

Un valor esperado es la media teórica de una distribución de probabilidad. Una variable aleatoria se usa para representar una posible distribución de probabilidad. Sea XX una variable aleatoria y P(X=x)P(X = x) la probabilidad de que el resultado de la variable aleatoria XX sea xx. Entonces, el valor esperado de XX, denotado E[X]E[X], es

xxP(X=x) \sum_x x \cdot P(X = x)

Por ejemplo, sea XX la distribución de probabilidad de un dado justo de 6 caras. P(X=x)P(X = x) es 16\frac{1}{6} para 1x61 \leq x \leq 6. Usando la fórmula, obtenemos E[X]=216=72E[X] = \frac{21}{6} = \frac{7}{2}.

HechoFuenteNombreDificultadTagsSolución
CSESCandy LotteryFácilen el módulo

Explicación

Sea XX la distribución de probabilidad del número máximo de caramelos que recibe un niño. Para obtener E[X]E[X], necesitamos P(X=x)P(X = x) para 1xk1 \leq x \leq k. (xk)n(\frac{x}{k})^n es la probabilidad de que cada niño reciba a lo sumo xx caramelos. Para obtener P(X=x)P(X = x), debemos restar la probabilidad de que cada niño reciba estrictamente menos de xx caramelos, que es (x1k)n(\frac{x - 1}{k})^n.

Por lo tanto, P(X=x)=(xk)n(x1k)nP(X = x) = (\frac{x}{k})^n - (\frac{x - 1}{k})^n, lo que nos permite calcular E[X]E[X].

Implementación

Complejidad temporal: O(k)\mathcal{O}(k) (asumiendo que la potencia corre en tiempo constante).

(Opcional) ¿Por qué hay que manejar un caso de forma especial?

Consideremos el siguiente fragmento de código:

#include <iomanip> #include <iostream> int main() { double ans = 9.1919575; std::cout << std::fixed << std::setprecision(6) << ans << "\n"; } // Output: 9.191957

¿Por qué el número 9.19195759.1919575 parece redondear hacia abajo cuando se supone que debería redondear hacia arriba? La razón es que no se puede representar exactamente en un double, así que ansans guarda en su lugar el double más cercano, y ese double resulta ser menor. De hecho, si aumentamos la precisión de 6 a 15 al imprimir, obtenemos la siguiente salida: 9.1919574999999999.191957499999999.

En general, una fracción a/ba/b no se puede representar exactamente en un double a menos que bb sea una potencia de dos.

Imprimamos todos los casos donde esto podría ser un problema. En el siguiente código, difdif es la diferencia entre la respuesta computada por nuestra solución y el número más cercano de la forma x.xxxxxx5.

#include <bits/stdc++.h> using namespace std; double solve_orig(int n, int k) { double expect_max = 0; for (int i = 1; i <= k; i++) { expect_max += i * (pow((double)i / k, n) - pow((double)(i - 1) / k, n)); } return expect_max; } int main() { vector<pair<double, pair<int, int>>> by_dif; for (int n = 1; n <= 100; ++n) for (int k = 1; k <= 100; ++k) { double a = solve_orig(n, k); double b = 1e6 * a - 0.5; double dif = abs(b - round(b)) / 1e6; by_dif.push_back({dif, {n, k}}); } sort(begin(by_dif), end(by_dif)); by_dif.resize(10); for (auto [d, p] : by_dif) { cout << "dif = " << d << " n = " << p.first << " " << "k = " << p.second << "\n"; stringstream ss; ss << fixed << setprecision(15) << solve_orig(p.first, p.second); cout << ss.str() << "\n\n"; } }

Salida:

dif = 0 n = 2 k = 64 43.164062500000000 dif = 0 n = 3 k = 32 24.492187500000000 dif = 0 n = 4 k = 4 3.617187500000000 dif = 0 n = 7 k = 2 1.992187500000000 dif = 0 n = 7 k = 10 9.191957499999999 dif = 1.86265e-15 n = 4 k = 20 16.483337500000001 dif = 3.12328e-11 n = 33 k = 62 60.632305500031237 dif = 1.14784e-10 n = 64 k = 98 96.938251499885212 dif = 1.89349e-10 n = 27 k = 71 68.932663499810644 dif = 2.14219e-10 n = 53 k = 17 16.958418499785783

Si miramos las diferencias más pequeñas, notamos que (n,k){(7,10),(4,20)}(n,k)\in \{(7,10), (4,20)\} son los dos primeros casos donde la respuesta no se puede representar exactamente en un double. (4,20)(4,20) resulta no ser un problema, pero (7,10)(7,10) sí lo es.

#include <cmath> #include <iomanip> #include <iostream> using std::cout; using std::endl; int main() { int n; int k; std::cin >> n >> k; double expect_max = 0; for (int i = 1; i <= k; i++) { expect_max += i * (pow((double)i / k, n) - pow((double)(i - 1) / k, n)); } if (n == 7 && k == 10) expect_max += 1e-12; // fix rounding for edge case cout << std::setprecision(6) << std::fixed; // set output precision cout << expect_max << endl; }
n, k = [int(i) for i in input().split()] expected_max = 0 for c in range(1, k + 1): expected_max += c * ((c / k) ** n - ((c - 1) / k) ** n) if n == 7 and k == 10: expected_max += 1e-12 print(f"{expected_max:.6f}")

Linealidad de la esperanza

La linealidad de la esperanza afirma que E[X+Y]=E[X]+E[Y]E[X + Y] = E[X] + E[Y] sin importar si XX e YY son independientes entre sí. Por ejemplo, si en un cierto día Alicia va al gimnasio con probabilidad 110\frac{1}{10} y su esposo Bob va al gimnasio con probabilidad 310\frac{3}{10}, el número esperado de visitas al gimnasio en un cierto día entre la pareja es 110+310=25\frac{1}{10} + \frac{3}{10} = \frac{2}{5}. Esto funciona aunque las decisiones de una persona puedan afectar a la otra.

Esto se puede generalizar a una secuencia de variables aleatorias X1,X2,,XnX_1, X_2, \dots, X_n y constantes arbitrarias c1,c2,,cnc_1, c_2, \dots, c_n:

E[i=1nciXi]=i=1nciE[Xi] E\left[\sum_{i = 1}^{n} c_iX_i \right] = \sum_{i = 1}^{n}c_i \cdot E[ X_i]
HechoFuenteNombreDificultadTagsSolución
ACErasing VerticesNormalen el módulo

Explicación

Aunque esto pueda parecer imposible a primera vista dada la cantidad de secuencias distintas de operaciones, la linealidad de la esperanza nos permite descomponer el valor esperado en la suma de partes más pequeñas.

Podemos descomponer esto partiendo la variable aleatoria inicial en las sumas de varias variables aleatorias indicadoras. Sea XuX_u una variable que indica si el nodo uu fue marcado de forma explícita para eliminación. Como es básicamente un booleano, solo puede tomar 00 o 11, lo que también significa que la probabilidad de que sea 11 es igual a su valor esperado.

Ahora, consideremos cómo una operación afecta al nodo uu.

  1. O bien elige un nodo que no puede alcanzar uu, y no pasa nada en lo que respecta a uu.
  2. Elige al propio uu, haciendo que la variable indicadora sea 11.
  3. Elige otro nodo que puede alcanzar uu, haciendo que la variable indicadora sea 00. Como todos los nodos tienen la misma chance de ser elegidos, la chance de que Xu=1X_u=1 es 1au\frac{1}{a_u}, donde aua_u es el número de nodos que pueden alcanzar uu incluyendo al propio uu.

Juntamos de nuevo los valores esperados de todas las variables indicadoras, lo que nos da la respuesta final.

Implementación

Complejidad temporal: O(N3)\mathcal{O}(N^3) usando un DFS o BFS naive.

#include <iomanip> #include <iostream> #include <vector> using std::cout; using std::endl; using std::vector; int main() { int n; std::cin >> n; vector<vector<int>> adj(n, vector<int>(n)); for (int n1 = 0; n1 < n; n1++) { for (int n2 = 0; n2 < n; n2++) { char connected; std::cin >> connected; adj[n1][n2] = connected == '1'; } } // reach_from[i] = the # of nodes that have a path to i vector<int> reach_from(n); for (int start = 0; start < n; start++) { vector<int> todo{start}; vector<bool> visited(n, false); visited[start] = true; while (!todo.empty()) { int curr = todo.back(); todo.pop_back(); for (int i = 0; i < n; i++) { if (!visited[i] && adj[curr][i]) { visited[i] = true; reach_from[i]++; todo.push_back(i); } } } } double expected_ops = 0; for (int i : reach_from) { expected_ops += 1.0 / (i + 1); } cout << std::setprecision(15) << expected_ops << endl; }
n = int(input()) adj = [[] for _ in range(n)] for i in range(n): for v, c in enumerate(input()): if c == "1": adj[i].append(v) # reach_from[i] = the # of nodes that have a path to i reach_from = [0 for _ in range(n)] for start in range(n): todo = [start] visited = [False for _ in range(n)] visited[start] = True while todo: curr = todo.pop() for next_ in adj[curr]: if not visited[next_]: visited[next_] = True reach_from[next_] += 1 todo.append(next_) expected_ops = 0 for i in reach_from: expected_ops += 1 / (i + 1) print(expected_ops)

Productos esperados

La linealidad de la esperanza trata E[X+Y]E[X + Y], pero ¿qué hay de E[XY]E[X \cdot Y]?

E[XY]=E[X]E[Y]E[X \cdot Y] = E[X] \cdot E[Y] si XX e YY son independientes entre sí. Podemos reconsiderar el ejemplo de un dado justo de 6 caras para mostrar que E[X2]E[X]2E[X^2] \neq E[X]^2. Sabemos que E[X]=72E[X] = \frac{7}{2}, así que E[X]2=7272=494E[X]^2 = \frac{7}{2} \cdot \frac{7}{2} = \frac{49}{4}.

Por otro lado,

E[X2]=xx2P(X=x)=1+4+9+16+25+366=916. E[X^2] = \sum_x x^2 \cdot P(X = x) = \frac{1 + 4 + 9 + 16 + 25 + 36}{6} = \frac{91}{6}.

Problemas

HechoFuenteNombreDificultadTagsSolución
CSESCreating Strings IIFácilCombinatoricsSolución
CSESDistributing ApplesFácilCombinatoricsSolución
CFAlmost Identity PermutationsFácilCombinatoricsSolución
CFClose TuplesFácilCombinatorics, Binary SearchSolución
BronzeJust StallingFácilCombinatoricsSolución
CFAND-arrayFácilCombinatorics
Bubble CupBotsNormalCombinatoricsSolución
CSESXor PyramidNormalCombinatorics, BitwiseSolución
CFMed and MexNormalCombinatoricsSolución
ACStrivoreNormalCombinatoricsSolución
GoldMoo RouteNormalCombinatoricsSolución
GoldCowpatibilityNormalPIE, BitsetSolución
CFArenaNormalCombinatorics, DPSolución
GoldHelp YourselfNormalCombinatorics, Prefix SumsSolución
CFFancy StackDifícilCombinatorics, DPSolución
GoldCow CampDifícilCombinatorics, Probability, Math, Binary SearchSolución