Grid Completion
Explicación
Este problema nos pide contar el número de formas válidas de completar la grilla respetando las restricciones impuestas por los caracteres A y B.
Cada fila puede contener:
- Ninguna celda marcada,
- Solo una
A, - Solo una
B, - O ambas.
De forma similar, cada columna puede o no contener ya una A o una B.
El objetivo es contar el número de permutaciones válidas de columnas asignadas a filas tales que:
- Se satisfacen todas las restricciones de
A, - Se satisfacen todas las restricciones de
B, - Y no ocurren conflictos.
Observación clave
Un enfoque de conteo directo es difícil porque las restricciones interactúan entre filas y columnas.
En lugar de contar solo permutaciones válidas:
- Contamos permutaciones bajo condiciones relajadas,
- Restamos configuraciones inválidas,
- Corregimos el sobreconteo usando el principio de inclusión-exclusión (PIE).
El sobreconteo ocurre cuando hay una violación.
¿Qué es una violación?
En nuestra construcción, a cada fila se le asigna una columna para A y una columna para B.
Una violación ocurre si estas dos asignaciones coinciden — es decir, tanto A como B terminan en la misma celda de una fila. Tal configuración es inválida.
El principio de inclusión-exclusión cuenta configuraciones en las que se permite que ciertas filas violen esta condición, y luego corrige el sobreconteo usando signos alternados.
Definiciones de categorías
Sea:
C0: número de filas que no contienen niAniBC1: número de filas que contienen unaBen la columnac, donde la columnacno contieneAC2: número de filas que contienen unaAen la columnac, donde la columnacno contieneBC3: número de columnas que no contienen niAniBC4: número de columnas sinAC5: número de columnas sinB
Estos valores resumen toda la flexibilidad disponible en la grilla.
Aplicar inclusión-exclusión
Iteramos sobre tres parámetros:
i: número de filas elegidas deC0j: número elegidas deC1k: número elegidas deC2
Para cada terna :
-
Elegir filas:
-
Elegir columnas coincidentes de las columnas libres:
-
Arreglar las columnas libres restantes:
-
Emparejar las filas y columnas seleccionadas:
-
Aplicar el signo alternado:
Este signo es el corazón de inclusión-exclusión:
- Sumar configuraciones con 0 violaciones,
- Restar las que tienen 1 violación,
- Volver a sumar las que tienen 2 violaciones,
- Restar 3 violaciones,
- Y así sucesivamente.
Fórmula final
La respuesta final es:
Esta fórmula cuenta todas las compleciones válidas mientras corrige el sobreconteo vía el principio de inclusión-exclusión.
Optimización adicional
La implementación directa evalúa todas las ternas (i, j, k), lo que da
complejidad temporal .
Para mejorar esto, fijamos un valor de i. Después de fijar i, la parte restante de la fórmula se vuelve:
Ahora observemos:
- Los términos de
jdependen solo dej. - Los términos de
kdependen solo dek. - No hay interacción entre
jyk.
Así que la suma doble se factoriza en:
Cada una de estas se puede computar en .
Implementación
Complejidad temporal:
Complejidad espacial:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 1e9 + 7;
const int MAXN = 505;
ll fact[MAXN], invfact[MAXN];
// BeginCodeSnip{Binary Exponentiation}
ll binpow(ll a, ll b) {
ll r = 1;
while (b) {
if (b & 1) r = r * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return r;
}
// EndCodeSnip
// BeginCodeSnip{Combination Formula}
ll nCr(int n, int r) {
if (r < 0 || r > n) return 0;
return fact[n] * invfact[r] % MOD * invfact[n - r] % MOD;
}
// EndCodeSnip
// BeginCodeSnip{Formula from Editorial}
ll formula(int C0, int C1, int C2, int C3, int C4, int C5) {
ll ans = 0;
for (int i = 0; i <= min(C0, C3); i++) {
// Compute sum over j
ll sumJ = 0;
for (int j = 0; j <= C1; j++) {
if (C4 - i - j < 0) continue;
ll cur = nCr(C1, j) * fact[C4 - i - j] % MOD;
if (j & 1) cur = (MOD - cur) % MOD;
sumJ = (sumJ + cur) % MOD;
}
// Compute sum over k
ll sumK = 0;
for (int k = 0; k <= C2; k++) {
if (C5 - i - k < 0) continue;
ll cur = nCr(C2, k) * fact[C5 - i - k] % MOD;
if (k & 1) cur = (MOD - cur) % MOD;
sumK = (sumK + cur) % MOD;
}
ll cur = 1;
cur = cur * nCr(C0, i) % MOD;
cur = cur * nCr(C3, i) % MOD;
cur = cur * fact[i] % MOD;
cur = cur * sumJ % MOD;
cur = cur * sumK % MOD;
if (i & 1) cur = (MOD - cur) % MOD;
ans = (ans + cur) % MOD;
}
return ans;
}
// EndCodeSnip
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
// BeginCodeSnip{Pre-Compute}
fact[0] = 1;
for (int i = 1; i <= n; i++) fact[i] = fact[i - 1] * i % MOD;
invfact[n] = binpow(fact[n], MOD - 2);
for (int i = n; i > 0; i--) invfact[i - 1] = invfact[i] * i % MOD;
// EndCodeSnip
vector<int> p(n, -1), q(n, -1);
vector<bool> inA(n, false), inB(n, false);
for (int i = 0; i < n; i++) {
string s;
cin >> s;
for (int j = 0; j < n; j++) {
if (s[j] == 'A') {
p[i] = j;
inA[j] = true;
}
if (s[j] == 'B') {
q[i] = j;
inB[j] = true;
}
}
}
int C0 = 0, C1 = 0, C2 = 0, C3 = 0, C4 = 0, C5 = 0;
for (int i = 0; i < n; i++) {
if (p[i] == -1 && q[i] == -1) C0++;
if (p[i] == -1 && q[i] != -1 && !inA[q[i]]) C1++;
if (p[i] != -1 && q[i] == -1 && !inB[p[i]]) C2++;
}
for (int i = 0; i < n; i++) {
if (!inA[i] && !inB[i]) C3++;
if (!inA[i]) C4++;
if (!inB[i]) C5++;
}
cout << formula(C0, C1, C2, C3, C4, C5) << "\n";
}