Análisis de intervalos consecutivos mediante estructuras de datos avanzadas

Transformación del problema

El problema se puede reformular como:

Determinar la centidad de subintervalos donde se cumple que Max - Min = r - l

Solución por fuerza bruta

Aprvoechando la propiedad anterior, podemos iterar todos los posibles intervalos y verificar si cumplen con la condición.

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;

const int MAXN = 50005;
int elementos[MAXN], logaritmo[MAXN];
int maximo[MAXN][17], minimo[MAXN][17];
int tam;

void inicializar() {
    memset(minimo, 0x3f, sizeof(minimo));
    logaritmo[0] = -1;
    for(int idx = 1; idx <= tam; idx++) {
        logaritmo[idx] = logaritmo[idx>>1] + 1;
        minimo[idx][0] = maximo[idx][0] = elementos[idx];
    }
    for(int nivel = 1; nivel <= 16; nivel++) {
        for(int pos = 1; pos + (1 << nivel-1) <= tam; pos++) {
            maximo[pos][nivel] = max(maximo[pos][nivel-1], maximo[pos+(1<<nivel-1)][nivel-1]);
            minimo[pos][nivel] = min(minimo[pos][nivel-1], minimo[pos+(1<<nivel-1)][nivel-1]);
        }
    }
}

int consultar(int inicio, int fin, bool esMax) {
    int longitud = fin - inicio + 1;
    int nivel = logaritmo[longitud];
    if(esMax)
        return max(maximo[inicio][nivel], maximo[fin-(1<<nivel)+1][nivel]);
    else
        return min(minimo[inicio][nivel], minimo[fin-(1<<nivel)+1][nivel]);
}

int main() {
    scanf("%d", &tam);
    for(int i = 1; i <= tam; i++)
        scanf("%d", &elementos[i]);
    
    inicializar();
    long long resultado = 0;
    
    for(int largo = 1; largo <= tam; largo++) {
        for(int inicio = 1; inicio + largo - 1 <= tam; inicio++) {
            int fin = inicio + largo - 1;
            resultado += (consultar(inicio, fin, true) - consultar(inicio, fin, false) == largo - 1);
        }
    }
    
    printf("%lld\n", resultado);
    return 0;
}

Complejidad temporal O(n²), insuficiente para el límite de tiempo de 750ms.

Solución divide y vencerás

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;

const int LIMITE = 50005, OFFSET = 100005;
typedef long long tipo_largo;

int cantidad, valores[LIMITE];
tipo_largo total;
int contador[OFFSET<<1];
int minimos[LIMITE], maximos[LIMITE];

void procesar(int izq, int der) {
    if(izq == der) {
        total++;
        return;
    }
    
    int medio = (izq + der) >> 1;
    
    // Calcular mínimos y máximos hacia atrás desde medio
    minimos[medio] = maximos[medio] = valores[medio];
    for(int i = medio - 1; i >= izq; i--) {
        minimos[i] = min(valores[i], minimos[i+1]);
        maximos[i] = max(valores[i], maximos[i+1]);
    }
    
    // Calcular mínimos y máximos hacia adelante desde medio+1
    minimos[medio+1] = maximos[medio+1] = valores[medio+1];
    for(int i = medio + 2; i <= der; i++) {
        minimos[i] = min(valores[i], minimos[i-1]);
        maximos[i] = max(valores[i], maximos[i-1]);
    }
    
    // Caso: máximo en lado izquierdo
    int ptr1 = medio + 1, ptr2 = medio + 1;
    for(int pos_izq = medio; pos_izq >= izq; pos_izq--) {
        while(ptr1 <= der && maximos[ptr1] < maximos[pos_izq]) 
            contador[minimos[ptr1] + ptr1 + OFFSET]++, ptr1++;
        while(ptr2 < ptr1 && minimos[ptr2] > minimos[pos_izq]) 
            contador[minimos[ptr2] + ptr2 + OFFSET]--, ptr2++;
        
        total += contador[maximos[pos_izq] + pos_izq + OFFSET];
        
        int limite_der = maximos[pos_izq] - minimos[pos_izq] + pos_izq;
        if(limite_der > medio && limite_der < ptr2) total++;
    }
    
    for(int i = ptr2; i < ptr1; i++)
        contador[minimos[i] + i + OFFSET] = 0;
    
    // Caso: máximo en lado derecho
    int izq1 = medio, izq2 = medio;
    for(int pos_der = medio + 1; pos_der <= der; pos_der++) {
        while(izq1 >= izq && maximos[izq1] < maximos[pos_der])
            contador[minimos[izq1] - izq1 + OFFSET]++, izq1--;
        while(izq2 > izq1 && minimos[izq2] > minimos[pos_der])
            contador[minimos[izq2] - izq2 + OFFSET]--, izq2--;
        
        total += contador[maximos[pos_der] - pos_der + OFFSET];
        
        int limite_izq = minimos[pos_der] - maximos[pos_der] + pos_der;
        if(limite_izq <= medio && limite_izq > izq2) total++;
    }
    
    for(int i = izq2; i > izq1; i--)
        contador[minimos[i] - i + OFFSET] = 0;
    
    procesar(izq, medio);
    procesar(medio + 1, der);
}

int main() {
    scanf("%d", &cantidad);
    for(int i = 1; i <= cantidad; i++)
        scanf("%d", &valores[i]);
    
    procesar(1, cantidad);
    printf("%lld\n", total);
    return 0;
}

Complejidad O(n log n), solución aceptable.

Segment tree con pilas monótonas

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;

const int MAX_ELEM = 50005;
typedef long long entero_largo;

entero_largo respuesta;
int total_elementos, datos[MAX_ELEM];

struct nodo_segmento {
    int inicio, fin, valor_min, cuenta, etiqueta_suma;
} arbol[4*MAX_ELEM];

void actualizar_arriba(int indice) {
    arbol[indice].valor_min = min(arbol[indice*2].valor_min, arbol[indice*2+1].valor_min);
    int izq = arbol[indice].valor_min==arbol[indice*2].valor_min ? arbol[indice*2].cuenta : 0;
    int der = arbol[indice].valor_min==arbol[indice*2+1].valor_min ? arbol[indice*2+1].cuenta : 0;
    arbol[indice].cuenta = izq + der;
}

void construir(int nodo, int ini, int fin) {
    arbol[nodo].etiqueta_suma = 0;
    arbol[nodo].inicio = ini;
    arbol[nodo].fin = fin;
    
    if(ini == fin) {
        arbol[nodo] = {ini, fin, ini, 1, 0};
        return;
    }
    
    int mitad = (ini + fin) >> 1;
    construir(nodo*2, ini, mitad);
    construir(nodo*2+1, mitad+1, fin);
    actualizar_arriba(nodo);
}

void aplicar_cambio(int nodo, int valor) {
    arbol[nodo].valor_min += valor;
    arbol[nodo].etiqueta_suma += valor;
}

void propagar(int nodo) {
    if(arbol[nodo].etiqueta_suma == 0) return;
    aplicar_cambio(nodo*2, arbol[nodo].etiqueta_suma);
    aplicar_cambio(nodo*2+1, arbol[nodo].etiqueta_suma);
    arbol[nodo].etiqueta_suma = 0;
}

void modificar_rango(int nodo, int ini, int fin, int delta) {
    if(arbol[nodo].inicio >= ini && arbol[nodo].fin <= fin) {
        aplicar_cambio(nodo, delta);
        return;
    }
    
    propagar(nodo);
    int mitad = (arbol[nodo].inicio + arbol[nodo].fin) >> 1;
    if(ini <= mitad) modificar_rango(nodo*2, ini, fin, delta);
    if(fin > mitad) modificar_rango(nodo*2+1, ini, fin, delta);
    actualizar_arriba(nodo);
}

int pila_max[MAX_ELEM], tope_max, pila_min[MAX_ELEM], tope_min;

void resolver() {
    construir(1, 1, total_elementos);
    
    for(int posicion = 1; posicion <= total_elementos; posicion++) {
        // Mantener pila de máximos
        while(tope_max && datos[posicion] > datos[pila_max[tope_max]]) {
            modificar_rango(1, pila_max[tope_max-1]+1, pila_max[tope_max], 
                           datos[posicion] - datos[pila_max[tope_max]]);
            tope_max--;
        }
        
        // Mantener pila de mínimos
        while(tope_min && datos[posicion] < datos[pila_min[tope_min]]) {
            modificar_rango(1, pila_min[tope_min-1]+1, pila_min[tope_min],
                           datos[pila_min[tope_min]] - datos[posicion]);
            tope_min--;
        }
        
        respuesta += arbol[1].cuenta;
        pila_max[++tope_max] = pila_min[++tope_min] = posicion;
    }
}

int main() {
    scanf("%d", &total_elementos);
    for(int i = 1; i <= total_elementos; i++)
        scanf("%d", &datos[i]);
    
    resolver();
    printf("%lld\n", respuesta);
    return 0;
}

Las pilas monótonas almacenan índices, no valores directamente. Es crucial manejar correctamente los límites durante las actualizaciones del árbol de segmentos.

Etiquetas: algoritmos estructuras-de-datos divide-y-venceras pilas-monotonas segment-tree

Publicado el 8-8 13:26