CP-UPV Bootcamp - Capítulo 5
Capítulo 5: Estructuras de Datos
PROBLEMAS DEL CAPÍTULO
Para acceder a los problemas de este capítulo, accede con tu usuario y contraseña a cpupv.contest.codeforces.com. Si no tienes usuario y contraseña, inscríbete aquí.Índice de contenidos
- 1. Estructuras Lineales (LIFO / FIFO)
- Ejemplo 1: El Frigorífico de Diego Provencio (Monotonic Stack)
- Ejemplo 2 (Avanzado): El Cartel Publicitario de la ETSINF
- 2. Contenedores Asociativos (Búsqueda y Mapeo)
- 3. Conjuntos Ordenados: set y multiset
- Ejemplo 3. El Reparto de Merchandising de CP-UPV
- 4. Colas de Prioridad (Heaps)
- Ejemplo 4. Las Paellas de CP-UPV
- 5. Uniones de Conjuntos Disjuntos (DSU / Union-Find)
- Ejemplo 5: La Red de PCs de CP-UPV
- Problemas de práctica
1. Estructuras Lineales (LIFO / FIFO)
Las estructuras de datos lineales organizan los elementos en una secuencia continua. La diferencia clave entre ellas radica en por dónde se insertan y por dónde se eliminan los datos, lo que condiciona el orden en el que se procesan.
std::stack (Pila - LIFO)
Una Pila es una estructura basada en el principio Last-In, First-Out (LIFO: el último en entrar es el primero en salir). Imagínala como una pila de platos en un restaurante: solo puedes colocar un plato nuevo en la parte superior y, si quieres coger uno, tienes que retirar obligatoriamente el que está arriba del todo.
No se puede acceder directamente a los elementos del fondo o del medio sin antes desapilar los superiores. Permite acceso, inserción y borrado en el tope con una complejidad constante de $O(1)$.
std::stack<int> st;
st.push(10); // Inserción en el tope O(1)
st.top(); // Consulta del elemento superior O(1)
st.pop(); // Elimina el elemento superior O(1)
bool emp = st.empty(); // Verifica si está vacía O(1)
st = []
st.append(10) # Inserción en el tope O(1)
top_val = st[-1] # Consulta del elemento superior O(1)
st.pop() # Elimina el elemento superior O(1)
emp = len(st) == 0 # Verifica si está vacía O(1)
ArrayDeque<Integer> st = new ArrayDeque<>();
st.push(10); // Inserción en el tope O(1)
st.peek(); // Consulta del elemento superior O(1)
st.pop(); // Elimina el elemento superior O(1)
boolean emp = st.isEmpty(); // Verifica si está vacía O(1)
Técnica Estrella: Monotonic Stack (Pila Monótona)
Una Pila Monótona es una pila convencional que se gestiona bajo una regla estricta: sus elementos siempre deben mantenerse ordenados (ya sea de forma estrictamente creciente o decreciente) desde la base hasta el tope.
¿Cómo funciona la inserción?
Para insertar un nuevo valor $X$, no basta con hacer un push directo. Primero debemos hacer un "limpieza": desapilamos (hacemos pop) de todos los elementos superiores que rompan la propiedad de orden deseada frente a $X$. Una vez eliminados, recién insertamos $X$.
- Monótona Creciente (Monotonic Increasing): Los elementos crecen de la base al tope ($1, 3, 5, 8$). Al insertar un nuevo elemento $X$, extraemos todo valor del tope que sea mayor que $X$. Se utiliza para encontrar el primer elemento menor a la izquierda/derecha.
- Monótona Decreciente (Monotonic Decreasing): Los elementos decrecen de la base al tope ($9, 7, 4, 2$). Al insertar un nuevo elemento $X$, extraemos todo valor del tope que sea menor que $X$. Se utiliza para encontrar el primer elemento mayor a la izquierda/derecha.
¿Por qué es $O(N)$ si hay un bucle interno?
Aunque procesemos el array con un bucle y dentro usemos un while para desapilar, cada elemento del array entra a la pila exactamente una vez (vía push) y sale como máximo una vez (vía pop). Por lo tanto, el número total de operaciones de pila a lo largo de todo el algoritmo está acotado por $2N$, lo que nos da un tiempo de ejecución amortizado de $O(N)$.
Ejemplo clásico: Siguiente Elemento Mayor (Next Greater Element)
Dado un array, queremos encontrar para cada posición cuál es el primer elemento estrictamente mayor que aparece a su derecha. Si no existe, guardamos $-1$.
// Devuelve un vector con el siguiente elemento mayor a la derecha para cada posición
std::vector<int> nextGreaterElement(const std::vector<int>& arr) {
int n = arr.size();
std::vector<int> ans(n, -1);
std::stack<int> st; // Guardamos ÍNDICES para poder asignar las respuestas
for (int i = 0; i < n; ++i) {
// Mientras la pila no esté vacía y el elemento actual sea MAYOR
// que el elemento apuntado por el índice del tope:
while (!st.empty() && arr[i] > arr[st.top()]) {
ans[st.top()] = arr[i]; // El elemento actual es la respuesta para st.top()
st.pop();
}
st.push(i); // Guardamos el índice actual
}
return ans;
}
def next_greater_element(arr):
n = len(arr)
ans = [-1] * n
st = [] # Pila que guardará ÍNDICES
for i in range(n):
# Mantenemos la propiedad monótona decreciente desapilando los menores
while st and arr[i] > arr[st[-1]]:
idx = st.pop()
ans[idx] = arr[i]
st.append(i)
return ans
import java.util.ArrayDeque;
import java.util.Arrays;
public static int[] nextGreaterElement(int[] arr) {
int n = arr.length;
int[] ans = new int[n];
Arrays.fill(ans, -1);
ArrayDeque<Integer> st = new ArrayDeque<>(); // Guardamos ÍNDICES
for (int i = 0; i < n; i++) {
while (!st.isEmpty() && arr[i] > arr[st.peek()]) {
ans[st.pop()] = arr[i];
}
st.push(i);
}
return ans;
}
std::queue (Cola - FIFO)
Una Cola sigue el principio First-In, First-Out (FIFO: el primero en entrar es el primero en salir). La mejor forma de visualizarla es la cola de la caja en un supermercado: la primera persona que llega a la fila es la primera en ser atendida, y los nuevos clientes se colocan siempre al final.
Los elementos entran por un extremo (el final o back) y salen por el otro (el frente o front). Todas estas operaciones se realizan en un tiempo constante de $O(1)$. Es la estructura fundamental para algoritmos de exploración por niveles, como la Búsqueda en Anchura (BFS) en grafos.
std::deque (Cola de Doble Extremo)
Un Deque (del inglés Double-Ended Queue) combina la potencia de las pilas y las colas. Es una estructura flexible que permite insertar, consultar y eliminar elementos de forma eficiente tanto por el frente como por el final en complejidad $O(1)$.
Piensa en un vagón de tren donde se pueden acoplar o desacoplar máquinas o vagones por ambos extremos con la misma facilidad. Es indispensable cuando necesitas mantener ventanas deslizantes (sliding window) o calcular mínimos y máximos en rangos dinámicos.
std::deque<int> dq;
dq.push_back(5); // Inserta al final O(1)
dq.push_front(2); // Inserta al inicio O(1)
dq.pop_back(); // Elimina del final O(1)
dq.pop_front(); // Elimina del inicio O(1)
int f = dq.front(); // Consulta el frente O(1)
int b = dq.back(); // Consulta el final O(1)
from collections import deque
dq = deque()
dq.append(5) # Inserta al final O(1)
dq.appendleft(2) # Inserta al inicio O(1)
dq.pop() # Elimina del final O(1)
dq.popleft() # Elimina del inicio O(1)
f = dq[0] # Consulta el frente O(1)
b = dq[-1] # Consulta el final O(1)
ArrayDeque<Integer> dq = new ArrayDeque<>();
dq.addLast(5); // Inserta al final O(1)
dq.addFirst(2); // Inserta al inicio O(1)
dq.removeLast(); // Elimina del final O(1)
dq.removeFirst(); // Elimina del inicio O(1)
int f = dq.peekFirst(); // Consulta el frente O(1)
int b = dq.peekLast(); // Consulta el final O(1)
Ejemplo 1: El Frigorífico de Diego Provencio
El arduo trabajador Diego Provencio almacena una fila de $N$ piezas de carne en el frigorífico donde es explotado, ordenadas de izquierda a derecha. Cada pieza $i$ tiene un peso de $W_i$ kilos. Para planificar los cortes de la semana, a Diego se le encarga saber, para cada pieza de carne, cuál es el índice de la primera pieza situada a su derecha que sea estrictamente más pesada que ella. Si no existe ninguna pieza más pesada a la derecha, se debe responder -1.
Pista (Haz clic para desplegar)
Una aproximación con dos bucles anidados requiere $O(N^2)$, lo cual dará TLE. Si recorremos la fila de carne de derecha a izquierda manteniendo una Monotonic Stack decreciente con los índices de los chuletones más pesados vistos hasta el momento, ¿cómo podemos resolver cada consulta en $O(1)$ amortizado?
ENLACE AL PROBLEMA
Ponte a prueba antes de desplegar el código: Resolver El Frigorífico de Diego Provencio en CodeforcesVer Solución (C++, Python, Java)
#include <bits/stdc++.h>
#include <iostream>
#include <vector>
#include <stack>
using namespace std;
int main() {
// Optimización de la entrada y salida para evitar TLE
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int cantidad_piezas;
if (!(cin >> cantidad_piezas)) return 0;
vector<long long> peso_carne(cantidad_piezas);
for (int i = 0; i < cantidad_piezas; i++) {
cin >> peso_carne[i];
}
// Array para almacenar la respuesta (índice 1-based del siguiente más pesado)
vector<int> respuesta(cantidad_piezas, -1);
// Pila monótona que almacenará los ÍNDICES de las piezas
stack<int> pila_indices;
// Recorremos la fila de carnes de derecha a izquierda
for (int i = cantidad_piezas - 1; i >= 0; i--) {
// Eliminamos de la pila las piezas a la derecha que sean
// de peso menor o igual al de la pieza actual
while (!pila_indices.empty() && peso_carne[pila_indices.top()] <= peso_carne[i]) {
pila_indices.pop();
}
// Si la pila no está vacía, el tope es la primera pieza más pesada a la derecha
if (!pila_indices.empty()) {
respuesta[i] = pila_indices.top() + 1; // Convertimos a indexación 1-based
}
// Apilamos el índice de la pieza actual
pila_indices.push(i);
}
// Imprimimos el resultado final separado por espacios
for (int i = 0; i < cantidad_piezas; i++) {
cout << respuesta[i] << (i == cantidad_piezas - 1 ? "" : " ");
}
cout << "\n";
return 0;
}
import sys
def resolver():
# Lectura rápida de todos los datos desde sys.stdin
entrada = sys.stdin.read().split()
if not entrada:
return
cantidad_piezas = int(entrada[0])
pesos_carne = [int(x) for x in entrada[1:cantidad_piezas + 1]]
# Inicializamos las respuestas con -1 por defecto
respuesta = [-1] * cantidad_piezas
# Pila monótona para guardar los índices
pila_indices = []
# Recorremos la lista de derecha a izquierda
for i in range(cantidad_piezas - 1, -1, -1):
peso_actual = pesos_carne[i]
# Limpiamos los elementos de la derecha que no sean estrictamente mayores
while pila_indices and pesos_carne[pila_indices[-1]] <= peso_actual:
pila_indices.pop()
# Si queda algún elemento en la pila, ese es el más cercano y más pesado
if pila_indices:
respuesta[i] = pila_indices[-1] + 1 # 1-based index
# Guardamos el índice actual en la pila
pila_indices.append(i)
# Imprimimos la lista de respuestas separada por espacios
print(*(respuesta))
if __name__ == "__main__":
resolver()
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.IOException;
import java.util.StringTokenizer;
import java.util.ArrayDeque;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
// Entrada rápida usando BufferedReader
BufferedReader lector = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer tokens = new StringTokenizer(lector.readLine());
if (!tokens.hasMoreTokens()) return;
int cantidadPiezas = Integer.parseInt(tokens.nextToken());
long[] pesosCarne = new long[cantidadPiezas];
tokens = new StringTokenizer(lector.readLine());
for (int i = 0; i < cantidadPiezas; i++) {
pesosCarne[i] = Long.parseLong(tokens.nextToken());
}
int[] respuesta = new int[cantidadPiezas];
Arrays.fill(respuesta, -1);
// Usamos ArrayDeque como pila (más rápido que java.util.Stack)
ArrayDeque<Integer> pilaIndices = new ArrayDeque<>();
// Procesa la fila de carnes de derecha a izquierda
for (int i = cantidadPiezas - 1; i >= 0; i--) {
while (!pilaIndices.isEmpty() && pesosCarne[pilaIndices.peek()] <= pesosCarne[i]) {
pilaIndices.pop();
}
if (!pilaIndices.isEmpty()) {
respuesta[i] = pilaIndices.peek() + 1; // Convertimos a 1-based index
}
pilaIndices.push(i);
}
// Salida rápida con PrintWriter
PrintWriter escritor = new PrintWriter(System.out);
for (int i = 0; i < cantidadPiezas; i++) {
escritor.print(respuesta[i] + (i == cantidadPiezas - 1 ? "" : " "));
}
escritor.println();
escritor.flush();
}
}
Ejemplo 2 (Avanzado): El Cartel Publicitario de la ETSINF
La Delegación de Alumnos quiere colocar un cartel publicitario rectangular gigante en la fachada de la ETSINF. La pared está formada por una hilera contigua de $N$ columnas verticalmente alineadas de ancho $1$, donde la $i$-ésima columna tiene una altura de $H_i$.
El cartel debe ser completamente rectangular y encajar dentro del área delimitada por las columnas (sin sobresalir por arriba ni por los lados). Tu objetivo es calcular el área máxima posible que puede tener dicho cartel.
Pista y Estrategia (Haz clic para desplegar)
Para cada columna $i$ con altura $H_i$, si consideramos que $H_i$ sea la **altura máxima fija del cartel**, ¿hasta dónde puede extenderse el cartel hacia la izquierda y hacia la derecha antes de chocar con una columna estrictamente más baja que $H_i$?
Usa **dos pases con Monotonic Stack**:
- Un array
L[i]que guarde el primer índice a la izquierda con una altura $ < H_i$. - Un array
R[i]que guarde el primer índice a la derecha con una altura $ < H_i$.
ENLACE AL PROBLEMA
Demuestra tu nivel resolviendolo aquí: Resolver El Cartel Publicitario de la ETSINF en CodeforcesVer Solución (C++, Python, Java)
#include <bits/stdc++.h>
#include <iostream>
#include <vector>
#include <stack>
using namespace std;
int main() {
// Optimización de la I/O para evitar TLE en competiciones
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int num_columnas;
if (!(cin >> num_columnas)) return 0;
vector<long long> altura_columna(num_columnas);
for (int i = 0; i < num_columnas; i++) {
cin >> altura_columna[i];
}
// L[i] guarda el índice de la primera columna a la IZQUIERDA estrictamente más baja que la i-ésima
// R[i] guarda el índice de la primera columna a la DERECHA estrictamente más baja que la i-ésima
vector<int> limite_izquierdo(num_columnas);
vector<int> limite_derecho(num_columnas);
stack<int> pila_indices;
// 1. Calculamos el límite izquierdo para cada columna
for (int i = 0; i < num_columnas; i++) {
while (!pila_indices.empty() && altura_columna[pila_indices.top()] >= altura_columna[i]) {
pila_indices.pop();
}
// Si la pila está vacía, el cartel se puede extender hasta el inicio (-1)
limite_izquierdo[i] = pila_indices.empty() ? -1 : pila_indices.top();
pila_indices.push(i);
}
// Vaciamos la pila para volver a utilizarla
while (!pila_indices.empty()) {
pila_indices.pop();
}
// 2. Calculamos el límite derecho para cada columna
for (int i = num_columnas - 1; i >= 0; i--) {
while (!pila_indices.empty() && altura_columna[pila_indices.top()] >= altura_columna[i]) {
pila_indices.pop();
}
// Si la pila está vacía, el cartel se puede extender hasta el final (num_columnas)
limite_derecho[i] = pila_indices.empty() ? num_columnas : pila_indices.top();
pila_indices.push(i);
}
// 3. Evaluamos el área máxima considerando la altura de cada columna como la fija del cartel
long long area_maxima = 0;
for (int i = 0; i < num_columnas; i++) {
long long ancho_cartel = limite_derecho[i] - limite_izquierdo[i] - 1;
long long area_actual = altura_columna[i] * ancho_cartel;
area_maxima = max(area_maxima, area_actual);
}
cout << area_maxima << "\n";
return 0;
}
import sys
def resolver():
entrada = sys.stdin.read().split()
if not entrada:
return
num_columnas = int(entrada[0])
altura_columna = [int(x) for x in entrada[1:num_columnas + 1]]
limite_izquierdo = [0] * num_columnas
limite_derecho = [0] * num_columnas
pila_indices = []
# 1. Calculamos el límite izquierdo para cada columna
for i in range(num_columnas):
while pila_indices and altura_columna[pila_indices[-1]] >= altura_columna[i]:
pila_indices.pop()
limite_izquierdo[i] = pila_indices[-1] if pila_indices else -1
pila_indices.append(i)
# Vaciamos la pila
pila_indices.clear()
# 2. Calculamos el límite derecho para cada columna
for i in range(num_columnas - 1, -1, -1):
while pila_indices and altura_columna[pila_indices[-1]] >= altura_columna[i]:
pila_indices.pop()
limite_derecho[i] = pila_indices[-1] if pila_indices else num_columnas
pila_indices.append(i)
# 3. Calculamos la máxima área posible
area_maxima = 0
for i in range(num_columnas):
ancho_cartel = limite_derecho[i] - limite_izquierdo[i] - 1
area_actual = altura_columna[i] * ancho_cartel
if area_actual > area_maxima:
area_maxima = area_actual
print(area_maxima)
if __name__ == "__main__":
resolver()
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.IOException;
import java.util.StringTokenizer;
import java.util.ArrayDeque;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader lector = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer tokens = new StringTokenizer(lector.readLine());
if (!tokens.hasMoreTokens()) return;
int numColumnas = Integer.parseInt(tokens.nextToken());
long[] alturaColumna = new long[numColumnas];
tokens = new StringTokenizer(lector.readLine());
for (int i = 0; i < numColumnas; i++) {
alturaColumna[i] = Long.parseLong(tokens.nextToken());
}
int[] limiteIzquierdo = new int[numColumnas];
int[] limiteDerecho = new int[numColumnas];
ArrayDeque<Integer> pilaIndices = new ArrayDeque<>();
// 1. Calculamos el límite izquierdo para cada columna
for (int i = 0; i < numColumnas; i++) {
while (!pilaIndices.isEmpty() && alturaColumna[pilaIndices.peek()] >= alturaColumna[i]) {
pilaIndices.pop();
}
limiteIzquierdo[i] = pilaIndices.isEmpty() ? -1 : pilaIndices.peek();
pilaIndices.push(i);
}
pilaIndices.clear();
// 2. Calculamos el límite derecho para cada columna
for (int i = numColumnas - 1; i >= 0; i--) {
while (!pilaIndices.isEmpty() && alturaColumna[pilaIndices.peek()] >= alturaColumna[i]) {
pilaIndices.pop();
}
limiteDerecho[i] = pilaIndices.isEmpty() ? numColumnas : pilaIndices.peek();
pilaIndices.push(i);
}
// 3. Buscamos el área máxima
long areaMaxima = 0;
for (int i = 0; i < numColumnas; i++) {
long anchoCartel = limiteDerecho[i] - limiteIzquierdo[i] - 1;
long areaActual = alturaColumna[i] * anchoCartel;
areaMaxima = Math.max(areaMaxima, areaActual);
}
PrintWriter escritor = new PrintWriter(System.out);
escritor.println(areaMaxima);
escritor.flush();
}
}
2. Contenedores Asociativos (Búsqueda y Mapeo)
A diferencia de los vectores o arrays (donde accedemos por un índice numérico $0, 1, 2...$), los contenedores asociativos nos permiten almacenar elementos y comprobar si un valor existe en tiempo récord sin tener que recorrer toda la lista de principio a fin.
Conjuntos: std::set vs std::unordered_set
Ambas estructuras sirven para lo mismo: guardar elementos únicos (sin duplicados) y responder al instante a la pregunta "¿está este elemento guardado aquí?". Sin embargo, por dentro funcionan de manera totalmente distinta:
1. std::set (Conjunto Ordenado)
Imagina una agenda telefónica perfectamente ordenada alfabéticamente. Cada vez que añades un nombre nuevo, lo colocas en el sitio exacto que le corresponde para no perder el orden.
- ¿Cómo funciona internamente? Utiliza una estructura en forma de árbol balanceado (conocido como Árbol Rojo-Negro). No necesitas saber cómo se implementa a bajo nivel; lo importante es entender que al estar siempre ordenado, puede hacer una búsqueda binaria interna continuamente.
- Complejidad: Búsquedas, inserciones y borrados funcionan en tiempo $O(\log N)$.
- Cuándo usarlo: Cuando necesites mantener los datos ordenados automáticamente de menor a mayor, o cuando quieras consultar elementos cercanos (por ejemplo, con
lower_boundpara hallar el menor elemento mayor o igual a $X$).
2. std::unordered_set (Conjunto no ordenado / Hash)
Imagina un guardarropa con percheros numerados con una regla matemática. Cuando llegas con un objeto, aplicas una fórmula mágica (una función Hash) a partir de su valor que te dice directamente en qué perchero exacto debes colgarlo, sin importar el resto de objetos.
- ¿Cómo funciona internamente? Utiliza una Tabla Hash. Al insertar o buscar un valor, calcula su "código hash" e va directo a esa posición en memoria.
- Complejidad: Operaciones en tiempo promedio constante $O(1)$. Es considerablemente más rápido que
std::set. - Punto débil / Cuidado en CP: Los elementos quedan guardados de forma completamente desordenada. Además, en casos extremos donde muchos elementos den el mismo código hash (un "ataque de colisiones", muy común en algunos problemas de Codeforces pensados para romper esta estructura), la complejidad puede degradar a $O(N)$.
| Estructura | Estructura interna | Complejidad | ¿Mantiene Orden? |
|---|---|---|---|
std::set |
Árbol balanceado | $O(\log N)$ | ✅ Sí (creciente) |
std::unordered_set |
Tabla Hash | $O(1)$ promedio | ❌ No |
El peligro de los Anti-Hash Tests en Codeforces
En plataformas como Codeforces, las pruebas son públicas durante la fase de hacks. Un rival puede generar un test que fuerce colisiones masivas en la función de hash por defecto, degradando la complejidad de $O(1)$ a $O(N)$ y causando TLE. Para evitar esto, se recomienda usar semillas aleatorias o estructuras de Hash seguras:
struct custom_hash {
static uint64_t splitmix64(uint64_t x) {
x += 0x9e3779b97f4a7c15;
x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9;
x = (x ^ (x >> 27)) * 0x94d049bb133111eb;
return x ^ (x >> 31);
}
size_t operator()(uint64_t x) const {
static const uint64_t FIXED_RANDOM = chrono::steady_clock::now().time_since_epoch().count();
return splitmix64(x + FIXED_RANDOM);
}
};
// Uso anti-hack seguro:
unordered_map<long long, int, custom_hash> safe_map;
import random
# En Python dict/set son vulnerables a hacks de colisión si se conocen los valores.
# Solución anti-hack: aplicar un XOR con un entero aleatorio único al insertar/consultar.
RANDOM_KEY = random.randint(1, 1 << 60)
class AntiHackDict:
def __init__(self):
self.d = {}
def insert(self, key, value):
self.d[key ^ RANDOM_KEY] = value
def get(self, key, default=None):
return self.d.get(key ^ RANDOM_KEY, default)
import java.util.*;
// En Java HashMap usa Árboles Rojo-Negro en cubetas con muchas colisiones (O(log N) peor caso),
// pero mezclar la clave con un entero aleatorio garantiza inmunidad total:
public class SafeHash {
private static final long RANDOM_SEED = new Random().nextLong();
private Map safeMap = new HashMap<>();
public void put(long key, int val) {
safeMap.put(key ^ RANDOM_SEED, val);
}
public Integer get(long key) {
return safeMap.get(key ^ RANDOM_SEED);
}
}
3. Conjuntos Ordenados: set y multiset
Un std::set mantiene elementos únicos estrictamente ordenados mediante un árbol auto-balanceado (Rojo-Negro). Su variante std::multiset admite elementos duplicados manteniendo la misma propiedad de ordenamiento automático.
std::set<int> st;
st.insert(5); // Inserción en O(log N)
st.insert(2);
st.erase(5); // Eliminación en O(log N)
// Consultas de Mínimo y Máximo en O(1) vía iteradores:
int min_val = *st.begin();
int max_val = *st.rbegin();
// Búsquedas binarias integradas en O(log N):
auto it1 = st.lower_bound(3); // Primer elemento >= 3
auto it2 = st.upper_bound(3); // Primer elemento > 3
import bisect
# En Python la librería estándar no incluye un set/multiset ordenado.
# Para CP se suele mantener una lista ordenada combinada con 'bisect':
st = [2, 5]
bisect.insort(st, 10) # Inserción manteniendo el orden O(N)
st.remove(5) # Eliminación O(N)
min_val = st[0] # Mínimo O(1)
max_val = st[-1] # Máximo O(1)
# Búsqueda binaria integrada en O(log N) con la librería estándar:
idx = bisect.bisect_left(st, 3) # Índice del primer elemento >= 3
TreeSet<Integer> st = new TreeSet<>();
st.add(5); // Inserción en O(log N)
st.add(2);
st.remove(5); // Eliminación en O(log N)
int minVal = st.first(); // Mínimo en O(log N) / O(1)
int maxVal = st.last(); // Máximo en O(log N) / O(1)
// Búsqueda binaria integrada en O(log N):
Integer lower = st.ceiling(3); // Primer elemento >= 3
Integer upper = st.higher(3); // Primer elemento > 3
⚠️ Trampa mortal con multiset::erase() en C++
En C++, llamar a ms.erase(val) pasando directamente un valor eliminará TODAS las instancias de ese valor presentes en el multiset. Si tu intención es eliminar únicamente una sola instancia del elemento, debes pasarle un iterador directo obtenido previamente con find():
std::multiset<int> ms = {3, 3, 3, 5};
// PELIGRO: Borra TODOS los '3' del multiset
// ms.erase(3); -> ¡El multiset queda solo con {5}!
// FORMA CORRECTA: Borra SOLO UNA instancia del '3'
auto it = ms.find(3);
if (it != ms.end()) {
ms.erase(it); // Se borra solo un '3', quedan {3, 3, 5}
}
import bisect
# Simulamos el comportamiento de multiset en Python con una lista ordenada y bisect
ms = [3, 3, 3, 5]
# Para borrar únicamente UNA instancia del valor:
val = 3
pos = bisect.bisect_left(ms, val) # Busca la primera aparición de 3 en O(log N)
if pos < len(ms) and ms[pos] == val:
del ms[pos] # Borra solo un 3. Quedan [3, 3, 5]
import java.util.*;
// Java no posee un MultiSet integrado en la librería estándar.
// La técnica estándar en CP es combinar TreeMap:
TreeMap<Integer, Integer> multiset = new TreeMap<>();
void add(int x) {
multiset.put(x, multiset.getOrDefault(x, 0) + 1);
}
void removeOne(int x) {
if (!multiset.containsKey(x)) return;
int count = multiset.get(x);
if (count == 1) multiset.remove(x);
else multiset.put(x, count - 1);
}
Ejemplo Práctico: El Reparto de Merchandising de CP-UPV
En el último evento de CP-UPV se han preparado $N$ camisetas de regalo. Cada camiseta $i$ tiene un nivel de exclusividad o talla $P_i$. Hay $M$ participantes en la fila para recoger su obsequio. El $j$-ésimo participante tiene un nivel de prioridad máximo de $T_j$ y desea llevarse la camiseta con la mayor exclusividad posible que no supere su límite $T_j$.
Cada camiseta solo se puede entregar a un único participante. Si un participante no encuentra ninguna camiseta cuyo valor sea menor o igual a su límite $T_j$, se va tristemente con las manos vacías y debemos imprimir -1. Para cada participante, indica el valor de la camiseta que consigue llevarse.
💡 Pista y Estrategia (Haz clic para desplegar)
Como varias camisetas pueden tener el mismo valor y necesitamos mantener el inventario ordenado a medida que regalamos prendas, la estructura ideal es un std::multiset.
Para un límite $T$, la función upper_bound(T) nos da el primer elemento estrictamente mayor que $T$. Por lo tanto, el elemento inmediatamente anterior a este iterador (decrementándolo con --it) será **el valor máximo $\le T$**.
Una vez entregada la camiseta, recuerda eliminar **solo esa prenda concreta** usando el iterador ms.erase(it) para no borrar las demás camisetas del mismo valor del inventario.
ENLACE AL PROBLEMA
Ponte a prueba antes de desplegar el código: Resolver El Reparto de Merchandising en CodeforcesVer Solución (C++, Python, Java)
#include <bits/stdc++.h>
using namespace std;
int main() {
// Optimización de I/O para CP
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int num_camisetas, num_participantes;
if (!(cin >> num_camisetas >> num_participantes)) return 0;
multiset<int> inventario_camisetas;
for (int i = 0; i < num_camisetas; i++) {
int valor;
cin >> valor;
inventario_camisetas.insert(valor);
}
for (int i = 0; i < num_participantes; i++) {
int limite_participante;
cin >> limite_participante;
// upper_bound nos da el primer elemento > limite_participante
auto it = inventario_camisetas.upper_bound(limite_participante);
if (it == inventario_camisetas.begin()) {
// Si el iterador está al inicio, no hay camisetas <= limite
cout << -1 << "\n";
} else {
// Retrocedemos un paso para obtener el valor máximo <= limite
--it;
cout << *it << "\n";
// ¡IMPORTANTE! Eliminamos por ITERADOR para borrar solo UNA camiseta
inventario_camisetas.erase(it);
}
}
return 0;
}
import sys
import bisect
def resolver():
entrada = sys.stdin.read().split()
if not entrada:
return
num_camisetas = int(entrada[0])
num_participantes = int(entrada[1])
idx = 2
# Leemos e introducimos las camisetas en una lista estándar ordenada
inventario = [int(x) for x in entrada[idx : idx + num_camisetas]]
inventario.sort()
idx += num_camisetas
salida = []
for _ in range(num_participantes):
limite = int(entrada[idx])
idx += 1
# bisect_right nos da la posición del primer elemento > limite
i = bisect.bisect_right(inventario, limite)
if i == 0:
salida.append("-1")
else:
# El elemento en i - 1 es el valor máximo <= limite
valor_elegido = inventario[i - 1]
salida.append(str(valor_elegido))
# Eliminamos esa camiseta concreta de la lista
del inventario[i - 1]
print("\n".join(salida))
if __name__ == "__main__":
resolver()
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.IOException;
import java.util.StringTokenizer;
import java.util.TreeMap;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader lector = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer tokens = new StringTokenizer(lector.readLine());
if (!tokens.hasMoreTokens()) return;
int numCamisetas = Integer.parseInt(tokens.nextToken());
int numParticipantes = Integer.parseInt(tokens.nextToken());
// Simulamos un multiset en Java usando TreeMap
TreeMap<Integer, Integer> inventarioCamisetas = new TreeMap<>();
tokens = new StringTokenizer(lector.readLine());
for (int i = 0; i < numCamisetas; i++) {
int valor = Integer.parseInt(tokens.nextToken());
inventarioCamisetas.put(valor, inventarioCamisetas.getOrDefault(valor, 0) + 1);
}
PrintWriter escritor = new PrintWriter(System.out);
tokens = new StringTokenizer(lector.readLine());
for (int i = 0; i < numParticipantes; i++) {
int limite = Integer.parseInt(tokens.nextToken());
// floorKey(limite) devuelve la clave más grande que sea <= limite
Integer valorElegido = inventarioCamisetas.floorKey(limite);
if (valorElegido == null) {
escritor.println(-1);
} else {
escritor.println(valorElegido);
// Reducimos el stock de esa camiseta en 1
int cantidad = inventarioCamisetas.get(valorElegido);
if (cantidad == 1) {
inventarioCamisetas.remove(valorElegido);
} else {
inventarioCamisetas.put(valorElegido, cantidad - 1);
}
}
}
escritor.flush();
}
}
4. Colas de Prioridad (Heaps)
Una Cola de Prioridad (o Heap) es una estructura de datos que procesa los elementos no en el orden en que llegan, sino en función de su prioridad. A diferencia de una cola tradicional (FIFO) donde el primero en entrar es el primero en salir, aquí el elemento con mayor (o menor) prioridad siempre está arriba del todo.
La analogía del Hospital de Urgencias:
Piensa en la sala de espera de un hospital. No se atiende a los pacientes por estricto orden de llegada: si alguien entra con una dolencia leve y minutos después llega un paciente grave, el sistema de triaje reorganiza inmediatamente la prioridad para que el paciente crítico pase primero. Un Heap hace exactamente esto de forma eficiente en segundo plano.
Estructura interna y Tipos de Heap:
Internamente se implementa como un Árbol Binario Completo alojado sobre un array contiguo, lo que garantiza un acceso al elemento prioritario en tiempo constante $O(1)$ y ajustes logarítmicos $O(\log N)$ al insertar o extraer elementos.
- Max-Heap (Cola de Máximos): El elemento raíz/tope es siempre el valor máximo del conjunto. Cada nodo padre es mayor o igual que sus nodos hijos.
- Min-Heap (Cola de Mínimos): El elemento raíz/tope es siempre el valor mínimo del conjunto. Cada nodo padre es menor o igual que sus nodos hijos.
Un error muy común en programación competitiva es asumir que todos los lenguajes usan la misma prioridad por defecto:
- En C++ (
std::priority_queue), la prioridad por defecto es un Max-Heap (saca primero los valores más grandes). - En Python (
heapq) y Java (PriorityQueue), la prioridad por defecto es un Min-Heap (saca primero los valores más pequeños).
#include <bits/stdc++.h;>
// 1. Max-Heap por defecto (mayor valor arriba)
std::priority_queue<int> max_heap;
max_heap.push(10); // Inserción en O(log N)
max_heap.push(5);
int top_val = max_heap.top(); // Consulta del máximo en O(1) -> 10
max_heap.pop(); // Elimina el tope en O(log N)
// 2. Min-Heap (menor valor arriba)
std::priority_queue<int, std::vector<int>, std::greater<int>> min_heap;
min_heap.push(10);
min_heap.push(5);
int min_val = min_heap.top(); // Consulta del mínimo en O(1) -> 5
import heapq
# 1. Min-Heap por defecto en Python
min_heap = []
heapq.heappush(min_heap, 10) # Inserción en O(log N)
heapq.heappush(min_heap, 5)
min_val = min_heap[0] # Consulta del mínimo en O(1) -> 5
val = heapq.heappop(min_heap) # Extrae el mínimo en O(log N)
# 2. Max-Heap (Multiplicando valores por -1)
max_heap = []
heapq.heappush(max_heap, -10) # Al guardar -10, -10 < -5 (queda arriba)
heapq.heappush(max_heap, -5)
max_val = -heapq.heappop(max_heap) # Recuperamos invirtiendo el signo -> 10
import java.util.PriorityQueue;
import java.util.Collections;
// 1. Min-Heap por defecto
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.add(10); // Inserción en O(log N)
minHeap.add(5);
int minVal = minHeap.peek(); // Consulta del mínimo en O(1) -> 5
minHeap.poll(); // Extrae el mínimo en O(log N)
// 2. Max-Heap
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
maxHeap.add(10);
maxHeap.add(5);
int maxVal = maxHeap.peek(); // Consulta del máximo en O(1) -> 10
| Operación | Descripción | Complejidad |
|---|---|---|
| Inserción (Push/Add) | Inserta un elemento y reajusta la propiedad del árbol. | $O(\log N)$ |
| Consulta Tope (Top/Peek) | Accede al elemento prioritario (raíz) sin eliminarlo. | $O(1)$ |
| Eliminación Tope (Pop/Poll) | Elimina la raíz y reestructura el árbol. | $O(\log N)$ |
Ejemplo Práctico: Las Paellas de CP-UPV
Para la gran fiesta de la competición de Paellas de CP-UPV, los organizadores han preparado $N$ recipientes pequeños con distintas cantidades de arroz $A_1, A_2, \dots, A_N$ en gramos. Como las paellas deben cocinarse en paelleras grandes, es necesario ir combinando los recipientes de dos en dos.
Fusionar dos recipientes que contienen $X$ y $Y$ gramos requiere una paellera de tamaño $X + Y$ y conlleva un coste energético de exactamente $X + Y$ julios, generando un nuevo recipiente de dicho tamaño. Este proceso se repite sucesivamente hasta unificar todo el arroz en un único recipiente. Tu objetivo es calcular el coste energético total mínimo para lograrlo.
Pista y Estrategia: (Haz clic para desplegar)
Para minimizar la suma de costes acumulados, la estrategia voraz (greedy) óptima consiste en fusionar siempre los dos recipientes más pequeños disponibles en cada paso.
Utiliza un Min-Heap (o priority_queue de mínimos):
- Inserta las $N$ cantidades iniciales en la cola de prioridad.
- Mientras la cola tenga más de un elemento, extrae los dos menores ($A$ y $B$), acumula su suma $(A + B)$ al coste total y vuelve a insertar la suma en el Min-Heap.
ENLACE AL PROBLEMA
Ponte a prueba antes de desplegar el código: Resolver Las Paellas de CP-UPV en CodeforcesVer Solución (C++, Python, Java)
#include <bits/stdc++.h>
using namespace std;
int main() {
// Optimización de I/O
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
if (!(cin >> n)) return 0;
// Min-Heap para obtener siempre los dos recipientes más pequeños
priority_queue<long long, vector<long long>, greater<long long>> min_heap;
for (int i = 0; i < n; i++) {
long long arroz;
cin >> arroz;
min_heap.push(arroz);
}
long long coste_total = 0;
// Mientras quede más de un recipiente por fusionar
while (min_heap.size() > 1) {
long long primero = min_heap.top();
min_heap.pop();
long long segundo = min_heap.top();
min_heap.pop();
long long suma = primero + segundo;
coste_total += suma;
// Devolvemos la mezcla al Min-Heap
min_heap.push(suma);
}
cout << coste_total << "\n";
return 0;
}
import sys
import heapq
def resolver():
entrada = sys.stdin.read().split()
if not entrada:
return
n = int(entrada[0])
# heapq en Python es nativamente un Min-Heap
arroz = [int(x) for x in entrada[1:n + 1]]
heapq.heapify(arroz)
coste_total = 0
# Mientras quede más de un recipiente
while len(arroz) > 1:
primero = heapq.heappop(arroz)
segundo = heapq.heappop(arroz)
suma = primero + segundo
coste_total += suma
# Insertamos de nuevo la mezcla en el Heap
heapq.heappush(arroz, suma)
print(coste_total)
if __name__ == "__main__":
resolver()
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.IOException;
import java.util.StringTokenizer;
import java.util.PriorityQueue;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader lector = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer tokens = new StringTokenizer(lector.readLine());
if (!tokens.hasMoreTokens()) return;
int n = Integer.parseInt(tokens.nextToken());
// PriorityQueue en Java es por defecto un Min-Heap
PriorityQueue<Long> minHeap = new PriorityQueue<>();
tokens = new StringTokenizer(lector.readLine());
for (int i = 0; i < n; i++) {
minHeap.add(Long.parseLong(tokens.nextToken()));
}
long costeTotal = 0;
while (minHeap.size() > 1) {
long primero = minHeap.poll();
long segundo = minHeap.poll();
long suma = primero + segundo;
costeTotal += suma;
minHeap.add(suma);
}
PrintWriter escritor = new PrintWriter(System.out);
escritor.println(costeTotal);
escritor.flush();
}
}
5. Uniones de Conjuntos Disjuntos (DSU / Union-Find)
El Disjoint Set Union (DSU) o Union-Find es una de las estructuras más elegantes y eficientes de la informática. Nos permite gestionar una colección de elementos divididos en varios conjuntos disjuntos (sin solapamiento) y responder a preguntas de conectividad en tiempo prácticamente instantáneo.
La analogía de las Redes Sociales y Grupos de Amigos:
Imagina un grupo de personas donde inicialmente nadie se conoce ($N$ conjuntos de $1$ persona). Cada persona elige a un "representante" o líder de su grupo (al principio, cada uno es su propio líder).
- Cuando dos personas de grupos distintos se hacen amigas, sus dos grupos se fusionan: el líder de un grupo pasa a reconocer como jefe al líder del otro grupo.
- Para saber si dos personas se conocen (directa o indirectamente a través de amigos comunes), solo hay que preguntarles quién es el líder supremo de su grupo. Si coinciden, ¡están en el mismo grupo!
Las dos operaciones fundamentales
DSU mantiene un array de padres parent[i] que representa una estructura de bosque de árboles, soportando dos métodos básicos:
find(i): Busca y devuelve el representante (raíz) del conjunto al que pertenece el elementoi.unite(i, j): Fusiona el conjunto que contiene aicon el conjunto que contiene aj.
Sin optimizar, la cadena de padres en find(i) podría degenerar en una lista enlazada de altura $N$, haciendo las consultas en $\mathcal{O}(N)$. Para evitarlo aplicamos dos técnicas:
- Compresión de Caminos (Path Compression): Cada vez que llamamos a
find(i), hacemos que todos los nodos visitados en el camino apunten directamente a la raíz. Las futuras búsquedas serán $\mathcal{O}(1)$. - Unión por Tamaño/Rango (Union by Size/Rank): Al unir dos conjuntos, siempre enganchamos el árbol más pequeño debajo de la raíz del árbol más grande, evitando que el árbol crezca en profundidad de forma innecesaria.
Al combinar ambas, la complejidad amortizada por operación pasa a ser $\mathcal{O}(\alpha(N))$, donde $\alpha$ es la Función Inversa de Ackermann. En la práctica, $\alpha(N) \le 4$ para cualquier $N$ en el universo, ¡haciendo que el DSU funcione en tiempo prácticamente constante $\mathcal{O}(1)$!
#include <bits/stdc++.h>
struct DSU {
std::vector<int> parent;
std::vector<int> sz;
DSU(int n) {
parent.resize(n + 1);
std::iota(parent.begin(), parent.end(), 0); // parent[i] = i
sz.assign(n + 1, 1); // tamaño inicial de cada conjunto = 1
}
// Compresión de caminos (Path Compression)
int find(int i) {
if (parent[i] == i) return i;
return parent[i] = find(parent[i]);
}
// Unión por tamaño (Union by Size)
bool unite(int i, int j) {
int root_i = find(i);
int root_j = find(j);
if (root_i == root_j) return false; // Ya están en el mismo conjunto
// Colocamos el árbol más pequeño bajo el más grande
if (sz[root_i] < sz[root_j]) std::swap(root_i, root_j);
parent[root_j] = root_i;
sz[root_i] += sz[root_j];
return true;
}
bool same(int i, int j) {
return find(i) == find(j);
}
};
class DSU:
def __init__(self, n):
self.parent = list(range(n + 1))
self.sz = [1] * (n + 1)
# Compresión de caminos (Path Compression)
def find(self, i):
if self.parent[i] == i:
return i
self.parent[i] = self.find(self.parent[i])
return self.parent[i]
# Unión por tamaño (Union by Size)
def unite(self, i, j):
root_i = self.find(i)
root_j = self.find(j)
if root_i == root_j:
return False
if self.sz[root_i] < self.sz[root_j]:
root_i, root_j = root_j, root_i
self.parent[root_j] = root_i
self.sz[root_i] += self.sz[root_j]
return True
def same(self, i, j):
return self.find(i) == self.find(j)
class DSU {
int[] parent;
int[] sz;
public DSU(int n) {
parent = new int[n + 1];
sz = new int[n + 1];
for (int i = 0; i <= n; i++) {
parent[i] = i;
sz[i] = 1;
}
}
// Compresión de caminos (Path Compression)
public int find(int i) {
if (parent[i] == i) return i;
return parent[i] = find(parent[i]);
}
// Unión por tamaño (Union by Size)
public boolean unite(int i, int j) {
int rootI = find(i);
int rootJ = find(j);
if (rootI == rootJ) return false;
if (sz[rootI] < sz[rootJ]) {
int temp = rootI; rootI = rootJ; rootJ = temp;
}
parent[rootJ] = rootI;
sz[rootI] += sz[rootJ];
return true;
}
public boolean same(int i, int j) {
return find(i) == find(j);
}
}
Ejemplo Práctico: La Red de PCs de CP-UPV
Para la próxima competición de CP-UPV, los organizadores están configurando $N$ ordenadores en el laboratorio. Inicialmente, ningún ordenador está conectado con otro.
Se van a procesar $Q$ eventos en orden:
1 u v: Se conecta un cable de red entre los ordenadores $u$ y $v$.2 u v: Se quiere verificar si los ordenadores $u$ y $v$ están en la misma red conectada. Para cada evento de este tipo, debes responderYESoNO.
Pista y Estrategia: Disjoint Set Union (Haz clic para desplegar)
Este problema requiere conectar componentes dinámicamente y realizar consultas de pertenencia al mismo grupo en tiempo prácticamente instantáneo.
Utilizamos la estructura DSU con Path Compression y Union by Size:
- Para eventos del tipo
1 u v, ejecutamosdsu.unite(u, v). - Para eventos del tipo
2 u v, comprobamos sidsu.same(u, v)(es decir,find(u) == find(v)).
ENLACE AL PROBLEMA
Ponte a prueba antes de desplegar el código: Resolver La Red de PCs de CP-UPV en CodeforcesVer Solución (C++, Python, Java)
#include <bits/stdc++-h>
using namespace std;
struct DSU {
vector<int> parent;
vector<int> sz;
DSU(int n) {
parent.resize(n + 1);
iota(parent.begin(), parent.end(), 0);
sz.assign(n + 1, 1);
}
int find(int i) {
if (parent[i] == i) return i;
return parent[i] = find(parent[i]);
}
bool unite(int i, int j) {
int root_i = find(i);
int root_j = find(j);
if (root_i == root_j) return false;
if (sz[root_i] < sz[root_j]) swap(root_i, root_j);
parent[root_j] = root_i;
sz[root_i] += sz[root_j];
return true;
}
bool same(int i, int j) {
return find(i) == find(j);
}
};
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, q;
if (!(cin >> n >> q)) return 0;
DSU dsu(n);
while (q--) {
int tipo, u, v;
cin >> tipo >> u >> v;
if (tipo == 1) {
dsu.unite(u, v);
} else {
if (dsu.same(u, v)) {
cout << "YES\n";
} else {
cout << "NO\n";
}
}
}
return 0;
}
import sys
sys.setrecursionlimit(300000)
class DSU:
def __init__(self, n):
self.parent = list(range(n + 1))
self.sz = [1] * (n + 1)
def find(self, i):
if self.parent[i] == i:
return i
self.parent[i] = self.find(self.parent[i])
return self.parent[i]
def unite(self, i, j):
root_i = self.find(i)
root_j = self.find(j)
if root_i == root_j:
return False
if self.sz[root_i] < self.sz[root_j]:
root_i, root_j = root_j, root_i
self.parent[root_j] = root_i
self.sz[root_i] += self.sz[root_j]
return True
def same(self, i, j):
return self.find(i) == self.find(j)
def resolver():
entrada = sys.stdin.read().split()
if not entrada:
return
n = int(entrada[0])
q = int(entrada[1])
dsu = DSU(n)
salida = []
idx = 2
for _ in range(q):
tipo = int(entrada[idx])
u = int(entrada[idx + 1])
v = int(entrada[idx + 2])
idx += 3
if tipo == 1:
dsu.unite(u, v)
else:
if dsu.same(u, v):
salida.append("YES")
else:
salida.append("NO")
print("\n".join(salida))
if __name__ == "__main__":
resolver()
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.IOException;
import java.util.StringTokenizer;
public class Main {
static class DSU {
int[] parent;
int[] sz;
public DSU(int n) {
parent = new int[n + 1];
sz = new int[n + 1];
for (int i = 0; i <= n; i++) {
parent[i] = i;
sz[i] = 1;
}
}
public int find(int i) {
if (parent[i] == i) return i;
return parent[i] = find(parent[i]);
}
public boolean unite(int i, int j) {
int rootI = find(i);
int rootJ = find(j);
if (rootI == rootJ) return false;
if (sz[rootI] < sz[rootJ]) {
int temp = rootI; rootI = rootJ; rootJ = temp;
}
parent[rootJ] = rootI;
sz[rootI] += sz[rootJ];
return true;
}
public boolean same(int i, int j) {
return find(i) == find(j);
}
}
public static void main(String[] args) throws IOException {
BufferedReader lector = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer tokens;
String linea = lector.readLine();
if (linea == null) return;
tokens = new StringTokenizer(linea);
int n = Integer.parseInt(tokens.nextToken());
int q = Integer.parseInt(tokens.nextToken());
DSU dsu = new DSU(n);
PrintWriter escritor = new PrintWriter(System.out);
for (int i = 0; i < q; i++) {
while (tokens == null || !tokens.hasMoreTokens()) {
tokens = new StringTokenizer(lector.readLine());
}
int tipo = Integer.parseInt(tokens.nextToken());
int u = Integer.parseInt(tokens.nextToken());
int v = Integer.parseInt(tokens.nextToken());
if (tipo == 1) {
dsu.unite(u, v);
} else {
if (dsu.same(u, v)) {
escritor.println("YES");
} else {
escritor.println("NO");
}
}
}
escritor.flush();
}
}
Problemas de práctica
Aplica las estructuras aprendidas completando la serie de ejercicios asignados en el concurso activo de nuestro bootcamp en Codeforces.