Longest Beautiful Subsequence
Explicación
Resolver este problema de una sola vez es extremadamente difícil: recorramos primero las subtareas.
Subtareas 1-2
Debido a las cotas pequeñas de , basta una solución .
Definimos como la longitud de la LBS (subsecuencia bella más larga) más larga que termina en , y como todos los tales que tiene bits. Transicionamos iterando sobre todos los donde . Para reconstruir una solución óptima definimos otro arreglo , donde es el índice más óptimo a incluir justo antes del índice .
Consideremos el primer caso de ejemplo: , , y .
- Caso base: para todo (cualquier número suelto forma una LBS) y .
- : intentamos extender la LBS más larga que termina en . Como , nuestra nueva LBS es válida, y actualizamos y .
- : intentamos extender la LBS tanto desde como desde .
- : y
- : y
- Tomamos el mayor de los dos valores de DP, así que y .
- : intentamos extender desde , y .
- : no hay transición
- : y
- : y
- Tomando la mayor de las dos opciones, y .
- Respuesta final:
- Reconstrucción de una solución:
- Usando nuestro arreglo y el índice final de la LBS, podemos hallar repetidamente el índice óptimo a incluir antes del actual.
- elegimos tal que
- inicializamos como .
- Agregamos repetidamente al frente de .
- , así que terminamos.
- Por lo tanto, nuestra respuesta final (indexada desde cero) será
Implementación
Complejidad temporal:
Código
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) { cin >> a[i]; }
vector<int> k(n);
for (int i = 0; i < n; i++) { cin >> k[i]; }
vector<int> dp(n, 1);
int ans = 1;
// best_i: índice más óptimo en el que terminar
int best_i = 0;
// prv[i] = índice óptimo a incluir justo antes de i
vector<int> prv(n);
// inicialmente, prv[i] = i
iota(prv.begin(), prv.end(), 0);
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
// bc[a[j]][a[i]] == k[i]
if (__builtin_popcount(a[j] & a[i]) == k[i]) {
if (dp[j] + 1 > dp[i]) {
prv[i] = j;
dp[i] = dp[j] + 1;
}
}
}
if (dp[i] > ans) {
ans = dp[i];
best_i = i;
}
}
cout << ans << endl;
vector<int> res;
while (prv[best_i] != best_i) {
res.push_back(best_i);
best_i = prv[best_i];
}
res.push_back(best_i);
reverse(res.begin(), res.end());
for (int x : res) { cout << x + 1 << " "; }
}Subtarea 3
Aunque las cotas de ahora son mucho mayores (), el valor máximo de ahora es solo . Podemos usar esto para optimizar la solución: en lugar de recorrer cada índice previo posible, recorremos todos los valores previos posibles.
Más concretamente, definimos como la longitud de la LBS más larga que termina con el valor (en lugar del índice) y es un subconjunto de los primeros números. Tenemos dos transiciones para cada índice :
- Incluir : para calcular , transicionamos desde para todo .
- No incluir : para todo .
Esto mejora nuestra complejidad temporal a . Notemos que, como solo depende de , podemos eliminar la primera dimensión.
Consideremos de nuevo el primer caso de ejemplo: , , y . El valor máximo es .
- Caso base: .
- todos los demás valores de DP empiezan en 0
- .
- recorremos todos los estados donde y está en . Los valores de que cumplen estas condiciones son , y .
- De estos , tiene el mayor valor de
len, así que:- .
- .
- .
- .
- aquí, y tiene el mayor valor, así que:
- .
- .
- .
- aquí, y tiene el mayor valor, así que:
- .
- aquí, y tiene el mayor valor, así que:
- .
- .
- .
- aquí, y tiene el mayor valor, así que:
- Respuesta final: el máximo valor
lende DP, .
Implementación
Complejidad temporal:
Código
#include <bits/stdc++.h>
using namespace std;
using pii = pair<int, int>;
const int B = 8;
struct State {
int len, end;
};
int main() {
// preprocess:
// bc[i][j] = bit_count(i & j)
vector<vector<int>> bc(1 << B, vector<int>(1 << B));
for (int i = 0; i < (1 << B); i++) {
for (int j = 0; j < (1 << B); j++) { bc[i][j] = __builtin_popcount(i & j); }
}
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
vector<int> k(n);
for (int i = 0; i < n; i++) cin >> k[i];
int ans = 1;
// best_i: índice más óptimo en el que terminar
int best_i = 0;
vector<int> prv(n); // prv[i]: mejor índice a incluir antes de i
iota(prv.begin(), prv.end(), 0); // prv[i] = i
vector<State> dp1(1 << B, State{-1, -1});
for (int i = 0; i < n; i++) {
if (dp1[a[i]].len == -1) {
dp1[a[i]].len = 1;
dp1[a[i]].end = i;
}
for (int j = 0; j < (1 << B); j++) {
if (bc[j][a[i]] == k[i] && dp1[j].len + 1 > dp1[a[i]].len) {
dp1[a[i]].len = dp1[j].len + 1;
dp1[a[i]].end = i;
prv[i] = dp1[j].end;
}
}
if (dp1[a[i]].len > ans) {
ans = dp1[a[i]].len;
best_i = i;
}
}
cout << ans << endl;
vector<int> res;
while (prv[best_i] != best_i) {
res.push_back(best_i);
best_i = prv[best_i];
}
res.push_back(best_i);
reverse(res.begin(), res.end());
for (int x : res) { cout << x + 1 << " "; }
}Subtarea 3 (solución alternativa)
En nuestra solución previa para esta Subtarea 3, el bucle de parece repetitivo y motiva restringir de alguna forma el espacio de búsqueda de estados de DP. ¿Cómo podemos procesar rápido todos los en ?
La forma más extrema de optimizar las transiciones es incrustar las restricciones de directamente en el estado de DP.
Concretamente, definimos como el valor máximo de sobre todos los en .
Para entender mejor esta redefinición de nuestro estado de DP, miremos de nuevo el ejemplo. Notemos que, en el cálculo de , procesamos todos los uno por uno. Sin embargo, en nuestro estado recién definido, podemos obtener la solución más óptima sobre todos los valores simplemente consultando .
En general, es ahora una transición para hallar la LBS más larga que termina en el índice .
Sin embargo, cada estado ahora encapsula de nuestros estados originales. De forma similar, una LBS con valor final fijo afecta estados comparado con un estado : a saber, todos los estados donde y .
Implementación (dp2)
Código
#include <bits/stdc++.h>
using namespace std;
using pii = pair<int, int>;
const int B = 8;
struct State {
int len, end;
};
int main() {
// preprocess:
// bc[i][j] = bit_count(i & j)
vector<vector<int>> bc(1 << B, vector<int>(1 << B));
for (int i = 0; i < (1 << B); i++) {
for (int j = 0; j < (1 << B); j++) { bc[i][j] = __builtin_popcount(i & j); }
}
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
vector<int> k(n);
for (int i = 0; i < n; i++) cin >> k[i];
int ans = 1;
// best_i: índice más óptimo en el que terminar
int best_i = 0;
vector<int> prv(n); // prv[i]: mejor índice a incluir antes de i
iota(prv.begin(), prv.end(), 0); // prv[i] = i
vector<vector<State>> dp2(1 << B, vector<State>(B, State{0, -1}));
for (int i = 0; i < n; i++) {
// longitud de la LBS más larga que termina en i
int len = dp2[a[i]][k[i]].len + 1;
if (dp2[a[i]][k[i]].end != -1) { prv[i] = dp2[a[i]][k[i]].end; }
for (int j = 0; j < (1 << B); j++) {
State &affected_state = dp2[j][bc[a[i]][j]];
if (len > affected_state.len) { affected_state = State{len, i}; }
}
if (len > ans) {
ans = len;
best_i = i;
}
}
cout << ans << endl;
vector<int> res;
while (prv[best_i] != best_i) {
res.push_back(best_i);
best_i = prv[best_i];
}
res.push_back(best_i);
reverse(res.begin(), res.end());
for (int x : res) { cout << x + 1 << " "; }
}Subtarea 4 (solución completa)
En resumen, si es el último número de una LBS:
- (solo un valor posible de ) transición , estados afectados
- (hasta valores posibles de ) transición , estados afectados
Notemos que, no importa cómo definamos el estado de DP, estas dos complejidades siempre se multiplicarán a . Por lo tanto, queremos hallar una restricción tal que ambas complejidades sean . Esto motiva usar una mezcla de y . Como ya restringe bits, parece natural considerar aplicar sobre algunos bits de y sobre el resto.
Concretamente, sea el número representado por los bits más a la izquierda de y el número representado por los \texttt{bit\\_count}(x) - b bits más a la derecha. En nuestro nuevo estado de DP , vamos a:
- fijar (esta es la parte de nuestro nuevo estado)
- restringir de modo que (esta es la parte de nuestro nuevo estado)
¿Cuáles son ahora las complejidades temporales de transición y estados afectados?
Transición
Digamos que estamos procesando . Si tenemos tal que , entonces . Al considerar los lados izquierdo y derecho de y por separado, podemos reescribir como .
Ejemplo: = 01010, = 01101, = 3
01010 & 01101 = 01000 -> 1
010 & 011 = 010 -> 1
10 & 01 = 00 -> 0.
Como nuestro estado de DP fija , deberíamos reescribir esta ecuación para despejar : . Notemos que esta restricción coincide con nuestra restricción de de , así que tenemos y . En cuanto a , simplemente enumeramos las posibilidades; por lo tanto, .
En resumen, las transiciones hacia son todos los estados donde .
Como la única variable en el estado es , la complejidad de la transición es la cantidad de opciones para , que es .
Estados afectados
Ahora tenemos la longitud de la LBS más larga que termina en . ¿Qué estados afecta esto?
Empezando por lo obvio, . También tenemos la restricción de que . Como es una constante, depende únicamente de .
En resumen, los estados afectados por son todos los estados .
tiene que tener la misma cantidad de bits que , que tiene bits. Por lo tanto, tiene posibilidades.
Para resumirlo todo, la complejidad de la transición es y la complejidad de los estados afectados es . Para minimizar la suma de estas complejidades, deberíamos igualarlas. Como , hallamos que el valor óptimo de es .
Implementación
En la implementación de abajo:
- denota los 10 bits más a la izquierda de
- denota los 10 bits más a la derecha de
Complejidad temporal:
Código
#include <bits/stdc++.h>
using namespace std;
using pii = pair<int, int>;
const int N = 1e5;
const int B = 10;
int bc[1 << B][1 << B]; // bc[i][j] = bit_count(i & j)
struct State {
int len;
int end;
} dp[1 << B][1 << B][B + 1];
int main() {
// preprocess:
for (int i = 0; i < (1 << B); i++) {
for (int j = 0; j < (1 << B); j++) { bc[i][j] = __builtin_popcount(i & j); }
}
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
vector<int> k(n);
for (int i = 0; i < n; i++) cin >> k[i];
int ans = 1;
// best_i: índice más óptimo en el que terminar
int best_i = 0;
vector<int> prv(n); // prv[i]: mejor índice a incluir antes de i
iota(prv.begin(), prv.end(), 0); // prv[i] = i
for (int i = 0; i < n; i++) {
int l = a[i] >> B; // l(a[i])
int r = a[i] % (1 << B); // r(a[i])
int lbs = 1; // longitud de la lbs más larga que termina en i
// enumeramos todas las posibilidades para l(prev_num)
for (int j = 0; j < (1 << B); j++) {
// aquí, usamos el hecho de que
// bc[x][y] = bc[l(x)][l(y)] + bc[r(x)][r(y)],
// o bc[r(x)][r(y)] = bc[x][y] - bc[l(x)][l(y)]
// valor requerido de bc[r(x)][r(y)]
int needed = k[i] - bc[j][l];
if (needed < 0 || needed > B) continue;
if (dp[j][r][needed].len + 1 > lbs) {
lbs = dp[j][r][needed].len + 1;
prv[i] = dp[j][r][needed].end;
}
}
if (lbs > ans) {
ans = lbs;
best_i = i;
}
// actualizamos todas las respuestas que a[i] afecta
for (int j = 0; j < (1 << B); j++) {
State &new_state = dp[l][j][bc[r][j]];
if (lbs > new_state.len) {
new_state.len = lbs;
new_state.end = i;
}
}
}
cout << ans << endl;
vector<int> res;
while (prv[best_i] != best_i) {
res.push_back(best_i);
best_i = prv[best_i];
}
res.push_back(best_i);
reverse(res.begin(), res.end());
for (int x : res) { cout << x + 1 << " "; }
}