Professional Layer
Pista 1
Sea el GCD de los valores del arreglo, . ¿Cuál es la cota sobre el número de primos en la factorización de ?
Pista 2
Si jugamos un juez, deberíamos intentar eliminar un conjunto específico de primos de su valor de en una partida.
Sea el número de primos en la factorización de . Como , podemos considerar DP con máscaras de bits, y potencialmente una DP de fusión de subconjuntos .
Pista 3
Con , ejecutar una DP por juez es demasiado lento. ¿Cómo podemos comprimir el número de jueces a una cantidad más manejable?
Pista 4
Para cada valor de , quitamos todos los factores primos que no están en la factorización de . Con las cotas dadas, hay a lo sumo valores distintos de en cualquier entrada. Como cada juez que juguemos debería quitar al menos un primo de , solo consideramos los jueces más baratos para cada valor de .
Sin embargo, hacer de forma naive una DP de fusión de subconjuntos en cada uno corre en , donde es el número de valores distintos de . ¿Cómo podemos reducir el número de transiciones de DP a considerar?
Solución
Explicación
Sea el GCD de los valores del arreglo, el conjunto de primos en la factorización de , y . Si , entonces la respuesta es .
Si jugamos un juez, deberíamos eliminar un subconjunto específico de primos de de su valor de . Como el producto de los primeros primos ya supera , tenemos
lo que motiva un enfoque con máscaras de bits.
Una DP naive con máscaras tendría como la suma mínima de valores de experiencia para quitar todos los primos en con jueces. Esto requeriría actualizar nuestra tabla de DP por cada juez, lo que da una complejidad temporal de , que es demasiado lenta para este problema.
Comprimir jueces
Para cada valor de , quitamos todos los factores primos que no están en la factorización de , ya que no afectan la respuesta. Después de quitar los primos innecesarios, hay a lo sumo valores distintos de en cualquier entrada.
Calcular esta cota
En el peor caso, es igual al producto de los primeros primos más pequeños, donde . Esto es porque cualquier valor de debe compartir los mismos primos que y además ser divisible por . Por lo tanto, el exponente de cada primo debería ser siempre , y los primos deberían minimizarse.
Para derivar la cota de forma rigurosa, hay que iterar sobre todos los valores de y calcular el número de valores distintos que son posibles. Se puede mostrar que y maximiza el número de valores distintos de , con la cota exacta siendo .
Para una estimación un poco más intuitiva, fingimos que todos los primos son equivalentes en valor, aunque siguen siendo distintos. Usamos para este ejemplo.
Si todos los primos equivalen a , entonces un número puede estar compuesto de a lo sumo primos, ya que . Suponiendo que está compuesto de factores primos, usamos estrellas y barras para hallar que hay valores posibles de . Esto se maximiza cuando , lo que da una estimación de .
Aunque esta cota estimada no es precisa, ayuda a mostrar que el número de jueces que consideramos se puede reducir considerablemente desde .
Cualquier juez útil debe eliminar al menos un factor primo de , así que jugamos a lo sumo jueces. Por lo tanto, consideramos a lo sumo jueces para cada valor único de , y siempre es óptimo elegir los de menor experiencia.
Esto reduce el número de jueces a a lo sumo , y la complejidad de nuestra DP a , donde es el número de valores únicos de .
Optimizar transiciones válidas
Para eliminar un conjunto de primos de un valor de , debemos dividir todas las potencias de primos relevantes. Si el producto de estas potencias de primos es , entonces quitar este conjunto de primos de es posible.
Formalmente, sea la potencia completa del primo que divide a . Podemos quitar un conjunto si
ya que este producto es el menor divisor de que contiene cada primo de .
El siguiente código calcula de forma eficiente el costo de quitar cada subconjunto de :
std::vector<ll> mask_cost(1 << m, 1);
for (int j = 1; j < (1 << m); j++) {
// we exclude least significant bit, and calculate contribution
// from that bit in our cost to remove it
int lsb = __builtin_ctz(j);
mask_cost[j] = mask_cost[j ^ (1 << lsb)];
ll val = a[i];
while (val % primes[lsb] == 0) {
// since mask_cost grows fast, overflow guard is needed!
if (mask_cost[j] > INF / primes[lsb]) break;
val /= primes[lsb];
mask_cost[j] *= primes[lsb];
}
if (mask_cost[j] <= k) {
// process this mask's transition by updating the DP table
}
}Como redujimos el número de jueces a considerar, podemos permitirnos calcular todo el costo de quitar subconjuntos para cada juez. Sin embargo, el número de transiciones sigue siendo demasiado grande.
Una vez más, podemos reducir el número de transiciones con la cota sobre el número máximo de jueces. Un estado de transición consiste en , que tiene hasta combinaciones. ¡Pero solo necesitamos las mejores transiciones para cada máscara de transición! Esto reduce el número de transiciones a .
Implementar la DP
El algoritmo general es el siguiente:
- Primero, quitamos todos los jueces redundantes, quedándonos con los jueces de menor experiencia para cada valor distinto de . Esto da jueces a considerar. Nótese que quitamos todas las potencias de primos que no están en de los valores de .
- Calculamos todas las máscaras de transición posibles para cada juez restante. Para una
máscara de transición posible, solo consideramos este juez si está entre los
de menor experiencia con esta máscara. Calculamos
inv(i), que guarda las máscaras donde el juez está entre los mejores. - Como no podemos reutilizar jueces, iteramos sobre los jueces en orden, actualizando nuestra tabla de DP para las transiciones que cada juez provee.
El siguiente código muestra las transiciones de DP:
// dp[# of judges used][mask] = minimum sum of experiences
std::vector dp(m + 1, std::vector<ll>(1 << m, INF));
dp[0][0] = 0;
for (int i = 0; i < n; i++) {
for (int j = m; j >= 1; j--) {
for (int mask : inv[i]) {
for (int super = mask; super < (1 << m); super = (super + 1) | mask) {
dp[j][super] = std::min(dp[j][super], dp[j - 1][super ^ mask] + e[i]);
}
}
}
}Esto es una implementación de DP de fusión de subconjuntos con máscaras de bits. Aunque una transición individual puede actualizar hasta estados, la complejidad total se amortiza a .
Con estas optimizaciones, obtenemos un tiempo de ejecución final de , que corre cómodamente dentro del límite de tiempo si se implementa de forma eficiente.
Implementación
Complejidad temporal: , donde es el número de jueces distintos.
#include <bits/stdc++.h>
using ll = long long;
constexpr ll INF = 1e18;
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
ll k;
std::cin >> n >> k;
ll g = 0;
std::vector<ll> a(n);
for (int i = 0; i < n; i++) {
std::cin >> a[i];
g = std::gcd(g, a[i]);
}
std::vector<int> e(n);
for (int i = 0; i < n; i++) { std::cin >> e[i]; }
if (g == 1) {
// we don't need to do anything
std::cout << 0 << '\n';
return 0;
}
// prime factorize the GCD
std::vector<ll> primes;
ll tmp = g;
for (int i = 2; 1LL * i * i <= tmp; i++) {
if (tmp % i == 0) {
primes.push_back(i);
while (tmp % i == 0) { tmp /= i; }
}
}
if (tmp > 1) { primes.push_back(tmp); }
const int m = primes.size();
// we process items in sorted order for easier processing of
// 'equivalent' judges (judges who cover same transition masks)
std::vector<int> ord(n);
std::iota(ord.begin(), ord.end(), 0);
std::sort(ord.begin(), ord.end(),
[&](int x, int y) -> bool { return e[x] < e[y]; });
std::map<ll, int> vis;
std::vector<int> seen_masks(1 << m);
std::vector<std::vector<int>> inv(n);
for (int i : ord) {
ll val = a[i];
// calculate the product of prime powers for the
// primes in our GCD (since only those primes matter)
ll cost = 1;
for (int j : primes) {
while (val % j == 0) {
cost *= j;
val /= j;
}
}
// we only use at most m equivalent judges
if (vis[cost] >= m) continue;
vis[cost]++;
// mask_cost[mask] = division value to remove primes in mask from the GCD
std::vector<ll> mask_cost(1 << m, 1);
for (int j = 1; j < (1 << m); j++) {
// we exclude least significant bit, and calculate contribution
// from that bit in our cost to remove it
int lsb = __builtin_ctz(j);
mask_cost[j] = mask_cost[j ^ (1 << lsb)];
val = a[i];
while (val % primes[lsb] == 0) {
// since mask_cost grows fast, overflow guard is needed!
if (mask_cost[j] > INF / primes[lsb]) break;
val /= primes[lsb];
mask_cost[j] *= primes[lsb];
}
// for a given transition (i.e. a set of primes we can 'clear'),
// we only care about the m cheapest judges for that transition
if (mask_cost[j] <= k && seen_masks[j] < m) {
seen_masks[j]++;
inv[i].push_back(j);
}
}
}
// dp[# of judges used][mask] = minimum sum of experiences
std::vector dp(m + 1, std::vector<ll>(1 << m, INF));
dp[0][0] = 0;
for (int i = 0; i < n; i++) {
for (int j = m; j >= 1; j--) {
for (int mask : inv[i]) {
for (int super = mask; super < (1 << m); super = (super + 1) | mask) {
dp[j][super] =
std::min(dp[j][super], dp[j - 1][super ^ mask] + e[i]);
}
}
}
}
ll res = INF;
for (int i = 0; i <= m; i++) {
if (dp[i][(1 << m) - 1] < INF) { res = std::min(res, dp[i][(1 << m) - 1] * i); }
}
std::cout << (res < INF ? res : -1) << '\n';
}