Subsecuencia creciente más larga
| Fuente | Recurso | Notas |
|---|---|---|
| cp-algo | Longest Increasing Subsequence | Una guía completa (cubre casi todo lo de este módulo) |
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| CSES | Increasing Subsequence | Fácil | en el módulo |
Tutorial
En este tutorial, sea el arreglo para el que queremos hallar la LIS.
Solución lenta
Complejidad temporal:
| Fuente | Recurso | Notas |
|---|---|---|
| CPH | 7.2 - LIS | solución lenta |
Sea la longitud de la subsecuencia creciente más larga que termina en . Entonces podemos calcular de forma naive (y así la LIS) en tiempo :
#include <vector>
using namespace std;
int find_lis(const vector<int> &a) {
int lis = 0;
vector<int> dp(a.size(), 1);
for (int i = 0; i < a.size(); i++) {
for (int j = 0; j < i; j++) {
if (a[j] < a[i]) { dp[i] = max(dp[i], dp[j] + 1); }
}
lis = max(lis, dp[i]);
}
return lis;
}public static int findLis(int[] a) {
int lis = 0;
int[] dp = new int[a.length];
Arrays.fill(dp, 1);
for (int i = 0; i < a.length; i++) {
for (int j = 0; j < i; j++) {
if (a[j] < a[i]) { dp[i] = Math.max(dp[i], dp[j] + 1); }
}
lis = Math.max(lis, dp[i]);
}
return lis;
}from typing import List
def find_lis(a: List[int]) -> int:
lis = 0
dp = [1] * len(a)
for i in range(len(a)):
for j in range(i):
if a[j] < a[i]:
dp[i] = max(dp[i], dp[j] + 1)
lis = max(lis, dp[i])
return lis¡Podemos hacerlo mucho mejor que esto!
Solución rápida
Complejidad temporal:
Sea un arreglo (indexado desde cero) donde es el menor elemento entre los primeros elementos de sobre el que termina una secuencia creciente de longitud (o si no existe tal elemento).
Por ejemplo, sea nuestro arreglo .
En este caso, sería . Algunas secuencias de ejemplo que satisfacen esto son , y .
Lema 1: forma una secuencia estrictamente creciente.
Demostración: Supongamos por contradicción que para algún se tiene . Sean una subsecuencia creciente cualquiera de longitud que termina en (así ). Nótese que es una subsecuencia creciente de longitud . Por lo tanto, tiene que ser mayor que . A partir de eso, podemos escribir esta ecuación: , que es exactamente lo que queríamos.
Lema 2: La longitud de la LIS que termina en es igual al menor índice (con indexación desde cero) tal que .
Demostración: Sea como en el enunciado. Ante todo, como , existe una secuencia creciente de longitud que termina en , ya que podemos anexar al final de la secuencia de longitud . Por el Lema 1, es estrictamente creciente, así que para todo . Esto significa que no puede haber una LIS que termine en más larga que .
Lema 3: A lo sumo 1 elemento difiere entre y .
Demostración: Sea como en el Lema 2. Hay que asignar igual a porque . para todo , porque para todo y no hay secuencias crecientes que terminen en para .
Para hallar y actualizar el descrito en tiempo , podemos usar una lista con búsqueda binaria, o un conjunto ordenado (como se muestra en la solución de PCB).
#include <algorithm>
#include <vector>
using namespace std;
int find_lis(const vector<int> &a) {
vector<int> dp;
for (int i : a) {
int pos = lower_bound(dp.begin(), dp.end(), i) - dp.begin();
if (pos == dp.size()) {
// we can have a new, longer increasing subsequence!
dp.push_back(i);
} else {
// oh ok, at least we can make the ending element smaller
dp[pos] = i;
}
}
return dp.size();
}public static int findLis(int[] a) {
ArrayList<Integer> dp = new ArrayList<Integer>();
for (int i : a) {
int pos = Collections.binarySearch(dp, i);
pos = pos < 0 ? Math.abs(pos + 1) : pos;
if (pos == dp.size()) {
// we can have a new, longer increasing subsequence!
dp.add(i);
} else {
// oh ok, at least we can make the ending element smaller
dp.set(pos, i);
}
}
return dp.size();
}from typing import List
from bisect import bisect_left
def find_lis(arr: List[int]) -> int:
min_endings = []
for i in arr:
pos = bisect_left(min_endings, i)
if pos == len(min_endings): # we can have a new, longer increasing subsequence!
min_endings.append(i)
else: # oh ok, at least we can make the ending element smaller
min_endings[pos] = i
return len(min_endings)Solución rápida (estructuras de datos para RMQ)
Complejidad temporal: , aunque quizá con un factor constante peor que la solución anterior.
Nótese que la siguiente solución asume conocimiento de una estructura de datos PURQ básica para RMQ (consulta de máximo en un rango). Implementaciones adecuadas incluyen un Árbol de Segmentos o un Árbol de Fenwick modificado, pero ambos se consideran en general temas de nivel Platino. Sin embargo, esta solución tiene la ventaja de ser más intuitiva si se está derivando LIS sobre la marcha.
Sea la longitud de la LIS de que termina con el elemento . El caso base es, obviamente, (tenemos una LIS que contiene solo , de longitud ). Como la LIS exhibe subestructura óptima, transicionamos como sigue, procesando de izquierda a derecha (lo cual debemos hacer para mantener la propiedad de que nuestra subsecuencia crece de forma estricta en esa dirección):
Si es ahora una estructura de datos para RMQ, el cambio en es simplemente una actualización puntual, mientras que el cálculo de es una consulta de rango.
La respuesta final es simplemente un RMQ de extremo a extremo (o, de forma alternativa, se puede mantener un máximo acumulado de la respuesta en otra variable y actualizarlo con cada update). Este método de hecho nos da la libertad de procesar online siempre que los elementos sean lo bastante pequeños para usarse como índices, o si usamos una estructura más avanzada como un Árbol de Segmentos disperso. Si estamos dispuestos a procesar offline, como a menudo podemos, sin embargo, podemos evitar una técnica más avanzada: basta con reunir los elementos del arreglo y ordenarlos para comprimir coordenadas, creando con esos IDs comprimidos.
La implementación es la siguiente:
#include <bits/stdc++.h>
using namespace std;
// BeginCodeSnip{Range Maximum Segment Tree}
class MaxSegTree {
private:
int len;
vector<int> segtree;
public:
MaxSegTree(int len) : len(len), segtree(2 * len) {}
void set(int ind, int val) {
for (segtree[ind += len] = val; ind > 1; ind >>= 1) {
segtree[ind >> 1] = max(segtree[ind], segtree[ind ^ 1]);
}
}
// maximum of the range [from, to)
int range_max(int from, int to) {
int max_ = INT32_MIN;
for (from += len, to += len; from < to; from >>= 1, to >>= 1) {
if ((from & 1) != 0) { max_ = max(max_, segtree[from++]); }
if ((to & 1) != 0) { max_ = max(max_, segtree[--to]); }
}
return max_;
}
};
// EndCodeSnip
int find_lis(vector<int> a) {
// apply coordinate compression to all elements of the array
vector<int> sorted(a);
sort(sorted.begin(), sorted.end());
int at = 0;
map<int, int> coord_comp;
for (int i : sorted) {
if (!coord_comp.count(i)) { coord_comp[i] = at++; }
}
// cmp(i) gives the compressed version of index i
auto cmp = [&](int i) -> int { return coord_comp[a[i]]; };
MaxSegTree dp(coord_comp.size());
dp.set(cmp(0), 1);
for (int i = 1; i < a.size(); i++) {
int max_prev = dp.range_max(0, cmp(i));
if (max_prev != INT32_MIN) {
dp.set(cmp(i), max_prev + 1);
} else {
dp.set(cmp(i), 1);
}
}
return dp.range_max(0, coord_comp.size());
}import java.util.*;
public class FindLis {
public static int findLis(int[] a) {
// apply coordinate compression to all elements of the array
int[] sorted = a.clone();
Arrays.sort(sorted);
HashMap<Integer, Integer> coordComp = new HashMap<>();
int at = 0;
for (int i : sorted) {
if (!coordComp.containsKey(i)) { coordComp.put(i, at++); }
}
MaxSegTree dp = new MaxSegTree(coordComp.size());
dp.set(coordComp.get(a[0]), 1);
for (int i = 1; i < a.length; i++) {
int cmp = coordComp.get(a[i]);
int maxPrev = dp.rangeMax(0, cmp);
if (maxPrev != Integer.MIN_VALUE) {
dp.set(cmp, dp.rangeMax(0, cmp) + 1);
} else {
dp.set(cmp, 1);
}
}
return dp.rangeMax(0, coordComp.size());
}
}
// BeginCodeSnip{Range Maximum Segment Tree}
class MaxSegTree {
int len;
int[] segtree;
public MaxSegTree(int len) {
this.len = len;
segtree = new int[2 * len];
}
void set(int ind, int val) {
for (segtree[ind += len] = val; ind > 1; ind >>= 1) {
segtree[ind >> 1] = Math.max(segtree[ind], segtree[ind ^ 1]);
}
}
// maximum of the range [from, to)
int rangeMax(int from, int to) {
int max = Integer.MIN_VALUE;
for (from += len, to += len; from < to; from >>= 1, to >>= 1) {
if ((from & 1) != 0) { max = Math.max(max, segtree[from++]); }
if ((to & 1) != 0) { max = Math.max(max, segtree[--to]); }
}
return max;
}
}
// EndCodeSnipfrom typing import List
# BeginCodeSnip{Range Maximum Segment Tree}
class MaxSegTree:
def __init__(self, len_: int):
self.len = len_
self.segtree = [0] * (2 * len_)
def set(self, ind: int, val: int) -> None:
ind += self.len
self.segtree[ind] = val
while ind > 1:
self.segtree[ind >> 1] = max(self.segtree[ind], self.segtree[ind ^ 1])
ind >>= 1
# maximum of the range [from, to)
def range_max(self, start: int, end: int) -> int:
max_ = -float("inf")
start += self.len
end += self.len
while start < end:
if (start & 1) != 0:
max_ = max(max_, self.segtree[start])
start += 1
if (end & 1) != 0:
end -= 1
max_ = max(max_, self.segtree[end])
start >>= 1
end >>= 1
return max_
# EndCodeSnip
def find_lis(a: List[int]) -> int:
# apply coordinate compression to all elements of the array
sorted_ = sorted(a)
coord_comp = {}
at = 0
for i in sorted_:
if i not in coord_comp:
coord_comp[i] = at
at += 1
# cmp(i) gives the compressed version of index i
cmp = lambda i: coord_comp[a[i]]
dp = MaxSegTree(len(coord_comp))
dp.set(cmp(0), 1)
for i in range(1, len(a)):
max_prev = dp.range_max(0, cmp(i))
if max_prev != -float("inf"):
dp.set(cmp(i), max_prev + 1)
else:
dp.set(cmp(i), 1)
return dp.range_max(0, len(coord_comp))Ejemplo - PCB
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| Baltic OI | ★ 2010 - PCB | Normal | en el módulo |
Este problema nos pide hallar el número mínimo de conjuntos disjuntos de segmentos que no se intersectan. Esto parece bastante intimidante, así que parémoslo en dos partes:
- Encontrar un conjunto de segmentos que no se intersectan
- Minimizar el número de esos conjuntos
Aplicación 1 - Segmentos que no se intersectan
Primero, ¿qué podemos decir de dos segmentos y si se intersectan (asumiendo )?
Como estos segmentos son rectos, nótese que .
¡Esto significa que un conjunto de segmentos que no se intersectan satisface para todos los pares !
Sea un arreglo donde significa que el segmento con su extremo derecho en la posición tiene su extremo izquierdo en la posición .
Si nos pidieran hallar el tamaño máximo de un conjunto de segmentos que no se intersectan, ¡la respuesta sería la LIS de !
Aplicación 2 - Número mínimo de secuencias crecientes
Continuando desde la aplicación 1, ahora queremos hallar el número mínimo de subsecuencias crecientes necesarias para cubrir .
Por suerte, hay una solución simple (aunque no tan obvia) para esto.
Lema (fácil): El número mínimo de subsecuencias crecientes necesarias para cubrir es al menos el tamaño de la subsecuencia no creciente más larga de .
Demostración: Ningún par de elementos de una subsecuencia no creciente puede formar parte de la misma subsecuencia creciente.
Afirmación: ¡El número mínimo de subsecuencias crecientes necesarias para cubrir es igual al tamaño de la subsecuencia no creciente más larga de !
Demostración incorrecta 1: Ver cp-algo (nótese que ese enlace describe particionar en subsecuencias no crecientes en lugar de subsecuencias crecientes). Sin embargo, no es correcta porque el proceso de desenganchar y reenganchar podría no terminar nunca. Por ejemplo, consideremos particionar en las subsecuencias no crecientes y . Entonces se moverá del frente de al frente de en el primer paso, de vuelta a en el segundo, y así sucesivamente.
Demostración incorrecta 2: Esta es esencialmente la misma que la anterior.
Motivación: Consideremos la estrategia voraz obvia para construir la colección de subsecuencias crecientes (esencialmente ordenamiento por paciencia ). Para cada elemento de de izquierda a derecha, lo añadimos a la subsecuencia creciente cuyo último elemento es menor que de modo que el valor de ese último elemento quede maximizado. Si no existe actualmente tal subsecuencia creciente, empezamos una nueva subsecuencia creciente con .
Este algoritmo realiza exactamente los mismos pasos que el algoritmo para calcular la longitud de la subsecuencia no creciente más larga, así que se sigue que devuelven el mismo resultado.
Demostración: Sea la longitud de la subsecuencia no creciente más larga que termina en . Entonces los que satisfacen para un fijo forman una subsecuencia creciente para cada . Así que hemos cubierto con (tamaño de la subsecuencia no creciente más larga) subsecuencias crecientes, listo.
¿Se ve por qué esto es equivalente al esbozo?
Demostración alternativa: Esto es solo un caso especial del teorema de Dilworth . Ver la demostración inductiva.
Código
#include <bits/stdc++.h>
using namespace std;
int lis = 0;
pair<int, int> a[100000];
set<int> s;
int main() {
iostream::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
for (int i = 0; i < n; i++) cin >> a[i].first >> a[i].second;
sort(a, a + n, greater<pair<int, int>>());
// finding the LIS of a reversed array = finding the LDS
for (int i = 0; i < n; i++) {
if (s.upper_bound(a[i].second) == s.end()) lis++;
else s.erase(s.upper_bound(a[i].second));
s.insert(a[i].second);
}
cout << lis;
}import java.io.*;
import java.util.*;
class PCB {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
TreeMap<Integer, Integer> a =
new TreeMap<Integer, Integer>(Collections.reverseOrder());
for (int i = 0; i < n; i++) {
StringTokenizer st = new StringTokenizer(br.readLine());
a.put(Integer.parseInt(st.nextToken()), Integer.parseInt(st.nextToken()));
}
// finding the LIS of a reversed array = finding the LDS
int lis = 0;
TreeSet<Integer> s = new TreeSet<Integer>();
for (int k : a.keySet()) {
if (s.higher(a.get(k)) == null) lis++;
else s.remove(s.higher(a.get(k)));
s.add(a.get(k));
}
System.out.println(lis);
}
}from bisect import bisect_left
n = int(input())
board = []
endpoints = []
for _ in range(n):
l, r = map(int, input().split())
board.append((l, r))
board.sort()
endpoints.append(board[0][1])
for x in range(1, n):
v = board[x][1]
if v < endpoints[-1]:
endpoints.append(v)
else:
index = bisect_left(
[-x for x in endpoints], -v
) # invert list in order to use bisect_left
endpoints[index] = v
print(len(endpoints))Problemas
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| CSES | Towers | Fácil | DP | Solución | |
| CF | Consecutive Subsequence | Fácil | LIS | Solución | |
| AC | Suitable Edit for LIS | Normal | DP, LIS | Solución | |
| AC | Kite | Normal | LIS | Solución | |
| CF | ★ LCS on Permutations | Normal | DP, LIS | Solución | |
| Old Gold | Cow Jog | Normal | DP | Solución | |
| CF | LCIS | Normal | DP, LIS | Solución | |
| LMiO | Rabbit Carrot | Normal | DP | Solución | |
| CF | Korney Korneevich and XOR (easy version) | Normal | DP, Bitmasks | Solución | |
| Baltic OI | ★ 2009 - Candy | Difícil | DP, Geometry | Solución | |
| CEOI | 2018 - Global Warming | Difícil | DP | Solución | |
| JOI | 2016 - Matryoshka | Difícil | DP, LIS, Binary Search | Solución |