DP de suma sobre subconjuntos
| Fuente | Recurso | Notas |
|---|---|---|
| CF | SOS Dynamic Programming [Tutorial] | Buena explicación + lista de problemas |
| GFG | Sum over Subsets | Dynamic Programming | Recorre las soluciones de fuerza bruta |
| CF | Some SOS DP Insights | Caracterizar la SOS DP como sumas de prefijos multidimensionales |
| queuedlab | Sum over Subsets (SOS) DP | Caracterizar la SOS DP como sumas de prefijos multidimensionales (en coreano) |
Suma sobre subconjuntos (Sum Over Subsets, SOS) es una técnica usada para calcular de forma eficiente la suma de valores de todos los subconjuntos de un conjunto o máscara de bits dado.
Problema de suma sobre subconjuntos
Considerar un arreglo con elementos. Nuestro objetivo es calcular para todo . Aquí, representa la suma de los valores del arreglo para todos los subconjuntos (submáscaras) de . Es decir:
Por ejemplo, .
Solución
Fuerza bruta
La solución naive sería iterar sobre todos los pares de máscaras, sumando solo cuando una de ellas es un subconjunto de la otra (es decir, ).
vector<int> sos(1 << n);
for (int x = 0; x < (1 << n); x++) {
// iterate over all other sets and checks whether they're a subset of x
for (int i = 0; i < (1 << n); i++) {
if ((i & x) == i) { sos[x] += a[i]; }
}
}sos = [0] * (1 << n)
for x in range(1 << n):
# iterate over all other sets and checks whether they're a subset of x
for i in range(1 << n):
if (x & i) == i:
sos[x] += a[i]Esta solución tiene complejidad temporal , que es demasiado lenta para valores grandes de .
Solución optimizada: iterar sobre submáscaras
En lugar de iterar sobre las máscaras de bits para , podemos optimizar iterando solo sobre las máscaras subconjunto de usando la fórmula , que genera de forma eficiente todos los subconjuntos válidos de en orden inverso. Este enfoque omite combinaciones innecesarias, reduce de forma significativa la cantidad de iteraciones y mejora la complejidad temporal.
vector<int> sos(1 << n);
for (int x = 0; x < (1 << n); x++) {
sos[x] = a[0];
// iterate over all subsets of x directly
for (int i = x; i > 0; i = (i - 1) & x) { sos[x] += a[i]; }
}sos = [0] * (1 << n)
for x in range(1 << n):
sos[x] = a[0]
i = x
# iterate over all subsets of x directly
while i > 0:
sos[x] += a[i]
i = (i - 1) & x¿Cómo funciona esto?
Cuando restamos de una máscara de bits , el más a la derecha se vuelve , y todos los bits a su derecha se vuelven . Al hacer un AND bit a bit con , nos aseguramos de que solo se conserven los bits dentro de . Empezando con y aplicando repetidamente , se visitan todas las máscaras subconjunto de en orden inverso.
La operación garantiza que cada nuevo valor de es estrictamente menor que el anterior y también es una submáscara válida de . No hace falta comprobar explícitamente .
Complejidad temporal:
Demostración
La cantidad de máscaras subconjunto de una máscara de bits de tamaño (es decir, bits activados) es . Hay máscaras de bits con bits activados. Así, el número total de operaciones es:
Solución más rápida usando programación dinámica
Aunque el método anterior es mejor, todavía tiene algo de redundancia. Por ejemplo, si una máscara de bits tiene bits apagados, entonces se suma veces. Agrupando las máscaras que aparecen juntas con frecuencia, podemos precomputar y reutilizar sus sumas para eliminar adiciones repetidas.
Partición de máscaras subconjunto con
Definimos el conjunto de subconjuntos de de la siguiente forma:
En términos más simples, contiene todas las máscaras subconjunto de cuyos bits coinciden con los de excepto por los bits más a la derecha.
Por ejemplo:
Podemos descomponer de la siguiente forma:
- Si el -ésimo bit de es , entonces .
- Si el -ésimo bit de es :
- Subconjuntos con el -ésimo bit .
- Subconjuntos con el -ésimo bit . Así:
Usando la partición de arriba, definimos una tabla de DP donde:
Complejidad temporal:
vector<int> sos(1 << n);
vector<vector<int>> dp(1 << n, vector<int>(n + 1));
for (int x = 0; x < (1 << n); x++) {
dp[x][0] = a[x];
for (int i = 0; i < n; i++) {
dp[x][i + 1] = dp[x][i];
if (x & (1 << i)) { dp[x][i + 1] += dp[x ^ (1 << i)][i]; }
}
sos[x] = dp[x][n];
}sos = [0] * (1 << n)
dp = [[0] * (n + 1) for _ in range(1 << n)]
for x in range(1 << n):
dp[x][0] = a[x]
for i in range(n):
dp[x][i + 1] = dp[x][i]
if x & (1 << i):
dp[x][i + 1] += dp[x ^ (1 << i)][i]
sos[x] = dp[x][n]Uso de memoria optimizado
Como solo depende de , podemos reutilizar el arreglo de DP.
sos = a;
for (int i = 0; i < n; i++) {
for (int x = 0; x < (1 << n); x++) {
if (x & (1 << i)) { sos[x] += sos[x ^ (1 << i)]; }
}
}sos = a[:]
for i in range(n):
for x in range(1 << n):
if x & (1 << i):
sos[x] += sos[x ^ (1 << i)]SOS DP como suma de prefijos N-dimensional
Antes de seguir, revisitemos las sumas de prefijos 2D. Dado un arreglo , el arreglo de sumas de prefijos se define como:
El enfoque estándar usa inclusión-exclusión:
Aunque este enfoque funciona para grillas 2D, tiene una limitación importante: a medida que aumenta el número de dimensiones, la cantidad de términos que hay que sumar o restar también crece de forma exponencial, lo que lo vuelve ineficiente para grillas de más dimensiones.
Un enfoque simple y más escalable sería barrer a lo largo de cada eje de a uno y calcular la suma de prefijos paso a paso:
// Initialize
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) { S[i][j] = A[i][j]; }
}
// Sweep along x-axis
for (int i = 1; i < n; i++) {
for (int j = 0; j < m; j++) { S[i][j] += S[i - 1][j]; }
}
// Sweep along y-axis
for (int i = 0; i < n; i++) {
for (int j = 1; j < m; j++) { S[i][j] += S[i][j - 1]; }
}# Initialize
for i in range(n):
for j in range(m):
S[i][j] = A[i][j]
# Sweep along the x-axis
for i in range(1, n):
for j in range(m):
S[i][j] += S[i - 1][j]
# Sweep along the y-axis
for i in range(n):
for j in range(1, m):
S[i][j] += S[i][j - 1]Este enfoque se generaliza a más dimensiones.
Digamos que queremos calcular el arreglo de sumas de prefijos para una grilla 4D. Podemos calcularlo barriendo a lo largo de cada eje de una grilla 4D, de a uno:
- Después de barrer a lo largo del eje x, contiene la suma de
donde:
- y .
- Después de barrer a lo largo del eje y, contiene la suma de
donde:
- y .
- Después de barrer a lo largo del eje z, contiene la suma de
donde:
- .
- Por último, después de barrer a lo largo del eje w,
contiene la suma de donde:
- y .
Si extendemos esta idea a dimensiones, esto es lo que ocurre después de barrer a lo largo del -ésimo eje. Para cada vector -dimensional , contiene la suma de los valores de donde las primeras coordenadas son menores o iguales que , y las coordenadas restantes coinciden con . ¿Suena familiar?
Comparemos esto con el problema SOS. Si pensamos cada bit de una máscara de bits como su propio eje, entonces una máscara de bits con bits se puede ver como un vector -dimensional, donde cada coordenada es o . Una submáscara de corresponde a un vector -dimensional donde cada coordenada es menor o igual que .
Por lo tanto, cuando interpretamos la máscara de bits como un vector -dimensional, coincide con la definición de una suma de prefijos -dimensional.
Aplicando el algoritmo de barrido a lo largo de cada eje, obtenemos la solución de SOS DP con memoria optimizada mencionada antes, lo que demuestra que la SOS DP es de hecho una suma de prefijos n-dimensional.
F = A;
for (int i = 0; i < n; i++) { // Sweep along the i-th axis
for (int x = 0; x < (1 << n); x++) {
if (x & (1 << i)) // If the i-th bit is set, accumulate
F[x] += F[x ^ (1 << i)];
}
}F = A[:]
for i in range(n): # Sweep along the i-th axis
for x in range(1 << n):
if x & (1 << i): # If the i-th bit is set, accumulate
F[x] += F[x ^ (1 << i)]| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| CF | Vowels | Fácil | Bitmasks, SOS DP | en el módulo |
Explicación
Primero, pensar cada palabra como una combinación de letras representada por
una máscara de bits. Por ejemplo, bcd = 0b1110, y ada = 0b1001, donde
cada bit representa una letra. También llevaremos la cuenta de con qué
frecuencia aparece cada máscara de bits en el diccionario.
Luego, usamos SOS DP para calcular la cantidad de palabras disjuntas de la máscara (es decir, palabras que contienen ninguna de las vocales de la máscara). Esto significa que la máscara de bits de la palabra debe ser un subconjunto del complemento de la máscara (~mask).
Una vez calculado esto, la cantidad de palabras válidas para un subconjunto es simplemente porque una palabra es válida si contiene al menos una vocal del subconjunto, es decir, no es disjunta de la máscara. Por último, elevamos al cuadrado el conteo de palabras válidas para cada subconjunto, hacemos XOR de todos esos valores al cuadrado, y eso nos da la respuesta.
Implementación
Complejidad temporal:
#include <iostream>
#include <string>
const int M = 24;
int sos[1 << M];
int main() {
int n;
std::cin >> n;
for (int i = 0; i < n; i++) {
std::string st;
std::cin >> st;
int mask = (1 << (st[0] - 'a')) | (1 << (st[1] - 'a')) | (1 << (st[2] - 'a'));
sos[mask] += 1;
}
for (int i = 0; i < M; i++) {
for (int mask = 0; mask < (1 << M); mask++) {
if (mask & (1 << i)) { sos[mask] += sos[mask ^ (1 << i)]; }
}
}
int res = 0;
for (int mask = 0; mask < (1 << M); mask++) {
// sos[mask] now contains the number of words whose bitmasks are subsets of mask
int valid_words = n - sos[(1 << M) - 1 - mask];
res ^= valid_words * valid_words;
}
std::cout << res << '\n';
}M = 24
sos = [0] * (1 << M)
n = int(input())
for i in range(n):
st = input()
mask = (
(1 << (ord(st[0]) - 97)) | (1 << (ord(st[1]) - 97)) | (1 << (ord(st[2]) - 97))
)
sos[mask] += 1
for i in range(M):
for mask in range(1 << M):
if mask & (1 << i):
sos[mask] += sos[mask ^ (1 << i)]
res = 0
for mask in range(1 << M):
# sos[mask] now contains the number of words whose bitmasks are subsets of mask
valid_words = n - sos[(1 << M) - 1 - mask]
res ^= valid_words * valid_words
print(res)Problemas generales
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| CSES | Bit Problem | Fácil | SOS DP | Solución | |
| Platinum | Problem Setting | Fácil | Combo, DP | — | |
| CF | Bits and Pieces | Normal | Bitmasks, SOS DP | Solución | |
| Platinum | Sleeping in Class | Normal | NT, Prefix Sums | — | |
| CF | Compatible Numbers | Normal | Bitmasks, SOS DP | — | |
| CF | Jzzhu and Numbers | Normal | Bitmasks, SOS DP | — | |
| kilonova | ★ XorTransform | Difícil | Bitmasks, SOS DP, NT | Solución | |
| CF | Varying Kibibits | Difícil | Bitmasks, DP | — | |
| JOI | 2018 - Snake Escaping | Difícil | SOS DP | Solución | |
| CF | Wise Men | Insano | Bitmasks, DP, SOS DP | — |