Counting Reorders
Counting Reorders
Calcular el número de formas en que se pueden reordenar los caracteres de un string de modo que no haya dos caracteres adyacentes iguales.
Estrategia: inclusión-exclusión
La restricción “ningún par de caracteres adyacentes son iguales” es difícil de contar de forma directa. En su lugar, usamos el principio de inclusión-exclusión (PIE) para transformar el problema en contar arreglos con un número específico de conexiones “malas”.
Definir “pares malos”
Sea un par malo una instancia en la que dos caracteres idénticos se colocan uno al lado del otro (p. ej., AA). Queremos encontrar el número de arreglos con exactamente cero pares malos.
En lugar de contar “exactamente cero” de forma directa, PIE nos dice que contemos arreglos en los que hemos forzado adyacencias específicas a ser malas. La fórmula para arreglos con cero pares malos es:
Por qué funciona
Cuando forzamos adyacencias, no estamos diciendo que el string tenga solo pares malos; podría tener más. Sin embargo, el signo alternado asegura que cualquier arreglo que realmente contenga pares malos (donde ) se cuente un total de:
Esto deja solo los arreglos con cero pares malos en nuestra suma final, cada uno contado exactamente una vez.
Conectar PIE con segmentos
Para calcular las “formas de forzar pares malos”, tratamos cada adyacencia forzada como un “enlace” que fusiona dos caracteres idénticos en un único bloque. Si tenemos un string de longitud total y usamos enlaces para forzar adyacencias, nos quedan segmentos independientes:
Esto motiva nuestra programación dinámica: si podemos contar cuántas formas hay de formar exactamente segmentos, podemos aplicar el signo de PIE a ese conteo para llegar a nuestra respuesta final.
Explicación paso a paso
1. Preprocesamiento
Contamos la frecuencia f de cada carácter. También precomputamos coeficientes binomiales usando la lógica del triángulo de Pascal. Esto nos permite acceder a combinaciones en tiempo mientras procesamos la DP.
2. El estado de programación dinámica
Definimos un estado de DP
i: hemos procesado los primerositipos de caracteres distintos.j: el número total de segmentos formados por estos caracteres.
3. La transición
Al introducir un nuevo carácter con frecuencia f, decidimos partirlo en k segmentos (). Esta transición involucra:
-
Partir los caracteres: formas de dividir
fítems idénticos enksegmentos no vacíos (problema de estrellas y barras): -
Fusionar con segmentos existentes: formas de intercalar estos
ksegmentos nuevos entre losjsegmentos ya procesados:
La regla de actualización:
4. Simplificación final de paridad
Después de procesar todos los grupos de caracteres, agregamos los valores en dp[groups][i] usando el signo de PIE . El signo depende de la paridad de :
- Si es par, el signo es positivo (+).
- Si es impar, el signo es negativo (-).
Esto es equivalente a verificar si es par: if ((i + n) % 2 == 0).
Implementación
Complejidad temporal:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 1e9 + 7;
const int MAX_N = 5005;
const int ALPH = 26;
ll nCr[MAX_N][MAX_N]; // Binomial coefficients table
ll dp[ALPH + 3][MAX_N]; // dp[i][j]: i-th char group, j total segments
ll freq[ALPH]; // Frequency of each character
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string s;
cin >> s;
int n = s.size();
// 1. Count Frequencies
for (char c : s) freq[c - 'a']++;
// Filter out characters that don't appear in the string
vector<int> counts;
counts.push_back(0); // Dummy 0-index
for (int i = 0; i < ALPH; i++)
if (freq[i]) counts.push_back(freq[i]);
// 2. Precompute nCr (Pascal's Triangle)
nCr[0][0] = 1;
for (int i = 1; i <= n; i++) {
nCr[i][0] = 1;
for (int j = 1; j <= n; j++)
nCr[i][j] = (nCr[i - 1][j - 1] + nCr[i - 1][j]) % MOD;
}
// 3. Dynamic Programming
dp[0][0] = 1;
int used_len = 0; // Tracks number of segments processed so far
for (int i = 1; i < (int)counts.size(); i++) {
int f = counts[i]; // Frequency of current char
// Iterate over previous number of segments (j)
for (int j = 0; j <= used_len; j++) {
// Iterate over how many segments (k) we split the current char into
for (int k = 1; k <= f; k++) {
ll ways = dp[i - 1][j];
// Ways to split frequency f into k parts
ways = (ways * nCr[f - 1][k - 1]) % MOD;
// Ways to interleave k new parts into j existing parts
ways = (ways * nCr[j + k][k]) % MOD;
// Update DP state
dp[i][j + k] = (dp[i][j + k] + ways) % MOD;
}
}
used_len += f;
}
// 4. Inclusion-Exclusion
ll answer = 0;
int groups = counts.size() - 1;
for (int i = 0; i <= n; i++) {
// Apply PIE based on parity
if ((i + n) % 2 == 0) answer = (answer + dp[groups][i]) % MOD;
else answer = (answer - dp[groups][i] + MOD) % MOD;
}
cout << answer << '\n';
return 0;
}import sys
MOD = 10**9 + 7
MAX_N = 5005
ALPH = 26
s = sys.stdin.readline().strip()
n = len(s)
# frequency of each character
freq = [0] * ALPH
for c in s:
freq[ord(c) - ord("a")] += 1
# keep only non-zero frequencies
counts = [0]
for f in freq:
if f:
counts.append(f)
# precompute binomial coefficients (Pascal triangle)
nCr = [[0] * (n + 1) for _ in range(n + 1)]
nCr[0][0] = 1
for i in range(1, n + 1):
nCr[i][0] = 1
for j in range(1, i + 1):
nCr[i][j] = (nCr[i - 1][j - 1] + nCr[i - 1][j]) % MOD
# dp[i][j] = ways using first i characters to form length j
dp = [[0] * (n + 1) for _ in range(len(counts))]
dp[0][0] = 1
used_len = 0
# process each character group
for i in range(1, len(counts)):
f = counts[i]
for j in range(used_len + 1):
if dp[i - 1][j] == 0:
continue
for k in range(1, f + 1):
ways = dp[i - 1][j] * nCr[f - 1][k - 1] % MOD * nCr[j + k][k] % MOD
dp[i][j + k] = (dp[i][j + k] + ways) % MOD
used_len += f
# inclusion-exclusion by parity
answer = 0
groups = len(counts) - 1
for i in range(n + 1):
if (i + n) % 2 == 0:
answer = (answer + dp[groups][i]) % MOD
else:
answer = (answer - dp[groups][i]) % MOD
print(answer % MOD)