Вы находитесь на странице: 1из 512

Manual de preparación para concursantes ICPC de la

Universidad de Matanzas Camilo Cienfuegos y IOI


del Instituto Preuniversitario Vocacional de Ciencias
Exactas Karl Marx

Autor de temas: Ernesto Ojea Torres


Luis Andrés Valido Fajardo
Correciones: Arian Amadeo Castellanos Rodriguez
Análisis de ejercicios: Luis Andrés Valido Fajardo

Matanzas, 21 de octubre de 2019


2
Índice general

Prólogo 15

1. Lenguajes 17
1.1. C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.2. Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.3. Tipo de dato . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
1.3.1. C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
1.3.2. Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
1.4. Formato de entrada y salida . . . . . . . . . . . . . . . . . . . 35
1.4.1. Entrada . . . . . . . . . . . . . . . . . . . . . . . . . . 35
1.4.2. Salida . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
1.5. Operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
1.5.1. Operadores Aritméticos . . . . . . . . . . . . . . . . . 59
1.5.2. Operadores de Asignación . . . . . . . . . . . . . . . . 60
1.5.3. Operadores de Relación . . . . . . . . . . . . . . . . . 60
1.5.4. Operadores Lógicos . . . . . . . . . . . . . . . . . . . . 60
1.5.5. Operadores Relacionados con punteros (C++) . . . . . 61
1.5.6. Operadores de Estructuras y Uniones (C++) . . . . . . 61
1.5.7. Operadores Lógicos y de Desplazamiento de Bits . . . . 61
1.5.8. Misceláneas (C++) . . . . . . . . . . . . . . . . . . . . 61
1.6. Estructuras de control condicional e iterativa . . . . . . . . . . 61
1.6.1. Estructura de control condicional (Bifurcaciones) . . . 61
1.6.2. Estructura de control iterativa (Bucles) . . . . . . . . . 66
1.6.3. Sentencias break, continue, goto . . . . . . . . . . . . . 68
1.7. Variantes de entradas de datos . . . . . . . . . . . . . . . . . . 69
1.7.1. Entrada simple . . . . . . . . . . . . . . . . . . . . . . 69
1.7.2. Entrada por casos de pruebas . . . . . . . . . . . . . . 72
1.7.3. Entrada por condición . . . . . . . . . . . . . . . . . . 72
1.7.4. Entrada hasta fin de ficheros . . . . . . . . . . . . . . . 72
1.8. Variantes de salidas de datos . . . . . . . . . . . . . . . . . . . 72

3
4 ÍNDICE GENERAL

2. Add Hoc 73
2.1. Análisis de ejercicios . . . . . . . . . . . . . . . . . . . . . . . 74

3. Fuerza Bruta 87
3.1. Complejidad . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
3.1.1. Eficiencia temporal . . . . . . . . . . . . . . . . . . . . 88
3.2. Análisis de ejercicios . . . . . . . . . . . . . . . . . . . . . . . 91

4. Aritmética y Álgebra 95
4.1. Multplicación de matrices . . . . . . . . . . . . . . . . . . . . 95
4.2. Exponenciación binaria . . . . . . . . . . . . . . . . . . . . . . 98
4.3. Métodos numéricos . . . . . . . . . . . . . . . . . . . . . . . . 99
4.3.1. Búsqueda ternaria . . . . . . . . . . . . . . . . . . . . 100
4.3.2. Newton-Raphson . . . . . . . . . . . . . . . . . . . . . 102
4.4. Problema de Flavio Josefo . . . . . . . . . . . . . . . . . . . . 104
4.5. Máximo común divisor . . . . . . . . . . . . . . . . . . . . . . 106
4.6. Algoritmo Euclidiano Extendido . . . . . . . . . . . . . . . . . 108
4.6.1. Implementación . . . . . . . . . . . . . . . . . . . . . . 109
4.7. Ecuación lineal de diofantina . . . . . . . . . . . . . . . . . . . 110
4.7.1. Encontrar una solución . . . . . . . . . . . . . . . . . . 110
4.7.2. Encontrar el número de soluciones y las soluciones en
un intervalo dado . . . . . . . . . . . . . . . . . . . . . 112
4.7.3. Encuentre la solución con un valor mı́nimo de x + y . . 114
4.8. Inverso Multiplicativo Modular . . . . . . . . . . . . . . . . . 114
4.8.1. Encontrar el inverso modular usando el algoritmo eu-
clidiano extendido . . . . . . . . . . . . . . . . . . . . . 115
4.8.2. Encontrar el inverso modular usando exponenciación
binaria . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
4.8.3. Encontrar el inverso modular para cada número módu-
lo m . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
4.9. Mı́nimo común múltiplo . . . . . . . . . . . . . . . . . . . . . 119
4.10. Sucesiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
4.10.1. Sucesiones aritméticas . . . . . . . . . . . . . . . . . . 119
4.10.2. Sucesiones geométricas . . . . . . . . . . . . . . . . . . 119
4.10.3. Sucesiones telescópicas . . . . . . . . . . . . . . . . . . 119
4.10.4. Técnica de interpolación . . . . . . . . . . . . . . . . . 119
4.10.5. Método de Langrange . . . . . . . . . . . . . . . . . . 119
4.10.6. Método de Newton . . . . . . . . . . . . . . . . . . . . 119
4.10.7. Tabla de diferencias dividida . . . . . . . . . . . . . . . 119
4.10.8. Sucesiones de recurrencias . . . . . . . . . . . . . . . . 119
4.11. Matrix Exponentiation . . . . . . . . . . . . . . . . . . . . . . 119
ÍNDICE GENERAL 5

4.12. Exponentiation by Squaring . . . . . . . . . . . . . . . . . . . 119


4.13. Assignment Problem (Galien Law Party) . . . . . . . . . . . . 119
4.14. Convex on the maximum method (golden section) . . . . . . . 119
4.15. Two-stage simplex method . . . . . . . . . . . . . . . . . . . . 119
4.16. Unconstrained nonlinear optimization method (Quasi-Newton) 119
4.17. Zero of a monotone function (dichotomous) . . . . . . . . . . . 119
4.18. Fast Fourier transform . . . . . . . . . . . . . . . . . . . . . . 119
4.19. Matrix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
4.20. LU decomposition . . . . . . . . . . . . . . . . . . . . . . . . . 119
4.21. Eigenvalue vector-specific . . . . . . . . . . . . . . . . . . . . . 119
4.22. Rational number . . . . . . . . . . . . . . . . . . . . . . . . . 119
4.23. Stern-Brocot Thurs . . . . . . . . . . . . . . . . . . . . . . . . 119
4.24. Jacobi symbol . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
4.25. Quadratic residue (Shanka-Tonelli) . . . . . . . . . . . . . . . 119
4.26. Simultaneous linear congruences . . . . . . . . . . . . . . . . . 119
4.27. The remainder should . . . . . . . . . . . . . . . . . . . . . . 119
4.28. Under the law of inverse n . . . . . . . . . . . . . . . . . . . . 119
4.29. Números Fibonacci . . . . . . . . . . . . . . . . . . . . . . . . 119
4.30. Fracción continua . . . . . . . . . . . . . . . . . . . . . . . . . 119
4.30.1. Fracción continua generalizada . . . . . . . . . . . . . . 119
4.31. Análisis de ejercicios . . . . . . . . . . . . . . . . . . . . . . . 119

5. Ordenamiento 127
5.1. Merge Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
5.2. Heap Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
5.3. Quick Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
5.4. Radix Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
5.5. Burble Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
5.6. CountingSort . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
5.7. Insertion Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
5.8. Selection Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
5.9. Bibliotecas con funciones de ordenamiento . . . . . . . . . . . 140
5.9.1. Bibliotecas de C++ . . . . . . . . . . . . . . . . . . . . 140
5.9.2. Bibliotecas de Java . . . . . . . . . . . . . . . . . . . . 143
5.10. Ordenamiento de datos personalizados . . . . . . . . . . . . . 145
5.10.1. C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
5.10.2. Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
5.11. Change frequency of bubble sort . . . . . . . . . . . . . . . . . 151
5.12. Select k th element . . . . . . . . . . . . . . . . . . . . . . . . 151
5.13. Análisis de ejercicios . . . . . . . . . . . . . . . . . . . . . . . 151
6 ÍNDICE GENERAL

6. Búsqueda 157
6.1. Búsqueda binaria . . . . . . . . . . . . . . . . . . . . . . . . . 157
6.2. Bibliotecas con funciones de búsqueda . . . . . . . . . . . . . 159
6.2.1. Bibliotecas de C++ . . . . . . . . . . . . . . . . . . . . 159
6.2.2. Bibliotecas de Java . . . . . . . . . . . . . . . . . . . . 161
6.3. Selección rápida . . . . . . . . . . . . . . . . . . . . . . . . . . 161
6.4. Búsqueda Exhaustiva . . . . . . . . . . . . . . . . . . . . . . . 163
6.5. Análisis de ejercicios . . . . . . . . . . . . . . . . . . . . . . . 163

7. Cadena 169
7.1. Operaciones básicas . . . . . . . . . . . . . . . . . . . . . . . . 169
7.2. Algoritmos de búsqueda de cadena . . . . . . . . . . . . . . . 170
7.2.1. Algoritmo Knuth-Morris-Pratt (KMP) . . . . . . . . . 170
7.2.2. String searching (Boyer-Moore) . . . . . . . . . . . . . 174
7.3. Palı́ndromo . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
7.3.1. Longest palindrome (Manacher) . . . . . . . . . . . . . 179
7.4. Hashing de cadenas . . . . . . . . . . . . . . . . . . . . . . . . 181
7.4.1. Cálculo del hash de una cadena . . . . . . . . . . . . . 182
7.4.2. Colisión . . . . . . . . . . . . . . . . . . . . . . . . . . 183
7.4.3. Buscar cadenas duplicadas en un arreglo de cadenas . . 184
7.4.4. Cálculo hash rápido de subcadenas de una cadena dada 185
7.4.5. Determine el número de subcadenas diferentes en una
cadena . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
7.5. Expresiones regulares . . . . . . . . . . . . . . . . . . . . . . . 187
7.6. Longest repeated substring (Karp Miller Rosenberg) . . . . . . 187
7.7. Recursive-descent parsing (LL (1)) . . . . . . . . . . . . . . . 187
7.8. Squares determination . . . . . . . . . . . . . . . . . . . . . . 187
7.9. Z Algorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
7.10. Multi-pattern search (Aho-Corasick) . . . . . . . . . . . . . . 187
7.11. String search (Shift And) . . . . . . . . . . . . . . . . . . . . . 187
7.12. Two-dimensional string search (Baker-Bird) . . . . . . . . . . 187
7.13. Análisis de ejercicios . . . . . . . . . . . . . . . . . . . . . . . 187

8. Combinatoria 193
8.1. Variaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
8.1.1. Variación con repetición . . . . . . . . . . . . . . . . . 194
8.2. Permutaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . 194
8.2.1. Permutación con repetición . . . . . . . . . . . . . . . 195
8.3. Combinaciones . . . . . . . . . . . . . . . . . . . . . . . . . . 195
8.3.1. Algunas propiedades de los números combinatorios . . 196
8.4. Máscara de bit . . . . . . . . . . . . . . . . . . . . . . . . . . 200
ÍNDICE GENERAL 7

8.5. Backtracking . . . . . . . . . . . . . . . . . . . . . . . . . . . 204


8.6. Números de Catalan . . . . . . . . . . . . . . . . . . . . . . . 207
8.7. K-Combinations . . . . . . . . . . . . . . . . . . . . . . . . . . 208
8.8. Permutations . . . . . . . . . . . . . . . . . . . . . . . . . . . 208
8.9. Power Set . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208
8.10. Principio de inclusión exclusión . . . . . . . . . . . . . . . . . 208
8.11. Análisis de ejercicios . . . . . . . . . . . . . . . . . . . . . . . 208

9. Teorı́a de número 215


9.1. Números primos . . . . . . . . . . . . . . . . . . . . . . . . . . 215
9.1.1. Gauss primality . . . . . . . . . . . . . . . . . . . . . . 215
9.1.2. Tests probabilı́sticos de primalidad . . . . . . . . . . . 215
9.1.3. Sieve interval . . . . . . . . . . . . . . . . . . . . . . . 217
9.1.4. Criba de Eratóstenes . . . . . . . . . . . . . . . . . . . 217
9.1.5. Criba de Atkin . . . . . . . . . . . . . . . . . . . . . . 219
9.2. Sistemas de numeración . . . . . . . . . . . . . . . . . . . . . 222
9.3. Factores primos . . . . . . . . . . . . . . . . . . . . . . . . . . 225
9.4. Reglas de divisibilidad . . . . . . . . . . . . . . . . . . . . . . 228
9.4.1. Usando BigInteger . . . . . . . . . . . . . . . . . . . . 229
9.4.2. Análisis matemático . . . . . . . . . . . . . . . . . . . 230
9.5. Euler’s function . . . . . . . . . . . . . . . . . . . . . . . . . . 231
9.6. The Carmichael function . . . . . . . . . . . . . . . . . . . . . 231
9.7. The Mobius function . . . . . . . . . . . . . . . . . . . . . . . 231
9.8. Autómata de divisibilidad . . . . . . . . . . . . . . . . . . . . 231
9.9. Análisis de ejercicios . . . . . . . . . . . . . . . . . . . . . . . 231

10.Geometrı́a 243
10.1. Distancia mı́nima en el plano (Closest Pair) . . . . . . . . . . 243
10.1.1. Primera variante de solución: Solución iterativa . . . . 243
10.1.2. Segunda variante de solución: Divide y Conquista . . . 245
10.1.3. Tercera variante de solución: Algoritmo de lı́nea de ba-
rrido . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
10.1.4. Cuarta variante de solución: Algoritmos aleatorios in-
crementales . . . . . . . . . . . . . . . . . . . . . . . . 251
10.2. Distancia esférica . . . . . . . . . . . . . . . . . . . . . . . . . 254
10.3. Cubiertas convexas . . . . . . . . . . . . . . . . . . . . . . . . 254
10.3.1. Algoritmo Incremental . . . . . . . . . . . . . . . . . . 256
10.3.2. La Caminata de Jarvis . . . . . . . . . . . . . . . . . . 257
10.3.3. Algoritmo QuickHull . . . . . . . . . . . . . . . . . . . 259
10.3.4. Algoritmo de Cubierta Convexa aplicando Técnica de
Divide y Vencerás . . . . . . . . . . . . . . . . . . . . . 261
8 ÍNDICE GENERAL

10.3.5. Algoritmo de Graham o Exploración Graham (Graham


Scan) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263
10.3.6. Convex hull (Andrew’s Monotone Chain) . . . . . . . . 268
10.4. Intersección entre lı́neas y segmentos . . . . . . . . . . . . . . 269
10.4.1. Intersección de segmentos . . . . . . . . . . . . . . . . 270
10.4.2. Intersección de lı́neas . . . . . . . . . . . . . . . . . . . 275
10.4.3. Intersección entre lı́neas y segmentos . . . . . . . . . . 275
10.5. Distancia máxima en el plano (Farthest Pair) . . . . . . . . . 275
10.6. Basic elements of plane geometry . . . . . . . . . . . . . . . . 275
10.7. Traveling direction of the point . . . . . . . . . . . . . . . . . 275
10.8. Distancia entre lı́neas y segmentos . . . . . . . . . . . . . . . . 275
10.9. End point . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
10.10.Perturbative deformation of a polygon . . . . . . . . . . . . . 275
10.11.Point - polygon inclusion decision . . . . . . . . . . . . . . . . 275
10.12.Simple polygon triangulation (decomposition ears) . . . . . . . 275
10.13.The area of a polygon . . . . . . . . . . . . . . . . . . . . . . 275
10.14.Convexity determination . . . . . . . . . . . . . . . . . . . . . 275
10.15.Convex polygon cutting . . . . . . . . . . . . . . . . . . . . . 275
10.16.Convex polygon intersection . . . . . . . . . . . . . . . . . . . 275
10.17.Endpoints of a convex polygon . . . . . . . . . . . . . . . . . . 275
10.18.Points - including a convex polygon determined . . . . . . . . 275
10.19.The diameter of a convex polygon . . . . . . . . . . . . . . . . 275
10.20.Delaunay triangulation . . . . . . . . . . . . . . . . . . . . . . 275
10.21.Line arrangement . . . . . . . . . . . . . . . . . . . . . . . . . 275
10.22.Visibility graph . . . . . . . . . . . . . . . . . . . . . . . . . . 275
10.23.Dual transformation . . . . . . . . . . . . . . . . . . . . . . . 275
10.24.Line crossing problem (lying flat scanning) . . . . . . . . . . . 275
10.25.Merging segment . . . . . . . . . . . . . . . . . . . . . . . . . 275
10.26.Point-to-last . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
10.27.Positioning of (Map slab) . . . . . . . . . . . . . . . . . . . . . 275
10.28.Scanning line arrangement . . . . . . . . . . . . . . . . . . . . 275
10.29.Search Area (kd Thursday) . . . . . . . . . . . . . . . . . . . . 275
10.30.Skewer straight line . . . . . . . . . . . . . . . . . . . . . . . . 275
10.31.Unsorted . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
10.32.Point in Convex Polygon . . . . . . . . . . . . . . . . . . . . . 275
10.33.Essential elements of the geometric space . . . . . . . . . . . . 275
10.34.Minimum enclosing ball . . . . . . . . . . . . . . . . . . . . . 275
10.35.Análisis de ejercicios . . . . . . . . . . . . . . . . . . . . . . . 275
ÍNDICE GENERAL 9

11.Programación dinámica 287


11.1. Coin change . . . . . . . . . . . . . . . . . . . . . . . . . . . . 287
11.2. Edit distance . . . . . . . . . . . . . . . . . . . . . . . . . . . 289
11.3. Subsecuencia de longitud máxima común (LCS) . . . . . . . . 291
11.4. Ordenaciones de objetos entre dos relaciones . . . . . . . . . . 295
11.5. Mochila (0,1) (Knapsack 0,1) . . . . . . . . . . . . . . . . . . 297
11.6. Mochila (0,1) con múltiples elementos . . . . . . . . . . . . . . 301
11.7. Multiplicación óptima de matrices . . . . . . . . . . . . . . . . 303
11.8. Máxima subsecuencia incremental (LIS) . . . . . . . . . . . . 308
11.9. Contar la cantidad de ocurrencia de un patrón dentro de una
cadena como subsecuencia de caracteres no consecutivos . . . 311
11.10.Needleman-Wunsch . . . . . . . . . . . . . . . . . . . . . . . . 314
11.11.Encontrar la submatriz cero más grande . . . . . . . . . . . . 319
11.12.Subarray máximo . . . . . . . . . . . . . . . . . . . . . . . . . 321
11.13.Subconjunto suma . . . . . . . . . . . . . . . . . . . . . . . . 321
11.13.1.Conjunto de números positivos solamente . . . . . . . . 322
11.13.2.Conjunto de números positivos y negativos . . . . . . . 322
11.14.Trı́angulo de números . . . . . . . . . . . . . . . . . . . . . . . 322
11.15.Segmented Least Squares . . . . . . . . . . . . . . . . . . . . . 322
11.16.Análisis de ejercicios . . . . . . . . . . . . . . . . . . . . . . . 322

12.Teorı́a de juego 327


12.1. Lo básico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327
12.2. El juego del Nim . . . . . . . . . . . . . . . . . . . . . . . . . 329
12.3. Números Grundy . . . . . . . . . . . . . . . . . . . . . . . . . 330
12.4. Conclusión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332
12.5. Análisis de ejercicios . . . . . . . . . . . . . . . . . . . . . . . 332

13.Estructura de datos 335


13.1. Arreglos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 336
13.1.1. C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
13.1.2. Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
13.2. Matrices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 338
13.2.1. C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 338
13.2.2. Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
13.3. Lista . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
13.3.1. C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340
13.3.2. Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . 345
13.4. Pila . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347
13.4.1. C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 348
13.4.2. Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . 348
10 ÍNDICE GENERAL

13.5. Cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 349


13.5.1. C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 350
13.5.2. Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . 350
13.6. Cola con prioridad . . . . . . . . . . . . . . . . . . . . . . . . 350
13.6.1. C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351
13.6.2. Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351
13.7. Cola doblemente terminada . . . . . . . . . . . . . . . . . . . 351
13.7.1. C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
13.7.2. Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
13.8. Conjunto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
13.8.1. C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
13.8.2. Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
13.9. Diccionario . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
13.9.1. C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
13.9.2. Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
13.10.Estructura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
13.11.Árbol . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
13.12.Árbol binario . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
13.13.Disjoint Set Union . . . . . . . . . . . . . . . . . . . . . . . . 357
13.14.Árbol binario indexado o Thurs Fenwick . . . . . . . . . . . . 359
13.15.Árbol de segmento (Segment Tree) . . . . . . . . . . . . . . . 362
13.16.Árbol de rango (Range Tree) . . . . . . . . . . . . . . . . . . . 370
13.16.1.Operaciones . . . . . . . . . . . . . . . . . . . . . . . . 370
13.16.2.Propagación diferida Lazy Propagation . . . . . . . . . 373
13.17.Árbol biselado (Splay Tree) . . . . . . . . . . . . . . . . . . . 374
13.18.Consulta de rango mı́nimo (Range Minimum Query RMQ) . . 380
13.18.1.Solución ingenua . . . . . . . . . . . . . . . . . . . . . 380
13.18.2.Solución usando tiempo constante y espacio linearithmic380
13.18.3.Solución usando tiempo logarı́tmico y espacio lineal . . 381
13.18.4.Solución usando tiempo constante y espacio lineal . . . 382
13.19.Descomposición raı́z cuadrada(Sqrt Decomposition) . . . . . . 384
13.19.1.Estructura de datos basada en descomposición raı́z cua-
drada . . . . . . . . . . . . . . . . . . . . . . . . . . . 384
13.19.2.Implementación . . . . . . . . . . . . . . . . . . . . . . 385
13.19.3.Otros problemas . . . . . . . . . . . . . . . . . . . . . 387
13.20.Algoritmo de Mo(Mo’s algorithm) . . . . . . . . . . . . . . . . 388
13.20.1.Implementación . . . . . . . . . . . . . . . . . . . . . . 389
13.20.2.Complejidad . . . . . . . . . . . . . . . . . . . . . . . . 390
13.21.Árbol AVL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 393
13.22.Árbol binario de búsqueda . . . . . . . . . . . . . . . . . . . . 393
13.23.Heap Binomial . . . . . . . . . . . . . . . . . . . . . . . . . . 393
ÍNDICE GENERAL 11

13.24.Bit Queue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 393


13.25.Circular Doubly-Linked List . . . . . . . . . . . . . . . . . . . 393
13.26.Fibonacci Heap . . . . . . . . . . . . . . . . . . . . . . . . . . 393
13.27.Link-cut Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . 393
13.28.Pairing Heap . . . . . . . . . . . . . . . . . . . . . . . . . . . 393
13.29.Proto-vEB Tree . . . . . . . . . . . . . . . . . . . . . . . . . . 393
13.30.XOR Linked List . . . . . . . . . . . . . . . . . . . . . . . . . 393
13.31.van Emde Boas Tree . . . . . . . . . . . . . . . . . . . . . . . 393
13.32.Trie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 393
13.33.Suffix Automaton . . . . . . . . . . . . . . . . . . . . . . . . . 393
13.34.Suffix Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . 393
13.35.Suffix Array + LCP Array . . . . . . . . . . . . . . . . . . . . 393
13.36.Thurs Splay . . . . . . . . . . . . . . . . . . . . . . . . . . . . 393
13.37.Treap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 393
13.38.Thurs interval . . . . . . . . . . . . . . . . . . . . . . . . . . . 393
13.39.Grilla de Buckets . . . . . . . . . . . . . . . . . . . . . . . . . 393
13.40.Análisis de ejercicios . . . . . . . . . . . . . . . . . . . . . . . 393

14.Greedy 399
14.0.1. Fundamentos . . . . . . . . . . . . . . . . . . . . . . . 399
14.1. Intervalo de Tareas (Interval Scheduling) . . . . . . . . . . . . 402
14.2. El fontanero diligente . . . . . . . . . . . . . . . . . . . . . . . 403
14.2.1. Mas fontaneros . . . . . . . . . . . . . . . . . . . . . . 405
14.3. Los ficheros y el disquete . . . . . . . . . . . . . . . . . . . . . 405
14.4. El camionero con prisa . . . . . . . . . . . . . . . . . . . . . . 406
14.5. Análisis de ejercicios . . . . . . . . . . . . . . . . . . . . . . . 408

15.Teorı́a de Grafos 415


15.1. Representación computacional . . . . . . . . . . . . . . . . . . 418
15.2. Búsqueda en profundidad (DFS) . . . . . . . . . . . . . . . . . 421
15.3. Búsqueda en anchura (BFS) . . . . . . . . . . . . . . . . . . . 424
15.4. Algoritmo Kruskal . . . . . . . . . . . . . . . . . . . . . . . . 426
15.5. Algoritmo Prim . . . . . . . . . . . . . . . . . . . . . . . . . . 431
15.6. Algoritmo Dijkstra . . . . . . . . . . . . . . . . . . . . . . . . 437
15.7. Algoritmo Bellman-Ford . . . . . . . . . . . . . . . . . . . . . 444
15.8. Algoritmo Floyd-Warshall . . . . . . . . . . . . . . . . . . . . 448
15.9. Orden topológico . . . . . . . . . . . . . . . . . . . . . . . . . 450
15.10.Componentes conexas . . . . . . . . . . . . . . . . . . . . . . . 452
15.11.Diámetro del árbol . . . . . . . . . . . . . . . . . . . . . . . . 455
15.12.Detención de ciclos de un grafo . . . . . . . . . . . . . . . . . 458
15.13.Ancestro común más bajo (LCA) . . . . . . . . . . . . . . . . 460
12 ÍNDICE GENERAL

15.13.1.Idea del algoritmo . . . . . . . . . . . . . . . . . . . . . 461


15.14.Buscar los puntos de articulación de un grafo . . . . . . . . . . 464
15.15.Grafo bipartido . . . . . . . . . . . . . . . . . . . . . . . . . . 470
15.15.1.Comprobar si el grafo es bipartido . . . . . . . . . . . . 471
15.15.2.Máximo pareo de un grafo bipartido . . . . . . . . . . 473
15.15.3.Edge-colored bipartite graph . . . . . . . . . . . . . . . 473
15.15.4.Minimum weight maximum matching of bipartite graph 473
15.16.Buscar los puentes de un grafo . . . . . . . . . . . . . . . . . . 473
15.17.K enésimo camino más corto . . . . . . . . . . . . . . . . . . . 473
15.18.Dinic’s Algorithm . . . . . . . . . . . . . . . . . . . . . . . . . 473
15.19.Edmonds-Karp Algorithm . . . . . . . . . . . . . . . . . . . . 473
15.20.Ford-Fulkerson Algorithm . . . . . . . . . . . . . . . . . . . . 473
15.21.Heavy-Light Decomposition . . . . . . . . . . . . . . . . . . . 473
15.22.Tarjan’s SCC Algorithm . . . . . . . . . . . . . . . . . . . . . 473
15.23.Minimum diameter spanning tree (Cuninghame-Green) . . . . 473
15.24.Minimum spanning directed tree (Chu-Liu - Edmond) . . . . . 473
15.25.Minimum Steiner tree (Dreyfus-Wagner) . . . . . . . . . . . . 473
15.26.Maximum flow (Goldberg-Tarjan) . . . . . . . . . . . . . . . . 473
15.27.Minimum cost flow (Primal-Dual) . . . . . . . . . . . . . . . . 473
15.28.Thurs Gomory-Hu . . . . . . . . . . . . . . . . . . . . . . . . 473
15.29.Undirected graph a minimum cut across (Nagamochi-Ibaraki
- Stoer-Wagner) . . . . . . . . . . . . . . . . . . . . . . . . . . 473
15.30.Maximum matching (Edmonds) . . . . . . . . . . . . . . . . . 473
15.31.Maximum matching of bipartite graph . . . . . . . . . . . . . 473
15.32.Minimum weight maximum matching . . . . . . . . . . . . . . 473
15.33.Height of the tree . . . . . . . . . . . . . . . . . . . . . . . . . 473
15.34.Off-line least common ancestor (Tarjan) . . . . . . . . . . . . 473
15.35.Isomorfismo de grafos . . . . . . . . . . . . . . . . . . . . . . . 473
15.36.Tree isomorphism . . . . . . . . . . . . . . . . . . . . . . . . . 473
15.37.Euler path Mukai Tamotsu . . . . . . . . . . . . . . . . . . . . 473
15.38.Shortest Hamiltonian path . . . . . . . . . . . . . . . . . . . . 473
15.39.Undirected Chinese postman problem . . . . . . . . . . . . . . 473
15.40.Undirected Eulerian path . . . . . . . . . . . . . . . . . . . . . 473
15.41.Análisis de ejercicios . . . . . . . . . . . . . . . . . . . . . . . 473

16.Misceláneas 491
16.1. Trabajo con fechas . . . . . . . . . . . . . . . . . . . . . . . . 491
16.1.1. Calendario Juliano . . . . . . . . . . . . . . . . . . . . 491
16.1.2. Calendario Gregoriano . . . . . . . . . . . . . . . . . . 492
16.1.3. Java: Trabajo con fecha . . . . . . . . . . . . . . . . . 492
16.1.4. Dı́a de la semana . . . . . . . . . . . . . . . . . . . . . 497
ÍNDICE GENERAL 13

16.1.5. Cantidad de dı́as entre dos fechas . . . . . . . . . . . . 497


16.1.6. Año bisiesto . . . . . . . . . . . . . . . . . . . . . . . . 497
16.2. Ajedrez . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 498
16.2.1. Rey . . . . . . . . . . . . . . . . . . . . . . . . . . . . 498
16.2.2. Dama . . . . . . . . . . . . . . . . . . . . . . . . . . . 498
16.2.3. Torre . . . . . . . . . . . . . . . . . . . . . . . . . . . . 498
16.2.4. Alfil . . . . . . . . . . . . . . . . . . . . . . . . . . . . 498
16.2.5. Caballo . . . . . . . . . . . . . . . . . . . . . . . . . . 499
16.3. Jurado Online . . . . . . . . . . . . . . . . . . . . . . . . . . . 501
16.4. Estrategias de competencia . . . . . . . . . . . . . . . . . . . . 502
16.5. Equipo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 505
16.6. Código Gray . . . . . . . . . . . . . . . . . . . . . . . . . . . . 505
16.7. Parser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 507
16.7.1. Leer una lı́nea completa con C++ . . . . . . . . . . . . 507
16.8. Puzzle 15 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 509
16.9. Recursividad . . . . . . . . . . . . . . . . . . . . . . . . . . . . 510
16.9.1. Funciones matemáticas recursivas . . . . . . . . . . . . 511
16.9.2. Recursividad simple . . . . . . . . . . . . . . . . . . . 511
16.9.3. Recursividad ramificada . . . . . . . . . . . . . . . . . 511
16.9.4. Recursividad con bactracking . . . . . . . . . . . . . . 511
16.10.Divide y Vencerás . . . . . . . . . . . . . . . . . . . . . . . . . 511
16.11.Fı́sica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 511
16.12.Probabilidades y estadı́sticas . . . . . . . . . . . . . . . . . . . 511
16.13.Team Reference . . . . . . . . . . . . . . . . . . . . . . . . . . 511
16.14.Análisis de ejercicios . . . . . . . . . . . . . . . . . . . . . . . 511
PRÓLOGO

14
Prólogo

El siguiente documento es una versión mejorada de muchos que comenza-


ron siendo un simple .txt donde tenı́a la mala costumbre de colocar el nombre
del ejercicio, por la temática por la cual lo habı́a resuelto y el jurado donde
lo habı́a solucionado. Creo por ahora este ha superado por mucho los ante-
riores, quizá dentro de algún tiempo lo vea horrendo. Tuve la necesidad de
retomarlo por dos cuestiones. La primera, no se imaginan cuanto me pesaba
tener que implementar desde cero ciertos algoritmos sabiendo que los habı́a
hecho con anterior para otros ejercicios, ası́ que comencé anotar por temáti-
ca los ejercicio que iba resolviendo, no es lo mismo tener que hacer la rueda
desde cero, a decir creo que para este ejercicio puede coger la rueda de otro
y solo tengo pintarla o cambiar el tipo de goma. Lo segundo para ayudar
muchachos sobre todo de la Universidad de Matanzas que se iniciaban en
este mundillo.
La estructura del documento esta acorde según mi punto de vista a las
principales áreas que debe dominar un concursante de programación. Dentro
de cada área abordaré aquellas temáticas o puntos que de cierta manera do-
mino aunque sea lo básico y que al menos tenga un ejemplo de problema en
algún jurado que lo solucione usando ese conocimiento. No obstante si alguien
cree que faltó algo que debe ser incluido y se siente en condiciones de abordar-
lo sin problema se puede poner en contacto conmigo (luis.valido@umcc.cu) e
incluyó su aporte a este documento.
Cada capı́tulo termina con el análisis de problemas los cuales se enmarca
dentro temas tratados en el capı́tulo. De no existir ninguna especificación se
puede asumir que la solución fue hecha en C++ y el problema se encuentra
publicado en Juez en Lı́nea Caribeño(COJ) http://coj.uci.cu y el veredicto
de ese análisis fue aceptado en ese momento. Bueno creo que por ahora no
hay nada más que aclarar. Creo que la cantidad de puntos que pierdo por
una ortografı́a no estandarizada es aceptable por la cantidad de puntos que
pueden otorgar los conocimientos aquı́ expuestos (No obstante se aceptan
correciones).Importante casi lo olvidaba este documento digamos que es un
beta que se va ir llenando a medida de las posibilidades de tiempo de los auto-

15
PRÓLOGO

res asi que puede encontrar temáticas con solamente el nombre, eso significa
que ya se pensado abordarlo lo que aún no se ha tenido el tiempo.
Tambien fueron utilizados los siguientes jurados online:
MOG Matcom Online Grader http://judge.matcom.uh.cu/
TIMUS Timus Online Judge http://acm.timus.ru
Athens Online Judge http://aoj.umcc.cu
DMOJ http://dmjo.uclv.cu

16
Capı́tulo 1

Lenguajes

Cuando no enfrentamos a los problemas de concurso de programación en


la selección de un determinado lenguaje de programación para implemen-
tación de la solución siempre debemos tener en cuenta que las tecnologı́as
están para ser parte de la solución y no parte del problema es por eso que es
importante dominar al menos dos lenguajes de programación de los lenguajes
permitido en los concursos de ACM porque hasta hoy no existe un lenguaje
que sea perfecto para cualquier situación, siempre son fuerte en determinadas
áreas pero cojean cuando son utilizados para solucionar problemas enfoca-
dos en otras áreas. Es por eso que es importante para un programador de
concurso dominar al menos dos lenguajes de programación uno que sea su
primario y otro como secundario con la finalidad que este sepa cubrir las
áreas donde el primario sea deficiente. En lo personal tengo como primario a
C++ y como secundario Java.

1.1. C++
C++ es un lenguaje de programación diseñado a mediados de los años
1980 por Bjarne Stroustrup. La intención de su creación fue el extender al
exitoso lenguaje de programación C con mecanismos que permitan la mani-
pulación de objetos. En ese sentido, desde el punto de vista de los lenguajes
orientados a objetos, el C++ es un lenguaje hı́brido. El nombre C++ fue
propuesto por Rick Mascitti en el año 1983, cuando el lenguaje fue utilizado
por primera vez fuera de un laboratorio cientı́fico. Antes se habı́a usado el
nombre C con clases. En C++, la expresión C++ significa incremento de C
y se refiere a que C++ es una extensión de C.
El 12 de agosto de 2011, Herb Sutter, presidente del comité de estándares
de C++, informó la aprobación unánime del nuevo estándar. Esta nueva

17
LENGUAJES

versión se conoce como C++11. Entre las caracterı́sticas del nuevo estándar
se pueden destacar:

1. Funciones lambda

2. Referencias rvalue

3. La palabra reservada auto

4. Inicialización uniforme

5. Plantillas con número variable de argumentos

Además se ha actualizado la biblioteca estándar del lenguaje.La continui-


dad del C++11 es C++14, que es la versión actual, y en el futuro, se estima
que a finales de 2017, será C++17.

Estructura de una solución con C++


Una solución en C++ para los problemas propuesto en concursos de pro-
gramación se puede en fragmentar en cuatro secciones, a continuación se
detallará cada una de las secciones.
La primera sección esta destinada para la incluir todas bibliotecas propias
del lenguaje que serán utilizadas en la solución del problema dentro dichas
bibliotecas la mas común es es iostream utilizada para la entrada y salida
de datos. A medidas que se avance en el curso veremos otras bibliotecas
utiles. Para utilizar dichas bibliotecas dentro de nuestra solución se utiliza la
directiva #include por ejemplo

# include < iostream >

La segunda sección de nuestra estructura va estar destinada a definir nuestra


propias macros y definiciones elementos muy útiles en pos agilizar y mejorar
la compresión de muestro código. Ejemplo de lo anterior expuesto son por lo
siguientes fragmentos.

# define PI 3.14
# define LL long long

En el primer caso define el valor de la constante de PI que para mı́ su valor


será 3.14, y no 3,1415926535897932384. Para luego usarlo donde deberı́a solo
poner 3.14 pongo PI. En el segundo caso lo que se hizo fue una abreviación
de un tipo de dato como es el long long y cuando vaya declarar una variable
de este tipo no tengo que poner long long sino sencillamente LL ejemplo:

18
LENGUAJES

LL a ;

La tercera sección la destinaremos a la declaración de variables y métodos


auxiliares que utilizaremos en la solución de nuestro problema. Tener en
cuenta que si se tiene dos métodos A, B y dentro de este último es invocado
el método A la implementación de A tiene que estar por encima o primero
que la implementación del método B. En la última y cuarta sección va estar
enmarcada dentro del programa principal

int main ()
{
// desarrollo de nuestra solucion
return 0;
}

Dentro de este metodo y por encima de la linea return 0; debemos leer la


entrada de datos, invocar y utilizar las variables, metodos para solucionar el
problema e imprimir la solución de acuerdo al formato de salida especificado
en el problema. A continuación se muestra una solución.

# include < iostream >


# include < vector >
# include < algorithm >
# include < stdio .h >
# include < math .h >
# include < stack >
# include <set >
# include < queue >
# include < string >
# include < string .h >
# include <map >
# include < complex >
# include < cmath >
# define MOD 1000003
# define MAX 3
# define LIMIT 9 2 2 33 7 2 03 6 8 54 7 7 58 0 7
# define INF 1000000000010
# define ULL unsigned long long
# define LL long long
# define ENDL ’\ n ’
# define SYMBOL ’# ’
using namespace std ;

19
LENGUAJES

const double radius = 6371009;


int cases ;
double lat_ptoA , lat_ptoB , long_ptoA , long_ptoB , alfa , arc ,
beta , triangule , AB ,
solutions , lower , upper ;

double convertToRadian ( double degre_sexa )


{
return degre_sexa /180* M_PI ;
}

double distanceSphere ( double A_lat , double A_long , double


B_lat , double B_long )
{
return acos ( sin ( convertToRadian ( A_lat ) ) * sin (
convertToRadian ( B_lat ) ) + cos ( convertToRadian (
A_lat ) ) * cos ( convertToRadian ( B_lat ) ) * cos (
convertToRadian ( A_long ) ) * cos ( convertToRadian (
B_long ) ) + cos ( convertToRadian ( A_lat ) ) * cos (
convertToRadian ( B_lat ) ) * sin ( convertToRadian (
A_long ) ) * sin ( convertToRadian ( B_long ) ) ) * radius
;
}

double distanceRect ( double a , double b , double c , double d )


{
return sqrt ((( a - c ) *( a - c ) ) +(( b - d ) *( b - d ) ) ) ;
}

int main ()
{
// freopen (" Input . txt " ," r " , stdin ) ;
cout . setf ( ios :: fixed , ios :: floatfield ) ;
cout . precision (0) ;
ios_base :: sync_with_stdio (0) ;
std :: cin . tie (0) ;
while ( cin > > cases )
{
while ( cases - -)
{
cin > > lat_ptoA > > long_ptoA > >
lat_ptoB > > long_ptoB ;
arc = distanceSphere ( lat_ptoA ,

20
LENGUAJES

long_ptoA , lat_ptoB , long_ptoB )


;
alfa =( arc *180) /( M_PI * radius ) ;
beta =(180 - alfa ) /2;
if ( alfa <180)
{
triangule =( radius * radius
* sin ( convertToRadian (
alfa ) ) ) /2;
AB =( triangule *2) /(
radius * sin (
convertToRadian ( beta
)));
}
else
AB =2* radius ;
solutions = arc - AB ;
cout < < solutions < < ENDL ;
}
}
return 0;
}

Captura y salida de datos


En múltiples ocasiones la diferencia entre una solución correcta y una
incorrecta se puede definir en la captura o salida de datos por esos es impor-
tante conocer que recursos nos brinda C++ para realizar dichas acciones.
Las clases basic ostream y basic stream, y los objetos cout y cin, pro-
porcionan la entrada y salida estándar de datos (teclado/pantalla). También
está disponible cerr, similar a cout, usado para la salida estándar de errores.
Estas clases tienen sobrecargados los operadores (( y )), respectivamente, con
el objeto de ser útiles en la inserción/extracción de datos a dichos flujos.
Son operadores inteligentes, ya que son capaces de adaptarse al tipo de da-
tos que reciben, aunque tendremos que definir el comportamiento de dicha
entrada/salida para clases/tipos de datos definidos por el usuario.
Es posible formatear la entrada/salida, indicando el número de dı́gitos
decimales a mostrar, si los textos se pasarán a minúsculas o mayúsculas, si
los números recibidos están en formato octal o hexadecimal, etc.
Como C++ es una extensión de C se puede usar en una implementación
de C++ para captura y mostrar los datos las funciones scanf y printf

21
LENGUAJES

El otro aspecto a tener en cuenta es el formato de entrada el cual puede


presentar varias variantes para las cuales se debe estar preparado, a conti-
nuación se detallan los más comunes.
Cuando me dan la cantidad de casos previamente.

int cases ;
cin > > cases ;
while ( cases - -)
{
/* Desarrollo de la solucion */
}
/* otra variante cuando necesito saber los casos */
for ( int i =1; i <= cases ; i ++)
{
/* Desarrollo de la solucion */
}

Se lee hasta que en la entrada se cumpla una determinada condición en


cuanto a sus valores.

bool exits = false ;


int entrada ;
while (! exits )
{
cin > > entrada ;
if ( entrada ==0)
exits = true ;
else
{
/* Desarrollo de la solucion */
}
}

Se lee hasta fin de fichero o se sabe cuándo termina la entrada

int entrada2 ;
while ( cin > > entrada2 )
{
/* Desarrollo de la solucion */
}

En ciertos problemas se hace necesario leer toda una lı́nea incluyendo los
espacios para dicha situación se puede utilizar lo siguiente:
string line

22
LENGUAJES

getline(cin,line)
Tener en cuenta que el salto de lı́nea no lo captura y si vuelves a leer otra
cadena seguido solo vas a leer el salto de lı́nea.

Conformación de una plantilla base para la implemen-


tación de una solución en C++.
La conformación de una plantilla o estructura base que nos sirva para
implementar cualquier solución en C++ nos permita agilizar el proceso de
codificación, nos evita tener que replicar ciertos fragmentos de códigos que
son comunes dentro cualquier solución. A continuación proponemos una.

/* Inicio inclusion de las bibliotecas que necesito


utilizar en mi solucion */
# include < iostream >
# include < vector >
# include < algorithm >
# include < stdio .h >
# include < math .h >
# include < stack >
# include <set >
# include < queue >
# include < string >
# include < string .h >
# include <map >
# include < complex >
# include < cmath >
/* Fin inclusion de las bibliotecas que necesito utilizar
en mi solucion */

/* Inicio de la macros y definiciones que necesito


utilizar en mi solucion */
# define REP (i , n ) for ( int i = 0; i <( int ) n ; ++ i )
# define FOR (i , c ) for ( __typeof (( c ) . begin () ) i =( c ) .
begin () ; i !=( c ) . end () ;++ i )
# define ALL ( c ) ( c ) . begin () ,( c ) . end ()
# define MOD 1000003
# define MAX 100
# define MAX_TREE ( MAX << 2)
# define LIMIT 9 2 2 33 7 2 03 6 8 54 7 7 58 0 7
# define INF 1000000000010
# define ULL unsigned long long

23
LENGUAJES

# define LL long long


# define ENDL ’\ n ’
# define SYMBOL ’# ’

/* Fin de la macros y definiciones que necesito utilizar


en mi solucion */

/* Estandar de C ++ que contiene la mayoria de las


bibliotecas que utilizamos en nuestras soluciones , no
quitar por nada del mundo . */
using namespace std ;

/* Inicio de las declaraciones e implementaciones de las


variables globales
y metodos auxiliares */

/* Fin de las declaraciones e implementaciones de las


variables globales
y metodos auxiliares */

int main ()
{

/* Para cuando leo los datos de un fichero local


en mi PC comentar la linea antes de subir
sino puede dar Wrong Answer o Compilation
Error */
freopen ( " Input . txt " ," r " , stdin ) ;

/* Aqui defino la cantidad de lugares despues de


la coma quiero imprimir cuando imprima
valores reales ( float y double ) donde esta el
7 es donde se define cantidad de lugares
despues de la coma en este caso esta puesto
para 7 lugares despues de la coma */
cout . setf ( ios :: fixed , ios :: floatfield ) ;
cout . precision (7) ;

/* Desincronizacion del cin , esto es para que el


cin tenga la misma velocidad del scanf de C ,
OJO si usa esto no puede combinar cin / cout
con scanf / printf o usa C todo el tiempo o usa

24
LENGUAJES

C ++ todo el tiempo */
ios_base :: sync_with_stdio (0) ;
std :: cin . tie (0) ;

/* Desarrollo de nuestra solucion */


return 0;
}

C++ independientemente de la versión que se use es el lenguaje más


utilizado en la codificación de soluciones a problemas, pero no significa que
sea el perfecto, el mismo presenta problemas en el trabajo de números grandes
y en determinados ejercicio donde la complejidad está en la captura de los
datos de entrada y las cadenas. Pero dichas deficiencias pueden ser suplidas
con el siguiente lenguaje de programación. A favor de C++ está su velocidad
y uso de memoria.

1.2. Java
El lenguaje de programación Java fue originalmente desarrollado por Ja-
mes Gosling de Sun Microsystems (la cual fue adquirida por la compañı́a
Oracle) y publicado en 1995 como un componente fundamental de la plata-
forma Java de Sun Microsystems. Su sintaxis deriva en gran medida de C y
C++, pero tiene menos utilidades de bajo nivel que cualquiera de ellos. Las
aplicaciones de Java son generalmente compiladas a bytecode (clase Java)
que puede ejecutarse en cualquier máquina virtual Java (JVM) sin importar
la arquitectura de la computadora subyacente.
Debido a su mecanismo de compilación la soluciones realizadas Java tiene
un consumo de tiempo y memoria mayor que las hechas en C++ con similar
algoritmo es por eso que en muchos jurados a las soluciones que se envı́a en
Java se le amplia las restricciones de tiempo y memoria tres veces con respec-
to a las restricciones hechas para soluciones con C++. Esto no significa que
una solución hecha con Java siempre sea su velocidad y consumo de memoria
tres veces superior a la misma implementación pero con C++. Esto puede
un arma que podemos usar a nuestro favor en determinadas situaciones. Si
tenemos una solución con cuyo resultado es tiempo lı́mite excedido (TLE)
porque su tiempo de ejecución fue superior por muy poco al lı́mite tiempo
impuesto para una solución en C++, bien podemos tratar de codificar la
misma solución de C++ con Java. Pueden pensar que volverá a dar como
resultado TLE y no se equivocan puede ser ese el resultado, ya lo sabiamos
por los intentos con C++. Pero como dicen que la matemática es una ciencia

25
LENGUAJES

exacta sin embargo 2+2 no siempre es 4 sobre todo si se trata de espacios


vectoriales, determinados TLE en C++ se pueden transformar en Aceptado
(AC) en Java. Con lo anterior dicho seguro muchos pensaran que lo mejor es
codificar siempre en Java. Siento tener que ser yo el que tenga que informarle
que existen determinados ejercicios que si lo intenta resolver con Java, como
dicen en beisbol “usted es out por regla ”. Unas de las soluciones aplicadas
a estos detalles por parte de los organizadores de competencia es que las res-
tricciones(tiempo, memoria y fuente) son las mismas sin importar el lenguaje
de programación utilizado.
Java es muy útil para ejecicios que requiren de trabajo con cadenas y
números ya sea enteros o reales superiores a los que permiten los tipos de
datos long long y double, en primer caso la clase String de Java te da mu-
cha utilidades para trabajar con una cadena, en determinado momento por
desconocimiento de elementos de C++ pensé que era superior, pero con el
estudio me dado cuenta que C++ tambien posee dichas utilidades pero con
diferencia de Java no estan todas centralizadas en una clase. Donde Java es
superior a C++ es en el trabajo con números cuya cantidad de digitos pueden
sobrepasar los 50. Las clases BigInteger y BigDouble han hecho posible que
determinados problemas sean reducidos a simples operaciones aritmética.

Estructura de una solución con Java


Similar a como se explicó con C++ lo haremos con Java. En este caso
vamos a dividir en dos secciones. La primera va estar destinada a incluir
todos aquellos paquetes o clases (La forma de agrupar u organizar en Java
es distinta que C++ ) que van hacer útil en nuestra solución. La forma de
incluirlos es utilizando la palabra reservada import, por ejemplo:

import java . io . BufferedReader ;


import java . math .*;

En la primera lı́nea estamos declarando que del paquete java en su subpa-


quete io utilizaremos la clase BufferedReader. Mientras en la segunda lı́nea
se declaró que del paquete java utilizaremos todas las clases presentes en el
subpaquete math.
La segunda sección va estar destinada a la solución. Diferencia de C++
donde contamos con un método main, en Java contaremos con una clase que
dentro de sus métodos esta el main el cual es estático o static1 . Dentro de
1
Cuando un método dentro de una clase es declarado static significa que no tengo
que crear un objeto de la clase para utilizarlo ( lo que mucho conocemos como “instan-
ciar”) sencillamente digo Nombre de clase::metodo(<argumentos>) si es C++ , si es Java

26
LENGUAJES

este método podemos perfectamente desarrollar nuestra solución. Tener en


cuenta que si necesitamos hacer métodos auxiliares estos también debe ser
static. Esta variante en lo personal solo la use en las primeras soluciones con
Java, luego me fui por la variante que muestro en la plantilla, la cual me da
una mejor organización del código, trabajar con variables de forma global.

Captura y salida de datos


La captura de los datos en Java hasta donde tengo conocimientos se puede
usar dos variantes (nunca he intentado combinar las dos y no he visto que
nadie lo haya hecho, ası́ que asumo dos cosas, no se puede porque el concepto
de captura es diferente entre ambas o no ha salido el kamikaze que lo intente)
La primera variante es utilizada la clase Scanner ubicada en el subpaquete
util del paquete de java. La clase Scanner según los que saben funciona
como un simple scanner de texto el cual puede “parsear”cadenas y tipos de
datos primitivos del lenguaje usando expresiones regulares. La clase Scanner
captura la entrada y la fraccionan en tokens2 tomando como delimitadores
ciertos patrones de texto(por defecto es el espacio en blanco). Para acceder
a los tokens solo basta invocar los métodos next de la clase. Tener en cuenta
que para cada tipo de dato la clase Scanner tiene un método next. En caso
que se necesite cambiar el patrón delimitador de los tokens la clase Scanner
da esa posibilidad.
La segunda variante es haciendo uso de la clase BufferedReader perte-
neciente al subpaquete io del paquete java. La clase BufferedReader es muy
eficiente en la captura de los datos(puede ser la diferencia entre un Aceptado
y un Tiempo Limite Excedido), para ello da tres variantes, la primera leer
carácter a carácter, la segunda es leer una porción de un arreglo de caracte-
res y la tercera leer toda una lı́nea ( similar a Console.ReadLine() de C# o
raw input() de Python).
En cuanto a la salida de igual manera se cuenta con dos variantes. La
primera usar directamente la que proporciona el lenguaje por defecto Sys-
tem.out la cual posee funcionalidades para imprimir. La segunda variante es
utilizar la clase PrintWriter perteneciente al subpaquete io del paquete java.
Una de las ventajas de esta variantes es la posibilidad de formatear la salida
de acuerdo a un estándar o reglas impuestas por el problema.

Nombre de clase.metodo(<argumentos>)
2
Un token o también llamado componente léxico es una cadena de caracteres que tiene
un significado coherente en cierto lenguaje de programación. Ejemplos de tokens podrı́an
ser palabras clave (if, else, while, int, ...), identificadores, números, signos, o un operador
de varios caracteres, (por ejemplo, :=).

27
LENGUAJES

Conformación de una plantilla base para la implemen-


tación de una solución en Java.

Debido a que para la captura de datos Java ofrece dos variantes aqui les
dejo dos posibles pantilla que pueden untilizar cada una con una variante
distintas de lectura de datos. La primera utiliza para la captura de datos la
clase Scanner.

import java . io . BufferedReader ;


import java . io . File ;
import java . io . FileReader ;
import java . io . FileWriter ;
import java . io . IOException ;
import java . io . Inp utStre amRea der ;
import java . io . PrintWriter ;
import java . math . BigInteger ;
import java . text . DecimalFormat ;
import java . util . ArrayList ;
import java . util . Arrays ;
import java . util . Collection ;
import java . util . Collections ;
import java . util . Comparator ;
import java . util . HashMap ;
import java . util . HashSet ;
import java . util . Iterator ;
import java . util . LinkedList ;
import java . util . List ;
import java . util . Queue ;
import java . util . Random ;
import java . util . Scanner ;
import java . util . Set ;
import java . util . Stack ;
import java . util . StringTokenizer ;
import java . util . TreeSet ;
import java . util . regex .*;
import java . util . Random ;

public class COJ


{

private Scanner in ;
private PrintWriter out ;
private double ERROR = 1e -15;

28
LENGUAJES

private int MAX_ELEMENT = 1000010;

public void solve () throws IOException


{
// Desarrollo de la solucion
}

public COJ () throws IOException {


/* Descomentar antes de enviar */
in = new Scanner ( new In putStr eamRea der (
System . in ) ) ;
/* Comentar antes de enviar , solo
utilizar cuando desea leer lo datos
desde un fichero y no introdurcirlo
manualmente */
// in = new Scanner ( new FileReader (" Input
. txt ") ) ;
out = new PrintWriter ( System . out ) ;
solve () ;
in . close () ;
out . close () ;
}

public static void main ( String [] args ) throws


IOException
{
new COJ () ;
}
}

La segunda variante con la clase BufferedReader

import java . io . BufferedReader ;


import java . io . IOException ;
import java . io . In putStr eamRea der ;
import java . io . PrintWriter ;
import java . math . BigInteger ;
import java . util . ArrayList ;
import java . util . Arrays ;
import java . util . BitSet ;
import java . util . List ;
import java . util . Set ;
import java . util . StringTokenizer ;

29
LENGUAJES

public class Main


{
static private int ELEMENT = 20100;
private BufferedReader in ;
private PrintWriter out ;
private StringTokenizer st ;

void solve () throws IOException


{
// aqui se desarrolla la solucion
}

private long min ( long l , long m , long n , long o )


{
return Math . min ( Math . min (l , m ) , Math . min
(n , o ) ) ;
}

Main () throws IOException


{
in = new BufferedReader ( new
Inp utStre amRead er ( System . in ) ) ;
out = new PrintWriter ( System . out ) ;
eat ( " " ) ;
solve () ;
in . close () ;
out . close () ;
}

private void eat ( String str )


{
st = new StringTokenizer ( str ) ;
}

String next () throws IOException


{
while (! st . hasMoreTokens () )
{
String line = in . readLine () ;
if ( line == null )
{
return null ;

30
LENGUAJES

}
eat ( line ) ;
}
return st . nextToken () ;
}

int nextInt () throws IOException


{
return Integer . parseInt ( next () ) ;
}

long nextLong () throws IOException


{
return Long . parseLong ( next () ) ;
}

double nextDouble () throws IOException


{
return Double . parseDouble ( next () ) ;
}

public static void main ( String [] args ) throws


IOException
{
new Main () ;
}

1.3. Tipo de dato


En ciencias de la computación un tipo de dato informático o simplemente
tipo es un atributo de los datos que indica al ordenador (y/o al programador)
sobre la clase de datos que se va a trabajar. Esto incluye imponer restricciones
en los datos, como qué valores pueden tomar y qué operaciones se pueden
realizar.
Los tipos de datos comunes son: números enteros, números con signo
(negativos), números de coma flotante (decimales), cadenas alfanuméricas (y
unicodes), estados, etc.
Un tipo de dato es, en esencia, un espacio en memoria con restricciones.
Por ejemplo, el tipo ı̈ntrepresenta, generalmente, un conjunto de enteros de

31
LENGUAJES

32 bits cuyo rango va desde el -2.147.483.648 al 2.147.483.647, ası́ como las


operaciones que se pueden realizar con los enteros, como son la suma, la
resta, y la multiplicación.
En varias ocasiones la incorrecta declaración de una variable de un de-
terminado tipo hace que nuestra solución este destinada al fracaso aunque
el análisis e implementación de la solución sea correcta. Esto último sucede
muchas veces sobre cuando no se tiene en cuenta los posibles valores extremos
que pueda tomar las variables utilizadas en la solución.
Por lo general los lenguajes de programación tienen similares tipos de
datos aunque si pueden variar los rangos de los mismos.
De forma de general los tipos de datos nativos de un lenguajes de pro-
gramación se puede agrupar de la siguiente manera:

Lógico: El tipo de dato lógico o booleano es en computación aquel que


puede representar valores de lógica binaria, esto es 2 valores, valores
que normalmente representan falso o verdadero.

Númerico: Un tipo de dato númerico en computación es un tipo de


dato que puede representar el conjunto infinito de los números. A sus
ves este se puede subdividir en datos númericos enteros, decimales y
complejos. El número mayor que puede representar depende del tamaño
del espacio usado por el dato y la posibilidad (o no) de representar
números negativos. Los tipos de dato entero disponibles y su tamaño
dependen del lenguaje de programación usado ası́ como la arquitectura
en cuestión.

Carácter: En terminologı́a informática y de telecomunicaciones, un


carácter es una unidad de información que corresponde aproximada-
mente con un grafema o con una unidad o sı́mbolo parecido, como los
de un alfabeto o silabario de la forma escrita de un lenguaje natural.

Cadena de caracteres: En programación, una cadena de caracteres,


palabras, ristra de caracteres o frase (string, en inglés) es una secuencia
ordenada (de longitud arbitraria, aunque finita) de elementos que per-
tenecen a un cierto lenguaje formal o alfabeto análogas a una fórmula o
a una oración. En general, una cadena de caracteres es una sucesión de
caracteres (letras, números u otros signos o sı́mbolos). Si no se ponen
restricciones al alfabeto, una cadena podrá estar formada por cualquier
combinación finita de los caracteres disponibles (las letras de la ’a’ a la
’z’ y de la ’A’ a la ’Z’, los números del ’0’ al ’9’, el espacio en blanco ’
’, sı́mbolos diversos ’ !’, ’@’, ’ %’, etcétera).

32
LENGUAJES

Abstracto: Un tipo de dato abstracto (TDA) o tipo abstracto de da-


tos (TAD) es un modelo matemático compuesto por una colección de
operaciones definidas sobre un conjunto de datos para el modelo.

Estructura: En programación, una estructura de datos es una forma


particular de organizar datos en una computadora para que pueda ser
utilizado de manera eficiente.

1.3.1. C++

Nombre Declaración Memoria Rango


Booleano bool - true - false
Descripción Define una bandera que puede tomar dos posibles valores:
true o false.
Nombre Declaración Memoria Rango
Entero short 16 Bits [-215 .. 215 -1]
Descripción Representación de un entero pequeño.
Nombre Declaración Memoria Rango
Entero int 32 Bits [-231 .. 231 -1]
Descripción Representación de un entero estándar.
Nombre Declaración Memoria Rango
Entero lar- long long 64 Bits [-263 .. 263 -1]
go
Descripción Representación de un entero de rango ampliado.
Nombre Declaración Memoria Rango
Real float 32 Bits [±1,18e − 38 .. ±3,40e38] Preci-
sión cientı́fica ( 7-dı́gitos)
Descripción Representación de un real estándar. Recordar que al ser
real, la precisión del dato contenido varı́a en función del
tamaño del número: la precisión se amplia con números más
próximos a 0 y disminuye cuanto más se aleja del mismo.
Nombre Declaración Memoria Rango
Real largo double 64 Bits [±2,23e − 308 .. ±1,79e308] Pre-
cisión cientı́fica (15-dı́gitos)
Descripción Representación de un real de mayor precisión. Double tiene
el mismo efecto con la precisión que float.
Nombre Declaración Memoria Rango
Real extra long dou- 80 Bits [±3,37e−4932 .. ±1,18e4932] Pre-
largo ble cisión cientı́fica (18-dı́gitos)

33
LENGUAJES

Descripción Representación de un real de mayor precisión. Double tiene


el mismo efecto con la precisión que float.
Nombre Declaración Memoria Rango
Carácter char 8 Bits [-128 ... 127]
Descripción Carácter o sı́mbolo
Nombre Declaración Memoria Rango
Carácter unsigned 8 Bits [0 ... 255]
char
Descripción Carácter o sı́mbolo
Nombre Declaración Memoria Rango
Carácter wchar t
Descripción Caracteres anchos, para determinado juegos de caracteres
Nombre Declaración Memoria Rango
Vacı́o void
Descripción Define en C++ el concepto de no existencia o no atribución
de un tipo en una variable o declaración.

En casos de los tipos de datos que soportan datos númericos de tipo


entero si se coloca delante del tipo dato el modificador unsigned indica que
la variable solo soportará enteros neutros y positivos desplazando el rango
de 0 a 216 -1 , 232 -1 o 264 -1 según sea el caso.

1.3.2. Java

Nombre Declaración Memoria Rango


Booleano boolean - true - false
Descripción Define una bandera que puede tomar dos posibles valores:
true o false.
Nombre Declaración Memoria Rango
Byte byte 1 byte [-128 .. 127]
Descripción Representación del número de menor rango con signo.
Nombre Declaración Memoria Rango
Entero pe- short 16 Bits [-32,768 .. 32,767]
queño
Descripción Representación de un entero cuyo rango es pequeño.
Nombre Declaración Memoria Rango
Entero int 32 Bits [-231 .. 231 -1]

34
LENGUAJES

Descripción Representación de un entero estándar. Este tipo puede re-


presentarse sin signo usando su clase Integer a partir de la
Java SE 8.
Nombre Declaración Memoria Rango
Entero lar- long 64 Bits [-263 .. 263 -1]
go
Descripción Representación de un entero de rango ampliado. Este tipo
puede representarse sin signo usando su clase Long a partir
de la Java SE 8.
Nombre Declaración Memoria Rango
Real float 32 Bits [±3,4 ∗ 10−38 .. ±3,4 ∗ 1038 ]
Descripción Representación de un real estándar. Recordar que al ser
real, la precisión del dato contenido varı́a en función del
tamaño del número: la precisión se amplia con números más
próximos a 0 y disminuye cuanto más se aleja del mismo.
Nombre Declaración Memoria Rango
Real largo double 64 Bits [±1,7 ∗ 10−308 .. ±1,7 ∗ 10308 ]
Descripción Representación de un real de mayor precisión. Double tiene
el mismo efecto con la precisión que float.
Nombre Declaración Memoria Rango
Carácter char 16 Bits [’u0000’ .. ’uffff’] o [0 .. 65.535]
Descripción Carácter o sı́mbolo. Para componer una cadena es preciso
usar la clase String, no se puede hacer como tipo primitivo.

1.4. Formato de entrada y salida


En muchas ocasiones la dificultad de un problema no viene en la com-
plejidad del algoritmo que lo solucione sino por el formato bien de capturar
los datos o de mostrar la solución. Es por eso que el siguiente apartado va
estar dedicado a explicar algunos aspectos importantes a la hora de captu-
rar y mostrar los datos en dependencia del lenguaje de programación que se
utilice.

1.4.1. Entrada
C++
Para capturar los datos en el lenguaje de programación C++ se puede
utilizar dos variantes. La primera es la que propone su lenguaje de programa-
ción antecesor C y la segunda es la que propone el lenguaje de programación

35
LENGUAJES

en sı́.
A diferencia de otros lenguajes, C no dispone de sentencias de entrada/-
salida. En su lugar se utilizan funciones contenidas en la librerı́a estándar y
que forman parte integrante del lenguaje.
Las funciones de entrada/salida (Input/Output) son un conjunto de fun-
ciones, incluidas con el compilador, que permiten a un programa recibir y
enviar datos al exterior. Para su utilización es necesario incluir, al comienzo
del programa, el archivo stdio.h en el que están definidos sus prototipos:

# include < stdio .h >

donde stdio proviene de standard-input-output.


La función scanf() es análoga en muchos aspectos a printf(), y se utiliza
para leer datos de la entrada estándar (que por defecto es el teclado). La
forma general de esta función es la siguiente:

int scanf ( " %x1 %x2 ... " , & arg1 , & arg2 , ...) ;

donde x1, x2, ... son los caracteres de conversión, mostrados en la tabla,
que representan los formatos con los que se espera encontrar los datos. La
función scanf() devuelve como valor de retorno el número de conversiones de
formato realizadas con éxito. La cadena de control de scanf() puede contener
caracteres además de formatos. Dichos caracteres se utilizan para tratar de
detectar la presencia de caracteres idénticos en la entrada por teclado. Si lo
que se desea es leer variables numéricas, esta posibilidad tiene escaso interés.
A veces hay que comenzar la cadena de control con un espacio en blanco
para que la conversión de formatos se realice correctamente.
En la función scanf() los argumentos que siguen a la cadena de control
deben ser pasados por referencia, ya que la función los lee y tiene que tras-
mitirlos al programa que la ha llamado. Para ello, dichos argumentos deben
estar constituidos por las direcciones de las variables en las que hay que depo-
sitar los datos, y no por las propias variables. Una excepción son las cadenas
de caracteres, cuyo nombre es ya de por sı́ una dirección (un puntero), y por
tanto no debe ir precedido por el operador (&) en la llamada.
Por ejemplo, para leer los valores de dos variables int y double y de una
cadena de caracteres, se utilizarı́an la sentencia:

int n ;
double distancia ;
char nombre [20];
scanf ( " %d %lf %s " , &n , & distancia , nombre ) ;

36
LENGUAJES

carácter caracteres leı́dos argumento


c cualquier carácter char
d, i entero decimal con signo int *
u entero decimal sin signo unsigned int
o entero octal unsigned int
x, X entero hexadecimal unsigned int
e, E, f, g, G número de punto flotante float
s cadena de caracteres sin ’ ’ char *
h, l para short, long y double
L modificador para long double

Cuadro 1.3: Caracteres de conversión para la función scanf().

en la que se establece una correspondencia entre n y %d, entre distancia


y %lf, y entre nombre y %s. Obsérvese que nombre no va precedido por el
operador (&). La lectura de cadenas de caracteres se detiene en cuanto se
encuentra un espacio en blanco, por lo que para leer una lı́nea completa con
varias palabras hay que utilizar otras técnicas diferentes.
En los formatos de la cadena de control de scanf() pueden introducirse
corchetes [...], que se utilizan como sigue. La sentencia,

scanf ( " %[AB \ n \ t ] " , s ) ; // se leen solo los caracteres


indicados

lee caracteres hasta que encuentra uno diferente de ( ’A’,’B’,’ ’,’\n’,’\t’


). En otras palabras, se leen sólo los caracteres que aparecen en el corchete.
Cuando se encuentra un carácter distinto de éstos se detiene la lectura y se
devuelve el control al programa que llamó a scanf(). Si los corchetes contienen
un carácter (∧), se leen todos los caracteres distintos de los caracteres que
se encuentran dentro de los corchetes a continuación del (∧). Por ejemplo, la
sentencia,

scanf ( " %[^\ n ] " , s ) ;

lee todos los caracteres que encuentra hasta que llega al carácter nueva
lı́nea ’
n’. Esta sentencia puede utilizarse por tanto para leer lı́neas completas, con
blancos incluidos. Recuérdese que con el formato %s la lectura se detiene al
llegar al primer delimitador (carácter blanco, tabulador o nueva lı́nea)

37
LENGUAJES

La macro3 getchar() permite leer un sólo carácter cada vez, en la entrada


. La macro getchar() recoge un carácter introducido por teclado y lo deja
disponible como valor de retorno. Por ejemplo:

c = getchar () ;

equivale a

scanf ( " %c " , & c ) ;

Como se ha dicho anteriormente, getchar() es una macro y no función,


aunque para casi todos los efectos se comportan como si fuera función.Esta
macro están definida en el fichero stdio.h, y su código es sustituido en el
programa por el preprocesador antes de la compilación. Por ejemplo, se puede
leer una lı́nea de texto completa utilizando getchar():

int i =0 , c ;
char name [100];
while (( c = getchar () ) != ’\ n ’) // se leen caracteres
hasta el ’\ n ’
name [ i ++] = c ; // se almacena el
caracter en Name []
name [ i ]= ’ \0 ’; // se adiciona el
caracter fin de cadena

C++ dispone de unas herramientas propias de entrada y salida de datos


basadas en clases y en la herencia que son fáciles de extender y modificar. Si
este tema no se ha visto anteriormente con más extensión, es porque conviene
haber visto la herencia para entenderlo correctamente.
Es necesario recordar aquı́ el concepto de stream o flujo, que se puede
definir como dispositivo que produce o consume información. Un flujo está
siempre ligado a un dispositivo fı́sico. Todos los flujos, independientemente
del dispositivo fı́sico al que se dirijan (disco, monitor...,) se comportan de
forma análoga.
Al ejecutarse un programa en C++ uno de los flujos que se abren au-
tomáticamente es:
cin: entrada estándar (teclado)
C++ dispone de dos jerarquı́as de clases para las operaciones de entra-
da/salida: una de bajo nivel, streambuf, que no se va a explicar porque sólo
3
Una macro representa una sustitución de texto que se realiza antes de la compila-
ción por medio del preprocesador. Para casi todos los efectos, estas macros pueden ser
consideradas como funciones.

38
LENGUAJES

es utilizada por programadores expertos, y otra de alto nivel, con las clases:
istream, ostream e iostream, que derivan de la clase ios. Estas clases dispo-
nen de variables y métodos para controlar los flujos de entrada y salida. En
nuestro caso utilizaremos la clase iostream de la siguiente manera.

# include < iostream >

Del mismo modo que puede sobrecargarse el operador de salida <<, pue-
de sobrecargarse el operador de entrada >> . En C++ , el operador >> se
denomina operador de extracción y la función que lo sobrecarga se denomina
extractor. La razón del uso de este término está en el hecho de que la intro-
ducción de información en un flujo elimina (esto es, extrae) los datos de ella.
La forma general de una función extractora es:

istream & operator > > ( istream & stream , class - name & obj )
{
// Instrucciones de para la lectura
return stream ;
}

Los extractores devuelven una referencia a istream, que es un flujo de


entrada. El primer parámetro debe ser una referencia a1 flujo de entrada. El
segundo parámetro es una referencia a1 objeto que recibe la entrada.
Por la misma razón por la que un insertor no puede ser un miembro, un
extractor tampoco puede serlo. A pesar de que dentro de un extractor puede
realizarse cualquier función, lo más aconsejable es limitar su actividad a la
de introducir información.
Hasta aquı́ las dos variantes más utilizadas para la captura de datos en
C++ para resolver problemas de concursos de programación. Cual escoger?.
Depende de la complejidad de la entrada. Al principio cuando comencé en
este ambiente utilizada la variante propia de lenguaje, hasta que me tropecé
con ejercicios que el veredicto de TLE cambiaba a ACC con solo cambiar
la estrategia de captura y salida de C++ a C. Luego después de un tiempo
leyendo y mostrando con C alguien me mostró las sentencias

ios_base :: sync_with_stdio (0) ;


std :: cin . tie (0) ;

que permiten la desincronización del cin, esto es para que el cin tenga la
misma velocidad del scanf de C, pero si usa esto no puede combinar cin/cout
con scanf/printf o usa C todo el tiempo o usa C++ todo el tiempo, nunca
un hibrido.

39
LENGUAJES

Java
En Java, la entrada desde teclado y la salida a pantalla están reguladas
a traves de la clase System. Esta clase pertenece al package java.lang y
agrupa diversos métodos y objetos que tienen relación con el sistema local.
Contiene, entre otros, tres objetos static que son:

System.in:Objeto de la clase InputStream preparado para recibir


datos desde la entrada estándar del sistema (habitualmente el teclado).

System.out: Objeto de la clase PrintStream que imprimirá los datos


en la salida estándar del sistema (normalmente asociado con la panta-
lla).

System.err: Objeto de la clase PrintStream. Utilizado para mensajes


de error que salen también por pantalla por defecto.

Estas clases permiten la comunicación alfanumérica con el programa a


través de lo métodos incluidos en la tabla. Son métodos que permiten la
entrada/salida a un nivel muy elemental.

Métodos de System.in Función que realizan


int read() Lee un carácter y lo devuelve como int.
Métodos de System.out Función que realizan
int print(cualquier tipo) Imprime en pantalla el argumento que se le pase.
Puede recibir cualquier tipo primitivo de variable de Java.
int println(cualquier tipo) Como el anterior, pero añadiendo salto de lı́nea al final.

Cuadro 1.4: Métodos elementales de lectura y escritura.

Para leer desde teclado se puede utilizar el método System.in.read()


de la clase InputStream. Este método lee un carácter por cada llamada. Su
valor de retorno es un int. Si se espera cualquier otro tipo hay que hacer una
conversión explı́cita mediante un cast.

char c ;
c =( char ) System . in . read () ;

Para leer datos más largos que un simple carácter es necesario emplear
un bucle while o for y unir los caracteres. Por ejemplo, para leer una lı́nea
completa se podrı́a utilizar un bucle while guardando los caracteres leı́dos en
un String o en un StringBuffer (más rápido que String):

40
LENGUAJES

char c ;
String frase = new String ( " " ) ; // StringBuffer frase = new
StringBuffer ("") ;
while (( c = System . in . read () ) != ’\ n ’)
frase = frase + c ; // frase . append ( c ) ;

Una vez que se lee una lı́nea, ésta puede contener números de coma flo-
tante, etc. Sin embargo, hay una manera más fácil de conseguir lo mismo:
utilizar adecuadamente la librerı́a java.io.
Para facilitar la lectura de teclado se puede conseguir que se lea una lı́nea
entera con una sola orden si se utiliza un objeto BufferedReader. El método
String readLine() perteneciente a BufferReader lee todos los caracteres hasta
encontrar un ’\n’ o ’\r’ y los devuelve como un String (sin incluir ’\n’ ni
’\r’). Este método también puede lanzar java.io.IOException.
Como se mencionó antes BufferedReader es una clase cuyas instancias nos
permiten hacer lecturas sencillas de texto desde un flujo de caracteres, debido
a que esta clase trabaja con su propio buffer provee una lectura eficiente de
caracteres, arreglos y lı́neas de texto. Otra ventaja de BufferedReader es que
esta clase está sincronizada lo cual es sinónimo de seguridad al utilizarla en
programación concurrente.
Primero que todo, esta clase se ubica en el paquete java.io por lo que en
los códigos donde se utilice BufferedReader es necesario poner los siguientes
import:

import java . io . BufferedReader ;


import java . io . IOException ;

En la siguiente tabla se encuentran algunos de los métodos del clase Buf-


feredReader.

Métodos Tipo de retorno Descripción


read() void Lee un solo carácter del flujo, lo curioso de este
método es que retorna un número entero
readLine() String Lee una lı́nea completa de texto.
ready() boolean Este método es utilizado para saber si aún hay
caracteres en el flujo para ser leı́dos, detalles
más adelante.
reset() void Reinicia el flujo hasta la marca más reciente que
se haya hecho.

Cuadro 1.5: Métodos de BufferedReader.

41
LENGUAJES

System.in es un objeto de la clase InputStream. BufferedReader pide un


Reader en el constructor. El puente de unión necesario lo dará InputStream-
Reader, que acepta un InputStream como argumento del constructor y es
una clase derivada de Reader. Por lo tanto si se desea leer una lı́nea comple-
ta desde la entrada estándar habrá que utilizar el siguiente código:

Inpu tStrea mReade r isr = new Inpu tStrea mRead er ( System . in )


;
BufferedReader br = new BufferedReader ( isr ) ;
// o en una linea :
// BufferedReader br2 = new BufferedReader ( new
Inpu tStrea mRead er ( System . in ) ) ;
String frase = br2 . readLine () ;
// Se lee la linea con una llamada

Ası́ ya se ha leido una lı́nea del teclado. El thread que ejecute este códi-
go estará parado en esta lı́nea hasta que el usuario termine la lı́nea (pulse
return). Es más sencillo y práctico que la posibilidad anterior.
¿Y qué hacer con una lı́nea entera? La clase java.util.StringTokenizer da
la posibilidad de separar una cadena de carácteres en las “palabras” (tokens)
que la forman (por defecto, conjuntos de caracteres separados por un espacio,
’\t’, ’\r’, o por ’\n’). Cuando sea preciso se pueden convertir las “palabras”
en números.
La siguiente tabla muestra los métodos más prácticos de la clase String-
Tokenizer.

Métodos Función que realizan


StringTokenizer(String) Constructor a partir de la cadena que hay que separar
boolean hasMoreTokens() ¿Hay más palabras disponibles en la cadena?
String nextToken() Devuelve el siguiente token de la cadena
int countTokens() Devuelve el número de tokens que se pueden extraer
de la frase

Cuadro 1.6: Métodos de StringTokenizer.

La clase StreamTokenizer de java.io aporta posibilidades más avanzadas


que StringTokenizer, pero también es más compleja. Directamente separa en
tokens lo que entra por un InputStream o Reader.
Se recuerda que la manera de convertir un String del tipo “3.141592654”
en el valor double correspondiente es crear un objeto Double a partir de él y
luego extraer su valor double:

42
LENGUAJES

double pi = ( Double . valueOf ( " 3.141592654 " ) ) . doubleValue


() ;

El uso de estas clases facilita el acceso desde teclado, resultando un código


más fácil de escribir y de leer. Además tiene la ventaja de que se puede
generalizar a la lectura de archivos.
Otra variante para la lectura de datos en Java es utilizando la clase Scan-
ner. La clase Scanner está disponible a partir de Java 5 y facilita la lectura
de datos en los programas Java. Para utilizar Scanner en el programa tendre-
mos que hacer lo siguiente. Como la clase Scanner se encuentra en el paquete
java.util por lo tanto se debe incluir al inicio del programa la instrucción:

import java . util . Scanner ;

Tenemos que crear un objeto de la clase Scanner asociado al dispositivo


de entrada. Si el dispositivo de entrada es el teclado escribiremos:

Scanner sc = new Scanner ( System . in ) ;

Se ha creado el objeto sc asociado al teclado representado por System.in


. Una vez hecho esto podemos leer datos por teclado. Para leer podemos usar
el método nextXxx() donde Xxx indica en tipo, por ejemplo nextInt() para
leer un entero, nextDouble() para leer un double, etc.

int n ;
System . out . print ( " Introduzca un numero entero : " ) ;
n = sc . nextInt () ;

De forma resumida podemos decir que cuando se introducen caracteres


por teclado, el objeto Scanner toma toda la cadena introducida y la divi-
de en elementos llamados tokens. El carácter predeterminado que sirve de
separador de tokens es el espacio en blanco.
A continuación, utilizando los métodos que proporciona la clase Scanner
se puede acceder a esos tokens y trabajar con ellos en el programa. Ya he-
mos visto el método nextXxx(). Además la clase Scanner proporciona otros
métodos, algunos de los métodos más usados son:

Método Descripción
nextXxx() Devuelve el siguiente token como un tipo básico. Xxx es el tipo.
Por ejemplo, nextInt() para leer un entero, nextDouble para leer
un double, etc.
next() Devuelve el siguiente token como un String.

43
LENGUAJES

nextLine() Devuelve la lı́nea entera como un String. Elimina el final


\n del buffer
hasNext() Devuelve un boolean. Indica si existe o no un siguiente
token para leer.
hasNextXxx() Devuelve un boolean. Indica si existe o no un siguiente token
del tipo especificado en Xxx, por ejemplo hasNextDouble()
useDelimiter(String) Establece un nuevo delimitador de token.
Cuadro 1.7: Métodos de la clase Scanner

1.4.2. Salida
C++
La función printf() imprime en la unidad de salida (el monitor, por
defecto), el texto, y las constantes y variables que se indiquen. La forma
general de esta función se puede estudiar viendo su prototipo:

int printf ( " c adena_ de_con trol " , tipo arg1 , tipo arg2 ,
...)

La función printf() imprime el texto contenido en cadena de control


junto con el valor de los otros argumentos, de acuerdo con los formatos inclui-
dos en cadena de control Los puntos suspensivos (...) indican que puede
haber un número variable de argumentos. Cada formato comienza con el
carácter ( %) y termina con un carácter de conversión. Considérese el ejem-
plo siguiente:

int i ;
double tiempo ;
float masa ;
printf ( " Resultado no : %d . En el instante %lf la masa
vale %f \ n " ,i , tiempo , masa ) ;

en el que se imprimen 3 variables (i, tiempo y masa) con los formatos


( %d, %lf y %f), correspondientes a los tipos (int, double y float), respec-
tivamente. La cadena de control se imprime con el valor de cada variable
intercalado en el lugar del formato correspondiente.
Lo importante es considerar que debe haber correspondencia uno a uno
(el 1o con el 1o , el 2o con el 2o , etc.) entre los formatos que aparecen en la
cadena de control y los otros argumentos (constantes, variables o expre-
siones). Entre el carácter % y el carácter de conversión puede haber, por el

44
LENGUAJES

carácter Tipo de argumento carácter Tipo de argumento


d,i int decimal o octal unsigned
u int unsigned x,X hexadecimal unsigned
c char s cadena de chars
f float notación decimal e, g float not. cientı́f. o breve
p puntero (void *)

Cuadro 1.8: Caracteres de conversión para la función printf().

siguiente orden, uno o varios de los elementos que a continuación se indican:


Un número entero positivo, que indica la anchura mı́nima del campo
en caracteres.
Un signo (-), que indica alineamiento por la izda (el defecto es por la
derecha).
Un punto (.), que separa la anchura de la precisión.
Un número entero positivo, la precisión, que es el no máximo de carac-
teres a imprimir en un string, el no de decimales de un float o double,
o las cifras mı́nimas de un int o long.
Un cualificador: una (h) para short o una (l) para long y double
A continuación se incluyen algunos ejemplos de uso de la función printf().
El primer ejemplo contiene sólo texto, por lo que basta con considerar la
cadena de control.

printf ( " Con cien fusiles por banda ,\ nviento en popa a


toda vela ,\ n " ) ;

El resultado serán dos lı́neas con las dos primeras estrofas de la famosa
poesı́a. No es posible partir cadena de control en varias lı́neas con caracte-
res intro, por lo que en este ejemplo podrı́a haber problemas para añadir más
estrofas. Una forma alternativa, muy sencilla, clara y ordenada, de escribir
la poesı́a serı́a la siguiente:

printf ( " %s \ n %s \ n %s \ n %s \ n " ,


" Con cien fulises por banda , " ,
" viento en popa a toda vela , " ,
" no cruza el mar sino vuela , " ,
" un velero bergantin . " ) ;

45
LENGUAJES

En este caso se están escribiendo 4 cadenas constantes de caracteres que


se introducen como argumentos, con formato %s y con los correspondientes
saltos de lı́nea. Un ejemplo que contiene una constante y una variable como
argumentos es el siguiente:

printf ( " En el dia %s gano %ld ptas .\ n " , " 24 " , beneficios
);

donde el texto 24 se imprime como cadena de caracteres ( %s), mientras


que beneficios se imprime con formato de variable long ( %ld). Es importante
hacer corresponder bien los formatos con el tipo de los argumentos, pues si
no los resultados pueden ser muy diferentes de lo esperado.
La función printf() tiene un valor de retorno de tipo int, que representa
el número de caracteres escritos en esa llamada.
Al ejecutarse un programa en C++ uno de los flujos que se abren auto-
maticomente es:
cout: salida estándar (pantalla)
Cada flujo de C++ tiene asociados unos indicadores, que son unas va-
riables miembro enum de tipo long que controlan el formato al activarse o
desactivarse alguno de sus bits. Su valor hexadecimal es:

enum {
skipws =0 x0001 , left =0 x0002 , rigth =0 x0004 ,
internal =0 x0008 ,
dec =0 x0010 , oct =0 x0020 , hex =0 x0040 ,
showbase =0 x0080 ,
showpoint = 0 x0100 , seupperca =0 x0200 , showpos =0 x0400 ,
scientific =0 x800 ,
fixed =0 x1000 , unitbuf =0 x2000
};

Su significado es el siguiente:

skipws: se descartan los blancos iniciales a la entrada.

left: la salida se alinea a la izquierda

rigth: la salida se alinea a la derecha

internal: se alinea el signo y los caracteres indicativos de la base por


la izquierda y las cifras por la derecha.

dec: salida decimal para enteros (defecto)

46
LENGUAJES

oct: salida octal para enteros

hex: salida hexadecimal al para enteros

showbase: se muestra la base de los valores numéricos

showpoint: se muestra el punto decimal

uppercase: los caracteres de formato aparecen en mayúsculas

showpos: se muestra el signo (+) en los valores positivos

scientific: notación cientı́fica para coma flotante

fixed: notación normal para coma flotante

unitbuf: salida sin buffer (se vuelca cada operación)

stdio: permite compatibilizar entrada/salida al modo de C con ¡st-


dio.h¿y al modo de C++ con ¡iostream.h¿

La forma de definir las constantes anteriores permite componerlas fácil-


mente, guardando toda la información sobre ellas en una única variable long.
Existen unos indicadores adicionales (adjustfield, basefield y floatfield) que
actúan como combinaciones de los anteriores (máscaras):

adjustfield: es una combinación excluyente (sólo una en on) de left,


rigth e internal

basefield: es una combinación excluyente (sólo una en on) de dec, oct


e hex

floatfield: es una combinación excluyente (sólo una en on) de scientific


y fixed

Por defecto todos los indicadores anteriores están desactivados, excepto


skipws y dec.
Para la activación de indicadores se pueden utilizar los métodos setf()
y flags() de la clase ios. Se comenzará viendo la primera de ellas. Los dos
prototipos del método setf() son:

long setf ( long indic ) ;


long setf ( long indic , long mask ) ;

47
LENGUAJES

El valor de retorno de esta función es la configuración anterior (interesa


disponer de ella al hacer algún cambio para poder volver a dicha configura-
ción si se desea), e indic es el long que contiene los indicadores. En el segundo
prototipo mask es uno de los tres indicadores combinación de otros (adjust-
field, basefield y floatfield). Se permite activar varios indicadores a la vez con
el operador lógico OR binario. Ejemplo:

cout . setf ( ios :: showpoint | ios :: fixed ) ;

Es necesario determinar el flujo afectado (cout) y la clase en la que están


definidos los indicadores (ios). Para desactivar los indicadores se utiliza la
función unsetf() de modo similar a setf().
El segundo prototipo se debe utilizar para cambiar los indicadores que
son exclusivos, como por ejemplo:

cout . setf ( ios :: left , ios :: adjustfield ) ;

que lo que hace es desactivar los tres bits que tienen que ver con la
alineación y después activar el bit de alineación por la izquierda. En la forma,

cout . setf (0 , ios :: adjustfield ) ;

pone los tres bits de alineación a cero.


La función flags() sin argumentos devuelve un long con la configuración
de todos los indicadores. Su prototipo es:

long flags () ;

Existe otra definición de la función flags() cuyo valor de retorno es un


long con la configuración anterior, que permite cambiar todos los indicadores
a la vez, pasando un long con la nueva configuración como argumento:

long flags ( long indic ) ;

donde indic contiene una descripción completa de la nueva configuración.


El inconveniente de la función flags() es que establece una nueva configura-
ción partiendo de cero, mientras que setf() simplemente modifica la configu-
ración anterior manteniendo todo lo que no se ha cambiado explı́citamente,
por lo que debe ser considerada como una opción más segura.Se presenta a
continuación un ejemplo con todo lo citado hasta ahora:

// se mostrara el signo + para numeros positivos


cout . setf ( ios :: showpos ) ;

48
LENGUAJES

// se mostrara el punto y no se utilizara notacion


cientifica
cout . setf ( ios :: showpoint | ios :: fixed ) ;
cout << 100.0;

que hace que se escriba por pantalla:

+100.000000

Las funciones miembro width(), precision() y fill() están declaradas en


ios y definidas en las clases istream, ostream e iostream. La función miembro
width() establece la anchura de campo mı́nima para un dato de salida. Sus
prototipos son:

int width ( int n ) ;


int width () ;

donde el valor de retorno es la anchura anterior.


La anchura establecida con la función width() es la mı́nima y siempre que
sea necesario el sistema la aumenta de modo automático. Esta anchura de
campo sólo es válida para el siguiente dato que se imprime. Si se desea que
siga siendo válida hay que llamarla cada vez.
La función miembro precision() establece el número de cifras para un
dato de salida. Si no se indica nada la precisión por defecto es 6 dı́gitos. Los
prototipos de la función precision() son:

int precision ( int n ) ;


int precision () ;

donde el valor de retorno es la precisión anterior.


La función miembro fill() establece el carácter de relleno para un dato de
salida. Por defecto el carácter de relleno es el blanco ’ ’. Los prototipos de
esta función son:

char fill ( char ch ) ;


char fill () ;

donde el valor de retorno es el carácter de relleno anterior.


Los operadores de inserción (<<) del de salida se pueden sobrecargar
como otros operadores de C++. Su prototipo es el siguiente:

ostream & operator < < ( ostream & co , const obj_type & a ) ;

49
LENGUAJES

Este flujo funciona como cinta transportadora que sale (<<) del progra-
ma. Se recibe una referencia al flujo como primer argumento, se añade o se
retira de él la variable que se desee, y se devuelve como valor de retorno una
referencia al flujo modificado. El valor de retorno es siempre una referencia
al stream de entrada/salida correspondiente. A continuación se presenta un
ejemplo de sobrecarga del operador (<<):

ostream & operator < <( ostream & co , const matriz & A )
{
for ( int i =0; i <= nfilas ; i ++)
for ( int j =0; j <= ncol ; j ++)
co << A . c [ i ][ j ] << ’\ t ’;
co << ’\ n ’;
return ( co ) ;
}

Estos operadores se sobrecargan siempre como operadores friend de la


clase en cuestión, ya que el primer argumento no puede ser un objeto de una
clase arbitraria.

Java
Para imprimir en la pantalla se utilizan los métodos System.out.print()
y System.out.println(). Son los primeros métodos que aprende cualquier
programador. Sus caracterı́sticas fundamentales son:
1. Pueden imprimir valores escritos directamente en el código o cualquier
tipo de variable primitiva de Java.

System . out . println ( " Hola , Mundo ! " ) ;


System . out . println (57) ;
double numeroPI = 3.141592654;
System . out . println ( numeroPI ) ;
String hola = new String ( " Hola " ) ;
System . out . println ( hola ) ;

2. Se pueden imprimir varias variables en una llamada al método corres-


pondiente utilizando el operador + de concatenación, que equivale a
convertir a String todas las variables que no lo sean y concatenar las
cadenas de caracteres (el primer argumento debe ser un String).

System . out . println ( " Hola , Mundo ! " +


numeroPI ) ;

50
LENGUAJES

Se debe recordar que el objeto System.out es de la clase PrintStream


y aunque imprime las variables de un modo legible, no permiten dar a la
salida un formato a medida. El programador no puede especificar un formato
distinto al disponible por defecto.

Con printf podemos lograr un formato preciso en la salida. El método


printf cuenta con las siguientes herramientas de formato, cada una de las
cuales se verá en este apartado:

Representación de números de punto flotante en formato ex-


ponencial. Un valor de punto fl otante contiene un punto decimal,
como en 33.5 , 0.0 o -657.983 . Los valores de punto fl otante se mues-
tran en uno de varios formatos.En la tabla se describen las conversiones
de punto flotante. Los caracteres de conversión e y E muestran valo-
res de punto flotante en notación cientı́fica computarizada (también
conocida como notación exponencial). La notación exponencial es el
equivalente computacional de la notación cientı́fica que se utiliza en las
matemáticas. Por ejemplo, el valor 150.4582 se representa en notación
cientı́fica matemática de la siguiente manera:

1.504582 x 102

y se representa en notación exponencial como

1.504582e+02

en Java. Esta notación indica que 1.504582 se multiplica por 10 elevado


a la segunda potencia ( e+02 ). La e representa al exponente.

3
Java SE 5 tomó prestada esta caracterı́stica del lenguaje de programación C

51
LENGUAJES

Carácter de Descripción
conversión
eoE Muestra un valor de punto flotante en notación ex-
ponencial. Cuando se utiliza el carácter de conver-
sión E , la salida se muestra en letras mayúsculas.
f Muestra un valor de punto flotante en formato de-
cimal.
goG Muestra un valor de punto flotante en el formato
de punto flotante f o en el formato exponencial e ,
con base en la magnitud del valor. Si la magnitud
es menor que 10−3 ,o si es mayor o igual que 107 , el
valor de punto flotante se imprime con e (o E ). En
cualquier otro caso, el valor se imprime en el for-
mato f. Cuando se utiliza el carácter de conversión
G , la salida se muestra en letras mayúsculas.
aoA Muestra un número de punto flotante en formato
hexadecimal. Cuando se usa el carácter de conver-
sión A , la salida se muestra en letras mayúsculas.
Los valores que se imprimen con los caracteres de conversión e , E y
f se muestran con seis dı́gitos de precisión en el lado derecho del pun-
to decimal de manera predeterminada (por ejemplo, 1.045921 ); otras
precisiones se deben especificar de manera explı́cita. Para los valores
impresos con el carácter de conversión g , la precisión representa el
número total de dı́gitos mostrados, excluyendo el exponente. El valor
predeterminado es de seis dı́gitos (por ejemplo, 12345678.9 se muestra
como 1.23457e+07 ). El carácter de conversión f siempre imprime por
lo menos un dı́gito a la izquierda del punto decimal. Los caracteres de
conversión e y E imprimen una e minúscula y una E mayúscula an-
tes del exponente, y siempre imprimen sólo un dı́gito a la izquierda
del punto decimal. El redondeo ocurre si el valor al que se está dando
formato tiene más dı́gitos significativos que la precisió
El carácter de conversión g (o G ) imprime en formato e ( E ) o f , depen-
diendo del valor de punto flotante. Por ejemplo, los valores 0.0000875
, 87500000.0 , 8.75 , 87.50 y 875.0 se imprimen como 8.750000e-05 ,
8.750000e+07 , 8.750000 , 87.500000 y 875.000000 con el carácter de
conversión g . El valor 0.0000875 utiliza la notación e ya que la mag-
nitud es menor que 10−3 . El valor 87500000.0 utiliza la notación e ,
debido a que la magnitud es mayor que 107 .

System . out . printf ( " %e \ n " , 12345678.9 ) ; // 1.234568

52
LENGUAJES

e +07
System . out . printf ( " %e \ n " , +12345678.9 ) ; //
1.234568 e +07
System . out . printf ( " %e \ n " , -12345678.9 ) ; //
-1.234568 e +07
System . out . printf ( " %E \ n " , 12345678.9 ) ; // 1.234568
E +07
System . out . printf ( " %f \ n " , 12345678.9 ) ; //
12345678.900000
System . out . printf ( " %g \ n " , 12345678.9 ) ; // 1.23457 e
+07
System . out . printf ( " %G \ n " , 12345678.9 ) ; // 1.23457 E
+07

Representación de enteros en formato octal y hexadecimal. Un


entero es un número completo, como 776 , 0 o -52 , que no contiene pun-
to decimal. Los valores enteros se muestran en uno de varios formatos.
En la tabla se describen los caracteres de conversión integrales.
Carácter de Descripción
conversión
d Muestra un entero decimal (base 10).
o Muestra un entero octal (base 8).
xoX Muestra un entero hexadecimal (base 16). X hace
que se muestren los dı́gitos del 0 al 9 y la letras A
a la F , y x hace que se muestren los dı́gitos del 0
al 9 y las letras de la a a la f.
System . out . printf ( " %d \ n " , 26 ) ; // 26
System . out . printf ( " %d \ n " , +26 ) ; // 26
System . out . printf ( " %d \ n " , -26 ) ; // -26
System . out . printf ( " %o \ n " , 26 ) ; // 32
System . out . printf ( " %x \ n " , 26 ) ; // 1 a
System . out . printf ( " %X \ n " , 26 ) ; // 1 A

Impresión de cadenas y caracteres. Los caracteres de conversión c


y s se utilizan para imprimir caracteres individuales y cadenas, respec-
tivamente. El carácter de conversión s también puede imprimir objetos
con los resultados de las llamadas implı́citas al método toString . Los
caracteres de conversión c y C requieren un argumento char . Los ca-
racteres de conversión s y S pueden recibir un objeto String o cualquier
objeto Object (se incluyen todas las subclases de Object ) como ar-
gumento. Cuando se pasa un objeto al carácter de conversión s , el

53
LENGUAJES

programa utiliza de manera implı́cita el método toString del objeto pa-


ra obtener la representación String del objeto. Cuando se utilizan los
caracteres de conversión C y S , la salida se muestra en letras mayúscu-
las.

char caracter = ’A ’; // inicializa el char


String cadena = " Esta tambien es una cadena " ; //
objeto String
Integer entero = 1234; // inicializa el entero (
autoboxing )
System . out . printf ( " %c \ n " , caracter ) ;
System . out . printf ( " %s \ n " , " Esta es una cadena " ) ;
System . out . printf ( " %s \ n " , cadena ) ;
System . out . printf ( " %S \ n " , cadena ) ;
System . out . printf ( " %s \ n " , entero ) ; // llamada
implicita a toString

Otros caracteres de conversión El resto de los caracteres de con-


versión son b , B , h , H , % y n . Éstos se describen en la siguiente
tabla. En las lı́neas 9 y 10 de la fi gura 29.11 se utiliza %b para im-
primir el valor de los valores booleanos false y true . En la lı́nea 11 se
asocia un objeto String a %b , el cual devuelve true debido a que no
es null . En la lı́nea 12 se asocia un objeto null a %B , el cual muestra
FALSE ya que prueba es null . En las lı́neas 13 y 14 se utiliza %h para
imprimir las representaciones de cadena de los valores de código hash
para las cadenas ”hola ”Hola”. Estos valores se podrı́an utilizar para
2

almacenar o colocar las cadenas en un objeto Hashtable o HashMap


(los cuales vimos en el capı́tulo 19, Colecciones). Observe que los valo-
res de código hash para estas dos ca denas difi eren, ya que una cadena
empieza con letra minúscula y la otra con letra mayúscula. En la lı́nea
15 se utiliza %H para imprimir null en letras mayúsculas. Las últimas
dos instrucciones printf (lı́neas 16 y 17) utilizan % % para imprimir el
carácter % en una cadena, y %n para imprimir un separador de lı́nea
especı́fico de la plataforma.

54
LENGUAJES

Carácter de Descripción
conversión
boB Imprime true o false para el valor de un boolean
o Boolean . Estos caracteres de conversión tam-
bién pueden aplicar formato al valor de cualquier
referencia. Si la referencia no es null, se imprime
true ; en caso contrario, se imprime false . Cuando
se utiliza el carácter de conversión B , la salida se
muestra en letras mayúsculas.
hoH Imprime la representación de cadena del valor de
código hash de un objeto en formato hexadecimal.
Si el correspondiente argumento es null , se impri-
me null . Cuando se utiliza el carácter de conver-
sión H , la salida se muestra en letras mayúsculas.
% Imprime el carácter de por ciento.
n Imprime el separador de lı́nea especı́fico de la pla-
taforma (por ejemplo, \r\n en Windows o \n en
UNIX/LINUX).
// Ot rasCon versio nes . java
// Uso de los caracteres de conversion b , B , h , H , %
y n.
public class Ot rasCon versio nes
{
public static void main ( String args [] )
{
Object prueba = null ;
System . out . printf ( " %b \ n " , false ) ;
System . out . printf ( " %b \ n " , true ) ;
System . out . printf ( " %b \ n " , " Prueba " ) ;
System . out . printf ( " %B \ n " , prueba ) ;
System . out . printf ( " El codigo hash de \" hola \" es %h
\ n " , " hola " ) ;
System . out . printf ( " El codigo hash de \" Hola \" es %h
\ n " , " Hola " ) ;
System . out . printf ( " El codigo hash de null es %H \ n " ,
prueba ) ;
System . out . printf ( " Impresion de un % % en una cadena
de formato \ n " ) ;
System . out . printf ( " Impresion de una nueva linea %
nla siguiente linea empieza
aqui " ) ;
} // fin de main

55
LENGUAJES

} // fin de la clase Otras Conver siones

Impresión con anchuras de campo y precisiones El tamaño exac-


to de un campo en el que se imprimen datos se especifica mediante una
anchura de campo. Si la anchura de campo es mayor que los datos
que se van a imprimir, éstos se justificarán a la derecha dentro de ese
campo, de manera predeterminada. Demostraremos la justificación a la
izquierda). El programador inserta un entero que representa la anchura
de campo entre el signo de porcentaje ( % ) y el carácter de conversión
(por ejemplo, %4d ) en el especificador de formato. En el ejemplo se
imprimen dos grupos de cinco números cada uno, y se justifican a la
derecha los números que contienen menos dı́gitos que la anchura de
campo. Observe que la anchura de campo se incrementa para imprimir
valores más anchos que el campo, y que el signo menos para un valor
negativo utiliza una posición de carácter en el campo. Además, si no se
especifica la anchura del campo, los datos se imprimen en todas las po-
siciones que sean necesarias. Las anchuras de campo pueden utilizarse
con todos los especificadores de formato, excepto el separador de lı́nea
( %n ).

// Pru eb aA nc hu raCa mp o . java


// Justificacion a la derecha de enteros en campos .
public class P ru eb aA nc hu ra Ca mp o
{
public static void main ( String args [] )
{
System . out . printf ( " %4d \ n " , 1 ) ;
System . out . printf ( " %4d \ n " , 12 ) ;
System . out . printf ( " %4d \ n " , 123 ) ;
System . out . printf ( " %4d \ n " , 1234 ) ;
System . out . printf ( " %4d \ n \ n " , 12345 ) ; // datos
demasiado extensos
System . out . printf ( " %4d \ n " , -1 ) ;
System . out . printf ( " %4d \ n " , -12 ) ;
System . out . printf ( " %4d \ n " , -123 ) ;
System . out . printf ( " %4d \ n " , -1234 ) ; // datos
demasiado extensos
System . out . printf ( " %4d \ n " , -12345 ) ; // datos
demasiado extensos
} // fin de main
} // fin de la clase P rue ba An ch ur aC am po

56
LENGUAJES

Cuando se utiliza con el carácter de conversión g , la precisión es el


número máximo de dı́gitos significativos a imprimir. Cuando se utiliza
con el carácter de conversión s , la precisión es el número máximo de
caracteres a escribir de la cadena. Para utilizar la precisión, se debe
colocar entre el signo de porcentaje y el especifi cador de conversión un
punto decimal ( . ), seguido de un entero que representa la precisión.
Se muestra el uso de la precisión en las cadenas de formato. Observe
que, cuando se imprime un valor de punto flotante con una precisión
menor que el número original de posiciones decimales en el valor, éste
se redondea. Además, obser- ve que el especifi cador de formato %.3g
indica que el número total de dı́gitos utilizados para mostrar el valor
de punto flotante es 3. Como el valor tiene tres dı́gitos a la izquierda
del punto decimal, se redondea a la posición de las unidades.

La anchura de campo y la precisión pueden combinarse, para lo cual se


coloca la anchura de campo, seguida de un punto decimal, seguido de
una precisión entre el signo de porcentaje y el carácter de conversión,
como en la siguiente instrucción:

printf ( " %9.3 f " , 123.456789 ) ;

la cual muestra 123.457 con tres dı́gitos a la derecha del punto decimal,
y se justifi ca a la derecha en un campo de nueve dı́gitos; antes del
número se colocarán dos espacios en blanco en su campo.

Uso de banderas en la cadena de formato de printf Pueden


usarse varias banderas con el método printf para suplementar sus he-
rramientas de formato de salida. Hay siete banderas disponibles para
usarlas en las cadenas de formato

57
LENGUAJES

Bandera Decripción
- (signo nega- Justifica a la izquierda la salida dentro del campo
tivo) especificado.
+ (signo posi- Muestra un signo positivo antes de los valores po-
tivo) sitivos, y un signo negativo antes de los valores
negativos.
espacio Imprime un espacio antes de un valor positivo que
no se imprime con la bandera + .
] Antepone un 0 al valor de salida cuando se utiliza
con el carácter de conversión octal o .Antepone 0x
al valor de salida cuando se usa con el carácter de
conversión hexadecimal x .
0 (cero) Rellena un campo con ceros a la izquierda.
, (coma) Usa el separador de miles especı́fico para la confi-
guración regional (es decir, ’ , ’ para los EUA), para
mostrar números decimales y de punto flotante.
( Encierra los números negativos entre paréntesis.

Para usar una bandera en una cadena de formato, coloque la bande-


ra justo a la derecha del signo de porcen- taje. Pueden usarse varias
banderas en el mismo especifi cador de formato.

Impresión de literales y secuencias de escape La mayorı́a de


los caracteres literales que se imprimen en una instrucción printf sólo
necesitan incluirse en la cadena de formato. Sin embargo, hay varios
caracteres “problemáticos”, como el signo de comillas dobles (”) que
delimita a la cadena de formato en sı́. Varios caracteres de control,
como el carácter de nueva lı́nea y el de tabu- lación, deben representarse
mediante secuencias de escape. Una secuencia de escape se representa
mediante una barra diagonal inversa ( \ ), seguida de un carácter de
escape.

58
LENGUAJES

Secuencias de escape Decripción


\’ (comilla sencilla) Imprime el carácter de comilla
sencilla ( ’ ).
\”(doble comilla) Imprime el carácter de doble co-
milla ( ”).
\\ (barra diagonal inversa) Imprime el carácter de barra dia-
gonal inversa ( \ ).
\b (retroceso) Desplaza el cursor una posición
hacia atrás en la lı́nea actual.
\f (nueva página o avance de Desplaza el cursor al principio de
página) la siguiente página lógica.
\n (nueva lı́nea) Desplaza el cursor al principio de
la siguiente lı́nea.
\r (retorno de carro) Desplaza el cursor al principio de
la lı́nea actual.
\t (tabulador horizontal) Desplaza el cursor hacia la si-
guiente posición del tabulador ho-
rizontal.

1.5. Operadores
Tanto C++ como Java están llenos de operadores que junto con las varia-
bles conforman las expresiones o sentencias que a su vez integran un programa
o algoritmo a continuación mencionaremos algunos de los más útiles.

1.5.1. Operadores Aritméticos


+ Suma los valores situados a su derecha y a su izquierda.
- Resta el valor de su derecha del valor de su izquierda.
- Como operador unario, cambia el signo del valor de su izquier-
da.
* Multiplica el valor de su derecha por el valor de su izquierda.
/ Divide el valor situado a su izquierda por el valor situado a
su derecha.
% Proporciona el resto de la división del valor de la izquierda
por el valor de la derecha (sólo enteros).
++ Suma 1 al valor de la variable situada a su izquierda (modo
prefijo) o de la variable situada a su derecha (modo sufijo).
– Igual que ++, pero restando 1.

59
LENGUAJES

1.5.2. Operadores de Asignación

= Asigna el valor de su derecha a la variable de su izquierda.


Cada uno de los siguientes operadores actualiza la variable de
su izquierda con el valor de su derecha utilizando la operación
indicada. Usaremos de d e i para izquierda.
+= Suma la cantidad d a la variable i.
-= Resta la cantidad d de la variable i.
*= Multiplica la variable i por la variable d.
/= Divide la variable i entre la cantidad d.
%= Proporciona el resto de la división de la variable i por la can-
tidad d

1.5.3. Operadores de Relación

< Menor que.


<= Menor o igual que.
== Igual a.
> Mayor que.
>= Mayor o igual que.
!= Distinto que

1.5.4. Operadores Lógicos

&& and La expresión combinada es cierta si ambos operandos lo son,


y falsa en cualquier otro caso.
|| or La expresión combinada es cierta si uno o ambos operandos
lo son, y falsa en cualquier otro caso.
! La expresión es cierta si el operador es falso, y viceversa.

60
LENGUAJES

1.5.5. Operadores Relacionados con punteros (C++)


& Opera- Cuando va seguido por el nombre de una variable, entrega la
dor de Di- dirección de dicha variable &abc es la dirección de la variable
rección abc.
* Ope- Cuando va seguido por un puntero, entrega el valor almace-
rador de nado en la dirección apuntada por él
Indirección
abc = 22;
def = & abc ; // puntero a abc
val = * def

El efecto neto es asignar a val el valor 22

1.5.6. Operadores de Estructuras y Uniones (C++)


1.5.7. Operadores Lógicos y de Desplazamiento de Bits
1.5.8. Misceláneas (C++)

1.6. Estructuras de control condicional e ite-


rativa
En principio, las sentencias de un programa en C++ y Java se ejecutan
secuencialmente, esto es, cada una a continuación de la anterior empezando
por la primera y acabando por la última. Ambos lenguajes dispone de varias
sentencias similares (la estructuras son muy parecidas) para modificar este
flujo secuencial de la ejecución. Las más utilizadas se agrupan en dos familias:
las bifurcaciones, que permiten elegir entre dos o más opciones según ciertas
condiciones, y los bucles, que permiten ejecutar repetidamente un conjunto
de instrucciones tantas veces como se desee, cambiando oactualizando ciertos
valores.

1.6.1. Estructura de control condicional (Bifurcacio-


nes)
Operador Condicional
El operador condicional es un operador con tres operandos (ternario) que
tiene la siguiente forma general:

61
LENGUAJES

expresion_1 ? expresion_2 : expresion_3 ;

Explicación: Se evalúa expresion 1. Si el resultado de dicha evaluación es


true (]0), se ejecuta expresion 2; si el resultado es false (=0), se ejecuta
expresion 3. Ejemplo:

9 %3==0 ? cout < < " El 3 divide al 9. " << endl : cout < < " El 3
no divide al 9. " << endl ;

Sentencia if
Esta sentencia de control permite ejecutar o no una sentencia simple o
compuesta según se cumpla o no una determinada condición. Esta sentencia
tiene la siguiente forma general:

if ( expresion ) {
sentencia ;
}

Explicación: Se evalúa expresion. Si el resultado es true (]0), se ejecuta


sentencia; si el resultado es false (=0), se salta sentencia y se prosigue en
la lı́nea siguiente. Hay que recordar que sentencia puede ser una sentencia
simple o compuesta (bloque ... ). Ejemplo:

int a , b ;
cin > >a > > b ;
if (a > b ) {
cout < < " El valor de la variable a es mayor que el valor
de la variable b . " << endl ;
}

Sentencia if ... else


Esta sentencia permite realizar una bifurcación, ejecutando una parte
u otra del programa según se cumpla o no una cierta condición. La forma
general es la siguiente:

if ( expresion ) {
sentencia_1 ;
} else {
sentencia_2 ;
}

62
LENGUAJES

Explicación: Se evalúa expresion. Si el resultado es true (]0), se ejecuta


sentencia 1 y se prosigue en la lı́nea siguiente a sentencia 2; si el resultado
es false (=0), se salta sentencia 1, se ejecuta sentencia 2 y se prosigue en la
lı́nea siguiente. Hay que indicar aquı́ también que sentencia 1 y sentencia 2
pueden ser sentencias simples o compuestas (bloques ... ). Ejemplo.

int a , b ;
cin > >a > > b ;
if (a > b ) {
cout < < " El valor de la variable a es mayor que el valor
de la variable b . " << endl ;
} else {
cout < < " El valor de la variable a es menor o igual que
el valor de la variable b . " << endl ;
}

Sentencia if ... else múltiple


Esta sentencia permite realizar una ramificación múltiple, ejecutando una
entre varias partes del programa según se cumpla una entre n condiciones.
La forma general es la siguiente:

if ( expresion_1 )
sentencia_1 ;
else if ( expresion_2 )
sentencia_2 ;
else if ( expresion_3 )
sentencia_3 ;
else if (...)
...
[ else
sentencia_n ;]

Explicación: Se evalúa expresion 1. Si el resultado es true, se ejecuta


sentencia 1. Si el resultado es false, se salta sentencia 1 y se evalúa expre-
sion 2. Si el resultado es true se ejecuta sentencia 2, mientras que si es false
se evalúa expresion 3 y ası́ sucesivamente. Si ninguna de las expresiones o
condiciones es true se ejecuta expresion n que es la opción por defecto (pue-
de ser la sentencia vacı́a, y en ese caso puede eliminarse junto con la palabra
else). Todas las sentencias pueden ser simples o compuestas.

int a , b ;

63
LENGUAJES

cin > >a > > b ;


if (a > b ) {
cout < < " El valor de la variable a es mayor que el valor
de la variable b . " << endl ;
} else if (a < b ) {
cout < < " El valor de la variable a es menor que el valor
de la variable b . " << endl ;
} else {
cout < < " El valor de la variable a es igual que el valor
de la variable b . " << endl ;
}

Sentencia switch
La sentencia que se va a describir a continuación desarrolla una función
similar a la de la sentencia if ... else con múltiples ramificaciones, aunque como
se puede ver presenta también importantes diferencias. La forma general de
la sentencia switch es la siguiente:

switch ( expresion ) {
case expresion_cte_1 :
sentencia_1 ;
break ;
case expresion_cte_2 :
sentencia_2 ;
break ;
...
case expresion_cte_n :
sentencia_n ;
break ;
[ default :
sentencia ;]
}

Explicación: Se evalúa expresion y se considera el resultado de dicha eva-


luación. Si dicho resultado coincide con el valor constante expresion cte 1, se
ejecuta sentencia 1 seguida de sentencia 2, sentencia 3, ..., sentencia. Si el re-
sultado coincide con el valor constante expresion cte 2, se ejecuta sentencia 2
seguida de sentencia 3, ..., sentencia. En general, se ejecutan todas aquellas
sentencias que están a continuación de la expresion cte cuyo valor coincide
con el resultado calculado al principio. Si ninguna expresion cte coincide se
ejecuta la sentencia que está a continuación de default. Si se desea ejecutar

64
LENGUAJES

únicamente una sentencia i (y no todo un conjunto de ellas), basta poner una


sentencia break a continuación (en algunos casos puede utilizarse la sentencia
return o la función exit()). El efecto de la sentencia break es dar por termi-
nada la ejecución de la sentencia switch. Existe también la posibilidad de
ejecutar la misma sentencia i para varios valores del resultado de expresion,
poniendo varios case expresion cte seguidos. Ejemplo.

int a ;
cin > > a ;
switch ( a ) {
case 0:
cout < < " El valor de a es 0. " ;
break ;
case 1:
cout < < " El valor de a es 1. " ;
break ;
default :
cout < < " El valor de a no es ni 1 ni 0. " ;;
}

Sentencias if anidadas
Una sentencia if puede incluir otros if dentro de la parte correspondiente
a su sentencia, A estas sentencias se les llama sentencias anidadas (una dentro
de otra), por ejemplo,

if (a >= b )
if ( b !=0.0)
c=a/b;

En ocasiones pueden aparecer dificultades de interpretación con senten-


cias if...else anidadas, como en el caso siguiente:

if (a >= b )
if ( b !=0.0)
c=a/b;
else
c =0.0;

En principio se podrı́a plantear la duda de a cuál de los dos if corresponde


la parte else del programa. Los espacios en blanco -las indentaciones de las
lı́neas parecen indicar que la sentencia que sigue a else corresponde al segundo
de los if, y ası́ es en realidad, pues la regla es que el else pertenece al if más

65
LENGUAJES

cercano. Sin embargo, no se olvide que el compilador de ya sea de Java


o C++ no considera los espacios en blanco (aunque sea muy conveniente
introducirlos para hacer más claro y legible el programa), y que si se quisiera
que el else perteneciera al primero de los if no bastarı́a cambiar los espacios
en blanco, sino que habrı́a que utilizar llaves, en la forma:

if (a >= b ) {
if ( b !=0.0)
c=a/b;
} else
c =0.0;

Recuérdese que todas las sentencias if e if...else, equivalen a una única


sentencia por la posición que ocupan en el programa.

1.6.2. Estructura de control iterativa (Bucles)


Además de bifurcaciones, en los lenguajes de programación existen tam-
bién varias sentencias que permiten repetir una serie de veces la ejecución de
unas lı́neas de código. Esta repetición se realiza, bien un número determinado
de veces, bien hasta que se cumpla una determinada condición de tipo lógico
o aritmético. De modo genérico, a estas sentencias se les denomina bucles.
Las tres construcciones de los lenguajes de programación C++ y Java para
realizar bucles son el while, el for y el do...while.

Sentencia while
Esta sentencia permite ejecutar repetidamente, mientras se cumpla una
determinada condición, una sentencia o bloque de sentencias. La forma ge-
neral es como sigue:

while ( e x p r e s i o n _ d e _ c o n t r o l ) {
sentencia ;
}

Explicación: Se evalúa expresion de control y si el resultado es false se


salta sentencia y se prosigue la ejecución. Si el resultado es true se ejecuta
sentencia y se vuelve a evaluar expresion de control (evidentemente alguna
variable de las que intervienen en expresion de control habrá tenido que ser
modificada, pues si no el bucle continuarı́a indefinidamente). La ejecución de
sentencia prosigue hasta que expresion de control se hace false, en cuyo caso
la ejecución continúa en la lı́nea siguiente a sentencia. En otras palabras,

66
LENGUAJES

sentencia se ejecuta repetidamente mientras expresion de control sea true, y


se deja de ejecutar cuando expresion de control se hace false. Obsérvese que
en este caso el control para decidir si se sale o no del bucle está antes de
sentencia, por lo que es posible que sentencia no se llegue a ejecutar ni una
sola vez.

int a ;
cin > > a ;
cout < < " Haciendo conteo regresivo " << endl ;
while (a >0) {
cout < < endl ;
a =a -1;
}

Sentencia for
For es quizás el tipo de bucle mas versátil y utilizado en los lenguajes de
programación. Su forma general es la siguiente:

for ( inicializacion ; e x p r e s i o n _ d e _ c o n t r o l ; actualizacion


){
sentencia ;
}

Explicación: Posiblemente la forma más sencilla de explicar la sentencia


for sea utilizando la construcción while que serı́a equivalente. Dicha construc-
ción es la siguiente:

inicializacion ;
while ( e x p r e s i o n _ d e _ c o n t r o l ) {
sentencia ;
actualizacion ;
}

donde sentencia puede ser una única sentencia terminada con (;), otra
sentencia de control ocupando varias lı́neas (if, while, for, ...), o una senten-
cia compuesta o un bloque encerrado entre llaves .... Antes de iniciarse el
bucle se ejecuta inicializacion, que es una o más sentencias que asignan valo-
res iniciales a ciertas variables o contadores. A continuación se evalúa expre-
sion de control y si es false se prosigue en la sentencia siguiente a la construc-
ción for; si es true se ejecutan sentencia y actualizacion, y se vuelve a evaluar
expresion de control. El proceso prosigue hasta que expresion de control sea

67
LENGUAJES

false. La parte de actualizacion sirve para actualizar variables o incrementar


contadores. Un ejemplo tı́pico puede ser el producto escalar de dos vectores
a y b de dimensión n:

for ( double pe =0.0 , int i =1; i <= n ; i ++) {


pe = pe + a [ i ]* b [ i ];
}

Primeramente se inicializa la variable pe a cero y la variable i a 1; el ciclo


se repetirá mientras que i sea menor o igual que n, y al final de cada ciclo el
valor de i se incrementará en una unidad. En total, el bucle se repetirá n veces.
La ventaja de la construcción for sobre la construcción while equivalente está
en que en la cabecera de la construcción for se tiene toda la información sobre
como se inicializan, controlan y actualizan las variables del bucle. Obsérvese
que la inicializacion consta de dos sentencias separadas por el operador (,).

Sentencia do ... while

Esta sentencia funciona de modo análogo a while, con la diferencia de que


la evaluación de expresion de control se realiza al final del bucle, después de
haber ejecutado al menos una vez las sentencias entre llaves; éstas se vuelven
a ejecutar mientras expresion de control sea true. La forma general de esta
sentencia es:

do {
sentencia ;
} while ( e x p r e s i o n _ d e _ c o n t r o l ) ;

donde sentencia puede ser una única sentencia o un bloque, y en la que


debe observarse que hay que poner (;) a continuación del paréntesis que
encierra a expresion de control, entre otros motivos para que esa lı́nea se
distinga de una sentencia while ordinaria.

1.6.3. Sentencias break, continue, goto


La instrucción break interrumpe la ejecución del bucle donde se ha inclui-
do, haciendo al programa salir de él aunque la expresion de control corres-
pondiente a ese bucle sea verdadera.
La sentencia continue hace que el programa comience el siguiente ciclo
del bucle donde se halla, aunque no haya llegado al final de las sentencia
compuesta o bloque.

68
LENGUAJES

La sentencia goto etiqueta hace saltar al programa a la sentencia donde


se haya escrito la etiqueta correspondiente. Por ejemplo:

sentencias ...
...
if ( condicion )
goto otro_lugar ; // salto al lugar indicado por la
etiqueta
sentencia_1 ;
sentencia_2 ;
...
otro_lugar : // esta es la sentencia a la que se salta
sentencia_3 ;
...

Obsérvese que la etiqueta termina con el carácter (:). La sentencia go-


to no es una sentencia muy prestigiada en el mundo de los programadores,
pues disminuye la claridad y legibilidad del código. Fue introducida en el
lenguaje por motivos de compatibilidad con antiguos hábitos de programa-
ción, y siempre puede ser sustituida por otras construcciones más claras y
estructuradas.

1.7. Variantes de entradas de datos


Para lograr una buena solución a un problema no debemos solo pensar
en el algoritmo que nos resolverá el problema sino también en todos aquellos
detalles que nos pueden hacer mas cómodo la implementación del algorit-
mo. Entre esos detalles esta la entrada o captura de datos. De forma general
los problemas de concursos presentan entradas de datos que son comunes y
pueden ser clasificadas o agrupadas de acuerdo a ciertos criterios. A continua-
ción veremos algunas de estas entradas y como deben ser enfocadas nuestras
soluciones para realizar una correcta captura de datos.

1.7.1. Entrada simple


Este tipo de entrada es la mas sencilla de todas. En ella te especifican
la cantidad de lı́neas valores que se deben leer en cada una de ellas. Veamos
algunos ejemplos:
DMOJ - Alex y la Cadena
Formato de entrada:

69
LENGUAJES

La primera y única lı́nea contiene la string S de tamaño (1 <= N <= 106 )


Se garantiza que cada caracter de la string S es una letra minúscula del
alfabeto Inglés.

Para este caso la captura del dato de entrada serı́a de la siguiente manera
dependiendo del lenguaje:

C++ Java

# include < iostream > import java . util . Scanner ;


using namespace std ;
class Main {
string S ; public static void main (
String [] args ) {
int main () { Scanner in = new Scanner
cin > > S ; ( System . in ) ;
// Resto de la solucion String S = in . next () ;
return 0; // Resto de la solucion
} }
}

COJ - 4221 - Campeonato en Coppelia

Formato de entrada:

La primera lı́nea de entrada contiene tres enteros no negativos menores o


iguales que 100, que representan la cantidad de Sundays, de Sierras Maestras
y de Gran Piedras que pidió Kyle. Las otras tres lı́neas de entrada contienen
la misma información pero con respecto a Cartman, Kenny y Stan respecti-
vamente.

70
LENGUAJES

C++ Java

# include < iostream > import java . util . Scanner ;


using namespace std ;
class Main {
int sKy , sMKy , gPKy ; public static void main (
int sC , sMC , gPC ; String [] args ) {
int sKe , sMKe , gPKe ; Scanner in = new Scanner
int sS , sMS , gPS ; ( System . in ) ;
int sKy , sMKy , gPKy ;
int main () { int sC , sMC , gPC ;
cin > > sKy > > sMKy > > gPKy ; int sKe , sMKe , gPKe ;
cin > > sS > > sMS > > gPS ; int sS , sMS , gPS ;
cin > > sKe > > sMKe > > gPKe ;
cin > > sS > > sMS > > gPS ; sKy = in . nextInt () ;
// Resto de la solucion sMKy = in . nextInt () ;
return 0; gPKy = in . nextInt () ;
}
sC = in . nextInt () ;
sMC = in . nextInt () ;
gPC = in . nextInt () ;

sKe = in . nextInt () ;
sMKe = in . nextInt () ;
gPKe = in . nextInt () ;

sS = in . nextInt () ;
sMS = in . nextInt () ;
gPS = in . nextInt () ;
// Resto de la solucion
}
}

71
LENGUAJES

1.7.2. Entrada por casos de pruebas


1.7.3. Entrada por condición
1.7.4. Entrada hasta fin de ficheros

1.8. Variantes de salidas de datos

72
Capı́tulo 2

Add Hoc

Unas de las temáticas dentro de las cuales pueden ser agrupados los pro-
blemas de concurso de programación es Add Hoc. Esta temática a diferencia
de otras donde existen tanto problemas como algoritmos clásicos bien defi-
nidos, no cuenta ni con uno ni con otro. Los problemas que son agrupados
en esta temática sus soluciones dependen en gran medida de las habilida-
des y creatividad de los programadores. Por lo general son los problemas
clasificados en esta temática los que comienzan hacer los concursantes de
programación cuando se inician, primero porque en su gran mayorı́a la solu-
ción radica en seguir algún procedimiento descripto en el propio problema,
segundo porque no se requiere un gran conocimiento en determinadas áreas
del conocimiento como pueden ser teorı́a de número, de grafos ,ect. Solo re-
quiere determinado nivel o habilidad sobre algún lenguaje de programación.
Esto no significa que sean los ejercicios más sencillos de resolver la mayoria
de los casos es ası́ pero hay algunos que se la traen.
Pues bien como los problemas de tipo Add Hoc no tienen problemas y
algoritmos clásicos que debamos saber para enfrentarlos, vamos aprovechar
esta sección para abordar algunos aspectos que son importantes no solo pa-
ra resolver los problemas enmarcados dentro de la temática sino cualquier
problema de concurso de programación :
Lo primero que se debe es leer bien el problema. Muchos se preguntan
porque leer el problema si existe una alta posibilidad de que no lo resuelva,
sobre todo si soy un notavo en este mundillo. Es un tremendo error pensar
de esta manera. Primero el no leer bien nos puede provocar omitir detalles
importante a la hora de generar una solución. Segundo, con una buena lec-
tura la compresión se hace mejor, y podemos tener un mejor criterio para
decidir si con los conocimientos propios y las restricciones del problema po-
demos desarrollar una soución que se ajuste. Por último le podemos dar una
verdadera clasificación al problema según nuestro criterio. Los grupos en los

73
ADD HOC

cuales se puede clasificar los problemas según nuestra valoración son:

Problema Sorpresa: Es el problema que la situación descrita es comple-


tamente nueva para ti. No tienes ni la menor idea de como solucionarlo.

Problema Piedra: Es el problema que la situación descrita la has visto


en otros problemas pero como las anteriores veces la falta de estudio
provoca que tenga que hacerlo de un lado.

Problema del Barrio:Es el problema piedra nuevamente pero esta vez


la diferencia ya tu ha estudiado acerca del tema y tienes nociones de
como solucionarlo o sabes dentro del equipo quien le puede ser de casa.

Problema de la Casa: Es el problema que puede ser el de más alta


complejidad de un concurso pero usted lo ha visto y solucionado tantas
veces que genera la solución con los ojos vendados y las manos atadas.

Por su puesto lo ideal serı́a tener que siempre enfrentarnos a problemas


del barrio o de la casa. Pero solo el estudio puede lograr esto. De lo contrario
te llevarás algunas sorpresas y tropezarás con bastantes piedras. La lectura
y compresión de los problemas hace posibles que luego en estudio de algún
tema convirtamos una sorpresa o una piedra en algo del barrio o de la casa.
Un segundo tema para tocar dentro de este capitulo son los datos de
entrada que nos brindan problemas para generar una respuesta al problema
tomando como base los datos proporcionado. En la mayorı́a de los casos para
los datos de entrada se definen los rangos. Muchos en sus inicios piensan que
se debe verificar esto cuando se implementa la solución. No es necesario
hacerlo. El hecho que los datos de entrada se definan sus rangos es para
ayudarnos o agobiarnos porque gracias a estos sabremos si nuestro posibles
algoritmo solución es factible o no dado los rangos de los datos de entrada.
Otro aspecto que nos facilita los rangos de los datos de entrada son los tipos
de datos que tendrán nuestra variable. A veces una mala selección de los tipos
de datos de nuestras variables pueden conducir a una respuesta incorrecta. Es
de suma importancia conocer los tipos de datos que nos proveen el lenguaje
o lenguajes de programación ası́ como los rangos definidos para estos.

2.1. Análisis de ejercicios


1000 - A+B Problem Clásico problema de los jurados online por el cual
comienza los programadores a adentrarse en este mundo. Dado dos números
decir la suma de estos.

74
ADD HOC

3698 - Upper-Lower Counting Un ejercicio sencillo donde solo se debe


recorrer una cadena y contar la cantidad de letras que están minúsculas y
mayúsculas.

3682 - Triangle Quality Ejercicio sencillo que solo se debe hacer lo que
plantea el enunciado del problema.

2695 – Vectors Para solucionar el ejercicio solo es necesario saber cómo


sumar vectores lo cual solo se puede hacer cuando dichos vectores tenga la
misma dimensión. El único elemento con dificultad del ejercicio es la entrada
del ejercicio la cual puede variar de acuerdo al lenguaje que utilice para
resolverlo.

2234 - Supercentral Point Solo se tiene que verificar si para un punto p


el mismo tiene vecinos segun las restricciones de problemas y lo cuentas para
la solución.

3326 - ICPC LEK-Team Si existe una solución entonces será siempre el


nombre inmediatamente a la derecha. O sea que solo basta comprobar cada
elemento de la lista con el que le sigue para saber si existe una solución o no,
en cada caso.

1750 - Alien Communicating Machines El ejercicio se reduce a saber


cómo convertir un número expresado en una b a otra base c sin tener que
utilizar como intermediario una tercera base.

1384 - Base -2 El ejercicio se reduce a saber convertir un número decimal


sin importar su signo a otra base numérica la cual puede ser negativa o posi-
tiva. Estudiar el epı́grafe sistemas de numeración para una mejor compresión.

3073 - Way Too Long Words Para solucionar este problema solo se
debe leer la cadena si la longitud es 10 o menor se imprime la cadena sino se
imprime el primer sı́mbolo de la cadena , la longitud de la cadena menos dos
y el último caracter de la cadena.

2845 - Quadrants La solución del problema solo depende de leer la coor-


denada del punto ver si esta sobre uno de los ejes o si esta esta sobre un
cuadrante ver cual.

75
ADD HOC

3765 - David’s writting Para solucionar el problema solo se debe leer


la cadena y tener una variable a la cual se le va sumar 0 cuando letra es
una vocal sino se le sumará (input[i]-’a’)+1 donde input es la variable que
almacena la cadena leida y input[i] hace referencia a la inesima posición de
la cadena.

3770 - Median of letters Para solucionar este problema solo debe ordenar
alfabeticamente de manera ascendente la cadena que dan como entrada e
imprimir la letra que se encuentra en la posición longitud/2 de la cadena
ordenada.

3769 - Word Play I Un ejercicio donde solo debemos cual es la pareja de


Li y Ci que su evaluación da el máximo EV e imprimirlo.

3772 - The Refund Solo debemos ir acumulando el 85 % del precio de


cada tickets de entrada. Y devolver ese acumulado con tres lugares después
de la coma.

1077 - The 3n + 1 Problem El problema que se nos plantean en bastane


sencillo de enteder tenemos un algoritmo determinado y si pide hallar un
número dentro de un rango determinado que una vez aplicado el algoritmo
descripto en el problema genere la secuencia mas larga. Para la solución basta
con iterar desde el incio del intervalo hasta el fin del intervalo y aplicar el el
algoritmo a cada número comprendido en el intervalo y quedarse con aquel
que el algoritmo genere la secuencia de mayor longitud. Como recomendación
les digo que no asuman que el primer número del rango es el menor y el
segundo el mayor en la lectura de datos. Otra sugerencia es que trabajen con
el mayor tipo de dato de entero que soporte el lenguaje que utilice porque
en la generación de la secuencia los valores pueden ser grandes.

2972 - Tobby and Sequence Este es un ejemplo de los problemas que


tienen soluciones multiples para una una misma entrada. Es por eso que los
jurados para dar un veredicto no usan ejemplos de salida en la mayoria de la
ocasiones, sino analizan la salida dada por la solución y si cumple con lo que
pide el problema se asume como correcta. Partiendo de esto, entonces solo
debemos implementar un algoritmo que genere una secuencia no menos de
mil números, los cuales deben ser únicos en dicha secuencia(no debe existir
repeticiones). Los números deben estar en el rango de 1 a 109 -1 y se debe
cumplir lo siguiente. Para ai donde de la secuencia donde i es mayor que
cero y menor que el n de la entrada se cumpla que |ai−1 − ai | > |ai − ai+1 |.

76
ADD HOC

Mi sugerencia es que el primer elemento de la secuencia es 1 y la diferencia


inicial es 1 , a partir de esto se genera la secuencia y luego se invierte para
que cumpla con las condiciones.

3103 - Darts Este ejercicio solo require simular el proceso que describen,
tener una función que devuelva la distancia de una cooredenada determinada
con respecto al centro de la diana y otra que dada una distancia devuelva la
cantidad de puntos que le corresponden. Tener en cuenta que si una dardo
cae sobre una de las circunferencia divisoras de la diana, se puntea el disparo
por el circulo menor.

3742 - Anders And The Names La solución del ejercicio solo radica
en leer toda una linea. De esa linea debemos contar la cantidad de palabras
y la cantidad de letras en mayúsculas. Si dichas cantidades son iguales la
respuesta es correct en caso contrario la repuesta es awful

3827 - Tuna Ejercicio sencillo del cual solo debemos implementar un al-
goritmo que siga el procedimiento descrito en el problema.

1099 - Pythagorean Numbers El ejercicio solo nos pide dada una triada
de números chequear si dichos números conforman una terna pitagórica. EL
detalle del problema es que se deben verificar todas las combinaciones posibles
(a2 + b2 == c2 , a2 + c2 == b2 ,c2 + b2 == a2 ), si al menos una de ellas es
verdadera , la respuesta será right sino la respuesta será wrong.

3869 - Elevator Task Para resolver el problema solo basta con implemen-
tar un algoritmo que resuelva la siguiente ecuación S=A+B+C donde:

A= |1 − a1 |

B= |a1 − 1|
Pi=1
C= n−1 |ai − ai+1 |

Siendo a el arreglo de los números de pisos hacia donde debe ir el elevador.

3912 - Assisting Grandpa Laino La solución del problema basta con


chequear que la palabra de la entrada de datos tenga al menos una letra i si
la tiene la respuesta es ’N’ en caso contrario la respuesta es ’S’ .

77
ADD HOC

1004 - Traversing Grid Una vez leido el problema la primera solución


que nos puede ser factible es la de simular el proceso. Esta variante debe
ser descartada rápidamente producto a las dimensiones que puede tomar
la matriz. Es por que el primer paso en pos de buscar una solución debe
ser en detectar posibles respuestas idénticas frente a situaciones que son
comunes. Es por eso que les digo que realicen el las pruebas para matrices
con dimensiones menores de 10 y detecte que sucede cuando:

La cantidad de filas y columnas son iguales y par.

La cantidad de filas y columnas son iguales e impar.

La cantidad de filas es mayor que las columnas y ambas par.

La cantidad de filas es menor que las columnas y ambas par.

La cantidad de filas es mayor que las columnas y ambas impar.

La cantidad de filas es menor que las columnas y ambas impar.

La cantidad de filas es par y mayor que la cantidad impar de columnas.

La cantidad de filas es par y menor que la cantidad impar de columnas.

La cantidad de filas es impar y mayor que la cantidad par de columnas.

La cantidad de filas es impar y menor que la cantidad par de columnas.

1065 - Money Matters A pesar que el ejercicio esta catalogado como Ad-
Hoc 2. Para solucionarlo me apoye en teorı́a de grafos. Construı́ el grafo donde
los nodos son las personas y las aristas las amistades. Una vez modelado
el problema determine la cantidad de componente conexa del grafo y para
cada componente sume las deudas y el dinero de sobra de cada nodo de esa
componente. Si alguna suma de las componentes da menor que cero entonces
la respuesta es IMPOSIBLE sino POSIBLE.

3811 - A xor B xor C El ejercicio esta catalogado como estructura de


datos y Add-Hoc pero mi solución es mas bien Add-Hoc. El problema nos
pide hallar tres números A, B, C dentro de una colección dada tal que la
operacion A xor B xor C sea igual a un valor X que nos proporcionan. La
solución trivial es tres for con i,j,k como variables iteradoras y comprobar
que exista una triada tal que L[i] xor L[j] xor L[k] == X con i!=j , i!=k
y j!=k pero esta solución tiene como complejidad O(n3 ) y se nos puede ir

78
ADD HOC

en tiempo. El operador xor tiene la propiedad que D xor C = X y D xor X


= C. Donde D= A xor B. En este caso se puede con dos for con i,j como
variables iteradoras y buscar un C dentro de la lista L. Donde C=(L[i] xor
L[j])xorX con i!=j. Esto nos da una complejidad de O(n2 ) mas accesible ya
que la cantidad de elementos de L es 1024, pero nos obliga buscar a C en
un tiempo constante en O(1) para ser más preciso. Como se logra esto ?.
Sencillo el problema también nos plantea que todos los valores de L estan en
el rango de 1 a 1024. Con esta información puedo crear un arreglo de entero
counts con dimension 1030 e inicializado todas sus posiciones en cero, donde
en la posición i voy almacenar la cantidad de repeticiones del valor i en L.
Ya con esto podemos saber si existe un C en L en tiempo O(1) y resuelto el
problema.

1327 - Barbecue Una vez leı́do el problema es claro entender que debe-
mos hallar cual es el miembro de la familia vamos a dejar sin carne. Para
hallarlo vamos a crear un estructura que bien se puede llamar Person la cual
va contener tres atributos de tipo entero: id , cantidad de votos recibidos,
cantidad de votos emitidos. Luego con un arreglo de esta estructura donde
cada posición va representar un miembro de la familia vamos actualizar el
valor de cada variable de los miembros de la famila según los datos de en-
tradas. Una vez actualizado los datos, ordenamos el arreglo de acuerdo a la
reglas para definir quien se debe quedar sin carne y la respuesta al ejercicio
quedará o bien en la ultima posición del arreglo o en la primera de acuerdo
a como se haya ordenado. Se debe verificar que ninguna persona vote más
de una vez por una misma persona o por ella misma elemento que se puede
controlar con una matriz booleana.

2465 - Adding Hex Numbers El problema nos pide dado dos núme-
ros representados en base hexadecimal determinar la cantidad acarreos que
produce la suma entre estos. La solución es bastante sencilla, solo basta im-
plementar un algoritmo que simule el proceso de suma y que nos permita
llevar la cuenta de la cantidad de acarreos que esta genera. Se debe tener en
cuenta los números no tienen que tener la misma cantidad de digitos.

2718 - Out Time Is El ejercicio nos pide determinar la hora en que el


robot comenzo el recorrido sabiendo la hora de llegada a la meta y el tiempo
de viaje. Se debe tener en cuenta en la implementación que el tiempo de
viaje puede mayor que el tiempo de llegada , lo que significa que el viaje lo
comenzo en el dia anterior. Lo otro importante es el formato en que se debe
dar la respuestas.

79
ADD HOC

2207 - Grid Rotation El problema nos pide determinar dada una matriz
A cuadrada de dimension n y una colección de matrices saber si cada matriz
de la colección la matriz A se puede transformar en ella aplicando una o
varias rotaciones de 90 grados lo mismo en sentido de las manecillas del
reloj o contraria. Para solucionar el problema se puede crear tres matrices
auxiliares en las cuales vamos a guardar como va quedar la matriz A con
un giro de 90, 180 y 270, luego para cada matriz de la colección basta con
probar que esta sea igual a una de las cuatro posibles transformaciones de la
matriz A para que la respuesta sea YES y NO en caso contrario. Sugiero que
tanto la matriz A como sus transformaciones sean alamcenadas en variables
de tipo cadena. De igual forma cada matriz a analizar se debe guardar en
variable de tipo cadena para ser mas fácil la comparación.

1633 - Minglanillas: Consultor Junior El problema nos plantea que


dado dos conjuntos de funcionalidades que tienen dos aplicaciones determinar
de acuerdo a un posible valor, la unión, intersección y diferencia entre los dos
conjuntos de funcionalidades. Las funcionalidades se identifican por un valor
único y no se repiten. En caso que utilice C++ puede utilizar la biblioteca
algorithm que tiene ya funciones que permiten hallar la unión, la intersección
y diferencia entre colecciones ordenadas.

1059 - Parity El ejercicio es bien sencillo solo debemos leer un número


mientras este sea distinto de 0, llevarlo a su repesentación en binario y contar
la cantidad de uno de dicha representación. La respuesta será el número leido
en binario y la cantidad de uno que existe en dicha representación. Hay que
tener en cuenta es el formato de salida que especifica el ejercicio.

2678 - My Brother’s Diary Dado un texto incriptado se pide que lo


desencripte siempre que sea posible. Se conoce que cada letra del mensaje
original es cambiada por otra letra que esta a una distancia d en el alfabeto
de la letra que se desea incriptar. Por ejemplo para d=1 A se convierte en
B, B en C .... Z en A. Para cada entrada de datos el valor d puede variar,
pero se sabe que en el mensaje original la letra que mas se escribe es la letra
E. Dado un texto incriptado se debe hallar si es posible el valor de d y el
mensaje original.
Lo primero es hallar el valor de d para esto debemos encontrar la letra
que más se repite en el mensaje incriptado. En caso de existir dos o mas
letras cuya cantidad de ocurrencias sean iguales y máximas entonces no se
puede decifrar el mensaje y se debe imprimir el mensaje NOT POSIBLE.

80
ADD HOC

En caso de existir una sola letra cuya cantidad de ocurrencias es máxima


y única entonces es posible hallar d y decifrar el mensaje. Si la letra es igual
a E o posterior a esta en el alfabeto entonces el valor de d será letra - E sino
26-(E-letra) donde A=0, B=1 ... E=4 .... Z=25.
Una vez hallada el valor de d pasamos a decifrar el mensaje el cual se pro-
cede de la siguiente forma recorro el mensaje incriptado y por cada carácter
que sea una letra si la posición que ocupa esta en el alfabeto (comenzando
con A=0, B=1 ... Z=25) menos el valor de d es mayor o igual a cero la letra en
el mensaje original la letra que ocupa en el alfabeto la posición P − d . Donde
P es la posición en el alfabeto que ocupa la letra cifrada. En caso contrario
la letra en esa posición en el mensaje sin cifrar es igual a 26 − (d − P ).
En caso que el carácter en la posición del mensaje cifrado no sea letra esa
posición en el mensaje sin cifrar va estar mismo carácter.

4006 - Numbers in French Este es un ejercicio bien trivial tipico de ser


publicado en una competencia de práctica. Solo debemos leer una cantidad
de numeros los cuales van estar en el rango del 1 al 20 y de acuerdo al valor
imprimir como se escribe dicho numero con letras pero en idioma frances.

4062 - Help a PhD candidate out! El ejercicio nos pide leer una serie de
entradas y para cada entrada que sea del tipo ’a + b’ donde a y b son números
imprimir la suma de estos. En caso de que la entrada sea del tipo ’P=NP’ la
salida para esa entrada debe ser ’skipped’. La complejidad es bastante baja
quiza es punto de mayor complejidad sea el análisis de la cadena entrada en
cada caso, lo cual puede ser complicado o no en dependencia del lenguajes
de programación que utilices.

1392 - Touch-Cows Game El problema nos plantea un juego y nos pide


dada las condiciones iniciales de cada jugador determinar el jugador gana-
dor. Para solucionar el problema podemos perfectamente simular el juego
producto que el máximo de jugadores que puede tener el juego no sobrepasa
los 1000 y en cada ronda de turno solo juega la mitad de los participantes
porque la otra mitad es eliminado por los que juegan. Esta misma situación
ocurre en cada ronda por lo que el máximo de ronda que se va jugar es igual
a log2 N donde N es la cantidad de jugadores. Como parte de la implementa-
ción voy a tener una estructura que representa un jugador dicha estructura
debe tener una colección con el resto de los jugadores ordenados de forma
ascedente teniendo como parámetro comparativo la distancia entre los ju-
gadores(en mi caso utilice una cola con prioridad donde a menor distancia
mayor prioridad), un arreglo booleano con una dimensión igual a la cantidad

81
ADD HOC

máxima de jugadores, el valor de la enésima posición del arreglo indicará si


el jugador enesimo esta en juego o fue eliminado.

4090 - Jirthday Es un ejercicio bien sencillo. Nos pide determinar la edad


que tendrı́a una persona en el año 3000 si se conoces el año de su nacimiento.
Solo debemos imprimir el resultado de la resta de 3000 menos el valor del
año en que nació la persona.

4081 - Almost Accepted Dada dos cadenas. Donde la primera hace re-
ferencia a una salida correcta de un problema determinado y la segunda es
la salida que da una solución que se le hizo al problema. Determinar si dicha
solución es Accepted, Wrong Answer o Presentation Error.
Para ver si es correcta se debe cumplir que:

L(N)==L(M) donde N es la cadena correcta y M es la cadena de la


solución y L(x) es la función que calcula la longitud de la cadena x.

Para cada i desde 0 hasta L(N)-1 Mi ==Ni

Para ver si es error de presentación se debe cumplir que:

K(N)==K(M) donde K(x) es la función que calcula la longitud de la


cadena x sin contar los espacios en blancos.

Para cada e desde 0 hasta K(N)-1 Qe ==Pe . Siendo Q y P las cadenas


M y N respectivamente pero sin espacios en blancos.

Si no cumplió con ninguno de los casos anteriores entonces debe conside-


rarse como respuesta incorrecta.

3997 - Saving Coins El problema nos pide determinar cuantas monedas


se ahorran duante un año de 365 dı́as y conociendo el dı́a en que comenzo
el año y cuantas monedas se ahorran para cada dı́a de la semana. Si nos
detenemos y analizamos un poco veremos que 365 dı́as es lo mismo que decir
52 semanas y un dı́a. Según lo descrito en el problema en una semana se es
capaz de ahorrar 13 monedas. Por lo que la solución al problema será igual
a 13*52+X . Donde X es la cantidad de monedas que se ahorra el dı́a en que
comienza el año.

82
ADD HOC

3946 - Counting vowels Una vez leido el problema nos podemos percatar
que para una secuencia de caracteres de longitud N nos piden hallar cuantas
vocales hay en el primer subgrupo de caracteres de la cadena que son los
primeros N/3+1 caracteres , cuantas vocales hay en el segundo subgrupo de
caracteres de la cadena que son los segundos N/3+1 caracteres y cuantas
vocales existen en el tercer subgrupo que son los caracteres sobrantes de la
cadena que no estan ni en el primer y segundo subgrupo.

4092 - Lightweigth Es un ejercicio bien sencillo dado el valor de un núme-


ro comparar con dos si es menor la salida debe ser WRONG ANSWER si
es mayor que tres la salida debe ser RUNTIME ERROR para cualquier otro
caso la salida debe ser ACCEPTED

4135 - Missing Runners Dado un N y una lista de N-1 elementos los


cuales sus valores están en el rango de 1 a N determinar que número del
rango no esta presente en la lista. La solución es bien sencilla. Solo debemos
hallar la suma de todos los enteros de 1 a N (Sumatoria de Gauss) y luego a
esa suma le restamos cada uno de los elementos de la lista. Una vez concluida
esta operación el número almacenado en la suma es el elemento que falta.

4124 - Chuqui and his little ball Una vez leı́do el problema nos po-
demos percatar que nos pide hallar la mayor longitud de subsecuencia de
elementos concecutivos de la secuencia de entrada cuyo valores en los extre-
mos sean iguales. Para resolver dicho problema vamos ver que los valores de
la secuencia están en el rango de 1 a 106 , este dato nos permitará implemen-
tar un algoritmo con una idea similar al algoritmo de ordenamiento similar
al CountingSort. Vamos a tener un arreglo llamado indexs cuya dimensión va
ser de 106 e inicialmente cada posición con un valor -1 y una variable range
que va guardar el resultado final cuyo valor inicial es 1 . Una vez hecho esto
vamos a empezar a leer cada uno de los números que componen ls secuencia
realizando el siguiente procedimiento:

Si indexs[valor]==-1 significa que es la primera ocurrencia del valor por


lo que indexs[valor] va ser igual al valor actual de la posición del valor
en la secuencia de datos de entrada.

Si indexs[valor]!=-1 significa que no es la primera ocurrencia del valor


por lo que ya este valor ya esta en la secuencia y por tanto range=max(range, i−
indexs[valor]+1) donde i es la posición actual del valor en la secuencia
que se esta leyendo.

83
ADD HOC

Se recomienda que la indexación del arreglo se haga partir de 1 y no de


0 como se hace habitualmente.

4105 - Elemento Faltante Una vez leı́do el problema podemos perca-


tarnos que su solución es similar al ejercicio 4135 - Missing Runners casi
identica.

4101 - Indecisos Una vez leı́do el problema podemos analizar que una ley
solo va ser aprobada si la cantidad de legisladores que van votar por el NO
mas la cantidad de legisladores que están indecisos no superan o igualan la
cantidad de legisladores que votarán por el SI. Forma inversa la ley no va
ser aprobada si la cantidad de legisladores que van a votar por el SI mas
la cantidad de legisladores que están indecisos no superan la cantidad de
legisladores que votarán por el NO. En cualquier otro caso el resultado es
INDECISOS.

3992 - Five Minutes El problema nos explica el horario docente y se


quiere dada una hora del dı́a saber si dicho instante de tiempo forma parte
de uno de los intervalos de clases del horario o forma parte de los 5 minutos de
recreo entre cada turno de clases del horario. Se puede hacer un método que
reciba un número que representa la cantidad de minutos transcurridos desde
el inicio del dı́a hasta ese instante y que chequee si dicho instante de tiempo
esta incluido en algunos de los intervalos de tiempo de los turnos. Luego
para cada hora y minutos se lleva a minutos de acuerdo a lo que responda la
función explicada anteriormente será la respuesta.

4221 - Campeonato en Coppelia Cuando se lee el problema nos pode-


mos percatar que solo debemos devolver el mayor valor que pueda producir
una de las triadas de valores de la entrada. Es imprimir el máximo que toma
la siguiente expresión sundays ∗ 2 + sierraM estras ∗ 5 + granP iedra ∗ 7 para
cada una de las triadas de valores de entrada.

DMJO A plus B Clásico primer problema de varios jurados online de


dado dos números hallar la suma de ambos. Quizá la variación de este es que
el rango de los numeros de entrada es bastante grande y son varios casos.

DMOJ - Mas próximo a C Dado los números a,b,c se quiere hallar el


número d que su diferencia absoluta con el valor c sea lo menor posible y
en caso de existir varios valores d con la misma diferencia mı́nima escoger
aquella d con el menor valor. Los posibles valores de d son los resultados de

84
ADD HOC

todas las variantes de suma y multiplicación utilizando los valores de a y b.


Es decir a + a, a + b, b + b, a ∗ a, a ∗ b, b ∗ b. Para solucionar el problema basta
con hacer una comprobación con cada uno de los posibles valores tomando
en cuenta que cuando la diferencia es menor que se tenı́a hasta ese momento
se toma ese valor pero si la diferencia es la misma solo se toma ese valor si
es menor que el valor que se tenı́a hasta ese momento.

1101 - Binaries Palindromes El ejercicio nos pide dado un rango definido


por dos números A y B imprimir todos aquellos números de forma creciente
que pertenezcan al intervalo incluyendo los extremos y que su representación
binaria sea palindrome. Una idea trivial y es la correcta es recorrer todos
los números enteros en el rango de [A ... B] convertir cada uno a binario y
ver si dicha representación es palindrome de serlo se imprime el número en
su representación decimal. Ahora bien si analizamos un poco nos podemos
percatar que cualquier número par en decimal cuando se lleva a binario no
es palindrome porque su último dı́gito es cero y el primero es uno. Por lo que
una optimización a la idea anterior es en vez de recorrer todos los enteros
del rango recorran todos los enteros impares del rango que a priori pudieran
se binarios palindromes. Tener en cuenta a la hora de la salida que pueden
solicitar un rango donde no exista números que cumplan la condición que
pide el problema.

85
ADD HOC

86
Capı́tulo 3

Fuerza Bruta

Similar a como le ocurre a los problemas que son enmarcados dentro


de la temática de Add-Hoc, los problemas clasificados como Fuerza Bruta no
presentan algoritmos clásicos para solucionarlos. La mayorı́a de los problemas
de esta temática permiten implementar una solución algorı́tmica que simule
proceso descripto en el problema y que obtenga una solución. Para percatarse
si un problema puede atacado usando esta técnica lo primero que se debe
analizar las restricciones que impone el problema y si nuestro algoritmo se
ajusta a dichas restricciones. Para saber si nuestro algoritmo se ajusta es
necesario conocer o determinar la complejidad del mismo. La complejidad
es una forma de calcular a priori la lentitud o rapidez de nuestro algoritmo
siempre pensando que el mismo se va a ejecutar con el juego de de datos
más adversos a él. La complejidad se expresa en base a una expresión o
función. El cálculo de la complejidad de algoritmos permite o es un criterio
muy fuerte a la hora de seleccionar un algoritmo entre dos o mas algoritmos
que permiten solucionar el mismo problema. Es por eso que en este capı́tulo
vamos a dedicar una sección a este tema.

3.1. Complejidad
Una vez dispongamos de un algoritmo que funciona correctamente, es
necesario definir criterios para medir su rendimiento o comportamiento. Estos
criterios se centran principalmente en su simplicidad y en el uso eficiente de
los recursos.
A menudo se piensa que un algoritmo sencillo no es muy eficiente. Sin
embargo, la sencillez es una caracterı́stica muy interesante a la hora de diseñar
un algoritmo, pues facilita su verificación, el estudio de su eficiencia y su
mantenimiento. De ahı́ que muchas veces prime la simplicidad y legibilidad

87
FUERZA BRUTA

del código frente a alternativas más crı́pticas y eficientes del algoritmo.


Respecto al uso eficiente de los recursos, éste suele medirse en función de
dos parámetros: el espacio, es decir, memoria que utiliza, y el tiempo, lo que
tarda en ejecutarse. Ambos representan los costes que supone encontrar la so-
lución al problema planteado mediante un algoritmo. Dichos parámetros van
a servir además para comparar algoritmos entre sı́, permitiendo determinar
el más adecuado de entre varios que solucionan un mismo problema.

3.1.1. Eficiencia temporal


El tiempo de ejecución de un algoritmo va a depender de diversos factores
como son: los datos de entrada que le suministremos, la calidad del código
generado por el compilador para crear el programa objeto, la naturaleza y
rapidez de las instrucciones máquina del procesador concreto que ejecute
el programa, y la complejidad intrı́nseca del algoritmo. Hay dos estudios
posibles sobre el tiempo:

Uno que proporciona una medida teórica (a priori), que consiste en


obtener una función que acote (por arriba o por abajo) el tiempo de
ejecución del algoritmo para unos valores de entrada dados.

Y otro que ofrece una medida real (a posteriori), consistente en medir


el tiempo de ejecución del algoritmo para unos valores de entrada dados
y en un ordenador concreto.

Ambas medidas son importantes puesto que, si bien la primera nos ofrece
estimaciones del comportamiento de los algoritmos de forma independiente
del ordenador en donde serán implementados y sin necesidad de ejecutarlos,
la segunda representa las medidas reales del comportamiento del algoritmo.
Estas medidas son funciones temporales de los datos de entrada.
Entendemos por tamaño de la entrada el número de componentes sobre
los que se va a ejecutar el algoritmo. Por ejemplo, la dimensión del vector a
ordenar o el tamaño de las matrices a multiplicar.
La unidad de tiempo a la que debe hacer referencia estas medidas de
eficiencia no puede ser expresada en segundos o en otra unidad de tiempo
concreta, pues no existe un ordenador estándar al que puedan hacer referen-
cia todas las medidas. Denotaremos por T(n) el tiempo de ejecución de un
algoritmo para una entrada de tamaño n.
También es importante hacer notar que el comportamiento de un algo-
ritmo puede cambiar notablemente para diferentes entradas (por ejemplo, lo
ordenados que se encuentren ya los datos a ordenar). De hecho, para muchos

88
FUERZA BRUTA

programas el tiempo de ejecución es en realidad una función de la entrada


especı́fica, y no sólo del tamaño de ésta. Ası́ suelen estudiarse tres casos para
un mismo algoritmo:caso peor, caso mejor y caso medio.
El caso mejor corresponde a la traza (secuencia de sentencias) del algorit-
mo que realiza menos instrucciones. Análogamente, el caso peor corresponde
a la traza del algoritmo que realiza más instrucciones. Respecto al caso medio,
corresponde a la traza del algoritmo que realiza un número de instrucciones
igual a la esperanza matemática de la variable aleatoria definida por todas
las posibles trazas del algoritmo para un tamaño de la entrada dado, con las
probabilidades de que éstas ocurran para esa entrada.
Es muy importante destacar que esos casos corresponden a un tamaño de
la entrada dado, puesto que es un error común confundir el caso mejor con el
que menos instrucciones realiza en cualquier caso, y por lo tanto contabilizar
las instrucciones que hace para n = 1.
A la hora de medir el tiempo, siempre lo haremos en función del número
de operaciones elementales que realiza dicho algoritmo, entendiendo por ope-
raciones elementales (en adelante OE) aquellas que el ordenador realiza en
tiempo acotado por una constante. Ası́, consideraremos OE las operaciones
aritméticas básicas, asignaciones a variables de tipo predefinido por el compi-
lador, los saltos (llamadas a funciones y procedimientos, retorno desde ellos,
etc.), las comparaciones lógicas y el acceso a estructuras indexadas básicas,
como son los vectores y matrices. Cada una de ellas contabilizará como 1
OE.
Resumiendo, el tiempo de ejecución de un algoritmo va a ser una función
que mide el número de operaciones elementales que realiza el algoritmo para
un tamaño de entrada dado.

Reglas generales para el cálculo del número de OE


La siguiente lista presenta un conjunto de reglas generales para el cálculo
del número de OE, siempre considerando el peor caso. Estas reglas definen el
número de OE de cada estructura básica del lenguaje, por lo que el número
de OE de un algoritmo puede hacerse por inducción sobre ellas.

- Vamos a considerar que el tiempo de una OE es, por definición, de orden


1. La constante c que menciona el Principio de Invarianza dependerá
de la implementación particular, pero nosotros supondremos que vale
1.
- El tiempo de ejecución de una secuencia consecutiva de instrucciones
se calcula sumando los tiempos de ejecución de cada una de las ins-
trucciones.

89
FUERZA BRUTA

- El tiempo de ejecución de la sentencia “CASE C OF v1:S1 |v2:S2|...|vn:Sn


END; ”es T = T(C) + max{T(S1 ),T(S2 ),...,T(Sn )}. Obsérvese que
T(C) incluye el tiempo de comparación con v1 , v2 ,..., vn .

- El tiempo de ejecución de la sentencia “IF C THEN S1 ELSE S2 END;


”es T = T(C) + max{T(S1 ),T(S2 )}.

- El tiempo de ejecución de un bucle de sentencias “WHILE C DO S


END; ”es T = T(C) + (no iteraciones)*(T(S) + T(C)). Obsérvese que
tanto T(C) como T(S) pueden variar en cada iteración, y por tanto
habrá que tenerlo en cuenta para su cálculo.

- Para calcular el tiempo de ejecución del resto de sentencias iterativas


(FOR, REPEAT, LOOP) basta expresarlas como un bucle WHILE.

- El tiempo de ejecución de una llamada a un procedimiento o función


F(P1 , P2 , ..., Pn ) es 1 (por la llamada), más el tiempo de evaluación de
los parámetros P1 , P2 , ..., Pn , más el tiempo que tarde en ejecutarse
F, esto es, T = 1 + T(P1 ) + T(P2 ) + ... + T(Pn )+T(F). No conta-
bilizamos la copia de los argumentos a la pila de ejecución, salvo que
se trate de estructuras complejas (registros o vectores) que se pasan
por valor. En este caso contabilizaremos tantas OE como valores sim-
ples contenga la estructura. El paso de parámetros por referencia, por
tratarse simplemente de punteros, no contabiliza tampoco.

- El tiempo de ejecución de las llamadas a procedimientos recursivos va


a dar lugar a ecuaciones en recurrencia.

- También es necesario tener en cuenta, cuando el compilador las in-


corpore, las optimizaciones del código y la forma de evaluación de las
expresiones, que pueden ocasionar “cortocircuitos”o realizarse de for-
ma “perezosa”(lazy). En el presente trabajo supondremos que no se
realizan optimizaciones, que existe el cortocircuito y que no existe eva-
luación perezosa.

Es evidente que las reglas planteadas anteriormente no arrojan un valor


que nos indique que tan rápido es nuestro algoritmo para dar solución a un
problema. Para dar un valor se necesitarı́a de otras variables a considerar
sobre todo del tipo hardware.
Las reglas planteadas nos permite saber la rapidez de nuestro algoritmos
en base a una función que bien puede depender de una o varias variables
independiente. Un problema puede tener varias vı́as de solución, pero como
saber cual es más óptima si lo que tenemos son funciones que depende de los

90
FUERZA BRUTA

valores de variables independiente para expresar su rapidez. Sencillo tome-


mos cada variable independiente de las funciones y le damos un valor1 que
contribuya a maximizar el valor final de la función. Como resultado obten-
dremos dos o más valores dependiendo de la cantidad de funciones y aquella
función que arroje un menor valor será la del algoritmo mas óptimo para el
peor de los casos al cual se puede enfrentar el algoritmo.
Y esa debe nuestra filosofı́a a la hora de enfrentar los problemas de concur-
so de programación. Siempre debemos pensar nuestro algoritmo asumiendo
que vamos a enfrentar el peor de los casos. Tomando como base el peor de los
casos debemos pensar un algoritmo óptimo para ese caso. Muy importante
el peor caso no siempre es aquel que tiene el valor mayor o el menor valor,
también puede ser el caso donde los valores se repiten o están más dispersos
depende mucho de la situación que plantea el problema. Hay una regla de
oro en la programación y es “Si tienes un problema primero haz una solución
que lo resuelva, después si puedes optimiza la solución.”Donde la segunda
parte es súper importante porque a veces tratando de optimizar algo que es
una solución correcta la convertimos en una solución incorrecta.

3.2. Análisis de ejercicios


3753 - Incredulous Ed Dado los números pequeños, podemos hacer sim-
plemente fuerza bruta y probar todas las secuencias posibles. Con el uso de
un DFS en su variante recursiva podemos generar todas las posibles combi-
naciones. Siempre teniendo en cuenta que existen nodos U y V que para ir de
U a V se tiene que verificar que el nodo X ya este visitado. El otro detalle que
se debe tener en cuenta que la cantidad de combinaciones para una matrix i
filas con k columnas es la misma cantidad de combinaciones para una matriz
de k filas y i columnas ası́ que por ahi se puede aplicar de dinamica para
optimizar.

1082 - Be or Not Interesting Squares Este es un clásico ejercicio don-


de se puede poner a prueba la maldad del programador. Se puede hacer una
solución aplicando un backtraking o sencillamente a fuerza bruta pero esta
ultima variante corremos el reisgo que la solución se nos vaya en tiempo.
Quizas podamos optimizar algo si no aplicamos el algoritmo a valores ya
calculamos anteriormente y guardamos los resultados para valores que sea la
primera vez. Con esas consideraciones se puede atacar el ejercicio. En opinión
personal este es el tipo de ejercicio donde uno puede realizar un algoritmo
1
Este valor debe estar en el rango definido por esa variable

91
FUERZA BRUTA

que solucione el problema, lo pruebe para cada posible valor de entrada (en
este caso solo son cinco posibles entradas) y guarde los resultados. A la hora
de generar la solución que se va enviar se crea una estructura de datos donde
la clave sea la posible entrada y el valor sea el resultado previamente calcula-
do para esa entrada. Este tipo de solución a veces en compentencias de alto
nivel son rechazadas. Pero pienselo por un momento. Tengo un problema que
no puedo resolverlo algoritmicamente por las restricciones que me imponen
el problema pero si puedo crear un algoritmo que me de la solución para cada
posible valor de entrada en un tiempo menor que el tiempo que dispongo (si
esta en una compentencia es casi siempre 4 horas) y mayor que el tiempo de
ejecucción que el tiempo del problema. Entonces puedo implementar un pro-
grama con mi solución, lo ejecuto, me concentro en otro problema y despues
de pasado un tiempo reviso los resultados y conformo una solución que se
ajuste al problema con las posibles entradas y su posibles resultados.

3782 - Cacho Ejercicio muy sencillo que solo debemos verificar si los cinco
numeros que nos dan en cada entrada cumplen la condición descrita en el
problema. De cumplirla la solución es Y y N en caso contrario.

1000 A+B Problem Es clásico problema que aparece como primero de


los problemas que publica un Juez en Lı́nea. Se nos pide leer dos números e
imprimir la suma resultante.

1023 - Financial Management El problema nos pide calcular el salario


promedio mensual de Larry dado el salario de los doce meses. La solución
radica en sumar los doce números que nos dan en la entrada y hallar el
promedio dividiendo la suma por 12.000 (OJO recuerde 12.000 y no 12). Lo
otro a tener en cuenta es el formato de salida del problema. Donde se debe
redondear dos lugares luego de la coma

3937 - Flooded Area El ejercicio nos pide determinar dada una matriz
de puntos y asteriscos determinar si los asteriscos existentes conforman un
solo cuadrado. La solución de ejercicio radica en buscar las filas y columnas
extremas que tengan asteriscos, es decir buscar la fila mas arriba y la fila
mas abajo y las columnas mas a la izquierda y mas a la derecha que tenga
asteriscos, de igual forma llevar un contador con la cantidad de asteriscos
encontrados. Una vez terminado la búsqueda y conteo, tenemos que el ancho
del supuesto rectángulo que encierra a todos asteriscos de la matriz es igual
a columnader -columnaizq +1, mientras el alto serı́a filaabajo -filaarriba +1. Con
esto basta con probar que la cantidad de asteriscos sea mayor que cero y que

92
FUERZA BRUTA

la multiplicación de alto por ancho sea igual a la cantidad de asteriscos y que


el ancho sea igual alto para que la respuesta sea positiva en caso contrario
que se deje de cumplir alguna de las restricciones anteriores la respuesta es
negativa.

1843 - Everyone out of the Pool Dado un rango definido por (a,b)
buscar cuantos números en ese rango cumplen las siguientes restricciones
a<x<b y a<x+1 < b . Donde x es el número buscado en el rango.
El valor x debe poder expresarse como:
x = A∗(A+1)
2
Donde A pertenece a los números naturales.
Además x+1 debe poder expresarse como: x + 1 = B 2
Donde B pertenece a los números naturales.
Una posible solución es generar todos los números triangulares hasta 109
y almacenar aquellos que cuando se incrementan en uno son cuadrados per-
fectos. Luego para cada rango de (a,b) ver cuantos de los números hallados
cumplen con la restricción. La búsqueda se puede hacer con fuerza bruta
porque del rango de 0 a 109 solo hay 13 números que cumplen con las res-
tricciones iniciales del problema.

1049 - Sum Este ejercicio es un claro ejemplo de lo importante que es leer


y analizar bien los ejecicios antes de codificar una solución y eso lo demuestra
su sencillez y el bajo por ciento de aceptado. Resulta que dado un entero N
nos pide calcular la suma de todos los enteros entre 1 y N, asi de simple y el
valor absoluto de N no supera 104 . La solución es bien sencilla podemos hacer
una iteración de 1 a N y sumar todos los números o aplicar la sumatoria de
Gauss que permite calcular la suma de todos los números entre 1 y un N
N (N +1)
2
Entonces donde esta el petardo. Sencillo resulta que te va entrar un entero
cuyo valor absoluto no supera 104 por tanto en la entrada puede existir
números negativos.

2549 - SETNJA Se tiene una serie de segmentos (N<=30) que describen


una ruta, cada segemento se describe con un punto (X, Y) que da el desplaza-
miento con relación a la ultima posición absoluta. Dada la serie de segmentos
determinar la posición final relativa que propone la ruta con relación a la po-
sición inicial. Y la distancia mı́nima entre las posiciones final e inicial de
la ruta si removemos un segmento del listado inicial. Dicha distancia debe
expresarse con dos lugares despues de la coma.

93
FUERZA BRUTA

Para determinar la posición relativa con respecto a la inicial que propone


la ruta vamos asumir como punto de partida el (0;0) y realizar el siguiente
procedimiento:
Pf (xf ;yf )=(0;0)
Por cada punto que pertenece a los segmentos:
Pf xf +=punto.x
Pf yf +=punto.y
Imprimir Pf
Para hallar la menor distancia entre el punto de salida y llegada quitando
un único segmento. Vamos hacer lo siguiente:

Una matriz de puntos con una dimensión de nx n. Donde n es la cantidad


de segmento +1. La columna 0 se va rellenar con el punto (0;0)

Para cada fila desde 1 hasta n:


Si filai == puntoi :
matriz [filai ][columna]=matriz[filai ][columna-1]
Sino
matriz [filai ][columna]=matriz[filai ][columna-1]+puntoi

Luego recorro la columna n y me quedo con mı́nimo valor que genere


la siguiente expresión dist(Point(0;0), matriz[i][n]), donde dist devuelve la
distancia entre dos puntos pasados por argumentos.

3119 - Anders And The Matrix Una vez analizado el problema nos
podemos percatar que nos están pidiendo ver si con una serie de números
podemos formar una matrix cuadrada tal que la suma de los elementos que
integran cada columna sea igual a determinados valores, la suma de los ele-
mentos que integran cada fila sea igual a determinados valores y de igual
manera con la suma de los elementos que integran las diagonales. Como la
máxima dimensión de la matriz es 3 lo que son 9 elementos podemos perfec-
tamente permutar los ı́ndices de los valores (no lo valores porque no aseguran
que sean únicos) y ver si al menos con una permutación generada se puede
formar la matrix que cumpla con las condiciones impuesta. En caso de existir
dicha matrix la respuesta es Yes sino No.

94
Capı́tulo 4

Aritmética y Álgebra

4.1. Multplicación de matrices


En matemática, la multiplicación o producto de matrices es la operación
de composición efectuada entre dos matrices, o bien la multiplicación entre
una matriz y un escalar según unas determinadas reglas.
Al igual que la multiplicación aritmética, su definición es instrumental,
es decir, viene dada por un algoritmo capaz de efectuarla. El algoritmo para
la multiplicación matricial es diferente del que resuelve la multiplicación de
dos números. La diferencia principal es que la multiplicación de matrices no
cumple con la propiedad de conmutatividad.
El producto de matrices se define de una manera muy peculiar y hasta
caprichosa cuando no se conoce su origen. El origen proviene del papel de las
matrices como representaciones de aplicaciones lineales. Ası́ el producto de
matrices, como se define, proviene de la composición de aplicaciones lineales.
En este contexto, el tamaño de la matriz corresponde con las dimensiones de
los espacios vectoriales entre los cuales se establece la aplicación lineal. De
ese modo el producto de matrices, representa la composición de aplicaciones
lineales.
El producto no estará bien definido, ya que si la matriz A no tiene el
mismo número de columnas que filas la matriz B entonces no podremos
establecer en donde acaba la suma: si la acabamos en el mayor de éstos
números habrá sumandos que no están definidos ya que una de las matrices
no tendrá más entradas, mientras que si tomamos el menor habrá entradas
de alguna de las matrices que no se tomen en cuenta. Ası́ es necesario que A
tenga el mismo número de columnas que B de filas para que la matriz AB
exista.
Dadas dos matrices A y B, tales que el número de columnas de la matriz

95
ARITMÉTICA Y ÁLGEBRA

A es igual al número de filas de la matriz B; es decir:


A=(aij )m∗n y B=(bij )n∗p
La multiplicación de A por B, que se denota A.B , A x B, , AoB o
simplemente AB, el resultado del producto es una nueva matriz C:
C=AB=(aij )m∗p
Donde cada elemento cij está definido por:

La multiplicación de matrices es muy útil para la resolución de sistemas


de ecuaciones de muchas variables, dado que son muy cómodas para ser im-
plementadas mediante un computador. El cálculo numérico se basa en gran
parte de estas operaciones, al igual que poderosas aplicaciones tales como
MATLAB. También actualmente se utiliza mucho en el cálculo de micro-
arrays, en el área de bioinformática.
A continuación se muestra el algoritmo ası́ como su utilización el mismo
tiene una complejidad O(n*l*m) donde n son las filas de A, l las columnas y
filas de A y B respectivamente, mientras m son las columnas de B.

# include < stdio .h >


# include < iostream >
# include < algorithm >
using namespace std ;
typedef long long lld ;

inline lld ** MatrixMultiply ( lld ** a , lld ** b , int n , int


l , int m )
{
lld ** c = new lld *[ n ];
for ( int i =0; i < n ; i ++) c [ i ] = new lld [ m ];

for ( int i =0; i < n ; i ++)


{
for ( int j =0; j < m ; j ++)
{
c [ i ][ j ]=0;
for ( int k =0; k < l ; k ++)
{
c [ i ][ j ] += a [ i ][ k ]* b [ k ][ j ];
}
}

96
ARITMÉTICA Y ÁLGEBRA

}
return c ;
}

int main ()
{
lld ** matA ;
matA = new lld *[2];
for ( int i =0; i <2; i ++) matA [ i ] = new lld [3];
matA [0][0] = 1;
matA [0][1] = 2;
matA [0][2] = 3;
matA [1][0] = 4;
matA [1][1] = 5;
matA [1][2] = 6;

lld ** matB ;
matB = new lld *[3];
for ( int i =0; i <3; i ++) matB [ i ] = new lld [2];
matB [0][0] = 7;
matB [0][1] = 8;
matB [1][0] = 9;
matB [1][1] = 10;
matB [2][0] = 11;
matB [2][1] = 12;

lld ** matC = MatrixMultiply ( matA , matB , 2 , 3 , 2) ;


for ( int i =0; i <2; i ++)
{
for ( int j =0; j <2; j ++)
{
printf ( " %lld " , matC [ i ][ j ]) ;
}
printf ( " \ n " ) ;
}

return 0;
}

97
ARITMÉTICA Y ÁLGEBRA

4.2. Exponenciación binaria


La exponenciaión binaria es una técnica que permite generar cualquier
cantidad de potencia nth para multiplicaciones O (log N ) (en lugar de n
multiplicaciones en el método habitual).
Además, la técnica descrita aquı́ es aplicable a cualquier operación aso-
ciativa, no solo a la multiplicación de números. Recordar que la operación se
llama asociativa, si para alguna a, b, c se lleva a cabo: (a*b)*c= a*(b*c)
La generalización más obvia: los restos de un determinado módulo (obvia-
mente, la asociatividad se conserva). Lo siguiente en la ”popularidad.es una
generalización del producto matricial (es una asociatividad bien conocida).
Tenga en cuenta que para cualquier número de una identidad obvia facti-
ble de un número par (que se deduce de la asociatividad de la multiplicación):
an = (an/2 )2 = an/2 ∗ an/2
Es el principal método de exponenciación binaria. De hecho, incluso para
n Hemos demostrado cómo, después de realizar solo una operación de multi-
plicación, podemos reducir el problema a menos de la mitad de la potencia.
Queda por entender qué hacer si el grado de n es impar. Aquı́ lo que hacemos
es muy simple: ve en la medida n-1 Eso tendrá incluso:
an = an−1 ∗ a
Entonces, en realidad encontramos una fórmula recursiva: el grado de
n que vamos, si es igual a n/2 y de lo contrario, a n-1. Está claro que no
habrá más transiciones 2 log(n) antes de que lleguemos a n = 0 (basado en
la fórmula recursiva). Por lo tanto, tenemos un algoritmo que funciona para
multiplicaciones de O (log N ).
Una implementación recursiva como solución:

int binpow ( int a , int n )


{
if ( n == 0)
return 1;
if ( n % 2 == 1)
return binpow (a , n - 1) * a ;
else
{
int b = binpow (a , n / 2) ;
return b * b ;
}
}

Una implementacion no recursiva optimizada (la división por 2 se reem-


plaza por operaciones de bit):

98
ARITMÉTICA Y ÁLGEBRA

int binpow ( int a , int n ) {


int res = 1;
while ( n )
if ( n & 1) {
res * = a ;
--n ;
}
else {
a * = a;
n > > = 1;
}
return res ;
}

Esta implementación se puede simplificar aún más al observar que la cons-


trucción a en el cuadrado siempre, independientemente de si la condición
funcionó de manera impar o no:

int binpow ( int a , int n ) {


int res = 1;
while ( n ) {
if ( n &1)
res *= a ;
a *= a ;
n > >=1;
}
return res ;
}

Finalmente, se debe tener en cuenta que la exponenciación binaria se imple-


menta en Java, pero solo para una clase de BigInteger de aritmética larga (la
función pow de esta clase de trabajo está en el algoritmo de exponenciación
binaria).

4.3. Métodos numéricos


Constituyen un conjunto de algoritmos para la solución aproximada de
problemas matemáticos y que nos pueden resultar útiles en la resolución de
algunos problemas.

99
ARITMÉTICA Y ÁLGEBRA

4.3.1. Búsqueda ternaria


Se nos da una función f(x) que es unimodal en un intervalo [l;r]. Por
función unimodal, nos referimos a uno de los dos comportamientos de la
función:

La función aumenta estrictamente primero, alcanza un máximo (en un


punto o en un segmento), y luego disminuye estrictamente.

La función disminuye estrictamente primero, alcanza un mı́nimo y luego


aumenta estrictamente.

Para la explicación de este algoritmo vamos asumir una función del primer
tipo y nuestro problema en como encontrar el máximo f(x) para el intervalo
[l;r]. Una vez descrita la solución para este caso para el segundo caso la
solución es completamente simétrica.
Considere 2 puntos m1 y m2 en este intervalo:l < m1 < m2 < r. Eva-
luamos la función en m1 y m2, es decir, encontramos los valores de f(m1 ) y
f(m2 ). Ahora, tenemos una de tres opciones:

1. f(m1 ) < f(m2 ) El máximo deseado no se puede ubicar en el lado iz-


quierdo de m1 , es decir, en el intervalo [l; m1 ], ya que ambos puntos
m1 y m2 o solo m1 pertenecen al área donde aumenta la función. En
cualquier caso, esto significa que tenemos que buscar el máximo en el
segmento [m1 , r].

2. f(m1 ) > f (m2 ) Esta situación es simétrica a la anterior: el máximo no


se puede ubicar en el lado derecho de m2 , es decir, en el intervalo [m2 ;
r], y el espacio de búsqueda se reduce al segmento [l; m2 ].

3. f(m1 ) = f(m2 ) Podemos ver que ambos puntos pertenecen al área donde
se maximiza el valor de la función, o m1 está en el área de valores
crecientes y m2 está en el área de valores descendentes (aquı́ usamos
el rigor de la función aumentando / disminuyendo). Por lo tanto, el
espacio de búsqueda se reduce a [m1 ;m2 ]. Para simplificar el código,
este caso se puede combinar con cualquiera de los casos anteriores.

Por lo tanto, en función de la comparación de los valores en los dos puntos


internos, podemos reemplazar el intervalo actual [l; r] con un nuevo intervalo
más corto [l’; r’]. Aplicando repetidamente el procedimiento descrito al inter-
valo, podemos obtener un intervalo arbitrariamente corto. Eventualmente,
su longitud será menor que una cierta constante predefinida (precisión), y el
proceso se puede detener. Este es un método numérico, por lo que podemos

100
ARITMÉTICA Y ÁLGEBRA

suponer que después de eso la función alcanza su máximo en todos los pun-
tos del último intervalo [l; r]. Sin pérdida de generalidad, podemos tomar f(l)
como valor de retorno.
No existe una regla en la elección de los puntos m1 y m2 . Esta elección
definirá la tasa de convergencia y la precisión de la implementación. La forma
más común es elegir los puntos para que dividan el intervalo [l; r] en tres
partes iguales. Ası́, tenemos:

(r − l)
m1 = l +
3

(r − l)
m2 = r −
3
Si m1 y m2 se eligen para estar más cerca el uno del otro, la tasa de
convergencia aumentará ligeramente.
El análisis del tiempo de ejecucción viene dado por la siguiente expresión:

T (n) = T (2n/3) + 1 = Θ(log n)


Se puede entender de la siguiente manera: cada vez que se evalúa la fun-
ción en los puntos m1 y m2 , esencialmente ignoramos aproximadamente un
tercio del intervalo, ya sea el izquierdo o el derecho. Por lo tanto el tamaño del
espacio de búsqueda es 2n/3. del original. Aplicando el Teorema de Master,
obtenemos la estimación de complejidad deseada.

double ternarySearch ( double l , double r ) {


double eps = 1e -9; // Definimos el error de precision
while ( r - l > eps ) {
double m1 = l + ( r - l ) / 3;
double m2 = r - ( r - l ) / 3;
double f1 = f ( m1 ) ; // evaluamos la funcion para m1
double f2 = f ( m2 ) ; // evaluamos la funcion para m2
if ( f1 < f2 ) l = m1 ;
else r = m2 ;
}
return f ( l ) ; // retorno el maximo fr f ( x ) en [l , r ]
}

La lógica de funcionamiento parte del método númerico de bisección. Con


la diferencia que el método de bisección trabaja con el cambio de signo en la
evaluación de la función. Aquı́ eps es, de hecho, el error absoluto (sin tener
en cuenta los errores debidos al cálculo incorrecto de la función).

101
ARITMÉTICA Y ÁLGEBRA

En lugar del criterio r−l > eps, podemos seleccionar un número constante
de iteraciones como criterio de detención. El número de iteraciones debe
elegirse para garantizar la precisión requerida. Normalmente, en la mayorı́a
de los desafı́os de programación, el lı́mite de error es 10−6 y, por lo tanto,
son suficientes 200 a 300 iteraciones. Además, el número de iteraciones no
depende de los valores de l y r, por lo que el número de iteraciones establece
un error relativo requerido.
Ahora bien, si f(x) toma un parámetro entero, el intervalo [l; r] se vuelve
discreto. Como no impusimos ninguna restricción en la elección de los puntos
m1 y m2 , la exactitud del algoritmo no se ve afectada. m1 y m2 aún se pueden
elegir para dividir [l; r] en 3 partes aproximadamente iguales.
La diferencia se produce en el criterio de parada del algoritmo. La búsque-
da ternaria tendrá que detenerse cuando (r − l) < 3, porque en ese caso ya
no podemos seleccionar que m1 y m2 sean diferentes entre sı́, ası́ como de l
y r, y esto puede causar iteraciones infinitas. Una vez (r − l) < 3, el con-
junto restante de puntos candidatos (l, l + 1, ..., r) debe comprobarse para
encontrar el punto que produce el valor máximo f(x).

4.3.2. Newton-Raphson
Este es un método iterativo inventado por Isaac Newton alrededor de
1664. Sin embargo, a veces este método se denomina método de Raphson,
ya que Raphson inventó el mismo algoritmo unos años después de Newton,
pero su artı́culo se publicó mucho antes.
Su aplicación es la siguiente. Dada la siguiente ecuación:

f (x) = 0

Queremos resolver la ecuación, más precisamente, para encontrar una de


sus raı́ces (se supone que la raı́z existe). Se supone que f (x) es continuo y
diferenciable en un intervalo [a; b].
Los parámetros de entrada del algoritmo consisten no solo en la función
f (x) sino también en la aproximación inicial, algunos x0 , con los que comienza
el algoritmo.
Supongamos que ya hemos calculado xi , calcule xi+1 de la siguiente ma-
nera. Dibuje la tangente a la gráfica de la función f (x) en el punto x = xi , y
encuentre el punto de intersección de esta tangente con el eje x-axis. xi+1 se
establece igual a la coordenada x del punto encontrado, y repetimos todo el
proceso desde el principio.
No es difı́cil obtener la siguiente fórmula:

102
ARITMÉTICA Y ÁLGEBRA

f (xi )
xi+1 = xi −
f 0 (xi )
Es intuitivamente claro que si la función f (x) es ”buena”(suave), y xi
está lo suficientemente cerca de la raı́z, entonces xi+1 estará aún más cerca
de la raı́z deseada.
La tasa de convergencia es cuadrática, lo que, condicionalmente hablando,
significa que el número de dı́gitos exactos en el valor aproximado xi se duplica
con cada iteración. Uno de los ejemplos donde se puede aplicar este método
es a la hora de calcular la raı́z cuadrada de un número, entremos en detalle.

Si sustituimos f (x) = x, Luego, después de simplificar la expresión,
obtenemos: Aplicación para calcular la raı́z cuadrada.
n
xi + xi
xi+1 =
2
La primera variante tı́pica del problema es cuando se da un número ra-
cional n, y su raı́z debe calcularse con cierta precisión eps:

double sqrt_newton ( double n ) {


const double eps = 1E -15;
double x = 1;
for (;;) {
double nx = ( x + n / x ) / 2;
if ( abs ( x - nx ) < eps )
break ;
x = nx ;
}
return x ;
}

Otra variante común del problema es cuando necesitamos calcular la raı́z


entera (para la n dada, encuentre la x más grande de manera que x2 ≤ n).
Aquı́ es necesario cambiar ligeramente la condición de terminación del algo-
ritmo, ya que puede suceder que x comience a ”saltarçerca de la respuesta.
Por lo tanto, agregamos una condición que si el valor x ha disminuido en el
paso anterior, y trata de aumentar en el paso actual, entonces el algoritmo
debe detenerse.

int isqrt_newton ( int n ) {


int x =1;
bool decreased = false ;
for (;;) {

103
ARITMÉTICA Y ÁLGEBRA

int nx =( x + n / x ) > >1;


if ( x == nx || nx > x && decreased )
break ;
decreased = nx < x ;
x = nx ;
}
return x ;
}

Finalmente, se nos da la tercera variante, para el caso de la aritmética


bignum. Como el número n puede ser lo suficientemente grande, tiene sentido
prestar atención a la aproximación inicial. Obviamente, cuanto más cerca esté
de la raı́z, más rápido se logrará el resultado. Es lo suficientemente simple y
efectivo tomar la aproximación inicial como el número 2bits/2 , donde bits es
el número de bits en el número n. Aquı́ está el código de Java que demuestra
esta variante:

public static BigInteger isqrtNewton ( BigInteger n ) {


BigInteger a = BigInteger . ONE . shiftLeft ( n . bitLength ()
/2) ;
boolean p_dec = false ;
for (;;) {
BigInteger b = n . divide ( a ) . add ( a ) . shiftRight (1) ;
if ( a . compareTo ( b ) == 0 || a . compareTo ( b ) < 0 &&
p_dec )
break ;
p_dec = a . compareTo ( b ) > 0;
a = b;
}
return a ;
}

Por ejemplo, este código se ejecuta en 60 milisegundos para n = 101000 , y


si eliminamos la selección mejorada de la aproximación inicial (comenzando
con 1), entonces se ejecutará en aproximadamente 120 milisegundos.
También puede ser usado para encontrar el máximo o mı́nimo de una
función, encontrando los ceros de su primera derivada.

4.4. Problema de Flavio Josefo


El problema de Flavio Josefo es un problema teórico que se encuentra en
matemática y ciencias de la computación. El nombre hace referencia a Flavio

104
ARITMÉTICA Y ÁLGEBRA

Josefo, un historiador judı́o que vivió en el siglo I. Según lo que cuenta Josefo,
él y 40 soldados camaradas se encontraban atrapados en una cueva, rodeados
de romanos. Prefirieron suicidarse antes que ser capturados y decidieron que
echarı́an a suertes quién mataba a quién. Los últimos que quedaron fueron él
y otro hombre. Entonces convenció al otro hombre que debı́an entregarse a
los romanos en lugar de matarse entre ellos. Josefo atribuyó su supervivencia
a la suerte o a la Providencia.
El problema plantea lo siguiente:
Hay n personas paradas en un cı́rculo esperando a ser ejecutadas. Después
de que ejecutan a la primera persona, evitan a k-1 personas y la persona
número k es ejecutada. Entonces nuevamente, evitan a k-1 personas y la
persona número k es ejecutada. La eliminación continúa alrededor del cı́rculo
(que se hace cada vez más pequeño a medida que más personas son eliminadas
del mismo) hasta que sólo queda la última, que es liberada.
El objetivo es escoger el lugar inicial en el cı́rculo para sobrevivir (es el
último que queda), dados n y k.
Este problema se puede resolver modelando el procedimiento por fuerza
bruta pero su complejidad serı́a O(n2 ). Usando en un Range Tree quizas po-
damos implementar una solución con una complejidad de O(n log n). Veamos
si podemos encontrar alguna solución un poco más eficiente.
Primero vamos a intentar encontrar un patrón en la respuesta para el
problema Jn,k .
Usando fuerza bruta podemos modelar la situación como se muestra en
la siguiente tabla.

n\k 1 2 3 4 5 6 7 8 9 10
1 1 1 1 1 1 1 1 1 1 1
2 2 1 2 1 2 1 2 1 2 1
3 3 3 2 2 1 1 3 3 2 2
4 4 1 1 2 2 3 2 3 3 4
5 5 3 4 1 2 4 4 1 2 4
6 6 5 1 5 1 4 5 3 5 2
7 7 7 4 2 6 3 5 4 7 5
8 8 1 7 6 3 1 4 4 8 7
9 9 3 1 1 8 7 2 3 8 8
10 10 5 4 5 3 3 9 1 7 8
En esta tabla podemos ver no tan claramente (tomesé su tiempo, si quiere
no tenga pena saque papel y lápiz ) el siguiente patrón:

Jn,k = (J(n−1),k + k − 1) mód n + 1

105
ARITMÉTICA Y ÁLGEBRA

J1,k = 1
Donde se toma como primera posición el valor 1 pero si tomará el la
posición 0 como la primera nos puede quedar más simple la primera expresión:

Jn,k = (J(n−1),k + k) mód n


Entonces hemos encontrado una solución al problema con una compleji-
dad O(n).
Una simple implementación recursiva asumiento que se empieza en 1 el
conteo:

int josephus ( int n , int k ) {


return n > 1 ? ( joseph (n -1 , k ) + k - 1) % n + 1 : 1;
}

Otra implementación no recursiva:

int josephus ( int n , int k ) {


int res = 0;
for ( int i = 1; i <= n ; ++ i )
res = ( res + k ) % i ;
return res + 1;
}

Esta fórmula puede ser determinada de forma analı́tica. Nuevamente asu-


mimos el conteo a partir de la posición 0. Después de matar a la primera per-
sona nos queda n − 1 personas a la izqierda. Y cuando repitamos el entonces
comenzaremos en la persona que inicialmente estaba en la posición k mód m.
J(n−1),k y de igual forma tendriamos las respuesta de las personas que están
en el resto del cı́rculo si comenzamos contando en 0 pero como estamos en k
tenemos que Jn,k = (J(n−1),k + k) mód n

4.5. Máximo común divisor


En matemática, se define el máximo común divisor (MCD) de dos o más
números enteros al mayor número entero que los divide sin dejar resto.
Si a y b son números enteros distintos de cero y si el número c es de
modo que c—a y a su vez c—b, a este número c se denomina divisor común
de los números a y b. Obsérvese que dos números enteros cualesquiera tienen
divisores comunes. Cuando existen, únicamente, como divisores comunes 1 y
-1 de los números a y b, estos se llaman primos entre sı́.

106
ARITMÉTICA Y ÁLGEBRA

Un número entero d se llama máximo común divisor (MCD) de los núme-


ros a y b cuando:

1. d es divisor común de los números a y b

2. d es divisible por cualquier otro divisor común de los números a y b.

Los tres métodos más utilizados para el cálculo del máximo común divisor
de dos números son:

Por descomposición en factores primos El máximo común divisor de


dos números puede calcularse determinando la descomposición en factores
primos de los dos números y tomando los factores comunes elevados a la
menor potencia, el producto de los cuales será el MCD. En la práctica, este
método solo es operativo para números pequeños tomando en general dema-
siado tiempo calcular la descomposición en factores primos de dos números
cualquiera.

Usando el algoritmo de Euclides Un método más eficiente es el algo-


ritmo de Euclides, que utiliza el algoritmo de la división junto al hecho que
el MCD de dos números también divide al resto obtenido de dividir el mayor
entre el más pequeño.

Usando el mı́nimo común múltiplo El máximo común divisor también


puede ser calculado usando el mı́nimo común múltiplo. Si a y b son distintos
de cero, entonces el máximo común divisor de a y b se obtiene mediante la
siguiente fórmula, que involucra el mı́nimo común múltiplo (mcm) de a y b.
a∗b
mcd(a, b) = M CM (a,b)
El máximo común divisor de tres o más números se puede definir usando
recursivamente:
mcd(a, b) = mcd(a, mcd(b, c))
Algunas de las propiedades del mcd son:

1. Si mcd(a, b) = d entonces mcd( ad , db ) = 1

2. Si m es un entero, mcd(m ∗ a, m ∗ b) = |m| ∗ mcd(a, b)

3. Si p es un número primo, entonces mcd(p, m) = p o bien mcd(p, m) = 1

4. Si m = n ∗ q + r, entonces mcd(m, n) = mcd(n, r)

5. Si m = pa1 ak b1 bk
1 ...pk y n = p1 ...pk , ai , bi

107
ARITMÉTICA Y ÁLGEBRA

El siguiente algoritmo calcula el mcd de dos números usando el algoritmo


de Euclides.

# include < stdio .h >


# include < vector >
# include < algorithm >

using namespace std ;

int GCD ( int a , int b )


{
while ( b > 0)
{
a = a %b ;
a ^= b ;
b ^= a ;
a ^= b ;
}
return a ;
}

int main ()
{
int gcd = GCD (3 ,6) ; // 3
return 0;
}

4.6. Algoritmo Euclidiano Extendido


Si bien el algoritmo euclidiano calcula solo el máximo divisor común
(MCD) de dos enteros a y b, la versión extendida también encuentra una
manera de representar el MCD en términos de a y b, es decir, coeficientes x
e y para los cuales:

a · x + b · y = gcd(a, b)
Los cambios en el algoritmo original son muy simples. Todo lo que tene-
mos que hacer es descubrir cómo cambian los coeficientes x e y durante la
transición de (a, b) a (b mod a, a).
Supongamos que encontramos los coeficientes (x1 , y1 ) para (b mod a, a):

(b mod a) · x1 + a · y1 = g

108
ARITMÉTICA Y ÁLGEBRA

y queremos encontrar el par (x, y) para (a, b):

a·x+b·y =g
Podemos representar b mod a es:
 
b
b mod a = b − ·a
a
Al sustituir esta expresión en la ecuación del coeficiente de (x1 , y1 ) se
obtiene que:
   
b
g = (b mod a) · x1 + a · y1 = b − · a · x1 + a · y1
a
y después de reorganizar los términos:
   
b
g = b · x1 + a · y1 − · x1
a
Encontramos los valores x e y
(
x = y1 − ab · x1
 

y = x1

4.6.1. Implementación

int gcd ( int a , int b , int & x , int & y ) {


if ( a == 0) {
x = 0;
y = 1;
return b ;
}
int x1 , y1 ;
int d = gcd ( b % a , a , x1 , y1 ) ;
x = y1 - ( b / a ) * x1 ;
y = x1 ;
return d ;
}

La función recursiva anterior devuelve el MCD y los valores de los coefi-


cientes x e y (que se pasan por referencia a la función).
El caso base para la recursión es a = 0, cuando el MCD es igual a b,
entonces los coeficientes x e y son 0 y 1, respectivamente. En todos los demás

109
ARITMÉTICA Y ÁLGEBRA

casos, las fórmulas anteriores se utilizan para volver a calcular los coeficientes
en cada iteración.
Esta implementación del algoritmo euclidiano extendido también produce
resultados correctos para enteros negativos.

4.7. Ecuación lineal de diofantina


Una ecuación lineal de diofantina (en dos variables) es una ecuación de
la forma general:

ax + by = c
Donde a, b, c son enteros y x, y son enteros desconocidos.
En este artı́culo, consideramos varias situaciones que pueden ser proble-
mas clásicos en estas ecuaciones:

Encontrar una solución

Encontrar todas las soluciones

Encontrar el número de soluciones y las soluciones en sı́ mismas en un


intervalo dado.

Encontrar una solución con un valor mı́nimo de x + y

Caso especial: Un caso especial que debe ser atendido es cuando a =


b = 0. Es fácil ver que no tenemos soluciones o tenemos infinitas soluciones,
dependiendo de si c = 0 o no. En el resto de este artı́culo, ignoraremos este
caso.

4.7.1. Encontrar una solución


Para encontrar una solución de la ecuación de Diophantine con 2 incógni-
tas, puede usar el algoritmo Euclidiano extendido. Primero, suponga que a y
b no son negativos. Cuando aplicamos el algoritmo euclidiano extendido para
a y b, podemos encontrar su mayor divisor común g y 2 números xg e yg de
manera que:

axg + byg = g
Si c es divisible por g = gcd(a, b), entonces la ecuación de diofantina dada
tiene una solución, de lo contrario no tiene ninguna solución. La prueba es

110
ARITMÉTICA Y ÁLGEBRA

directa: una combinación lineal de dos números es divisible por su divisor


común.
Ahora se supone que c es divisible por g, entonces tenemos:
c c
a · xg · + b · yg · = c
g g
Por lo tanto, una de las soluciones de la ecuación diofantina es:
c
x0 = xg · ,
g
c
y0 = yg · .
g
La idea anterior todavı́a funciona cuando a o b o ambos son negativos.
Solo necesitamos cambiar el signo de x0 e y0 cuando sea necesario.
Finalmente, podemos implementar esta idea de la siguiente manera (tenga
en cuenta que este código no considera el caso a = b = 0):

int gcd ( int a , int b , int &x , int & y ) {


if ( a ==0) {
x =0; y =1;
return b ;
}
int x1 , y1 ;
int d = gcd ( b %a ,a , x1 , y1 ) ;
x = y1 -( b / a ) * x1 ;
y = x1 ;
return d ;
}

bool findAnySolution ( int a , int b , int c , int & x0 , int &


y0 , int & g ) {
g = gcd ( abs ( a ) , abs ( b ) , x0 , y0 ) ;
if ( c %g ) {
return false ;
}

x0 *= c / g ;
y0 *= c / g ;
if (a <0) x0 = - x0 ;
if (b <0) y0 = - y0 ;
return true ;
}

111
ARITMÉTICA Y ÁLGEBRA

4.7.2. Encontrar el número de soluciones y las solucio-


nes en un intervalo dado
De la sección anterior, debe quedar claro que si no imponemos restric-
ciones a las soluciones, habrı́a un número infinito de ellas. Entonces, en esta
sección, agregamos algunas restricciones en el intervalo de x e y, e intentare-
mos contar y enumerar todas las soluciones.
Que haya dos intervalos: [minx ; maxx ] y [miny ; maxy ] y digamos que solo
queremos encontrar las soluciones en estos dos intervalos.
Tenga en cuenta que si a o b es 0, entonces el problema solo tiene una
solución. No consideramos este caso aquı́.
Primero, podemos encontrar una solución que tenga un valor mı́nimo de
x, tal que x ≥ minx . Para hacer esto, primero encontramos cualquier solución
de la ecuación diofantina. Luego, cambiamos esta solución para obtener x ≥
minx (usando lo que sabemos sobre el conjunto de todas las soluciones en la
sección anterior). Esto se puede hacer en O(1). Denote este valor mı́nimo de
x por lx1 .
Del mismo modo, podemos encontrar el valor máximo de x que satisfaga
x ≤ maxx . Denote este valor máximo de x por rx1 .
Del mismo modo, podemos encontrar el valor mı́nimo de y (y ≥ miny )
y los valores máximos de y (y ≤ maxy ). Denote los valores correspondientes
de x por lx2 y rx2 .
La solución final es todas las soluciones con x en la intersección de [lx1 , rx1 ]
y [lx2 , rx2 ]. Deje denotar esta intersección por [lx , rx ].
El siguiente es el código que implementa esta idea. Observe que dividimos
a y b al principio por g. Como la ecuación ax + by = c es equivalente a la
ecuación ag x + gb y = gc , podemos usar esta en su lugar y tener gcd( ag , gb ) = 1
lo que simplifica las fórmulas.

void shiftSolution ( int &x , int &y , int a , int b , int cnt
) {
x += cnt * b ;
y -= cnt * a ;
}

int findAllSolutions ( int a , int b , int c , int minx , int


maxx , int miny , int maxy ) {
int x , y , g ;
if (! findAnySolution (a ,b ,c ,x ,y , g ) )
return 0;
a /= g ;
b /= g ;

112
ARITMÉTICA Y ÁLGEBRA

int sign_a =a >0?+1: -1;


int sign_b =b >0?+1: -1;

shiftSolution (x ,y ,a ,b ,( minx - x ) / b ) ;
if (x < minx )
shiftSolution (x ,y ,a ,b , sign_b ) ;
if (x > maxx )
return 0;
int lx1 = x ;

shiftSolution (x ,y ,a ,b ,( maxx - x ) / b ) ;
if (x > maxx )
shiftSolution (x ,y ,a ,b , - sign_b ) ;
int rx1 = x ;

shiftSolution (x ,y ,a ,b , -( miny - y ) / a ) ;
if (y < miny )
shiftSolution (x ,y ,a ,b , - sign_a ) ;
if (y > maxy )
return 0;
int lx2 = x ;

shiftSolution (x ,y ,a ,b , -( maxy - y ) / a ) ;
if (y > maxy )
shiftSolution (x ,y ,a ,b , sign_a ) ;
int rx2 = x ;

if ( lx2 > rx2 )


swap ( lx2 , rx2 ) ;
int lx = max ( lx1 , lx2 ) ;
int rx = min ( rx1 , rx2 ) ;

if ( lx > rx )
return 0;
return ( rx - lx ) / abs ( b ) +1;
}

Una vez que tenemos lx y rx , también es simple enumerar todas las solu-
ciones. Solo necesita iterar a través de x = lx +k · gb para todos los k ≥ 0 hasta
x = rx , y encontrar los valores de y correspondientes utilizando la ecuación
ax + by = c.

113
ARITMÉTICA Y ÁLGEBRA

4.7.3. Encuentre la solución con un valor mı́nimo de x


+y
Aquı́, x e y también deben tener alguna restricción, de lo contrario, la
respuesta puede convertirse en infinito negativo.
Finalmente, use el conocimiento del conjunto de todas las soluciones para
encontrar el mı́nimo:

b
x0 = x + k · ,
g
a
y0 = y − k · .
g
Tenga en cuenta que x + y cambian de la siguiente manera:
 
0 0 b a b−a
x +y =x+y+k· − =x+y+k·
g g g

Si a < b, necesitamos seleccionar el valor más pequeño posible de k. Si


a > b,, debemos seleccionar el mayor valor posible de k. Si a = b, toda
solución tendrá la misma suma x + y.

4.8. Inverso Multiplicativo Modular


Un inverso multiplicativo modular de un entero a es un entero x tal que
a · x es congruente con 1 modular algún módulo m. Para escribirlo de manera
formal: queremos encontrar un número entero x para que

a · x ≡ 1 mod m

También denotaremos x simplemente con a−1 .


Debemos tener en cuenta que el inverso modular no siempre existe. Por
ejemplo, sea m = 4, a = 2. Al verificar todos los valores posibles, el módulo
m deberı́a quedar claro que no podemos encontrar un a−1 que satisfaga la
ecuación anterior. Se puede demostrar que el inverso modular existe si y solo
si a y m son relativamente primos (es decir, gcd(a, m) = 1).
En este sección, presentamos dos métodos para encontrar el inverso mo-
dular en caso de que exista, y un método para encontrar el inverso modular
para todos los números en tiempo lineal.

114
ARITMÉTICA Y ÁLGEBRA

4.8.1. Encontrar el inverso modular usando el algorit-


mo euclidiano extendido
Considere la siguiente ecuación (con x e y desconocidas):

a·x+m·y =1

Esta es una ecuación lineal de diofantina en dos variables. Como se conoce


de esta ecuación, cuando gcd gcd(a, m) = 1, la ecuación tiene una solución
que se puede encontrar utilizando el algoritmo euclidiano extendido. Tenga en
cuenta que gcd(a, m) = 1 también es la condición para que exista el inverso
modular.
Ahora, si tomamos el módulo m de ambos miembros, podemos deshacer-
nos de m · y, y la ecuación se convierte en:

a · x ≡ 1 mod m

Por lo tanto, el inverso modular de a es x.


La implementación es la siguiente:

int x , y ;
int g = ex te nd ed _eu cl id ea n (a , m , x , y ) ;
if ( g !=1) {
cout << " No solution ! " ;
} else {
x = (x % m + m) % m;
cout << x << endl ;
}

Tenga en cuenta que modificamos x. La x resultante del algoritmo eu-


clidiano extendido puede ser negativa, por lo que x % m también podrı́a ser
negativa, y primero tenemos que agregar m para que sea positiva.

4.8.2. Encontrar el inverso modular usando exponen-


ciación binaria
Otro método para encontrar inversa modular es usar el teorema de Eu-
ler, que establece que la siguiente congruencia es verdadera si a y m son
relativamente primos:

aφ(m) ≡ 1 mód m

115
ARITMÉTICA Y ÁLGEBRA

φ es la función Totient de Euler. Nuevamente, tenga en cuenta que a y m


siendo primo relativo también era la condición para que existiera el inverso
modular.
Si m es un número primo, esto se simplifica al pequeño teorema de Fermat:

am−1 ≡ 1 mód m
Multiplicamos ambos lados de las ecuaciones anteriores por a−1 , y obte-
nemos:

Para un módulo arbitrario (pero coprime) m: m: aφ(m)−1 ≡ a−1 mód m

Para un módulo primo m: am−2 ≡ a−1 mód m

A partir de estos resultados, podemos encontrar fácilmente el inverso


modular utilizando el algoritmo de exponenciación binaria, que funciona en
tiempo O (log m).
Aunque este método es más fácil de entender que el método descrito en
el párrafo anterior, en el caso de que m no sea un número primo, necesita-
mos calcular la función phi de Euler, que implica la factorización de m, que
podrı́a ser muy difı́cil. Si se conoce la factorización prima de m, entonces la
complejidad de este método es O (log m).

4.8.3. Encontrar el inverso modular para cada número


módulo m
El problema es el siguiente: queremos calcular el inverso modular pa-
ra cada número en el rango [1, m − 1]. Aplicando los algoritmos descritos
en las secciones anteriores, podemos obtener una solución con complejidad
O(m log m). Aquı́ presentamos un mejor algoritmo con complejidad O(m).
Sin embargo, para este algoritmo especı́fico requerimos que el módulo m sea
primo.
Denotamos por inv[i] el inverso modular de i. Entonces para i > 1 la
siguiente ecuación es válida:
jmk
inv[i] = − · inv[m mód i] mód m
i
Por lo tanto, la implementación es muy simple:

inv [1]=1;
for ( int i =2; i < m ;++ i )
inv [ i ]=( m -( m / i ) * inv [ m %i] %m ) %m ;

116
ARITMÉTICA Y ÁLGEBRA

Prueba

Tenemos:

jmk
m mód i = m − ·i
i

Tomando ambos lados, el módulo m produce:

jmk
m mód i ≡ − · i mód m
i

Multiplique ambos lados por i−1 · (m mód i)−1

jmk
−1 −1
(m mód i) · i · (m mód i) ≡− · i · i−1 · (m mód i)−1 mód m,
i

que se simplifica a:

jmk
−1
i ≡− · (m mód i)−1 mód m,
i

El uso del inverso multiplicativo modular en los problemas es muy común


sobre todo donde para la solución del problema se parte de una función don-
de los resultados crecen de forma casi exponencial y entre las operaciones de
dicha función esta la división por lo que se hace necesario modular los resul-
tados para que no se produzca desbordamiento de los datos en las variables.

117
ARITMÉTICA Y ÁLGEBRA

118
ARITMÉTICA Y ÁLGEBRA

4.9. Mı́nimo común múltiplo


4.10. Sucesiones
4.10.1. Sucesiones aritméticas
4.10.2. Sucesiones geométricas
4.10.3. Sucesiones telescópicas
4.10.4. Técnica de interpolación
4.10.5. Método de Langrange
4.10.6. Método de Newton
4.10.7. Tabla de diferencias dividida
4.10.8. Sucesiones de recurrencias

4.11. Matrix Exponentiation


4.12. Exponentiation by Squaring
4.13. Assignment Problem (Galien Law Party)
4.14. Convex on the maximum method (gol-
den section)
4.15. Two-stage simplex method
4.16. Unconstrained nonlinear optimization met-
hod (Quasi-Newton)
4.17. Zero of a monotone function (dichoto-
mous)
4.18. Fast Fourier transform
4.19. Matrix 119

4.20. LU decomposition
4.21. Eigenvalue vector-specific
ARITMÉTICA Y ÁLGEBRA

(N ∗ (N ∗ N + 1))/2

3320 - Sum of Numbers I Lo primero es precisar el hecho de cuáles son


los números que se están sumando. La cantidad de divisores de un número es
par cuando no se trata de un cuadrado perfecto. Simplifiquemos el problema
considerando resuelto el caso N=1. Si denotamos por S M esta solución, el
caso general se resuelve mediante la diferencia S M – S (N-1), tomando S 0
=0. Para N=1, el problema se resuelve mediantes la diferencia de la suma
de todos los números desde 1 hasta M menos la suma de los cuadrados de
los números desde 1 hasta b[Sqrt(M)], donde [Sqrt(M)] es la parte entera de
raı́z cuadrada de M.

1115 - Sequence Sum Possibilities Un número Z se puede expresar


como suma de números consecutivos cuando cumple la condición siguiente:
Z = n ∗ a + ((n − 1) ∗ n)/2
Donde Z es el número que se desea saber si se puede expresar como suma
de enteros positivos, n la cantidad de números consecutivos para expresar
la suma, a es el primer número y menor de la secuencia de los sumandos.
Sabiendo eso solo basta comprobar para un Z todo los n que cumpla los
siguientes criterios:
Z >= ((n − 1) ∗ n)/2
(Z − (((n − 1) ∗ n)/2)) %n == 0
(Z − (((n − 1) ∗ n)/2))! = 0

3757 - I Love Divisors Es un ejercicio bastante sencillo solo nos pide


hallar la cantidad de divisores impares y pares de un número. La primera
idea puede recorrer desde uno hasta el número y ver cuales son los números
que lo dividen con resto cero y clasificarlos como pares e impares. Esta idea
tiene de malo el rango del posible número de entrada en cual es muy grande lo
que harı́a lento nuestro algoritmo para el tiempo permitido. Pero sabemos que
para A=B*C√ sabemos que B es un valor que cumple√ la condición de ser menor
o igual a A y C es siempre mayor o igual que A. Por lo que esta propiedad
nos puede servir√ para optimizar la idea originar. Ahora solo debemos iterar
desde 2 hasta N y cada vez que halle un B calcular el C correspondiente
y luego la paridad de B y C. Tenga cuenta que esta modificación no analiza
cuando B=1 y C=número, pero lo desbes tener en cuenta para la solución
final.

3823 - The Birth of the Islanders Es claro que la solución del problema
radica en la construcción de un sucesión muy similar a la de los números

120
ARITMÉTICA Y ÁLGEBRA

de Fibonacci, casi identica dirı́a yo. Uno de los detalles del problema es la
cantidad de elementos pertenecientes que debemos calcular, los cuales son
números con una cantidad de dı́gitos significativa por lo que recomiendo
utilizar un lenguaje que soporte numeros grandes como Java o Python.

3801 - Amelia and Rabbit Island Para saber la cantidad de conejos


solo debemos iterar desde 3 hasta N incluyendo los extremos. Conociendo
que en cada iteración C = (A*B) %MOD (donde MOD es igual 1000000) y
que para la próxima iteracción A=B y B=C. En cuanto a la cantidad de
semanas con la sencilla ecuación 4+(N-2)*3 es suficiente.

3797 - Law in Transylvania Una vez leido y analizado el problema nos


podemos percatar que dada la longitud del nombre personaje. Si esta es par
significa que el personaje no es habitantes de Transylvania. En caso de que
la longitud del nombre(N) fuera impar la cantidad de hijos que podrá tener
este personaje será la sumatoria de todos los números impares desde 1 hasta
N. Como todos sabemos la suma de los primeros k números impares es igual
k2 . Para saber cuantos números impares hay desde 1 hasta N siendo N impar
es (N+1)/2.

3763 - Playing with series Una vez leido el problema nos damos cuenta
que no estan dificil hallarle solución porque la cantidad de elementos que se
deben calcular de la serie es pequeña (solo los primeros 1000) ası́ que se puede
precalcular. La única dificultad radica en la rapidez con que puede crecer los
números que componen la serie pero eso soluble si se utiliza como lenguaje
de programación Java y si su clase BigInteger. Una vez hecho solo debemos
modular la respuesta.

3693 - Average El problema nos explica un proceso iterativo que comienza


con dos números A y B los cuales son distintos. Nos piden hallar el menor
valor de B dado un A y la cantidad de iteraciones que se debe ejecutar el
proceso. Se debe garantizar que B este en el rango de 1 a 108 sino la respuesta
es ”No existe”. El problema es sencillo si tenemos en cuenta que el promedio
entre dos números equidista de estos. Entonces será entero en caso que la
diferencia entre estos sea par. Ahora para que se pueda realizar n iteraciones
se necesita una diferencia entre A y B igual 2n . Por lo que primero se va
verificar si A-2n esta en el rango para B sino se verifica para A+2n y si
tampoco cumpliera con el rango entonces la respuesta serı́a ”No existe”. La
solución lo hice con Java para usar BigInteger ya que los números pueden ser
bien grandes.

121
ARITMÉTICA Y ÁLGEBRA

1661 - Brick Wall Patterns El problema nos pide hallar con cuantos
patrones de construcciónes se puede un muro de longitud n con dos unidades
de altura utilizando piezas cuyas dimensiones son 2x1. Cuando se analiza
detenidamente el problema nos podemos dar cuenta que para muros de lon-
gitud 1 y 0 la cantidad de patrones de construcciones que se pueden utilizar
es uno para construir muros con dichas longitudes. Lo otro que podemos
analizar que para un muro de longitud k la cantidad de patrones para cons-
truirlo va ser igual a la suma de la cantidad de patrones con los que se puede
construir un muro de longitud k-1 y un muro de longitud k-2. Dicho esto ya
estamos frente una sucesión bien famosa como son los números de Fibonacci.
La solución del problema es dado un n calcular el enesimo número Fibonacci,
siempre que n n sea distinto de cero en este caso debe detenerse el problema.
Se puede precalcular los Fibonacci y guardarlos en un arreglo.

1640 - Super 008 for Kids El problema nos determinar cuantos grupos de
exactamente K niños se puede formar con G niñas y B niños. Como restricción
que en el grupo debe tener al menos una niña y cuatro niños. Para solucionar
el problema debemos probar todas la variantes de conformación de un grupo
de K niños que cumplan con las restricciones de la cantidades mı́nimas de
niños y niñas en el grupo. Para estos vamos a:
Para b desde 4 hasta el min(B,K-1)
g=K-b
Si b>=4 y g>=1 y b<=B y g<=G y el la variante(b,g) no ha sido
analizada
Marco la variante (b,g) como analizada
solucion = solucion + CB,b *CG,g
Luego de forma similar:
Para g desde 1 hasta el min(G,K-4)
b=K-g
Si b>=4 y g>=1 y b<=B y g<=G y el la variante(b,g) no ha sido
analizada
Marco la variante (b,g) como analizada
solucion = solucion + CB,b *CG,g
Donde la variable solución su valor es cero inicialmente. Mientras CB,b y
CG,g son las combinaciones de escoger b niños de B disponibles y g niñas de
G disponibles. Se puede precalcular todas las posibles combinaciones que se
pueden utilizar con la utilización del triángulo de Pascal en una matriz de
dimensión 35 tanto en las filas como en columnas. Como el resultado puede
ser bastante grande recomiendo utilizar Java para trabajar con BigInteger.
Para mi solución utilice este lenguaje.

122
ARITMÉTICA Y ÁLGEBRA

1681 - Solving Polynomials Un ejercicio bien sencillo dado los coeficien-


tes a, b y c de una ecuación de la forma ax2 +bx+c, determinar si tiene al
menos una solución. Con solo calcular el valor del discrimante (b2 -4ac) y ver
que el valor sea mayor o igual a cero la respuesta es YES sino será NO.

1811 - Weird Numbers El problema nos pide realizar dada las posibles
entradas realizar tres posibles operaciones:

Si la entrada tiene el siguiente formato:


to <basenegativa> <value>
<basenegativa>: Base negativa a la cual se desea convertir el valor
decimal expresado en <value>
<value>: Representación númerica de un valor en base decimal.
Se debe convertir <value> a su representación en la base expresada en
<basenegativa>
Si la entrada tiene el siguiente formato:
from <basenegativa> <value>
<basenegativa>: Base negativa en el cual esta expresado el valor
de <value>
<value>: Representación númerica de un valor expresado en la
base númerica <basenegativa>.
Se debe convertir el valor <value> que esta expresado en <basenegativa>
a su representación en base decimal
Si la entrada tiene el siguiente formato:
end
Termina el programa.

Para responder la segunda operación es bastante sencilla solo se debe


seguir el procedimiento habitual para convertir de una base a otra. Para
responder la primera operación solo debemos leer el apartado Sistemas de
numeración de este manual para resolver. Debido al rango planteado en el
problema se puede hacer en C++.

1933 - Og Una vez leido el ejercicio es fácil darse cuenta que solo debemos
hacer un programa que lea dos números para cada lı́nea hasta que ambos
números sean cero y por cada entrada de números solo debemos imprimir el
resultado de la suma de los dos numeros.

123
ARITMÉTICA Y ÁLGEBRA

4017 - Is a triangle El ejercicio nos pide chequear si dada una cantidad


de monedas se puede formar una triángulo equilatero con ellas donde en
la primera fila se pone una moneda, en la segunda fila dos modenas y ası́
sucesivamente de forma tal que en la enésima fila existan exactamente N
monedas. En otras palabras solo debemos comprobar si para cada N que nos
entran N es un número triangular. Para chequear si N es un número de este
tipo solo debemos combrobar√lo siguiente A ∗ (A + 1) = N ∗ 2 donde A es la
parte entera de la operación 2 N ∗ 2. Si se cumple entonces la respuesta será
A sino será -1.

4080 - Empty Water Tank El problema plantea que existe un tanque con
una capacidad C que necesita llenar en al menos T minutos con un sistema
de bombeo que es capaz de verter en el primer minuto una cantidad F, en el
segundo minuto F+1 , en el tercer minuto F+2 y ası́ sucesivamente en cada
minuto que pase el flujo de litro que se bombea aumenta en uno. Y se desea
hallar el F tal que el tanque se llene en al menos T minutos. Analizamos el
problema debemos dar solución a la siguiente expresión:

C <= F + (F + 1) + (F + 2) + (F + 1) + ... + (F + T − 1)
Simplificando la expresión nos queda:
T −1
X
C <= T F + i
i=1

Que es los mismo que decir por sumatoria de Gauss

T (T − 1)
C <= T F
2
Organizando la inecuación nos queda que:

T (T − 1)
TF >= C
2
Despejando F nos queda que :
T (T −1)
2
F >=
T
Una vez resuelta esta inecuación nos puede quedar un número decimal e
incluso negativo pero el problema nos dice que el flujo de bombeo inicial es
positivo y entero por lo que tenemos que reacomodar la respuesta según sea
el caso. Si nos da un flujo de bombeo inicial negativo entonces la respuesta
es 1. Si el flujo inicial sea positivo decimal debemos chequear si com el mayor

124
ARITMÉTICA Y ÁLGEBRA

entero menor que la respuesta se cumple la inecuación si se cumple esa es


la respuesta sino la respuesta el menor entero mayor que la solución de la
inecuación.

4130 - Petter and Apples Se quiere comprar w manzanas y se sabe que


la primera cuesta k dolares, la segunda 2k, la tercera 3k, ası́ hasta que la
manzana cuesta wk. Se tiene además n dolares para comprar las w manza-
nas. Hallar cuantos dolares tiene que pedir a sus amigos para comprar las
manzanas. Bien haciendo el análisis matemático nos queda lo siguiente:

S = (k + 2k + 3k + ... + wk) − n

= k(1 + 2 + 3 + ...w) − n

w(w + 1)
=k −n
2
Quedando la solución de la siguiente manera:

w(w + 1)
S = max(0, k − n)
2
Esto queda de esta manera porque puede darse el caso que la cantidad
de dolares inicial (n) sea suficiente para pagar las manzanas y no tenga que
pedir dinero.

125
ARITMÉTICA Y ÁLGEBRA

126
Capı́tulo 5

Ordenamiento

En múltiples problemas podemos encontrar que la solución radica en or-


denar una serie de elementos dados o que sencillamente nuestro algoritmo
solución fuera más eficiente si los datos iniciales estuvieran ordenados de
acuerdo cierto criterio. Es por eso que se hace necesario conocer los diferen-
tes algoritmos de ordenación sus principales caracterisiticas de las cuales se
derivan su ventajas y desventajas las cuales conducen a las situaciones donde
son aplicables o no.

Clasificación de los algoritmos de ordenamiento


Los algoritmos de ordenamiento se clasifican o se agrupan de acuerdo a
dos criterios el primero es estabilidad del algoritmo. Se dice que un algoritmo
de ordenamiento es estable cuando en la colección existente valores similares
y una vez ordenados estos valores mantienen entre ellos el orden inicial , no
cumplirse esto se dice que el algoritmo de ordenamiento no estable. Veamos
el próximo ejemplo
Se tiene la siguiente secuencia de números : 1, 2, 3, 5, 4, 2, 1. Como pode-
mos observar en esta secuencia existe la ocurrencia de los valores uno y dos
dos veces, pues bien ahora repersentemos la primera ocurrencia de los valores
con letra cursiva en la secuencia y negirta la segunda. La secuencia quedaria
ası́: 1, 2, 3, 5, 4, 2, 1. Ahora contamos con dos métodos de ordenamien-
to el método X y el método Y veamos la secuencia como queda ordenada
aplicando cada metodo:
Aplicando X a la secuencia:1, 1, 2, 2, 3, 4, 5.
Aplicando Y a la secuencia:1, 1, 2, 2, 3, 4, 5.
El algoritmo X es un ejemplo de un método de ordenamiento no estable
porque no mantuvo el orden de ocurrencia entre los elementos de igual valor
mientras el método Y si lo hizo lo que hace que sea considerado un método

127
ORDENAMIENTO

de ordenamiento estable. Este criterio puede influirnos mucho a la hora de


escoger un determinado algoritmo de ordenamiento sobre otros sobre todos
cuando necesito para la via de solución los elementos sean ordenados de
manera estable.
El otro criterio por el cual se clasifica los algoritmos de ordenamientos
es el referido al uso de memoria adicional a la reservada para almacenar
los elementos. Para ordenar una secuencia de elementos es necesario tener
dichos elementos contenidos dentro una estructura computacional de alma-
cenamiento como son los arreglos, listas y vectores. Cuando un método utiliza
memoria adicional a la utilizada con el uso de alguna estructura computacio-
nal de almacenamiento se dice que el algoritmo es no in-situ en caso contrario
de solo utilizar la memoria relacionada con las estructura computacional de
almacenamiento se dice que el algorimo es in-situ. En otras paralabras si el
algoritmo utiliza alguna estructura de datos adicional a la que contiene los
elementos de la secuencia es un algoritmo no in-situ en caso contrario es
in-situ
Conociendo estos elementos antemano estamos en condiciones de aden-
trarnos en las peculiaridades de cada uno de los algoritmos de ordenación.

5.1. Merge Sort


El algoritmo de ordenamiento por mezcla (merge sort en inglés) es un
algoritmo de ordenamiento externo estable basado en la técnica divide y
vencerás. Es de complejidad O (n log n) Conceptualmente, el ordenamiento
por mezcla funciona de la siguiente manera:

1. Si la longitud de la lista es 0 ó 1, entonces ya está ordenada. En otro


caso.

2. Dividir la lista desordenada en dos sublistas de aproximadamente la


mitad del tamaño.

3. Ordenar cada sublista recursivamente aplicando el ordenamiento por


mezcla.

4. Mezclar las dos sublistas en una sola lista ordenada.

El ordenamiento por mezcla incorpora dos ideas principales para mejorar su


tiempo de ejecución:

1. Una lista pequeña necesitará menos pasos para ordenarse que una lista
grande.

128
ORDENAMIENTO

2. Se necesitan menos pasos para construir una lista ordenada a partir de


dos listas también ordenadas, que a partir de dos listas desordenadas.
Por ejemplo, sólo será necesario entrelazar cada lista una vez que están
ordenadas.

Aunque Heap sort tiene los mismos lı́mites de tiempo que merge sort,
requiere sólo O(1) espacio auxiliar en lugar del O(n) de merge sort, y es a
menudo más rápido en implementaciones prácticas. Quick sort, sin embargo,
es considerado por mucho como el más rápido algoritmo de ordenamiento de
propósito general. En el lado bueno, merge sort es un ordenamiento estable,
paraleliza mejor, y es más eficiente manejando medios secuenciales de acceso
lento. Merge sort es a menudo la mejor opción para ordenar una lista enlaza-
da: en esta situación es relativamente fácil implementar merge sort de manera
que sólo requiera O(1) espacio extra, y el mal rendimiento de las listas enla-
zadas ante el acceso aleatorio hace que otros algoritmos (como Quick sort)
den un bajo rendimiento, y para otros (como Heap sort) sea algo imposible.

int n ;
int niz [ MAX_N ] , tmp [ MAX_N ];

inline void merge ( int left , int mid , int right )


{
int h ,i ,j , k ;
h = left ;
i = left ;
j = mid +1;
while ( h <= mid && j <= right )
{
if ( niz [ h ] <= niz [ j ])
{
tmp [ i ] = niz [ h ];
h ++;
}
else
{
tmp [ i ] = niz [ j ];
j ++;
}
i ++;
}
if ( h > mid )
{
for ( k = j ;k <= right ; k ++)

129
ORDENAMIENTO

{
tmp [ i ] = niz [ k ];
i ++;
}
}
else
{
for ( k = h ;k <= mid ; k ++)
{
tmp [ i ] = niz [ k ];
i ++;
}
}
for ( k = left ;k <= right ; k ++) niz [ k ] = tmp [ k ];
}

void mergeSort ( int left , int right )


{
if ( left == right ) return ;
int MID = ( left + right ) /2;
mergeSort ( left , MID ) ;
mergeSort ( MID +1 , right ) ;
merge ( left , MID , right ) ;
}

La llamada se realiza de la siguiente manera


mergeSort(0,n-1);

5.2. Heap Sort


El ordenamiento por montı́culos (heapsort en inglés) es un algoritmo de
ordenamiento no recursivo, no estable, con complejidad computacional O
(n log n) Este algoritmo consiste en almacenar todos los elementos del vector
a ordenar en un montı́culo (heap), y luego extraer el nodo que queda como
nodo raı́z del montı́culo (cima) en sucesivas iteraciones obteniendo el conjunto
ordenado. Basa su funcionamiento en una propiedad de los montı́culos, por
la cual, la cima contiene siempre el menor elemento (o el mayor, según se
haya definido el montı́culo) de todos los almacenados en él. El algoritmo,
después de cada extracción, recoloca en el nodo raı́z o cima, la última hoja
por la derecha del último nivel. Lo cual destruye la propiedad heap del árbol.
Pero, a continuación realiza un proceso de “descenso ”del número insertado
de forma que se elige a cada movimiento el mayor de sus dos hijos, con el que

130
ORDENAMIENTO

se intercambia. Este intercambio, realizado sucesivamente “hunde”el nodo en


el árbol restaurando la propiedad montı́culo del árbol y dejando paso a la
siguiente extracción del nodo raı́z.
El algoritmo, en su implementación habitual, tiene dos fases. Primero una
fase de construcción de un montı́culo a partir del conjunto de elementos de
entrada, y después, una fase de extracción sucesiva de la cima del montı́culo.
La implementación del almacén de datos en el heap, pese a ser conceptual-
mente un árbol, puede realizarse en un vector de forma fácil. Cada nodo tiene
dos hijos y por tanto, un nodo situado en la posición i del vector, tendrá a
sus hijos en las posiciones 2*i, y 2*i+1 suponiendo que el primer elemento
del vector tiene un ı́ndice = 1. Es decir, la cima ocupa la posición inicial del
vector y sus dos hijos la posición segunda y tercera, y ası́, sucesivamente.
Por tanto, en la fase de ordenación, el intercambio ocurre entre el primer
elemento del vector (la raı́z o cima del árbol, que es el mayor elemento del
mismo) y el último elemento del vector que es la hoja más a la derecha en el
último nivel. El árbol pierde una hoja y por tanto reduce su tamaño en un
elemento. El vector definitivo y ordenado, empieza a construirse por el final
y termina por el principio.

int n ;
int niz [ MAX_N ];

int heap_size ;

inline void Heapify ( int pos )


{
if ( pos > heap_size ) return ;
int ret = pos ;
int left = pos *2;
int right = pos *2+1;
if ( left <= heap_size && niz [ left ] > niz [ ret ]) ret =
left ;
if ( right <= heap_size && niz [ right ] > niz [ ret ]) ret
= right ;
if ( ret != pos )
{
swap ( niz [ pos ] , niz [ ret ]) ;
Heapify ( ret ) ;
}
}

inline void Pop ()

131
ORDENAMIENTO

{
int pos = 1;
swap ( niz [ pos ] , niz [ heap_size - -]) ;
while ( pos <= heap_size )
{
int ret = pos ;
int left = pos *2;
int right = pos *2+1;
if ( left <= heap_size && niz [ left ] > niz [ ret ])
ret = left ;
if ( right <= heap_size && niz [ right ] > niz [ ret ])
ret = right ;
if ( ret != pos )
{
swap ( niz [ pos ] , niz [ ret ]) ;
pos = ret ;
}
else
break ;
}
}

int main ()
{
n = 5;
niz [1] = 4;
niz [2] = 2;
niz [3] = 5;
niz [4] = 1;
niz [5] = 3;

heap_size = n ;

for ( int i = n /2; i >=1; i - -)


Heapify ( i ) ;
while ( heap_size > 1)
Pop () ;
for ( int i =1; i <= n ; i ++)
printf ( " %d " , niz [ i ]) ;
printf ( " \ n " ) ;
return 0;
}

132
ORDENAMIENTO

5.3. Quick Sort


El ordenamiento rápido (quicksort en inglés) es un algoritmo creado por
el cientı́fico británico en computación C. A. R. Hoare, basado en la técnica
de divide y vencerás, que permite, en promedio, ordenar n elementos en un
tiempo proporcional a N log N .
El algoritmo trabaja de la siguiente forma:

Elegir un elemento de la lista de elementos a ordenar, al que llamaremos


pivote.

Resituar los demás elementos de la lista a cada lado del pivote, de


manera que a un lado queden todos los menores que él, y al otro los
mayores. Los elementos iguales al pivote pueden ser colocados tanto
a su derecha como a su izquierda, dependiendo de la implementación
deseada. En este momento, el pivote ocupa exactamente el lugar que
le corresponderá en la lista ordenada.

La lista queda separada en dos sublistas, una formada por los elementos
a la izquierda del pivote, y otra por los elementos a su derecha.

Repetir este proceso de forma recursiva para cada sublista mientras


éstas contengan más de un elemento. Una vez terminado este proceso
todos los elementos estarán ordenados.

Como se puede suponer, la eficiencia del algoritmo depende de la posición


en la que termine el pivote elegido.

En el mejor caso, el pivote termina en el centro de la lista, dividiéndola


en dos sublistas de igual tamaño. En este caso, el orden de complejidad
del algoritmo es O(N log N ).

En el peor caso, el pivote termina en un extremo de la lista. El orden de


complejidad del algoritmo es entonces de O(N 2 ). El peor caso depen-
derá de la implementación del algoritmo, aunque habitualmente ocurre
en listas que se encuentran ordenadas, o casi ordenadas. Pero princi-
palmente depende del pivote, si por ejemplo el algoritmo implementado
toma como pivote siempre el primer elemento del array, y el array que
le pasamos está ordenado, siempre va a generar a su izquierda un array
vacı́o, lo que es ineficiente.

En el caso promedio, el orden es O(N log N ).

133
ORDENAMIENTO

No es extraño, pues, que la mayorı́a de optimizaciones que se aplican al


algoritmo se centren en la elección del pivote.

int n ;
int niz [ MAX_N ];

void qsort ( int left , int right )


{
if ( left < right )
{
int i = left ;
int j = right ;
int pivot = niz [( i + j ) /2];
while (i <= j )
{
while ( niz [ i ] < pivot ) i ++;
while ( niz [ j ] > pivot ) j - -;
if (i <= j )
{
swap ( niz [ i ] , niz [ j ]) ;
i ++;
j - -;
}
}
qsort ( left , j ) ;
qsort (i , right ) ;
}
}

La llamada se realiza de la siguiente manera:


qsort(0,n-1);

5.4. Radix Sort


En informática, el ordenamiento Radix (radix sort en inglés) es un algo-
ritmo de ordenamiento que ordena enteros procesando sus dı́gitos de forma
individual. Como los enteros pueden representar cadenas de caracteres (por
ejemplo, nombres o fechas) y, especialmente, números en punto flotante es-
pecialmente formateados, radix sort no está limitado sólo a los enteros.
La mayor parte de los ordenadores digitales representan internamente to-
dos sus datos como representaciones electrónicas de números binarios, por
lo que procesar los dı́gitos de las representaciones de enteros por represen-

134
ORDENAMIENTO

taciones de grupos de dı́gitos binarios es lo más conveniente. Existen dos


clasificaciones de radix sort: el de dı́gito menos significativo (LSD) y el de
dı́gito más significativo (MSD). Radix sort LSD procesa las representaciones
de enteros empezando por el dı́gito menos significativo y moviéndose hacia
el dı́gito más significativo. Radix sort MSD trabaja en sentido contrario.
Las representaciones de enteros que son procesadas por los algoritmos
de ordenamiento se les llama a menudo “claves”, que pueden existir por
sı́ mismas o asociadas a otros datos. Radix sort LSD usa tı́picamente el
siguiente orden: claves cortas aparecen antes que las claves largas, y claves
de la misma longitud son ordenadas de forma léxica. Esto coincide con el
orden normal de las representaciones de enteros, como la secuencia “1, 2, 3,
4, 5, 6, 7, 8, 9, 10”. Radix sorts MSD usa orden léxico, que es ideal para la
ordenación de cadenas de caracteres, como las palabras o representaciones de
enteros de longitud fija. Una secuencia como “b, c, d, e, f, g, h, i, j, ba”será
ordenada léxicamente como “b, ba, c, d, e, f, g, h, i, j”. Si se usa orden léxico
para ordenar representaciones de enteros de longitud variable, entonces la
ordenación de las representaciones de los números del 1 al 10 será “1, 10, 2,
3, 4, 5, 6, 7, 8, 9”, como si las claves más cortas estuvieran justificadas a la
izquierda y rellenadas a la derecha con espacios en blanco, para hacerlas tan
largas como la clave más larga, para el propósito de este ordenamiento.

5.5. Burble Sort


La ordenación de burbuja (Bubble Sort en inglés) es un sencillo algorit-
mo de ordenamiento. Funciona revisando cada elemento de la lista que va a
ser ordenada con el siguiente, intercambiándolos de posición si están en el
orden equivocado. Es necesario revisar varias veces toda la lista hasta que
no se necesiten más intercambios, lo cual significa que la lista está ordenada.
Este algoritmo obtiene su nombre de la forma con la que suben por la lista
los elementos durante los intercambios, como si fueran pequeñas ”burbujas”.
También es conocido como el método del intercambio directo. Dado que solo
usa comparaciones para operar elementos, se lo considera un algoritmo de
comparación, siendo el más sencillo de implementar. Este algoritmo es esen-
cialmente un algoritmo de fuerza bruta lógica. El ordenamiento de burbuja
tiene una complejidad O(n2 ) igual que ordenamiento por selección. Cuando
una lista ya está ordenada, a diferencia del ordenamiento por inserción que
pasará por la lista una vez y encontrará que no hay necesidad de intercambiar
las posiciones de los elementos, el método de ordenación por burbuja está for-
zado a pasar por dichas comparaciones, lo que hace que su complejidad sea
cuadrática en el mejor de los casos. Esto lo cataloga como el algoritmo más

135
ORDENAMIENTO

ineficiente que existe, aunque para muchos programadores sea el más sencillo
de implementar.

int n ;
int niz [ MAX_N ];
inline void bubbleSort ()
{
bool swapped ;
int it = 0;
do
{
swapped = false ;
for ( int i =0; i <n - it -1; i ++)
{
if ( niz [ i ] > niz [ i +1])
{
swap ( niz [ i ] , niz [ i +1]) ;
swapped = true ;
}
}
it ++;
} while ( swapped ) ;
}

5.6. CountingSort
El ordenamiento por cuentas (counting sort en inglés) es un algoritmo de
ordenamiento en el que se cuenta el número de elementos de cada clase para
luego ordenarlos. Sólo puede ser utilizado por tanto para ordenar elementos
que sean contables (como los números enteros en un determinado intervalo,
pero no los números reales, por ejemplo).
El primer paso consiste en averiguar cuál es el intervalo dentro del que
están los datos a ordenar (valores mı́nimo y máximo). Después se crea un vec-
tor de números enteros con tantos elementos como valores haya en el intervalo
[mı́nimo, máximo], y a cada elemento se le da el valor 0 (0 apariciones). Tras
esto se recorren todos los elementos a ordenar y se cuenta el número de apa-
riciones de cada elemento (usando el vector que hemos creado). Por último,
basta con recorrer este vector para tener todos los elementos ordenados.
Se trata de un algoritmo estable cuya complejidad computacional es
O(n+k), siendo n el número de elementos a ordenar y k el tamaño del vector
auxiliar (máximo - mı́nimo).

136
ORDENAMIENTO

La eficiencia del algoritmo es independiente de lo casi ordenado que estu-


viera anteriormente. Es decir no existe un mejor y peor caso, todos los casos
se tratan iguales.El algoritmo counting, no se ordena in situ, sino que requie-
re de una memoria adicional.El algoritmo posee una serie de limitaciones que
obliga a que sólo pueda ser utilizado en determinadas circunstancias.
Sólo ordena números enteros, no vale para ordenar cadenas y es desacon-
sejable para ordenar números decimales. Teóricamente se puede, pero deberı́a
recrear en la matriz auxiliar tantas posiciones como decimales quepan entre
2 números consecutivos, si se restringe a 1 o 2 decimales podrı́a ser asequible
un número mayor de decimales puede llegar a suponer una memoria auxiliar
impracticable. Otra limitación (por ineficiencia) incluso con números enteros
es cuando el rango entre el mayor y el menor es muy grande. Imaginemos una
lista de 1000 elementos, donde el menor es el 0 y el mayor 123456789. Ordenar
esta lista supondrı́a crear una matriz auxiliar de 123456790 elementos. Una
cantidad muy elevada de memoria para ordenar sólo 1000 elementos. También
supondrı́a un desperdicio de tiempo pués la matriz auxiliar para trasvasar
a la lista ordenada debe recorrerse entera, aunque sólo se reasignarán 1000
valores de los 123456790 elementos. Con lenguajes de programación que no
permitan definir vectores cuyo primer ı́ndice sea un valor distinto de 0 ó 1 es
necesario realizar una traducción de los valores. Por ejemplo, si el intervalo
es (4,10) y el vector auxiliar comprende el rango (1-7), para cada elemento
se deberá incrementar el contador de la posición en 3.

int n ;
int niz [ MAX_N ];
int Count [ MAX_K ];

void countingSort ()
{
for ( int i =0; i < n ; i ++)
Count [ niz [ i ]]++;
int ii = 0;
for ( int i =0; i < MAX_K ; i ++)
{
while ( Count [ i ] > 0)
{
niz [ ii ++] = i ;
Count [ i ] - -;
}
}
}

137
ORDENAMIENTO

5.7. Insertion Sort


El ordenamiento por inserción (insertion sort en inglés) es una manera
muy natural de ordenar para un ser humano, y puede usarse fácilmente para
ordenar un mazo de cartas numeradas en forma arbitraria. Requiere O(n2 )
operaciones para ordenar una lista de n elementos. Inicialmente se tiene un
solo elemento, que obviamente es un conjunto ordenado. Después, cuando
hay k elementos ordenados de menor a mayor, se toma el elemento k+1 y
se compara con todos los elementos ya ordenados, deteniéndose cuando se
encuentra un elemento menor (todos los elementos mayores han sido despla-
zados una posición a la derecha) o cuando ya no se encuentran elementos
(todos los elementos fueron desplazados y este es el más pequeño). En este
punto se inserta el elemento k+1 debiendo desplazarse los demás elementos.

int n ;
int niz [ MAX_N ];
inline void insertionSort ()
{
for ( int i =1; i < n ; i ++)
{
int j = i - 1;
int tmp = niz [ i ];
while ( j >= 0 && niz [ j ] > tmp )
{
niz [ j +1] = niz [ j ];
j - -;
}
niz [ j +1] = tmp ;
}
}

5.8. Selection Sort


El ordenamiento por selección (Selection Sort en inglés) es un algoritmo
de ordenamiento que requiere O(n2 ) operaciones para ordenar una lista de n
elementos. Su funcionamiento es el siguiente:
Buscar el mı́nimo elemento de la lista.

Intercambiarlo con el primero.

Buscar el siguiente mı́nimo en el resto de la lista.

138
ORDENAMIENTO

Intercambiarlo con el segundo.

Y en general

Buscar el mı́nimo elemento entre una posición i y el final de la lista.

Intercambiar el mı́nimo con el elemento de la posición i.

Este algoritmo mejora ligeramente el algoritmo de la burbuja. En el caso


de tener que ordenar un vector de enteros, esta mejora no es muy sustancial,
pero cuando hay que ordenar un vector de estructuras más complejas, la ope-
ración intercambiar() serı́a más costosa en este caso. Este algoritmo realiza
muchas menos operaciones intercambiar() que el de la burbuja, por lo que lo
mejora en algo.
Otra desventaja de este algoritmo respecto a otros como el de burbuja
o de inserción directa es que no mejora su rendimiento cuando los datos ya
están ordenados o parcialmente ordenados. Ası́ como, por ejemplo, en el caso
de la ordenación de burbuja se requerirı́a una única pasada para detectar
que el vector ya está ordenado y finalizar, en la ordenación por selección se
realizarı́an el mismo número de pasadas independientemente de si los datos
están ordenados o no.

int n ;
int niz [ MAX_N ];

inline void selectionSort ()


{
for ( int i =0; i <n -1; i ++)
{
int minPos = i ;
for ( int j = i +1; j < n ; j ++)
{
if ( niz [ j ] < niz [ minPos ])
{
minPos = j ;
}
}
swap ( niz [ i ] , niz [ minPos ]) ;
}
}

139
ORDENAMIENTO

5.9. Bibliotecas con funciones de ordenamien-


to
Hoy en dı́a la mayorı́a de los lenguajes de programación poseen un grupo
de bibliotecas o funciones que permiten ordenar una colección de elementos
sean de un tipo dato primitivo del lenguaje o bien creados por el desarrolla-
dor. Este elemento es muy importante por dos motivos:

Reduce el esfuerzo. No debemos emplear tiempo en la implementación


de un algoritmo que ordene, sino que podemos utilizar alguna función
de las definidas en lenguaje de programación escogido.

Reduce la posibilidad de errores que puede surgir a la hora de imple-


mentar por nuestro propios esfuerzos un algoritmo de ordenación. Las
funciones o métodos que nos brinda los lenguajes de programación hay
seguridad de su funcionamiento correcto y de manera eficiente

A continuación vamos a ver algunas de esas funciones de los lenguajes de


programación C++ y Java

5.9.1. Bibliotecas de C++


Para ordenar el lenguaje de programación C++ cuenta con la biblioteca
algorithm la cual posee las siguientes funcionalidades

sort(). Esta función ordena los elementos de una colección de manera


ascedente. Como detalle de la función es que no garantiza el orden
inicial entre elementos de igual valor. Su complejidad es O(N log (N))
tanto en el caso promedio como en el peor de los casos. Una segunda
variante de esta funcionalidad se le pasa un función para comparar los
elementos de la colección. Esta variante es utilizada cuando se desea
ordenar tipos de datos creados por el programador.

# include < algorithm >


void sort ( iterator start , iterator end ) ;
void sort ( iterator start , iterator end ,
St ri ct We ak Or de ri ng cmp ) ;

He aquı́ un ejemplo de su utilización

# include < algorithm >


// Ejemplo #1

140
ORDENAMIENTO

vector < int > v ;


v . push_back ( 23 ) ;
v . push_back ( -1 ) ;
v . push_back ( 9999 ) ;
v . push_back ( 0 ) ;
v . push_back ( 4 ) ;

cout << " Before sorting : " ;


for ( unsigned int i = 0; i < v . size () ; i ++ )
{
cout << v [ i ] << " " ;
}
cout << endl ;

sort ( v . begin () , v . end () ) ;

cout << " After sorting : " ;


for ( unsigned int i = 0; i < v . size () ; i ++ )
{
cout << v [ i ] << " " ;
}
cout << endl ;

// Ejemplo #2
int array [] = { 23 , -1 , 9999 , 0 , 4 };
unsigned int array_size = 5;

cout << " Before sorting : " ;


for ( unsigned int i = 0; i < array_size ; i ++ )
{
cout << array [ i ] << " " ;
}
cout << endl ;

sort ( array , array + array_size ) ;

cout << " After sorting : " ;


for ( unsigned int i = 0; i < array_size ; i ++ )
{
cout << array [ i ] << " " ;
}
cout << endl ;

141
ORDENAMIENTO

// Ejemplo #3
bool cmp ( int a , int b )
{
return a > b ;
}

...

vector < int > v ;


for ( int i = 0; i < 10; i ++ )
{
v . push_back ( i ) ;
}

cout << " Before : " ;


for ( int i = 0; i < 10; i ++ )
{
cout << v [ i ] << " " ;
}
cout << endl ;

sort ( v . begin () , v . end () , cmp ) ;

cout << " After : " ;


for ( int i = 0; i < 10; i ++ )
{
cout << v [ i ] << " " ;
}
cout << endl ;

stable sort. Esta función es similar a la anterior con la diferencia que si


mantiene el orden inicial de los elementos cuando estos tienen similar
valor. Otra diferencia es el tiempo de ejecucción el cual puede alcanzar
N (logN )2 en el peor de los casos.

# include < algorithm >


void stable_sort ( iterator start , iterator end ) ;
void stable_sort ( iterator start , iterator end ,
St ri ct We ak Or de ri ng cmp ) ;

Para realizar el ordenamiento Heap Sort se cuenta con las funcionali-

142
ORDENAMIENTO

dades sort heap, is heap,make heap, pop heap, push heap


partial sort Es una función que permite ordenar los primeros N ele-
mentos de una colección. N elementos es definido por la cantidad de
elementos en rango comprendido [start,end).

# include < algorithm >


void partial_sort ( iterator start , iterator middle ,
iterator end ) ;
void partial_sort ( iterator start , iterator middle ,
iterator end , S tr ic tWe ak Or de ri ng cmp ) ;

5.9.2. Bibliotecas de Java


En el caso del lenguaje de programación Java cuenta con la clase Collec-
tions perteneciente al paquete java en el subpaquete util. Esta clase con un
número métodos estáticos entre los cuales podemos encontrar métodos para
ordenar colecciones. El algoritmo sort ordena los elementos de un objeto List
, el cual debe implementar a la interfaz Comparable . El orden se determina
en base al orden natural del tipo de los elementos, según su implementación
mediante el método compareTo de ese objeto. El método compareTo está
declarado en la interfaz Comparable y algunas veces se le conoce como el
método de comparación natural. La llamada a sort puede especificar como
segundo argumento un objeto Comparator , para determinar un ordenamien-
to alterno de los elementos.

void sort ( List list )


void sort ( List list , Comparator c )

El primer método lo utilizaremos cuando los elementos de la lista im-


plementan la interfaz Comparable vista anteriormente y el segundo lo utili-
zaremos cuando querramos utilizar nuestro propio comparador o cuando no
nos guste el funcionamiento del comparador por defecto de los elementos de
nuestra lista.
Ambas versiones garantizan un coste de O(nlog(n)) y puede acercarse a
un rendimiento lineal cuando las los elementos se encuentran cerca de su
orden natural. El algoritmo utilizado es una pequeña variación del algoritmo
de mergesort y la operación que realiza es destructiva, es decir, no podremos
recuperar el orden original si no hemos guardado la lista previamente.

Ordenamiento ascendente Se utiliza el algoritmo sort para ordenar los


elementos de un objeto List en forma ascendente (lı́nea 20). Recuerde que

143
ORDENAMIENTO

List es un tipo genérico y acepta un argumento de tipo, el cual especifi ca el


tipo de elemento de lista; en la lı́nea 15 se declara a lista como un objeto List
de objetos String . Observe que en las lı́neas 18 y 23 se utiliza una llamada
implı́cita al método toString de lista para imprimir el contenido de la lista en
el formato que se muestra en las lı́neas segunda y cuarta de los resultados.

import java . util . List ;


import java . util . Arrays ;
import java . util . Collections ;
public class Ordenamiento1
{
private static final String palos [] ={ "
Corazones " , " Diamantes " , " Bastos " , " Espadas "
};
// muestra los elementos del arreglo
public void im primir Elemen tos ()
{
List < String > lista = Arrays . asList (
palos ) ; // crea objeto List
// imprime lista
System . out . printf ( " Elementos del
arreglo desordenados :\ n %s \ n " , lista )
;
Collections . sort ( lista ) ; // ordena
ArrayList
// imprime lista
System . out . printf ( " Elementos del
arreglo ordenados :\ n %s \ n " , lista ) ;
} // fin del metodo impri mirEle mentos
public static void main ( String args [] )
{
Ordenamiento1 orden1 = new Ordenamiento1 () ;
orden1 . im primir Elemen tos () ;
} // fin de main
} // fin de la clase Ordenamiento1

Ordenamiento descendente se ordena la misma lista de cadenas utili-


zadas en el ejemplo, en orden descendente. El ejemplo introduce la interfaz
Comparator , la cual se utiliza para ordenar los elementos de un objeto Co-
llection en un orden distinto. En la lı́nea 21 se hace una llamada al método
sort de Collections para ordenar el objeto List en orden descendente. El
método static reverseOrder de Collections devuelve un objeto Comparator

144
ORDENAMIENTO

que ordena los elementos de la colección en orden inverso.

import java . util . List ;


import java . util . Arrays ;
import java . util . Collections ;
public class Ordenamiento1
{
private static final String palos [] ={ "
Corazones " , " Diamantes " , " Bastos " , " Espadas "
};
// muestra los elementos del arreglo
public void im primir Elemen tos ()
{
List < String > lista = Arrays . asList (
palos ) ; // crea objeto List
// imprime lista
System . out . printf ( " Elementos del
arreglo desordenados :\ n %s \ n " , lista )
;
Collections . sort ( lista , Collections .
reverseOrder () ) ;
// imprime lista
System . out . printf ( " Elementos del
arreglo ordenados :\ n %s \ n " , lista ) ;
} // fin del metodo impri mirEle mentos

public static void main ( String args [] )


{
Ordenamiento1 orden1 = new Ordenamiento1
() ;
orden1 . im primir Elemen tos () ;
} // fin de main
} // fin de la clase Ordenamiento1

Mientras para ordenar arreglos Java proporciona la clase Array que de


similar manera que Collections posee un grupo de métodos para trabajar
pero con arreglos.

5.10. Ordenamiento de datos personalizados


En ocasiones nos encontramos problemas que lo que se necesita ordenar
no es una colección o arreglo de datos primitivos propios de lenguajes de

145
ORDENAMIENTO

programación que estemos utilizando sino estructuras o clases creadas por


nosotros mismo. Como podemos ordenarlas entonces utilizando los métodos
de ordenamiento que nos brinda el lenguaje de programación utilizado. Una
primera idea es implementar por nuestro medios un algoritmo de ordenación
que se ajuste a l problema y donde se normalmente se realiza la comparación
entre los elementos de la colección se invoca a una función que recibe dos
objetos del tipo de datos que queremos comparar y los comprara devolviendo
un valor booleano. Bueno, cercana a esta idea los métodos de ordenamiento
de los lenguajes de programación permite ordenar tipos de datos creados por
nosotros.
Veamos como se hace en cada lenguaje de programación:

5.10.1. C++
Con el lenguaje de programación existe dos variantes. La primera es muy
sencilla solo debemos implementar un método que reciba como parámetros
dos variables del tipo de dato que se desea ordenar. Dentro de la método se
compara las variables según el criterio definido para ese tipo de datos. Dicho
método devuelve el valor booleano. Luego solo debemos invocar algunos de
los métodos con sus dos parámetros tradicionales y como tercer parámetro
el nombre de la método de comparación creado. A continuación un ejemplo.

# include < stdio .h >


# include < algorithm >
# include < string .h >
using namespace std ;

struct Submission
{
int time ;
char verdict [5];
};

bool cmpSubmission ( const Submission &a , const Submission


&b)
{
return ( a . time < b . time ) ;
}

Submission submission [110];

......................

146
ORDENAMIENTO

int main ()
{
..............

sort ( submission , submission + nSubmission ,


cmpSubmission ) ;

..............
return 0;
}

La otra variante es la sobrecarga de operador < dentro de la estructura o


clase que definimos y luego solo tenemos que invocar algunos de lo métodos
conocidos. El siguiente código es un ejemplo de lo expresado:

# include < algorithm >


# include < vector >
using namespace std ;

struct Order
{
ULL begin , end , price ;

Order ( ULL _begin =0 , ULL _end =0 , ULL _price =0)


{
begin = _begin ;
end = _end ;
price = _price ;
}
};

bool operator <( Order _a , Order _b )


{
return ( _a . end < _b . end ) ;
}

vector < Order > orders ;

.......

147
ORDENAMIENTO

int main ()
{
.....................

sort ( orders . begin () , orders . end () ) ;

.....................
return 0;
}

5.10.2. Java
En el caso del lenguaje de programación Java se puede utilizar la interface
Comparator permite ordenar listas y colecciones cuyos objetos pertenecen
a clases de tipo cualquiera. Esta interface permitirı́a por ejemplo ordenar
figuras geométricas planas por el área o el perı́metro. Su papel es similar al
de la interface Comparable, pero el usuario debe siempre proporcionar una
implementación de esta clase. Sus dos métodos se declaran en la forma:

public int compare ( Object o1 , Object o2 )


public boolean equals ( Object obj )

Existen dos interfaces que soportan organizaciones. La interfaz Compa-


rable, que se utiliza cuando una clase tiene un orden natural. Dados muchos
objetos del mismo tipo, la interfaz permite a uno de ellos ordenar todos los
demás. La definición de la interfaz Comparable es:

public interface Comparable {


public int compareTo ( Object obj ) ;
}

Si el valor devuelto es negativo, indica que la llamada a la interfaz que im-


plementa va antes del parámetro. Cero significa que tanto el implementador
como el parámetro son iguales, y un valor positivo indica que el parámetro
va antes. Si una clase implementa a la interfaz Comparable, los objetos de
la clase pueden usarse como la clave para una colección basada en un esque-
ma de árbol. Si un objeto no implementa Comparable, pero queremos que
sea una clave, se puede implementar la interfaz Comparator y se harán las
comparaciones automáticamente. A continuación un ejemplo con Comparable

148
ORDENAMIENTO

import java . io .*;


import java . util . Arrays ;
import java . util . StringTokenizer ;

public class ESSFootball {

private static class Equipo implements


Comparable < Equipo >
{

public String nombre ;


public int puntos ;
public int partidos ;

public int victorias , empates , derrotas ;


public int golesFavor , golesContra ;

public Equipo ( String nombre )


{
this . nombre = nombre ;
puntos = partidos = victorias =
empates = derrotas =
golesFavor = golesContra = 0;
}

public int diferenciaGoles ()


{
return golesFavor - golesContra ;
}

@Override
// 0 iguales , -1: a < b , 1: a > b
public int compareTo ( Equipo o )
{
if ( this . puntos > o . puntos )
return -1;
else if ( this . puntos < o . puntos )
return 1;
if ( this . victorias > o . victorias
)
return -1;
else if ( this . victorias < o .

149
ORDENAMIENTO

victorias )
return 1;
if ( this . diferenciaGoles () > o .
diferenciaGoles () )
return -1;
else if ( this . diferenciaGoles ()
< o . diferenciaGoles () )
return 1;
if ( this . golesFavor > o .
golesFavor )
return -1;
else if ( this . golesFavor < o .
golesFavor )
return 1;
if ( this . partidos < o . partidos )
return -1;
else if ( this . partidos > o .
partidos )
return 1;
return this . nombre .
c om p a re T o Ig n o re C a se ( o . nombre )
;
}
}
public static void main ( String [] args ) throws
IOException
{
....................

Equipo [] equipos = new Equipo [ nEquipos ];

.......................

Arrays . sort ( equipos ) ;


..........................
}
}

150
ORDENAMIENTO

5.11. Change frequency of bubble sort

5.12. Select k th element

5.13. Análisis de ejercicios


3710 - Presidential Election Solo debemos debemos ir acumulando los
votos obtenidos por cada candidato en cada región a partir de los datos de
entrada y después ordenarlos, de igual forma se debe ir acumulando todos
los votos para saber si el primer candidato obtuvo más del 50 % ciento, en
ese caso solo se debe imprimir el identificador de dicho candidato sino se
debe imprimir adicionalmente el identificador del segundo candidato con más
votos;

MOG A ordenar! Un ejercicio donde solo debemos leer la lista de núme-


ros de la entrada e imprimir dicha lista pero con los elementos ordenados
ascedentemente.

3821 - Coco Olympiad Este ejercicio solo requiere que las notas de los
concursantes sean ordenadas de menor a mayor, eliminamos la menor y mayor
y se suma el resto esa suma se puede almacenar en una colección luego esa
colección se ordena de mayor a menor y se imprime el primer elemento.Otra
variante es tener una variable donde voy almacenar la suma de las notas que
sea mayor, para esto se inicializa dicha variable con un valor bien pequeño
y luego de sumar las notas del conursante si esta es mayor que el valor
almacenado en la variable almacenamos dicha variable ese nuevo valor. Luego
se imprime la variable.

1089 - Open Source Para darle solución al problema vamos a crear pri-
meramente una estructura llamada Proyecto que va tener como campos, una
cadena para almacenar el nombre del proyecto y un entero para almacenar
la cantidad de integrantes de ese proyecto. Vamos utilizar dos estructura de
datos extras. La primera va ser un cojunto de cadenas donde se va alma-
cenar todos los usuarios registrados , mientras utilizaremos un diccionario
donde las claves serán los nombres de los usuarios y el valor asociado será
la posición que ocupa el proyecto en el arreglo de proyectos al que quieren
asociarse. Cada vez que se lea un usuario debemos chequear lo siguiente:
Si usuario no pertence al conjunto de usuarios:
Insertamos el usuario al cojunto de usuarios.

151
ORDENAMIENTO

Insertamos el usuario y la posicion del proyecto al que quiere asociarse


en el diccionario.
Sino si usuario pertence al conjunto de usuarios y el usuario esta asociado
a otro proyecto con antelación: Elimino del dicionario la tupla donde la
clave sea el usuario.
Luego se recorre todas las tuplas del dicionario y por cada tupla se se bus-
ca el proyecto en el arreglo de proyectos asociado al usuario y se incrementa
en uno la cantidad de integrantes. Por ultimo ordenamos arreglo de proyec-
tos y se imprime luego. Aqui como punto a tener en cuenta es la entrada de
datos que puede resultar dificil dependiendo del lenguaje que se utilice. Lo
otro que es la estructura Proyecto se debe sobrecargar el operador < donde
primero se debe tener en cuenta la cantidad de integrantes y luego el nombre.

1841 - Combat Forces El problema nos plantea un juego y debemos


definir el bando que gana y cantidad de sobrevientes del bando ganador, en
caso de empate se debe decir la cantidad de sobrevientes de cada bando.
Para deteminar la respuesta solo debemos hacer un algoritmo que simule
el juego descripto en el problema. Para eso vamos a crear una estructura
que nombraremos Bando y la misma tendrá como atributos la suma de la
fuerzas todos los soldados que integran ese bando, además de una colección
ordenada por fuerza de forma descendente de los soldados que conforman el
bando. Dicha colección debe permitir eliminar y acceder al primer elemento
de la colección en el caso de C++ utilice un deque. Luego una vez leido los
datos calculados la suma total de fuerza de los bandos asi como ordenados
los soldados en los bandos de acuerdo a su fuerza podemos simular el juego
de la siguiente manera.
eliminarSSJimmy es verdadero
eliminarSSManny es verdadero
Mientras Jimmy tenga soldados y Manny tenga soldado y (eliminarSS-
Jimmy o eliminarSSManny sean verdadero)
ssJimmy=lista de soldados de Jimmy en la primera posición
ssManny=lista de soldados de Manny en la primera posición
eliminarSSJimmy es falso
eliminarSSManny es falso
Si la fuerza del bando de Manny mayor que fuerza de ssJimmy
Eliminar la primera posición de la lista de soldados de Jimmy
eliminarSSJimmy es verdadero
Si la fuerza del bando de Jimmy mayor que fuerza de ssManny
Eliminar la primera posición de la lista de soldados de Manny
eliminarSSManny es verdadero

152
ORDENAMIENTO

Si eliminarSSJimmy es verdadero
La fuerza del bando de Jimmy debe decrementar en ssJimmy
Si eliminarSSManny es verdadero
La fuerza del bando de Manny debe decrementar en ssManny
Luego solo tenemos que chequear que bando se quedo sin soldados y cual
tiene y ese será el ganador, recuerde que si ambos tienen soldados significa
que terminó empatado el juego.

1318 - Abc El ejercicio nos pide dado tres números y una secuencia de
las letras ’A’, ’B’ y ’C’. Imprimir los números de acuerdo a la secuencia de
letras donde ’A’ es el menor valor de los tres, ’B’ es la segundo menor valor,
mientras ’C’ es el mayor de los tres números.

3986 - Playing with digits Una vez leı́do el problema nos podemos per-
catar que debemos leer la cadena entrante que solo va estar compuesta por
digitos exceptuando el digito 0. De esta entrada debemos imprimir los di-
gitos presentes ordenados de forma descendentemente según la cantidad de
ocurrencias de los mismos en la entrada. Segun el problema si dos digitos
tienen la misma frecuencia decide el valor númerico de los digitos. Para so-
lucionar el problema podemos crear una estructura con dos atributos (valor
y frecuencia). Inicializo un arreglo de 10 elementos de esta estructura donde
cada posición i, la estructura en esa posición su valor será igual i y frecuen-
cia igual 0. Leo la secuencia y la recorro y por cada dı́gito de la secuencia
voy a su posición en el arreglo e incremento en uno la frecuencia. Ordeno
los elementos de las estructura (primero por la frecuencia y por el valor) .
Imprimo los valores de los elementos después de ordenarlo descendentemente
y siempre que la frecuencia de dicho valor sea distinto de 0.

4207 - Pedro Picapiedras Una vez leı́do el problema podemos ver que
nos piden a partir de un grupo de ruedas de radios variables tenemos que
calcular cuantos carros se pueden armar de dichas ruedas y cuantas ruedas
sobran. Se conoce que un carro necesita la cuatros ruedas con similar radio.
Un dato interesante del problema es que el rango de radio de las ruedas es
menor que la cantidad de posibles ruedas a analizar por lo que para resolver
el problema bien pudieramos aplicar una estrategia parecida al método de
ordenamiento CountingSort. Vamos a crear un arreglo y el la posición i del
arreglo vamos alamacenar la cantidad de ruedas cuyo radio es igual a i de
esta forma para dar solución al problema solo debemos recorrer cada posición
de arreglo tomamos el valor de la posición y lo dividimos entre cuatro y esa
va ser la cantidad carros con ruedas de ese tipo que se pueden armar y si

153
ORDENAMIENTO

tomamos el valor y hallamos el resto de la división con cuatro nos va indicar


cuantas ruedas de ese tipo van a sobrar luego vamos sumando estos resultados
parciales para llegar a la solución final.

2352 - Phone List Se tiene una lista de numeros de teléfonos y se quiere


saber si dicha lista es consistente. Se dice que una lista de números de telefo-
nos es consistente si no existe un número de teléfono que sea el prefijo de otro
número de telefono presente en la lista. Para solucionar este problema vamos
a trabajar los números de teléfonos como cadenas texto y no como números.
La razón de esta decisión es porque si ordenamos las cadenas para el número
teléfonico i su posible número teléfonico que sea prefijo este va quedar en la
posición i-1 sino lo cree analice la siguiente secuencia 111,91,91125426 que
no es consistente:

Si trabajamos los datos como números y ordenamos la secuencia que-


darı́a 91,111,91125426.

Si trabajamos los datos como cadena y ordenamos la secuencia quedarı́a


111,91,91125426. Vemos como el número que es prefijo de otro queda
delante de este.

Por lo que para solucionar el problema solo debemos almacenar en una


coleción los números de teléfono como cadena de texto ordenarlos de menor a
mayor y luego comprobar para cada cadena i si no es prefijo de la cadena i+1,
en caso de existir una tupla de cadenas que cumpla la condición la respuesta
es NO sino se debe imprimir YES.

MOG13B - Phone List La misma idea del problema 2352 - Phone List.

DMOJ - Lista de telefonos La misma idea del problema 2352 - Phone


List y MOG13B - Phone List.

DMOJ - POI El ejercicio nos pide hallar la puntación y el lugar que alcan-
za un determinado concursante durante un concurso. Para esto nos explica
como se calcula la puntación de cada tarea que integra el concurso, las tareas
hecha por cada concursante. Para resolver el problema solo debemos tener
una estructura de tipo concursante que almacene su identificador asi como
los identificadores de las tareas que realizo y los puntos que este obtuvo. El
primer paso es calcular para cada tarea cuanto puntos aporta su realización.
Luego por cada concursantes vemos las tareas que realizo y podemos sacar
su puntaje final. Una vez realizado esto podemos ordenar los concursantes

154
ORDENAMIENTO

or su puntaje de forma descendente y luego buscar el concursante requerido


e imprimir su puntaje y posición fina en el concurso.

155
ORDENAMIENTO

156
Capı́tulo 6

Búsqueda

Un algoritmo de búsqueda es aquel que está diseñado para localizar un


elemento con ciertas propiedades dentro de una estructura de datos; por
ejemplo, ubicar el registro correspondiente a cierta persona en una base de
datos, o el mejor movimiento en una partida de ajedrez. La variante más
simple del problema es la búsqueda de un número en un vector. Se utiliza
cuando el vector no está ordenado o no puede ser ordenado previamente.
Consiste en buscar el elemento comparándolo secuencialmente (de ahı́ su
nombre) con cada elemento del vector.
Este sencillo algoritmo tiene un tiempo de ejecución en el peor caso de
O(N). Es por eso que los algoritmos de búsqueda que veremos tienen dos
objetivos fundamentales:
Buscar
Realizar la búsqueda de manera eficiente.

6.1. Búsqueda binaria


La búsqueda binaria conocida también como búsqueda dicotómica se uti-
liza para determinar la existencia de un elemento dentro de una estuctura
secuencial iterable como arreglos, vectores y lista que como precondición se
encuentra ordenada.
El algoritmo se puede implementar mediante dos vı́as la primera recusirva:

# include < iostream >


# include < vector >

bool bu s q ue d a _ di c o to m i ca ( const vector < int > &v , int


principio , int fin , int & x )

157
BÚSQUEDA

{
bool res ;
if ( principio <= fin )
{
int m = (( principio + fin ) /2) + principio ;
if ( x < v [ m ])
res = b u sq u e da _ d ic o t om i c a (v , principio , m
-1 , x ) ;
else if ( x > v [ m ])
res = b u sq u e da _ d ic o t om i c a (v , m +1 , fin , x ) ;
else
res = true ;
}
else
res = false ;
return res ;
}

La otra variante es la iterativa:

int n ;
int niz [ MAX_N ];

inline int b_search ( int left , int right , int x )


{
int i = left ;
int j = right ;
while ( i < j )
{
int mid = ( i + j ) /2;
if ( niz [ mid ] == x )
return mid ;
if ( niz [ mid ] < x )
i = mid +1;
else
j = mid -1;
}
if ( niz [ i ] == x )
return i ;
return -1;
}

En ambos casos el intervalo de búsqueda cuando se invoque el método


de ser de 0 a n-1 donde n es la cantidad de elementos de la estructura itera-

158
BÚSQUEDA

tiva secuencial. En la variante recursiva devuelve verdadero si se encuentra


el elemento y falso en caso contrario mientras en la iterativa se devuelve la
posición en arreglo donde esta el elemento sino se devuelve -1. La búsque-
da binaria presenta una complejidad O(log(n)) siendo n es la cantidad de
elementos de la estructura iterativa secuencial.

6.2. Bibliotecas con funciones de búsqueda


Como mismo sucede con los algoritmos de ordenamientos, los lenguajes
de programación ofrecen un grupo de métodos para buscar de manera eficien-
te sobre coleciones. La mayorı́a de estos métodos tienen como precondición
para su utilización que la coleción de datos debe estar ordenada de forma
ascendente. A continuación veremos dichos métodos por lenguajes.

6.2.1. Bibliotecas de C++


De igual manera que los algoritmos de ordenamiento la biblioteca algo-
rithm proporciona un grupo de método para buscar entre los cuales podemos
encontrar:

binary search: Realiza la busqueda binaria de un elemento dentro de


una coleción.

# include < algorithm >


bool binary_search ( iterator start , iterator end
, const TYPE & val ) ;
bool binary_search ( iterator start , iterator end
, const TYPE & val , Comp f ) ;

Como parametros se le pasa el incio y fin de la coleción, y el valor


a buscar como parametro opcional se le puede pasar una función que
defina como se puede comparar los elementos de la coleción. Retorna
verdadero en caso de encontrarse el elemento y falso en caso contrario.
Como precondición para la utilización de este método la coleción debe
estar ordenada ascedentemente. He aquı́ un ejemplo.

int nums [] = { -242 , -1 , 0 , 5 , 8 , 9 , 11 };


int start = 0;
int end = 7;

for ( int i = 0; i < 10; i ++ )

159
BÚSQUEDA

{
if ( binary_search ( nums + start , nums + end , i
) )
{
cout << " nums [] contains " << i << endl ;
}
else
{
cout << " nums [] DOES NOT contain " << i
<< endl ;
}
}

lower bound: Es una variación de la búsqueda binaria.

# include < algorithm >


iterator lower_bound ( iterator first , iterator last ,
const TYPE & val ) ;
iterator lower_bound ( iterator first , iterator last ,
const TYPE & val , CompFn f ) ;

Devuelve la posición o iterador mas a la izquierda donde se puede


insertar el elemento deseado de manera que que la coleción sobre la
que se opera se mantenga ordenada. Es claro que como precondición
para la utilización de este método debe estar ordenada ascedentemente.
Su complejidad es O(logN).

vector < int > nums ;


nums . push_back ( -242 ) ;
nums . push_back ( -1 ) ;
nums . push_back ( 0 ) ;
nums . push_back ( 5 ) ;
nums . push_back ( 8 ) ;
nums . push_back ( 8 ) ;
nums . push_back ( 11 ) ;

cout << " Before nums is : " ;


for ( unsigned int i = 0; i < nums . size () ; i ++ )
{
cout << nums [ i ] << " " ;
}
cout << endl ;

160
BÚSQUEDA

vector < int >:: iterator result ;


int new_val = 7;

result = lower_bound ( nums . begin () , nums . end () ,


new_val ) ;

nums . insert ( result , new_val ) ;

cout << " After , nums is : " ;


for ( unsigned int i = 0; i < nums . size () ; i ++ )
{
cout << nums [ i ] << " " ;
}
cout << endl ;

upper bound: Su funcionamiento es similar a la anterior, se diferencia


que este devuelve posición o iterator más a la derecha donde se puede
insertar el elemento de forma que la coleción de mantenga ordenada.

# include < algorithm >


iterator upper_bound ( iterator start , iterator end ,
const TYPE & val ) ;
iterator upper_bound ( iterator start , iterator end ,
const TYPE & val , S tri ct We ak Or de ri ng cmp ) ;

6.2.2. Bibliotecas de Java


Similar a como sucede con los métodos de ordenamiento. En el lenguaje de
programación Java las clases Arrays y Colections poseen el método búsqueda
binaria para buscar un elemento bien sea dentro de un arreglo o colección,
en ambos caso devuelve la posición donde se encuentra el elemento buscado.
En caso de encontrarse el valor devuelto es -1.

6.3. Selección rápida


Digamos que tenemos una colección de números A conformado por a0 ,
a1 , a2 , ..., an−1 y queremos saber cual es k-enésimo menor elemento dentro
de la colección.
Una primera idea trivial ordenar la colección devolver el valor en la posi-
ción deseada de la colección.

161
BÚSQUEDA

Este algoritmo tendrı́a una complejidad O(nlogn) en tiempo.


Pues bien vamos a ver un alagoritmo que permite buscar dentro de una
colección el k-enésimo menor elemento. El algoritmo es similar al quicksort.
Primero, se escoje el pivote correcto. Para clasificar los elementos menores
que el pivote para el siguiente valor superior. El número de elementos y cada
elementos del conjunto de k determinan cuál es el próximo cojunto a buscar
recursivamente. La complejidad de algoritmo es O(n).
El código del algoritmo es el siguiente:

# include < stdio .h >


# include < math .h >
# include < string .h >
# include < iostream >
# include < vector >
# include < list >
# include < string >
# include < algorithm >
# include < queue >
# include < stack >
# include <set >
# include <map >
# include < complex >
# define MAX_N 1000001
using namespace std ;
typedef long long lld ;

int n , k ;
int niz [ MAX_N ];

int kselect ( int left , int right , int k )


{
if ( left < right )
{
int pivotIndex = left ;
int pivot = niz [( left + right ) /2];
swap ( niz [( left + right ) /2] , niz [ right ]) ;
for ( int i = left ;i < right ; i ++)
{
if ( niz [ i ] < pivot )
{
swap ( niz [ pivotIndex ] , niz [ i ]) ;
pivotIndex ++;
}

162
BÚSQUEDA

}
swap ( niz [ pivotIndex ] , niz [ right ]) ;
if ( pivotIndex == k ) return niz [ pivotIndex ];
else if ( k < pivotIndex ) return kselect ( left ,
pivotIndex -1 , k ) ;
else return kselect ( pivotIndex +1 , right , k ) ;
}
else return niz [ left ];
}

int main ()
{
n = 5 , k = 3;
niz [0] = 4;
niz [1] = 2;
niz [2] = 5;
niz [3] = 1;
niz [4] = 3;
printf ( " %d \ n " , kselect (0 , n -1 , k ) ) ;
return 0;
}

Esta implementación devuelve el k-enésimo menor elemento dentro de la


colección donde k siempre debe estar en el rango de 0 a n-1 donde n es la
cantidad de elementos de la colección. En caso que queramos el k-enésimo
mayor elemento solo debemos cambiar el operador ¡por ¿en la lı́nea:

if ( niz [ i ] < pivot )

6.4. Búsqueda Exhaustiva


6.5. Análisis de ejercicios
1713 - Pie En este problema debemos darnos cuenta que nos piden ha-
llar el máximo volumen X por el cual yo puedo fraccionar cada uno de los
pasteles iniciales en subpasteles con un volumen igual o menor a X donde
la cantidad de subpasteles con el volumen igual a X sea igual o mayor que
la cantidad de amigos entre los cuales debes repartir los subpasteles. Im-
portante aquellos subpasteles cuyo volumen sea menor que X se deshechan,
nada de juntarlos o algo parecido. Expuesto esto es evidente que con una
búsqueda binaria podemos hallar la solución. El rango inicial de búsqueda

163
BÚSQUEDA

será desde 0 hasta Z donde Z es igual a la sumatoria de los volumenes de los


pasteles entre la cantidad de amigos. La busqueda se debe buscar mientras
la diferencia absoluta de los rangos sea mayor 10e-8. Para cada pivote (pivo-
te=(max rango+min rango)/2) que cumpla la condición se debe comparar
con uno de referencia y en caso de ser mayor actualizar la referencia con ese
valor.

1005 - Rent your Airplane and make Money La solución del pro-
blema parte de ordenar las órdenes de acuerdo al tiempo de culminación de
cada uno. Una vez ordenado debemos buscar para cada orden k cual orden i
donde la diferencia entre el tiempo culminación de la orden k y el tiempo de
comienzo de la orden i sea menor siempre con la restricción que el tiempo de
comienzo de la orden i sea mayor que el tiempo de culminación de la orden k,
esta búsqueda la podemos hacerde forma binaria para hacerla más eficiente.
Si existe una orden i entonces el precio de la orden i será igual al precio
de la orden k mas el precio de la orden i. Una vez hecho esto solo debemos
chequear entre la orden k y el orden k-1 cual precio es el mayor y actualizar
con ese valor el precio de la orden k. La solución del problema va estar en la
última orden de la lista ordenada.

3529 - How to See More Games La solución aplicada a este problema


es muy similar casi un 99 % a la explicada para el problema 1005 - Rent your
Airplane and make Money.

1593 - Soccer in 2014 La solución de este ejercicio es identica a las


soluciones de los ejercicios 3529 - How to See More Games y 1005 - Rent
your Airplane and make Money.

3346 - Bells Este ejercicio aunque en el jurado de COJ esta clasificado


como Ad-Hoc y Programación dinámica, mi solución se basa ordenamiento y
luego una búsqueda. El problema nos pide determinar cuantas campanas se
van oir en la casa de Eggsy de acuerdo al precio de venta del material aislante
y el monto de dinero con cuenta Eggsy para comprar. Por descripción del
problema cada capa aisla en una unidad. Lo primero que haremos es ordenar
las campanas de manera ascedente de acuerdo a su nivel de ruido. Luego para
cada posible precio del aislante la cantidad de aislante es igual a la parte
entera entre el dinero disponible y el precio, luego nos queda buscar cuantas
campanas son mayor que esa cantidad de aislante y esa será la respuesta para
ese precio. Para buscar la cantidad de campanas se usa utiliza el metodo
upper bound pasando como parametros el principio y final de la colección

164
BÚSQUEDA

de campanas y la cantidad de aislante. Luego la respuesta será la resta del


iterador que apunta al final de la colección de campana y el iterador que
devuelve la llamada a la función upper bound.

3469 - Generosity Game El problema nos habla de una colección de


donaciones que se hacen y un grupos de premios que se otorgan cuando
la donación acumulada mas la cantidad que se va a donar por alguien es
igual o mayor que una cantidad, a esa persona se le otorga un premio. El
problema nos pide hallar a que personas se le otorgaran los premios dadas
las cantidades que se van donar en un orden especifico y las cantidades por
las cuales se otorgarán premios. La solución del problema es bastante sen-
cilla basta con hacer una tabla acumulativa de la donaciones de la forma
tablai =donacióni +tablai−1 siendo tabla0 =donación0 y luego para cada pre-
mio es buscar el menor ı́ndice en tabla cuyo valor en esa posición sea igual o
mayor que el premio y esa será la respuesta a cada premio. Para optimizar
la búsqueda en caso de C++ se puede usar el método lower bound. Se debe
tener en cuenta que si se otorga un premio para una cantidad mayor que la
suma de todas las donaciones la respuesta es ”none”.

1258 - 365’s Family El problema es bien sencillo dado un rango de A a B


saber cuantos números en ese rango son de la forma XY donde X=(Y+1)2 .
Una solución es implementar una función que genere todos los numeros de
esa forma mientras sean menores que 263−1 y almacenarlos en una colección
de forma ordenada. Luego para saber cuantos hay en el rango que se nos pida
basta restar D-C donde D es el iterador que devuelve la función upper bound
pasandolé como parametros el inicio y fin de la colección de los números
generados y el valor B, mientras C es el iterador que devuelve la función
lower bound que de igual forma recibe como parámetros el inicio y fin de
la colección de números generados y el valor B. Recomiendo que antes de
leer la cantidad de casos invoque la función que genera los números, como
precálculo.

1332 - Gordon Conformity Una vez leido el problema. Podemos deter-


minar que nos piden hallar la cantidad de estudiantes que solicitaron el o los
cursos más populares. Lo primero que debemos ver es como se determina la
popularidad de un curso. La popularidad de un curso es igual a la cantidad de
estudiantes que lo escogen. Un curso esta integrado por cinco asignaturas y
de cada estudiantes se conoce las cinco asignaturas escogidas que conforman
el curso que desea pasar. Ahora detalle importante, tenemos al estudiantes
A y B que selecionaron las quintentas de asignaturas a,b,c,d,e y b,c,d,a,e,

165
BÚSQUEDA

tanto el estudiante A como el B selecionaron el mismo curso porque son las


mismas asignaturas aunque no estén en el mismo orden. Esto es un detalle
muy importante lo que nos obliga a tener una función que dada cualquiera
permutación de cinco asignaturas siempre genere el mismo identificador para
ese curso. Por restriciones del problema nos dicen que las asignaturas son
identificadas por un números único para cada una de ellas que oscila en el
rango de 100 a 499. Dicho esto propongo que el identificador de un curso sea
igual a :
idenficador=10004 *a+10003 *b+10002 *c+10001 *d+e
Donde a es la asignatura con el menor identificador de todas las asig-
naturas que conforman el curso, b es la asignatura con el segundo menor
identificador de todas las asignaturas que conforman el curso, c es la asigna-
tura con el tercer menor identificador de todas las asignaturas que conforman
el curso y asi sucesivamente con las asignaturas d ye.
Una vez resuelto el problema de como generar un idenficador único para
un curso independientemente de la permutación de las asignaturas que lo
conforman. Vamos a tener una estructura de tipo diccionario donde la clave
va ser el identificador del curso y el valor la cantidad de estudiantes que han
seleccionado ese curso. Luego por cada estudiante que sea capture las cincos
asignaturas que conforman su curso, se genera el identificador del curso se va
diccionario y en la clave que sea igual al identificador del curso se incrementa
en uno el valor. Luego solo tenemos que buscar la tupla en el diccionario
que su valor sea mayor, para luego volver a recorrer el diccionario y por
cada tupla que su valor sea igual al máximo valor encontrado incrementar la
variable que va imprimir la respuesta final en máximo valor.

2526 - More Triangles El problema nos plantea que dado un grupo de


puntos sobre el borde de una circunferencia de los cuales se sabe la longitud
del arco entre ellos, determinar la cantidad de triángulos equilátero que se
pueden construir con dichos puntos.
Para construir un triángulo equilátero con una trı́ada de dichos puntos la
longitud de los arcos entre ellos debe ser iguales.
Como las longitudes de los arcos que separan los puntos analizar son
enteros el perimetro de la circunferencia debe ser un valor entero. Al ser el
perimetro un valor entero K el mismo debe ser múltiplo de 3. Para poder
fraccionar el mismo en 3 arcos iguales. Tres puntos sobre la circunferencia
conformen un triángulo equilátero si la longitud de los arcos que lo separan
en igual a K/3 = L.
Para un punto X ubicado en la posición q del arco de la circunferencia (el
primero punto esta en la posición 0 , el segundo en X0 , el tercero X0 +X1 ,

166
BÚSQUEDA

...) podrá formar un triángulo equilátero si existe un punto Y en la posición


q + L ≤ K y un punto Z en la posición q + 2L ≤ K. Para saber si existe
los puntos Y y Z se puede aplicar una búsqueda binaria sobre un vector que
tenga almacenado la posición de los puntos en el perimetro de la circuferencia
de forma ascendente.

4121 - Scheduling Una vez leı́do el problema podemos ver que su mode-
lación y solución en muy identica a las soluciones de los ejercicios 3529 - How
to See More Games, 1005 - Rent your Airplane and make Money y 1593 -
Soccer in 2014 los cuales feuron explicados anteriormente en este apartado.

DMJO Números Primos de nuevo Una vez leido el problema nos pode-
mos percatar que le problema radica en dado un valor X debemos encontrar
el mayor número primo que sea menor que X y el menor número primo
mayor que X. Para solucionar el problema vamos a partir de calcular con
una criba todos los primos menores de 106 y almacenarlos en una colección
ordenados de forma ascedente. Una vez hecho esto si utilizamos C++ solo
debemos chequear si X es un número primo esto se puede hacer con una
búsqueda binaria sobre la colección de numeros primos precalculada. Si esto
ocurre entonces los dos números buscados es el propio X. En caso de X no
sea un número primo con las función lower bound como parámetros la colec-
ción de números primos y X encuentra el menor número primo mayor que X
mientras el mayor número primo menor que text va ser el primo anterior en
la colección del hallado con la función lower bound.

4126 - Prime Matrix El problema nos plantea hallar la menor cantidad


de movimientos que se debe hacer para que una matriz sea una matriz de
primo. El problema nos dice que una matriz de primo cuando la matriz
tiene al menos una fila o columna donde todos sus elementos son primos. El
movimiento permitido en la matriz es escoger algún elemento de la matriz e
incrementar en uno su valor. Esta operación se puede realizar varias veces y
sobre el mismo elementos si se desea. Para resolver el problema vamos crear
una matriz incremento con las mismas dimensiones y en la posición [i,j] de
esta matriz vamos a colocar cuantos incrementos debe recibir el elemento en
esa misma posición en la matriz de entrada para que sea primo. Luego solo
debemos buscar en la matriz incremento cual es columna o fila cuya suma de
todos sus elementos es menor y dicha suma es el valor que debe ser impreso.
Para mejorar la solución podemos precalcular todos los primos hasta 106 y
almacenarlos en un vector de forma ordenada ascedente de forma que para
para saber el primo superior mas cercano a un valor X podamos utilizar

167
BÚSQUEDA

dicho vector con la función upper bound.

168
Capı́tulo 7

Cadena

7.1. Operaciones básicas


A veces para resolver ejercicios con cadenas basta con remplazar dentro
una cadena una subcadena por otra. Otra operación es la fraccionar, frag-
mentar o picar una cadena por un determinada subcadena dentro de ella. Los
ejercicios dode debı́a realizar estas acciones optaba por realizarlo con Java
ya que este lenguaje su clase String cuenta con los métodos remplace y split
que son funciones que realizan las operaciones anteriormente mencionadas.
Pero en C++ como hacerlo ?. Pues bien aquı́ les dejo como realizar estas
operaciones.

vector < string > splitAll ( string s , string t ) {


vector < string > v ;
for ( int p =0; ( p = s . find ( t ) ) != s . npos ;) {
v . push_back ( s . substr (0 , p ) ) ;
s = s . substr ( p + t . size () ) ;
}
v . push_back ( s ) ;
return v ;
}

vector < string > split ( string s , string t ) {


vector < string > v ;
int p = s . find ( t ) ;
if ( p != s . npos ) {
v . push_back ( s . substr (0 , p ) ) ;
s = s . substr ( p + t . size () ) ;
}
v . push_back ( s ) ;

169
CADENA

return v ;
}

string replaceAll ( string s , string f , string t ) {


string r ;
for ( int p = 0; ( p = s . find ( f ) ) ! = s . npos ;) {
r += s . substr (0 , p ) + t ;
s = s . substr ( p + f . size () ) ;
}
return r + s ;
}

string replace ( string s , string f , string t ) {


string r ;
int p = s . find ( f ) ;
if ( p != s . npos ) {
r += s . substr (0 , p ) + t ;
s = s . substr ( p + f . size () ) ;
}
return r + s ;
}

7.2. Algoritmos de búsqueda de cadena


En múltiples problemas podemos encontrar que dada una cadena S nos
pide hallar si existe una subcadena T dentro de esta. No confundir subcadena
con secuencias de caracteres o subcadenas no concecutivas. Para resolver este
problema existen múltiples algoritmos los cuales veremos en los siguientes
subepı́grafes.

7.2.1. Algoritmo Knuth-Morris-Pratt (KMP)


El algoritmo KMP es un algoritmo de búsqueda de subcadenas simple y
por lo tanto su objetivo es buscar la existencia de una subcadena dentro de
una cadena. Para ello utiliza información basada en los fallos previos, apro-
vechando la información que la propia palabra a buscar contiene de sı́ (sobre
ella se precalcula una tabla de valores), para determinar donde podrı́a darse
la siguiente existencia, sin necesidad de analizar más de 1 vez los caracteres
de la cadena donde se busca.
El algoritmo originalmente fue elaborado por Donald Knuth y Vaughan

170
CADENA

Pratt y de modo independiente por James H. Morris en 1977, pero lo publi-


caron juntos los tres.
El algoritmo KMP, trata de localizar la posición de comienzo de una
cadena, dentro de otra. Antes que nada con la cadena a localizar se precalcula
una tabla de saltos (conocida como tabla de fallos) que después al examinar
entre si las cadenas se utiliza para hacer saltos cuando se localiza un fallo.
Supongamos una tabla ’F’ ya precalculada, y supongamos que la cadena
a buscar esté contenida en el array ’P()’, y la cadena donde buscamos esté
contenida en un array ’T()’. Entonces ambas cadenas comienzan a compa-
rarse usando un puntero de avance para la cadena a buscar, si ocurre un fallo
en vez de volver a la posición siguiente a la primera coincidencia, se salta
hacia donde sobre la tabla, indica el puntero actual de avance de la tabla.
El array ’T’ utiliza un puntero de avance absoluto que considera donde se
compara el primer carácter de ambas cadenas, y utiliza como un puntero
relativo (sumado al absoluto) el que utiliza para su recorrido el array ’P’. Se
dan 2 situaciones:

1. Mientras existan coincidencias el puntero de avance de ’P’, se va incre-


mentando y si alcanza el final se devuelve la posición actual del puntero
del array ’T’.
2. Si se da un fallo, el puntero de avance de ’T’ se actualiza hasta, con la
suma actual del puntero de ’P’ + el valor de la tabla ’F’ apuntado por
el mismo que ’P’. A continuación se actualiza el puntero de ’P’, bajo
una de 2 cicunstancias; Si el valor de ’F’ es mayor que -1 el puntero
de ’P’, toma el valor que indica la tabla de salto ’F’, en caso contrario
vuelve a recomenzar su valor en 0.

Descripción de la tabla adicional (conocida como ’función de fallo’)


El objetivo de la tabla (precalculada) de fallo ’F’ es no permitir que cada
carácter del array ’T()’ sea examinado más de 1 vez. El método clave para
lograr esto, consiste en haber comprobado algún trozo de la cadena donde se
busca con algún trozo de la cadena que se busca, lo que nos proporciona en
qué sitios potenciales puede existir una nueva coincidencia, sobre el sector
analizado que indica fallo.
Dicho de otro modo, partiendo del texto a buscar, elaboramos una lista
con todas las posiciones, de salto atrás que señalen cuanto se retrocede desde
la posición actual del texto a buscar. Por ejemplo si el texto a buscar es
’esconderse’ y estamos examinando un texto como ’se esconden tras la mesa’,
cuando llegamos a la 2a ’n’ de ’esconden’ (posición 7 en el texto a buscar
es una ’r’), falla, la pregunta lógica serı́a ¿ dónde se encuentra de nuevo

171
CADENA

(si existe) la primera letra en el texto ’esconderse’(antes del fallo), y hasta


donde logra repetirse ?. La respuesta a esta pregunta será el punto de salto,
en el caso propuesto (’esconderse’). Dicho punto se encuentra en la posición 6
(antes de la ’r’), luego para la tabla en la siguiente posición deberı́a de haber
un 1.
Por tanto esta tabla se confecciona con la distancia que existe desde un
punto en la palabra a la última ocurrencia (de la 0a letra de la palabra)
distinta de la primera vez que aparece,y mientras sigan coincidiendo, se marca
la distancia, cuando haya una ruptura de coincidencia se marca 0 o un valor
previo ya calculado anteriormente, y ası́ sucesivamente hasta terminar con el
texto.
La tabla tiene sus 2 primeros valores fijados, de modo que la función de
fallo empieza siempre examinando el 3 carácter del texto. La razón por la
que dichos valores están fijados es obvia: si para el 2o carácter se marcara 1,
nunca se lograrı́a un salto, pues siempre retornarı́a a dicho punto. En cuanto
al primero, por necesidad se marca -1, pues de ese modo le es imposible
regresar más atrás, sino siempre adelante.
Debido a que el algoritmo precisa de 2 partes donde se analiza una cadena
en cada parte, la complejidad resultante es O(k) y O(n), cuya suma resulta
ser O(n + k).

int * buildFail ( char * p)


{
int m = strlen ( p ) ;
int * fail = new int [ m + 1];
int j = fail [0] = - 1;
for ( int i = 1; i <= m; + + i) {
while (j > = 0 && p [ j ]! = p [i -1])
j = fail [ j ];
fail [ i ] = ++ j ;
}
return fail ;
}

int match ( char * t , char * p , int * fail )


{
int n = strlen ( t ) , m = strlen ( p ) ;
int count = 0;
for ( int i = 0 , k = 0; i <n ; ++ i )
{
while (k >=0 && p [ k ]!= t [ i ])
k = fail [ k ];

172
CADENA

if (++ k >= m )
{
++ count ; // match at t [i - m + 1 .. i ]
k = fail [ k ];
}
}
return count ;
}

El valor devuelvo por el método match es la cantidad de ocurrencia de


la cadena de la variable p en la cadena de la variable t. Otra variante del
mismo algoritmo es la siguiente.

vector < int > KMP ( string _text , string _pattern )


{
vector < int > matches ;

int * P = new int [ MAX ]; /* MAX debe ser un valor mayor


que la maxima longitud de las cadenas */

int n_pattern = _pattern . size () ;


int n_text = _text . size () ;
for ( int i =0; i < n_pattern ; i ++)
P [ i ] = -1;
for ( int i =0 , j = -1; i < n_pattern ;)
{
while ( j > -1 && _pattern [ i ] != _pattern [ j ])
j = P [ j ];
i ++;
j ++;

P[i] = j;
}

for ( int i =0 , j =0; i < n_text ;)


{
while ( j > -1 && _text [ i ] != _pattern [ j ])
j = P [ j ];
i ++;
j ++;
if ( j == n_pattern )
{
matches . push_back ( i - n_pattern ) ;

173
CADENA

j = P [ j ];
}
}
return matches ;
}

Como se puede apreciar el algoritmo descripto aquı́ devuelve un vector


donde están las posiciones donde comienzan las ocurrencias de la cadena de
la variable pattern en la cadena de la variable text. En caso que dicho vector
este vació significa que la subcadena T no se encuentra dentro de la cadena
S.
Como podemos apreciar a pesar que se explicó que el KMP esta destinado
a saber si una cadena T esta dentro de una cadena S como subcadena el
algoritmo mostrado aprovecha algunos elementos utilizados por el algoritmo
y realiza una pequeña modificación devolviendo un vector con las posiciones
dentro de la cadena S donde comienzan las ocurrencias de la cadena T.

7.2.2. String searching (Boyer-Moore)


El algoritmo de búsqueda de cadenas Boyer-Moore es un particularmente
eficiente algoritmo de búsqueda de cadenas, y ha sido el punto de referencia
estándar para la literatura de búsqueda de cadenas práctica. Fue desarro-
llado por Bob Boyer y J Strother Moore en 1977. El algoritmo preprocesa
la cadena objetivo (clave) que está siendo buscada, pero no en la cadena en
que se busca (no como algunos algoritmos que procesan la cadena en que
se busca y pueden entonces amortizar el coste del preprocesamiento median-
te búsqueda repetida). El tiempo de ejecución del algoritmo Boyer-Moore,
aunque es lineal en el tamaño de la cadena siendo buscada, puede tener un
factor significativamente más bajo que muchos otros algoritmos de búsque-
da: no necesita comprobar cada carácter de la cadena que es buscada, puesto
que salta algunos de ellos. Generalmente el algoritmo es más rápido cuanto
más grande es la clave que es buscada, usa la información conseguida desde
un intento para descartar tantas posiciones del texto como sean posibles en
donde la cadena no coincida.
A la gente frecuentemente le sorprende el algoritmo de Boyer-Moore,
cuando lo conoce, porque en su verificación intenta comprobar si hay una
coincidencia en una posición particular marchando hacia atrás. Comienza
una búsqueda al principio de un texto para la palabra ANPANMAN, por
ejemplo, comprueba que la posición octava del texto en proceso contenga
una N. Si encuentra la N, se mueve a la séptima posición para ver si contie-
ne la última A de la palabra, y ası́ sucesivamente hasta que comprueba la

174
CADENA

Figura 7.1: La X en la posición 8 excluye todas la 8 posibles posiciones de


comienzo mostradas.

primera posición del texto para una A.


La razón por la que Boyer-Moore elige este enfoque está más clara cuando
consideramos que pasa si la verficación falla-por ejemplo, si en lugar de una
N en la octava posición, encontramos una X. La X no aparece en ANPAN-
MAN, y esto significa que no hay coincidencia para la cadena buscada en el
inicio del texto o en las siguientes siete posiciones, puesto que todas falları́an
también con la X. Después de comprobar los ocho caracteres de la palabra
ANPANMAN para tan sólo un carácter X, seremos capaces de saltar hacia
delante y comenzar buscando una coincidencia en el final en la 16.a posición
del texto.
Esto explica por qué el rendimiento del caso promedio del algoritmo,
para un texto de longitud n y patrón fijo de longitud m , es n/m en el mejor
caso, solo uno en caracteres necesita ser comprobado. Esto también explica
el resultado algo contra-intuitivo de que cuanto más largo es el patrón que
estamos buscando, el algoritmo suele ser más rápido para encontrarlo.
El algoritmo precalcula dos tablas para procesar la información que ob-
tiene en cada verificación fallada: una tabla calcula cuantas posiciones hay
por delante en la siguiente búsqueda basada en el valor del carácter que no
coincide; la otra hace un cálculo similar basado en cuantos caracteres coin-
cidieron satisfactoriamente antes del intento de coincidencia fallado. (Puesto
que estas dos tablas devuelven resultados indicando cuán lejos ”saltar”hacia
delante, son llamada en ocasiones ”tablas de salto”, que no deberı́an ser con-
fundidas con el significado más común de tabla de saltos en ciencia de la
computación.) El algoritmo se desplazará con el valor más grande de los dos
valores de salto cuando no ocurra una coincidencia.

175
CADENA

Tabla primera
Rellénese la primera tabla como sigue. Para cada i menor que la longitud
de la cadena de búsqueda, constrúyase el patrón consistente en los últimos i
caracteres de la cadena precedida por un carácter no-coincidente, alinéense
a la derecha el patrón y la cadena, y anótese el menor número de caracteres
para que el patrón tenga que desplazarse a la izquierda para una coincidencia.
Por ejemplo, para la búsqueda de la cadena ANPANMAN, la tabla serı́a
como sigue: (NMAN significa una subcadena en ANPANMAN consistente en
un carácter que no es ’N’ más los caracteres ’MAN’.)

Figura 7.2: La .A”no coincidente en la posición 5 (3 atrás desde la última


letra de la aguja) excluye las primeras 6 de las posibles posiciones iniciales
mostradas.
La cantidad de desplazamiento calculada por la primera tabla es a veces
llamada ”desplazamiento de sufijo bueno.o regla de sufijo bueno (fuerte)”.
El algoritmo original Boyer-Moore publicado usa una más simple, más débil,
versión de la regla de sufijo bueno en que cada entrada en tabla de arriba no
requiere una no-coincidencia para el carácter de más a la izquierda. Esto es
a veces llamado regla del sufijo bueno débil no es suficiente para conseguir
2

que Boyer-Moore funcione en tiempo lineal en el peor caso.

Tabla segunda
La segunda tabla es fácil de calcular: iniciése en el último carácter de
la cadena vista y muévase hacia el primer carácter. Cada vez que usted se
mueve a la izquierda, si el carácter sobre el que está no está ya en la tabla,
añádalo; su valor de desplazamiento es la distancia desde el carácter más a
la derecha. Todos los otros caracteres reciben un valor igual a la longitud de
la cadena de búsqueda.
Para la cadena ANPANMAN, la segunda tabla serı́a como se muestra
(por claridad, las entradas son mostradas en el orden que serı́an añadidas a

176
CADENA

i Patrón Desplazamiento a la izquierda


0 N Es cierto que la letra siguiente a la izquierda
en ’ANPANMAN’ no es N (es A), de aquı́ que
el patrón N debe desplazarse una posición a la
izquierda para una coincidencia; por tanto = 1
1 AN AN no es una cadena en ANPANMAN, por tan-
to : el desplazamiento izquierdo es el número de
letras en ’ANPANMAN’ = 8
2 MAN Subcadena MAN coincide con ANPANMAN
tres posiciones a la izquierda. Por tanto despla-
zamiento a la izquierda = 3
3 NMAN Vemos que ’NMAN’ no es una subcadena de
’ANPANMAN’ pero ’NMAN’ es una posible
subcadena 6 posiciones más a la izquierda :
(’NMANPANMAN’); por tanto = 6
4 ANMAN 6
5 PANMAN 6
6 NPANMAN 6
7 ANPANMAN 6

la tabla): (La N que se supuestamente serı́a cero está basada en la segunda


N desde la derecha porque solo anotamos el cálculo para las primeras m-1
letras)
La cantidad de desplazamiento calculada por la segunda tabla es a veces
llamada ”desplazamiento de carácter malo”
El caso peor para encontrar todas las coincidencias en un texto necesita
aproximadamente 3n comparaciones,de aquı́ que la complejidad sea O(n) ,
a pesar de que el texto contenga una coincidencia o no. Esta prueba llevó
algunos años para desarrollarse. En el año en que se ideó el algoritmo, 1977,
se mostró que el número máximo de comparaciones no era más de 6n en 1980

Carácter Desplazamiento
A 1
M 2
N 3
P 5
caracteres restantes 8

177
CADENA

se demostró que no era más de 4n hasta el resultado de Cole.


A continuación una implementación del algoritmo

int * buildSkip ( string _pattern ) {


int m = _pattern . size () ;
int * skip = new int [0 x100 ];
for ( int i =0; i < m ;++ i )
skip [ _pattern [ i ]] = m -i -1;
return skip ;
}

int * buildNext ( string _pattern ) {


int m = _pattern . size () ;
int g [ m ]; fill (g , g +m , m ) ;
int * next = new int [ m ];
for ( int i =0; i < m ;++ i )
next [ i ] = 2* m -i -1;
int j = m ;
for ( int i =m -1; i >=0; --i , --j ) {
g[i] = j;
while (j < m && _pattern [ j ]!= _pattern [ i ]) {
next [ j ]= min ( next [ j ] , m -i -1) ;
j = g [ j ];
}
}
for ( int i =0; i < m ;++ i ) {
next [ i ] = min ( next [ i ] , j +m - i ) ;
if ( i >= j ) j = g [ j ];
}
return next ;
}

int boyerMoore ( string _text , string _pattern , int * skip ,


int * next ) {
int n = _text . size () , m = _pattern . size () ;
int count = 0;
for ( int i =m -1; i < n ; ) {
int j = m -1;
while ( j >= 0 && _text [ i ] == _pattern [ j ]) --i , --j ;
if (j <0) {
++ count ; // match at text [ i +1 , ... , i + m ]
i += m +1;
} else i += max ( skip [ _text [ i ]] , next [ j ]) ;

178
CADENA

}
return count ;
}

Estas implementación tiene diferentes complejidades en dependencia de


su uso. La construcción tiene una complejidad de O(m) siendo m la canti-
dad de caracteres el patrón a buscar. Si se utilizará para encontrar la primera
ocurrencia del patrón en el texto la complejidad es O(m+n) siendo m la can-
tidad de caracteres el patrón a buscar y n la cantidad de caracteres del texto
donde se busca. En caso que se buscará todas las ocurrencias la complejidad
O(mn)
El algoritmo de búsqueda de cadenas Boyer-Moore es utilizado para bus-
car patrones dentro de una cadena, siendo ideal cuando el patrón a buscar es
grande. Para ejercicios de hallar la primera ocurrencia de una larga cadena
es más rápido. Sin embargo, hallar todas las ocurrencias de un patrón de una
larga cadena KMP es mejor.

7.3. Palı́ndromo
Un palı́ndromo (del griego palin dromein, volver a ir hacia atrás) es una
palabra, número o frase que se lee igual hacia adelante que hacia atrás. Si se
trata de un número, se llama capicúa. Habitualmente, las frases palindrómi-
cas se resienten en su significado cuanto más largas son. Muchos son los
problemas de cadenas que involucra este concepto.

7.3.1. Longest palindrome (Manacher)


El algoritmo Manacher permite dentro de una cadena de carácteres hallar
la longitud de la subsecuencia máxima concecutiva que es palı́ndrome. Lo
interesante de este algoritmo es que lo realiza en un tiempo O(n) lo cual los
hace muy eficiente. Su funcionamiento parte de calcular para cada carácter
de la cadena el radio palı́ndrome que tiene como centro ese carácter, veamos
el siguiente ejemplo:
asdbaabasd
1111113111
Una vez analizado esto nos puede surgir la siguiente duda. Y si la máxima
longitud de un palı́ndrome dentro de la cadena es par como en el ejemplo visto
asdbaabasd es capaz de resolver esta situación el Manacher. Si el algoritmo es
capaz de resolverlo y lo hace insertado un carácter no presente en la cadena
quedando el procedimiento de la siguiente manera.

179
CADENA

a]s]d]b]a]a]b]a]s]d
1010101014103010101

Ahora cuando se toma como carácter centro el insertado se esta calculando


la longitud para el palı́ndrome de longitud par, tener en cuenta que cuando
se realiza el cálculo para un carácter insertado solo se cuenta los carácteres
de la cadena original. Luego solo debemos recorrer este arreglo y devolver
el máximo valor encontrado y esta será la longitud del máximo palı́ndrome
encontrado en la cadena original. En caso de que queramos extraer la cadena
se hace el siguiente el procedimiento:

Se tiene r[i] donde r[i] es la longitud del palı́ndrome encontrado en la


cadena y i la posición en el arreglo r.

Si i es par entonces el principio de la cadena palı́ndrome empieza en


i/2 - r[i]/2 y termina en la posición i/2 + r[i]/2 de la cadena original.

Si i es impar entonces el principio de la cadena palı́ndrome empieza


en i/2 - r[i]/2 +1 y termina en la posición i/2 + r[i]/2 de la cadena
original.

Ahora veamos la implementaión del algoritmo:

int manacher ( string _text )


{
int n = _text . size () ;
int rad [2* n ];
int i ,j , k ;
for ( i =0 , j =0; i <2* n ; i += k , j = max (j -k ,0) )
{
while (i -j >=0 && i + j +1 <2* n && _text [( i - j ) /2]== _text [(
i + j +1) /2])
++ j ;
rad [ i ]= j ;
for ( k =1; i -k >=0 && rad [ i ] -k >=0 && rad [i - k ]!= rad [ i ] - k
;++ k )
rad [ i + k ]= min ( rad [i - k ] , rad [ i ] - k ) ;
}
return * max_element ( rad , rad +(2* n ) ) ;
}

180
CADENA

7.4. Hashing de cadenas


A las función hash, también se les llama funciones picadillo, funciones
resumen o funciones de digest.
Una función hash es un método para generar claves o llaves que repre-
senten de manera casi unı́voca a un documento o conjunto de datos. Es una
operación matemática que se realiza sobre este conjunto de datos de cualquier
longitud, y su salida es una huella digital, de tamaño fijo e independiente de
la dimensión del documento original. El contenido es ilegible.
Una función hash H es una función computable mediante un algoritmo,
actúa como una proyección del conjunto U sobre el conjunto M.
Observa que M puede ser un conjunto definido de enteros. En este caso
podemos considerar que la longitud es fija si el conjunto es un rango de
números enteros ya que podemos considerar que la longitud fija es la del
número con mayor cantidad de cifras. Todos los números se pueden convertir
al número especificado de cifras simplemente anteponiendo ceros.
Normalmente el conjunto U tiene un número elevado de elementos y M es
un conjunto de cadenas con un número relativamente pequeño de sı́mbolos.
Por esto se dice que estas funciones resumen datos del conjunto dominio.
La idea básica de un valor hash es que sirva como una representación
compacta de la cadena de entrada. Por esta razón decimos que estas funciones
resumen datos del conjunto dominio. Los algoritmos de hash son útiles para
resolver muchos problemas de cadenas.
El problema que queremos resolver es el problema, queremos comparar
cadenas de manera eficiente. La forma de fuerza bruta de hacerlo es sim-
plemente comparar las letras de ambas cadenas, que tiene una complejidad
de tiempo de O(mı́n(n1 , n2 )) si n1 y n2 son los tamaños de las dos cadenas.
Queremos hacerlo mejor. La idea detrás de las cadenas es la siguiente: con-
vertimos cada cadena en un entero y las comparamos en lugar de las cadenas.
La comparación de dos cadenas es una operación O(1).
Para la conversión necesitamos una llamada función hash. El objetivo de
esto es convertir una cadena en un entero, el llamado hash de la cadena. La
siguiente condición debe mantenerse: si dos cadenas s y t son iguales (s = t),
entonces sus hashes deben ser iguales (hash(s) = hash(t)). De lo contrario
no podremos comparar cadenas.
Note, la dirección opuesta no tiene que mantenerse. Si los hashes son igua-
les (hash(s) = hash(t)), entonces las cadenas no tienen por qué ser iguales.
Por ejemplo una función hash válida serı́a simplemente hash(s) = 0 para
cada s. Ahora, esto es solo un ejemplo estúpido, porque esta función será
completamente inútil, pero es una función hash válida. La razón por la que
no es necesario mantener la dirección opuesta, porque porque hay muchas

181
CADENA

cadenas exponenciales. Si solo queremos que esta función hash distinga en-
tre todas las cadenas que consisten en caracteres en minúscula de longitud
menor a 15, entonces el hash ya no cabrı́a en un entero de 64 bits (por ejem-
plo, sin signo largo largo), porque hay muchos de ellos. Y, por supuesto, no
queremos comparar enteros largos arbitrarios, porque esto también tendrá la
complejidad O(n).
Por lo general, queremos que la función hash asigne cadenas a números
de un rango fijo [0, m), luego, comparar cadenas es solo una comparación
de dos enteros con una longitud fija. Y, por supuesto, queremos que hash
hash(s) 6= hash(t) sea muy probable, si s 6= t.
Esa es la parte importante que tienes que tener en cuenta. El uso de hash
no será 100 % determinı́sticamente correcto, porque dos cadenas diferentes
pueden tener el mismo hash (los hashes chocan). Sin embargo, en una am-
plia mayorı́a de problemas donde son utilizados, esto puede ser ignorado de
manera segura ya que la probabilidad de que dos hashes diferentes colisio-
nen es muy pequeña. Mas adelante discutiremos algunas técnicas sobre cómo
mantener muy baja la probabilidad de colisiones.

7.4.1. Cálculo del hash de una cadena


La forma buena y ampliamente utilizada de definir el hash de una cadena
s de longitud n es:

n−1
X
hash(s) = s[0]+s[1]·p+s[2]·p2 +...+s[n−1]·pn−1 mod m = s[i]·pi mod m
i=0

donde p y m son algunos números positivos elegidos. Se llama una función


de hash rodante polinomial.
Es razonable hacer que p sea un número primo aproximadamente igual
al número de caracteres en el alfabeto de entrada. Por ejemplo, si la entrada
está compuesta solo de letras minúsculas del alfabeto inglés, p = 31 es una
buena opción. Si la entrada puede contener letras mayúsculas y minúsculas,
entonces p = 53 es una opción posible. El código en este artı́culo usará p =
31.
Obviamente, m deberı́a ser un número grande, ya que la probabilidad de
que dos cadenas aleatorias colisionen es de aproximadamente ≈ m1 . A veces
se elige m = 264 , ya que los desbordamientos de enteros de enteros de 64
bits funcionan exactamente igual que la operación de módulo. Sin embargo,
existe un método que genera cadenas en colisión (que funcionan indepen-
dientemente de la elección de p). Ası́ que en la práctica no se recomienda

182
CADENA

m = 264 . Una buena opción para m es un número primo grande. El código


en este artı́culo solo usará m = 109 + 9. Este es un número grande, pero aún
lo suficientemente pequeño para que podamos realizar la multiplicación de
dos valores utilizando enteros de 64 bits.
Aquı́ hay un ejemplo de cálculo del hash de una cadena s, que contiene
solo letras minúsculas. Convertimos cada carácter de s en un entero. Aquı́
usamos la conversión a → 1, b → 2, . . . , z → 26. Convertir a → 0 no es una
buena idea, porque entonces los hashes de las cadenas a, aa, aaa, ... todos se
evalúan como 0.

long long computeHash ( string s ) {


const long long p = 31;
const long long m = 1 e9 + 9;
long long hashValue = 0;
long long pPow = 1;
int ncharacter = s . size () ;
char c ;
for ( int i =0; i < ncharacter ; i ++) {
c = s [ i ];
hashValue = ( hashValue + ( c - ’a ’ + 1) * pPow ) % m
;
pPow = ( pPow * p ) % m ;
}
return hashValue ;
}

Precomputar las potencias de p podrı́a mejorar el rendimiento de la an-


terior implementación. Alguna de las aplicaciones tı́picas de Hashing:

1. Algoritmo de Rabin-Karp para la coincidencia de patrones en una ca-


dena en O(n).

2. Cálculo del número de diferentes subcadenas de una cadena en O(n2 log n).

3. Cálculo del número de subcadenas palindrómicas en una cadena.

7.4.2. Colisión
Una colisión ocurre cuando dos valores de entrada diferente generan el
mismo resumen. Una función hash debe ser resistente a la colisión.
El mismo hash siempre será el resultado de los mismos datos (funciones
deterministas), pero la modificación de la información, aunque sea un solo
bit dará como resultado un hash totalmente distinto.

183
CADENA

La idea básica de un valor hash es que sirva como una representación


compacta de la cadena de entrada. Cuando dos claves se apuntan a la misma
dirección o bucket se dice que hay una colisión y a las claves se les denomina
sinónimos.
Formas de disminuir el número de colisiones y garantizar el funcionamien-
to de la función:

Esparcir los registros.

Usar memoria adicional.

Colocar más de un registro en una dirección (Compartimientos).

Muy a menudo, el hash polinomial mencionado anteriormente es lo sufi-


cientemente bueno, y no habrá colisiones durante las pruebas. Recuerde, la
probabilidad de que ocurra una colisión es solo ≈ m1 . Para m = 109 + 9 la
probabilidad es ≈ 10−9 , que es bastante baja. Pero note, que solo hicimos
una comparación. ¿Qué pasa si comparamos una cadena s con 106 cadenas
diferentes. La probabilidad de que ocurra al menos una colisión ahora es
≈ 10−3 . Y si queremos comparar 106 cadenas diferentes entre sı́ (por ejem-
plo, contando cuántas cadenas únicas existen), entonces la probabilidad de
que ocurra al menos una colisión ya es ≈ 1. Está bastante garantizado que
la función terminará con una colisión y devolverá el resultado incorrecto.
Hay un truco muy fácil para obtener mejores probabilidades. Podemos
simplemente calcular dos hashes diferentes para cada cadena (usando dos
p diferentes y/o m diferentes, y comparar estos pares en su lugar. Si m es
aproximadamente 109 para cada una de las dos funciones hash, entonces esto
es más o menos equivalente a teniendo una función hash con m ≈ 1018 . Al
comparar 106 cadenas entre sı́, la probabilidad de que ocurra al menos una
colisión ahora se reduce a ≈ 10−6 .
A continuación veremos algunas situaciones donde el uso de hashing me-
jora el rendimiento de nuestra solución.

7.4.3. Buscar cadenas duplicadas en un arreglo de ca-


denas
Problema: dada una lista de n cadenas si , cada una de no más de m
caracteres, encuentre todas las cadenas duplicadas y divı́dalos en grupos.
Del algoritmo obvio que implica la clasificación de las cadenas, obtendrı́amos
una complejidad de tiempo de O(nm log n) donde la clasificación requiere
comparaciones de O(n log n) y cada comparación lleva tiempo de O(m). Sin

184
CADENA

embargo, mediante el uso de hashes, reducimos el tiempo de comparación a


O(1), lo que nos da un algoritmo que se ejecuta en el tiempo O(nm+n log n).
Calculamos el hash para cada cadena, ordenamos por los indices de las
cadenas por el hashes y los grupos se forma por cadenas con identico hashes.

vector < vector < int > > g r o u p _ i d e n t i c a l _ s t r i n g s ( vector <


string > const & s ) {
int n = s . size () ;
vector < pair < long long , int > > hashes ( n ) ;
for ( int i =0; i < n ; i ++)
hashes [ i ]={ compute_hash ( s [ i ]) ,i };
sort ( hashes . begin () , hashes . end () ) ;
vector < vector < int > > groups ;
for ( int i =0; i < n ; i ++) {
if ( i ==0 || hashes [ i ]. first != hashes [i -1]. first )
groups . emplace_back () ;
groups . back () . push_back ( hashes [ i ]. second ) ;
}
return groups ;
}

7.4.4. Cálculo hash rápido de subcadenas de una ca-


dena dada
Problema: Dada una cadena s y los ı́ndices i y j, encuentre el hash de la
subcadena s[i . . . j].
Por definición, tenemos:
j
X
hash(s[i . . . j]) = s[k] · pk−i mod m
k=i
i
Multiplicando por p tenemos:
j
X
i
hash(s[i . . . j])·p = s[k]·pk mod m = hash(s[0 . . . j])−hash(s[0 . . . i−1])mod m
k=i

Entonces, al conocer el valor de hash de cada prefijo de la cadena s,


podemos calcular el hash de cualquier subcadena utilizando esta fórmula di-
rectamente. El único problema que enfrentamos al calcularlo es que debemos
ser capaces de dividir el hash(s[0 . . . j]) − hash(s[0 . . . i − 1]) por pi . Por lo
tanto, necesitamos encontrar el inverso multiplicativo modular de pi y luego

185
CADENA

realizar la multiplicación con este inverso. Podemos calcular previamente la


inversa de cada pi , lo que permite calcular el hash de cualquier subcadena de
s en O(1).
Sin embargo, existe una manera más fácil. En la mayorı́a de los casos,
en lugar de calcular los hashes de la subcadena exactamente, es suficiente
para calcular el hash multiplicado por algún poder de p. Supongamos que
tenemos dos hashes de dos subcadenas, una multiplicada por pi y la otra por
pj . Si i < j entonces multiplicamos el primer hash por pj−i , de lo contrario
multiplicamos el segundo hash por pi−j . Al hacer esto, obtenemos los hashes
multiplicados por la misma potencia de p (que es el máximo de i y j) y
ahora estos hashes se pueden comparar fácilmente sin necesidad de ninguna
división.

7.4.5. Determine el número de subcadenas diferentes


en una cadena
Problema: dada una cadena s de longitud n, que consiste solo en letras
minúsculas en inglés, encuentre el número de subcadenas diferentes en esta
cadena.
Para resolver este problema, iteramos sobre todas las longitudes de sub-
cadena l = 1 . . . n. Para cada longitud de subcadena l construimos un arreglo
de hashes de todas las subcadenas de longitud l multiplicada por la misma
potencia de p. El número de elementos diferentes en el arreglo es igual al
número de subcadenas distintas de longitud l en la cadena. Este número se
añade a la respuesta final.
Para mayor comodidad, utilizaremos h[i] como hash del prefijo con ca-
racteres i , y definiremos h[0] = 0.

int c o u n t _ u n i q u e _ s u b s t r i n g s ( string const & s ) {


int n = s . size () ;
const int p = 31;
const int m = 1 e9 + 9;
vector < long long > p_pow ( n ) ;
p_pow [0] = 1;
for ( int i = 1; i < n ; i ++)
p_pow [ i ] = ( p_pow [i -1] * p ) % m ;

vector < long long > h ( n + 1 , 0) ;


for ( int i = 0; i < n ; i ++)
h [ i +1] = ( h [ i ] + ( s [ i ] - ’a ’ + 1) * p_pow [ i ]) % m ;

186
CADENA

int cnt = 0;
for ( int l = 1; l <= n ; l ++) {
set < long long > hs ;
for ( int i = 0; i <= n - l ; i ++) {
long long cur_h = ( h [ i + l ] + m - h [ i ]) % m ;
cur_h = ( cur_h * p_pow [n -i -1]) % m ;
hs . insert ( cur_h ) ;
}
cnt += hs . size () ;
}
return cnt ;
}

7.5. Expresiones regulares


7.6. Longest repeated substring (Karp Miller
Rosenberg)
7.7. Recursive-descent parsing (LL (1))
7.8. Squares determination
7.9. Z Algorithm
7.10. Multi-pattern search (Aho-Corasick)
7.11. String search (Shift And)
7.12. Two-dimensional string search (Baker-
Bird)
7.13. Análisis de ejercicios
3401 - Find the Replacements Haciendo un KMP teniendo como patrón
a buscar dentro de la cadena la palabra que disgusta. El KMP me devuelve
un vector de las posiciones iniciales donde se encontró aquellas subsecuencias

187
CADENA

dentro del texto que coinciden con el patrón recorro cada posición del vector y
pidiendo de la cadena la subcadena (vector[i], vector[i]+longitud del patrón)
compruebo que esa subcadena sea igual a la palabra que disgusta si ocurre
esto cuento un remplazo y en la última posición de esta subcadena sustituyo
el carácter por el de interrogación.

2250 - Substring Frequency Haciendo un KMP y devuelvo la longitud


del vector matches.

3507 - Near-Palindrome El problema plantea que dada una palabra de-


finir si la misma es palı́ndrome, near-palı́ndrome o ninguna de las dos tipos
anteriores. La solución es muy simple solo debemos implentar una función
que reciba una cadena y diga si o no es palidrome la cadena que reciba por
parámetro, esta nos servirá para determinar si las cadenas de las entradas
son del primer tipo de cadena (palı́ndrome). En caso del segundo tipo la
cadena la cumple cuando se elimina un carácter y la cadena resultante es
palı́ndrome, esto significa que si se tiene una cadena de longitud N se debe
realizar N comprobaciones para esa cadena y con sola una comprobación que
de afirmativa, la cadena es de segundo tipo. En caso que la cadena no cumpla
ni con tipo uno ni dos pues es del tercer tipo.

3435 - Swords Without Words Un ejercicio sencillo donde solo debe-


mos determinar dentro de una colección palabras todas aquellas palabrasi
que está contenida dentro de una palabraj . Siendo i y j menores que la lon-
gitud que la cantidad de palabras y mayores o iguales a cero pero diferentes
entre sı́. Donde la longitud de la palabrai es menor que la longitud que la
longitud de la palabraj . Cuando encontremos una tupla que cumpla con estas
condiciones podemos conformar una palabrak que es igual a la concatenación
de las palabras i y j en ese orden con un espacio entre ellas. La palabrak la
almacenaremos en otra colección que luego ordenaremos e imprimiremos en
ese orden.

3939 - Counting substrings Un ejercicio bastante sencillo que se puede


hacer por fuerza bruta y el método substring y generar todas las posibles
cadenas las cuales la podemos almacenar en una estructura que no permi-
ta repetición como puede ser un conjunto y luego imprimir la cantidad de
elementos de este.

3941 - Classifying palindromes El ejercicio nos pide que clasifiquemos


una lista de palabras en palindromes, bipalindromes y no palindrome. Se nos

188
CADENA

aclara que una palabra palindrome es aquella que se lee lo mismo de derecha
a izquierda o viceversa. Mientras una palabra bipalindrome es aquella que no
es palindrome pero puede ser dividida en exactamente en dos bipalindrome,
mientras una palabra no palindrome es aquella que no es ninguna de las dos
anteriores. Dicho esto no es dificil ver que podemos hacer dos metodos uno
para que chequee si la palabra es palindrome y otro para que cheque si es
bipalindrome. En caso del caso basta con copiar el valor para otra variable
aplicarle el método reverse1 a la copia y luego compararla con la original.
En caso del método que me determina si la palabra es bipalindrome vamos
hacer el siguiente análisis.
Una palabra que empieza con un cáracter x y termina con el cáracter z
para que sea bipalindrome debe contar con la secuencia xz en su interior para
poder dividir la palabra por esa secuencia de forma que tal que me queda
una palabra que comienza y temina con el cáracter x y otra que comienza y
termina con z. Esto nos indica que para saber si una palabra es bipalindrome
solo debemos buscar las ocurrencias de la secuencias xz (donde x y z son los
caracteres por los cuales comienza y termina la palabra respectivamente) en
la palabra y por cada ocurrecia ver si la division por esa secuencia genera
dos palabras palindromes. Con una ocurrencia que cumpla ya la palabra es
bipalindrome. Para buscar la secuencia xz dentro de la palabra podemos usar
un KMP.
Una vez que la palabra no sea palindrome ni bipalindrome entonces será
no palindrome

2700 - Uniqwords Comparison Es un ejercicio bien sencillo dada dos


palabras donde en cada una no existe letras repetidas se debe responder lo
siguientes:

En una primera lı́nea las letras ordenadas alfabeticamente que están


en la palabra A y que no estan en la palabra B. En otras palabras la
diferencia entre el conjunto de letras que conforman la palabra A y el
conjunto de letras que conforman la palabra B.

En una segunda lı́nea las letras ordenadas alfabeticamente que están


en la palabra B y que no estan en la palabra A. En otras palabras la
diferencia entre el conjunto de letras que conforman la palabra B y el
conjunto de letras que conforman la palabra A.

En una tercera lı́nea las letras ordenadas alfabeticamente que son co-
munes entre las dos palabras. En otras palabras la intersección entre el
1
La función reverse pertenece a la biblioteca algorithms del lenguaje C++

189
CADENA

conjunto de letras que conforman la palabra B y el conjunto de letras


que conforman la palabra A.

Para su solución se puede aprovechar los metodos que propone la biblio-


teca algorithm del lenguaje de programación C++ para las operaciones con
conjuntos. Sino se puede implementar algoritmos que hagan las operaciones
porque como máximo cada cadena tendrán una logitud de 26.

3084 - Searching their Path La solución del ejercicio es bien sencilla


solo debemos leer cada cadena e imprimirla en orden inverso. Nos podemos
auxiliar en funciones que nos brinde el lenguaje utilizado para invertir o bien
cadenas o colecciones o nosostros mismo hacer el método de inversión.

4052 - A - Wi-Fi Password Una vez leido el problema solo debemos


percatarnos que lo que se nos pide es para cada cadena de entrada que su
longitud es par, imprimir los digitos en el orden que aparecen en las posiciones
impares (empezamos a contar a partir de cero) una cantidad de veces igual
a la cifra del digito que le antecede. Veamos los ejemplos de entrada y salida
del ejercicio.
Para la entrada 1234 la salida es 2444 poque se imprime una (1) vez 2 y
luego se imprime (3) tres veces 4. Para la entrada 014721 la salida es 777711
, se imprime cero (0) veces el 1, luego se imprime (4) cuatro veces el 7 y por
último se imprime dos (2) veces el 1. Recomiendo trabajar la secuencias de
digitos de la entrada como string y no como un valor numérico luego para
saber la cantidad de veces que debo imprimir un determinado carácteri solo
tengo que efectuar la siguiente resta carácteri−1 -’0’.

4120 - Perfect strings Una vez que se lee el problema nos podemos per-
catar que no posee una alta complejidad. Para que una cadena pueda ser
considerada perfect solo debemos chequear que la cantidad de vocales de la
palabra sea superior a la cantidad de consonantes de la palabra mas uno.
Siempre que se cumpla n > m + 1 va existir un reordenamiento tal que cada
consonante de la palabra va estar entre dos vocales.

4086 - Find Palindrome Spread El ejercicio nos pide determinar la


cantidad de caracteres de una cadena que no forma parte de una subcadena
palindrome cuya longitud sea inferior a un valor K. Para resolver el ejercicio
vamos a primero realizar una Manacher. Como se sabe este algoritmo genera
un arreglo que para cada posición calcula el máximo palı́ndrome tomando
esa posición como caracter central de la cadena palı́ndrome. Bien una vez

190
CADENA

ejecutado el Manacher nos quedaremos con el arreglo y lo recorremos de


principio a fin. Cada vez que encontremos un valor en una posición que
sea igual o superior a K calcularemos en la cadena inicial la primera y la
útima posición de ese paı́ndrome. A la par de la cadena inicial tendremos
un arreglo de entero con similar capacidad que la longitud de la cadena y
todas sus posiciones con valor cero. Cada vez que calculemos el principio de
una subcadena palı́ndrome cuya longitud sea mayor e igual que K en esa
misma posición incrementaremos en uno mientras en la posición que termina
decrementaremos en uno. Si analizan estamos marcando los incios y finales
de las cadenas palı́ndromes que cumple las condiciones requeridas y por tanto
todos los caracteres contenidos entre estas marcas no deben ser considerados
para la respuesta final, pero como hacerlo eficientemente ?. Sencillo luego
recorremos el arreglo y decimos que el valor del arregloi es igual a su valor
mas el valor de la posición anterior. Una vez ejecutado esto verán que las
posiciones cuyo valor sea cero coinciden con las posiciones de los caracteres
que no están dentro de las subcadenas palı́ndromes y portanto la solución
sera contar la cantidad de posiciones del arreglo cuyo valor sea cero.

4173 - Palindromic Finder El problema nos pide dada una cadena de


caracteres determinar si en dicha cadena existe una subsecuencia concecutivas
de caracteres que conformen un palindrome cuya longitud de ser igual o
superior a 3. Para resolver el problema basta aplicar el algoritmo Manacher
que nos permite hallar la longitud máxima de un palindrome dentro de una
cadena. Luego solo debemos chequear que dicha longitud sea mayor o igual
3 para imprimir TAK sino la respuesta será NIE.

DMOJ - Alex y la Cadena Se tiene una cadena de carácteres de hasta


106 carácteres a la cual se le puede añadir cualquier cáracter al final. Se quiere
saber cúal es la cantidad mı́nima de carácteres que se tiene que adicionar
para que la palabra sea palı́ndrome. Si analizamos el mejor de los casos
es 0 si la palabra es palı́ndrome desde el primer momento. El peor de los
casos es que no exista una subcadena comenzando desde la último cáracter
de la palabra que sea palı́ndrome. Entonces la idea es buscar el máximo
palı́ndrome dentro de la cadena inicial que incluya el último cáracter de la
cadena. Para hacer eso puedes utilizar el algoritmo Manacher realizando una
pequeña modificación en su implementación. Utilizando el arreglo radio del
algorimo busco palı́ndrome que su longitud sea lo mayor posible y extremo
derecho sea el último cáracter de la cadena inicial. Si existe tal palı́ndrome la
solución va ser la longitud de la cadena menos la longitud de dicho palı́ndrome
sino la respuesta será la longitud de la cadena menos uno.

191
CADENA

DMOJ Haciendo Palı́ndromos Si leemos el problema detenidamente ve-


remos que identicamente al problema Alex y la Cadena publicado en el mismo
jurado que este problema y explicado su solución en este mismo epı́grafe. Por
tanto la misma solución puede servir para los dos problemas.

4104 - Expresión El problema nos pide evualuar una determinada expre-


sión que solo contiene los operadores unarios ++ y – en sus dos variantes de
pre/pos incremento/decremento. Para solucionar podemos implementar un
sencillo parser que permita separar las expressiones en expresiones simples y
evaluar cada una de ellas por separadas.

192
Capı́tulo 8

Combinatoria

Según los que saben la combinatoria es la rama de las matemáticas que


estudia los diversos modos de agrupar los elementos de un conjunto sometidos
a unas u otras condiciones.

8.1. Variaciones
Se llama variación de los n objetos tomados p a p, a todo conjunto orde-
nado formado por p objetos escogidos de cualquier modo entre los n objetos
considerando distintas dos variaciones cuando difieran en algún objeto o en
el orden.
Ejemplo: Con los cuatro objetos a, b, c, d las variaciones dos a dos son:
ab ba ca da
ac bc cb db
ad bd cd dc
El número de estas variaciones lo denotaremos por Vn,p
Las variaciones con cierto número de objetos dados a, b, c, d, e se pueden
ir formando sucesivamente (primero las monarias, luego las binarias, después
las ternarias, etc.) por un método uniforme que consiste en agregar a cada
variación de cierto orden cada una de las letras (objetos) que no están en
ellas. Las variaciones monarias (de primer orden) o variaciones tomadas uno
a uno con las cinco letras a, b, c, d, e son evidentemente:
abcde
Para formar las binarias agregamos a cada una las letras restantes y se
obtiene el cuadro:
ab ba ca da ea
ac bc cb db eb
ad bd cd cd ec

193
COMBINATORIA

ae be ce de ed
Se forman ahora las ternarias agregando sucesivamente a cada binaria
las letras que no están en ella, como hay 20 binarias y a cada una se le
puede agregar 5 - 2 = 3 letras, resultarán 60 variaciones ternarias. Siguiendo
el mismo proceso se forman las variaciones de cuarto orden y las de quinto
orden.
Sea Vn,p el número de variaciones de orden p y Vn,p−1 el número de varia-
ciones de orden p-1.
Una vez formado el cuadro de variaciones de orden p-1, para formar el de
orden p se le agrega a cada una los n - (p - 1) = n – p + 1 elementos que no
están en ella: por lo tanto cada variación de orden p - 1 produce n – p + 1
variaciones de orden p y como hay Vn,p−1 variaciones de orden p-1 el número
total de variaciones de orden p será:
Vn,p−1 = n ∗ (n − 1) ∗ (n − 2) ∗ ... ∗ (n–p + 1)

8.1.1. Variación con repetición


En cuanto a las variaciones de n objetos tomado p a p tratado anterior-
mente podemos ver que p ≤ n. Si agrupamos k obetos de los n disponibles,
siendo k>n, lógicamente habrá repeticiones.
Todas las posibles distribuciones con k objetos en cada una donde en cada
repetición pueden aparecer objetos repetidos y se diferencian por su orden
recibe el nombre de variaciones con repetición.
El número de variaciones con repetición lo denotaremos por Wn,p . Si el
número de objetos es igual a n y en cada variación aparecen k objetos se
pueden formar nk variaciones con repetición.
Wn,p = nk
Ası́, por ejemplo, con dos objetos (los dı́gitos cero y uno), las variaciones
con repetición de tamaño ocho es W2,8 = 256.

8.2. Permutaciones
Se llama permutación de los n objetos a todo conjunto ordenado for-
mado por dichos n elementos. Dos permutaciones se distinguen una de otra
solamente por el orden de colocación de sus elementos.
Las permutaciones son un caso particular de las variaciones, cuando p =
n. Es decir, son las variaciones de n objetos tomados n a n.
Denotaremos por Pn al número de permutaciones de n objetos.
Pn = Vn,n

194
COMBINATORIA

Como las permutaciones de n objetos son las variaciones de orden n,


pueden formarse de igual modo que las variaciones.
Pn = Vn,n = n ∗ (n − 1) ∗ (n − 2) ∗ ... ∗ (n–n + 1)
Pn = n ∗ (n − 1) ∗ (n − 2) ∗ ... ∗ 1
Pn = n!
Como de cada una de las Pn−1 permutaciones se originan n permutaciones
nuevas, se tiene la relación: Pn = n ∗ Pn−1

8.2.1. Permutación con repetición


Hasta ahora las permutaciones que hemos formado ha sido a base de un
conjunto donde todos los objetos son diferentes. Sin embargo, si algunos de
los objetos son iguales se obtendrán menos permutaciones (algunas serán
iguales). Por ejemplo, permutando abcd obtenemos 24 permutaciones dife-
rentes, si en lugar de abcd tenemos que permutar las letras abab ya no son
24 porque algunas se repetirán.
El problema general se anuncia ası́: Se tienen k objetos diferentes. ¿Cuántas
permutaciones se pueden hacer tomando n1 elementos del primer tipo, n2 del
segundo, ..., nk del k–ésimo?
El número total de elementos de cada permutación es igual a n.Los ele-
mentos del tipo n1 pueden ser permutados de n1 ! formas, pero como son
iguales, estas permutaciones no cambian nada, de forma análoga no cambian
nada las n2 ! permutaciones de los elementos del segundo tipo, etc. Las per-
mutaciones anteriores se pueden permutar de n1 ! * n2 ! * ... * nk ! maneras de
forma que no varı́e, por eso el conjunto de las n! permutaciones se separa en
partes formadas por n1 ! * n2 ! * ... * nk ! permutaciones iguales. El número de
permutaciones con repetición diferentes se puede escribir:
Pn (n1 , n2 , ..., nk ) = n1 !∗n2n!
!∗...∗nk !

8.3. Combinaciones
Se llama combinación de n objetos tomados p a p, a todo conjunto de
p objetos elegidos entre ellos de tal modo que dos conjuntos se diferencien al
menos en un objeto.
Denotaremos por Cn,p al número de combinaciones de n objetos tomados
p a p.
Las combinaciones de n objetos tomados p a p son los distintos conjuntos
que pueden formarse con p objetos elegidos entre n dados, de modo que un
conjunto se diferencie de otro al menos en uno de los elementos.

195
COMBINATORIA

En las variaciones, abc y bca son variaciones distintas, pero es la misma


combinación, sólo un conjunto que contenga un nuevo elemento se considera
una nueva combinación. ej: abd
Si imaginamos formadas Cn,p combinaciones de orden p que se pueden
formar con n objetos, ejemplo, las combinaciones ternarias de las cuatro
letras a, b, c, d
abc abd acd bcd
En cada combinación permutamos las letras de todas las maneras posibles
y obtenemos el cuadro:
abc abd acd bcd
acb adb adc bdc
bac bad cad cbd
bca bda dac dbc
cba dba dca dcb
El cual contiene las variaciones ternarias de las cuatro letras a, b, c, d
pues las que proceden de la misma combinación difieren en el orden de las
letras y las que proceden de combinaciones distintas difieren al menos en
una letra. Como cada combinación de orden p da lugar a Pp combinaciones
distintas, entre los números Cn,p , Pp y Vp,n existe la relación:
Cn,p ∗ Pp = Vp,n
de donde
Cn,p = VPp,n
p
sustituyendo Pp y Vp,n
Cn,p = n∗(n−1)∗(n−2)∗...∗(n−p+1)
p!
Con el objetivo de completar n! en el numerador, multiplicamos el nume-
rador y el denominador por (n-p)!
Cn,p = n∗(n−1)∗(n−2)∗...∗(n−p+1)∗(n−p)!
p!∗(n−p)!
n!
Cn,p = p!∗(n−p)!
Las expresiones de la forma Cn,p reciben el nombre de números combina-
torios para representarlos se usa la notación:

8.3.1. Algunas propiedades de los números combina-


torios
Al número n lo llamaremos base del número combinatorio y a p lo llama-
remos orden. Dos ordenes son complementarios cuando su suma es igual a la

196
COMBINATORIA

base. Los números combinatorios de igual base y órdenes complementarios


son iguales.

La suma de dos números combinatorios de igual base y órdenes consecu-


tivos p-1, p da el número combinatorio de orden p y base inmediatamente
superior.

Esta propiedad proporciona un medio de calcular rápidamente los núme-


ros combinatorios de bases sucesivas formando el llamado triángulo de Pas-
cal1 . Cada fila comienza y termina por uno y los números intermedios se
obtienen sumando los dos inmediatos a la fila precedente.

1
En matemática, el triángulo de Pascal es una representación de los coeficientes bi-
nomiales ordenados en forma triangular. Es llamado ası́ en honor al matemático francés
Blaise Pascal, quien introdujo esta notación en 1654, en su Traité du triangle arithmétique.

197
COMBINATORIA

Los números en el triángulo tienen algunas propiedades.Los números de la


segunda fila y diagonal son los números naturales. Los números de la tercera
fila y diagonal se llaman triangulares y son de la forma n ∗ (n + 1)/2. Los
números de la cuarta fila y diagonal se denominan triángulo-piramidales y
son de la forma n ∗ (n + 1) ∗ (n + 2)/6
A continuación se muestra el código para construir el triángulo de Pascal

# define MAX_N 5001


# define MAX_K 5001
typedef long long lld ;

lld binom [ MAX_N ][ MAX_K ];

int lasti = 0 , lastj = 0;


bool init = false ;

inline lld Bi n o mi a l Co e f fi c i e nt ( int n , int k )


{
if ( lasti >= n && lastj >= k )
return binom [ n ][ k ];
if (! init )
{
for ( int i =0; i <= n ; i ++)
binom [ i ][0] = 1;
for ( int j =1; j <= k ; j ++)
binom [0][ j ] = 0;
for ( int i =1; i <= n ; i ++)
{
for ( int j =1; j <= k ; j ++)
{
binom [ i ][ j ] = binom [i -1][ j -1] + binom [i
-1][ j ];
}
}
init = true ;
lasti = n ;
lastj = k ;
return binom [ n ][ k ];
}
else
{
if ( lastj < k )

198
COMBINATORIA

{
for ( int i =1; i <= lasti ; i ++)
{
for ( int j = lastj +1; j <= k ; j ++)
{
binom [ i ][ j ] = binom [i -1][ j -1] +
binom [i -1][ j ];
}
}
lastj = k ;
}
if ( lasti >= n )
return binom [ n ][ k ];
else
{
for ( int i = lasti +1; i <= n ; i ++)
{
binom [ i ][0] = 1;
for ( int j =1; j <= lastj ; j ++)
{
binom [ i ][ j ] = binom [i -1][ j -1] +
binom [i -1][ j ];
}
}
lasti = n ;
return binom [ n ][ k ];
}
}
}

El mismo presenta una complejidad de O(n*k). A continuación se muestra


un algoritmo que permite calcular las combinaciones de n en k sin necesidad
de utililar el triángulo de Pascal.

long gcd ( long a , long b )


{
return ( a %b ==0) ? b : gcd (b , a %b ) ;
}

void Divbygcd ( long & a , long & b )


{
long g = gcd (a , b ) ;
a /= g ;

199
COMBINATORIA

b /= g ;
}

long C ( int n , int k )


{
if (n < k )
return 0;
else if ( n == k )
return 1;
else
{
long numerator =1 , denominator =1 , toMul ,
toDiv , i ;
if (k > n /2)
k =n - k ;
for ( int i = k ;i >0; i - -)
{
toMul =n - k + i ;
toDiv = i ;
Divbygcd ( toMul , toDiv ) ; /* siempre
dividir antes de multiplicar */
Divbygcd ( numerator , toDiv ) ;
Divbygcd ( toMul , denominator ) ;
numerator *= toMul ;
denominator *= toDiv ;
}
return numerator / denominator ;
}
}

8.4. Máscara de bit

En múltiples problemas podemos encontrar que dado un conjunto S de N


elementos, se necesita determinar de cuantas formas se puede seleccionar K
elementos (K <= N ) y que todos los elementos de ese subconjunto cumpla
con alguna restricción o condición determinada. O encontrar cuanto subcon-
junto Ks del conjunto inicial S sus elementos cumple con alguna restricción
o condición determinada.

200
COMBINATORIA

Operadores and y desplazamiento hacia la izquierda a


nivel de bit
Estos operadores permiten actuar sobre los operandos para modificar un
solo bit, se aplican a variables del tipo char , short , int y long y no pueden
ser usados con float ó double, sólo se pueden aplicar a expresiones enteras. En
ocasiones los operadores de bits se utilizan para compactar la información,
logrando que un tipo básico (por ejemplo un long) almacene magnitudes más
pequeñas mediante aprovechamientos parciales de los bits disponibles.

Desplazamiento a izquierda <<


Este operador binario realiza un desplazamiento de bits a la izquierda.
El bit más significativo (más a la izquierda) se pierde, y se le asigna un 0 al
menos significativo (el de la derecha). El operando derecho indica el número
de desplazamientos que se realizarán. Los desplazamientos no son rotaciones;
los bits que salen por la izquierda se pierden, los que entran por la derecha
se rellenan con ceros. Este tipo de desplazamientos se denominan lógicos en
contraposición a los cı́clicos o rotacionales.
La técnica de la máscara de bit es muy útil cuando se desea generar todos
los subconjuntos derivados de un conjunto inicial, tener en cuenta que dicha
técnica es solo aplicable cuando el número de elementos del conjunto base
no supera los 20, se puede aplicar hasta 21 en dependencia de la cantidad
de tiempo lı́mite del problema. Otro aspecto a tener en cuenta en cuanto a
esta técnica es que cuando en el conjunto inicial existe elemento repetidos la
técnica generará subconjuntos idénticos.
Sintaxis:
expr − desplazada << expr − desplazamiento
El patrón de bits de expr-desplazada sufre un desplazamiento izquierda
del valor indicado por la expr-desplazamiento. Ambos operandos deben ser
números enteros o enumeraciones. En caso contrario, el compilador realiza
una conversión automática de tipo. El resultado es del tipo del primer ope-
rando. expr-desplazamiento, una vez promovido a entero, debe ser un entero
positivo y menor que la longitud del primer operando. En caso contrario el
resultado es indefinido (depende de la implementación).
Ejemplo:

unsigned long x = 1 0 ; // En b i n a r i o e l 10 e s 1010

int y = 2 ;

unsigned long z = x << y ;

201
COMBINATORIA

/∗ z t i e n e n e l v a l o r 40 que en b i n a r i o e s 101000 vea que s e


a d i c i o n o dos c e r o s a l f i n a l que son l o s d e s p l a z a m i e n t o s . ∗/

AND lógico & (palabra clave bitand)


Este operador binario compara ambos operandos bit a bit, y como resul-
tado devuelve un valor construido de tal forma, que cada bits es 1 si los bits
correspondientes de los operandos están a 1. En caso contrario, el bit es 0.
Sintaxis:
AN D − expresion&equality − expresion
Ejemplo:

int x = 1 0 , y = 2 0 ; // x en b i n a r i o e s 0 1 0 1 0 , y en b i n a r i o
10100

int z = x & y ; // e q u i v a l e a : i n t z = x b i t a n d y ; z
toma e l v a l o r 00000 e s d e c i r 0

Conjunto potencia
Se denomina conjunto potencia del conjunto S a al conjunto de todos los
subconjuntos de S, ejemplo:
Sea S= {a, b, c} un conjunto conformado por los elementos a, b y c el
conjunto potencia de S es {{a}, {b}, {c}, {a, b}, {a, c}, {b, c}, {a, b, c},
{Ø}}
Por anterior expuesto se puede decir que la cantidad de subconjuntos de
un conjunto va ser 2 a la potencia de la cardinalidad del conjunto teniendo
como cardinalidad de un conjunto la cantidad de elementos de este. En este
caso el conjunto S tiene cardinalidad 3 y la cantidad de subconjuntos es 8.

Máscara de bit
La técnica de la máscara de bit se encarga de combinar los dos temas
abordados en esta presentación anteriormente para eso tomemos como caso
de estudio tomemos el conjunto S= {a, b, c} del cual conformaremos todos
los posibles subconjunto aplicando la técnica máscara de bit.
Bien como sabemos el conjunto S tiene 8 subconjuntos, pues bien vamos a
construir una pequeña tabla con tres columnas en la primera columna estarán
todos los números desde el 0 hasta el 7, en la segunda la representación binaria
del número a la izquierda mientras en la tercera estarán los elementos del

202
COMBINATORIA

conjunto S que su posición en la representación binario su bit este activo


(valor 1). Recuerde que en binario el bit más a la derecha es el primero.

Número Binario Elementos seleccionados


0 000
1 001 a
2 010 b
3 011 a,b
4 100 c
5 101 a,c
6 110 b,c
7 111 a,b,c

La idea de la técnica es iterar desde 0 hasta 2n − 1 donde n es la cantidad


de elementos del conjunto inicial y para cada número ver cuáles son sus bit
activos y seleccionar del conjunto aquellos elementos que sus posiciones se
corresponde con las posiciones del bits activos. Ahora duda que puede surgir
es como sé que bit del número está activo, sencillo a priori sabemos que el
número va estar conformado por n bits donde n es cantidad de elementos del
conjunto pues haremos una operación and entre el número y 1 desplazado
hacia la izquierda x lugares donde x va ser un número entre 0 y n. veamos
en código como serı́a:

v e c t o r <char> s ( ’a’ , ’b’ , ’c’ ) ;


int n= s . s i z e ( ) ;
int c o m b i n a t i o n s=pow ( 2 , n ) ;
f o r ( int i =0; i < c o m b i n a t i o n s ; i ++)
{
int number=i ;
v e c t o r <char> k ;
f o r ( int e =0; e< n ; e++)
{
i f ( number & (1<< e ) )
{
k . push back ( s . a t [ e ] ) ;
}
}
/∗Ya k t i e n e l o s e l e m e n t o s que conforma e l s u b c o n j u n t o i −
nesimo ∗/
}

Analizando la complejidad del algoritmo nos damos cuenta que el mismo


es 2n ∗ n donde n como ya se ha reiterado en otras ocasiones es la cantidad

203
COMBINATORIA

de elementos del conjunto inicial. Por lo que dicha técnica es aplicable para
n menor igual que 20 ya 21 habrı́a que analizar el tiempo lı́mite del problema
ası́ como las adecuaciones hechas dentro de este algoritmo incluso con 20
elementos hay que hilar una óptima solución.
Para n igual 20 el número de iteraciones es 20971520 mientras para 21 es
44040192.

8.5. Backtracking
Dentro de las técnicas de diseño de algoritmos, el método de Vuelta Atrás
(del inglés Backtracking) es uno de los de más ámplia utilización, en el senti-
do de que puede aplicarse en la resolución de un gran número de problemas,
muy especialmente en aquellos de optimización.Permite hacer búsqueda sis-
temática a través de todas las configuraciones posibles dentro de un espacio
de búsqueda.
Para lograr esto, los algoritmos de tipo backtracking construyen posibles
soluciones candidatas de manera sistemática. En general, dado una solución
candidata s:

1. Verifican si s es solución. Si lo es, hacen algo con ella (depende del


problema).

2. Construyen todas las posibles extensiones de s, e invocan recursivamen-


te al algoritmo con todas ellas.

A veces los algoritmos de tipo backtracking se usan para encontrar una


solución, pero otras veces interesa que las revisen todas (por ejemplo, para
encontrar la más corta).
En su forma básica la Vuelta Atrás se asemeja a un recorrido en profundi-
dad dentro de un árbol cuya existencia sólo es implı́cita, y que denominaremos
árbol de expansión. Este árbol es conceptual y sólo haremos uso de su orga-
nización como tal, en donde cada nodo de nivel k representa una parte de la
solución y está formado por k etapas que se suponen ya realizadas. Sus hijos
son las prolongaciones posibles al añadir una nueva etapa. Para examinar el
conjunto de posibles soluciones es suficiente recorrer este árbol construyendo
soluciones parciales a medida que se avanza en el recorrido.
En este recorrido pueden suceder dos cosas. La primera es que tenga éxito
si, procediendo de esta manera, se llega a una solución (una hoja del árbol).
Si lo único que buscabamos era una solución al problema, el algoritmo finaliza
aquı́; ahora bien, si lo que buscabamos eran todas las soluciones o la mejor

204
COMBINATORIA

de entre todas ellas, el algoritmo seguirá explorando el árbol en búsqueda de


soluciones alternativas.
Por otra parte, el recorrido no tiene éxito si en alguna etapa la solución
parcial construida hasta el momento no se puede completar; nos encontramos
en lo que llamamos nodos fracaso. En tal caso, el algoritmo vuelve atrás (y
de ahı́ su nombre) en su recorrido eliminando los elementos que se hubieran
añadido en cada etapa a partir de ese nodo. En este retroceso, si existe uno o
más caminos aún no explorados que puedan conducir a solución, el recorrido
del árbol continúa por ellos.
La filosofı́a de estos algoritmos no sigue unas reglas fijas en la búsqueda
de las soluciones. Podrı́amos hablar de un proceso de prueba y error en el
cual se va trabajando por etapas construyendo gradualmente una solución.
Para muchos problemas esta prueba en cada etapa crece de una manera
exponencial, lo cual es necesario evitar.
Gran parte de la eficiencia (siempre relativa) de un algoritmo de Vuelta
Atrás proviene de considerar el menor conjunto de nodos que puedan llegar a
ser soluciones, aunque siempre asegurándonos de que el árbol “podado”siga
conteniendo todas las soluciones. Por otra parte debemos tener cuidado a
la hora de decidir el tipo de condiciones (restricciones) que comprobamos
en cada nodo a fin de detectar nodos fracaso. Evidentemente el análisis de
estas restricciones permite ahorrar tiempo, al delimitar el tamaño del árbol
a explorar. Sin embargo esta evaluación requiere a su vez tiempo extra, de
manera que aquellas restricciones que vayan a detectar pocos nodos fracaso
no serán normalmente interesantes. No obstante, y como norma de actuación
general, podrı́amos decir que las restricciones sencillas son siempre apropia-
das, mientras que las más sofisticadas que requieren más tiempo en su cálculo
deberı́an reservarse para situaciones en las que el árbol que se genera sea muy
grande.
Existen diversos ejemplos donde pueden ser aplicados algoritmos que fun-
cionen con esta filosofı́a para su solución, por ejemplo:

- Un problema clásico que puede ser resuelto con un diseño Vuelta Atrás
es el denominado de las ocho reinas y en general, de las n reinas. Dispo-
nemos de un tablero de ajedrez de tamaño 8 x 8, y se trata de colocar
en él ocho reinas de manera que no se amenacen según las normas del
ajedrez, es decir, que no se encuentren dos reinas ni en la misma fila,
ni en la misma columna, ni en la misma diagonal.

- Dado un tablero de ajedrez de tamaño n x n, un rey es colocado en una


casilla arbitraria de coordenadas (x,y). El problema consiste en deter-
minar los 2n -1 movimientos de la figura de forma que todas las casillas

205
COMBINATORIA

del tablero sean visitadas una sola vez, si tal secuencia de movimientos
existe.
- Supongamos que tenemos n hombres y n mujeres y dos matrices M y
H que contienen las preferencias de los unos por los otros. Más con-
cretamente, la fila M[i,·] es una ordenación (de mayor a menor) de las
mujeres según las preferencias del i-ésimo hombre y, análogamente, la
fila H[i,·] es una ordenación (de mayor a menor) de los hombres según
las preferencias de la i-ésima mujer. El problema consiste en diseñar
un algoritmo que encuentre, si es que existe, un emparejamiento de
hombres y mujeres tal que todas las parejas formadas sean estables.
Diremos que una pareja (h,m) es estable si no se da ninguna de estas
dos circunstancias:
1. Existe una mujer m’ (que forma la pareja (h’,m’)) tal que el hom-
bre h la prefiere sobre la mujer m y además la mujer m’ también
prefiere a h sobre h’.
2. Existe un hombre h” (que forma la pareja (h”,m”)) tal que la
mujer m lo prefiere sobre el hombre h y además el hombre h”
también prefiere a m sobre la mujer m”.
- Una matriz bidimensional n x n puede representar un laberinto cua-
drado. Cada posición contiene un entero no negativo que indica si la
casilla es transitable (0) o no lo es (∞). Las casillas [1,1] y [n,n] corres-
ponden a la entrada y salida del laberinto y siempre serán transitables.
Dada una matriz con un laberinto, el problema consiste en diseñar un
algoritmo que encuentre un camino, si existe, para ir de la entrada a la
salida.
- Dadas n personas y n tareas, queremos asignar a cada persona una
tarea. El coste de asignar a la persona i la tarea j viene determinado
por la posición [i,j] de una matriz dada (TARIFAS). Diseñar un algo-
ritmo que asigne una tarea a cada persona minimizando el coste de la
asignación.
- Sea W un conjunto de enteros no negativos y M un número entero
positivo. El problema consiste en diseñar un algoritmo para encontrar
todos los posibles subconjuntos de W cuya suma sea exactamente M.
- Dado un grafo conexo, se llama Ciclo Hamiltoniano a aquel ciclo que
visita exactamente una vez cada vértice del grafo y vuelve al punto de
partida. El problema consiste en detectar la presencia de ciclos Hamil-
tonianos en un grafo dado.

206
COMBINATORIA

Existen otros en donde su solución tambı́en sea con la aplicación de esta


técnica. Pero ojo muy importante, siempre antes de decidirse si la solución
va ser aplicar esta técnica, revise el rango de los datos del problema, analice
si el posible árbol de expasión que genera esta técnica es factible dada las
restricciones del problema, analice las posibles podas que puede realizar. A
lo mejor ya exiten algoritmos que dan solución al problema incluso algunos
de los ejemplos mencionado tienen algoritmos que lo solucionan sin utilizar
esta técnica.

8.6. Números de Catalan


En combinatoria, los números de Catalan forman una secuencia de núme-
ros naturales que aparece en varios problemas de conteo que habitualmente
son recursivos. Obtienen su nombre del matemático belga Eugène Charles
Catalan (1814–1894).
El n-ésimo número de Catalan se obtiene, aplicando coeficientes binomia-
les, a partir de la siguiente fórmula:

Una expresión alternativa para Cn es:

Los números de Catalan satisfacen la siguiente relación de recurrencia:

Y también satisfacen, que puede ser una forma más eficiente de calcular-
los.
Existen múltiples problemas de concurso de programación cuya solución
la dan los números de Catalan o son parte ella. El libro Enumerative Combi-
natorics: Volume 2, de Richard P. Stanley contiene un conjunto de ejercicios
que describen 66 interpretaciones distintas de los números de Catalan. A
continuación vamos algunos ejemplos.
Cn es el número de palabras de Dyck de longitud 2n. Una palabra de
Dyck es una cadena de caracteres que consiste en n X’s y n Y’s de
forma que no haya ningún segmento inicial que tenga más Y’s que X’s.

207
COMBINATORIA

Reinterpretando el sı́mbolo X como un paréntesis abierto y la Y como


un paréntesis cerrado, Cn cuenta el número de expresiones que contie-
nen n pares de paréntesis correctamente colocados.

Cn es el número de formas distintas de agrupar n + 1 factores median-


te paréntesis (o el número de formas de asociar n aplicaciones de un
operador binario).

Las aplicaciones sucesivas de un operador binario pueden representarse


con un árbol binario. En este caso, Cn es el número de árboles binarios
de n + 1 hojas, en los que cada nodo tiene cero o dos hijos.

Cn es el número de caminos monótonos que se pueden trazar a través de


las lı́neas de una malla de n*n celdas cuadradas, de forma que nunca
se cruce la diagonal. Un camino monótono es aquél que empieza en
la esquina inferior izquierda y termina en la esquina superior derecha,
y consiste únicamente en tramos que apuntan hacia arriba o hacia la
derecha. El recuento de estos caminos es equivalente a contar palabras
de Dyck: X significa ”moverse a la derecha.eY significa ”moverse hacia
arriba”.

Cn es el número de formas distintas de cortar un polı́gono convexo de


n+2 lados en triángulos conectando vértices con lı́neas rectas sin que
ninguna se corte.

8.7. K-Combinations
8.8. Permutations
8.9. Power Set
8.10. Principio de inclusión exclusión
8.11. Análisis de ejercicios
2739 - Coco-Bits and the Guidance Race Generando todos los posi-
bles recorridos desde el punto inicial hasta el final pasando por todos puntos

208
COMBINATORIA

me quedo con aquel recorrido que registre la menor distancia. Aplicar un


DFS recursivo como un backtracking. La cantidad de puntos permite la im-
plementación del DFS de forma recursiva.

1524 - WikiSpy F1 Generando todos los posibles números con la cadena


inicial y quedándome solo con aquellas combinaciones parciales o total que
sean números primos. Aplicar un DFS recursivo como un backtracking.

3705 - Polı́gono La solución de problema es la siguiente ecuación : n ∗


(n − 1) ∗ (n − 2) ∗ (n − 3)/24

2697 - Magic Star Dado que son solo 12 letras se puede implementar
backtracking que genere una posible combinación siempre respetando las
posibles letras fijas , que la combinación sea alfabeticamente menor y la
suma siempre debe ser 26.

2663 - Growing Numbers Se puede implementar una función que per-


mita generar todos los números Growing luego los ordenas. y despues con un
simple for cuentas todos los que estén entre el min(A,B) y max(A,B).

1900 - John and his Sheeps Dada las condiciones del problema se puede
hacer una DFS recursivo que genere todas los posibles recorridos desde la
celda (1,1) hasta la celda (N,N). Claro esta variante lleva adeuaciones porque
si generamos todas las combinaciones hasta el final nos puede conducir un
TLE. Ahora si al DFS recursivo le aplicamos la técnica de poda y corte
podemos llevar un TLE a AC. Para hacer el corte o poda en el DFS recursivo
vamos a visitar el próximo nodo siempre y cuando no halla derteminado la
longitud de un posible camino. En caso de que ya tenga una longitud previa
solo voy a visitar el próximo nodo solo si la distancia recorrida hasta ese
momento mas la distancia de ir del nodo actual al deseado es menor o igual
la distancia calculada previamente. En caso de que todos los nodos estén
visitados sumar la distancia recorrida mas la distancia desde el último nodo
visitado hasta la celda (N,N) y si dicha distancia es menor que la que se tiene
como referencia actualizar la referencia con ese valor.

3492 - Dancing Doubles La solución de problema es que para cada valor


N se debe imprimir el valor N!.

209
COMBINATORIA

3640 - Triples of Even Parity El problema nos pide que dada una co-
lección de números al cual se les puede eliminar e insertar números con la
restricciones que cuando se inserta un elemento si este ya existe en la colec-
ción no se procede a insertar y de igual forma cuando se elimina un elemento
no almacenado no se elimina, calcular en cualquier momento la cantidad tria-
da que puedo formar con la condición que la suma de los elementos sea par.
Para que una triada de números su suma sea par existen dos forma (P,P,P) y
(I,I,P) donde I representa un número impar mientras P representa un núme-
ro par. Este pequeño análisis nos conduce a la ecuación que nos resuelve el
problema la cual es CN,3 + CM,2 ∗ N . Donde N es la cantidad de números
pares y M la cantidad de números impares almacenados hasta el momento
de la pregunta. CN,3 es la cantidad de forma de seleccionar 3 elementos de
N sin de tal modo que dos conjuntos se diferencien al menos en un objeto.
De igual forma sucede para la expresión CM,2 . Para controlar la inserción y
eliminación de los números, se puede utilizar una estructura de tipo conjunto
una para los números pares y otra para números impares.

1958 - Grazing Patterns Dada una matriz de 5x5 tenemos vacas situadas
en (1;1) y (5;5) las cuales se mueven paralelo a los ejes hacia una celda que
tenga pasto. Determinar la cantidad de forma que las vacas terminan en la
misma celda en la matriz despues de consumir todas las celdas que tenga
pasto. Existen celdas en la matriz que no tienen pasto inicialmente .
Realizar un DFS de manera recursiva donde en cada llamada las dos
vacas avacen una celda que tenga pasto. Tener en cuenta que las las dos
vacas pueden acceder a la misma casilla en un mismo turno. El DFS debe
recibir como parametros las posiciones de las vacas y la cantidad de celdas en
la matriz que no tienen pasto hasta ese momento. Cuando las posiciones son
las misma y la cantidad de celdas sin pastos es 25 entonces hemos encontrado
una forma. Si las posiciones son distintas entonces buscar dos nuevas celdas
para donde se pueda mover las vacas y volver a invocar el metodo con estas
nuevas posiciones. Las posiciones de las vacas puede ser una estructura que
indique fila y columna.

2538 - How Many Ways El problema nos plantea determinar de un con-


junto de números de cuantas formas se puede agrupar dichos números de
forma tal que cada subconjunto formado la suma de los elementos sea múlti-
plo de 3. Para hallar la solución vamos a dividir los números los conjuntos A,
B y C. Siendo An , Bn y Cn las cantidades de elementos de cada subconjunto.
El conjunto A va estar conformado por los números que el resto de su
división por tres sea igual a cero. En el caso del subconjunto B van estar los

210
COMBINATORIA

numeros cuyo resto de su división por tres es uno mientras en el subconjunto


C estarán aquellos cuyo resto de su división por tres es dos.
En el conjunto A estarán números que por si solo son múltiplos de tres
por lo que cualquier subconjunto de este conjunto cumplirá con la condición
inicial del problema. La cantidad de subconjuntos que se puede conformar con
este conjunto es igual al conjunto potencia del conjunto A menos uno porque
no debemos considerar el caso del subconjunto vacio. En otras palabras X =
2An − 1
Se puede generar un subconjunto que cumpla la condición inicial del pro-
blema tomando elementos de los conjuntos B y C. siempre que la suma de
los elementos tomados de ambos conjuntos sea múltiplo 3 con las siguientes
variantes:

Si la suma de los elementos tomados del conjunto C modulado a tres


es uno.

- La cantidad de subconjuntos que se puede conformar del conjunto C


con dicha cantidad elementos por la cantidad de subconjuntos que se
puede conformar con los elementos del conjunto B cuya suma de los
elementos seleccionados modulado a tres sea dos.

Si la suma de los elementos tomados del conjunto C modulado a tres


es dos.

- La cantidad de subconjuntos que se puede conformar del conjunto C


con dicha cantidad elementos por la cantidad de subconjuntos que se
puede conformar con los elementos del conjunto B cuya suma de los
elementos seleccionados modulado a tres sea uno.

Si la suma de los elementos tomados del conjunto C modulado a tres


es cero.

- La cantidad de subconjuntos que se puede conformar del conjunto C


con dicha cantidad elementos por la cantidad de subconjuntos que se
puede conformar con los elementos del conjunto B cuya suma de los
elementos seleccionados modulado a tres sea cero.

Esta solución parcial vamos nombrarla W.


El conjunto B están aquellos números cuyo resto es uno. Para generar
un subconjunto cuya suma sea múltiplo de tres la cantidad de elementos
seleccionados debe ser multiplo dePtres. Siendo Bn la cantidad de números
del conjunto B la solución es Y= C(Bn ,3i) donde 1 ≤ i ≤ Bn /3. Donde

211
COMBINATORIA

C(Bn ,3i) es el número de subconjunto de 3i elementos de Bn de elementos


disponibles.
Esta solución parcial vamos nombrarla Y.
El conjunto C están aquellos números cuyo resto es dos. Para generar
un subconjunto cuya suma sea múltiplo de tres la cantidad de elementos
seleccionados debe ser multiplo dePtres. Siendo Cn la cantidad de números
del conjunto C la solución es Y= C(Cn ,3i) donde 1 ≤ i ≤ Cn /3. Donde
C(Cn ,3i) es el número de subconjunto de 3i elementos de Cn de elementos
disponibles.
Esta solución parcial vamos nombrarla Z.
Con estas soluciones parciales podemos construir la solución final la cual
queda de la siguiente manera: X + Y + Z + W + XY + XZ + XW . El otro
detalle del problema es que la solución debe ser modulada a 109 .

3463 - Onerous Oracle Dado un valor N se quiere generar todas las cade-
nas posibles que las primeras letras sean C las intermedias A y las últimas T
y de forma tal que la palabra CAT este como subcadena no necesariamente
concecutiva dentro de la palabra una cantidad de veces igual a N.
La idea es buscar todas las trı́adas posibles sin repetir (a,b,c) tal que
a∗b∗c = N y cada vez que se encuentren una trı́ada que cumpla la condición,
la palabra para ese caso va a tener una cantidad de a de C, una cantidad b
de A y una cantidad c de T.
Cada palabra conformada se almacena en un set para evitar duplicados
y luego debe ser ordenada esta colección de forma similar a como se pide el
formato de salida del problema para facilitar el proceso de impresión.

3476 - Festival El problema nos plantea que para la apertura de una


danza de han seleccionado N hombre y M mujeres los cuales son colocados
todos en una fila y nos piden hallar de cuantas maneras se puede conformar
una fila de tal manera que dos mujeres no esten una al lado de la otra en la
fila. Por lo datos del problema te aseguran que va existir mas hombres que
mujeres. Haciendo un análisis en hoja de la situación nos podemos percatar
que la solución a nuestro problema es la siguiente expresión C(N +1,M )*M!*N!
mod 109 +7. Vamos a desglosar esta expresión para una mejor compresión.
Si tomamos a las mujeres la cantidad de filas que puedo formar con ellas es
igual a M!. Pero como por restricción dos mujeres no pueden estar continuas
debemos colocar al menos un hombre entre ellas y como no me interesa el
orden de estos es N!. Sin embargo el orden en que ponga a los hombres no me
interesa solo que exista al menos uno entre dos mujeres por lo que voy a tener
M! variantes de formación. Que sucede que de la cantidad de hombres solo

212
COMBINATORIA

necesito la cantidad de variantes donde al menos superen en uno la cantidad


de mujeres y de ahı́ viene la expresión C(N +1,M ) donde determino la cantidad
de combinaciones que puedo formar de N+1 hombres escogiendo M hombres.

213
COMBINATORIA

214
Capı́tulo 9

Teorı́a de número

9.1. Números primos


En matemáticas, particularmente en teorı́a de números o aritmética, un
número primo es cualquier número natural mayor que 1 cuyo únicos diviso-
res posibles son el mismo número primo y el factor natural 1 .A diferencia
de los números primos , los números compuestos son naturales que pueden
factorizarse .

9.1.1. Gauss primality


9.1.2. Tests probabilı́sticos de primalidad
La cuestión de la determinación de si un número n dado es primo es cono-
cida como el problema de la primalidad. Un test de primalidad (o chequeo de
primalidad) es un algoritmo que, dado un número de entrada n, no consigue
verificar la hipótesis de un teorema cuya conclusión es que n es compuesto.
Esto es, un test de primalidad sólo conjetura que “ante la falta de certifica-
ción sobre la hipótesis de que n es compuesto podemos tener cierta confianza
en que se trata de un número primo”. Esta definición supone un grado menor
de confianza que lo que se denomina prueba de primalidad (o test verdadero
de primalidad), que ofrece una seguridad matemática al respecto.

Test de primalidad de Miller-Rabin


El Test de primalidad de Miller-Rabin es un test de primalidad, es decir,
un algoritmo para determinar si un número dado es primo, similar al test
de primalidad de Fermat. Su versión original fue propuesta por G. L. Miller,
se trata de un algoritmo determinista, pero basado en la no demostrada

215
TEORÍA DE NÚMERO

hipótesis generalizada de Riemann; Michael Oser Rabin modificó la propuesta


de Miller para obtener un algoritmo probabilı́stico incondicional.
Supóngase que n > 1 es un número impar del cual queremos saber si es
primo o no. Sea m un valor impar tal que n-1 = 2k m y a un entero escogido
aleatoriamente entre 2 y n-2.
Cuando se cumple que:
am ≡ ±1(modn)
o bien
r
a2 m ≡ −1(modn)
para al menos un r entero entre 1 y k-1, se considera que n es un probable
primo; en caso contrario n no puede ser primo. Si n es un probable primo se
escoge un nuevo valor para a, y se itera nuevamente reduciendo el margen de
error probable. Al utilizar exponenciación binaria las operaciones necesarias
se realizan muy rápidamente.
Se puede demostrar que un número compuesto es clasificado ”probable
primo.en una iteración del algoritmo con una probabilidad inferior a 1/4; de
hecho, en la práctica la probabilidad es mucho menor.
Asumiendo correcta la hipótesis generalizada de Riemann, se puede de-
mostrar que, si todo valor de a hasta 2(ln n)2 ha sido verificado y n todavı́a es
clasificado como probable primo, entonces n es en realidad un número primo.
Con esto se tiene un test de primalidad de costo O((ln n)4 ).

int powMod ( int x , int k , int m)


{
i f ( k == 0 )
return 1;
i f ( k % 2 == 0 )
return powMod ( x∗x %m, k / 2 , m) ;
else
return x ∗ powMod ( x , k−1, m) %m;
}

bool s u s p e c t ( int a , int s , int d , int n )


{
int x = powMod ( a , d , n ) ;
i f ( x == 1 )
return true ;
f o r ( int r = 0 ; r<s ; ++r )
{
i f ( x == n − 1 )
return true ;
x = x∗x %n ;
}
return f a l s e ;
}

216
TEORÍA DE NÚMERO

// { 2 , 7 , 6 1 , − 1} para n <4759123141 (= 2 ˆ 3 2 )
// { 2 , 3 , 5 , 7 , 1 1 , 1 3 , 1 7 , 1 9 , 2 3 , − 1} para n <10 ˆ 16 ( h a s t a
e l momento )
bool i s P r i m e ( int n )
{
i f ( n<=1 | | ( n>2 && n %2==0))
return f a l s e ;
int t e s t [ ] = { 2 , 3 , 5 , 7 , 1 1 , 1 3 , 1 7 , 1 9 , 2 3 , − 1 } ;
int d = n − 1 , s = 0 ;
while ( d %2==0) ++s , d/= 2 ;

f o r ( int i = 0 ; t e s t [ i ] <n && t e s t [ i ]!= −1; ++i )


i f ( ! suspect ( test [ i ] , s , d , n) )
return f a l s e ;
return true ;
}

9.1.3. Sieve interval


9.1.4. Criba de Eratóstenes
La criba de Eratóstenes es un algoritmo que permite hallar todos los
números primos menores que un número natural dado N. Se forma una tabla
con todos los números naturales comprendidos entre 2 y n, y se van tachando
los números que no son primos de la siguiente manera: Comenzando por el 2,
se tachan todos sus múltiplos; comenzando de nuevo, cuando se encuentra un
número entero que no ha sido tachado, ese número es declarado primo, y se
procede a tachar todos sus múltiplos, ası́ sucesivamente. El proceso termina
cuando el cuadrado del mayor número confirmado como primo es mayor que
n.
Un refinamiento de la criba consiste en tachar los múltiplos del k-ésimo
número primo pk, comenzando por pk2 pues en los anteriores pasos se habı́an
tachado los múltiplos de pk correspondientes a todos los anteriores números
primos, esto es, 2pk, 3pk, 5pk,..., hasta (pk-1)pk. El algoritmo acabarı́a cuan-
do p2 k¿n ya que no habrı́a nada que tachar.
La codificación de la cibra serı́a como se muestra a continuación :

#define MAX N 1000001


#include <v e c t o r >

v e c t o r <int> p r i m e s ;
bool mark [MAX N ] ;

217
TEORÍA DE NÚMERO

i n l i n e void s i e v e ( int B)
{
i f (B > 1 ) p r i m e s . push back ( 2 ) ;
f o r ( int i =3; i<=B ; i +=2)
{
i f ( ! mark [ i ] )
{
mark [ i ]= true ;
p r i m e s . push back ( i ) ;
i f ( i<=s q r t (B) +1)
f o r ( int j=i ∗ i ; j<=B ; j+=i )
mark [ j ]= true ;
}
}
}

La complejidad del anterior algoritmo es O(N log log N ) lo que hace que
sea un algoritmo bastante rapido. El problema del algoritmo radica en el
costo de memoria ya que necesita un arreglo de N+1 elementos siendo N el
máximo número hasta donde deseamos conocer todos los números primos.
Por lo que el algoritmo solo es aconsejable usarlo cuando N no es mayor de
106 , para un intervalo mayor se puede este algoritmo junto con otras tecnicas
como los test de Primalidad.
Otra variante de dicho algoritmo el cual posee similar complejidad pero
devuelve un vector donde si primes[i]!=0 entonces i es un número primo es
la siguiente

v e c t o r <int> s i e v e o f e r a t o s t h e n e s ( int n )
{
v e c t o r <int> p r i m e s ( n ) ;
f o r ( int i = 2 ; i <n ; ++i )
primes [ i ] = i ;
f o r ( int i = 2 ; i ∗ i <n ; ++ i )
i f ( primes [ i ] )
f o r ( int j = i ∗ i ; j <n ; j+=i )
primes [ j ] = 0 ;
return p r i m e s ;
}

El algoritmo y las implementaciones abordadas son muy eficiente para


generar los números primos en cuanto tiempo de ejecucción pero hay que
tener cuidado con la memoria que consume y la memoria disponible del
problema en que lo apliquemos. El otro punto importante a destacar es como
usar el algoritmo. En los problemas siempre se va trabajar con rangos hasta N
para cada caso por lo que no debemos recalcular los primos en cada caso sino
lo calculamos antes de leer la cantidad casos y seguimos con nuestra solucion

218
TEORÍA DE NÚMERO

y en cada caso usamos el vector donde están amalcenados los primos.

9.1.5. Criba de Atkin


La criba de Atkin es un algoritmo rápido y moderno empleado en ma-
temática para hallar todos los números primos menores o iguales que un
número natural dado. Es una versión optimizada de la criba de Eratóstenes,
pero realiza algo de trabajo preliminar y no tacha los múltiplos de los núme-
ros primos, sino concretamente los múltiplos de los cuadrados de los primos.
Fue ideada por A. O. L. Atkin y Daniel J. Bernstein.
Ası́ funciona el algoritmo:

Todos los restos son módulo 60, es decir, se divide el número entre 60
y se toma el resto.

Todos los números, incluidos x e y, son enteros positivos.

Invertir un elemento de la lista de la criba significa cambiar el valor


(“primos”o “no primos”) al valor opuesto.

1. Crear una lista de resultados, compuesta por 2, 3 y 5.


2. Crear una lista de la criba con una entrada por cada entero po-
sitivo; todas las entradas deben marcarse inicialmente como “no
primos”.
3. Para cada entrada en la lista de la criba:
• Si la entrada es un número con resto 1, 13, 17, 29, 37, 41, 49 ó
53, se invierte tantas veces como soluciones posibles hay para
4x2 + y2 = entrada.
• Si la entrada es un número con resto 7, 19, 31 ó 43, se invierte
tantas veces como soluciones posibles hay para 3x2 + y2 =
entrada.
• Si la entrada es un número con resto 11, 23, 47 ó 59, se invierte
tantas veces como soluciones posibles hay para 3x2 - y2 =
entrada con la restricción x > y.
• Si la entrada tiene otro resto, se ignora.
4. Se empieza con el menor número de la lista de la criba.
5. Se toma el siguiente número de la lista de la criba marcado como
“primos”.
6. Se incluye el número en la lista de resultados.

219
TEORÍA DE NÚMERO

7. Se eleva el número al cuadrado y se marcan todos los múltiplos


de ese cuadrado como “no primos”.
8. Repetir los pasos 5 a 8.
El algoritmo ignora cualquier número divisible por 2, 3 ó 5. Todos los
números con resto, módulo 60, igual a 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20,
22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56 ó 58 son
pares y por tanto compuestos. Los de resto 3, 9, 15, 21, 27, 33, 39, 45, 51 ó
57 son divisibles por 3 y por tanto compuestos. Finalmente, los de resto 5,
25, 35 ó 55 son divisibles entre 5 y por tanto compuestos.Todos estos restos
son ignorados.
Todos los números con resto, módulo 60, igual a 1, 13, 17, 29, 37, 41, 49
ó 53 tienen un resto, módulo 4, de 1. Estos números son primos si y sólo si
el número de soluciones de 4x2 + y2 = n es impar y el número es libre de
cuadrados.
Todos los números con resto, módulo 60, igual a 7, 19, 31 ó 43 tienen un
resto, módulo 6, de 1. Estos números son primos si y sólo si el número de
soluciones de 3x2 + y2 =n es impar y el número es libre de cuadrados.
Todos los números con resto, módulo 60, de 11, 23, 47 ó 59 tienen un
resto, módulo 12, de 11. Estos números son primos si y sólo si el número de
soluciones de 3x2 - y2 = n es impar y el número es libre de cuadrados.
Ninguno de los candidatos a primos es divisible entre 2, 3 ó 5, por lo que
no puede ser divisible entre sus cuadrados. Esta es la razón por la que las
comprobaciones de si un número es libre de cuadrados no incluyen los casos
22 , 32 y 52 .
A continuación una implementación con una complejidad de O (n / log
log n).

void s i e v e o f a t k i n ( )
{
int n ;
f o r ( int z = 1 ; z <= 5 ; z+=4)
{
f o r ( int y = z ; y<= s q r t (N) ; y+=6)
{
f o r ( int x = 1 ; x <= s q r t (N) && ( n=4∗x∗x+y∗y )<= N; ++x )
isprime [ n]=! isprime [ n ] ;
f o r ( int x = y + 1 ; x <= s q r t (N) && ( n=3∗x∗x−y∗y )<=N; x
+=2)
isprime [ n]=! isprime [ n ] ;
}
}

f o r ( int z = 2 ; z<= 4 ; z+=2)

220
TEORÍA DE NÚMERO

{
f o r ( int y = z ; y<=s q r t (N) ; y +=6)
{
f o r ( int x = 1 ; x<=s q r t (N) && ( n=3∗x∗x+y∗y )<=N; x+=2)
isprime [ n]=! isprime [ n ] ;
f o r ( int x = y+1;x<=s q r t (N) && ( n=3∗x∗x−y∗y )<=N; x+=2)
isprime [ n]=! isprime [ n ] ;
}
}

f o r ( int y = 3 ; y <= s q r t (N) ; y+=6)


{
f o r ( int z = 1 ; z <=2; ++z )
{
f o r ( int x = z ; x<=s q r t (N) && ( n=4∗x∗x+y∗y )<=N; x+= 3 )
isprime [ n]=! isprime [ n ] ;
}
}

f o r ( int n=5; n<=s q r t (N) ; ++n )


i f ( isprime [ n ] )
f o r ( int k=n∗n ; k<=N; k+=n∗n )
i s p r i m e [ k]= f a l s e ;
i s p r i m e [ 2 ] = i s p r i m e [ 3 ] = true ;
}

Esta es una variante con una complejidad de O(n)

void s i e v e o f a t k i n ( )
{
int n ;
f o r ( int x = 1 ; x<=s q r t (N) ; ++x )
{
f o r ( int y = 1 ; y<=s q r t (N) ; ++y )
{
n= 4∗ x∗x+y∗y ;
i f ( n <=N && ( n %12==1 | | n %12==5))
isprime [ n]=! isprime [ n ] ;
n=3∗x∗x+y∗y ;
i f ( n<=N && n %12==7)
isprime [ n]=! isprime [ n ] ;
n=3∗x∗x−y∗y ;
i f ( x>y && n<=N && n %12==11)
isprime [ n]=! isprime [ n ] ;
}
}
f o r ( int n = 5 ; n<=s q r t (N) ; ++n )
i f ( isprime [ n ] )
f o r ( int k = n∗n ; k<=N; k+=n∗n )

221
TEORÍA DE NÚMERO

i s p r i m e [ k]= f a l s e ;
i s p r i m e [ 2 ] = i s p r i m e [ 3 ] = true ;
}

9.2. Sistemas de numeración


Un sistema de numeración es un conjunto de sı́mbolos y reglas que per-
miten representar datos numéricos.
Un sistema de numeración puede representarse como:
N = S, R
Donde:

1. N es el sistema de numeración considerado (p.ej. decimal, binario, etc.).

2. S es el conjunto de sı́mbolos permitidos en el sistema. En el caso del


sistema decimal son 0,1,...9; en el binario son 0,1; en el octal son 0,1,...7;
en el hexadecimal son 0,1,...9, A, B, C, D, E, F.

3. R son las reglas que nos indican qué números y qué operaciones son
válidos en el sistema, y cuáles no. En un sistema de numeración po-
sicional las reglas son bastante simples, mientras que la numeración
romana requiere reglas algo más elaboradas.

Los sistemas de numeración pueden clasificarse en dos grandes grupos:


posicionales y no-posicionales:
En los sistemas no-posicionales los dı́gitos tienen el valor del sı́mbolo
utilizado, que no depende de la posición (columna) que ocupan en el núme-
ro. Estos son los más antiguos, se usaban por ejemplo los dedos de la mano
para representar la cantidad cinco y después se hablaba de cuántas manos se
tenı́a. También se sabe que se usaba cuerdas con nudos para representar can-
tidad. Tiene mucho que ver con la cardinalidad entre conjuntos. Entre ellos
están los sistemas del antiguo Egipto, el sistema de numeración romana, y
los usados en Mesoamérica por mayas, aztecas y otros pueblos. Al igual que
otras civilizaciones mesoamericanas, los mayas utilizaban un sistema de nu-
meración de raı́z mixta de base 20 (vigesimal). También los mayas preclásicos
desarrollaron independientemente el concepto de cero (existen inscripciones
datadas hacia el año 36 a. C. que ası́ lo atestiguan.).
En los sistemas de numeración ponderados o posicionales el valor
de un dı́gito depende tanto del sı́mbolo utilizado, como de la posición que
ése sı́mbolo ocupa en el número.

222
TEORÍA DE NÚMERO

El número de sı́mbolos permitidos en un sistema de numeración posicional


se conoce como base del sistema de numeración. Si un sistema de numeración
posicional tiene base b significa que disponemos de b sı́mbolos diferentes para
escribir los números, y que b unidades forman una unidad de orden superior.
Los sistemas de numeración actuales son sistemas posicionales, que se
caracterizan porque un sı́mbolo tiene distinto valor según la posición que
ocupa en la cifra.

Teorema fundamental de la numeración Este teorema establece la


forma general de construir números en un sistema de numeración posicional.
Primero estableceremos unas definiciones básicas:
N: Número válido en el sistema de numeración.
B: Base del sistema de numeración. Número de sı́mbolos permitidos en
el sistema.
di: Un sı́mbolo cualquiera de los permitidos en el sistema de numera-
ción.
n: Número de dı́gitos de la parte entera.
,: Coma fraccionaria. Sı́mbolo utilizado para separar la parte entera de
un número de su parte fraccionaria.
k: Número de dı́gitos de la parte decimal.
Los sistemas de numeración más conocidos:
Sistema decimal Es sistema de numeración que se utiliza habitual-
mente, que se compone de diez sı́mbolos o dı́gitos (0, 1, 2, 3, 4, 5, 6, 7,
8 y 9).
Sistema binario El sistema de numeración binario utiliza sólo dos
dı́gitos, el cero (0) y el uno (1). llamado también sistema diádico en
ciencias de la computación, es un sistema de numeración en el que los
números se representan utilizando solamente las cifras cero y uno (0 y
1).
Sistema octal El sistema numérico octal o de base ocho es el sistema
de numeración que utiliza ocho dı́gitos o sı́mbolos (0 al 7).
Sistema hexadecimal: Este sistema es de base 16, lo que significa que
para cada columna es posible escoger uno de entre 16 dı́gitos. Éstos son
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E y F.

223
TEORÍA DE NÚMERO

En variados problemas de concursos de programación se presentan pro-


blemas donde se deben convertir números de una base a otra, es por eso que
a continuación se muestra un grupo de algoritmos que puede resultar útiles
en estos casos.
El siguiente algoritmo convierte un número en base decimal que puede ser
positivo o negativo a la representación numérica en una base B (debe sistemas
de numeración ponderados o posicionales) la cual bien puede ser positiva o
negativa. El algoritmo recibe como parámetros el numero en representación
decimal y la base a que se desea convertir y devuelve en un vector los dı́gitos
que conforman la nueva representación.

#define MAX N 1000001


#include <v e c t o r >
#define LL long long

v e c t o r <LL> convertDecimalToB (LL n , LL b )


{
v e c t o r <LL> d i g i t s ;
LL x ;
while ( n != 0 )
{
x= n % b ;
n/= b ;
i f ( x<0)
n +=1;
x<0 ? d i g i t s . push back ( x+( b ∗−1) ) : d i g i t s . push back ( x
);
}
i f ( d i g i t s . empty ( ) )
d i g i t s . push back ( 0 ) ;
r e v e r s e ( d i g i t s . b e g i n ( ) , d i g i t s . end ( ) ) ;

return d i g i t s ;
}

Esta claro que cuando la base a convertir sea mayor que 10 se hace ne-
cesario otra función auxiliar que luego los valores 11, 12 , 13 a A, B, C y ası́
sucesivamente por ejemplo para el caso de la base hexadecimal.
También basados en autómatas de divisibilidad es posible determinar si
un numero expresado en una base X (debe sistemas de numeración pondera-
dos o posicionales) es divisible por un número es base decimal.

bool a u t o m a t a D i v i s i b i l i d a d ( s t r i n g numero , int base , int d e c i m a l


)
{
int e s t a d o =0;

224
TEORÍA DE NÚMERO

int t S i z e=numero . s i z e ( ) ;

f o r ( int i =0; i <t S i z e ; i ++)


{
e s t a d o =( e s t a d o ∗ b a s e +( v a l u e ( numero [ i ] ) ) ) %d e c i m a l
;
}

i f ( ! estado )
return true ;
else
return f a l s e ;
}

Donde la función value devuelve el valor en decimal del carácter numero[i].


La siguiente implementación permite convertir un número decimal a ro-
mano.

s t r i n g numberRoman ( int x )
{
s t r i n g roman ;
roman . c l e a r ( ) ;
while ( x>=1000){x−=1000;roman+="M" ; }
while ( x>=900){x−=900;roman+="CM" ; }
while ( x>=500){x−=500;roman+="D" ; }
while ( x>=400){x−=400;roman+="CD" ; }
while ( x>=100){x−=100;roman+="C" ; }
while ( x>=90){x−=90;roman+="XC" ; }
while ( x>=50){x−=50;roman+="L" ; }
while ( x>=40){x−=40;roman+="XL" ; }
while ( x>=10){x−=10;roman+="X" ; }
while ( x>=9){x−=9;roman+="IX" ; }
while ( x>=5){x−=5;roman+="V" ; }
while ( x>=4){x−=4;roman+="IV" ; }
while ( x>=1){x−=1;roman+="I" ; }
return roman ;
}

9.3. Factores primos


En teorı́a de números, los factores primos de un número entero son los
números primos divisores exactos de ese número entero. El proceso de búsque-
da de esos divisores se denomina factorización de enteros, o factorización en
números primos.
Para un factor primo p de n, la multiplicidad de p es el máximo exponente

225
TEORÍA DE NÚMERO

a para el cual pa es un divisor de n. La factorización de un número entero es


una lista de los factores primos de ese número, junto con su multiplicidad.
El Teorema fundamental de la Aritmética establece que todo número entero
positivo tiene una factorización de primos única.
Para descomponer un número en factores primos solo basta con seguir la
idea aprendida en enseñanzas anteriores ejemplo:

En esta caso 4620 = 22 ∗ 31 ∗ 51 ∗ 71 ∗ 111


Idea: dividir por el primo actual comenzando por 2 hasta que el número
no sea divisible por este y el primo actual sea menor que N, luego pasar al
próximo primo. Es necesario tener una lista de los primos hasta la raı́z de N
pues si luego de dividir N por todos los primos hasta la raı́z si lo que queda
es distinto de 1 entonces lo que queda de N es un número primo.

// d e f i n i r una t u p l a para almacenar a ˆb


// ( a b a s e )=>. f i r s t ( b expo )=>.s e c o n d
typedef p a i r <int , int>f a c ;
v e c t o r <f a c > F a c t o r i z a r ( int n ) {
int l = primos . s i z e ( ) ;
// i t e r a r s o b r e t o d o s l o s primos
// h a s t a que a l g u n primo s e a mayor que n
v e c t o r <f a c >FP ;
f o r ( int i =0; i <l && primos [ i ]<=n ; i ++){
int p i = primos [ i ] ;
int exp = 0 ;
i f ( n %p i ==0){
while ( n %p i ==0){
exp++;// c o n t a d o r para e l exponente
n/=p i ; // d i v i d o e n t r e l a b a s e
}
// p i ˆ exp
// {a , b} e s l o mismo que m a k e p a i r ( a , b ) o P a i r ( a , b )
FP . push back ( { pi , exp } ) ;
}
}
// s i n e s d i s t i n t o n e s un primo
i f ( n>1){
FP . push back ( { n , 1 } ) ;
}
return FP ;

226
TEORÍA DE NÚMERO

Que pasa si ahora queremos descomponer N! la idea básica seria iterar


desde 1 hasta N e ir descomponiendo cada número acumulando las poten-
cias o hallar
√ N! y luego descomponerlo, pero esto tendrı́a una complejidad
de O(N ∗ N ) el cual serı́a muy costoso para valores muy grandes de N.
Analicemos la siguiente idea:
10!=1*2*3*4*5*6*7*8*9*10 Necesitamos hallar cuantos 2 hay.
10! 1 2 3 4 5 6 7 8 9 10 total
Descomposición de n1 1 2 3 2 5 2*3 7 23 32 2*5
2

Primo 2 0 1 0 2 0 1 0 3 0 1 8
Primo 3 0 0 1 0 0 1 0 0 2 0 4
Para el caso del 2 seria todos los múltiplos de 2 hasta N ejemplo para 10
hay 5 que seria 10/2 luego todos los múltiplos de 4 hasta N que serı́a N/4
y ası́ hasta que la potencia de 2 sea mayor que N la suma de estos seria el
exponente de la potencia de 2 luego de descomponer N!, en este caso serı́a
210/2+10/4+10/8 =25+2+1 =28 y este serı́a el procedimiento para todos los primos
hasta N.

v e c t o r <f a c > F a c t o r i z a r F a c t o r i a l ( int n ) {


int l = primos . s i z e ( ) ;
v e c t o r <f a c >FP ;
// i t e r a r s o b r e t o d o s l o s primos menores e i g u a l e s que n
f o r ( int i =0; i <l && primos [ i ]<=n ; i ++){
// acumulador para l a p o t e n c i a d e l primo a c t u a l
int exp = 0 ;
// p o t e n c i a d e l primo a c t u a l
int pot = primos [ i ] ;
// m i e n t r a s l a p o t e n c i a s e a menor que n
while ( pot<=n ) {
// m u l t i p l o s de l a p o t e n c i a menores que n
exp+=n/ pot ;
// proxima p o t e n c i a d e l primo
pot∗=primos [ i ] ;
}
FP . push back ( { primos [ i ] , exp } ) ;
}
return FP ;
}

Con la factorización de un número N en números primos es posible hallar


la cantidad de divisores de N y la suma de estos. Veamos:
Si descomponemos N en ba11 *ba22 *ba33 * ... * bamm
La cantidad de divisores de N serı́a :

227
TEORÍA DE NÚMERO

(a1 + 1)*(a2 + 1)*(a3 + 1)* ... * (am + 1)


Por ejemplo 12=22 *31 , (2+1)*(1+1)= 6 el 12 tiene 6 divisores: 1, 2, 3,
4, 6, 12.
La suma de los divisores de N es igual a:

Para el caso de 12:

Comprobación los divisores de 12 son 1+2+3+4+6+12 = 28. En el epı́gra-


fe Análisis de ejercicios de este capı́tulo se abordan diferente ejercicios que
su solución parten de los elementos abordados en este epı́grafe.

9.4. Reglas de divisibilidad


Las reglas de divisibilidad nos permiten saber, de forma más o menos
rápida, si un número es divisible entre otro sin la necesidad de dividir.
Por muy trivial que parezca este asunto, ha sido bastante explotado en
un buen número de problemas de concursos e inclusos en competencias. Y
porque ?. Si para saber si un número a es divisible por otro número b solo
basta con comprobar si el resto de la división es cero. Donde esta entonces
el saber algunas de las reglas divisibilidad ?.
Resulta que el el 100 % de los problemas donde se necesita verificar si
a mod b = 0 el número a tiene una cantidad de dı́gitos no puede ser almace-
nado por ningún de los tipos de datos númericos enteros conocidos. Entonces
que hacer. Primero veamos alguna de las reglas de divisibilidad conocidas.

2: Si termina en 0, 2, 4, 6 ó 8.

3: Si la suma de sus cifras es múltiplo de 3.

4: Si sus dos últimas cifras son 00 ó un múltiplo de 4 (12, 16, 20, 24,
28, 32, 36 y 40).

5: Si termina en 0 y 5.

7: Cuando la diferencia entre el número sin la cifra de las unidades y


el doble de la cifra de las unidades es 0 ó múltiplo de 7.

228
TEORÍA DE NÚMERO

8: Si sus tres últimas cifras son ceros o múltiplo de 8.

9: Si la suma de sus dı́gitos nos da múltiplo de 9.

10: Si la cifra de las unidades es 0.

11: Si la suma de las cifras que ocupan un lugar par menos la suma de
las otras cifras es 0 ó un múltiplo de 11 (11, 22, 33, 44,. . . )

25: Si sus dos últimas cifras son ceros o múltiplo de 25.

Bien volviendo al problema inicial como podemos dado un número a en


base decimal con una cantidad de digitos bastante grandes como para no
almacenarlo en tipos de datos númericos enteros saber si es divisible por un
valor b en base decimal que si puede ser almacenado en un tipo de datos
númericos enteros.
Existen dos variantes una usando las ciertas ventajas que nos puede dar
el lenguaje de programación Java y la otra aplicando un poco de análisis
matemático.

9.4.1. Usando BigInteger


El lenguaje de programación de Java nos ofrece la clase BigInteger pa-
ra operar y manipular números grandes como lo pueden ser los que hemos
mencionado anteriormente. Y la solución para este tipo de problema estan
sencillo como se muestra a continuación:

import j a v a . math . B i g I n t e g e r ;
import j a v a . u t i l . Scanner ;

public c l a s s Main {
public s t a t i c void main ( S t r i n g [ ] a r g s ) {

Scanner i n=new Scanner ( System . i n ) ;


B i g I n t e g e r a=i n . n e x t B i g I n t e g e r ( ) ;

B i g I n t e g e r b=new B i g I n t e g e r ( "495" ) ;

i f ( a . mod( b ) . compareTo ( B i g I n t e g e r .ZERO)==0)


System . out . p r i n t l n ( "Es divisible " ) ;
else
System . out . p r i n t l n ( "No es divisible " ) ;
}
}

229
TEORÍA DE NÚMERO

Esta variante es bastante simple fácil de implementar pero importante


tiene en la mayorı́a de los casos un alto consumo de memoria. Asi que uso
debe ser analizada previamente.

9.4.2. Análisis matemático


Esta variante es más efeciente aunque tiene un grado análisis mayor que
el anterior. Es la variante que deberı́an utilizar aquellos amantes de C++.
El primer paso es almacenar el número a en una variable de tipo cadena
string no hay otra variante. Lo siguiente es ver quien es b si es un valor fijo
siempre o es una valor variable para cada caso.
Si fuera el primer caso el valor b es fijo siempre es el mismo realizamos el
siguiente análisis. Analizamos si el valor b es un valor que tiene definida su
regla de divisibilidad. De ser ası́ implementamos un algoritmo que compruebe
la regla de divisibilidad del valor b en la cadena donde esta almacenado el
valor a.
En caso de que el valor b no tenga regla de divisibilidad conocida entonces
lo descomponemos en factores que tengan definido sus reglas de divisibilidad
y luego solo debemos comprobar que la cadena cumpla con cada una de las
reglas de divisibilidad de cada factor. De cumplir con todas entonces podemos
afirmar que el valor a es divisible por el valor b.
Pero que hacer cuando el valor b es variable para cada caso o simplemen-
te no tiene regla de divisibilidad ni él ni sus factores. Bueno en esta caso
podemos aplicar un autómata de divisibilidad. Un autómata de divisibilidad
permite saber si un número X con una gran cantidad de digitos expresado
en una base Y es divisible por un número Z el cual esta en base decimal.
Extrapolando lo anterior a nuestro problema a es X 10 es Y y b es Z. El
código es el siguiente:

int v a l u e ( char symbol )


{
i f ( ’0’<= symbol && symbol<=’9’ )
return ( symbol−’0’ ) ;
i f ( ’A’<= symbol && symbol <=’F’ )
return ( symbol−’A’ ) +10;
i f ( ’a’<= symbol && symbol <=’f’ )
return ( symbol−’a’ ) +10;
return 0 ;
}

bool a u t o m a t D i v i s i b l e ( s t r i n g number , int b a s e , int decimal ){


int s t a t e =0;

230
TEORÍA DE NÚMERO

int s i z e= number . s i z e ( ) ;
REP( i , s i z e )
s t a t e =( s t a t e ∗ b a s e+v a l u e ( number [ i ] ) ) % d e c i m a l ;
i f ( ! s t a t e ) return true ;
e l s e return f a l s e ;
}

9.5. Euler’s function

9.6. The Carmichael function

9.7. The Mobius function

9.8. Autómata de divisibilidad

9.9. Análisis de ejercicios


2889 - Integer Estate Agent Este ejercicio es similar al ejercicio 1115
- Sequence Sum Possibilities (Su análisis está en el capitulo de aritmética y
álgebra) todo se reduce a saber de cuantas formas un número Z puede ser
expresado como suma de números de concecutivos. Las únicas adecuaciones
con respecto al 1115 - Sequence Sum Possibilities es que para este caso se
considera una solución posible una secuencia de numeros consecutivos de
longitud uno, es decir Z, y el menor número de la secuencia de números
consecutivos debe ser 2 y no 1 como en el ejercicio 1115.
Un número Z se puede expresar como suma de números consecutivos
cuando cumple la condición siguiente:
Z = n ∗ a + ((n − 1) ∗ n)/2
Donde Z es el número que se desea saber si se puede expresar como suma
de enteros positivos, n la cantidad de números consecutivos para expresar
la suma, a es el primer número y menor de la secuencia de los sumandos.
Sabiendo eso solo basta comprobar para un Z todo los n que cumpla los
siguientes criterios:
Z >= ((n − 1) ∗ n)/2
(Z − (((n − 1) ∗ n)/2)) %n == 0
(Z − (((n − 1) ∗ n)/2))! = 0
a>1

231
TEORÍA DE NÚMERO

2842 - Lazy Cat Hacer una criba de primos hasta un millón guardando
en un vector auxiliar aquellos que también son palı́ndromos. El resto de
los primos palı́ndromos del rango de 106 a 107 se construye números que
sean palı́ndromos y verificar que sean primos en caso de cumplir con los
dos se guardan en el vector auxiliar. Se lee los lı́mites del intervalo se hace
un lower bound y upper bound con el vector de primos palı́ndromos y se
imprime la diferencias de los iteradores.

3466 - Take This El ejercicio se reduce a tomar el valor N y representarlo


en base P y si dicha representación solo tiene los dı́gitos 0 y 1 es posible
encontrar al verdadero Master Sword sino no se puede.

2897 - GCD and Longest Subsequence II El ejercicio se reduce a


encontrar la mayor secuencia de números coprimos entre sı́ de la lista dada
inicialmente. Para esto dividiremos la lista de números en tres grupos. Un
primer grupo estarán todos los unos que tengan la lista estos elementos se
adicionan a la secuencia que P que será la que contendrá los números que
conforman la solución. En el segundo grupo agruparemos lo números primos
de la lista sin repetir, y posterior lo adicionamos a P. En el tercer grupo
estarán los números no primos ordenados de menor a mayor por la cantidad
de primos que lo dividen y en caso de igualdad lo decide el valor del elemento.
Tomaremos de este grupo aquellos cuyos primos divisores no estén incluidos
en P, lo adicionaremos P (importante cuando se adiciona un número no primo
a P hay que tener algo que me diga que lo divisores primos de ese número
fueron utilizados o están en P). La solución es la longitud de P.

1870 - A Sum Game Si analizamos detenidamente para formar un nume-


ro siempre se va sumar potencias de 4 las cuales de cada potencias se puede
usar cero, una o dos veces lo que nos llevas a coger el N inicial llevarlo a base
3 esto nos garantiza que la representación de N solo use sı́mbolos {0, 1, 2}
luego al N convertido en un número ternario (base 3) lo volvemos a convertir
a decimal asumiendo que la base en vez de tres es cuatro y ese nuevo valor
decimal es la respuesta que busca el ejercicio.

1936 - An Easy Problem! Para resolver este problema solo se tiene que
conocer que un número R expresado en una base numérica N es divisible por
X donde X >= N + 1 siempre y cuando la suma de los dı́gitos de R de un
múltiplo de X. Ojo el numero entrado puede ser negativo.

232
TEORÍA DE NÚMERO

2704 - Playing with Numbers La solución de este ejercicio es idéntica-


mente la misma que la del problema 2897 - GCD and Longest Subsequence
II el único cambio es el rango de los números en el 2897 los números van de
1 a 25 mientras en este van de 1 a 100.

3324 - List of Natural Numbers La solución es bien simple. Para cada


N, excepto para n=1, la solución coincide con la cantidad de naturales que son
primos relativos con N menores que él. (Función de Euler). La complejidad
del problema radica en darse cuenta de este hecho, en honor a la verdad solo
simule el proceso hasta un numero prudente y trantando de sacar posibles
pistas noté esto, ası́ que arriegue y tiré una auténtica ’pedra’. Desconozco
cual es la solución cientı́fica.

1131 - Divisors Una vez leido el problema es evidente que nos piden
calcular el numero de divisores de Cn,p el cual a priori se calcula de la siguiente
forma:
n!
Cn,k = k!∗(n−k)!
Como vemos si calculamos primero el valor de Cn,p para luego determinar
la cantidad de divisores de ese número este proceso puede ser lento y existir el
desbordamiento de datos si el lenguaje escogido no soporte enteros grandes.
Ahora sabemos que la cantidad de divisores de número se puede calcular a
partir de la siguiente fórmula:
N = k1 ∗ q1 + k2 ∗ q2 + k3 ∗ q3 + ... + kn ∗ qn
Donde N es el número al cual deseamos conocer la cantidad de divisores
y el miembro derecho de la fómula es la factorización de N en los números
primos q1 , q2 , q3 y qn . Y k1 , k2 , k3 y kn la cantidad de veces que esta presente
el número primo asociado a este en la factorización. Luego la cantidad de
divisores de un número N es igual al resultado de la siguiente fómula:
(k1 + 1) ∗ (k2 + 1) ∗ (k3 + 1) ∗ .... ∗ (kn + 1)
Una vez visto esto vayamos a otro dellate de la siguiente ecuación:
n!
k!∗(n−k)!
Si la expandimos de la siguiente manera:
1∗2∗3∗...∗n
1∗2∗3∗...∗k∗1∗2∗3∗...(n−k)
Y comenzamos a simplificar los números del denominador con los núme-
ros presentes en numerador, la simplicación va dar como resultado en el
numerador todos los números primos menores e iguales a N mientras en el
denominador va estar todos los numeros primos menores e iguales que n-k
y todos los numeros primos menores e iguales que k. Tanto en denominador
como el numerador los numeros primos pueden estar repetidos.

233
TEORÍA DE NÚMERO

Una vez visto estos detalles la solución del problema se reduce a realizar
una criba de primos para determinar todos los primos desde 1 hasta 435.
Luego tener un vector de la misma dimensión del vector donde almacenaste
los primos que arrojó la criba. Este vector al cual llamaremos countPrimes al-
macenará en la posición e la cantidad de veces que el primo enesimo divide a
un determinado valor. Inicialmente los valores almacenados en countPrimes
serán cero luego factorizamos en numeros primos k sumando en countPri-
mes[e] la cantidad de veces que primes[e] divide a k. Luego factorizamos k-n
y n pero en vez de sumar en countPrimes[e] restaremos la cantidad de veces
que primes[e] divide a k-n y n. Muy importante primes[e] no tiene que ser
divisor de k , ni k-n y n. solo menor e igual que este y contamos la parte
entera
Q de la división. Luego la solución es:
(countP rimes[i] + 1)
Otra variante solución es serı́a descomponer los 3 términos n!, (n-k)! y
k! luego a la descomposición de n! le restamos los exponentes de los otros
2 términos y aplicar la fórmula para la cantidad de divisores de un valor N
visto en el epı́grafe Factores primos.

3771 - Divisors and Coprimes El ejercicio se reduce a contar de divisores


de N que son coprimos con √ K. Para ver todo los divisores de N solo tenemos
que iterar desde 1 hasta N y cada vez que encontremos un i que cumpla la
siguiente condicion que N %i==0 solo tenemos que chequear que i y K son
coprimos con solo invocar el algortimo del máximo común divisor donde si
el resultado es 1 entoces i y K son coprimos y debo contar. De igual forma
debo chequear j con K donde j es igual N÷i. Tener en cuenta que si j==i no
se hace falta hacer el último chequeo.

TIMUS 1086. Cryptography La solución de problema radica en hallar


el enésimo número primo con la restricciones que el 1 no es número primo
y la N su mayor valor es 15000. Por lo que solo debemos hallar los primeros
15000 números primos sin contar el 1 almacenarlo y luego la respuesta serı́a
number primes[n-1]. Para hallar los primeros 15000 números primos se puede
utilizar una criba Eratóstenes hasta un millón.

3822 - AlmostPrime La solución del problema radica en dado un número


N descomponerlo en factores primos. Una vez factorizado en números primos
se va contar de cada número primo divisor aquellos multiplos de estos que
sean menores e iguales que N y que su máximo común divisor con N sea el
propio número primo que los divide a ambos. y el resultado de esa cuenta
será la solución.

234
TEORÍA DE NÚMERO

3760 - PDF Check El ejercicio solo nos pide chequear si dado dos números
A y B todos los divisores propios de A sumados es igual a B o viceversa con
al menos uno de los dos casos se cumplan la respuesta es positiva sino es
negativo. Se puede tener un arreglo donde la inésima posición se va alamcenar
la suma de todos los divisores del inésimo valor. Para llenar el arreglo se puede
utilizar un algoritmo parecido a la criba de primos de Eratóstenes.

2874 - Lazy Cat III Este problema bien se puede solucionar al menos
con dos variantes, las cuales explicares. Para comenzar que dos números
son coprimos cuando el máximo común divisor de los dos es uno. Dicho
esto vamos solucionar el problema utilizando una técnica de programación
dinámica como es la tabla acumulativa. Primero creamos una matriz de 2 filas
con 105 +10 columnas todas con valor cero. Luego recorremos la matriz las
dos filas simultaneas de 1 a N y para cada columna i planteamos lo siguiente:
Si i es coprimo del numero de Garfield entonces
matriz[0][i]=1+matriz[0][i-1]
sino
matriz[0][i]=matriz[0][i-1]
De similar manera se plantea:
Si i es coprimo del numero de Anders entonces
matriz[1][i]=1+matriz[1][i-1]
sino
matriz[1][i]=matriz[1][i-1]
Luego para saber si Garfield respondió bien basta con probar que matriz[1][aj]-
matriz[1][ai-1] es igual al número dicho por Garfield. De igual manera suce-
de con Anders basta con chequear su respuesta coincide con matriz[0][gj]-
matriz[0][gi-1]. Luego de acuerdo a si aceptan o no en sus respuestas se va
llevando un acumulado de puntos para cada y luego se saca el resultado del
problema de acuerdo a los valores de esas variables.
La segunda variante es utilizando la estructura de datos Range Tree con
algunas modificaciones en su operación de construcción y consulta. Aquı́ les
dejo las modificaciones para este caso.

struct Node
{
int c C o p r i m e G a r f i e l d ;
int cCoprimeAnders ;
};

Node ST [MAXTREE] ;

....

235
TEORÍA DE NÚMERO

void I n i t T r e e ( int idx , int l e f t , int r i g h t )

{
i f ( l e f t == r i g h t )
{
i f (GCD( min ( l e f t , numberAnders ) ,max( l e f t , numberAnders ) )
==1)
ST [ i d x ] . cCoprimeAnders =1;
else
ST [ i d x ] . cCoprimeAnders =0;
i f (GCD( min ( l e f t , n u m b e r G a r f i e l d ) ,max( l e f t ,
n u m b e r G a r f i e l d ) )==1)
ST [ i d x ] . c C o p r i m e G a r f i e l d =1;
else
ST [ i d x ] . c C o p r i m e G a r f i e l d =0;
return ;
}
I n i t T r e e ( 2 ∗ idx , l e f t , MID) ;
I n i t T r e e ( 2 ∗ i d x +1, MID+1, r i g h t ) ;
ST [ i d x ] . cCoprimeAnders = ST [ 2 ∗ i d x ] . cCoprimeAnders+ST [ 2 ∗
i d x + 1 ] . cCoprimeAnders ;
ST [ i d x ] . c C o p r i m e G a r f i e l d = ST [ 2 ∗ i d x ] . c C o p r i m e G a r f i e l d+ST
[2∗ idx +1]. cCoprimeGarfield ;
}

int Q u e r y G a r f i e l d ( int idx , int l , int r , int l e f t , int r i g h t )


{

i f ( l <= l e f t && r i g h t <= r ) return ST [ i d x ] . c C o p r i m e G a r f i e l d


;
int r e t = 0 ;
i f ( l <= MID)
r e t += Q u e r y G a r f i e l d ( 2 ∗ idx , l , r , l e f t , MID) ;
i f ( r > MID)
r e t += Q u e r y G a r f i e l d ( 2 ∗ i d x +1, l , r , MID+1, r i g h t
);
return r e t ;
}

int QueryAnders ( int idx , int l , int r , int l e f t , int r i g h t )


{
i f ( l <= l e f t && r i g h t <= r ) return ST [ i d x ] . cCoprimeAnders ;
int r e t = 0 ;
i f ( l <= MID)
r e t += QueryAnders ( 2 ∗ idx , l , r , l e f t , MID) ;
i f ( r > MID)
r e t += QueryAnders ( 2 ∗ i d x +1, l , r , MID+1, r i g h t ) ;
return r e t ;

236
TEORÍA DE NÚMERO

3938 - Computing multiples El ejercicio nos pide determinar cuantos


números en el rango [1 K] cuantos son multiplos de al menos de uno de los
números de la lista L. Basta con tener un arreglo de dimension mayor que
K inicializado con todos sus valores en false. Luego realizando una especie
de criba marcamos en true todas las posiciones en arreglo donde el valor de
la posición es divisible por al menos de unos de los números de L. Luego
basta con recorrer de 1 a K en arreglo y contar las posiciones que son true e
imprimir ese resultado.

2849 - FF’s Divisors El problema nos pide dado un número N determinar


la cantidad de divisores del mismo. Sabiendo que:
N = aα bβ ...z γ
Donde a,b,c son números primos. La cantidad de divisores es igual a :
Np = (α + 1)(β + 1)....(γ + 1)
Con lo anterior explicado es facil deducir que para resolver el problema
hallamos todos los primos hasta un 106 en precalculo antes de comenzar a
leer las posibles N. Luego para cada N

Si es primo y no esta entre los primos hallados lo adiciono a la colección


y la respuesta es dos.

Si es primo y esta entre los primos hallados la respuesta es dos

Si no es primo lo factorizo en números primos y almaceno el exponen-


te de cada primo de la factorización y resuelvo la fórmula explicada
anteriormente para dar respuesta.

3225 - D-dimentional Mesh (Lode) El texto le da un buen a camuflaje


a la esencia del problema el cual es dado un K que representa la capacidad
de carga de la nave te piden hallar el numero de item de cada dimensión que
puede llevar donde en una dimensión no puede llevar mas de dos item en caso
de de tres representa un item en la proxima dimesión por lo que el ejercicio
se reduce a leer el K y imprimir según el formato de salida del problema su
representación en el sistema de numeración ternario o base 3.

1602 - Numerically Speaking Para solución del ejercicio solo basta saber
como llevar un número en base decimal a su representación númerica en base
26 teniendo en cuenta que 1 es a hasta 26 que es z y de forma contraria

237
TEORÍA DE NÚMERO

dada una secuencia de caracteres que representa un valor númerico en base


26 llevarlo a base decimal. Lo complejo es formato de salida para cada caso y
que el valor númerico en base decimal puede superar el rango que soporta los
tipos de datos númericos nativos de los lenguajes como C++ y Java, asi que
recomiendo implementar la solución en Java con la utilización del BigInteger.
En mi solución utilice Java.

1306 - Div 4 Es un ejercicio bien sencillo diga si un número es divisible por


cuatro o no. Para realizar el ejercicio es importante conocer que un número
es divisible por 4 solo si el número que conforman los digitos de las decenas y
unidades en ese orden es multiplo de de 4. Lo intersante del problema es que
el número del cual debemos determinar si es o no divisible por 4 puede tener
hasta 100 dı́gitos, asi que sugiero que trabajen el número como una cadena
de caracteres, al no ser que usen el BigInteger de Java o Python.

1288 - Div 6 La idea del ejercicio es bien sencilla dado una colección
de números debemos de cir de cada uno si es divisible o no por 6. La una
dificultad esta es que los valores númericos estan en el rango de de números
hasta 1000 dı́gitos. Es evidente que desea solucionar el problema debe tratar
el número como cadena (a no ser que use BigInteger de Java o Ptyhon).
Ahora como se puede saber si la cadena que representa un valor númerico es
divisible por 6. Bien sabemos que el 6 factorizado en numeros primos es 2 y
3. Esto significa que cualquier número divisible por 6 debe ser divisible por 3
y 2 a la vez. Como sabemos un número es divisble por 2 cuando termina su
ultima cifra en 0,2,4,6,8. Esto es fácil de comprobar en la cadena solo debemos
acceder a la ultima posición y chequear que valor tiene este. Ahora sabemos
que un número es divisible por 3 cuando la suma de los digitos que componen
el número es multiplo de 3. Esto lo podemos comprobar recorriendo la cadena
y cada posición y el caracter llevarlo a su valor en número y sumarlo y si dicha
suma es divisible por 3 entonces el valor númerico contenido en la cadena
es divisble por 3. Con lo explicado anteriormente ya la solución del ejercicio
queda mucho mas clara.

4012 - Dividing Hexadecimal Numbers Una vez leı́do el problema nos


podemos percatar que nos estan preguntando si dado un número expresado
en base hexadecimal es divisible por el valor 17 en decimal. Para solucionar
este problema solo basta aplicar un automata de divisibilidad para solucionar
el problema. Recordar que el valor númerico e hexadecimal contiene las letras
en el rango de la A a la F.

238
TEORÍA DE NÚMERO

1362 - Diophantes Leyendo el tı́tulo del problema y luego la descripción


del mismo es evidente que el ejercicio se soluciona con lo tratado en la sección
de ecuaciones diofánticas de este capı́tulo. Dada los coeficientes A,B y C de
una ecuación de la forma Ax+By=C debemos verificar si existe al menos
un par de enteros (x,y) que satisfazcan la ecuación. Su solución es bastante
simple solo debemos calcular el máximo común divisor entre los valores de
A y B. Si este valor calculado es divisor del valor C entonces se cumple que
existe al menos un par de enteros que satisfazcan la ecuación. Si el valor
calculado no es divisor de C entonces no existe ningún par de enteros que
satisfazcan la ecuación.

1132 - Divisor Sumation Te piden hallar la suma de todos los divisores


de N que sean menor que el, solo basta con hallar la suma aplicando la
fórmula explicada en el epı́grafe Factores Primos y restarle N pues la formula
lo incluye.

1151 - Coprimes II Para saber cuántos números son coprimos con N des-
de 1 hasta A descomponemos N en factores primos y aplicamos una fórmula
de inclusión y exclusiones con una máscara de bit para saber cuántos núme-
ros comparten algún primo y si es ası́ este no es coprimo ejemplo para N=10
y A = 15 los que no son coprimos con 10 = 2*5 son 15/2 + 15/5 – 15/10
= 7+3-1 = 9 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 quedándonos ası́ que la
cantidad de coprimos con 10 desde 1 hasta 15 son 15-9 = 6 para un intervalo
[A,B] seria IE(B)-IE(A-1) siendo IE el método de Inclusión y exclusiones.

1226 - Factoring large Numbers Ejemplo básico de descomponer un


número en factores primos cumpliendo con las especificaciones de salida.

1274 - Prime Factorization Otro ejemplo de descomponer pero en este


caso te piden N! con algunas especificaciones de salida.

1390 - Max Factor Dado una serie de números descomponerlos y decir


cuál de ellos tiene el primo más grande.

1996 - Big Divisor Otro ejemplo para trabajar con números descompues-
to en factores primos. En este caso dan un arreglo de multiplicaciones en el
denominador y otro en el denominador, descomponer cada uno de estos y los
exponentes del denominador restarlo al del numerador. Luego si el exponente
es positivo pertenece al numerador y si en negativo pertenece al denominador
y ası́ quedan ya simplificados.

239
TEORÍA DE NÚMERO

2562 – Brain in Action Dado N descomponerlo en factores primos e


imprimirlos con un formato de salida con parentesis.

1284 – Last Digit Solo hay que darse cuenta que lo que te piden es la
n!
permutación con repeticiones de un conjunto de letras n1!∗n2!∗n3!∗...∗nk! donde
nk es la cantidad de veces que se repite el elemento k. luego de descomponer
en factores primos n! le restamos los exponentes de los nk! Y sobre el número
descompuesto que nos queda realizamos el siguiente análisis: en la Base 10
que números nos da un 0 al final, la multiplicación de un 5 por un 2 miramos
en la descomposición del número 2x y 5y luego nos quedamos con el mayor
de los dos exponente si x¿y nos quedamos con 2x−y y si no con 5y−x ejemplo
si tenemos un número que en su descomposición tienes 28 y 53 que es por
ejemplo 32000 si realizamos este análisis nos queda 25 solamente que es 32
lo mismo que el caso anterior pero sin los ceros. Luego de aplicar este análi-
sis multiplicamos el primo de la descomposición por el exponente y vamos
modulando por 10 para ası́ quedarnos con el último digito.

2228 - Math Dare El problema nos pide hallar para un N un K tal que sea
el menor posible pero que cumpla la siguiente condición: que cada divisor de N
no sea primo relativo con K. Para empezar vamos a explicar que dos números
se dicen que son primos relativos entre si solo si el máximo común divisor
entre los números es 1. Para solucionar el problema solo basta con factorizar
en números primos N y multiplicar todos números primos presentes una sola
vez es decir si un número primo en la factorización su exponente es mayor
que uno solo se debe tener en cuenta una vez. Como todo los divisores de N
menos el 1 en sus factorizaciones van a tener presente alguna combinación
de los números primos presentes en la factorización de N el K a buscar debe
tener en su factorización presente con exponente uno cada uno de los primos
que dividen a N para que cada máximo común divisor de K con algún divisor
de N arroje un número primo o un valor que es la multiplicación de varios
de estos. Tener en cuenta que cuando N es primo K es igual a N.

1102 - You Can Say 11 El ejercicio nos pide determinar si para un núme-
ro N determinar si el mismo es múltiplo de 11 o no. El único detalle es que
dicho número puede tener hasta 1000 digitos por lo que no podemos alama-
cenar dicho valor en variables de tipo númericas. Para resolver el problema
vamos apoyarnos en la regla de divisibilidad del número 11 la cual plantea
que un número es divisible por 11 si la diferencia de las sumas de los digitos
en posiciones pares e impares producen un número multiplo de 11. Para al-
macenar el número vamos a utilizar una variable de tipo string y para hallar

240
TEORÍA DE NÚMERO

las sumas basta con recorrer el string e ir sumando en dos variables sumado-
ras los dı́gitos que están en las posiciones pares y los dı́gitos que están en la
posiciones impares.

241
TEORÍA DE NÚMERO

242
Capı́tulo 10

Geometrı́a

La geometrı́a es una rama de la matemática que se ocupa del estudio de


las propiedades de las figuras en el plano o el espacio, incluyendo: puntos,
rectas, planos, politopos (que incluyen paralelas, perpendiculares, curvas,
superficies, polı́gonos, poliedros, etc.).
Gran parte de los ejercicios que se enmarcan dentro de esta temática para
su solución basta con tener un conocimiento sólido de los contenidos impar-
tidos acerca de esta temática en ensenñanzas precedentes a la ensenñanza
superior.

10.1. Distancia mı́nima en el plano (Closest


Pair)
El problema de los puntos más cercanos (Closest Pair Problem) parte de
un conjunto de n puntos pertenecientes al plano XY, donde cada punto está
representado por sus coordenadas X y Y, se desea hallar encontrar el par de
puntos tal que la distancia entre ambos puntos es mı́nima. El algoritmo debe
devolver dichos puntos.

10.1.1. Primera variante de solución: Solución iterati-


va
La solución iterativa a este problema es simple, se recorre la colección de
puntos y por cada p punto se le compara contra todos los demás calculando
la distancia como (xi − xj )2 + (yi − yj )2 , cada vez que una distancia sea
inferior al mı́nimo anterior actualizamos cuales son los puntos más cercanos.
Cuando ya comparamos todos los puntos contra todos el resultado es el par
de puntos cuya distancia es seguro es mı́nima.

243
GEOMETRÍA

Figura 10.1: Closest Pair Problem

Es evidente que esta solución tanto en peor como en el mejor de los casos
compara todos los puntos contra todos por lo que podemos decir que la
complejidad de este algoritmo es O(n2 ) siendo n la cantidad de puntos en el
plano. Por lo que solo es recomendable usar esta variante en problemas de
concursos cuando la cantidad de puntos no superen los 1000.

#include <s t d i o . h>


#include <f l o a t . h>
#include < s t d l i b . h>
#include <math . h>
#include <a l g o r i t h m >

struct Po i nt
{
double X,Y;
};

double d i s t ( P oi nt p1 , P oi nt p2 )
{
return s q r t ( ( p1 . x − p2 . x ) ∗ ( p1 . x − p2 . x ) +(p1 . y − p2 . y )
∗ ( p1 . y − p2 . y ) ) ;
}

double c l o s e s t P a i r V 1 ( Po in t P [ ] , int n )
{
double minDistance = FLT MAX;
f o r ( int i = 0 ; i < n ; ++i )
f o r ( int j = i +1; j < n ; ++j )
i f ( d i s t (P [ i ] , P [ j ] ) < minDistance )
minDistance = d i s t (P [ i ] , P [ j ] ) ;
return minDistance ;
}

244
GEOMETRÍA

10.1.2. Segunda variante de solución: Divide y Con-


quista
Hasta este momento tenemos una solución al problema cuya complejidad
es O(n2 ), es por eso que en este apartado vamos a intentar aplicar conoci-
mientos en algoritmos de tipo ”Dividir para Conquistar”1 para resolver el
problema de forma más eficiente. Vamos aplicar las tres fases del paradigma:
dividir, conquistar y combinar.

Dividir: Partimos que tenemos los puntos almacenados en alguna es-


tructura de dato secuencial. Si son pocos puntos podemos aplicar la
primera variante. Si hay más puntos de lo permisible entonces traza-
mos una lı́nea vertical l que subdivide a la colección P de puntos en
dos colecciones aproximadamente del mismo tamaño, Pl y Pr.

Conquistar: Recursivamente aplicamos el procedimiento en ambas co-


lecciones por lo que obtenemos δl y δr distancias mı́nimas para ambas
colecciones. De ambas obtenemos δ = min(δl , δr).

Combinar: Lamentablemente δ no es el resultado final, ya que se


podrı́a darse la lı́nea l que hemos elegido pasa justo entre dos pun-
tos que están a distancia mı́nima. Debemos chequear si no hay dos
puntos que están a distancia mı́nima. Debemos chequear si no hay dos
puntos, uno a cada lado de la lı́nea, cuya distancia sea menor que δ. En
primer lugar observemos que no necesitamos chequear todos los puntos,
si un punto esta a mayor distancia de l que δ entonces es seguro no hay
vecino del otro lado de la lı́nea que pueda formar una distancia menor
que δ. Creamos una lista P’ de puntos que están a menos de δ de cada
lado de l. Determinamos entonces la distancia mı́nima para los puntos
de P’ que llamaremos δ 0 y devolvemos la distancia sino los dos puntos
que están a dicha distancia uno del otro.

El problema consiste en analizar cómo encontrar la distancia mı́nima en


P’, lo cual requiere un poco de astucia. Para optimizar el algoritmo queremos
encontrar la distancia mı́nima en P’ en tiempo O(n) vamos demostrar que
1
Los algoritmos de este tipo se caracterizan por estar diseñados siguiendo estrictamente
las siguientes fases:
Dividir: Se divide el problema en partes más pequeñas.
Conquistar: Se resuelven recursivamente los problemas mas chicos.
Combinar: Los problemas mas chicos se combinan para resolver el grande.
Los algoritmos que utilizan este principio son netamente recursivos.

245
GEOMETRÍA

esto puede hacerse suponiendo que los puntos de P’ están ordenados por la
coordenada y, luego tendremos que resolver como ordenar los puntos.

Supongamos que los puntos de P’ están ordenados de acuerdo a su coor-


denada y. Consideremos un punto P’[i] en lista ordenada. Cuál es el punto
más cercano a P’[i]. Podemos restringir la búsqueda a los puntos que tienen
ı́ndice j mayor que i ya que el proceso lo vamos a hacer para todos los puntos
(si el vecino más cercano está arriba, entonces esto ya fue analizado cuando
buscamos el vecino de dicho punto). Podrı́amos pensar que necesariamente el
vecino más cercano es P’[i+1], pero esto no es verdad, dos puntos que tienen
distancia mı́nima en cuanto a la coordenada y no necesariamente tienen dis-
tancia mı́nima en el plano, queremos saber que tan lejos tenemos que buscar
hasta P’[i+2]?, P’[i+8]? tal vez podamos acotar la búsqueda a una cantidad
constante de puntos.
Resulta que podemos limitar la cantidad de puntos a buscar y además
resulta que no nos hace falta analizar más de 7 puntos en la lista P’ para
cada punto.
Resumiendo el funcionamiento del algoritmo:

Presort: Dada la lista P, hacemos dos copias PX y PY. Ordenamos


PX por la coordenada x y la lista PY por las coordenadas y.
Parte Recursiva: DistMin(X,Y)
• bf Condición de corte: Si la cantidad de puntos es menor que 4
entonces resolvemos el problema por fuerza bruta y devolvernos
la distancia mı́nima δ analizando todas las distancias posibles.

246
GEOMETRÍA

• Dividir: Sino, sea l la mediana de las coordenadas x de PX. Divi-


dimos ambas listas PX y PY por esa lı́nea, manteniendo el orden,
creando Xl , Xr , Yl , Yr .
• Conquistar: δl = DistM in(Xl , Yl ) , δr = DistM in(Xr , Yr ).
• Combinar: δ = min(δl , δr) . Creamos la lista Y’ copiando todos
los puntos de Y que están a distancia menor que δ de l. Para
i desde 1 hasta la longitud de Y’ y para j desde i+1 hasta i+7
calcular la distancia entre Y’[i] y Y’[j]. Sea δ 0 la distancia mı́nima
entre dichas distancias. Devolver min(δ, δ 0 ).

#include <s t d i o . h>


#include <f l o a t . h>
#include < s t d l i b . h>
#include <math . h>
#include <a l g o r i t h m >

struct Po i nt
{
double X,Y;
};

int compareX ( const void ∗ a , const void ∗ b )


{
P oi nt ∗p1 = ( P oi nt ∗ ) a , ∗p2 = ( P oi nt ∗ ) b ;
return ( p1−>X − p2−>X) ;
}

int compareY ( const void ∗ a , const void ∗ b )


{
P oi nt ∗p1 = ( P oi nt ∗ ) a , ∗ p2 = ( P oi nt ∗ ) b ;
return ( p1−>Y − p2−>Y) ;
}

double d i s t ( P oi nt p1 , P oi nt p2 )
{
return s q r t ( ( p1 .X − p2 .X) ∗ ( p1 .X − p2 .X) +(p1 .Y − p2 .Y)
∗ ( p1 .Y − p2 .Y) ) ;
}

double c l o s e s t P a i r V 1 ( Po in t P [ ] , int n )
{
double minDistance = FLT MAX;
f o r ( int i = 0 ; i < n ; ++i )
f o r ( int j = i +1; j < n ; ++j )
i f ( d i s t (P [ i ] , P [ j ] ) < minDistance )

247
GEOMETRÍA

minDistance = d i s t (P [ i ] , P [ j ] ) ;
return minDistance ;
}

double s t r i p C l o s e s t ( P oi nt s t r i p [ ] , int s i z e , double d )


{
double minDistance = d ;
q s o r t ( s t r i p , s i z e , s i z e o f ( P oi nt ) , compareY ) ;
f o r ( int i = 0 ; i < s i z e ; ++i )
f o r ( int j = i +1; j < s i z e && ( s t r i p [ j ] . Y −
s t r i p [ i ] . Y) < minDistance ; ++j )
if ( dist ( strip [ i ] , strip [ j ]) <
minDistance )
minDistance = d i s t ( s t r i p [ i ] ,
strip [ j ]) ;
return minDistance ;
}

double c l o s e s t U t i l ( P oi nt P [ ] , int n )
{
i f ( n <= 3 )
return c l o s e s t P a i r V 1 (P , n ) ;

int mid = n / 2 ;
Po i nt midPoint = P [ mid ] ;
double d l = c l o s e s t U t i l (P , mid ) ;
double dr = c l o s e s t U t i l (P + mid , n−mid ) ;

double d = min ( dl , dr ) ;

Po i nt s t r i p [ n ] ;
int j = 0 ;
f o r ( int i = 0 ; i < n ; i ++)
i f ( abs (P [ i ] . X − midPoint .X) < d )
{
s t r i p [ j ] = P[ i ] ;
j ++;
}
return min ( d , s t r i p C l o s e s t ( s t r i p , j , d ) ) ;
}

double c l o s e s t P a i r V 2 ( Po in t P [ ] , int n )
{
q s o r t (P , n , s i z e o f ( P oi nt ) , compareX ) ;
return c l o s e s t U t i l (P , n ) ;
}

Debemos analizar cuanto tiempo tarda este algoritmo en su peor caso,


en el caso promedio en análisis es muy complejo porque depende de cuantos

248
GEOMETRÍA

puntos tiene P’ en promedio y esto depende en cual es la distancia mı́nima


esperada en cada sublista. En el peor de los casos podemos suponer que P’
tiene a todos los puntos. La fase pre-sort insume O(nlogn) igual valor arroja
la parte recursiva del algoritmo por lo que obtenemos un algoritmo O(nlogn)
mas eficiente que el algoritmo de fuerza bruta que era O(n2 ).

10.1.3. Tercera variante de solución: Algoritmo de lı́nea


de barrido
En la geometrı́a computacional, un algoritmo de lı́nea de barrido o ba-
rrido de plano es un paradigma algorı́tmico que utiliza una lı́nea de barrido
conceptual o una superficie de barrido para resolver diversos problemas en el
espacio euclidiano . Es una de las técnicas clave en geometrı́a computacional.
La idea detrás de los algoritmos de este tipo es imaginar que una lı́nea
(a menudo una lı́nea vertical) se barre o se mueve a través del plano, dete-
niéndose en algunos puntos. Las operaciones geométricas están restringidas
a objetos geométricos que se cruzan o se encuentran en las inmediaciones
de la lı́nea de barrido cada vez que se detiene, y la solución completa está
disponible una vez que la lı́nea ha pasado por todos los objetos.
Con ese algoritmo, barreremos el plano de izquierda a derecha (o de de-
recha a izquierda es una posibilidad) y cuándo se alcance alcance un punto
computaremos todos los puntos candidatos cercanos a este (los candidatos
que puede estar en el par más cercano).
Por lo que haremos las siguientes operaciones:

1. Ordenamos los puntos de izquierda a derecha por el eje x.

2. Por cada punto:

a) Quitamos de los candidatos todo el punto que está más allá en el


eje x de la distancia mı́nima actual.
b) Tomamos a todos los candidatos que son localizados a igual o
menor distancia que la distancia mı́nima del punto actual en eje
vertical.
c) Probamos para la distancia mı́nima todos los candidatos encon-
trados con el punto actual.
d ) Y finalmente le añadimos el punto actual a la lista de candidatos.

Ası́ es que cuando encontramos una distancia mı́nima nueva , podemos


hacer más pequeños el rectángulo de candidatos en el eje de las abscisas y

249
GEOMETRÍA

más pequeño en el eje vertical. Ası́ hacemos mucho menos comparaciones


entre los puntos.

A continuación una implementación de lo anterior expuesto:

struct Po i nt
{
double X,Y;
Po i nt ( double X=0,double Y=0)
{
X= X ;
Y= Y ;
}

bool operator <(const Po i nt &P) const


{
i f (X < P .X)
return true ;
i f (X > P .X)
return f a l s e ;
return Y < P .Y;
}

void r e a d P o i n t ( )
{
c i n >>X>>Y;
}
};

double d i s t a n c e P o i n t T o P o i n t ( Po in t a , Po in t b )
{
double d i s t =( a . X− b .X) ∗ ( a . X− b .X) +( a . Y− b .Y) ∗ ( a . Y− b
.Y) ;
return s q r t ( d i s t ) ;
}

double c l o s e s t P a i r W i t h L i n e S w e e p ( P oi nt p o i n t s [ ] , int n p o i n t )
{
s o r t ( p o i n t s +1, p o i n t s+ n p o i n t +1) ;
i f ( p o i n t s == 0 )
return 0 ;
double d i s t = INF ; /∗ INF debe s e r un v a l o r que s e a mayor
que l a d i s t a n c i a maxima
en que s e pueda e n c o n t r a r dos puntos segun e l problema .
S i d i c e n que l a s c o o r d e n a d a s de l o s puntos va e s t a r
e n t r e −10ˆ9 a 10ˆ9 e n t o n c e s :
INF= d i s t a n c e P o i n t T o P o i n t ( P oi nt ( −10ˆ9 , −10ˆ9) , P o in t
( 1 0 ˆ 9 , 1 0 ˆ 9 ) ) +100

250
GEOMETRÍA

∗/
int t B e g i n = 1 , tEnd = 1 , t o t = 1 ;

f o r ( int i =2; i<= n p o i n t ; i ++)


{
while ( t o t > 0 && 0.0+ p o i n t s [ i ] . X− p o i n t s [ t B e g i n
] .X > dist )
{
t B e g i n++;
t o t −−;
}

f o r ( int j=t B e g i n ; j<=tEnd ; j ++)


d i s t = min ( d i s t , d i s t a n c e P o i n t T o P o i n t (
points [ i ] , points [ j ]) ) ;
tEnd++;
t o t ++;
}

return d i s t ;
}

La complejidad de este algoritmo es O(nlogn).

10.1.4. Cuarta variante de solución: Algoritmos alea-


torios incrementales
Una sub-división de los algoritmos aleatorizados son los algoritmos incre-
mentales en este caso vamos presentar un algoritmo tipo Las Vegas incre-
mental para resolver el problema de los puntos mas cercanos en un plano. La
idea es la siguiente, los puntos van ser insertados uno a uno en un conjunto
inicialmente vacio y por cada punto que insertamos vamos a chequear si no
es necesario actualizar cual es la distancia minima.
El funcionamiento del algoritmo dependerá del orden en que se inserten
los puntos, algunos ordenamiento serán particularmente malos ya que nece-
sitaremos actualizar la distancia minima por cada punto insertado lo cual
nos insumirá tiempo de O(n2 ) . Sin embargo al insertar los puntos en orden
aleatorio el tiempo esperado del algoritmo sera de O(n). No lo vamos a de-
mostrar pero la probabilidad de que el algoritmo insuma mas de O(n log n)
es extremadamente chica.
Para simplificar asumamos que los puntos han sido normalizados para
pertenecer al cuadrado [0,1]. (Esta suposición es solo para facilitar la expli-
cación del algoritmo). En primer lugar permutamos en forma aleatoria los
puntos, y sea (p1 ,p2 ... ,pn ) la permutación resultantes. Inicialmente pone-

251
GEOMETRÍA

mos el primer punto en el conjunto y la distancia minima al haber solo un


punto en el conjunto es ∞. Uno por uno insertamos los p2 ,p3 , ect. Sea Pi−1 el
conjunto p1 , ... ,pi−1 ) y sea δ la distancia minima en Pi−1 . Sea δ’ la distancia
minia entre pi y cualquier punto de Pi−1 si δ 0 < δ, entonces actualizamos la
distancia.
La gran prengunta es como encontrar el punto mas cercano a pi , serı́a
demasiado leno cosiderar todos los puntos en Pi−1 . En primer lugar observe-
mos que solo necesitamos considerar puntos de Pi−1 que estan a distancia δ
de Pi , ya que ningún otro punto puede afectar la dupla mas cercana actual.
Declaramos que esto se puede hacer en tiempo constante si los pntos son
almacenados en una estructura apropiada.
La estructura de datos que vamos a utilizar para guardar el conjunto se
denomina grilla de buckets. En particular subdividimos el cuadrado uni-
tario[0,1] en una grilla de cuadraditos de lado δ. Dentro de cada cuadradito,
esta lista se denomina bucket.
Para determinar a que bucket corresponde un punto observemos que si
dividimos el intervalo [0,1] en subintervalos de longitud δ tendermos d =e1/δd
subintervalos. Si asumimos que indexamos los subintervalos de la forma 0,1,
...,d-1 para determinar el subintervalo que contiene un cierto valor x dividimos
x por δ y redondeamos hacia abajo el entero mas cercano es decir.
Iδ = bx/δc
Por ejemplo si δ=0.3 entonces los subintervalos son 0:[0,0.3], 1:[0.3,0.6],
2:[0.6,0.9], 3:[0.9,1]. El valor x=0.72 es mapeado al intervalo b0,72/0,3c = 2
Extendiendo esto a dos dimensiones mapeamos el puunto (x,y) al bucket
[Iδ (x), Iδ (y)]. Cuando el algoritmo comienza inicialmente δ = ∞ y existe un
único bucket.
Para almacenar los buckets necesitamos una hash table ya que no pode-
mos usar una matriz porque para δ muy chicos tendriamos mas entradas en
la matriz de las que podriamos manejar. Ese es un buen ejemplo de como
manejar el costo espacial de algoritmo. Aquellos que no conozcan como fun-
ciona una hash-table pueden despreocuparse y suponer que los buckets estan
almacenados en una matriz.
Un aspecto notable es que no podemos tener mas de 4 (cuatro) puntos
en cada bucket (uno en cada esquina del bucket) ya que de lo contrario la
distancia minima no serı́a δ sino que serı́a menor.
Dado el punto a inserat pi , para determinar elvecino mas cecano o bien
esta en el bucket correspondiente a pi o bien es uno de los 8 bucktes vecinos,
por lo tanto a lo sumo revisamos 9 buckets y cada uno de ellos tiene a lo
sumo 4 puntos, por lo que la cantidad máxima de puntos a revisar es 4*9=36
y es constante.
Si no hay un punto a distancia menor que δ entonces la distancia minima

252
GEOMETRÍA

permanece inalterada e insertamos el nuevo punto en su bucket correspon-


diente. Si la distancia minima es modificada debemos reconstruir la grilla
para la nueva distancia minima, esto insume O(i) tiempo, ya que cada pun-
to a ubicar insume una cantidad de tiempo constante. El algoritmo es el
siguiente.

1. Permutar aleatoriamente los puntos. Sea p1 ,p2 ... ,pn la permutación


resultante. Crear una grilla trivial de 1x1 e insertar p1 en la grilla. Sea
δ = ∞.

2. Para i desde 1 hasta n hacer:

(a) Sea pi =(Xi ,Yi ). Selecionar el bucket [Iδ (Xi ), Iδ (Yi )] y sus buckets
vecinos de la grilla.
(b) Si no ay puntos en ninguno de estos buckets entonces continuar
el ciclo. Sino computar la distancia minima y los (a lo suma 36)
puntos encontrados en los buckets. Sea δ 0 esta nueva distancia
(c) Si δ 0 < δ entonces δ = δ 0 destruir la grilla anterior y construir una
nueva grilla a partir de Pi = p1 , ..., pi . Sino simplemente agregar
pi a la grilla.

3. Devolver δ.

253
GEOMETRÍA

Aquı́ termina nuestro seguimiento del problema de la distancia mı́nima


en el plano, hemos visto como a medida que estudiábamos distintas técni-
cas para el diseño de algoritmos podı́amos resolver el problema en forma
más eficiente. Planteamos primero un algoritmo iterativo que usando fuer-
za bruta resolvı́a el problema en O(n2 ), luego mediante un algoritmo de tipo
“Divide & Conquer”obtuvimos O(n log n), por último mediante un algoritmo
aleatorizado resolvimos el problema en O(n).

10.2. Distancia esférica


Las personas usan latitudes (las lı́neas horizontales) y las longitudes (las
lı́neas verticales) en el sistema de coordenadas de la Tierra. La longitud
avanza en intervalos desde 0 grados (Greenwich) hasta +180 * hacia el este
y - 180 * hacia el oeste. La latitud avanza en intervalos desde 0 grados (el
Ecuador) para +90 * (hacia el Polo Norte) y - 90 * (hacia el Polo Sur).
La pregunta más interesante es: cual es la distancia / geográfica esférica
entre dos ciudades (p y q) en tierra con radio r, denotada de por (p lat,p long)
y (q lat,q long). Todas las coordenadas están en radianes. (o sea el rango del
- 180..180 del converso de longitud y - 90..90 se extienden de latitudes para
- pi..pi respectivamente.
Después de derivar las ecuaciones matemáticas. La respuesta es la siguien-
te:
spherical distance(p lat,p long,q lat,q long) = acos( sin(p lat) * sin(q lat)
+ cos(p lat) * cos(q lat) * cos(p long - q long) ) * r
Como cos(a-b) = cos(a)*cos(b) + sin(a)*sin(b), nosotros podemos sim-
plicar la fórmula anteriormente citada
spherical distance(p lat,p long,q lat,q long) = acos( sin(p lat) * sin(q lat)
+ cos(p lat) * cos(q lat) * cos(p long) * cos(q long) + cos(p lat) * cos(q lat)
* sin(p long) * sin(q long) ) * r

10.3. Cubiertas convexas


Intuitivamente, una cubierta convexa puede definirse como una banda
elástica que rodea una colección de puntos, la cual se ajusta exactamente al
contorno de los puntos (más adelante lo definiremos más formalmente)
Existen muchas razones por las cuales un cubierta convexa de un conjunto
de puntos es una estructura geométrica importante:

Es una de las aproximaciones de forma de un conjunto de puntos más


simples (otras incluyen rectángulos, cı́rculos, etc.)

254
GEOMETRÍA

Figura 10.2: Ejemplo de cubierta convexa

Puede ser usada para aproximar formas más complejas (cubiertas con-
vexas de polı́gonos o poliedros)

Algunos algoritmos calculan la cubierta convexa como una etapa inicial


(preprocesamiento) de su ejecución (filtrar puntos irrelevantes)

Por ejemplo, el diámetro de un conjunto de puntos es la máxima distancia


entre cualesquiera dos puntos del conjunto. Puede demostrarse que el par de
puntos que determina el diámetro son ambos vértices de la cubierta convexa.
También se puede observar que las mı́nimas formas convexas envolventes
(rectángulo, cı́rculo, etc.) depende sólo de los puntos de la cubierta convexa.
Convexidad: Un conjunto S es convexo si dados cualesquiera dos puntos
p, q ∈ S implican que el segmento de lı́nea pq ⊆ S
Combinación convexa: dado un conjunto finito de puntos p1 , p2 , ...
, pm una combinación convexa es cualquier punto que puede ser expresado
como una suma ponderada

donde 0 ≤ α i ≤ 1 y

Por lo tanto un segmento de lı́nea consiste de todas las combinaciones


convexas de sus puntos extremos
Cubierta convexa: dado un conjunto de puntos S su cubierta convexa
de es el conjunto de puntos que pueden ser expresados como combinaciones
convexas de los puntos en S. Una cubierta convexa de un conjunto de puntos
S también puede ser definida como la intersección de todos los conjuntos
convexos que contiene S. Intuitivamente, una cubierta convexa es el conjunto
convexo más pequeño que contiene S y se denota como conv(S).

255
GEOMETRÍA

El problema (planar) de la cubierta convexa se define de la siguiente


forma: Dado un conjunto P de n puntos en el plano, calcular la representación
del polı́gono convexo cerrado que representa la cubierta convexa de P. La
representación más simple de una cubierta convexa es la enumeración en
el sentido inverso a las manecillas del reloj de sus vértices. Idealmente la
cubierta convexa debe consistir sólo de los puntos extremos, en el sentido
que si tres puntos caen en un vértice de la frontera de la cubierta convexa,
entonces el punto medio no debe ser tomado en cuenta como parte de la
cubierta.

Esto no es un problema si asumimos que los puntos están en la posición


general, y en particular no hay tres colineales. Existe un algoritmo simple
para calcular la cubierta convexa en O(n3 ), que funciona considerando cada
par ordenado de puntos (p, q), y determinado si todos los puntos restantes del
conjunto caen dentro del semi-plano que cae a la derecha de lı́nea dirigida de p
a q. Observe que esto puede ser verificado usando la prueba de la orientación.
Se verifican n2 -n pares de puntos. Cada par se compara con otros n-2
puntos, lo que toma O(n3 ). El paso final toma O(n3 ). El tiempo total de
ejecución es O(n3 ), ¿Podrá hacerse más eficientemente?

10.3.1. Algoritmo Incremental


Supone el problema resuelto para tamaño n y en cada paso se añade
un nuevo punto para resolver n+1. Para añadir cada nuevo punto pi+1 basta
con lanzar tangentes (superior e inferior) desde dicho punto hasta el polı́gono
convexo obtenido en el paso anterior:

Siempre ocurre que el punto más bajo de tangencia pi es tal que q está
a la izquierda de pi−1 pi pero a la derecha de pi pi+1 .

El punto pj será el punto de mayor tangencia si q está a la derecha de


pj−1 pj pero a la izquierda de pj pj+1 .

256
GEOMETRÍA

Orden de ejecución:

La operación de ordenación con coste O(nlogn).

Cada uno de los n-3 puntos que añaden necesitan calcular las dos tan-
gentes, operación lineal en el peor de los casos.

Aunque se puede mejorar, en el peor de los casos tiene un tiempo de


O(n2 ).

10.3.2. La Caminata de Jarvis


El algoritmo de Caminata de Jarvis también es conocido como el método
de envoltura de regalo. Dado un conjunto S de n puntos en el plano, supon-
gamos que movemos una lı́nea L recta barriendo el plano hasta que L haga
contacto con un punto p1 de S. El punto p1 debe estar en la frontera de la
cubierta convexa de S dado que hasta ese momento, todos los puntos de S
están situados a un lado de la lı́nea L y p1 sobre la lı́nea.
A continuación la lı́nea L es girada sobre el punto p1 , en sentido contrario
a las manecillas del reloj, hasta que L haga contacto con otro punto p1 de
S. El segmento p1 p2 es entonces una arista de la cubierta convexa dado que
nuevamente todos los puntos de S están situados a un lado de la lı́nea L y el
segmento p1 p2 está sobre la lı́nea L.
Ahora L es girada sobre p2 , en sentido contrario a las manecillas del reloj,
hasta que L toque un tercer punto p3 de S. El segmento p2 p3 es la segunda
arista de la cubierta convexa. El proceso continua hasta llegar nuevamente
al punto p1 y de esta forma cerrar la cubierta convexa.
Este proceso puede ser visto como un método de envoltura. Supongamos
que fijamos el extremo de una cuerda en el punto p1 que se sabe es un
vértice de la cubierta. Entonces intentamos envolver los puntos con la cuerda.

257
GEOMETRÍA

Entonces intentamos envolver los puntos con la cuerda. La cuerda obviamente


representa la frontera de la cubierta convexa.
Veamos a detalle el proceso anterior. Supongamos un punto intermedio
de la ejecución del algoritmo en el cual se han encontrado los vértices de la
cubierta convexa p1 , p2 , ··· , pi ¿Qué punto será el próximo vértice de la
cubierta?. Obviamente, será el primer punto pi+1 tocado por la lı́nea, cuando
es girada sobre el puntopi . El ángulo 6 pi−1 pi pi+1 debe ser el más grande.
CaminataDeJarvis(S):

1. H = ∅ (lista de vértices de la cubierta convexa).

2. p1 ← el punto en S que tiene la coordenada y más pequeña.

3. p2 ← el punto en S tal que la pendiente de p1 − p2 es la más pequeña,


con respecto al eje x.

4. Agregar p1 y p2 a H.

5. i ← 2

6. mientras p1 6= pi :

pi+1 ← el punto en S tal que el 6 pi−1 pi pi+1 es el más grande.


i ← i +1
Agregar pi a H

7. Retornar H

258
GEOMETRÍA

Estudiemos ahora la complejidad de este algoritmo. Suponga que hay k


vértices en la cubierta convexa H de un conjunto de puntos S. Los puntos p1 ,
p2 son obviamente vértices de H. Además, es claro que encontrar los puntos p1
y p2 toma un tiempo O(n), asumiendo que |S| = n. Para encontrar el siguiente
vértice de la cubierta pi+1 , necesitamos verificar el ángulo 6 pi−1 pi pi+1 para
cada punto p en S. Este paso toma un tiempo O(n) en cada vértice de la
cubierta. Por lo tanto el algoritmo de Caminata de Jarvis corren en tiempo
O(kn). Notemos que si k es asintóticamente más pequeño que log n (o(log
n)) entonces este algoritmo es mejor que el de Graham, ya que correrı́a en
tiempo lineal. Sin embargo, si k es más grande, de manera que k = O(n),
entonces la complejidad temporal del algoritmo de Caminata de Jarvis es
O(n2 ).

10.3.3. Algoritmo QuickHull


Si el algoritmo Divide y Vencerás para ver determinar cubiertas convexas
puede ser visto como una generalización del MergeSort, podrı́amos pregun-
tarnos si existen las generalizaciones correspondientes a otros algoritmos de
ordenamiento para calcular cubiertas convexas. En particular, el siguiente
algoritmo que vamos a considerar puede ser visto como la generalización del
QuickSort. El algoritmo resultante es conocido como QuickHull.
Al igual que el QuickSort corre en tiempo O(n log n) para entradas favo-
rables pero puede tomar tiempo O(n2 ) con datos desfavorables. Sin embargo,
a diferencia del QuickSort, no hay una forma obvia de convertirlo en un algo-
ritmo aleatorizado con tiempo de ejecución esperado O(nlogn). No obstante,
QuickHull tiende a desempeñarse muy bien en la práctica.
La intuición indica que en muchas aplicaciones la mayorı́a de los puntos
caen en el interior de la cubierta. Por ejemplo, si los puntos están unifor-
memente distribuı́dos en un cuadrado unitario, entonces puede ser mostrado
que el número esperado de puntos en la cubierta convexa es O(log n).
La idea detrás del algoritmo QuickHull es descartar los puntos que no
están en la cubierta tan pronto como sea posible. QuickHull comienza por
calcular dos puntos extremos. Utilizaremos el punto x más a la derecha y más
abajo y el punto y más a la izquierda y más arriba. Claramente estos puntos
deben estar en la cubierta.
La cubierta convexa completa estará formada por una cubierta superior
sobre xy y una cubierta inferior bajo xy. QuickHull encuentra estas cubiertas
mediante un procedimiento que inicia con los puntos extremos (a, b). Con-
tinua encontrando un tercer punto extremo c estrictamente a la derecha de
ab. Elimina todos los puntos dentro del triángulo abc. Itera recursivamente
en (a, c) y (c, b).

259
GEOMETRÍA

Sea S el conjunto de puntos estrictamente a la derecha de ab (S podrı́a


estar vacı́o). La idea clave es que el punto c ∈ S que está más alejado de
ab debe estar en la cubierta convexa. De hecho es un punto extremo en la
dirección ortogonal a ab. Por lo tanto se pueden eliminar todos los puntos
sobre o dentro del triángulo abc (excepto por a, b y c). El resto de los puntos
es dividido en dos subconjuntos A y B:

A, contiene aquellos puntos que caen fuera (a la derecha) de ac.

B, incluye aquellos puntos que caen fuera (a la derecha) de cb.

Podemos clasificar cada punto p, calculando las orientaciones de las tri-


pletas acp y cbp. Se reemplaza la arista ab con ac y cb para continuar recur-
sivamente el procedimiento.

QuickHull(a, b, S):
1. Si S=∅ retornar ∅

2. Sino si S=a,b retornar (a,b) que es una arista de la cubierta

3. Sino

c ← ı́ndice del punto con máxima distancia de ab


A ← conjunto de puntos estrictamente a la derecha de (a, b)
B ← conjunto de puntos estrictamente a la derecha de (c, b)
retornar QuickHull(a, c, A) ∪ (c) ∪ QuickHull(c, b, B)

260
GEOMETRÍA

La complejidad de este algoritmo depende de la distribución de los puntos.


El mejor tiempo de ejecución para el algoritmo QuickHull es O(n log n)
(cuando los puntos están aleatoriamente distribuidos) en caso contrario la
complejidad puede ser de O(n2 ).

10.3.4. Algoritmo de Cubierta Convexa aplicando Técni-


ca de Divide y Vencerás
A continuación estudiaremos otro algoritmo de orden O(nlogn) el cual
está basado en la técnica de diseño conocida como Divide y Vencerás que
permite resolver el problema de construcción de la cubierta convexa para un
conjunto P de puntos en el plano.
Puede ser vista como una generalización del famoso algoritmo de ordena-
miento MergeSort. El funcionamiento del algoritmo es de la siguiente forma:

1. Ordenar los puntos en P de acuerdo a su coordenada x.

2. Particiona el conjunto de puntos P en dos conjuntos A y B, donde A


consiste de los dn/2e puntos con las coordenadas x máspequeñas (izq.)
y B los bn/2c puntos restantes (der.).

3. Calcula recursivamente HA = conv(A) y HB = conv(B)

El ordenamiento del primer paso garantiza que los conjuntos A y B estén


separados por una lı́nea vertical, lo cual asegura que A y B no se trasla-
pan.Esto simplifica el paso de combinación (paso 4). Los pasos 2, 3 y 4 se
repiten en cada nivel de recursión, deteniéndose cuando n ≤ 3. Si n = 3 la
cubierta es un triángulo (asumiendo que no hay 3 puntos colineales).
El tiempo de ejecución asintótico del algoritmo puede expresarse mediante
una relación de recurrencia. Dada una entrada de tamaño n, consideraremos

261
GEOMETRÍA

el tiempo necesario para realizar todos los pasos del algoritmo, ignorando las
llamadas recursivas Esto incluye el tiempo para:

1. Particionar el conjunto de puntos.

2. Calcular las dos tangentes

3. Regresar el resultado final

Claramente los pasos 2 y 3 pueden ser realizados en tiempo O(n), asu-


miendo que los vértices de la cubierta son representados con una lista ligada.
A continuación veremos como las tangentes pueden ser calculadas en tiem-
po O(n). Algo que simplifica el proceso de calcular las tangentes es que los dos
conjuntos A y B están separados por una lı́nea vertical. Esto asumiendo que
los puntos no tienen coordenadas x duplicadas. Vamos a concentrarnos en la
tangente inferior (la superior es simétrica). El algoritmo funciona mediante
un procedimiento simple de caminata. Inicializamos a como el punto más a
la derecha de Ha y b el más a la izquierda de Hb (esto puede ser encontrado
en tiempo lineal).

La tangencia inferior es una condición que puede ser verificada localmente


mediante una prueba de orientación involucrando los dos vértices y vértices
vecinos en la cubierta. Se iteran los siguientes dos ciclos, los cuales avanzan
a y b hasta que ellos alcanzan los puntos de tangencia inferior. La cual se
calcula de la siguiente manera

Sea a el punto más a la derecha de Ha .

Sea b el punto más a la izquierda de Hb .

Mientras ab no es una tangente inferior de Ha y Hb . ) hacer:

262
GEOMETRÍA

• Mientras ab no es una tangente inferior de Ha a igual a su prede-


cesor en la lista de los puntos que integran la cubierta en sentido
de las manecillas del reloj.
• Mientras ab no es una tangente inferior de Hb b igual a su suce-
sor en la lista de los puntos que integran la cubierta en sentido
contrario a las manecillas del reloj.

Regresar ab

La condición ab no es una tangente inferior de Ha puede ser implemen-


tada con la prueba de orientación orient(b, a, a.pred) ≤ 0. Esta prueba es
similar con Hb

10.3.5. Algoritmo de Graham o Exploración Graham


(Graham Scan)
Ahora presentaremos un algoritmo O(nlogn) para cubiertas convexas lla-
mado algoritmo de Graham o exploración de Graham. Este algoritmo data
de comienzos de los años 70. El algoritmo está basado en un enfoque de so-
lución común para construir estructuras geométricas llamado construcción
incremental. En la construcción incremental los objetos (puntos) se agregan
uno a la vez, y la estructura (cubierta convexa) se actualiza con cada nueva
inserción.
Un punto importante a considerar en los algoritmos incrementales es el
orden de la inserción. Si agregáramos puntos en algún orden arbitrario, ne-
cesitarı́amos un método para probar si el nuevo punto insertado está dentro
de la cubierta convexa. Simplificarı́a mucho las cosas el agregar los puntos
en algún orden apropiado, en nuestro caso, en orden incremental de la coor-
denada x. Esto garantiza que cada nuevo punto agregado está fuera de la
cubierta convexa.
En realidad el algoritmo de Graham original ordenaba los puntos en una
manera diferente. Buscaba el punto más bajo del conjunto de datos y des-
pués ordenaba los puntos cı́clicamente alrededor de este punto. Sin embargo,
ordenar de acuerdo a la coordenada x es más fácil de implementar.
Dado que estamos trabajando de izquierda a derecha, serı́a conveniente si
los vértices de la cubierta convexa también estuvieran ordenados de izquierda
a derecha. Las cubiertas convexas son conjuntos ordenados cı́clicos. Este tipo
de conjuntos son algo más complicados de trabajar que los conjuntos ordena-
dos linealmente, por esta razón dividiremos la cubierta convexa en dos, una
superior y una inferior.

263
GEOMETRÍA

Los puntos comunes p1 y pn a ambas cubiertas serán los vértices extremos


izquierdo y de derecho de la cubierta convexa. Después de construir ambas
cubiertas, éstas se pueden concatenar en una lista cı́clica de izquierda a dere-
cha. Como será común durante el cuatrimestre, haremos la suposición de que
los puntos están en la posición general. En este caso significa que ningún par
de puntos tienen la misma coordenada x y además que no hay tres puntos
colineales.
El algoritmo de Graham utiliza una pila (stack) H durante su ejecución.
En esta pila el tope o cima (top) corresponde al punto agregado más recien-
temente. Vamos a denotar H.first y H.second el tope y el segundo elemento
de la pila H respectivamente.
Observemos que mientras se leen los elementos de la pila del tope hacia
abajo (i.e. de derecha a izquierda) las tripletas consecutivas de puntos de la
cubierta superior harán un giro (estricto) hacia la izquierda. Es decir, tendrán
una orientación positiva.

El algoritmo de Graham trabaja de la siguiente forma:

Sea pi el próximo punto que se agregará al ordenamiento de izquierda


a derecha de los puntos.

264
GEOMETRÍA

Si la tripleta pi , H.first, H.second tiene orientación positiva, entonces


podemos simplemente agregar pi a la pila

Sino, se puede inferir que le punto medio de la tripleta H.first no puede


estar en la cubierta convexa

Por lo tanto lo borramos de la pila.

Ésto es repetido hasta alcanzar una tripleta con orientación positiva, o


haya menos de dos elementos en la pila

A continuación una implementación del algoritmo:

#include <s t d i o . h>


#include <math . h>
#include <s t r i n g . h>
#include <i o s t r e a m >
#include <v e c t o r >
#include < l i s t >
#include <s t r i n g >
#include <a l g o r i t h m >
#include <queue>
#include <s t a c k >
#include <s e t >
#include <map>
#include <complex>
#define MAX N 100001
#define INF 987654321
using namespace s t d ;
typedef long long l l d ;

struct Po i nt
{
double X, Y;
P oi nt ( )
{

265
GEOMETRÍA

this−>X = this−>Y = 0 ;
}
P oi nt ( double x , double y )
{
this−>X = x;
this−>Y = y;
}
};

int n ;
P oi nt P [MAX N ] ;
P oi nt R;

i n l i n e bool cmp( Po in t A, P oi nt B)
{
return atan2 (A. Y−R. Y,A. X−R.X) < atan2 (B . Y−R. Y, B . X−R.X) ;
}

i n l i n e double CCW( P o int a , Po in t b , Po in t c )


{
return ( ( b .X − a .X) ∗ ( c .Y − a .Y) − ( b .Y − a .Y) ∗ ( c .X − a .X)
);
}

i n l i n e int GrahamScan ( v e c t o r <Point> &cH )


{
int m i n i d = 1 ;
f o r ( int i =2; i<=n ; i ++){
i f (P [ i ] . Y < P [ m i n i d ] . Y | | (P [ i ] . Y == P [ m i n i d ] . Y && P [ i ] .
X < P [ m i n i d ] . X) )
{
min id = i ;
}
}

swap (P [ 1 ] , P [ m i n i d ] ) ;
R = P[1];
s o r t (P+2, P+n+1, cmp) ;

P[ 0 ] = P[ n ] ;
int H u l l S i z e = 1 ;

f o r ( int i =2; i<=n ; i ++)


{
while (CCW(P [ H u l l S i z e −1] , P [ H u l l S i z e ] , P [ i ] ) <= 0 )
{
i f ( H u l l S i z e > 1 ) H u l l S i z e −−;
e l s e i f ( i == n ) break ;
e l s e i ++;

266
GEOMETRÍA

}
swap (P[++ H u l l S i z e ] , P [ i ] ) ;
}

f o r ( int i =1; i<=H u l l S i z e ; i ++) cH . push back (P [ i ] ) ;


return H u l l S i z e ;
}

int main ( )
{
n = 4;

P[1] = Po in t ( 4 , 8) ;
P[2] = Po in t ( 4 , 12) ;
P[3] = Po in t ( 5 , 9.3) ;
P[4] = Po in t ( 7 , 8) ;

v e c t o r <Point> cH ;
int m = GrahamScan ( cH ) ;

p r i n t f ( "Hull size: %d\n" ,m) ;


f o r ( int i =0; i <m; i ++)
{
p r i n t f ( " %lf %lf\n" , cH [ i ] . X, cH [ i ] . Y) ;
}
return 0 ;
}

A continuación otra implementación del algoritmo.

using namespace s t d ;

struct pt {
double x , y ;
};

bool cmp ( pt a , pt b ) {
return a . x<b . x | | a . x==b . x && a . y<b . y ;
}

bool cw ( pt a , pt b , pt c ) {
return a . x ∗ ( b . y−c . y ) + b . x ∗ ( c . y−a . y ) + c . x ∗ ( a . y−b . y )
<0;
}

bool ccw ( pt a , pt b , pt c ) {
return a . x ∗ ( b . y−c . y ) + b . x ∗ ( c . y−a . y ) + c . x ∗ ( a . y−b . y )>
0;
}

267
GEOMETRÍA

void c o n v e x H u l l ( v e c t o r <pt> & a ) {


i f ( a . s i z e ( ) == 1 ) return ;
s o r t ( a . b e g i n ( ) , a . end ( ) , &cmp) ;
pt p1 = a [ 0 ] , p2 = a . back ( ) ;
v e c t o r <pt> up , down ;
up . push back ( p1 ) ;
down . push back ( p1 ) ;
f o r ( s i z e t i = 1 ; i <a . s i z e ( ) ; ++i ) {
i f ( i == a . s i z e ( ) −1 | | cw ( p1 , a [ i ] , p2 ) ) {
while ( up . s i z e ( ) >= 2 && ! cw ( up [ up . s i z e ( ) −2] , up [ up
. s i z e ( ) −1] , a [ i ] ) )
up . pop back ( ) ;
up . push back ( a [ i ] ) ;
}
i f ( i == a . s i z e ( ) −1 | | ccw ( p1 , a [ i ] , p2 ) ) {
while ( down . s i z e ( ) >= 2 && ! ccw ( down [
down . s i z e ( ) −2] , down [ down . s i z e ( )
−1] , a [ i ] ) )
down . pop back ( ) ;
down . push back ( a [ i ] ) ;
}
}
a . clear () ;
f o r ( s i z e t i = 0 ; i <up . s i z e ( ) ; ++ i )
a . push back ( up [ i ] ) ;
f o r ( s i z e t i = down . s i z e ( ) −2; i > 0;−− i )
a . push back ( down [ i ] ) ;
}

10.3.6. Convex hull (Andrew’s Monotone Chain)


El algoritmo de Andrew’s Chain Monotone es optimización del algoritmo
de Exploración Graham para determinar la cubierta convexa. Este caso se
halla de manera separada las cubiertas superior e inferior de la cubierta a
continuación una implementación de la misma.

#define Po i nt complex<double>

namespace s t d {
bool operator <(const Po i nt & a , const Po i nt & b ) {
return r e a l ( a ) != r e a l ( b ) ? r e a l ( a )<r e a l ( b ) : imag ( a )<imag ( b ) ;
}
}

double c r o s s ( const P oi nt & a , const P oi nt & b ) {


return imag ( c o n j ( a ) ∗ b ) ;

268
GEOMETRÍA

double dot ( const Po i nt & a , const Po i nt & b ) {


return r e a l ( c o n j ( a ) ∗ b ) ;
}

int ccw ( P oi nt a , Po in t b , Po in t c ) {
b −= a ; c −= a ;
i f ( c r o s s ( b , c )> 0 ) return +1; // counter clockwise
i f ( c r o s s ( b , c ) <0) return −1; // clockwise
i f ( dot ( b , c ) <0) return +2; // c − a − b on l i n e
i f ( norm ( b ) <norm ( c ) ) return −2; // a − b − c on l i n e
return 0 ;
}

v e c t o r <Point> c o n v e x H u l l ( v e c t o r <Point> ps ) {
int n = ps . s i z e ( ) , k = 0 ;
s o r t ( ps . b e g i n ( ) , ps . end ( ) ) ;
v e c t o r <Point> ch ( 2 ∗ n ) ;
f o r ( int i =0; i <n ; ch [ k++]=ps [ i ++]) // lower −h u l l
while ( k >= 2 && ccw ( ch [ k −2] , ch [ k −1] , ps [ i ] ) <=0) −−k ;
f o r ( int i = n−2, t = k + 1 ; i >=0; ch [ k++] = ps [ i −−]) //
upper−h u l l
while ( k >= t && ccw ( ch [ k −2] , ch [ k −1] , ps [ i ] )<= 0 ) −−k ;
ch . r e s i z e ( k−1) ;
return ch ;
}

Esta implementación asume que el vector que contiene todos los puntos
y se le pasa como parámetro a la función tiene al menos 3 puntos.

10.4. Intersección entre lı́neas y segmentos


Es posible que te encuentres problemas donde tengas que determinar si
una colección bien de lı́neas o de segmentos o la combinación de ambas existan
elementos que se intersectan entre sı́ como solución del problema o como parte
de la solución. Es por eso que el siguiente apartado va estar enfocado a como
determinar la intersección de estos elementos. Vamos a dividir el análisis en
sus tres posibles variantes:

Intersección de segmentos.

Intersección de lı́neas

Intersección entre lı́neas y segmentos

269
GEOMETRÍA

Antes de empezar es necesario definir bien estos elementos que pueden


traer confunsión.

Segemento: Un segmento, en geometrı́a, es un fragmento de recta que está


comprendido entre dos puntos, llamados puntos extremos o finales.

Lı́nea: Una lı́nea funciona como una sucesión continua de puntos trazados,
como por ejemplo un trazo o un guion. En geometrı́a euclidiana, la recta o
la lı́nea recta se extiende en una misma dirección por tanto tiene una sola
dimensión y contiene infinitos puntos; se puede considerar que está compuesta
de infinitos segmentos. Dicha recta también se puede describir como una
sucesión continua e indefinida de puntos extendidos en una sola dimensión,
es decir, no posee principio ni fin.
Ambos se pueden definir con dos puntos distintos entre sı́ con la diferencia
que el primero esta acotado al intervalo que define dichos puntos mientras la
segunda pueden extenderse mas alla del intervalo definido por los puntos.

10.4.1. Intersección de segmentos


Para analizar este caso vamos a partir que tenemos dos segmentos A y
C definidos por los puntos a y b el primero y c y d el segundo. Para que los
segmentos A y C se intersecten una de las siguientes condiciones tiene que
cumplirse o ser verdadera.

1. Los puntos a, b y c ser colineales y c estar entre a y b.

2. Los puntos a, b y d ser colineales y d estar entre a y b.

3. Los puntos c, d y a ser colineales y a estar entre c y d.

4. Los puntos c, d y b ser colineales y b estar entre c y d.

5. Los puntos c y d estar en lados opuestos con respecto al segmento A y


los puntos a y b estar en lados opuestos con respecto al segmento C.

Una vez definida las condiciones que harı́an que los segmentos A y C se
intersecten, nos podemos percatar que que para primera cuatro condiciones
son identicas solo cambian los puntos o su orden. Mientras en la quinta se
comprueba en un sentido y luego en el otro. Es por eso que vamos centrarnos
en como determinar:

Dados tres puntos saber son colineales.

270
GEOMETRÍA

Saber si un punto está entre otros dos.

Dado un punto saber de que lado esta con respecto a un segmento


definido por dos puntos.

Dados tres puntos saber son colineales . Dos puntos siempre van ser
colineales porque es la mı́nima cantidad de puntos para definir un lı́nea.
Ahora tres o más puntos van ser colineales si se es capaz de trazar una lı́nea
recta entre los dos puntos extremos y que pase por el resto de los puntos.
Para determinar si los puntos e, f y g son colineales vamos construir

− →

dos vectores F y G que tengan como punto de origen el punto e y como
de destino los puntos f y g respectivamente. Sin importar las posiciones de
los puntos el ángulo entre los dos vectores simepre va estar en el rango de
0 a 180 grados. Precisamente con esa amplitudes serı́a los casos en que los
puntos serı́an colineales. Bueno entonces queda ver como hallar el ángulo
entre dos vectores y luego ver si esa amplitud es 0 ó 180 entonces los puntos
son colineales.
Con la operación binaria producto vectorial o producto cruz entre dos
vectores es posible determinar el valor del seno del ángulo comprendido entre
los vectores segun la siguiente expresión:

− → − →
− →−
F x G = (| F || G | sin θ)n̂

− →

donde n̂ es el vector unitario y ortogonal a los vectores F y G y su
dirección está dada por la regla de la mano derecha y θ es, como antes, el
ángulo entre a y b. A la regla de la mano derecha se la llama a menudo
también regla del sacacorchos. El valor de θ para nuestro caso será 0 o 180
(0 o π) y en ambos caso la expresión sin θ se hace cero y esto produce que el
miembro derecho de la
expresión sea cero.

− → −
F xG = 0
Esto conduce a plantaer que tres puntos son colineales si el producto
vectorial o cruz entre dos de los vectores que se construir con ellos es igual
cero. Desarrollando el miembro izquierdo de la expresión:

((P ex − P fx ) ∗ (P gy − P fy ) − (P ey − P fy ) ∗ (P gx − P fx )) = 0

Saber si un punto está entre otros dos puntos Determinar si un punto


c esta entre los puntos a y b es bastante sencillo. Solo se debe cumplir las
siguientes condiciones.

min(ax ,bx ) <= cx

271
GEOMETRÍA

min(ay ,by ) <= cy

max(ax ,bx ) >= cx

max(ay ,by ) >= cy

Donde min y max son funciones que devuelven el mı́nimo y máximo res-
pectivamente entre los valores pasado por argumentos.

Dado un punto saber de que lado esta con respecto a un segmento


definido por dos puntos Como saber que posición ocupa el punto c o
de que lado está con respecto al segmento delimitado por los puntos a y
b. Anteriormente vimos como saber si tres puntos son colineales, pues bien
el análisis para resolver esta nueva problemática es identica a la de los tres
puntos colineales. Una vez hecho el mismo análisis nos podemos percatar que
cuando se cumpla la siguiente expresión:

− → −
F xG > 0
El punto se encuentra a la derecha del segmento.

Una vez resuelto todos estos detalles, estamos en condiciones de imple-


mentar un algoritmo que diga si dos segementos definidos por sus puntos se
intersectan o no. A continuación la implementación del mismo:

#include <s t d i o . h>


#include <math . h>
#include <i o s t r e a m >
#include <a l g o r i t h m >
#include <complex>
using namespace s t d ;
typedef long long l l d ;

struct Po i nt
{
double X, Y;
Po i nt ( double x , double y )
{
this−>X = x ;
this−>Y = y ;
}
};

i n l i n e double c r o s s P r o d u c t ( P oi nt a , Po in t b , P oi nt c )
{
return ( ( b .X − a .X) ∗ ( c .Y − a .Y) − ( b .Y − a .Y) ∗ ( c .X − a .X) )
;

272
GEOMETRÍA

i n l i n e bool i s L e f t ( P oi nt a , Po in t b , P o in t c )
{
return ( c r o s s P r o d u c t ( a , b , c ) > 0 ) ;
}

i n l i n e bool i s C o l l i n e a r ( P oi nt a , Po in t b , P oi nt c )
{
return ( c r o s s P r o d u c t ( a , b , c ) == 0 ) ;
}

i n l i n e bool o p p o s i t e S i d e s ( Po in t a , Po i nt b , Po in t c , Po in t d )
{
return ( i s L e f t ( a , b , c ) != i s L e f t ( a , b , d ) ) ;
}

i n l i n e bool i s B e t w e e n ( P oi nt a , Po in t b , Po in t c )
{
return ( min ( a . X, b .X) <= c .X && c .X <= max( a . X, b .X) && min ( a .
Y, b .Y) <= c .Y && c .Y <= max( a . Y, b .Y) ) ;
}

i n l i n e bool i n t e r s e c t ( P oi nt a , Po in t b , P oi nt c , P oi nt d )
{
i f ( i s C o l l i n e a r ( a , b , c ) && i s B e t w e e n ( a , b , c ) ) return true ;
i f ( i s C o l l i n e a r ( a , b , d ) && i s B e t w e e n ( a , b , d ) ) return true ;
i f ( i s C o l l i n e a r ( c , d , a ) && i s B e t w e e n ( c , d , a ) ) return true ;
i f ( i s C o l l i n e a r ( c , d , b ) && i s B e t w e e n ( c , d , b ) ) return true ;
return ( o p p o s i t e S i d e s ( a , b , c , d ) && o p p o s i t e S i d e s ( c , d , a ,
b) ) ;
}

int main ( )
{
P oi nt A( 0 . 0 , 0 . 0 ) , B( 0 . 0 , 2 . 0 ) , C( −1.0 , 2 . 0 ) , D( 1 . 0 , 2 . 0 ) ;
p r i n t f ( i n t e r s e c t (A, B, C,D) ? "YES" : "NO" ) ;
p r i n t f ( "\n" ) ;
return 0 ;
}

Como podemos la implementación cuenta con una estructura de tipo


Point que servirá para almacenar los puntos que definen los segmentos. Es-
te algoritmo como solo cuenta con operaciones elementales por lo que su
complejidad es O(1).

273
GEOMETRÍA

274
GEOMETRÍA

10.4.2. Intersección de lı́neas


10.4.3. Intersección entre lı́neas y segmentos

10.5. Distancia máxima en el plano (Farthest


Pair)
10.6. Basic elements of plane geometry
10.7. Traveling direction of the point
10.8. Distancia entre lı́neas y segmentos
10.9. End point
10.10. Perturbative deformation of a polygon
10.11. Point - polygon inclusion decision
10.12. Simple polygon triangulation (decom-
position ears)
10.13. The area of a polygon
10.14. Convexity determination
10.15. Convex polygon cutting
10.16. Convex polygon intersection
10.17. Endpoints of a convex polygon
10.18. Points - including a convex polygon
determined
10.19. The diameter of a convex polygon
275
10.20. Delaunay triangulation
10.21. Line arrangement
10.22. Visibility graph
GEOMETRÍA

que el volumen de la figura es igual a la siguiente fórmula:


2∗B∗H +2∗A∗H +2∗A∗B+A∗B∗H +π∗B+π∗A+π∗H +
((π ∗ 4,000)/3,000)

1853 - Determine the Number of Triangles Solo basta con contar


aquellas triadas de puntos que no sean colineales y que esten dentro de las
dimensiones de la matriz N.

2629 - Mondelo’s Coins Lo primero que debemos hacer es dibujar la


figura que nos plantea el problema la cual queda conformada como se muestra
en la siguiente figura:

Figura 10.3: 2629 - Mondelo’s Coins

Como se puede observar en la figura que los centros de las tres circun-
ferencia (A,B,C) son puntos periféricos de las otras dos circunferencias. Por
tanto las longitudes de AB, AC y BC son iguales al radio el cual como todos
conocemos casi siempre es la mitad del diámetro que es el único dato inicial
que da el problema. Por tanto AB es igual AC y BC por lo que el triángulo
ABC es equilátero. La solución del problema es X + Y donde X es área verde
mientras Y es igual al área amarrilla. Bien el área verde es igual al área de un
triangulo equilátero conocido su lado. Si tomamos la circunferencia de centro
C vemos que con los puntos A y B se forma un sector circular cuyo ángulo es
60(el ángulo del sector circular coincide con el equilátero), teniendo el radio
y el ángulo es fácil calcular el área del sector circular y a este resultado se le
resta el área del triángulo equilátero se tendrá un tercio del área amarrilla.
Solo tenemos que multiplicar ese resultado por tres para tener el área ama-
rilla. Una vez visto lo principales aspectos a tener en cuenta en la solución
y haciendo los despejes y agrupaciones pertinentes la solución queda en la
siguiente expresión:

276
GEOMETRÍA


√ radio∗radio∗(((1,00000/4,00000)∗ 3,0000)+(π/2,0000)−((3,00000/4,00000)∗
3,0000))

2157 - A Counting Problem I Para saber cuántos cuadrados se pueden


formar en una matriz de N por N desde 1 hasta N solo basta con resolver la
siguientePfórmula P
x = i ∗ i + j ∗ j ∗ (N − j)
Donde la primera sumatoria es la cantidad de cuadrado no girados es-
ta sumatoria va desde 1 hasta N mientras la segunda sumatoria me da la
cantidad de cuadrados con giros y j va desde 1 hasta N-1.

1436 - Tunnelling the Earth Para solucionar el problema es necesario


saber cómo calcular la distancia entre dos puntos en una esfera dadas las
latitudes y longitudes de esto, una vez hallado esa distancia que representa
un arco se puede hallar el ángulo asociado a ese arco si dicho ángulo es 180
la distancia recta es dos veces el radio, si es menor se puede hallar con el
triángulo que se forma.

3048 - Tangent Circumferences Si se une los centros de cada circunfe-


rencia se forma un triangulo equilátero de longitud 2R sus lados, para hallar
el área seleccionada basta hallar el área del triangulo equilátero que se forma
menos los 3 sectores circulares que contiene el triángulo.

Figura 10.4: 3048 - Tangent Circumferences

2876 - Shooting Competition Cuando se conforma la nueva diana a


donde deben disparar, la misma se puede descomponer en figuras básicas

277
GEOMETRÍA

quedando la solución del problema en la siguiente ecuación π ∗ (1 ∗ 1) + 2 ∗


B+2∗A+A∗B

Figura 10.5: 2876 - Shooting Competition

2737 - Coco-Bits’ Running Track Para resolver problema se necesita


determinar los puntos P1(intersección de las rectas v3 y r1), P2(intersección
de las rectas v3 y r2), P3 (intersección de las rectas v2 y r3), P4(intersección
de las rectas v1 y r2) y P5(intersección de las rectas v1 y r1), esto se determina
sabiendo como calcular el punto de intersección teniendo la ecuaciones de la
forma Ax + By + C = 0 de dichas rectas. La solución serı́a dist(P 1, P 2) +
dist(P 2, P 3) + dist(P 3, P 4) + dist(P 4, P 5) + dist(P 5, P 1) donde dist es la
distancia entre dos puntos en un espacio euclidiano.

Figura 10.6: 2737 - Coco-Bits’ Running Track

2922 - Euclid Para resolver este problema se debe seguir los siguientes
pasos:

278
GEOMETRÍA

1. Con la fórmula de Euclides determinar las distancias entre los puntos


A y B, E y D, E y F, D y E.

2. Con las distancias de ED, EF y DF se halla el área del triángulo EDF


con la fórmula de Herón.

3. Hallar la amplitud del ángulo CAB conocidas las coordenadas de los


puntos A,B y C (ángulo entre vectores o ángulo entre 3 puntos no
colineales)

4. Hallar la longitud del segmento AH conociendo los valores del ángulo


CAB, el la lı́nea AB y el área del paralegramo ABCH la cual es igual
al área del triángulo EDF por datos iniciales de problema.

5. Determino la relación de AH/AC=Z

6. H.x = Z ∗ (C.x − A.x) + A.x


H.y = Z ∗ (C.y − A.y) + A.y
~ = (H.x − A.x, H.y − A.y)
7. AH
~
8. G.x = AH.x + B.x
~
G.y = AH.y + B.y

2305 - Expanding Triangle Lo primero que debemos hacer es establecer


que el segmento AB es X, BC es Y y AC es Z. Una vez definido esto podemos
hallar el área del 4ABC aplicando la fórmula de herón. Con el área del
4ABC asi como las longitudes de cada uno de sus lados, es fácil hallar la
amplitud de cada uno de los ángulos de dicho triángulo. Las longitudes de los
segmentos QC, AR y BP son las mismas que las de los segmentos BC , AC y
AB respectivamente, por datos iniciales del problema. De lo anterior tambı́en
se determina la longitud de los segmentos BQ , AP y CR. Los 6 QCR, 6 QBP
y 6 RAP son ángulos adyacentes los 6 ACB, 6 CBA y 6 CAB respectivamente
por lo que también se pueden hallar sus respectivas amplitudes. Con todos
estos datos se puede hallar las áreas de los 4ABC, 4QBP ,4RAP y 4RCQ
cuyas áreas sumadas es el área del 4QRP .

2153 - Playing with Triangles El ejercicio se resuelve aplicando el al-


goritmo que dado una colección de puntos que conforman un polı́gono y un
punto determinar la posición de este con respecto al polı́gono. Siempre que
el algoritmo devuelva dentro o sobre el perı́metro la respuesta es 1 en caso
contrario es 0.

279
GEOMETRÍA

3679 - Are You Ok? La solución del ejercicio radica en dado dos colec-
ciones de puntos la primera conforma un polı́gono mientras de la segunda se
necesita saber cuantos de esos puntos estan dentro del polı́gono. Solo se ne-
cesita aplicar el algoritmo que determina la posición (dentro, en el perimetro
o fuera) de un punto con respecto a un polı́gono, cada punto de la segunda
colección.

3427 - Lost in the Forest Para la solucion de este problema solo se debe
recorrer la coleccion de puntos dados en el orden de entrada y analizar para
cada triada de puntos i-1, i y i+1 hacia donde abre el angulo que forman. En
caso de los puntos sean colineales no se debe imprimir nada.

MOG A clasificar Triángulos Un ejercicio muy sencillo que solo de-


bemos dados tres longitudes de segmentos decir si se puede o no formar
un triángulo. En caso afirmativo decir según dichas longitudes que tipo de
triángulo se forma.

3761 - Shortest Path Para explicar este problema primero vamos des-
glosar en la diferentes situaciones o casos en que se puede encontrar en este
problema para eso vamos a utilizar la siguiente figura:

Figura 10.7: 3761 - Shortest Path

En la misma hemos determinado los cuatro posibles casos de acuerdo a


la ruta optima (segmento de A a B en verde) y su posición con respecto a la
fuente (circunferencia de centro C), mientras el segmento h (color azul indica
la distancia mı́nima en el centro de la circunferencia y la ruta óptima entre A
y B) los cuales son perpendiculares entre sı́. Con color rojo se ha representado
la ruta óptima sin pasar por la circunferencia (cumpliendo conlas restricciones

280
GEOMETRÍA

). Dada la gráfica es evidente que el primer paso es determinar la longitud


del segmento h. Para eso debemos saber como hallar la distancia de un punto
a un segmento, en este caso el punto es C y el segmento esta definido por los
puntos A y B. Una vez hallado la longitud del segmento h es evidente que si su
valor es mayor o igual al radio de la circunferencia de centro C( significa que
el segmento AB es bien tangente o una recta exterior a la circunferencia) la
solución será la distancia euclidiana entre los puntos A y B. En caso contrario
de acuerdo al valor de h estaremos en el caso 1 (h igual 0) o en el caso 2(h
mayor que 0 pero menor que el radio de la circunferencia). Para el caso 1 la
solución es :
dist(A, B) + radius ∗ ((−2) + π)
donde dist es la distancia entre dos puntos en un espacio euclidiano.
Mientras la solución
p para el caso 2 será:
dist(A, B) − (2 ∗ (radio ∗ radio − h ∗ h)) + ((2 ∗ acos(h/radio)) ∗ radio)

3936 - Shaded Area Ejercicio muy sencillo, solo basta con plantear la
suma de las áreas del rectángulo y el triangulo, con las dimensiones descritas
en el problema.

3655 - Inside the Matrix El ejercicio se reduce a encontrar dada sus


posiciones en el plano el par de agentes más cercanos entre si y devolver esa
distancia. Se debe tener en cuanta que la respuesta debe ser truncada a la
parte entera.

3365 - Guarding Bananas El problema consiste en encontrar aquella


banana desde donde el mono pueda ver las demás banana girando la cabeza
con el menor ángulo posible. Una vez hecho el análisis gráfico de la situación
descrita es evidente que las bananas que estan en la periferia son las candi-
datas a elegir porque si escogemos una banana dentro de la distribución el
mono tendrı́a que girar más a priori que si fuera de la periferia. Entonces
el primero problema a resolver es identificar las bananas que conforman la
periferia. Unas vez detectadas las bananas de la periferia que buscar aquella
triada de bananas de la periferia que sea consecutivas cuyo ángulo entre ellas
sea menor. Para resolver el primer problema basta con realizar un convexHull
donde las bananas son los puntos. Una vez ejecutado el convexHull sobre la
colección de puntos que conforman la envoltura convexa es buscar la triada
de puntos pi−1 , pi , pi+1 cuyo angulo sea menor y esa será la respuesta. El otro
detalle es que la respuesta del problema debe darse con seis lugares despues
de la coma.

281
GEOMETRÍA

1845 - Ratio 3-4-5 Dada una circunferencia de radio R se divide en tres


arcos (a,b,c) cuyas longitudes son relaciones 3:4:5. En los puntos de división
se trazan rectas tangentes. Nos piden hallar el área del triangulo formado
por las tangentes. El primer paso para solucionar el problema es dibujar la
situación del problema para un mejor entendimiento, como se muestra en la
figura.

Figura 10.8: 1845 - Ratio 3-4-5

Esta claro que necesitamos determinar bien las longitudes de los lados
del triángulo o dos sus lados de este con la amplitud del ángulo comprendido
para determinar el área del triángulo.
Vamos a comenzar por calcular la longitud de los arcos a,b y c. Vamos
a definir la logitud de la circunferencia de centro O como L que va ser igual
como sabemos a 2πR. El problema nos plantea que los arcos tienen deter-
minada proporción con respecto a a la longitud de la circuferencia. Esto nos
permite plantaer lo siguiente:
a = 3x
b = 4x
c = 5x
a+b+c=L
3x + 4x + 5x = L
3x + 4x + 5x = 2πR
Donde x es el factor de proporción, despejando y calculando x en la ultima
ecuación planteada podemos determinar luego las longitudes de los arcos a,b

282
GEOMETRÍA

y c. Una vez calculada las longitudes de los arcos podemos determinar las
amplitudes de los ángulos corespondientes (Alfa, Beta y Sigma) a dichos
arcos.
Los ángulos 6 CDO, 6 ADO, 6 AF O, 6 BF O, 6 CEO y 6 AEO, son rectos
o con una amplitud de 900 grado por ser los segmentos AC , AB y BC
tangentes a la circuferencia en los puntos D , F y E respectivamente.
En un cuadrilatero los ángulos interiores suman 3600 grados. Conociendo
esto es posible calcular los valores de los ángulos 6 M iu, 6 Lambda y 6 DAF
por ser los cuartos ángulos de los cuadrilateros ADOF, BEOF y BEOF respec-
tivamente de los cuales ya se conocen la amplitud de los tres restantes ángulos
que lo componen. Una vez determinado la amplitud del ángulo 6 DAF ve-
mos que independientemente de la longitud del radio de la circuferencia las
tangentes van a conformar un triángulo rectángulo en el ángulo que se opone
al arco a.
Este dato es importante porque la suma de la hipotemusa y el dia-
metro de una circuferencia inscrito en el triángulo rectángulo es
igual a la suma de sus catetos. Dicho esto podemos plantaer que:
2R + h = e + d(1)
Donde h , e y d son las longitudes de los segmentos BC , AB y AC
respectivamente. De igual manera se puede plantear que:
d = sin(Lambda) ∗ h(2)
e = sin(M iu) ∗ h(3)
Sustituyendo (2) y (3) en (1) y despejando h queda que:
2R + h = sin(M iu) ∗ h + sin(Lambda) ∗ h
2R = sin(M iu) ∗ h + sin(Lambda) ∗ h − h
2R = h(sin(M iu) + sin(Lambda) − 1)
2R
(sin(M iu)+sin(Lambda)−1)
=h
Una vez hallado el valor de h se determina los valores de e y d. Luego con
esos datos es suficiente para hallar el área del 4ABC que era lo pedido en el
problema. Como sugerencia trabajen los ángulos en radianes. El otro detalle
del problema es que la solución debe ser impresa con cuatro lugares despues
de la coma.

1875 - Analysis & Deftness (A&D) El problema es bien sencillo. Nos


pide hallar el área de un hexágono regular del cual lo único se conoce es
el radio de la circunferencia circunscrita a dicho hexágono. Lo primero es
graficar la situación planteada como se ve en la figura:
Como es sabido un hexágono regular está formado por seis triángulos
equilateros cuya longitud de los lados coinciden con la longitud del radio
de la circuferencia circunscrita al hexágono regular. El área de un triángulo

283
GEOMETRÍA

Figura 10.9: 1875 - Analysis & Deftness (A&D)

equilatero es igual
√ a:
2 3
area = a 4
Y como el hexágono regular esta compuesto por seis triángulos equiláteros
el área del hexágono es igual a la ecuación anterior por seis. Simplificando
nos queda la siguiente
√ función solución:
2 3
area = 3a 2
El otro detalle del problema es que la solución debe imprimirse con dos
lugares decimales.

2337 - Six Distances 2D El problema nos pide dado un conjunto de


puntos de la dimensión 2D hallar la seis menores distancia entre todas la dis-
tancias de los puntos. Si analizamos es una variante del famoso problema de
vecinos más cercanos (Closet Pair ) con una pequeña variación. El algoritmo
devuelve bien el par de puntos o la menor distancia según sea el caso. Para
este problema podemos hacer una pequeña variante que en vez de almacenar
la menor distancia, almacenar las seis menores distancias que se hallen.

1554 - Convex Hull Finding Si leemos el tı́tulo del problema ası́ como
su descripción nos podemos percatar que ambos se ajustan y que es evidente
que el problema se resuelve aplicando el algoritmo para hallar la cubierta
convexa mı́nima que encierra a todos los puntos en una dimensión 2D. El
único detalle del ejercicio es la salida del mismo.

1127 - Intersection El problema nos pide dado un segemento definido por


sus dos puntos extremos y los puntos superior izquierdo y el inferior derecho
que definen un rectángulo determinar si dichas figuras se intersectan. Para
resolver este problema basta con chequear dos aspcetos. El primero que el
segmento se intersecte con al menos uno de los segmentos que conforman el

284
GEOMETRÍA

perı́metro del rectángulo. El segundo es verificar que el segmento este dentro


de los lı́mites del rectángulo. Si se cumple uno de estos dos elementos entonces
la respuesta es T sino la respuesta será F.

2336 - Six Distances 3D El problema nos pide dado un conjunto de


puntos de la dimensión 3D hallar la seis menores distancia entre todas la
distancias de los puntos. La solución de este problema es similar al problema
2337 - Six Distances 2D con la única variación es la dimensión de los
puntos pero el algoritmo solución es el mismo utilizando la técnica LineSweep.

DMOJ - Calculando areas Se tiene un cı́rculo inscrito en un cuadrado


del cual se conoce la longitud de su lado. Se quiere conocer el área encerrada
entre cı́rculo y el cuadrado tal y como se muestra en la figura:

Figura 10.10: DMOJ - Calculando areas

El área del cuadrado es L2 donde L es el lado del cuadrado el cual es


conocido. El área del cı́rculo es πr2 donde r = L/2 por ser un cı́rculo inscrito
al cuadrado por tanto π(L/2)2 = π(L2 /4). Una vez visto esto la solución al
problema es:

πL2
S = L2 −
4
π
S = L2 (1 −)
4
Tener en cuenta que se debe imprimir dos lugares decimales.

285
GEOMETRÍA

286
Capı́tulo 11

Programación dinámica

En informática, la programación dinámica es un método para reducir el


tiempo de ejecución de un algoritmo mediante la utilización de subproblemas
superpuestos y subestructuras óptimas. Una subestructura óptima significa
que se pueden usar soluciones óptimas de subproblemas para encontrar la
solución óptima del problema en su conjunto. Se pueden resolver problemas
con subestructuras óptimas siguiendo estos tres pasos:

1. Dividir el problema en subproblemas más pequeños.

2. Resolver estos problemas de manera óptima usando este proceso de tres


pasos recursivamente.

3. Usar estas soluciones óptimas para construir una solución óptima al


problema original.

Decir que un problema tiene subproblemas superpuestos es decir que se


usa un mismo subproblema para resolver diferentes problemas mayores. Por
ejemplo, en la Sucesión de Fibonacci (F3 = F1 + F2 y F4 = F2 + F3 ) calcular
cada término supone calcular F2 . Como para calcular F5 hacen falta tanto F3
como F4 , una mala implementación para calcular F5 acabará calculando F2
dos o más veces. Esto sucede siempre que haya subproblemas superpuestos:
una mala implementación puede acabar desperdiciando tiempo recalculando
las soluciones óptimas a subproblemas que ya han sido resueltos anterior-
mente.

11.1. Coin change


Cuando no dirigimos hacia un cajero automático y solicitamos un canti-
dad N el cajero puede que nos de dos variantes de respuesta, la primera es

287
PROGRAMACIÓN DINÁMICA

que no nos puede entregar la cantidad N solicitada por que no tiene como dar
esa cantidad bien sea porque con la denominaciones de los billetes con que
cuenta el cajero no puede conformar la cantidad requerida o N es tan grande
que con todo los billetes disponibles en el cajero no alcanza la cifra solicita-
da. La segunda es una cantidad de billetes que sumados dan la cantidad N
solicitada inicialmente.
La primera variante del coin change no servirá para determinar de cuantas
formas podemos devolver un valor N usando una serie de K denominaciones
de billetes. En otras palabras de cuanta formas podemos devolver 13 pesos
teniendo una cantidad infinita de billetes con los valores de 1, 3, 5, 10, 15.
Lo primeros que vamos a tener es una estructura lineal con una capacidad
máxima del máximo N que me pueden pedir que calcule en dicha estructura
voy almacenar en cada posición x la cantidad de maneras que se puede de-
volver ese valor x que indica posición. Esta estructura lineal sera una arreglo
que denominaremos way.
Inicialmente este arreglo sera rellenado con cero cada posición , excepto
la posición 0 la cual su valor será 1. Una vez planteado solo queda recorrer
cada posición para cada denominación de billetes y plantear los siguiente
way[j+monedas[i]]+=way[j] siempre y cuando j+monedas[i] sea menor que
N máximo. Si estoy en la posición 8 con el billete 5 significa que las formas
de pagar 13(8 + 5) va hacer la cantidad que este tenia mas la cantidad de
forma que yo puedo pagar 8 porque cada forma de 8 yo le sumo 5 y da 13.
El código quedarı́a de la siguiente manera:

long long nway [MAXTOTAL+ 1 ] ;


f i l l ( nway , nway+MAXTOTAL+1 ,0) ;
int c o i n [ 5 ] = { 5 0 , 2 5 , 1 0 , 5 , 1 } ;
int v = 5 ;
way [ 0 ] = 1 ;
f o r ( int i =0; i <v ; i ++)
{
c=c o i n [ i ] ;
f o r ( j=c ; j<=n ; j ++)
way [ j ]+=way [ j −c ] ;
}

De esta forma la cantidad de forma de devolver un valor N con k denomina-


ciones de billetes va estar en way[N].
La otra variante de esta técnica radica en determinar la mı́nima canti-
dad de billetes necesarios para devolver una cantidad N teniendo infinita
cantidad de billetes de k denominaciones.De similar manera tendremos un
arreglo que llenaremos inicialmente con valor bien grande. Luego iremos a
cada posición que represente una de las denominaciones de los billetes dis-

288
PROGRAMACIÓN DINÁMICA

ponibles y su valor será 1 (Algo lógico para devolver 5 pesos solo tengo que
usar un billete porque entre denominaciones esta el valor 5). Luego para ca-
da posición que se cumpla que way[i+denominacion[j]]>way[i]+1 actualizo
way[i+denominacion[j]]=way[i]+1 ejemplo si para devolver 13 tengo que usar
5 billetes pero si para 8 tengo que utilizar 2 y con un billete de 5 ahora podrı́a
usar solo 3 billetes para 13 (dos billetes para que sumen 8 y un billete de 5).
Quedando el código de la siguiente manera.

f o r ( int i =0; i <MAX; i ++)


{
c h a n g e s [ i ]= i ;
}
f o r ( int i =0; i <4; i ++)
c h a n g e s [ money [ i ] ] = 1 ;
f o r ( int i =1; i <MAX; i ++)
{
f o r ( int j =0; j <4; j ++)
{
i f ( i+money [ j ]<MAX && c h a n g e s [ i+money [ j ]] > c h a n g e s [ i ]+1)
c h a n g e s [ i+money [ j ] ] = c h a n g e s [ i ] + 1 ;
}
}

Quedando la solución para un valor N en changes[N]


La complejidad de ambos algoritmos es de O(N*K) donde N es el valor
máximo y K el número de denominaciones diferentes de billetes disponibles
tener en cuenta que solo puede aplicar cuando no nos ponen restricciones
en cuanto la cantidad de billetes por denominaciones además, el costo de
memoria puede ser peligroso mientras N sea mas grande, se recomienda solo
usar para N menores que 106 .

11.2. Edit distance


Sean u y v dos cadenas de caracteres. Se desea transformar u en v con
el mı́nimo número de operaciones básicas del tipo siguiente: eliminar un
carácter, añadir un carácter, y cambiar un carácter. Por ejemplo, podemos
pasar de abbac a abcbc en tres pasos:
abbac −→ abac (eliminamos b en la posición 3)
−→ ababc (añadimos b en la posición 4)
−→ abcbc (cambiamos a en la posición 3 por c)
Sin embargo, esta transformación no es óptima. Lo que queremos en este
caso es diseñar un algoritmo que calcule el número mı́nimo de operaciones,

289
PROGRAMACIÓN DINÁMICA

de esos tres tipos, necesarias para transformar u en v y cuáles son esas ope-
raciones, estudiando su complejidad en función de las longitudes de u y v.
En primer lugar, la transformación mostrada arriba no es óptima ya que
podemos pasar de abbac a abcbc en sólo dos pasos:
abbac −→ abcac (cambiamos b en la posición 3 por c)
−→ abcbc (cambiamos a en la posición 4 por c)
Llamaremos m a la longitud de la cadena u, n a la longitud de la ca-
dena v, y OB(m,n) indicará el número de operaciones básicas mı́nimo para
transformar una cadena u de longitud m en otra cadena v de longitud n.
Para resolver el problema utilizando Programación Dinámica es necesa-
rio plantearlo como una sucesión de decisiones que satisfaga el principio de
óptimo.
Para plantearla, vamos a fijarnos en el último elemento de cada una de las
cadenas. Si los dos son iguales, entonces tendremos que calcular el número
de operaciones básicas necesarias para obtener de la primera cadena menos
el último elemento, y la segunda cadena también sin el último elemento, es
decir,
OB(m, n) = OB(m − 1, n − 1) si um = vn Pero si los últimos elementos
fueran distintos habrı́a que escoger la situación más beneficiosa de entre tres
posibles: (i) considerar la primera cadena y la segunda pero sin el último
elemento, o bien (ii) la primera cadena menos el último elemento y la segunda
cadena, o bien (iii) las dos cadenas sin el último elemento. Esto da lugar a
la siguiente relación en recurrencia para OB(m,n) para este caso:
OB(m, n) = 1 + M in{OB(m, n − 1), OB(m − 1, n), OB(m − 1, n − 1)}
Donde:
m 6= 0, n 6= 0y um 6= vn
En cuanto a las condiciones iniciales, tenemos las tres siguientes:
OB(0, 0) = 0, OB(m, 0) = m y OB(0, n) = n
Una vez disponemos de la ecuación en recurrencia necesitamos resolverla
utilizando alguna estructura que nos permita reutilizar resultados interme-
dios, para esto utilizaremos una matriz con m filas y n columnas, a continua-
ción se muestra el código.

#include <a l g o r i t h m >


#include <math . h>
using namespace s t d ;

unsigned int e d i t d i s t a n c e ( s t r i n g s1 , s t r i n g s 2 )
{
const s i z e t l e n 1 = s 1 . s i z e ( ) , l e n 2 = s 2 . s i z e ( ) ;
v e c t o r < v e c t o r < unsigned int > > d ( l e n 1 + 1 , v e c t o r <
unsigned int > ( l e n 2 + 1 ) ) ;

290
PROGRAMACIÓN DINÁMICA

d [ 0 ] [ 0 ] = 0;

f o r ( unsigned int i =1; i<=l e n 1 ; ++i )


d[ i ][0]= i ;
f o r ( unsigned int i =1; i<=l e n 2 ; ++i )
d [ 0 ] [ i ]= i ;

f o r ( unsigned int i =1; i<=l e n 1 ; ++i )


f o r ( unsigned int j =1; j<=l e n 2 ; ++j )
d [ i ] [ j ] = min ( min ( d [ i − 1 ] [ j ]+1 , d [ i ] [ j −1]+1) , d [ i − 1 ] [ j
−1]+( s 1 [ i −1] == s 2 [ j −1] ? 0 : 1 ) ) ;
return d [ l e n 1 ] [ l e n 2 ] ;
}

Como el algoritmo se limita a dos bucles anidados que sólo incluyen ope-
raciones constantes la complejidad de este algoritmo es de orden O(mn).
El edit distance entre dos cadenas está definido como el número mı́nimo de
operaciones para convertir una cadena en otra con tres operaciones, inserción,
eliminación y reemplazo.
Note que d[i-1][j]+1 representa un costo de 1 para la inserción, d[i][j-1]+1
costo 1 para eliminación, y d[i-1][j-1]+(s1[i-1] == s2[j-1] 0 : 1)) representa
costo 1 para reemplazo (en caso de que no sean iguales). Con estas conside-
raciones es fácil adaptar este problema a otros similares.

11.3. Subsecuencia de longitud máxima común


(LCS)
Dada una secuencia X={ x1 , x2 , ..., xm }, decimos que Z={ z1 , z2 , ..., zk
} es una subsecuencia de X (siendo k ≤ m) si existe una secuencia creciente
{ i1 , i2 , ...,ik } de ı́ndices de X tales que para todo j = 1, 2, ..., k tenemos xij
= zj .
Dadas dos secuencias X e Y, decimos que Z es una subsecuencia común
de X e Y si es subsecuencia de X y subsecuencia de Y. Deseamos determinar
la subsecuencia de longitud máxima común a dos secuencias.
Llamaremos L(i,j) a la longitud de la secuencia común máxima (LCS) de
las secuencias Xi e Yj , siendo Xi el i-ésimo prefijo de X (esto es, X i = {x1
x2 ... xi }) e Yj el j-ésimo prefijo de Y, (Yj = {y1 y2 ... yj }).
Aplicando el principio de óptimo podemos plantear la solución como una
sucesión de decisiones en las que en cada paso determinaremos si un carácter
forma o no parte de la LCS. Escogiendo una estrategia hacia atrás, es decir,
comenzando por los últimos caracteres de las dos secuencias X e Y, la solución
viene dada por la siguiente relación en recurrencia:

291
PROGRAMACIÓN DINÁMICA

La solución recursiva resulta ser de orden exponencial, y por tanto Progra-


mación Dinámica va a construir una tabla con los valores L(i, j) para evitar la
repetición de cálculos. Para ilustrar la construcción de la tabla supondremos
que X e Y son las secuencias de valores:

La tabla que permite calcular la subsecuencia común máxima es:


Esta tabla se va construyendo por filas y rellenando de izquierda a dere-
cha. Como podemos ver en cada L[i,j] hay dos datos: uno el que corresponde
a la longitud de cada subsecuencia, y otro necesario para la construcción de
la subsecuencia óptima.
La solución a la subsecuencia común máxima de las secuencias X e Y
se encuentra en el extremo inferior derecho (L[9,8]) y por tanto su longitud
es seis. Si queremos obtener cuál es esa subsecuencia hemos de recorrer la
tabla (zona sombreada) a partir de esta posición siguiendo la información que
nos indica cómo obtener las longitudes óptimas a partir de su procedencia
(izquierda, diagonal o superior). El algoritmo para construir la tabla tiene una
complejidad de orden O(nm), siendo n y m las longitudes de las secuencias
XeY
Para encontrar cuál es esa subsecuencia óptima hacemos uso de la infor-
mación contenida en el campo procedencia de la tabla L, sabiendo que ‘I’(por
“Izq”) significa que la información la toma de la casilla de la izquierda, ‘S’
(“Sup”) de la casilla superior y de la misma manera ‘D’(“Diag”) corresponde
a la casilla que está en la diagonal. El algoritmo que recorre la tabla cons-
truyendo la solución a partir de esta información y de la secuencia Y. La
complejidad de este algoritmo es de orden O(n+m) ya que en cada paso de
la recursión puede ir disminuyendo o bien el parámetro i o bien j hasta al-
canzar la posición L[i,j] para i = 0 ó j = 0. A continuación se muestra un
ejemplo con ambas implementaciones.

292
PROGRAMACIÓN DINÁMICA

#include <s t d i o . h>


#include <math . h>
#include <s t r i n g . h>
#include <i o s t r e a m >
#include <v e c t o r >
#include < l i s t >
#include <s t r i n g >
#include <a l g o r i t h m >
#include <queue>
#include <s t a c k >
#include <s e t >
#include <map>
#include <complex>
#define MAX N 1001

293
PROGRAMACIÓN DINÁMICA

using namespace s t d ;
typedef long long l l d ;

int n , m;
s t r i n g A, B ;
int dp [MAX N ] [ MAX N ] ;

i n l i n e int LCS ( )
{
f o r ( int i =0; i<=n ; i ++) dp [ i ] [ 0 ] = 0 ;
f o r ( int j =0; j<=m; j ++) dp [ 0 ] [ j ] = 0 ;
f o r ( int i =1; i<=n ; i ++)
{
f o r ( int j =1; j<=m; j ++)
{
i f (A[ i −1] == B [ j −1])
{
dp [ i ] [ j ] = dp [ i − 1 ] [ j −1] + 1 ;
}
else
{
dp [ i ] [ j ] = max( dp [ i ] [ j −1] , dp [ i − 1 ] [ j ] ) ;
}
}
}
return dp [ n ] [m] ;
}

i n l i n e s t r i n g getLCS ( )
{
string ret ;
s t a c k <char> S ;
int i i = n , j j = m;
while ( i i != 0 && j j != 0 )
{
i f (A[ i i −1] == B [ j j −1])
{
S . push (A[ i i −1]) ;
i i −−; j j −−;
}
e l s e i f ( dp [ i i − 1 ] [ j j ] > dp [ i i ] [ j j −1])
i i −−;
else
j j −−;
}
while ( ! S . empty ( ) )
{
r e t += S . top ( ) ;

294
PROGRAMACIÓN DINÁMICA

S . pop ( ) ;
}
return r e t ;
}

int main ( )
{
n = 5 , m = 6;
A = " aleks " ;
B = " abcdef " ;
p r i n t f ( " %d\n" ,LCS ( ) ) ;
p r i n t f ( " %s\n" , getLCS ( ) . c s t r ( ) ) ;
return 0 ;
}

11.4. Ordenaciones de objetos entre dos re-


laciones
Dados n objetos, queremos calcular el número de ordenaciones posi-
bles según las relaciones “<”e “=”. Por ejemplo, dados tres objetos A,
B y C, el algoritmo debe determinar que existen 13 ordenaciones distin-
tas: A=B=C, A=B<C, A<B=C, A<B<C, A<C<B, A=C<B, B<A=C,
B<A<C, B<C<A, B=C<A, C<A=B, C<A<B y C<B<A.
Solución
Llamaremos Cn al número de ordenaciones posible con n objetos. Si 1 ≤
k ≤ n, podemos expresar Ck como:

(k)
Siendo Ij el número de formas posibles de poner k elementos en donde
hay j sı́mbolos “=”(es decir, k-j-1 sı́mbolos distintos), con 0 ≤ j <k, 1 ≤ k ≤
n. Vamos a tratar de expresar Ck en función de Ck−1 . Para ello, supongamos
que ya tenemos

y añadimos un nuevo elemento. Pueden ocurrir dos casos: que sea distinto
a todos los k–1 elementos anteriores, o bien que sea igual a uno de ellos.
Entonces, la expresión de Ck va a venir dada por:

295
PROGRAMACIÓN DINÁMICA

(k−1)
Con esto, tenemos Ck en función de los Ij , es decir, de los componentes
del caso anterior. Ahora bien, es posible también relacionar los I(k) con los
I(k−1) siguiente manera:
(k) (k−1)
I0 = kI0
(k) (k−1) (k−1)
I1 =(k-1) I1 +(k-1)I0
(k) (k−1) (k−1)
I2 =(k-2)I2 +(k-2)I1
...
(k) (k−1) (k−1)
Ik−2 = 2Ik−2 + 2Ik−3
(k) (k−1)
Ik−1 = Ik−2
(2) (2)
Cuyas condiciones iniciales son I0 =2 , I1 = 1 . Esto también puede
expresarse como sigue:
(j) (k−1) (k−1)
Ik =(k-j)(Ij +Ij−1 ) para 0 ≤ j ≤ k-1 y 2 ≤ k ≤n
(2)
I0 = 2
(2)
I1 = 1
(k)
I−1 = 0
(k)
Ik = 0
(j)
Ası́, el problema puede resolverse calculando cada I n (0 ≤ j ≤ n–1),
para finalmente calcular Cn mediante la expresión:

El algoritmo que implementa tal estrategia es el siguiente:

#define MAX 155


#define ULL unsigned long long

ULL I [MAX] ;

ULL s o l v e ( int n )
{
i f ( n<=1)
return n ;

f o r ( int i =0; i <n+10; i ++)

296
PROGRAMACIÓN DINÁMICA

I [ i ]=0;

I [0]=1;
ULL y , x=0;

f o r ( int k=2;k<=n ; k++)


{
f o r ( int j =0; j<=n−1; j ++)
{
i f ( j >1)
I [ j −2]=y ;
y=x ;
x=((k−j ) ∗ ( I [ j ]+ I [ j −1]) ) ;
}
I [ n−2]=y ;
I [ n−1]=x ;
}
ULL s =0;
f o r ( int j =0; j<=n−1; j ++)
s =( s+I [ j ] ) ;
return s ;
}

Respecto a su complejidad espacial, tan sólo utiliza el vector I por lo


que es de orden O(n). Y en cuanto a su complejidad temporal, el algoritmo
utiliza dos bucles anidados para el cálculo de los valores del vector, por lo
que podemos afirmar que su orden es O(n2 ).

11.5. Mochila (0,1) (Knapsack 0,1)


Dados n elementos e1 , e2 , ..., en con pesos p1 , p2 , ..., pn y beneficios
b1 , b2 , ..., bn , y dada una mochila capaz de albergar hasta un máximo de
peso M (capacidad de la mochila), queremos encontrar las proporciones de
los n elementos x1 , x2 , ..., xn (0 ≤ xi ≤ 1) que tenemos que introducir en
la mochila de forma que la suma de los beneficios de los elementos escogidos
sea máxima. Esto es, hay que encontrar valores (x1 , x2 , ..., xn ) de forma
que se maximice la: Σni=1 bi ∗ xi , sujeta a la restricción Σni=1 pi ∗ xi ≤ M .
Para encontrar un algoritmo de Programación Dinámica que lo resuelva,
primero hemos de plantear el problema como una secuencia de decisiones
que verifique el principio de óptimo. De aquı́ seremos capaces de deducir
una expresión recursiva de la solución. Por último habrá que encontrar una
estructura de datos adecuada que permita la reutilización de los cálculos de
la ecuación en recurrencia, consiguiendo una complejidad mejor que la del
algoritmo puramente recursivo.

297
PROGRAMACIÓN DINÁMICA

Siendo M la capacidad de la mochila y disponiendo de n elementos, lla-


maremos V(i,p) al valor máximo de la mochila con capacidad p cuando con-
sideramos i objetos, con 0 ≤ p ≤ M y 1 ≤ i ≤ n. La solución viene dada
por el valor de V(n,M). Denominaremos d1 , d2 , ..., dn a la secuencia de
decisiones que conducen a obtener V(n,M), donde cada di podrá tomar uno
de los valores 1 ó 0, dependiendo si se introduce o no el i-ésimo elemento.
Podemos tener por tanto dos situaciones distintas:

Que dn = 1. La subsecuencia de decisiones d1 , d2 , ..., dn−1 ha de ser


también óptima para el problema V(n–1,M–pn ), ya que si no lo fuera
y existiera otra subsecuencia e1 , e2 , ..., en−1 óptima, la secuencia e1 ,
e2 , ..., en−1 , dn también serı́a óptima para el problema V(n,M) lo que
contradice la hipótesis.

Que dn = 0. Entonces la subsecuencia decisiones d1 , d2 , ..., dn−1 ha


de ser también óptima para el problema V(n–1,M).

Podemos aplicar por tanto el principio de óptimo para formular la relación


en recurrencia. Teniendo en cuenta que en la mochila no puede introducirse
una fracción del elemento sino que el elemento i se introduce o no se introduce,
en una situación cualquiera V(i,p) tomará el valor mayor entre V(i–1,p), que
indica que el elemento i no se introduce, y V(i–1,p–p i )+bi , que es el
resultado de introducirlo y de ahı́ que la capacidad ha de disminuir en pi y el
valor aumentar en bi , y por tanto podemos plantear la solución al problema
mediante la siguiente ecuación:

La complejidad del algoritmo viene determinada por la construcción de


una tabla de dimensiones n*M y por tanto su tiempo de ejecución es de orden
de complejidad O(nM).
Si además del valor de la solución óptima se desea conocer los elementos
que son introducidos, es decir, la composición de la mochila, es necesario
añadir al algoritmo la construcción de una tabla de valores lógicos que indique
para cada valor E[i,j] si el elemento i forma parte de la solución para la
capacidad j o no. A continuación la implementación del algoritmo en Java.

298
PROGRAMACIÓN DINÁMICA

public c l a s s Knapsack
{
public s t a t i c void main ( S t r i n g [ ] a r g s )
{
int N = I n t e g e r . p a r s e I n t ( a r g s [ 0 ] ) ; // number o f i t e m s
int W = I n t e g e r . p a r s e I n t ( a r g s [ 1 ] ) ; // maximum w e i g h t o f
knapsack
int [ ] p r o f i t = new int [N + 1 ] ;
int [ ] w e i g h t = new int [N + 1 ] ;
// g e n e r a t e random i n s t a n c e , i t e m s 1 . . N
f o r ( int n = 1 ; n <= N; n++)
{
p r o f i t [ n ] = ( int ) ( Math . random ( ) ∗ 1 0 0 0 ) ;
w e i g h t [ n ] = ( int ) ( Math . random ( ) ∗ W) ;
}
// opt [ n ] [ w ] = max p r o f i t o f
// p a c k i n g i t e m s 1 . . n with w e i g h t l i m i t w

// s o l [ n ] [ w ] = d o e s opt s o l u t i o n t o pack
// i t e m s 1 . . n with w e i g h t

// l i m i t w i n c l u d e item n?

int [ ] [ ] opt = new int [N + 1 ] [W + 1 ] ;


boolean [ ] [ ] s o l = new boolean [N + 1 ] [W + 1 ] ;
f o r ( int n = 1 ; n <= N; n++)
{
f o r ( int w = 1 ; w <= W; w++)
{
// do not t a k e item n
int o p t i o n 1 = opt [ n − 1 ] [ w ] ;
// t a k e item n
int o p t i o n 2 = I n t e g e r . MIN VALUE ;
i f ( w e i g h t [ n ] <= w)
o p t i o n 2 = p r o f i t [ n ] + opt [ n − 1 ] [ w − w e i g h t [ n ] ] ;
// s e l e c t b e t t e r o f two o p t i o n s
opt [ n ] [ w ] = Math . max( o p t i o n 1 , o p t i o n 2 ) ;
s o l [ n ] [ w] = ( option2 > option1 ) ;
}
}
// d e t e r m i n e which i t e m s t o t a k e
boolean [ ] t a k e = new boolean [N + 1 ] ;
f o r ( int n = N, w = W; n > 0 ; n−−)
{
i f ( s o l [ n ] [ w] )
{
t a k e [ n ] = true ;
w = w − weight [ n ] ;

299
PROGRAMACIÓN DINÁMICA

}
else
{
take [ n ] = false ;
}
}
// p r i n t r e s u l t s
System . out . p r i n t l n ( "item" + "\t" + " profit " + "\t" + " weight
" + "\t" + "take" ) ;
f o r ( int n = 1 ; n <= N; n++)
{
System . out . p r i n t l n ( n + "\t" + p r o f i t [ n ] + "\t" + w e i g h t [ n
] + "\t" + t a k e [ n ] ) ;
}
}
}

El siguiente código en C++ tambı́en es una variante de este algoritmo y


presenta la misma complejidad que el algoritmo descripto anteriormente.

#include <s t d i o . h>


#include <math . h>
#include <s t r i n g . h>
#include <i o s t r e a m >
#include <v e c t o r >
#include < l i s t >
#include <s t r i n g >
#include <a l g o r i t h m >
#include <queue>
#include <s t a c k >
#include <s e t >
#include <map>
#include <complex>
using namespace s t d ;
typedef long long l l d ;

int n , c a p a c i t y ;
int Weight [ 1 0 1 ] , Value [ 1 0 1 ] , S o l [ 1 0 0 1 ] ;

i n l i n e int Knapsack01 ( )
{
f o r ( int i =0; i<=c a p a c i t y ; i ++) S o l [ i ] = 0 ;
f o r ( int i =0; i <n ; i ++)
{
f o r ( int j=c a p a c i t y ; j >=1; j −−)
{
i f ( Weight [ i ] <= j )
{
int x = S o l [ j ] ;

300
PROGRAMACIÓN DINÁMICA

int y = S o l [ j −Weight [ i ] ] + Value [ i ] ;


S o l [ j ] = max( x , y ) ;
}
}
}
return S o l [ c a p a c i t y ] ;
}

int main ( )
{
n = 4 , capacity = 6;
Weight [ 0 ] = 1 , Value [ 0 ] = 4 ;
Weight [ 1 ] = 2 , Value [ 1 ] = 6 ;
Weight [ 2 ] = 3 , Value [ 2 ] = 1 2 ;
Weight [ 3 ] = 2 , Value [ 3 ] = 7 ;
p r i n t f ( " %d\n" , Knapsack01 ( ) ) ;
return 0 ;
}

11.6. Mochila (0,1) con múltiples elementos


Este problema se basa en el de la Mochila (0,1) pero en vez de existir n
objetos distintos, de lo que disponemos es de n tipos de objetos distintos.
Con esto, de un objeto cualquiera podemos escoger tantas unidades como
deseemos.
Este problema se puede formular también como una modificación al pro-
blema de la Mochila (0,1), en donde sustituimos el requerimiento de que xi =0
óxi =1, por el que xi sean números naturales. Como en el problema original,
deseamos maximizar la suma de los beneficios de los elementos introducidos,
sujeta a la restricción de que éstos no superen la capacidad de la mochila.
Para encontrar un algoritmo de Programación Dinámica que resuelva el
problema, primero hemos de plantearlo como una secuencia de decisiones
que verifiquen el principio del óptimo. De aquı́ seremos capaces de deducir
una expresión recursiva de la solución. Por último habrá que encontrar una
estructura de datos adecuada que permita la reutilización de los cálculos de
la ecuación en recurrencia, consiguiendo una complejidad mejor que la del
algoritmo puramente recursivo.
Con esto en mente, llamaremos V(i,p) al valor máximo de una mochila
de capacidad p y con i tipos de objetos. Iremos decidiendo en cada paso si
introducimos o no un objeto de tipo i. Por consiguiente, para calcular V(i,p)
existen dos opciones en cada paso:

No introducir ninguna unidad del tipo i, con lo cual el valor de la

301
PROGRAMACIÓN DINÁMICA

mochila V(i,p) es el calculado para V(i–1,p).

Introducir una unidad más del objeto i lo cual indica que el valor de
V(i,p) será el resultado obtenido para V(i,p–pi ) más el valor del objeto
vi , con lo cual se verifica que V(i,p) = V(i,p–pi ) + bi .

Esto nos permite establecer la siguiente relación en recurrencia para


V(i,p):

Utilizaremos un arreglo lineal con una capacidad de nxM para almace-


nar los valores de V que vayamos obteniendo y ası́ no repetir cálculos. El
algoritmo que resuelve el problema es el siguiente:

#include <s t d i o . h>


#include <math . h>
#include <s t r i n g . h>
#include <i o s t r e a m >
#include <v e c t o r >
#include < l i s t >
#include <s t r i n g >
#include <a l g o r i t h m >
#include <queue>
#include <s t a c k >
#include <s e t >
#include <map>
#include <complex>
using namespace s t d ;
typedef long long l l d ;

int n , c a p a c i t y ;
int Weight [ 1 0 1 ] , Value [ 1 0 1 ] , S o l [ 1 0 0 1 ] ;

i n l i n e int Knapsack ( )
{
Sol [ 0 ] = 0;
f o r ( int i =1; i<=c a p a c i t y ; i ++)
{
int maks = 0 ;

302
PROGRAMACIÓN DINÁMICA

int t e k ;
f o r ( int j =0; j <n ; j ++)
{
i f ( Weight [ j ] <= i )
{
t e k = Value [ j ] + S o l [ i −Weight [ j ] ] ;
i f ( t e k > maks ) maks = t e k ;
}
}
S o l [ i ] = maks ;
}
return S o l [ c a p a c i t y ] ;
}

int main ( )
{
n = 5 , capacity = 17;
Weight [ 0 ] = 3 , Value [ 0 ] = 4 ;
Weight [ 1 ] = 4 , Value [ 1 ] = 5 ;
Weight [ 2 ] = 7 , Value [ 2 ] = 1 0 ;
Weight [ 3 ] = 8 , Value [ 3 ] = 1 1 ;
Weight [ 4 ] = 9 , Value [ 4 ] = 1 3 ;
p r i n t f ( " %d\n" , Knapsack ( ) ) ;
return 0 ;
}

La complejidad del algoritmo es la que corresponde a la construcción del


arreglo, es decir O(n*M).

11.7. Multiplicación óptima de matrices


Necesitamos calcular la matriz producto M de n matrices dadas M =
M1 , M2 ...Mn minimizando el número total de multiplicaciones escalares a
realizar.
En primer lugar, vamos a suponer que cada Mi es de dimensión di–1 xdi
(1 ≤ i ≤ n), y por tanto realizar la multiplicación Mi Mi+1 va a requerir
un total de di–1 di di+1 operaciones. Llamaremos M(i,j) al número mı́nimo de
multiplicaciones escalares necesarias para el cómputo del producto de Mi Mi+1
... Mj con 1 ≤ i ≤ j ≤ n. Por consiguiente, la solución al problema planteado
coincidirá con M(1,n).
Para plantear la ecuación en recurrencia que define la solución, suponga-
mos que asociamos las matrices de la siguiente manera:
(Mi Mi+1 ... Mk ) (Mk+1 Mk+2 ... Mj ).
Aplicando el principio de óptimo, el valor de M(i,j) será la suma del núme-
ro de multiplicaciones escalares necesarias para calcular el producto de las

303
PROGRAMACIÓN DINÁMICA

matrices Mi Mi+1 ...Mk , que corresponde a M(i,k), más el número de multi-


plicaciones escalares para el producto de las matrices Mk+1 Mk+2 ...Mj que es
M(k+1,j), más el producto que corresponde a la última multiplicación entre
las matrices de dimensiones (di–1 dk ) y (dk dj ) es decir, di–1 dk dj . En conse-
cuencia, el valor de M(i,j) para la asociación anteriormente expuesta viene
dado por la expresión:
M(i,j) = M(i,k) + M(k+1,j) + d i–1 d k d j .
Pero k puede tomar cualquier valor entre i y j–1, y por tanto M(i,j) deberá
escoger el más favorable de entre todos ellos, es decir:
M(i,j) = Mini≤k≤j M(i,k)+M(k+1,j)+d i–1 d k d j ,
lo que nos lleva a la siguiente relación en recurrencia:

Para resolver tal ecuación en un tiempo de complejidad polinómico es


necesario crear una tabla en la que se vayan almacenando los valores M(i,j)
(1 ≤ i ≤ j ≤ n) y que permita a partir de las condiciones iniciales reutilizar
los valores calculados en los pasos anteriores.
Esta tabla se irá rellenando por diagonales sabiendo que los elementos
de la diagonal principal son todos cero. Cada elemento M[i,j] será el valor
mı́nimo de entre todos los pares (M[i,k] + M[k+1,j]) señalados con la lı́nea
de doble flecha en la siguiente figura, más la aportación correspondiente a
la última multiplicación (di–1 d k d j ). Los valores que requiere el cálculo de
M[i,j] y que el algoritmo reutiliza para conseguir una tiempo de ejecución
aceptable se encuentran sombreados:

Rellenada la tabla, la solución la podemos encontrar en el extremo supe-


rior derecho, que nos indica el número de multiplicaciones escalares buscado,
M[1,n].

304
PROGRAMACIÓN DINÁMICA

El siguiente algoritmo (Pascal) Matriz es de creación de las tablas M


y Factor. En la tabla M se almacenan los valores del número mı́nimo de
multiplicaciones y en la tabla Factor la información necesaria para construir
la asociación óptima.

TYPE MATRIZ = ARRAY [ 1 . . n ] , [ 1 . . n ] OF CARDINAL;


ORDEN = ARRAY [ 0 . . n ] OF CARDINAL; ( ∗ d i m e n s i o n e s ∗ )
PROCEDURE Matriz (VAR d :ORDEN; n :CARDINAL;VAR M, F a c t o r :MATRIZ) ;
VAR i , d i a g o n a l :CARDINAL;
BEGIN
FOR i :=1 TO n DO
M[ i , i ] : = 0
END;
FOR d i a g o n a l :=1 TO n−1 DO
FOR i :=1 TO n−d i a g o n a l DO
M[ i , i+d i a g o n a l ] : = Minimo ( d ,M, i , i+d i a g o n a l , F a c t o r [ i , i+
diagonal ] ) ;
END
END
END Matriz ;

La función Mı́nimo es la que calcula el mı́nimo de la expresión en recu-


rrencia vista anteriormente y devuelve no sólo el valor de este mı́nimo, sino
el valor de k para el que se alcanza (mediante el parámetro k1):

PROCEDURE Minimo (VAR d :ORDEN;VAR M:MATRIZ; i , j :CARDINAL;VAR k1 :


CARDINAL) :CARDINAL;
VAR aux , k , min :CARDINAL;
BEGIN
min:=MAX(CARDINAL) ;
FOR k:= i TO j −1 DO
aux :=M[ i , k]+M[ k+1, j ]+d [ i −1]∗d [ k ] ∗ d [ j ] ;
IF aux<min THEN
min:= aux ;
k1 :=k
END
END;
RETURN min
END Minimo ;

Observando el procedimiento Matriz vemos que existe un bucle externo


que se repite desde diagonal = 1 hasta n – 1, y en su interior un bucle
dependiente de la iteración estudiada y del valor de diagonal, y que se ejecuta
desde 1 hasta n – diagonal. En el interior de este bucle hay una llamada al
procedimiento Mı́nimo que tiene una complejidad del orden del valor de la
diagonal, y por tanto el tiempo de ejecución del algoritmo es:
por lo que concluimos que su complejidad temporal es de orden O(n3 ).

305
PROGRAMACIÓN DINÁMICA

Por otro lado, la complejidad espacial del algoritmo es de orden O(n2 ).


En caso que deseemos reconstruir la solución a partir de la tabla Factor,
el siguiente procedimiento muestra por pantalla la forma de multiplicar las
matrices para obtener ese valor mı́nimo:

PROCEDURE EscribeOrden (VAR F a c t o r :MATRIZ; i , j :CARDINAL) ;


VAR k :CARDINAL;
BEGIN
IF i=j THEN
WrStr ( ‘M’);
WrCard (i ,0)
ELSE
k:= Factor [i,j];
WrStr (‘(’ ) ;
EscribeOrden ( Factor , i , k ) ;
WrStr ( ‘ ∗ ’);
EscribeOrden (Factor ,k+1,j);
WrStr (‘)’ )
END
END EscribeOrden ;

El algoritmo aquı́ presentado es capaz de encontrar el valor de la solución


óptima junto con una de las formas de obtenerla. Sin embargo, existen casos
en donde puede haber más de una forma de multiplicar las matrices para
obtener el valor óptimo, como muestra el siguiente ejemplo.
Sean las matrices M1 (10 x 10), M2 (10 x 50) y M3 (50 x 50). Existen dos
formas de asociarlas para multiplicarlas, y en ambos casos obtenemos:
(M1 M2 )M3 = 10*10*50 +10*50*50 = 30000
M1 (M2 M3 )=10*50*50 +10*10*50 = 30000
Es posible modificar el algoritmo para que encuentre todas las soluciones
que llevan al valor óptimo, y para esto es suficiente valerse de la matriz
Factor, si bien esta modificación reviste poco interés desde un punto de vista
práctico.
A continuación se muestra dos implementaciones del algoritmos y su uti-
lización.

#include <s t d i o . h>


#include <math . h>
#include <s t r i n g . h>
#include <i o s t r e a m >

306
PROGRAMACIÓN DINÁMICA

#include <v e c t o r >


#include < l i s t >
#include <s t r i n g >
#include <a l g o r i t h m >
#include <queue>
#include <s t a c k >
#include <s e t >
#include <map>
#include <complex>
#define MAX N 305
#define INF 987654321
using namespace s t d ;
typedef long long l l d ;

int n;
int p [MAX N ] ;
int m[MAX N ] [ MAX N ] ;
int memo [MAX N ] [ MAX N ] ;

i n l i n e int M a t r i x C h a i n M u l t i p l i c a t i o n ( int i , int j )


{
i f ( i == j )
return 0 ;
i f (memo [ i ] [ j ] > 0 )
return memo [ i ] [ j ] ;
int r e t = INF ;
f o r ( int k=i ; k<j ; k++)
{
int c u r r = M a t r i x C h a i n M u l t i p l i c a t i o n ( i , k ) +
M a t r i x C h a i n M u l t i p l i c a t i o n ( k+1, j ) + p [ i −1] ∗ p [ k ] ∗ p [ j
];
r e t = min ( r e t , c u r r ) ;
}
memo [ i ] [ j ] = r e t ;
return r e t ;
}

int main ( )
{
n = 6;
p [ 0 ] = 30 , p [ 1 ] = 35 , p [ 2 ] = 15 , p [ 3 ] = 5 , p [ 4 ] = 10 , p [ 5 ] =
20 , p [ 6 ] = 25;
p r i n t f ( " %d\n" , M a t r i x C h a i n M u l t i p l i c a t i o n ( 1 , 6 ) ) ;
return 0 ;
}

En el siguiente ejemplo además de determinar la cantidad mı́nima de


multiplicaciones se definen como deben el orden en que se deben realizar.

307
PROGRAMACIÓN DINÁMICA

void show ( int i , int j , v e c t o r <v e c t o r <int>> & s )


{
i f ( j> i )
{
c o u t <<"(" ;
show ( i , s [ i ] [ j ] , s ) ;
c o u t <<"x" ;
show ( s [ i ] [ j ]+1 , j , s ) ;
c o u t <<")" ;
}
else
{
c o u t <<"A" <<i + 1 ;
}
}

int m a t r i x c h a i n ( v e c t o r <int> & p , v e c t o r <v e c t o r <int>> & s )


{
const int n = p . s i z e ( ) − 1 ;
v e c t o r <v e c t o r <int>> X ( n , v e c t o r <int> ( n , i n f ) ) ;
s . r e s i z e ( n , v e c t o r <int> ( n ) ) ;
f o r ( int i =0; i <n ; ++i )
X[ i ] [ i ] = 0 ;
f o r ( int w=1; w<n ; ++w)
f o r ( int i =0, j ; j=i+w, j <n ; ++i )
f o r ( int k=i , f ; k<j ;++k )
{
int f =p [ i ] ∗ p [ k +1]∗p [ j + 1 ] ;
i f (X[ i ] [ k]+X[ k + 1 ] [ j ]+ f <X[ i ] [ j ] )
{
X[ i ] [ j ]=X[ i ] [ k]+X[ k + 1 ] [ j ]+ f ;
s [ i ] [ j ]=k ;
}
}
return X [ 0 ] [ n − 1 ] ;
}

En ambas implementaciones su complejidad es O(n3 ).

11.8. Máxima subsecuencia incremental (LIS)


El problema de la máxima subsecuencia incremental conocido también
como LIS por su nombre en ingles Longest increasing subsequences plantea
hallar dentro de una colección o secuencias de elementos de la forma a1 , a2 ,
a3 , ... , an cualquier subconjunto o subsecuencia de los elementos dados en
orden, de la forma ai1 , ai2 , ai3 , ... , aik donde 1≤ i1 < i2 < i3 < ... < ik ≤ n

308
PROGRAMACIÓN DINÁMICA

donde cada elemento es estrictamente mayor que su anterior y estrictamente


menor que su sucesor. Por ejemplo para la colección 5, 2, 8, 6, 3, 6, 9, 7 la
solución serı́a 2, 3, 6, 9.

Una vez vista la imagen donde las flechas indican la transición entre
los elementos que conforman la solución óptima podemos entender mejor la
solución del problema.
Vamos a representar la colección de elementos como nodos de un grafo
donde entre los elementos ai y aj a de existir una arista siempre y cuando se
cumpla las siguientes restricciones ai < aj y i < j. Si se analiza el grafo que
se construye es un grafo dirigido acı́clico (DAG). Por lo que la solución del
problema inicial radica en encontrar el camino mas largo el grafo construido.
for j=1,2,...,n:
L(j)=1+max{L(i):(i,j) ∈ E }
return maxj L(j)
Donde L(j) es la longitud del camino más largo que finaliza en el nodo
j. Este caso se le suma uno porque la distancia del camino no la define la
cantidad de aristas sino la cantidad de nodos que lo componen. La siguiente
implementación resuelve el problema planteado devolviendo los elementos
que conforman la LIS siguiendo la idea planteada anteriormente.

#include <s t d i o . h>


#include <math . h>
#include <s t r i n g . h>
#include <i o s t r e a m >
#include <v e c t o r >
#include < l i s t >
#include <s t r i n g >
#include <a l g o r i t h m >
#include <queue>
#include <s t a c k >
#include <s e t >
#include <map>
#include <complex>
using namespace s t d ;
typedef long long l l d ;

v e c t o r <int> l i s ( const v e c t o r <int> & a )


{
const int n = a . s i z e ( ) ;

309
PROGRAMACIÓN DINÁMICA

v e c t o r <int> X ( n , 1 ) ;
v e c t o r <int> Y ( n , − 1 ) ;
f o r ( int i =1; i <n;++ i )
{
f o r ( int j =0; j <i ;++ j )
{
i f ( a [ j ] <a [ i ] )
{
i f (X[ i ]<X[ j ]+1)
{
X[ i ]=X[ j ] + 1 ;
Y[ i ]= j ;
}
}
}
}
v e c t o r <int> b ;
int k = 0 ;
f o r ( int i = 0 ; i <n ; ++ i )
i f (X[ k]<X[ i ] )
k=i ;
f o r ( int i = k ; i >=0; i=Y[ i ] )
b . push back ( a [ i ] ) ;
r e v e r s e ( b . b e g i n ( ) , b . end ( ) ) ;
return b ;
}

Esta implementación de tiene una complejidad de O(n2 ) siendo n la can-


tidad de elementos de la secuencia o colección inicial. La siguiente imple-
mentación soluciona igualmente el problema con una complejidad inferior a
la anterior en este caso es O (nlogn)

#include <s t d i o . h>


#include <math . h>
#include <s t r i n g . h>
#include <i o s t r e a m >
#include <v e c t o r >
#include < l i s t >
#include <s t r i n g >
#include <a l g o r i t h m >
#include <queue>
#include <s t a c k >
#include <s e t >
#include <map>
#include <complex>
using namespace s t d ;
typedef long long l l d ;
const int i n f = 9 9 9 9 9 9 9 9 ;

310
PROGRAMACIÓN DINÁMICA

int indexOf ( v e c t o r <int> as , int x )


{
return d i s t a n c e ( a s . b e g i n ( ) , lower bound ( a s . b e g i n ( ) , a s . end ( ) , x )
);
}

v e c t o r <int> l i s f a s t ( const v e c t o r <int> & a )


{
const int n = a . s i z e ( ) ;
v e c t o r <int> A ( n , i n f ) ;
v e c t o r <int> i d ( n ) ;
f o r ( int i =0; i <n;++ i )
{
i d [ i ]= indexOf (A, a [ i ] ) ;
A[ i d [ i ] ] = a [ i ] ;
}
int m = ∗ max element ( i d . b e g i n ( ) , i d . end ( ) ) ;
v e c t o r <int> b (m + 1 ) ;
f o r ( int i=n−1; i >=0;−− i )
i f ( i d [ i ]==m)
b [m−−]=a [ i ] ;
return b ;
}

11.9. Contar la cantidad de ocurrencia de un


patrón dentro de una cadena como sub-
secuencia de caracteres no consecutivos
Durante la lectura de problemas nos podemos encontrar algunos que su
solución se reduce a encontrar dentro de una cadena la cantidad de ocu-
rrencias de un determinado patrón como una subsecuencia de caracteres de
no necesariamente consecutivos. Notesé la diferencia con el algoritmo KMP
visto en el capı́tulo de Cadena de este manual. Veamos un ejemplo:
Digamos que tenemos como cadena en la que debemos buscar subsequence
y como patrón a buscar la secuencia sue. Si aplicamos un KMP con estos dos
elementos el algoritmo nos devolverá como respuesta 0 porque no existe una
subsecuencia de caracteres consecutivos que sea igual a sue.
Si embargo si buscamos el patrón sue dentro de la cadena como subse-
cuencia de caracteres no consecutivas encotramos que la respuesta es 7.
subsequence
subsequence

311
PROGRAMACIÓN DINÁMICA

subsequence
subsequence
subsequence
subsequence
subsequence
Una idea para resolver este problema es utilizar la recursividad. Si compa-
ramos los últimos carácteres de la cadena donde vamos a buscar( a partir de
ahora vamos a nombrarla X) y del patrón que queremos encontrar (a partir
de ahora la nombraremos Y) siendo m y n las longitudes de las cadenas X y
Y respectivamente podemos encontrar dos posibilidades.

Si el último cáracter de la cadena es el mismo que el último cáracter


del patrón, recursamos para analizar ahora para las subcadenas X[0 ...
m-1] y Y[0 ... n-1]. Como queremos hallar todas las posibles soluciones
debemos considerar el caso que el último cáracter de la cadena no sea
igual al último cáracter del patrón, en ese caso recursamos para analizar
las subcadenas X[0 ... m-1] y Y[0 ... n].

Si el último cáracter de la cadena no es el mismo que el último cáracter


del patrón entonces recursamos para analizar las subcadenas X[0 ...
m-1] y Y[0 ... n].

Veamos la implementación de esta idea

#include <i o s t r e a m >


using namespace s t d ;

unsigned long long count ( s t r i n g X, s t r i n g Y, int m, int n )


{
// Caso b a s e 1 : s i ambas l o n g i t u d e s de l a s c a d e n a s son 1
i f (m == 1 && n == 1 )
return (X [ 0 ] == Y [ 0 ] ) ;
// Caso b a s e 2 : S i l a l o n g i t u d de l a cadena X e s c e r o ,
s i g n i f i c a que l l e g u e a l
// p r i n c i p i o de l a cadena s i n e n c o n t r a r l a s u b s e q u e n c i a
i f (m == 0 )
return 0 ;
// Caso b a s e 3 : S i l a l o n g i t u d de l a cadena Y e s c e r o ,
s i g n i f i c a que l l e g u e
// p r i n c i p i o d e l p a t r o n por t a n t o e n c o n t r e una s u b s e c u e n c i a
i f ( n == 0 )
return 1 ;
// O p t i m i z a c i o n : No e x i s t e s o l u c i o n p o s i b l e s i l a l o n g i t u d
// de l a cadena e s menor que l a l o n g i t u d d e l p a t r o n a b u s c a r
i f ( n > m)

312
PROGRAMACIÓN DINÁMICA

return 0 ;
// S i e l u l t i m o c a r a c t e r de l a cadena c o i n c i d e con e l u l t i m o
// c a r a c t e r d e l patron ,
// 1 . Llamo de nuevo a l a f u n c i o n e x c l u y e n d o e l u l t i m o
caracter
// de l a cadena y d e l p a t r o n i n both s t r i n g and p a t t e r n
// 2 . Llamo de nuevo a l a f u n c i o n e x l u y e n d o s o l a m e n t e a l
ultimo
// c a r a c t e r de l a cadena
// s i n o son son i g u a l e s l o s u l t i m o c a r a c t e r e s l l a m o a l a
funcion excluyendo
// s o l a m e n t e e l u l t i m o c a r a c t e r de l a cadena ,
return ( (X[m−1] == Y[ n −1]) ? count (X, Y, m − 1 , n − 1 ) : 0 ) +
count (X, Y, m − 1 , n ) ;
}

int main ( )
{
s t r i n g X = " subsequence " ;
s t r i n g Y = "sue" ;
// cadena
// p a t r o n
c o u t << count (X, Y, X. s i z e ( ) , Y. s i z e ( ) ) ;
return 0 ;
}

El único problema de esta solución es que su complejidad temporal es ex-


ponencial pero por el lado bueno su complejidad en cuanto a uso de memoria
es O(1).
La idea es utilizar a Programación Dinámica para solucionar este proble-
ma. El problema tiene una subestructura óptima. Por encima de la solución
también exhibe subproblemas que se superponen . Si dibujamos el árbol del
recursion de la solución, podemos ver que los mismos subproblemas son cal-
culados una y otra vez.
Sabemos que los problemas que tienen subestructura óptima y los subpro-
blemas implicados pueden ser solucionados usando programación dinámica,
en cuál los subproblemas solucionados y memorizados en vez de calculados
una y otra vez. La versión Memorización sigue el acercamiento de arriba a
abajo, desde que primero quebrantamos el problema en los subproblemas y
luego calculamos y almacenamos valores. También podemos solucionar este
problema en la manera de abajo hacia arriba. En el acercamiento de aba-
jo hacia arriba, solucionamos subproblema más pequeño primero, entonces
solucionan mayores subproblemas de ellos.
A continuación el código para solucionar el mismo problema usando pro-

313
PROGRAMACIÓN DINÁMICA

gramación dinámica usando la tecnica de memorización.

unsigned long long count ( s t r i n g X, s t r i n g Y, int m, int n )


{
//T [ i ] [ j ] almacena e l numero de v e c e s que e l p a t r o n Y [ 0 . . j )
// a p a r e c e en l a cadena X [ 0 . . i ) como s u b s e c u e n c i a
unsigned long long T[m + 1 ] [ n + 1 ] ;
// S i e l p a t r o n e s v a c i o encontramos un s u b s e c u e n c i a
f o r ( int i = 0 ; i <= m; i ++)
T[ i ] [ 0 ] = 1 ;
// S i l a cadena e s v a c i a .
f o r ( int j = 1 ; j <= n ; j ++)
T[ 0 ] [ j ] = 0;
// S i e l a c t u a l c a r a c t e r de l a cadena y e l p a t r o n c o i n c i d e n ,
// 1 . e x c l u i m o s e l c a r a c t e r en l a cadena y e l p a t r o n
// 2 . e x c l u i m o s e l c a r a c t e r s o l a m e n t e en l a cadena
// s i n o e l a c t u a l c a r a c t e r de l a cadena y e l p a t r o n no
coinciden ,
// e x c l u i m o s e l c a r a c t e r s o l a m e n t e en l a cadena
f o r ( int i = 1 ; i <= m; i ++)
f o r ( int j = 1 ; j <= n ; j ++)
T [ i ] [ j ] = ( (X[ i −1] == Y[ j −1]) ? T [ i − 1 ] [ j −1] : 0 )+T [ i − 1 ] [ j
];
// r e t o r n a l a u l t i m a c e l d a de l a m a t r i z con l a s o l u c i o n a l
problema
return T [m] [ n ] ;
}

int main ( )
{
s t r i n g X = " subsequence " ;
s t r i n g Y = "sue" ;
// cadena
// p a t r o n
c o u t << count (X, Y, X. s i z e ( ) , Y. s i z e ( ) ) ;
return 0 ;
}

Esta solución es que su complejidad temporal es O(nm) y su complejidad


en cuanto a uso de memoria es O(nm).

11.10. Needleman-Wunsch
El algoritmo de Needleman-Wunsch sirve para realizar alineamientos glo-
bales de dos secuencias. Se suele utilizar en el ámbito de la bioinformática
para alinear secuencias de proteı́nas o de ácidos nucleicos. Fue propuesto por

314
PROGRAMACIÓN DINÁMICA

primera vez en 1970, por Saul Needleman y Christian Wunsch. Se trata de un


ejemplo tı́pico de programación dinámica. El algoritmo funciona del mismo
modo independientemente de la complejidad o longitud de las secuencias y
garantiza la obtención del mejor alineamiento.
Las dos secuencias a alinear, llamadas A y B en los ejemplos, de longitud
|A| = m y |B| = n, están formadas por elementos de un alfabeto finito
de sı́mbolos. El algoritmo necesita saber qué sı́mbolos son diferentes entre
sı́ y cuáles son iguales. Podemos utilizar una matriz cuadrada (S) para este
propósito, en la que cada elemento Sij indique la similitud entre los elementos
i y j del alfabeto usado. Si nuestro alfabeto de sı́mbolos no fuese finito, en
vez de una matriz podrı́amos usar una función R2 → R que tuviese como
parámetros ambos sı́mbolos a comparar y cuya salida fuese la similitud entre
ambos. También se necesita otro parámetro (d) que nos indique cómo vamos
a valorar que un sı́mbolo no quede alineado con otro y que en su lugar se
utilice un hueco.
Por ejemplo podemos definir la siguiente matriz:

Y entonces el siguiente alineamiento:


AGACTAGTTAC
CGA—GACGT
con una penalización por hueco de d=-5 nos devolverı́a como solución
óptima:

Para determinar la puntuación óptima y poder reconstruir el alineamien-


to que devolverı́a esa puntuación se necesita otra matriz, F, que almacena
los resultados parciales de cada posible alineamiento. Las dimensiones de la
matriz F son el número de elementos en la secuencia A y el de B (|A| × |B|).
En cada iteración del algoritmo recibe valor un elemento de la matriz
F. El valor que recibe el elemento Fij representa la puntuación obtenida al

315
PROGRAMACIÓN DINÁMICA

alinear de forma óptima los primeros i elementos de A y los primeros j de


B. Cuando el algoritmo termine, el último elemento de F (Fmn , con m = |A|
y n = |B|) contendrá la puntuación para el alineamiento óptimo de ambas
secuencias.
Inicio del algoritmo: F0j = d ∗ j
Fi0 = d ∗ i Recursión para obtener el siguiente elemento de forma óptima:
Fij = máx(Fi−1,j−1 + S(Ai , Bj ), Fi,j−1 + d, Fi−1,j + d)
Cuando el algoritmo acaba tenemos calculada la matriz F; el resultado
es la puntuación devuelta por el mejor alineamiento posible, de acuerdo a
los parámetros que hemos definido. Para obtener la secuencia se necesita
ejecutar el siguiente algoritmo, que hace uso de la matriz F. Este algoritmo
comienza por el último elemento, Fmn , y va retrocediendo hasta llegar a un
elemento de la primera fila o la primera columna de F. En cada paso se com-
paran 3 elementos de F para ver cuál de ellos es el que se ha seguido en la
solución óptima. Para cada Fij debemos comparar Fi−1,j , Fi,j−1 y Fi−1,j−1 .
Si el elemento usado es Fi−1,j , entonces Ai se ha alineado con un hueco; si es
Fi,j−1 , entonces Bi se ha alineado con un hueco; y si no, si el elemento elegido
es Fi−1,j−1 , los elementos Ai y Bi han sido alineados. Es importante desta-
car que el que dos elementos sean alineados no implica necesariamente que
sean iguales; significa que entre esa posibilidad, alinear con huecos o alinear
sı́mbolos diferentes, esa era la mejor opción. Veamos una implementación del
algoritmo.

#include <s t d i o . h>


#include <math . h>
#include <s t r i n g . h>
#include <time . h>
#include <i o s t r e a m >
#include <v e c t o r >
#include <l i s t >
#include <s t r i n g >
#include <a l g o r i t h m >
#include <queue>
#include <s t a c k >
#include <s e t >
#include <map>
#include <complex>

#define MAX N 1001

using namespace s t d ;
typedef long long l l d ;
typedef unsigned long long l l u ;

316
PROGRAMACIÓN DINÁMICA

int n , m;
int m a t c h s c o r e , m i s m a t c h s c o r e , g a p s c o r e ;
s t r i n g A, B ;
int dp [MAX N ] [ MAX N ] ;

i n l i n e int needleman wunsch ( )


{
f o r ( int i =0; i<=n ; i ++)
dp [ i ] [ 0 ] = dp [ 0 ] [ i ] = − i ∗ g a p s c o r e ;
f o r ( int i =1; i<=n ; i ++)
{
f o r ( int j =1; j<=m; j ++)
{
int S = (A[ i −1] == B [ j −1]) ? m a t c h s c o r e : −
mismatch score ;
dp [ i ] [ j ] = max( dp [ i − 1 ] [ j −1] + S , max( dp [ i − 1 ] [ j ] −
g a p s c o r e , dp [ i ] [ j −1] − g a p s c o r e ) ) ;
}
}
return dp [ n ] [m] ;
}

i n l i n e p a i r <s t r i n g , s t r i n g > g e t o p t i m a l a l i g n m e n t ( )
{
s t r i n g retA , retB ;
s t a c k <char> SA , SB ;
int i i = n , j j = m;

while ( i i != 0 | | j j != 0 )
{
i f ( i i == 0 )
{
SA . push ( ’-’ ) ;
SB . push (B [ j j −1]) ;
j j −−;
}
e l s e i f ( j j == 0 )
{
SA . push (A[ i i −1]) ;
SB . push ( ’-’ ) ;
i i −−;
}
else
{
int S = (A[ i i −1] == B [ j j −1]) ? m a t c h s c o r e : −
mismatch score ;
i f ( dp [ i i ] [ j j ] == dp [ i i − 1 ] [ j j −1] + S )
{
SA . push (A[ i i −1]) ;

317
PROGRAMACIÓN DINÁMICA

SB . push (B [ j j −1]) ;
i i −−; j j −−;
}
e l s e i f ( dp [ i i − 1 ] [ j j ] > dp [ i i ] [ j j −1])
{
SA . push (A[ i i −1]) ;
SB . push ( ’-’ ) ;
i i −−;
}
else
{
SA . push ( ’-’ ) ;
SB . push (B [ j j −1]) ;
j j −−;
}
}
}

while ( ! SA . empty ( ) )
{
retA += SA . top ( ) ;
retB += SB . top ( ) ;
SA . pop ( ) ;
SB . pop ( ) ;
}
return m a k e p a i r ( retA , retB ) ;
}

int main ( )
{
n = 5 , m = 6;
match score = 2 , mismatch score = 1 , gap score = 1;
A = " CATGT " ;
B = " ACGCTG " ;
p r i n t f ( " %d\n" , needleman wunsch ( ) ) ;
p a i r <s t r i n g , s t r i n g > a l i g n m e n t = g e t o p t i m a l a l i g n m e n t ( ) ;
p r i n t f ( " %s\n %s\n" , a l i g n m e n t . f i r s t . c s t r ( ) , a l i g n m e n t . s e c o n d .
c str () ) ;
return 0 ;
}

Se puede demostrar formalmente que tanto el tiempo de ejecución como


el espacio necesario para ejecutar el algoritmo son de orden O(nm). Para al-
guna aplicaciones, sobre todo en bioinformática, el requerimiento de espacio
es prohibitivo, puesto que se alinean secuencias muy largas. Existe una op-
timización de este algoritmo, denominada algoritmo de Hirschberg, que sólo
necesita espacio del orden O(m+n), pero a costa de incrementar el tiempo
de computación.

318
PROGRAMACIÓN DINÁMICA

11.11. Encontrar la submatriz cero más gran-


de
Vamos a suponer que tenemos una matriz con n filas y m columnas. Y
necesitamos encontrar la submatriz más grande que consta de solo ceros (una
submatriz es un área rectangular de la matriz).
Los elementos de la matriz serán a[i][j], donde i = 0 ... n - 1, j = 0 ...
m - 1. Para simplificar, consideraremos todos los elementos distintos de cero
iguales a 1.

Paso 1: dinámica auxiliar


Primero, calculamos la siguiente matriz auxiliar: d[i][j], la fila más cercana
que tiene un 1 por encima de a[i][j]. Hablando formalmente, d[i][j] es el
número de fila más grande (de 0 a i − 1), en el que hay un elemento igual a
1 en la columna j-ésima. Mientras iteramos de arriba a la izquierda a abajo
a la derecha, cuando estamos en la fila i, conocemos los valores de la fila
anterior, por lo tanto, es suficiente actualizar solo los elementos con el valor
1. Podemos guardar los valores en una matriz simple d[i], i = 1...m − 1,
porque en el algoritmo adicional procesaremos la matriz una fila a la vez y
solo necesitamos los valores de la fila actual.

v e c t o r <int> d (m, −1) ;


f o r ( int i= 0 ; i <n;++ i ) {
f o r ( int j =0; j <m;++ j ) {
i f ( a [ i ] [ j ]==1){
d [ j ]= i ;
}
}
}

Paso 2: resolución de problemas


Podemos resolver el problema en O(nm2 ) iterando a través de filas, consi-
derando todas las posibles columnas izquierda y derecha para una submatriz.
La parte inferior del rectángulo será la fila actual, y usando d[i][j] podemos
encontrar la fila superior. Sin embargo, es posible ir más allá y mejorar sig-
nificativamente la complejidad de la solución.
Está claro que la submatriz cero deseada está limitada por los cuatro
lados por unos, lo que evita que aumente de tamaño y mejore la respuesta.
Por lo tanto, no perderemos la respuesta si actuamos de la siguiente manera:
para cada celda j en la fila i (la fila inferior de una submatriz cero potencial)

319
PROGRAMACIÓN DINÁMICA

tendremos d[i][j] como la fila superior de la submatriz cero actual. Ahora


queda por determinar los lı́mites óptimos izquierdo y derecho de la submatriz
cero, es decir, empujar al máximo esta submatriz a la izquierda y derecha de
la columna j-ésima.
¿Qué significa empujar el máximo hacia la izquierda? Significa encontrar
un ı́ndice k1 para el cual d[i][k1] > d[i][j], y al mismo tiempo k1, el más
cercano a la izquierda del ı́ndice j. Está claro que entonces k1 + 1 da el
número de la columna izquierda de la submatriz cero requerida. Si no hay
tal ı́ndice, entonces ponga k1 = −1 (esto significa que pudimos extender la
submatriz cero actual a la izquierda hasta el borde de la matriz a).
Simétricamente, puede definir un ı́ndice k2 para el borde derecho: este es
el ı́ndice más cercano a la derecha de j tal que d[i][k2] > d[i][j] (o m, si no
existe dicho ı́ndice)
Entonces, los ı́ndices k1 y k2, si aprendemos a buscarlos de manera efec-
tiva, nos darán toda la información necesaria sobre la submatriz cero actual.
En particular, su área será igual a (i − d[i][j]) ∗ (k2 − k1 − 1).
¿Cómo buscar estos ı́ndices k1 y k2 de manera efectiva con i y j fijas?
Podemos hacer eso en O(1) en promedio.
Para lograr tal complejidad, puede usar la pila de la siguiente manera.
Primero, aprendamos cómo buscar un ı́ndice k1 y guardar su valor para
cada ı́ndice j dentro de la fila actual i en la matriz d1[i][j]. Para hacer esto,
revisaremos todas las columnas j de izquierda a derecha, y almacenaremos en
la pila solo aquellas columnas que tengan d[][] estrictamente mayor que d[i][j].
Está claro que al pasar de una columna j a la siguiente columna, es necesario
actualizar el contenido de la pila. Cuando hay un elemento inapropiado en la
parte superior de la pila (es decir, d[][] <= d[i][j]), revı́salo. Es fácil entender
que es suficiente eliminarlo de la pila solo desde su parte superior y de ninguno
de sus otros lugares (porque la pila contendrá una secuencia d creciente de
columnas).
El valor d1[i][j] para cada j será igual al valor que se encuentra en ese
momento en la parte superior de la pila.
La dinámica d2[i][j] para encontrar los ı́ndices k2 se considera similar,
solo que necesita ver las columnas de derecha a izquierda.
Está claro que dado que hay exactamente m piezas agregadas a la pila
en cada lı́nea, tampoco podrı́a haber más eliminaciones, la suma de comple-
jidades será lineal, por lo que la complejidad final del algoritmo es O(nm).
También se debe tener en cuenta que este algoritmo consume memoria O
(m) (sin contar los datos de entrada, la matriz a[][]).

int z e r o m a t r i x ( v e c t o r <v e c t o r <int>> a ) {


int n = a . s i z e ( ) ;

320
PROGRAMACIÓN DINÁMICA

int m = a [ 0 ] . s i z e ( ) ;

int ans = 0 ;
v e c t o r <int> d (m, −1) , d1 (m) , d2 (m) ;
s t a c k <int> s t ;
f o r ( int i =0; i <n;++ i ) {
f o r ( int j =0; j <m;++ j ) {
i f ( a [ i ] [ j ]==1)
d [ j ]= i ;
}

f o r ( int j =0; j <m;++ j ) {


while ( ! s t . empty ( ) && d [ s t . top ( ) ] <= d [ j ] )
s t . pop ( ) ;
d1 [ j ]= s t . empty ( ) ? −1 : s t . top ( ) ;
s t . push ( j ) ;
}

while ( ! s t . empty ( ) )
s t . pop ( ) ;

f o r ( int j=m−1; j >=0;−− j ) {


while ( ! s t . empty ( ) && d [ s t . top ( ) ] <= d [ j ] )
s t . pop ( ) ;
d2 [ j ] = s t . empty ( ) ? m : s t . top ( ) ;
s t . push ( j ) ;
}

while ( ! s t . empty ( ) )
s t . pop ( ) ;

f o r ( int j =0; j <m;++ j )


ans = max( ans , ( i −d [ j ] ) ∗ ( d2 [ j ]−d1 [ j ] −1) ) ;
}
return ans ;
}

11.12. Subarray máximo

11.13. Subconjunto suma


El problema se reduce a un conjunto de enteros y un determinado valor,
determinar si hay un subconjunto del conjunto dado con suma igual a va-
lor dado. Para resolver este problema vamos dividirlo en dos subproblemas
dependiendo de los valores presentes en el conunto. Un primer caso cuando

321
PROGRAMACIÓN DINÁMICA

todos los números son positivos y otros caso cuando existen números positi-
vos y negativos.

11.13.1. Conjunto de números positivos solamente


11.13.2. Conjunto de números positivos y negativos

11.14. Trı́angulo de números

11.15. Segmented Least Squares

11.16. Análisis de ejercicios


3113 - Marathon Lo primero es recorrer todos los puntos desde 0 hasta
n-1 e ir calculando las distancia desde el punto i hasta i+1 e ir acumulando
eso valores en una variable. Luego vuelvo recorrer los puntos desde 1 hasta
n-1 y verifico si quitando las distancias entre los puntos i-1 y i, y la distancia
entre i y i+1 a la distancia total calculada y sumándole las distancia entre
i-1 y i+1 es menor que la distancia mı́nima que tengo hasta esos momento si
es ası́ actualizo y hago este análisis para cada punto entre 1 y n-1.

1430 - Traverse through the Board Su solución es de forma dinámica


(una variante de tabla acumulativa), para eso tendremos dos matrices la pri-
mera es la que me entra por datos, la segunda vamos a tener i,j la cantidad de
forma de llegar hasta celda. Y la solución del ejercicio va estar en dp[n-1][n-1].
Inicialmente todas las celdas dp son igual 0 luego dp[0][0]=1. Entonces para
celda de dp y matrix su valor sea distinto de 0 vamos a realizar análisis: si la
fila que ocupa esa celda se le suma el valor almacenado en matrix y es me-
nor que la máxima cantidad de fila entonces dp[i+matrix[i][j]][j]+=dp[i][j], si
la columna que ocupa esa celda se le suma el valor almacenado en matrix y es
menor que la máxima cantidad de columna entonces dp[i][j+matrix[i][j]]+=dp[i][j]
, recordar que la solución hay que modular con respecto a un valor.

1478 - Basic Edit Distance Es el clásico problema que se resuelve apli-


cando el algoritmo Edit Distance.

1404 - Final Ranking Si extrapolamos el problema a la situación que se


nos plantea en el epigrafe Ordenaciones de objetos entre dos relaciones donde
en este caso las relaciones son los posibles lugares que se pueden otorgar en

322
PROGRAMACIÓN DINÁMICA

la carrera, es facil ver que la solución del problema se soluciona aplicando el


algoritmo descrito en dicho epigrafe realizandole los ajuste pertinente para
que la solución sea modulada.

3820 - Returning Coins El problema se resuelve aplicando algoritmo de


coin change para calcular la menor cantidad de monedas se debe dar para una
determinada cantidad sabiendo de antemano las denominaciones de los tipos
de monedas disponibles y asumiendo que tenemos una cantidad ilimitada de
cada denominación. La única diferencia con respecto al algoritmo original es
que las denominaciones de las monedas no son las mismas siempre para cada
caso sino varia de acuerdo al caso que sea.

1926 - Party at Hali-Bula El problema se resuelve primero modelando


la jerarquı́a de mando como un árbol luego para cada nodo (trabajador) se
calcula la cantidad de invitados que el puede invitar tomando el máximo entre
todos los invitados que pueden invitar sus subordinados y todos los invitados
que pueden invitar los subordinados de sus subordinados mas uno (en este
caso el puede ir a la fiesta). Si ambas cantidades son iguales entonces no existe
una solución única. El cálculo se debe hacer recursivamente con un recorrido
donde primero postorden,es decir primero visito a mis hijos y después visito
a la raı́z.

1624 - Counting Palindromes Vamos a partir que en una una cadena


de n letras la cantidad máxima de palindromes es n donde cada palindrome
tiene como longitud 1, es por eso que vamos construir un vector A con la
misma dimensión de la palabra entrada en los datos y cada posición en el
vector tendrán el valor de esa posición (A1 = 1,A2 = 2, ..., An = n) luego la
solución va ser aplicando el principio de optimalidad de Bellman que dicta
que ”dada una secuencia óptima de decisiones, toda subsecuencia de ella es,
a su vez, óptima”:
Para i desde 1 hasta n:
Para j desde 1 hasta i:
Si A[j-1]+1<A[i] y esPalindrome(j,i)
A[i]=A[j-1]+1;
Donde la función esPalidrome me devuelve verdadero si de la palabra de
la posición j a la posición i es palindrome.

3330 - The Number Of The Witch Una vez leı́do el problema podemos
percatarnos que se nos esta pidiendo determinar cuantas subsecuencias con
los caracteres no necesariamente consecutivos existen dentro de una cadena

323
PROGRAMACIÓN DINÁMICA

conformada solo por dı́gitos tales que sean iguales a un determinado a un


patrón solo conformado por dı́gitos. Es evidente que aplicando el algoritmo
visto en la sección Contar la cantidad de ocurrencia de un patrón
dentro de una cadena como subsecuencia de caracteres no conse-
cutivos. El único detalle que se debe tener en cuenta que las respuesta debe
ser modulada a 109 + 7 por lo que en el algoritmo solo tendrı́amos que hacer
la siguiente modificación:

.....
f o r ( int i = 1 ; i <= m; i ++)
f o r ( int j = 1 ; j <= n ; j ++)
{
i f ( t e x t [ i −1] == p a t t e r n [ j −1])
table [ i ] [ j ] = ( t a b l e [ i − 1 ] [ j −1]+ t a b l e [ i − 1 ] [ j ] ) %
MOD;
else
table [ i ] [ j ] = (0 + t a b l e [ i −1][ j ] ) %
MOD;
}
....

Donde MOD es igual a 109 + 7

4004 - Nerdson and Alejandra Una vez leı́do el problema podemos per-
catarnos que se nos esta pidiendo determinar cuantas subsecuencias con los
caracteres no necesariamente consecutivos existen dentro de una cadena ta-
les que sean iguales a un determinado a un patrón que este caso es siempre
el mismo papa. Es evidente que aplicando el algoritmo visto en la sección
Contar la cantidad de ocurrencia de un patrón dentro de una ca-
dena como subsecuencia de caracteres no consecutivos llegamos a la
solución del ejercicio.

2345 - Finding Social Portfolio Una vez leı́do el problema podemos


analizar que la solución del mismo es aplicar el algoritmo mochila 01 sobre
los datos de entrada.

2512 - Powers of Two II Dada una secuencia de dı́gitos ver cuantas


subsecuencia de caracteres no necesariamente consecutivos existen tales que
conforman una potencia de dos.
Para resolver este problema podemos hacer la dinámica correspondiente
a este tipo Contar la cantidad de ocurrencia de un patrón dentro de
una cadena como subsecuencia de caracteres no consecutivos. Una
dinámica con la secuencia dada y la secuencia de cada potencias de 2 hasta

324
PROGRAMACIÓN DINÁMICA

232 . Es decir hacer la dinámica 31 veces y sumar los resultados modulando


los mismos con 109 +7.

2561 - Arrow Flips II Dada una secuencia de carácteres compuesta sola-


mente por los sı́mbolos > y < buscar la secuencia de carácteres de longitud
mayor tal que exista una cantidad indeterminada de sı́mbolo del tipo > y
exactamente K sı́mbolo del tipo < o de forma inversa buscar la mayor secuen-
cia de sı́mbolos del tipo < y que en dicha subsecuencia concecutivos exista
exactamente K sı́mbolos del tipo >. Para resolver este ejercicio hacer una
dinamica que permita llevar para cada intervalos [i, j] cuantos carácteres hay
de cada sı́mbolo. Luego con una búsqueda binaria busco la longitud deseada
donde la búsqueda se hace con el siguiente criterio:

#define MIDLE ( l e f t +r i g h t +1) /2


...

bool makeKChanges ( int i n d e x ) {


f o r ( int i = i n d e x ; i<=s i z e I n p u t ; i ++){
int a r r o w L e f t = tableAcumLeft [ i ]− tableAcumLeft [ i − i n d e x ] ;
int arrowRigth = tableAcumRigth [ i ]− tableAcumRigth [ i −
index ] ;
i f ( a r r o w L e f t<=K && s i z e I n p u t − i n d e x >=K−a r r o w L e f t )
return true ;
i f ( arrowRigth<=K && s i z e I n p u t − i n d e x >=K−arrowRigth )
return true ;
}
return f a l s e ;
}

....

int l e f t =0, r i g h t=s i z e I n p u t ;


while ( l e f t <r i g h t ) {
int mid = MIDLE ;
i f ( makeKChanges ( mid ) ) {
l e f t = mid ;
answer= l e f t ;
} else
r i g h t=mid −1;
}

DMOJ - Prince Una vez leı́do el problema nos podemos percatar que es
un clásico problema de mochila01 donde solo debemos aplicar el algoritmo
para cada caso y devolver la suma de todas mochilas juntas.

325
PROGRAMACIÓN DINÁMICA

DMOJ - La pronunciación de vocales Se tiene una cadena de hasta


105 y se quiere hallar la longitud de la subcadena mayor que contenga solo
vocales. Para resolver el problema podemos aplicar una especie de dinamica
que recorra la cadena e incremente un contador cada vez que la letra analizada
sea vocal y que su valor sea cero cuando no sea vocal. Luego dentro de la
misma estructura repetitiva que nos permite analizar cada carácter quedarnos
con el mayor valor que puede tomar el contador mencionado anteriormente.

1103 Coin Change Ejercicio clásico para resolver con el algoritmo Coin
Change donde para varios valores debes hallar la cantidad de fora que se pue-
den devolver utilizando billetes de las cuales se conocen sus denominaciones

3105 Filling the Hole El ejercicio se reduce a encontrar de cuantas formas


se puede tapar un hueco de longitud L usando una cantidad infinita de dos
piezas de maderas las cuales tienen una longitudes l y s respectivamente.
Si extrapolamos el problema podemos resolverlo usando el algoritmo Coin
Change donde L serı́a la cantidad de dinero que debe hallar la cantidad de
devolverlo utilizando dos denominaciones de billetes los cuales son s,l. El otro
detalle del problema es que la respuesta debe ser modulada a 109 +7.

2616 Easy Change Ejercicio clásico para resolver con el algoritmo Coin
Change para la variante en que se debe calcular un valor N la cantidad mı́ni-
ma de billetes que se debe devolver utilizando billetes cuyas denominaciones
son 1,3,5,6.

1104 - Cross-Country Una vez leı́do el problema nos podemos percatar


que dada los puntos de chequeo que debe pasar Agnes en su ruta debemos
hallar de las posibles rutas que tiene Tom aquella que tenga mayor cantidad
de puntos de chequeos coincidente con Agnes para tener la mayor cantidad
de posibles encuentros con ellas. Como en la ruta se debe respetar en orden
de los puntos de chequeo por los cual debe pasar para resolver el problema
vamos a realizar un LCS entre la ruta de Agnes y cada una de las rutas
de Tom y nos quedaremos como respuesta con aquella que produzca mayor
valor.

326
Capı́tulo 12

Teorı́a de juego

Es rama de la matemática y la lógica que se ocupa del análisis de jue-


gos (ejemplo: Las situaciones que implica partes con conflictos de intereses).
Además de la elegancia matemática y soluciones completas para juegos sim-
ples, los principios de la teorı́a de juego encuentran también aplicación en
juegos mas complicados como son los naipes (cartas), damas y ajedrez, ası́
como también los problemas realmente mundiales tan diverso como la eco-
nomı́a, la división de la propiedad, la polı́tica, y la guerra.
Los juegos de los que hablaremos son juegos para dos personas con infor-
mación perfecta, sin movimientos al azar, y un resultado de ganar o perder.
En estos juegos, los jugadores suelen alternar movimientos hasta alcanzar
una posición terminal. Después de eso, un jugador es declarado ganador y el
otro perdedor. La mayorı́a de los juegos de cartas no se ajustan a esta ca-
tegorı́a, por ejemplo, porque no tenemos información sobre qué cartas tiene
nuestro oponente.
Primero veremos la división básica de posiciones para ganar y perder.
Después de eso, dominaremos el juego más importante, el Juego de Nim,
y veremos cómo comprenderlo nos ayudará a jugar juegos compuestos. No
podremos jugar muchos de los juegos sin descomponerlos en partes más pe-
queñas (subjuegos), pre-calcular algunos valores para ellos y luego obtener
el resultado combinando estos valores.

12.1. Lo básico
Un ejemplo simple es el siguiente juego, jugado por dos jugadores que se
turnan para moverse. Al principio hay n monedas. Cuando es el turno de un
jugador, él puede quitar 1, 3 o 4 monedas. El jugador que se lleva el último
es declarado ganador (en otras palabras, el jugador que no puede hacer un

327
TEORÍA DE JUEGO

movimiento es el perdedor). La pregunta es: ¿para qué n ganará el primer


jugador si ambos juegan de manera óptima?.
Podemos ver que n = 1, 3, 4 son posiciones ganadoras para el primer
jugador, porque simplemente puede tomar todas las monedas. Para n = 0 no
hay movimientos posibles (el juego está terminado), por lo que es la posición
perdedora para el primer jugador, ya que no puede moverse de él. Si n = 2,
el primer jugador tiene solo una opción, para eliminar 1 moneda. Si n = 5 o
6, un jugador puede moverse a 2 (eliminando 3 o 4 monedas), y está en una
posición ganadora. Si n = 7, un jugador puede mover solo a 3, 4, 6, pero de
todos ellos su oponente puede ganar.
Las posiciones tienen las siguientes propiedades:

Todas las posiciones terminales están perdiendo.

Si un jugador puede moverse a una posición perdedora, entonces está


en una posición ganadora.

Si un jugador puede moverse solo a las posiciones ganadoras, entonces


está en una posición perdedora.

Estas propiedades podrı́an usarse para crear un algoritmo WL-Algoritmo


recursivo simple:

b o o l e a n isWinning ( p o s i t i o n pos ) {
moves [ ] =// P o s i b l e s p o s i c i o n e s a l a s que puedo moverme d e s d e
l a p o s i c i o n pos ;
f o r ( a l l x i n moves )
i f ( ! isWinning ( x ) )
return true ;
return f a l s e ;
}

n 0 1 2 3 4 5 6 7 8 9 10 11
posición L W L W W W W L W L W W

Este juego podrı́a jugarse también con una regla (generalmente llamada
la regla de juego misere) de que el jugador que quita la última moneda es de-
clarado perdedor. Solo necesita cambiar el comportamiento de las posiciones
de terminal en el algoritmo WL. La tabla cambiará a esto:
n 0 1 2 3 4 5 6 7 8 9 10 11
posición W L W L W W W W L W L W

328
TEORÍA DE JUEGO

Se puede ver que si una posición está ganando o perdiendo depende solo
de las últimas k posiciones, donde k es el número máximo de monedas que
podemos quitar. Si bien solo hay 2k valores posibles para las secuencias de
la longitud k, nuestra secuencia se volverá periódica.

12.2. El juego del Nim


El juego matemático más famoso es probablemente el Juego de Nim. Este
es el juego que probablemente encontrarás más veces y hay muchas variacio-
nes en él, ası́ como juegos que se pueden resolver utilizando el conocimiento
de cómo jugar el juego. Aunque estos problemas a menudo requieren una
idea inteligente, generalmente son muy fáciles de codificar.
El juego clásico plantea que: Tenemos una pila con n piedras, y los juga-
dores pueden tomar de dicha pila, desde 1 hasta n piedras. El jugador que
en su turno, no pueda retirar más piedras del bulto, se considera perdedor.
Está claro que siempre gana el jugador A, retirando todas las piedras, es
decir, el jugador B alcanza un estado terminal que es un bulto con 0 piedras,
por lo que podemos definir que la posición p = 0 es perdedora y que cualquier
posición p ¿0 es ganadora, porque podemos retirar todas las piedras y ganar.
Pero hagamos el juego más interesante, supongamos que cada jugador
puede remover una o dos piedras solamente. La posición p = 0 sigue siendo
una posición perdedora porque no podemos retirar ninguna piedra, fácilmente
podemos ver que las posiciones p = 1 y p = 2 son ganadoras, simplemente
retiramos todas las piedras. ¿Pero, qué sucederı́a si el bulto tiene 3 piedras?
De la posición p= 3 nos podemos mover a p = 2 retirando una piedra, y
a p = 1 retirando dos piedras, ambas posiciones ganadoras para el próximo
jugador a jugar, por lo tanto, p = 3 es perdedora.
Y si el bulto tuviera 4 piedras, ¿Cuántas piedras se podrı́an quitar?, ¿una
o dos?. Si quitamos dos alcanzamos la posición p= 2 que es ganadora para
el próximo jugador, y si quitamos una alcanzamos la posición p= 3 que es
perdedora para el próximo jugador, ası́ que la jugada óptima es quitar una
sola piedra.
Entonces ya podemos definir dos conceptos importantes:
Posición ganadora (G): Una posición es ganadora, si de esta podemos
movernos al menos a una posición perdedora.
Posición perdedora (P): Una posición es perdedora si de esta solo
podemos movernos a posiciones ganadoras. Todos los estados terminales son
posiciones perdedoras.
n 0 1 2 3 4 5 6 7 8 9
posición P G G P G G P G G P

329
TEORÍA DE JUEGO

Figura 12.1: Juego del Nim

En esta tabla se puede ver el patrón PGG, y llegar a la siguiente conclu-


sión:
Definiendo P(n) como la posición del juego en el estado n.
P(n) = P (perdedora) si n es divisible por 3.
P(n) = G (ganadora) si n no es divisible por 3.
Pero, ¿Qué sucederı́a si nos enfrentamos a un juego de Nim con varias
pilas de piedra?, ¿Qué son los números Grundy y cómo nos pueden ayudar
a ganar el juego de Nim?
Variante del juego de Nim: Hay n pilas de monedas. Cuando es el turno
de un jugador, él elige una pila y toma al menos una moneda de ella. Si
alguien no puede moverse, pierde (por lo tanto, el que saca la última moneda
es el ganador).
Sean n1 , n2 , ... nk , los tamaños de las pilas. Es una posición perdedora
para el jugador cuyo turno es si y solo si n1 xor n2 xor .. xor nk = 0.

¿Por qué funciona? Desde las posiciones perdedoras solo podemos pasar
a las ganadoras: Si xor de los tamaños de las pilas es 0, se cambiará después
de nuestro movimiento (al menos 1 se cambiará a 0, por lo que en esa columna
habrá un número impar de 1s).
Desde las posiciones ganadoras es posible pasar a al menos una perdedora:
Si xor de los tamaños de las pilas no es 0, podemos cambiarlo a 0 encontrando
la columna más a la izquierda donde el número de 1s es impar, cambiando
uno de ellos a 0 y luego cambiando 0s o 1s en el lado derecho de para ganar
un número par de 1s en cada columna.

12.3. Números Grundy


Los números Grundy son tratados en la teorı́a de juegos como los valores
de los bultos del juego de Nim. A lo largo del artı́culo aclararemos algunas
interrogantes como: ¿Dónde podemos aplicar los números Grundy?, ¿Para
qué problemas nos sirven?, entre otras preguntas.
Para empezar definiremos un concepto básico que llamaremos Teore-
ma de Sprague-Grundy. Este teorema dice que todo juego imparcial es
equivalente a un bulto de Nim de cierto tamaño.

330
TEORÍA DE JUEGO

A partir de esto podemos inferir que muchos de los juegos conocidos,


podemos expresarlos como un juego de Nim, y ası́ por medio de los números
Grundy darle solución.
Entonces quedan planteadas las siguientes interrogantes: ¿Qué sucede si
se presenta un juego de Nim con varios bultos de piedra? ¿Cómo calcular el
número Grundy de cierto estado n el cual definiremos por G(n)?
Sea y: Todas las posibles posiciones a las cuales nos podemos mover desde
n.
G(n) = min(x >= 0, x! = G(y))
Es decir, del conjunto de todos los números Grundy de las posiciones a
las que me puedo mover desde n, el número Grundy de n va a ser el menor
entero no negativo que no aparezca en dicho conjunto. Todas las posiciones
terminales tienen números Grundy 0.
Vamos a ver cómo calcular los números Grundy en dos variantes de Nim.

Variante Nim quitando desde 1 hasta n piedras


n 0 1 2 3 4 5 6 7 8 9
G(n) 0 1 2 3 4 5 6 7 8 9
Como la posición n = 0 es una posición terminal su valor grundy es G(0)
= 0. Desde n = 1 se puede mover hacia n = 0 con G(0) = 0, por tanto
aplicando la definición:
G(1) = min(x¿=0, x != G(0)). El menor número no negativo es 1, por
tanto G(1) = 1.
Para G(2) = min(x¿=0, x != G(0), x!=G(1)), que es x=2. Por tanto G(2)
= 2, y ası́ para los demás estados.
En este caso, no se tiene que ir iterando por cada posición a las que nos
podemos mover desde el momento inicial, ya que fácilmente se puede observar
que G(n) = n.

Variante de Nim quitando una o dos piedras solamente.


n 0 1 2 3 4 5 6 7 8 9
G(n) 0 1 2 0 1 2 0 1 2 0
G(0) = 0 ya que n = 0 es una posición terminal. Desde n = 1 nos podemos
mover hacia n=0 con G(0) = 0, por lo que G(1) = 1, y consecutivamente
queda que G(2) = 2.
Ahora, desde n=3 nos podemos mover hacia n=1 con G(1) = 1, y a n =
2 con G(2) = 2, entonces el menor número no negativo que sea distinto de
G(1) y G(2) es cero, por tanto G(3) = 0.

331
TEORÍA DE JUEGO

Viendo el patrón formado podemos llegar a la conclusión de que G(n) =


n %3.
Una vez que hemos visto los números Grundy de estos ejemplos, ¿Cómo
podemos usarlos? Pues si una posición tiene valor Grundy cero significa que
es una posición perdedora, y si una posición tiene un número Grundy mayor
que cero, significa que es ganadora.

12.4. Conclusión
No se preocupe si ve un problema de teorı́a de juego durante un concurso:
puede ser similar a uno de los juegos descritos anteriormente o puede reducirse
a uno de ellos. Si no, solo piénselo en ejemplos concretos. Una vez que lo
descubres, la parte de codificación suele ser muy simple y directa. Buena
suerte y diviertete.

12.5. Análisis de ejercicios


3699 - An interesting game El segundo jugador solo podrá ganar cuando
la cantidad de elementos disponibles al inicio es impar y distinto de uno. Esto
se debe a que con una cantidad impar de elementos cada jugador va hacer
la misma cantidad de jugadas. Si cada jugador hace la misma cantidad de
jugadas el segundo jugador solo tiene que sumar el número generado por la
jugada anterior del primer jugador mas uno de los números adyacentes a este.
Esto le garantiza en cada jugada sumar la misma catidad de puntos que el
primer jugador mas un adicional que hace que su acumulado sea mayor. El
primer jugador solo puede ganar cuando la cantidad de elemento al iniciar
el juego es par. Esto es porque con una cantidad par elementos la cantidad
de sumas es impar. Lo que significa que el primer jugador realiza una suma
mas que el segundo. Por lo que solo tiene que “perder un turno”sumando
cualquier par de elementos en su primera jugada. A partir de la segunda
jugada asume la estrategia ganadora del segundo jugador cuando la cantidad
de elementos es impar y sumar el número generado por la jugada anterior del
segundo jugador por uno de sus adyacentes. Es empate cuando la cantidad
de elementos inicial es 1.

2585 - Cats and Rabbits El problema plantea un juego donde si hace-


mos una analogı́a del juego Nim con mútiples pilas con difeentes cantidades
de piedra y donde cada jugador puede tomar una piedra o cualquier otra
cantidad de una misma pila podemos resolverlo. Para hacer la analogı́a ha-

332
TEORÍA DE JUEGO

remos lo siguiente. La cantidad de pilas va ser igual a la cantidad de tramos


de subcadenas conformadas por el sı́mbolo ’.’ y encerradas y delimitada en
al menos en sus extremos por el sı́mbolo ]. La cantidad de elementos en cada
pila es igual a la cantidad de elementos del sı́mbolo ’.’ que conforman esa
subcadena.
Hay que validar que la cadena inicial tenga el sı́mbolo ] sino tiene gana
Rabbit porque Cat no tiene forma de empezar.

3608 - Chocolate Game Para resolver este problema nos podemos per-
catar que para una barra de chocolate de dimensiones AxB el resultado es
el mismo que BxA. Bien si simulamos para pequeños casos el juegos nos
podemos percatar que para barra de chocolates cuya dimensiones ancho y
largo sean iguales el jugador ganador es siempre es 2 en caso que lo anterior
no se cumpla el jugador ganador es 1.

333
TEORÍA DE JUEGO

334
Capı́tulo 13

Estructura de datos

Una vez que se ha elegido el algoritmo, la implementación puede hacerse


usando las estructuras más simples, comunes en casi todos los lenguajes de
programación: escalares, arreglos y matrices. Sin embargo algunos problemas
se pueden plantear en forma más simple o eficiente en términos de estruc-
turas informáticas más complejas, como listas, pilas, colas, árboles, grafos,
conjuntos. Por ejemplo, el TSP1 se plantea naturalmente en términos de un
grafo donde los vértices son las ciudades y las aristas los caminos que van
de una ciudad a otra. Estas estructuras están incorporadas en muchos len-
guajes de programación o bien pueden obtenerse de librerı́as. El uso de estas
estructuras tiene una serie de ventajas.

Se ahorra tiempo de programación ya que no es necesario codificar.

Estas implementaciones suelen ser eficientes y robustas.

Se separan dos capas de código bien diferentes, por una parte el algo-
ritmo que escribe el programador, y por otro las rutinas de acceso a las
diferentes estructuras.

Existen estimaciones bastante uniformes de los tiempos de ejecución de


las diferentes operaciones.

Las funciones asociadas a cada estructura son relativamente indepen-


dientes del lenguaje o la implementación en particular. Ası́, una vez
que se plantea un algoritmo en términos de operaciones sobre una tal
estructura es fácil implementarlo en una variedad de lenguajes con una
performance similar.
1
“Problema del Agente Viajero”(TSP, por “Traveling Salesman Problem”) el cual con-
siste en encontrar el orden en que se debe recorrer un cierto número de ciudades.

335
ESTRUCTURA DE DATOS

13.1. Arreglos
En programación, un arreglo (llamados en inglés array) es una zona de
almacenamiento continuo, que contiene una serie de elementos del mismo
tipo.
Esta estructura de dato son adecuadas para situaciones en las que el ac-
ceso a los datos se realice de forma aleatoria e impredecible. Por el contrario,
si los elementos pueden estar ordenados y se va a utilizar acceso secuencial
serı́a más adecuado utilizar una lista, ya que esta estructura puede cambiar
de tamaño fácilmente durante la ejecución de un programa.
Todo arreglo se compone de un determinado número de elementos. Cada
elemento es referenciado por la posición que ocupa dentro del vector. Dichas
posiciones son llamadas ı́ndice y siempre son correlativos. Existen tres formas
de indexar los elementos de un arreglo:

Indexación base-cero (0): En este modo el primer elemento del vector


será la componente cero (‘0’) del mismo, es decir, tendrá el ı́ndice ‘0’.
En consecuencia, si el vector tiene ‘n’componentes la última tendrá
como ı́ndice el valor ‘n-1’. La mayorı́a de los lenguajes de programación
asumen esta forma de indexar los elementos de un arreglo.

Indexación base-uno (1): En esta forma de indexación, el primer elemen-


to de la elemento tiene el ı́ndice ‘1’y el último tiene el ı́ndice ‘n’(para
un arreglo de ‘n’ componentes). En varios problemas es conveniente
utilizar este en ves de usar la indexación que propone los lenguajes de
programación. El detalle con este proceder es que a la hora de definir el
tamaño del arreglo no pude ser de n sino de n+<una cantidad mayor
que cero>.

Indexación base-n (n): Este es un modo versátil de indexación en la


que el ı́ndice del primer elemento puede ser elegido libremente, en al-
gunos lenguajes de programación se permite que los ı́ndices puedan
ser negativos e incluso de cualquier tipo escalar (también cadenas de
caracteres).

La forma de acceder a los elementos de un arreglo es directa; esto sig-


nifica que el elemento deseado es obtenido a partir de su ı́ndice y no hay
que ir buscándolo elemento por elemento (en contraposición, en el caso de
una lista, para llegar, por ejemplo, al tercer elemento hay que acceder a los
dos anteriores o almacenar un apuntador o puntero que permita acceder de
manera rápida a ese elemento).

336
ESTRUCTURA DE DATOS

13.1.1. C++
En lenguaje de programación C++ el arreglo se puede declarar según la
situación. Ahora veremos cada una de ellas

/∗ Sabemos l a c a n t i d a d de e l e m e n t o s a p r i o r i ,
<t i p o d e da to > <n o m b r e a r r e g l o > [< c a n t i d a d > ] ; ∗/
bool i s P i m e s [ 1 0 0 ] ;

/∗La c a n t i d a d de e l e m e n t o s puede v a r i a r y depende d e l


v a l o r de una v a r i a b l e . Es l a mas usada
<t i p o d e da to > ∗ <n o m b r e a r r e g l o >= new <t i p o d e da to > [<
variable >];
<t i p o d e da to > ∗ <n o m b r e a r r e g l o >= new <t i p o d e da to > [<
cantidad >];
∗/
int cantidadMaxima =100;
int ∗ n o t a s ;
n o t a s=new int [ cantidadMaxima ] ;
double ∗ promedio=new double [ 1 0 0 ] ;

/∗Cuando conocemos l o s v a l o r e s que i n t e g r a n e l a r r e g l o ∗/


s t r i n g nombres [ ] = { "Luis" , " Ernesto " , " Susana " } ;

/∗ Para a c c e d e r o m o d i f i c a r a l g u n e l e m e n t o d e l a r r e g l o l o
hacemos de
de l a s i g u i e n t e manera ∗/
int primeraNota=n o t a s [ 0 ] ;
n o t a s [ 2 3 ] = cantidadMaxima ;
n o t a s [ cantidadMaxima −1]=cantidadMaxima ;
int ultimaNota=n o t a s [ cantidadMaxima − 1 ] ;

13.1.2. Java
Los arreglos de Java se tratan como objetos de una clase predefinida.
Los arrays son objetos, pero con algunas caracterı́sticas propias. Los arreglos
pueden ser asignados a objetos de la clase Object y los métodos de Object
pueden ser utilizados con arreglos.

/∗ D e c l a r a c i o n de un a r r e g l o . Se i n i c i a l i z a a n u l l ∗/
int [ ] v e c t o r ;

/∗ a r r e g l o de 10 e n t e r o s , i n i c i a l i z a d o s a 0 ∗/
v e c t o r = new int [ 1 0 ] ;

/∗ D e c l a r a c i o n e i n i c i a l i z a c i o n de un a r r e g l o de 3 e l e m e n t o s

337
ESTRUCTURA DE DATOS

con l o s v a l o r e s e n t r e l l a v e s ∗/
double [ ] v = { 1 . 0 , 2 . 6 5 , 3 . 1 } ;

/∗ Se c r e a un a r r e g l o de 5 r e f e r e n c i a s a o b j e t o s
Las 5 r e f e r e n c i a s son i n i c i a l i z a d a s a n u l l ∗/
MyClass [ ] l i s t a =new MyClass [ 5 ] ;

// Se a s i g n a a l i s t a [ 0 ] e l mismo v a l o r que unaRef


l i s t a [ 0 ] = unaRef ;

/∗
Se a s i g n a a l i s t a [ 1 ] l a r e f e r e n c i a a l nuevo o b j e t o
El r e s t o ( l i s t a [ 2 ] . . . l i s t a [ 4 ] s i g u e n con v a l o r n u l l
∗/
l i s t a [ 1 ] = new MyClass ( ) ;

13.2. Matrices
Una matriz es un arreglo de areglos fila, o más en concreto un arreglo de
referencias a los arreglos fila. Con este esquema, cada fila podrı́a tener un
número de elementos diferente.

13.2.1. C++
Los arreglos bidimensionales o matrices en C++ se pude declarar similar
a como se hace un arreglo unidimensional.

/∗ Se c o n o c e de antemano l a s d i m e n s i o n e s e s t a manera e s
e s t a t i c a s e recomienda que s e a dinamica ∗/
int mat [ 3 ] [ 4 ] ;

/∗De forma dinamica ∗/


int ∗∗ mat ;
mat = new int ∗ [ ncolumns ] ;
f o r ( int i =0; i <ncolumns ; i ++)
mat [ i ]=new int [ n f i l a s ] ;

/∗Con l o s v a l o r e s c o n o c i d o s ∗/
double c a r r o t s [ 3 ] [ 4 ] { { 2 . 5 , 3 . 2 , 3 . 7 , 4 . 1 } , // p r i m e r a f i l a
{ 4 . 1 , 3 . 9 , 1 . 6 , 3 . 5 } , // segunda f i l a
{ 2 . 8 , 2 . 3 , 0 . 9 , 1 . 1 } // t e r c e r a f i l a
};

338
ESTRUCTURA DE DATOS

13.2.2. Java
Los arrays bidimensionales de Java se crean de un modo muy similar al
de C++ (con reserva dinámica de memoria). En Java una matriz se puede
crear directamente en la forma,

int [ ] [ ] mat = new int [ 3 ] [ 4 ] ;

o bien se puede crear de modo dinámico dando los siguientes pasos:

1. Crear la referencia indicando con un doble corchete que es una referen-


cia a matriz,

int [ ] [ ] mat ;

2. Crear el vector de referencias a las filas,

mat = new int [ n f i l a s ] [ ] ;

3. Reservar memoria para los vectores correspondientes a las filas,

f o r ( int i =0; i <n f i l a s ; i ++) ;


mat [ i ] = new int [ n c o l s ] ;

A continuación se presentan algunos ejemplos de creación de arrays bidi-


mensionales:

// c r e a r una m a t r i z 3 x3
// s e i n i c i a l i z a n a c e r o
double mat [ ] [ ] = new double [ 3 ] [ 3 ] ;
int [ ] [ ] b = { { 1 , 2 , 3 } ,
{ 4 , 5 , 6 } , // e s t a coma e s p e r m i t i d a
};
int c = new [ 3 ] [ ] ; // s e c r e a e l a r r a y de r e f e r e n c i a s a a r r a y s
c [ 0 ] = new int [ 5 ] ;
c [ 1 ] = new int [ 4 ] ;
c [ 2 ] = new int [ 8 ] ;

13.3. Lista
Las listas constituyen una de las estructuras lineales más flexibles, por-
que pueden crecer y acortarse según se requiera, insertando o suprimiendo

339
ESTRUCTURA DE DATOS

elementos tanto en los extremos como en cualquier otra posición de la lista.


Por supuesto esto también puede hacerse con vectores, pero en las implemen-
taciones más comunes estas operaciones son O(n) para los vectores, mientras
que son O(1) para las listas.
Es una de las estructuras de datos fundamentales, y puede ser usada para
implementar otras estructuras de datos. Consiste en una secuencia de nodos,
en los que se guardan campos de datos arbitrarios y una o dos referencias,
enlaces o punteros al nodo anterior o posterior. El principal beneficio de las
listas enlazadas respecto a los vectores convencionales es que el orden de los
elementos enlazados puede ser diferente al orden de almacenamiento en la
memoria o el disco, permitiendo que el orden de recorrido de la lista sea
diferente al de almacenamiento.
Una lista enlazada es un tipo de dato autorreferenciado porque contie-
nen un puntero o enlace (en inglés link, del mismo significado) a otro dato
del mismo tipo. Las listas enlazadas permiten inserciones y eliminación de
nodos en cualquier punto de la lista en tiempo constante (suponiendo que
dicho punto está previamente identificado o localizado), pero no permiten un
acceso aleatorio. Existen diferentes tipos de listas enlazadas: listas enlaza-
das simples, listas doblemente enlazadas, listas enlazadas circulares y listas
enlazadas doblemente circulares.
Comparado con otros contenedores de secuencia estándar base (matriz,
vector y deque), las listas funcionan mejor en insertar, extraer y mover ele-
mentos en cualquier posición dentro del contenedor para el que ya se ha
obtenido un iterador, y por lo tanto también en algoritmos que hacen un uso
intensivo de estos, como los algoritmos de clasificación.
El principal inconveniente de las listas y las listas directas en comparación
con estos otros contenedores de secuencia es que carecen de acceso directo a
los elementos por su posición; Por ejemplo, para acceder al sexto elemento en
una lista, uno tiene que iterar desde una posición conocida (como el principio
o el final) hasta esa posición, que toma tiempo lineal en la distancia entre es-
tos. También consumen algo de memoria extra para mantener la información
de vinculación asociada a cada elemento (que puede ser un factor importante
para grandes listas de elementos de pequeño tamaño).

13.3.1. C++
En el caso de C++ propone tres variantes de estructura de datos de
tipo dato lista las cuales varı́an entre sı́ por su modelo de implementación.
A continuación veremos cada una de las variantes. De forma general cada
estructura tienen definidas las operaciones para manipular y gestionar los
datos almacenados.

340
ESTRUCTURA DE DATOS

Vector

Es un modelo basado en un modelo de arreglo dinámico que permite el


trabajo y gestión con los elementos almacenados en un arreglo dinámico.
Pero a diferencia de los arreglos regulares, el almacenamiento en vectores se
maneja automáticamente, lo que permite que se amplı́e y se contrate según
sea necesario. Para utilizar un vector en C++ se debe incluir en la cabecera
del archivo el siguiente fragmento:

#i n c l u d e <v e c t o r >

Para declarar un vector solo basta con poner algo como lo que sigue:

v e c t o r <T> n o m b r e d e l v e c t o r ;

Donde T debe ser uno de los tipos de datos definidos por el lenguaje de
progrmación o por el propio programador. El vector es muy útil cuando se va
acceder a los elementos conocidos su posición dentro de la estructura y se va
añadir o eliminar elementos de última posición. No presenta igual desempeño
cuando las inserciones y eliminaciones se producen en otras posiciones. Una
forma muy eficiente de utilizar el vector es una vez creado inicializarlo con
la cantidad máxima de elementos que puede alamcenar siempre y cuando se
conozca este valor.

#include <i o s t r e a m >


#include <v e c t o r >
using namespace s t d ;

int main ( )
{
unsigned int i ;

// v a r i a n t e s para c o n s t u i r un v e c t o r :
// un v e c t o r de e n t e r o s v a c i o s
v e c t o r <int> f i r s t ;

/∗un v e c t o r con c u a t r o e l e m e n t o s , cada e l e m e n t o s con e l v a l o r


100 ∗/
v e c t o r <int> s e c o n d ( 4 , 1 0 0 ) ;

/∗ c r e a n d o un v e c t o r i t e r a n d o s o b r e o t r o ∗/
v e c t o r <int> t h i r d ( s e c o n d . b e g i n ( ) , s e c o n d . end ( ) ) ;

/∗ c r e a n d o un v e c t o r c o p i a d e l t e r c e r v e c t o r ∗/
v e c t o r <int> f o u r t h ( t h i r d ) ;

341
ESTRUCTURA DE DATOS

/∗ c r e a n d o un v e c t o r a p a r t i r de un v e c t o r ∗/
int myints [ ] = { 1 6 , 2 , 7 7 , 2 9 } ;
v e c t o r <int> f i f t h ( myints , myints + s i z e o f ( myints ) / s i z e o f (
int ) ) ;

/∗ a d i c i o n a n d o un e l e m e n t o a l v e c t o r ∗/
f i r s t . push back ( 2 ) ;

/∗ l i m p i a r un v e c t o r ∗/
second . c l e a r ( ) ;

/∗ s a b e r s i un v e c t o r no t i e n e e l e m e n t o ∗/
i f ( f o u r t h . empty ( ) )
cout<<" Vector empty "<<e n d l ;
else
cout<<" Vector not empty "<<e n d l ;

c o u t << "The contents of fifth are:" ;


f o r ( i =0; i < f i f t h . s i z e ( ) ; i ++)
c o u t << " " << f i f t h [ i ] ;

c o u t << e n d l ;

return 0 ;
}

Deque
Deque (generalmente pronunciado como deck) es un acrónimo irregular de
doble cola. Las colas de doble final son una clase de contenedores de secuencia.
Como tales, sus elementos están ordenados siguiendo una secuencia lineal
estricta.
Las bibliotecas especı́ficas pueden implementar Deques de diferentes ma-
neras, pero en todos los casos permiten que se pueda acceder a los elementos
individuales a través de iteradores de acceso aleatorio, con el almacenamiento
siempre manejado automáticamente (expandiéndose y contrayéndose según
sea necesario).
Las secuencias de Deque tienen las siguientes propiedades:

Se puede acceder a los elementos individuales por su ı́ndice de posición.

La iteración sobre los elementos se puede realizar en cualquier orden.

Los elementos se pueden agregar y eliminar de manera eficiente desde


cualquiera de sus extremos (ya sea al principio o al final de la secuencia).

342
ESTRUCTURA DE DATOS

Por lo tanto, proporcionan una funcionalidad similar a la proporcionada


por los vectores, pero con una inserción y eliminación de elementos eficientes
también al principio de la secuencia y no solo al final. En el lado negativo,
a diferencia de los vectores, no se garantiza que los deques tengan todos
sus elementos en ubicaciones de almacenamiento contiguas, eliminando ası́
la posibilidad de un acceso seguro a través de la aritmética de punteros.
Tanto los vectores como los deques proporcionan una interfaz muy si-
milar y se pueden usar para propósitos similares, pero internamente ambos
funcionan de formas bastante diferentes: mientras que los vectores son muy
similares a una matriz simple que crece al reasignar todos sus elementos en
un bloque único cuando se la capacidad se ha agotado, los elementos de un
deques se pueden dividir en varios trozos de almacenamiento, con la clase
manteniendo toda esta información y proporcionando un acceso uniforme a
los elementos. Por lo tanto, los deques son un poco más complejos interna-
mente, pero esto generalmente les permite crecer más eficientemente que los
vectores con su capacidad administrada automáticamente, especialmente en
secuencias grandes, porque se evitan las reasignaciones masivas.
Para las operaciones que involucran la inserción frecuente o la eliminación
de elementos en posiciones distintas al principio o al final, los deques tienen
peor desempeño y tienen iteradores y referencias menos consistentes que las
listas.

#include <i o s t r e a m >


#include <deque>
using namespace s t d ;

int main ( )
{
unsigned int i ;

// v a r i a n t e s para c o n s t u i r un deque :
// un deque de e n t e r o s v a c i o s
deque<int> f i r s t ;

/∗un deque con c u a t r o e l e m e n t o s , cada e l e m e n t o s con e l v a l o r 100


∗/
deque<int> s e c o n d ( 4 , 1 0 0 ) ;

/∗ c r e a n d o un deque i t e r a n d o s o b r e o t r o ∗/
deque<int> t h i r d ( s e c o n d . b e g i n ( ) , s e c o n d . end ( ) ) ;

/∗ c r e a n d o un deque c o p i a d e l t e r c e r v e c t o r ∗/
deque<int> f o u r t h ( t h i r d ) ;

/∗ c r e a n d o un deque a p a r t i r de un a r r e g l o ∗/

343
ESTRUCTURA DE DATOS

int myints [ ] = { 1 6 , 2 , 7 7 , 2 9 } ;
deque<int> f i f t h ( myints , myints + s i z e o f ( myints ) / s i z e o f ( int )
);

/∗ a d i c i o n a n d o un e l e m e n t o a l deque ∗/
f i r s t . push back ( 2 ) ;

/∗ l i m p i a r un deque ∗/
second . c l e a r ( ) ;

/∗ s a b e r s i un deque no t i e n e e l e m e n t o ∗/
i f ( f o u r t h . empty ( ) )
cout<<" deque empty "<<e n d l ;
else
cout<<" deque not empty "<<e n d l ;

c o u t << "The contents of fifth are:" ;


f o r ( i =0; i < f i f t h . s i z e ( ) ; i ++)
c o u t << " " << f i f t h [ i ] ;

c o u t << e n d l ;

return 0 ;
}

List

Las listas son una clase de contenedores de secuencia. Como tales, sus
elementos están ordenados siguiendo una secuencia lineal.
Los contenedores de listas se implementan como listas doblemente enla-
zadas; Las listas doblemente vinculadas pueden almacenar cada uno de los
elementos que contienen en ubicaciones de almacenamiento diferentes y no
relacionadas. El orden se mantiene por la asociación a cada elemento de un
enlace al elemento que lo precede y un enlace al elemento que lo sigue.
Esto proporciona las siguientes ventajas para listar contenedores:

Inserción y eliminación eficiente de elementos en cualquier parte del


contenedor (tiempo constante).

Elementos en movimiento eficientes y bloque de elementos dentro del


contenedor o incluso entre diferentes contenedores (tiempo constante).

Iterando sobre los elementos en orden directo o inverso (tiempo lineal).

344
ESTRUCTURA DE DATOS

En comparación con otros contenedores de secuencia estándar base (vecto-


res y deques), las listas generalmente funcionan mejor en la inserción, extrac-
ción y movimiento de elementos en cualquier posición dentro del contenedor
y, por lo tanto, también en algoritmos que hacen un uso intensivo de estos,
como los algoritmos de clasificación.
El principal inconveniente de las listas en comparación con estos otros
contenedores de secuencias es que carecen de acceso directo a los elementos
por su posición; Por ejemplo, para acceder al sexto elemento de una lista, hay
que iterar desde una posición conocida (como el principio o el final) hasta esa
posición, lo que requiere un tiempo lineal en la distancia entre estos. También
consumen algo de memoria adicional para mantener la información de enlace
asociada a cada elemento (lo que puede ser un factor importante para listas
grandes de elementos pequeños).

#include <i o s t r e a m >


#include < l i s t >
using namespace s t d ;

int main ( )
{
l i s t <int> f i r s t ;
l i s t <int> s e c o n d ( 4 , 1 0 0 ) ;
l i s t <int> t h i r d ( s e c o n d . b e g i n ( ) , s e c o n d . end ( ) ) ;
l i s t <int> f o u r t h ( t h i r d ) ;

int myints [ ] = { 1 6 , 2 , 7 7 , 2 9 } ;
l i s t <int> f i f t h ( myints , myints+s i z e o f ( myints ) / s i z e o f ( int ) ) ;

c o u t << "The contents of fifth are: " ;


f o r ( l i s t <int > : : i t e r a t o r i t= f i f t h . b e g i n ( ) ; i t != f i f t h . end ( ) ; i t ++)
cout <<∗i t << " " ;

c o u t << e n d l ;

return 0 ;
}

13.3.2. Java
La interface List define métodos para operar con colecciones ordenadas y
que pueden tener elementos repetidos. Por ello, dicha interface declara méto-
dos adicionales que tienen que ver con el orden y con el acceso a elementos
o rangos de elementos. Además de los métodos de Collection, la interface
List declara los métodos siguientes:

345
ESTRUCTURA DE DATOS

public i n t e r f a c e j a v a . u t i l . L i s t extends j a v a . u t i l . C o l l e c t i o n
{
public abstract void add ( j a v a . l a n g . Object ) ;
public abstract void add ( int , j a v a . l a n g . Object ) ;
public abstract boolean addAll ( int , j a v a . u t i l . C o l l e c t i o n ) ;
public abstract boolean addAll ( j a v a . u t i l . C o l l e c t i o n ) ;
public abstract j a v a . l a n g . Object g e t ( int ) ;
public abstract int indexOf ( j a v a . l a n g . Object ) ;
public abstract int l a s t I n d e x O f ( j a v a . l a n g . Object ) ;
public abstract j a v a . u t i l . L i s t I t e r a t o r l i s t I t e r a t o r ( ) ;
public abstract j a v a . u t i l . L i s t I t e r a t o r l i s t I t e r a t o r ( int ) ;
public abstract j a v a . l a n g . Object remove ( int ) ;
public abstract j a v a . l a n g . Object s e t ( int , j a v a . l a n g . Object ) ;
public abstract j a v a . u t i l . L i s t s u b L i s t ( int , int ) ;
}

Los nuevos métodos add() y addAll() tienen un argumento adicional


para insertar elementos en una posición determinada, desplazando el elemen-
to que estaba en esa posición y los siguientes. Los métodos get() y set()
permiten obtener y cambiar el elemento en una posición dada. Los métodos
indexOf() y lastIndexOf() permiten saber la posición de la primera o la
última vez que un elemento aparece en la lista; si el elemento no se encuentra
se devuelve –1.
El método subList(int fromIndex, int toIndex) devuelve una vista
de la lista, desde el elemento fromIndex inclusive hasta el toIndex exclusive.
Un cambio en esta vista se refleja en la lista original, aunque no conviene hacer
cambios simultáneamente en ambas. Lo mejor es eliminar la vista cuando ya
no se necesita.
Existen dos implementaciones de la interface List, que son las clases
ArrayList y LinkedList. La diferencia está en que la primera almacena los
elementos de la colección en un array de Objects, mientras que la segunda
los almacena en una lista vinculada. Los arrays proporcionan una forma de
acceder a los elementos mucho más eficiente que las listas vinculadas. Sin
embargo tienen dificultades para crecer (hay que reservar memoria nueva,
copiar los elementos del array antiguo y liberar la memoria) y para insertar
y/o borrar elementos (hay que desplazar en un sentido u en otro los elementos
que están detrás del elemento borrado o insertado). Las listas vinculadas sólo
permiten acceso secuencial, pero tienen una gran flexibilidad para crecer, para
borrar y para insertar elementos. El optar por una implementación u otra
depende del caso concreto de que se trate.
Para hacer uso de estas estructuras basta con:
Incluirlas en el fichero solución según sea el caso.

346
ESTRUCTURA DE DATOS

import j a v a . u t i l . L i s t ;
import j a v a . u t i l . A r r a y L i s t ;
import j a v a . u t i l . L i n k e d L i s t ;

Declararlas.

L i s t <I n t e g e r > l 1=new A r r a y L i s t <I n t e g e r >() ;


L i s t <I n t e g e r > l 2=new L i n k e d L i s t <I n t e g e r >() ;
A r r a y L i s t <I n t e g e r > l 3=new A r r a y L i s t <I n t e g e r >() ;
L i n k e d L i s t <I n t e g e r > l 4=new L i n k e d L i s t <I n t e g e r >() ;

Para utilizarlas solo basta con la siguiente sintaxis <nombre lista>.<operación>(<paráme-


tros de la operación>). Por ejemplo:

l 1 . Add ( 1 0 1 ) ;

13.4. Pila
Básicamente es una lista en la cual todas las operaciones de inserción y
borrado se producen en uno de los extremos de la lista. Un ejemplo gráfico
es una pila de libros en un cajón. A medida que vamos recibiendo más libros
los ubicamos en la parte superior. En todo momento tenemos acceso sólo
al libro que se encuentra sobre el “tope”de la pila. Si queremos acceder a
algún libro que se encuentra más abajo (digamos en la quinta posición desde
el tope) debemos sacar los primeros cuatro libros y ponerlos en algún lugar
para poder acceder al mismo. La Pila es el tı́pico ejemplo de la estructura tipo
“LIFO”(por “Last In First Out”, es decir “el último en entrar es el primero
en salir”).
Una pila (stack en inglés) es una lista ordenada o estructura de datos que
permite almacenar y recuperar datos. Esta estructura se aplica en multitud
de ocasiones en el área de informática debido a su simplicidad y ordenación
implı́cita de la propia estructura. Para el manejo de los datos se cuenta con
dos operaciones básicas: apilar (push), que coloca un objeto en la pila, y su
operación inversa, retirar (o desapilar, pop), que retira el último elemento
apilado.
En cada momento sólo se tiene acceso a la parte superior de la pila, es
decir, al último objeto apilado (denominado TOS, Top of Stack en inglés).
La operación retirar permite la obtención de este elemento, que es retirado
de la pila permitiendo el acceso al siguiente (apilado con anterioridad), que
pasa a ser el nuevo TOS.

347
ESTRUCTURA DE DATOS

Por analogı́a con objetos cotidianos, una operación apilar equivaldrı́a a


colocar un plato sobre una pila de platos, y una operación retirar a retirarlo.
Las pilas suelen emplearse en los siguientes contextos:

Evaluación de expresiones en notación postfija (notación polaca inver-


sa).

Reconocedores sintácticos de lenguajes independientes del contexto

Implementación de recursividad.

13.4.1. C++
13.4.2. Java
En java para se uso de la pila debemos hacer uso de la clase Stack la
cual extiende de la clase Vector. Para incluirla en nuestra solución debemos
primero importarla del paquete java en el subpaquete util

import j a v a . u t i l . Stack ;

Luego solo debemos crear una instancia de esta clase y tenemos una pila
lista para ser usada en nuestra solución.

Stack< <Tipo de dato> > p i l a =new Stack< <Tipo dedato> > ( ) ;

Por supuesto la expresión <Tipo de dato> se sustituye en la sentencia


anterior por el tipo de dato que va almacenar la pila, por ejemplo a conti-
nuación una pila para almacenar cadena de caracteres.

Stack<S t r i n g > p i l a =new Stack<S t r i n g > ( ) ;

Los principales fucionalidades que posee la clase Stack son:

push: Adiciona al tope de la pila el elemento pasado por parámetro

p i l a . push ( " Matanzas " ) ;

pop: Elimina y devuelve el elemento que esta en el tope de la pila


siempre que esta tenga elemento, en caso de estar vacia se lanza una
excepción.

S t r i n g t o p e=p i l a . pop ( ) ;

348
ESTRUCTURA DE DATOS

peek: Devuelve el elemento sin eleiminarlo que esta en el tope de la


pila siempre que esta tenga elemento, en caso de estar vacia se lanza
una excepción.

S t r i n g t o p e=p i l a . peek ( ) ;

empty: Comprueba si la pila esta vacia. Devuelve verdadero si la pila


esta vacia y falso en caso contrario.

boolean isEmpty=p i l a . empty ( ) ;

search: Busca un elemento de la pila y devuelve la posición con res-


pecto al tope de la pila donde se encuentra la primera ocurrencia del
elemento. En caso que el elemento fuera el tope de la pila el valor de-
vuelto serı́a 1 y ası́ sucesivamente se irı́a incrementando a medida que
se alejara del tope. En caso que elemento no este el valor devuelto será
-1.

int p o s i t i o n=p i l a . s e a r c h ( " Matanzas " ) ;

13.5. Cola
Por contraposición con la pila, la cola es un contenedor de tipo “FI-
FO”(por “First In First Out”, el primero en entrar es el primero en salir). El
ejemplo clásico es la cola de la caja en el supermercado. La cola es un objeto
muchas veces usado como buffer o pulmón, es decir un contenedor donde
almacenar una serie de objetos que deben ser procesados, manteniendo el
orden en el que ingresaron. La cola es también, como la pila, un subtipo de
la lista llama también a ser implementado como un adaptador.

Colas circulares (anillos): en las que el último elemento y el primero


están unidos.

Colas de prioridad: En ellas, los elementos se atienden en el orden


indicado por una prioridad asociada a cada uno. Si varios elementos
tienen la misma prioridad, se atenderán de modo convencional según
la posición que ocupen.

Bicolas: son colas en donde los nodos se pueden añadir y quitar por
ambos extremos; se les llama DEQUE (Double Ended QUEue). Para

349
ESTRUCTURA DE DATOS

representar las bicolas lo podemos hacer con un array circular con Inicio
y Fin que apunten a cada uno de los extremos. Hay variantes:

1. Bicolas de entrada restringida: Son aquellas donde la inserción sólo


se hace por el final, aunque podemos eliminar al inicio ó al final.
2. Bicolas de salida restringida: Son aquellas donde sólo se elimina
por el final, aunque se puede insertar al inicio y al final.

13.5.1. C++

13.5.2. Java

13.6. Cola con prioridad


Una cola de prioridades es una estructura de datos en la que los elementos
se atienden en el orden indicado por una prioridad asociada a cada uno. Si
varios elementos tienen la misma prioridad, se atenderán de modo conven-
cional según la posición que ocupen. Este tipo especial de colas tienen las
mismas operaciones que las colas , pero con la condición de que los elemen-
tos se atienden en orden de prioridad. Ejemplos de la vida diaria serı́an la
sala de urgencias de un hospital, ya que los enfermos se van atendiendo en
función de la gravedad de su enfermedad. Entendiendo la prioridad como un
valor numérico y asignando a altas prioridades valores pequeños, las colas de
prioridad nos permiten añadir elementos en cualquier orden y recuperarlos
de menor a mayor. Hay 2 formas de implementación. La primera añadir un
campo a cada nodo con su prioridad. Resulta conveniente mantener la cola
ordenada por orden de prioridad. La segunda crear tantas colas como priori-
dades haya, y almacenar cada elemento en su cola. De igual manera las colas
con prioridad se pueden clasificar de acuerdo la prioridad que se le otorga al
elemento.

Colas de prioridades con ordenamiento ascendente: en ellas los elemen-


tos se insertan de forma arbitraria, pero a la hora de extraerlos, se
extrae el elemento de menor prioridad.

Colas de prioridades con ordenamiento descendente: son iguales que


la colas de prioridad con ordenamiento ascendente, pero al extraer el
elemento se extrae el de mayor prioridad.

350
ESTRUCTURA DE DATOS

13.6.1. C++
13.6.2. Java

13.7. Cola doblemente terminada


Una cola doblemente terminada o deque (del inglés double ended queue)
es una estructura de datos lineal que permite insertar y eliminar elementos
por ambos extremos, podrı́a verse como un mecanismo que permite aunar
en una única estructura las funcionalidades de las pilas (estructuras LIFO)
y las colas (estructuras FIFO), en otras palabras, estas estructuras (pilas y
colas) podrı́an implementarse fácilmente con una deque.
Hay al menos dos formas eficientes de implementar una cola doblemente
terminada: Con un vector dinámico modificado o con una lista doblemente
enlazada.

Implementación con vector dinámico


La cola doblemente terminada se puede implementar utilizando una va-
riante del vector dinámico que pueda crecer por ambos extremos. Este vector
tiene todas las propiedades de un vector dinámico, como el acceso en tiempo
constante a cualquiera de sus elementos, buena identificación de referencias,
y una ineficiente forma de insertar o eliminar elementos por en medio de la
estructura. A estas caracterı́sticas se añade la de que el tiempo de inserción
y borrado de elementos en los dos extremos de la estructura es constante (en
vez de sólo uno de los extremos). Esta implementación requiere:
Almacenar los elementos de la cola doblemente terminada en un buffer
circular, este sólo se debe redimensionar cuando se encuentre comple-
tamente lleno, de este modo se reduce la frecuencia de redimensiona-
mientos. Este sistema requiere de un mecanismo de indexación más
elaborado.
Asignar los contenidos de la pila desde el centro del vector subyacente
y redimensionarlo cuando se llegue a cualquiera de los extremos. Esta
aproximación también requiere redimensionamientos muy frecuentes y
genera residuos de espacio en la memoria, particularmente cuando sólo
se están insertando elementos por un sólo extremo.

Soporte
La Librerı́a Estándar de Plantillas de C++ proporciona las clases genéri-
cas std::deque y std::list, para las implemteaciones con vector dinámico y

351
ESTRUCTURA DE DATOS

lista enlazada respectivamente.


El Collections Framework de Java incluye una nueva interfaz Deque que
proporciona la funcionalidad para insertar y eliminar en ambos extremos.
Está implementada por clases como ArrayDeque y LinkedList, las imple-
mentaciones con array dinámico y lista enlazada respectivamente.

Complejidad
En una implementación realizada con una lista doblemente enlazada la
complejidad de todas las iteraciones es O(1), excepto para acceder a un ele-
mento que no se encuentre en uno de los extremos de la estructura, que la
complejidad será O(n).
Si se implementa mediante un vector, la complejidad de las operaciones
de la cola doblemente terminada coincide con la de la implementación con
una lista.

13.7.1. C++
13.7.2. Java

13.8. Conjunto
Un conjunto es una colección de “miembros”o “elementos”de un “conjun-
to universal”. Por contraposición con las listas y otros contenedores vistos
previamente, todos los miembros de un conjunto deben ser diferentes, es decir
no puede haber dos copias del mismo elemento. Si bien para definir el concep-
to de conjunto sólo es necesario el concepto de igualdad o desigualdad entre
los elementos del conjunto universal, en general las representaciones de con-
juntos asumen que entre ellos existe además una “relación de orden estricta”,
que usualmente se denota como <. A veces un tal orden no existe en forma
natural y es necesario saber definirlo, aunque sea sólo para implementar el
tipo conjunto.

13.8.1. C++
13.8.2. Java

13.9. Diccionario
Los diccionarios, también llamados matrices asociativas, deben su nombre
a que son colecciones que relacionan una clave y un valor.El primer valor se

352
ESTRUCTURA DE DATOS

trata de la clave y el segundo del valor asociado a la clave. Como clave


podemos utilizar cualquier valor: podrı́amos usar números, cadenas.
Esto es ası́ porque los diccionarios se implementan como tablas hash, y
a la hora de introducir un nuevo par clave-valor en el diccionario se calcula
el hash de la clave para después poder encontrar la entrada correspondiente
rápidamente. Si se modificara el objeto clave después de haber sido introdu-
cido en el diccionario, evidentemente, su hash también cambiarı́a y no podrı́a
ser encontrado.
La diferencia principal entre los diccionarios y las otras colecciones es que
a los valores almacenados en un diccionario se les accede no por su ı́ndice,
porque de hecho no tienen orden, sino por su clave,

13.9.1. C++
La estructura de datos map en C++, se define como un contenedor aso-
ciativo ordenado que consta de pares de valores key-valor; donde key en una
clave única que define a cada valor. En orden de poder crear objetos map en
un programa se debe incluir el uso de la clase map mediante la directiva:

#include <map>

Otro aspecto que se debe de entender acerca de la estructura map es que


esta está organizada para contener elementos asociados en parejas, de ahı́,
la necesidad de entender el comportamiento de la plantilla pair. Esta es una
plantilla cuyo propósito es contener una pareja de valores. Los miembros de
pair son dos, first es el nombre del primero de ellos y el segundo se llama
second. Cada uno de los miembros de pair pueden ser de tipos diferentes.
Para poder usar la plantilla pair se debe de incluir la directiva:

#include < u t i l i t y >

pair es una estructura independiente y puede ser usada con diversos fines,
sin embargo, la importancia de pair radica en el hecho de que esta es usada
como estructura elemental para construir contenedores tipo map.
Un objeto de tipo map se puede definir mediante los siguientes construc-
tores:

#include <map>
map ( ) ;
map( const map& m ) ;
map( i t e r a t o r s t a r t , i t e r a t o r end ) ;
map( i t e r a t o r s t a r t , i t e r a t o r end , const key compare& cmp ) ;
map( const key compare& cmp ) ;

353
ESTRUCTURA DE DATOS

Un objeto map puede ser comparado y/o asignado con los operadores
estándar de C++: ==, ! =, <=, >=, <, > y =. Además, si se desea acceder
a un único elemento dentro de la estructura se puede utilizar el operador [ ].
Téngase en cuenta que dos objetos de tipo map son iguales si:

1. Tienen el mismo tamaño.

2. Cada miembro en la posición i del primero es igual al miembro en la


misma posición del segundo.

Entre los métodos y propiedades a destacar en esta estructura se encuen-


tran:

clear:Se encarga de eliminar todos los elementos.

size: Retorna la cantidad de elementos que contiene la estructura

empty: Retorna verdadero si el objeto no contiene ningún elemento y


falso en caso contrario.

find: Retorna el iterador donde esta el elemento cuya clave coincida


con la que se pasó como argumento al método. En caso de no existir
ninguna clave que coincida con el argumento el iterador devuelto es
igual al iterador que devuelve la funcionalidad end de la estructura.

insert: Inserta un elemento dentro de la estructura map. Este método


implementa además, otras dos sobrecargas.

A continuación dos ejemplos del uso de esta estructura.

#include <i o s t r e a m >


#include <s t r i n g >
#include <u t i l i t y >
#include <map>
#include <iomanip>

using namespace s t d ;

typedef p a i r <s t r i n g , s t r i n g > componente ;

int main ( )
{
c o u t << e n d l << "Otra simple prueba de map" << e n d l ;

map<s t r i n g , s t r i n g > d i r e c t o r i o ;

354
ESTRUCTURA DE DATOS

d i r e c t o r i o . i n s e r t ( p a i r <s t r i n g , s t r i n g > ( " Blanca " , "555 -6666"


) );
d i r e c t o r i o . i n s e r t ( m a k e p a i r ( " Oscar " , "555 -5555" ) ) ;

directorio . insert ( componente ( " Teresa " , "555 -4444" ) ) ;


directorio . insert ( componente ( " Carlos " , "555 -3333" ) ) ;
directorio . insert ( componente ( "Juan" , "555 -2222" ) ) ;
directorio . insert ( componente ( " Ruben " , "555 -1111" ) ) ;
directorio . insert ( componente ( " Andrea " , "555 -0000" ) ) ;

s t r i n g s = " Ruben " ;

map<s t r i n g , s t r i n g > : : i t e r a t o r p = d i r e c t o r i o . f i n d ( s ) ;
i f ( p != d i r e c t o r i o . end ( ) )
c o u t << " Numero telefonico de : " << s << " = " << p−>
s e c o n d << e n d l ;
else
c o u t << s << " no esta en el directorio .\n" ;
return 0 ;
}

El segundo

#include <i o s t r e a m >


#include <s t r i n g >
#include <u t i l i t y >
#include <map>
#include <iomanip>

using namespace s t d ;

typedef p a i r <s t r i n g , double> v e n t a d i a ;

int main ( )
{
c o u t << "\nUna simple prueba de map\n" ;
c o u t << " ........................\ n" ;
map<s t r i n g , double> semana ;

semana . insert ( v e n t a d i a ( " lunes " , 3 0 0 . 6 5 ) ) ;


semana . insert ( v e n t a d i a ( " martes " , 4 5 6 . 1 2 ) ) ;
semana . insert ( v e n t a d i a ( " miercoles " , 2 3 4 . 5 6 ) ) ;
double total = 0;

map<s t r i n g , double > : : i t e r a t o r p = semana . b e g i n ( ) ;


while ( p != semana . end ( ) )
{

355
ESTRUCTURA DE DATOS

c o u t << setw ( 1 0 ) << p−> f i r s t << setw ( 1 2 ) << p−>s e c o n d <<


endl ;
t o t a l += p−>s e c o n d ;
p ++;
}

c o u t << " ........................\ n" ;


c o u t << setw ( 1 0 ) << " total :" << setw ( 1 2 ) << t o t a l << e n d l ;

return 0 ;
}

13.9.2. Java

13.10. Estructura
En programación, una estructura de datos es una forma particular de
organizar datos en una computadora para que pueda ser utilizado de manera
eficiente.

13.11. Árbol
Los árboles son contenedores que permiten organizar un conjunto de ob-
jetos en forma jerárquica. Ejemplos tı́picos son los diagramas de organización
de las empresas o instituciones y la estructura de un sistema de archivos en
una computadora. Los árboles sirven para representar fórmulas, la descom-
posición de grandes sistemas en sistemas más pequeños en forma recursiva y
aparecen en forma sistemática en muchı́simas aplicaciones de la computación
cientı́fica. Una de las propiedades más llamativas de los árboles es la capa-
cidad de acceder a muchı́simos objetos desde un punto de partida o raı́z en
unos pocos pasos.

13.12. Árbol binario


Un árbol binario es una estructura de datos en la cual cada nodo puede
tener un hijo izquierdo y un hijo derecho. No pueden tener más de dos hijos
(de ahı́ el nombre ”binario”). Si algún hijo tiene como referencia a null, es
decir que no almacena ningún dato, entonces este es llamado un nodo externo.
En el caso contrario el hijo es llamado un nodo interno. Usos comunes de los

356
ESTRUCTURA DE DATOS

árboles binarios son los árboles binarios de búsqueda, los montı́culos binarios
y Codificación de Huffman.
En teorı́a de grafos, se usa la siguiente definición: ((Un árbol binario es
un grafo conexo, acı́clico y no dirigido tal que el grado de cada vértice no es
mayor a 3)). De esta forma solo existe un camino entre un par de nodos.
Un árbol binario con enraizado es como un grafo que tiene uno de sus
vértices, llamado raı́z, de grado no mayor a 2. Con la raı́z escogida, cada
vértice tendrá un único padre, y nunca más de dos hijos. Si rehusamos el
requerimiento de la conectividad, permitiendo múltiples componentes conec-
tados en el grafo, llamaremos a esta última estructura un bosque’.

13.13. Disjoint Set Union


La estructura disjoint set union es una estructura de datos que imple-
menta una serie de conjuntos disjuntos. Las operaciones que permiten son:

- MakeSet(x) Crea un nuevo conjunto que contiene el elemento x es


decir {x}.

- FindSet(x) Devielve el nombre del conjunto que contiene el elemento


x. El nombre en la mayoria de las implementaciones es el elemento
representativo del conjunto2 de forma tal que Find(x)==Fnd(y) si x y
y pertenencen al mismo conjunto.

- Unión(x,y) Toma dos conjuntos como parámetros y realiza la unión, el


resultado es un nuevo conjunto cuyo nombre es el nombre de algunos de
los conjuntos que lo originaron, los conjuntos anteriores son destruidos.

Ahora veamos una implementación de esta estructura:

#include <s t d i o . h>


#include <math . h>
#include <s t r i n g . h>
#include <i o s t r e a m >
#include <v e c t o r >
#include <l i s t >
#include <s t r i n g >
#include <a l g o r i t h m >
#include <queue>
#include <s t a c k >
#include <s e t >
#include <map>
2
Un elemento cualquiera ya que los conjntos son disjuntos

357
ESTRUCTURA DE DATOS

#include <complex>
#define MAX N 1000001
using namespace s t d ;
typedef long long l l d ;

int numComponents , m;

struct Node
{
int p a r e n t ;
int rank ;
};
Node DSU[MAX N ] ;

i n l i n e void MakeSet ( int x )


{
DSU[ x ] . p a r e n t = x ;
DSU[ x ] . rank = 0 ;
}

i n l i n e int Find ( int x )


{
i f (DSU[ x ] . p a r e n t == x ) return x ;
DSU[ x ] . p a r e n t = Find (DSU[ x ] . p a r e n t ) ;
return DSU[ x ] . p a r e n t ;
}

i n l i n e void Union ( int x , int y )


{
int xRoot = Find ( x ) ;
int yRoot = Find ( y ) ;
i f ( xRoot == yRoot ) return ;
numComponents−−;
i f (DSU[ xRoot ] . rank < DSU[ yRoot ] . rank )
{
DSU[ xRoot ] . p a r e n t = yRoot ;
}
e l s e i f (DSU[ xRoot ] . rank > DSU[ yRoot ] . rank )
{
DSU[ yRoot ] . p a r e n t = xRoot ;
}
else
{
DSU[ yRoot ] . p a r e n t = xRoot ;
DSU[ xRoot ] . rank++;
}
}

358
ESTRUCTURA DE DATOS

int main ( )
{
numComponents = 4 , m = 8 ;

MakeSet ( 1 ) ;
MakeSet ( 2 ) ;
MakeSet ( 3 ) ;
MakeSet ( 4 ) ;

Union ( 1 , 2) ;
Union ( 2 , 1) ;
Union ( 2 , 3) ;
Union ( 1 , 3) ;

p r i n t f ( " %d\n" , numComponents ) ;

return 0 ;
}

Esta estructura es utilizada en el algoritmo de Kruskal aunque tiene varias


otras aplicaciones entre la que podemos mencionar la de determinar si dos
nodos en un grafo no dirigido se encuentran en la misma componente conexa.

13.14. Árbol binario indexado o Thurs Fen-


wick
Un árbol binario indexado o árbol de Fenwick es una estructura de da-
tos que proporciona métodos eficientes para el cálculo y la manipulación de
las cantidades de prefijos de una array de valores. Fue propuesto por Peter
Fenwick en 1994. El Fenwick Tree principalmente resuelve el problema de
equilibrar la eficiencia de la suma del prefijo con la eficiencia de modificar
un elemento. La eficacia de estas operaciones se presenta como una solución
de compromiso, (dado que una mayor eficiencia en el cálculo de la suma del
prefijo se consigue precalculando los valores, pero una vez que los valores son
precalculados, debe volverse a calcular cada vez que ocurra una modifica-
ción). Esto harı́a O(1) las consultas pero las modificaciones serı́an O(n). El
Fenwick Tree hace ambas operaciones en O(log n), donde n es el tamaño del
array.
Dado un array de elementos, a veces es deseable calcular el total acumu-
lado de los valores hasta un ı́ndice de acuerdo con alguna operación binaria
asociativa. El Fenwick Tree proporciona un método para consultar el total
acumulado hasta cualquier ı́ndice, además de permitir cambios en el array en
un ı́ndice especı́fico y que las consultas siguientes reflejen dicho cambio. Aun-

359
ESTRUCTURA DE DATOS

que los Fenwick Tree son árboles en concepto, en la práctica se implementan


usando un array análogo a las implementaciones de un montı́culo binario.
Como se sabe cada entero puede ser representado como suma de poten-
cias de 2. De la misma manera las frecuencias acumulativas (suma de las
posiciones del principio hasta una posición i) pueden ser representadas como
suma de subfrecuencias. Esta es la idea básica de esta estructura de datos.
Sólo se necesitarı́a un array (tree) de la misma longitud del array original.
Sea idx cualquier ı́ndice del tree y r la posición del último dı́gito 1 (de
izquierda a derecha) en la representación en binario de idx ; entonces tree[idx]
es la suma desde el ı́ndice (idx-2r +1) hasta el ı́ndice idx.
Si por ejemplo se quisiera saber la suma hasta 13. Como en binario el 13
se escribe como 1101, podemos calcularlo ası́:
sum(13) = tree(1101) + tree(1100) + tree(1000)
A continuación se muestra el pseudocódigo de las dos operaciones (con-
sulta, modificación) que presenta esta estructura de datos:

f u n c t i o n Consulta ( ind )
suma := 0
while i n d > 0
suma := suma + t r e e [ i n d ]
i n d := i n d − ( i n d & −i n d )
return suma

f u n c t i o n M o d i f i c a r ( ind , v a l )
while i n d < MaxVal
t r e e [ i n d ] : = t r e e [ i n d ]+ v a l
i n d := i n d + ( i n d & −i n d )

A continación se muestra dos implementaciones. La primera permite ac-


tualizar el valor de un ı́ndice y determinar la suma de todos los valores desde
el indice 0 hasta un ı́ndice.

#include <s t d i o . h>


#include <math . h>
#include <s t r i n g . h>
#include <i o s t r e a m >
#include <v e c t o r >
#include <l i s t >
#include <s t r i n g >
#include <a l g o r i t h m >
#include <queue>
#include <s t a c k >
#include <s e t >
#include <map>
#include <complex>

360
ESTRUCTURA DE DATOS

#define MAX N 1000001


using namespace s t d ;
typedef long long l l d ;

int n ;
int b i t [MAX N ] ;

i n l i n e void update ( int x , int v a l )


{
while ( x <= n )
{
b i t [ x ] += v a l ;
x += ( x & −x ) ;
}
}

i n l i n e int r e a d ( int x )
{
int r e t = 0 ;
while ( x > 0 )
{
r e t += b i t [ x ] ;
x −= ( x & −x ) ;
}
return r e t ;
}

int main ( )
{
n = 10;
update ( 1 , 1 ) ;
update ( 3 , 1 ) ;
update ( 5 , 5 ) ;
update ( 2 , −2) ;
update ( 5 , −1) ;
p r i n t f ( " %d\n" , r e a d ( 6 ) ) ;
return 0 ;
}

Tanto la operación read como update tiene complejidad O(log N).La se-
gunda variante es una plantilla de la estructura que puede determinar la
suma dado un rango, ası́ como la actualización del valor dado un ı́ndice.

template <c l a s s T>


struct f e n w i c k t r e e
{
v e c t o r <T> x ;
f e n w i c k t r e e ( int n ) : x ( n , 0 ) {}

361
ESTRUCTURA DE DATOS

T sum ( int i , int j )


{
i f ( i == 0 )
{
T S = 0;
f o r ( j ; j>= 0 ; j =( j & ( j +1) ) −1)
S+=x [ j ] ;
return S ;
} else
return sum ( 0 , j ) − sum ( 0 , i −1) ;
}

void add ( int k , T a )


{
f o r ( ; k<x . s i z e ( ) ; k|=k+1)
x [ k]+=a ;
}
};

La complejidad de las operaciones add y sum es O(log N).


El Fenwick Tree es muy fácil para manejar un array de suma acumula-
tiva y de este array de suma acumulativa es posible calcular la suma de las
frecuencias en un cierto rango en el orden de O (log (n)). Permite hacer es-
te procedimiento además con cualquier operación asociativa. El Fenwick
Tree se utiliza para implementar el algoritmo de codificación aritmética.

13.15. Árbol de segmento (Segment Tree)


Un árbol de segmento (en inglés: Segment tree) es una estructura de datos
en forma de árbol para guardar intervalos o segmentos. Permite consultar
cuál de los segmentos guardados contiene un punto. Este es, en principio,
una estructura estática; es decir, su contenido no puede ser modificado una
vez que su estructura es construida. Una estructura de datos similar es el
árbol de intervalo.
Un árbol de segmento para un conjunto I de n intervalos usa O(nlogn)
de memoria de almacenamiento y puede construirse en un tiempo O(nlogn).
Los árboles de segmento soportan búsqueda para todos los intervalos que
contienen un punto de consulta en O(logn+k), k el número de intervalos o
segmentos recuperados.Algunas aplicaciones del árbol de segmento son vistas
en las áreas de la geometrı́a computacional y en los sistemas de información
geográfica. El árbol de segmentos puede generalizarse para espacios multidi-
mensionales.
Sea S un conjunto de intervalos o segmentos. Sea p1 , p2 , ..., p2 una lista

362
ESTRUCTURA DE DATOS

de distintos puntos extremos de intervalos, ordenada de izquierda a derecha.


Considere la división de la lı́nea de los números reales provocado por estos
puntos. La región de esta división es llamada intervalos elementales. Por
tanto, los intervalos elementales son, de izquierda a derecha:
(-∞, p1 ),[p1 , p1 ],(p1 , p2 ),[p2 , p2 ], ... ,(pm−1 , pm ),[pm , pm ],(pm , +∞)
Es decir, la lista de intervalos elementales está compuesta de intervalos
abiertos entre dos puntos finales consecutivos pi y pi+1 , alternándose con
intervalos cerrados compuesto por un único punto extremo. Puntos por se-
parados son tratados como intervalos porque la respuesta a una consulta no
es necesariamente la misma en el interior de un intervalo elemental que en
sus puntos extremos.
Dado un conjunto I de intervalos o segmentos, un árbol de segmento T
para I está estructurado de la siguiente manera:
1. T es un árbol binario.
2. Sus nodos hojas corresponden a los intervalos elementales provocados
por los puntos extremos en I, en una forma ordenada: la hoja más a
la izquierda coincide con el intervalo más a la izquierda. El intervalo
elemental correspondiente a una hoja v es denotado por Int(v).
3. Los nodos internos de T coinciden con intervalos que son la unión de
intervalos elementales: el intervalo Int(N) correspondiente al nodo N es
la unión de los intervalos correspondientes a las hojas del subárbol con
raı́z en N. Eso implica que Int(N) es la unión de los intervalos de sus
dos hijos.
4. Cada nodo v en T almacena el intervalo Int(v) y un conjunto de in-
tervalos, en alguna estructura de datos. Este subconjunto canónico del
nodo v contiene los intervalos[x,x’] de I tal que [x,x’] contiene Int(v)
y no contiene Int(padre(v)). Es decir, cada segmento en I almacena
los segmentos que abarcan completamente su intervalo, pero no abarca
completamente el intervalo de su padre.
Un árbol de segmento T en un conjunto I de n intervalos usa O(nlogn) de
almacenamiento.El conjunto I tiene como máximo 4n + 1 intervalo elemental.
Porque T es un árbol balanceado con a lo máximo 4n + 1 nodos hojas, su
altura es O(logn). Como cualquier intervalo es almacenado a lo máximo dos
veces en una profundidad del árbol, entonces la cantidad total almacenada
es O(nlogn).
Un árbol de segmento desde el conjunto de segmentos I, puede ser cons-
truido como sigue. Primero, los puntos finales de los intervalos en I se orde-
nan. Los intervalos elementales son obtenidos desde este. Entonces, un árbol

363
ESTRUCTURA DE DATOS

binario balanceado es construido con los intervalos elementales y para ca-


da nodo v es determinado el intervalo Int(v) que este representa. Quedando
computar los subconjuntos canónicos para los nodos. Para lograr esto, los
intervalos en I son insertados uno por uno en el árbol de segmento. Un inter-
valo X = [x,x’] puede ser insertado en un subárbol en T, usando el siguiente
procedimiento:

Si Int(T) está contenido en X entonces se almacena X en T y final.

Si no:

• Si X se interseca con el subconjunto canónico del hijo izquierdo


de T entonces inserta X en este hijo, recursivamente.
• Si X se interseca con el subconjunto canónico del hijo derecho de
T entonces inserta X en este hijo, recursivamente.

La operación de construcción completa toma un tiempo O(nlogn), donde


n es el número de segmentos en I.
Una consulta para un árbol de segmento, recibe un punto qx y recupera
una lista de todos los segmentos almacenados que contienen el punto qx .
Formalmente; dado un nodo (subárbol) v y un punto de consulta qx, la
consulta puede ser hecha usando el siguiente algoritmo:

Reportar todos los intervalos en I(v).

Si v no es hoja

• Si qx está en Int(hijo izquierdo de v) entonces realizar una consulta


en el hijo izquierdo de v.
• Si qx está en Int(hijo derecho de v) entonces realizar una consulta
en el hijo derecho de v.

En un árbol de segmento que contiene n intervalos, quienes contienen un


punto de consulta pueden ser reportados en un tiempo O(logn + k), donde
k es el número de intervalos reportados. A continucación la implementación
de dicha estructura.

#i n c l u d e <c s t d i o >
#i n c l u d e <v e c t o r >

// Tipo de l o s puntos
#d e f i n e Po in t f l o a t

364
ESTRUCTURA DE DATOS

// R e p r e s e n t a +i n f i n i t o
#d e f i n e FLT MAX 1E+37

using namespace s t d ;

struct Segment
{
Po i nt minValue ;
Po i nt maxValue ;
};

// Nodos d e l a r b o l de segmento
struct BinaryTree
{
Po i nt minValue ;
bool minOpen ;
Po i nt maxValue ;
bool maxOpen ;
v e c t o r <Segment> s u b S e t ;
BinaryTree ∗ l e f t C h i l d ;
BinaryTree ∗ r i g h t C h i l d ;
};

// B u i l t methods

// Comparar dos puntos


// u t i l i z a d o s para o r d e n a r l a l i s t a de puntos extremos
int cmp( const void ∗ arg1 , const void ∗ a r g 2 )
{
Po i nt v a l u e 1 = ∗ ( P oi nt ∗ ) a r g 1 ;
Po i nt v a l u e 2 = ∗ ( P oi nt ∗ ) a r g 2 ;
i f ( value1 < value2 )
return −1;
i f ( value1 > value2 )
return 1 ;
return 0 ;
}

i n l i n e BinaryTree ∗ Union ( BinaryTree ∗ nodeMin , BinaryTree ∗


nodeMax )
{
BinaryTree ∗ r e s u l t = new BinaryTree ( ) ;
r e s u l t −>minValue = nodeMin−>minValue ;
r e s u l t −>minOpen = nodeMin−>minOpen ;
r e s u l t −>maxValue = nodeMax−>maxValue ;
r e s u l t −>maxOpen = nodeMax−>maxOpen ;
return r e s u l t ;
}

365
ESTRUCTURA DE DATOS

i n l i n e BinaryTree ∗ CreateLeafNode ( Po i nt minValue , Po in t


maxValue , bool minOpen , bool maxOpen )
{
BinaryTree ∗ newNode = new BinaryTree ( ) ;
newNode−>minOpen = minOpen ;
newNode−>maxOpen = maxOpen ;
newNode−>minValue = minValue ;
newNode−>maxValue = maxValue ;
newNode−>l e f t C h i l d = 0 ;
newNode−>r i g h t C h i l d = 0 ;
newNode−>s u b S e t = v e c t o r <Segment >() ;
return newNode ;
}

BinaryTree ∗ B u i l t B a l a n c e d B i n a r y T r e e ( P oi nt e n d P o i n t s [ ] , int
countEndPoint )
{
BinaryTree ∗ e l e m e n t a r y I n t e r v a l [ countEndPoint ∗ 2 + 1 ] ;
P oi nt minValue = −FLT MAX;
f o r ( int i = 0 ; i < countEndPoint ; i ++)
{
int towI = 2 ∗ i ;
e l e m e n t a r y I n t e r v a l [ towI ] = CreateLeafNode ( minValue ,
e n d P o i n t s [ i ] , true , true ) ;
e l e m e n t a r y I n t e r v a l [ towI + 1 ] = CreateLeafNode (
endPoints [ i ] , endPoints [ i ] , false , false ) ;
minValue = e n d P o i n t s [ i ] ;
}
int countNodes = countEndPoint ∗ 2 + 1 ;
e l e m e n t a r y I n t e r v a l [ countNodes − 1 ] = CreateLeafNode (
e n d P o i n t s [ countEndPoint − 1 ] , FLT MAX, true , true ) ;
while ( countNodes > 1 )
{
f o r ( int i = 0 ; i < countNodes / 2 ; i ++)
{
int towI = 2 ∗ i ;
BinaryTree ∗newNode = Union ( e l e m e n t a r y I n t e r v a l [
towI ] , e l e m e n t a r y I n t e r v a l [ towI + 1 ] ) ;
newNode−>l e f t C h i l d = e l e m e n t a r y I n t e r v a l [ towI ] ;
newNode−>r i g h t C h i l d = e l e m e n t a r y I n t e r v a l [ towI +
1];
newNode−>s u b S e t = v e c t o r <Segment >() ;
e l e m e n t a r y I n t e r v a l [ i ] = newNode ;
}
i f ( countNodes % 2 )
{
e l e m e n t a r y I n t e r v a l [ countNodes / 2 ] =
e l e m e n t a r y I n t e r v a l [ countNodes − 1 ] ;
}

366
ESTRUCTURA DE DATOS

countNodes = countNodes / 2 + ( countNodes % 2 ) ;


}
return e l e m e n t a r y I n t e r v a l [ 0 ] ;
}

i n l i n e bool ContainsIntNode ( Segment segment , BinaryTree ∗ t r e e )


{
return t r e e −>minValue >= segment . minValue & t r e e −>maxValue
<= segment . maxValue ;
}

i n l i n e bool I n t e r s e c t I n t N o d e ( Segment segment , BinaryTree ∗ t r e e )


{
return ( segment . minValue < t r e e −>maxValue | ( segment .
minValue == t r e e −>maxValue & ! t r e e −>maxOpen ) )
& ( segment . maxValue > t r e e −>minValue | ( segment .
maxValue == t r e e −>minValue & ! t r e e −>minOpen ) ) ;
}

void I n s e r t S e g m e n t ( Segment segment , BinaryTree ∗ t r e e )


{
i f ( ContainsIntNode ( segment , t r e e ) )
{
t r e e −>s u b S e t . push back ( segment ) ;
return ;
}
// S i e s nodo h o j a
i f ( ! t r e e −>l e f t C h i l d )
return ;
i f ( I n t e r s e c t I n t N o d e ( segment , t r e e −>l e f t C h i l d ) )
I n s e r t S e g m e n t ( segment , t r e e −>l e f t C h i l d ) ;
i f ( I n t e r s e c t I n t N o d e ( segment , t r e e −>r i g h t C h i l d ) )
I n s e r t S e g m e n t ( segment , t r e e −>r i g h t C h i l d ) ;
}

void D e l e t e E q u a l P o i n t ( P oi nt ∗ endPoint , int ∗ countEndPoint )


{
int count = ∗ countEndPoint ;
int i n d e x = 0 ;
int i = 0 ;
while ( i < count )
{
endPoint [ i n d e x ] = endPoint [ i ] ;
while ( i < count && endPoint [ i n d e x ] == endPoint [ i ] )
i ++;
i n d e x++;
}
∗ countEndPoint = i n d e x ;
}

367
ESTRUCTURA DE DATOS

// Crea e l a r b o l de segmento a p a r t i r d e l c o n j u n t o de segmentos


BinaryTree ∗ B u i l t T r e e ( Segment segments [ ] , int segmentCount )
{
int countEndPoint = segmentCount ∗ 2 ;
Po i nt ∗ e n d P o i n t s = new P oi nt [ countEndPoint ] ;
f o r ( int i = 0 ; i < segmentCount ; i ++)
{
int towI = 2 ∗ i ;
e n d P o i n t s [ towI ] = segments [ i ] . maxValue ;
e n d P o i n t s [ towI + 1 ] = segments [ i ] . minValue ;
}
q s o r t ( endPoints , countEndPoint , s i z e o f ( P oi nt ) , cmp) ;
D e l e t e E q u a l P o i n t ( endPoints , &countEndPoint ) ;
BinaryTree ∗ t r e e = B u i l t B a l a n c e d B i n a r y T r e e ( endPoints ,
countEndPoint ) ;
delete [ ] e n d P o i n t s ;
f o r ( int i = 0 ; i < segmentCount ; i ++)
I n s e r t S e g m e n t ( segments [ i ] , t r e e ) ;
return t r e e ;
}

// L i b e r a l a memoria ocupada por e l a r b o l c o n s t r u i d o


void D e l e t e T r e e ( BinaryTree ∗ t r e e )
{
i f ( t r e e −>l e f t C h i l d != 0 )
D e l e t e T r e e ( t r e e −>l e f t C h i l d ) ;
i f ( t r e e −>r i g h t C h i l d != 0 )
D e l e t e T r e e ( t r e e −>r i g h t C h i l d ) ;
delete t r e e ;
}

// Query methods
i n l i n e bool C o nt a i nP o i n t ( BinaryTree ∗ t r e e , Po in t p o i n t )
{
return ( t r e e −>minValue < p o i n t | ( t r e e −>minValue == p o i n t &
! t r e e −>minOpen ) )
& ( t r e e −>maxValue > p o i n t | ( t r e e −>maxValue == p o i n t &
! t r e e −>maxOpen ) ) ;
}

void Query ( BinaryTree ∗ t r e e , Po in t p o i n t , v e c t o r <Segment> ∗


report )
{
i f ( C o n ta i n P oi n t ( t r e e , p o i n t ) )
{
int s i z e S u b S e t = t r e e −>s u b S e t . s i z e ( ) ;
f o r ( int i = 0 ; i < s i z e S u b S e t ; i ++)
{

368
ESTRUCTURA DE DATOS

( ∗ r e p o r t ) . push back ( t r e e −>s u b S e t [ i ] ) ;


}
}
// S i e s nodo h o j a
i f ( ! t r e e −>l e f t C h i l d )
return ;
i f ( C o n t ai n P oi n t ( t r e e −>l e f t C h i l d , point ) )
Query ( t r e e −>l e f t C h i l d , p o i n t , report ) ;
i f ( C o n t ai n P oi n t ( t r e e −>r i g h t C h i l d , point ) )
Query ( t r e e −>r i g h t C h i l d , p o i n t , report ) ;
}

El árbol de segmento puede ser generalizado a espacios multidimensiona-


les, en forma de árbol de segmento multinivel. En versiones multidimensio-
nales, el árbol de segmento almacena una colección de rectángulos de ejes pa-
ralelos y puede recuperar los rectángulos que contienen un punto de consulta
dado. La estructura usa O(nlogd n) de almacenamiento y responde consultas
en O(logd n). El uso de fractional cascading baja el tiempo de consulta por un
factor logarı́tmico. El uso del árbol de intervalo en el nivel más profundo de
las estructuras asociadas baja el almacenamiento con un factor logarı́tmico.
El árbol de segmento es menos eficiente que el árbol de intervalo para con-
sultas con rangos en una dimensión, debido a su requisito de almacenamiento
más alto: O(nlogn) en contra del O(n) del árbol de intervalo. Lo importante
del árbol de segmento es que los segmentos dentro del subconjunto canónico
de cada nodo pueden ser almacenados de cualquier manera arbitraria.
Para n intervalos cuyos puntos finales están en el rango de un entero
pequeño (small integer), existe una estructura de datos óptima con un tiempo
de procesamiento lineal y un tiempo de consulta O(1+k) para reportar todos
los k intervalos que contienen al punto de consulta dado.
Otra ventaja del árbol de segmento es que puede ser fácilmente adaptado
para consultas de cantidad; esto es, reportar el número de segmentos que
contienen un punto dado, en lugar de reportar todos los segmentos. . En
lugar de guardar los intervalos en subconjuntos canónicos, puede simplemente
guardar el número de ellos. Semejante árbol de segmento usa almacenamiento
lineal y requiere un tiempo de consulta O(logn), ası́ que es óptimo.
Una versión multidimensional del árbol de intervalo es árbol de búsque-
da con prioridad y no existe, es decir, no hay ninguna extensión clara de
estas estructuras que solucione el problema análogo multidimensional. Pero
las estructuras pueden ser usadas como estructura asociada de árboles de
segmentos.

369
ESTRUCTURA DE DATOS

13.16. Árbol de rango (Range Tree)


Un árbol de rango (Range Tree) en un conjunto de elementos de una
dimensión es un árbol de búsqueda binario equilibrado en esos elementos.
Los elementos almacenados en el árbol se almacenan en las hojas del árbol;
cada nodo interno almacena el valor más grande contenido en sus subárboles.
Un árbol de rango en un conjunto de elementos en d-dimensiones es un
árbol de búsqueda binario multinivel recursivamente definido. Cada nivel
de la estructura de datos es un árbol de búsqueda binario en una de las
dimensiones d. El primer nivel es un árbol de búsqueda binario en la primera
de las coordenadas d. Cada vértice v de este árbol contiene una estructura
asociada que es un árbol de rango (d-1)-dimensional en las últimas (d-1)
coordenadas de los elementos almacenados en el subárbol de v.

Figura 13.1: Representación de un árbol de rango

13.16.1. Operaciones
Construción Un árbol de rango 1-dimensional en un conjunto de n ele-
mentos es un árbol de búsqueda binario, que se puede construir en el tiempo
O ( n log n ). Los árboles de rango en dimensiones más altas se constru-
yen recursivamente construyendo un árbol de búsqueda binario equilibrado
en la primera coordenada de los elementos y luego, para cada vértice v en
este árbol, construyendo un árbol de distribución (d-1)-dimensional en los
elementos contenidos en el subárbol de v. Construir un árbol de rango de
esta manera requerirı́a tiempo O (nlog d n).

Intervalo de consultas Árboles de rango se puede utilizar para encontrar


el conjunto de elementos que se encuentran dentro de un intervalo dado. Para
informar los puntos que se encuentran en el intervalo [ x1 , x2 ], comenzamos
por buscar x1 y x2 . En algún vértice del árbol, las rutas de búsqueda a x1 y x2
divergirán. Sea v split el último vértice que estos dos caminos de búsqueda

370
ESTRUCTURA DE DATOS

tienen en común. Continúe buscando x1 en el árbol de rangos. Para cada


vértice v en la ruta de búsqueda de v dividida a x1 , si el valor almacenado
en v es mayor que x1 , reporte cada elemento en el subárbol derecho de v . Si
v es una hoja, informe el valor almacenado en v si está dentro del intervalo
de consulta. Del mismo modo, reportar todos los puntos almacenados en los
subárboles izquierdos de los vértices con valores menores que x2 a lo largo de
la ruta de búsqueda de v dividida a x2 e informar la hoja de esta ruta si se
encuentra dentro del intervalo de consulta.
Dado que el árbol de rango es un árbol binario balanceado, las rutas de
búsqueda a x1 y x2 tienen longitud O (log n ). La generación de informes
de todos los elementos almacenados en el subárbol de un vértice se puede
hacer en tiempo lineal utilizando cualquier algoritmo de recorrido de árbol.
Se deduce que el tiempo para realizar una consulta de rango es O (log n+k
), donde k es el número de elementos en el intervalo de consulta.
Las consultas de rango en d-dimensiones son similares. En lugar de in-
formar de todos los puntos almacenados en los subárboles de las rutas de
búsqueda, realice una consulta de rango (d-1)-dimensional sobre la estructu-
ra asociada de cada subárbol. Eventualmente, se realizará una consulta de
rango 1-dimensional y se reportarán los puntos correctos.
Dado que una consulta d- dimensional consiste en consultas de rango O
(log n ) (d-1), se sigue que el tiempo requerido para realizar una consulta de
rango d-dimensional es O (log d n + k ), donde k es el número de puntos en
el intervalo de consulta. Esto se puede reducir a O (log d−1 n+k) usando la
técnica de cascada .
A continuación una implementación de dicha estructura de datos en C++.

#i n c l u d e <s t d i o . h>
#i n c l u d e <math . h>
#i n c l u d e <s t r i n g . h>
#i n c l u d e <i o s t r e a m >
#i n c l u d e <v e c t o r >
#i n c l u d e < l i s t >
#i n c l u d e <s t r i n g >
#i n c l u d e <a l g o r i t h m >
#i n c l u d e <queue>
#i n c l u d e <s t a c k >
#i n c l u d e <s e t >
#i n c l u d e <map>
#i n c l u d e <complex>
#d e f i n e MID ( l e f t +r i g h t ) /2
#d e f i n e MAX N 1000001
#d e f i n e MAX TREE (MAX N << 2 )
#d e f i n e INF 987654321
using namespace s t d ;

371
ESTRUCTURA DE DATOS

typedef long long l l d ;

int n ;
int n i z [MAX N ] ;
int RT[MAX TREE ] ;

void I n i t T r e e ( int idx , int l e f t , int r i g h t )


{
i f ( l e f t == r i g h t )
{
RT[ i d x ] = n i z [ l e f t ] ;
return ;
}
I n i t T r e e ( 2 ∗ idx , l e f t , MID) ;
I n i t T r e e ( 2 ∗ i d x +1, MID+1, r i g h t ) ;
RT[ i d x ] = min (RT[ 2 ∗ i d x ] , RT[ 2 ∗ i d x +1]) ;
}

void Update ( int idx , int x , int val , int l e f t , int r i g h t )


{
i f ( l e f t == r i g h t )
{
RT[ i d x ] = v a l ;
return ;
}
i f ( x <= MID) Update ( 2 ∗ idx , x , val , l e f t , MID) ;
e l s e Update ( 2 ∗ i d x +1, x , val , MID+1, r i g h t ) ;
RT[ i d x ] = min (RT[ 2 ∗ i d x ] , RT[ 2 ∗ i d x +1]) ;
}

int Query ( int idx , int l , int r , int l e f t , int r i g h t )


{
i f ( l <= l e f t && r i g h t <= r ) return RT[ i d x ] ;
int r e t = INF ;
i f ( l <= MID) r e t = min ( r e t , Query ( 2 ∗ idx , l , r , l e f t , MID) )
;
i f ( r > MID) r e t = min ( r e t , Query ( 2 ∗ i d x +1, l , r , MID+1,
right ) ) ;
return r e t ;
}

int main ( )
{
n = 6;
niz [ 1 ] = 4;
niz [ 2 ] = 2;
niz [ 3 ] = 5;
niz [ 4 ] = 1;
niz [ 5 ] = 6;

372
ESTRUCTURA DE DATOS

niz [ 6 ] = 3;

InitTree (1 , 1 , n) ;
p r i n t f ( " %d\n" , Query ( 1 , 1 , 3 , 1 , n ) ) ;

Update ( 1 , 4 , 1 0 , 1 , n ) ;
Update ( 1 , 5 , 0 , 1 , n ) ;
p r i n t f ( " %d\n" , Query ( 1 , 4 , 6 , 1 , n ) ) ;

return 0 ;
}

En dicha implementación la estructura fue concebida para determinar el


menor valor en el arreglo, dado un intervalo dado. Se le agrego la funcionali-
dad de actualizar un valor del arreglo dada su posición. Todas las funcionali-
dades de esta implementación presentan una complejidad de temporal O(log
N) donde N es la cantidad de elementos del arreglo o colección de elementos.

13.16.2. Propagación diferida Lazy Propagation


Bien en el epı́grafe anterior vimos una estructura de datos que permite
en tiempos O(log N ) dentro de una colección realizar dos operaciones bası́ca-
mente:

1. Actualización: Dado un ı́ndice Y de la colección y un valor X acceder


la posición Y de la posición de la colección de lista y modificar s valor
por X.
2. Consulta: Dado un rango de ı́ndices de la colección obtener un posible
resultado (suma, máximo, mı́nimo, ect) de los elementos contenidos en
dicho rango.

Bien ahora si en vez de actualizar una posición quisieramos actualizar


con un mismo valor todas la posiciones en un rango de la colecciones. Por
ejemplo, modifique por 10 a todos los valores en los ı́ndices de 2 a 7 en la
colección.
Una solución pudiera ser invocarse la función de actualización para cada
número de 2 a 7. Esto puede ser una solución pero no es óptima porque en el
peor de los casos la complejidad de esto es igual a QN log N donde sabemos
que los rangos de Q y N son de 1 hasta 105 .
Entonces esta vez radica en como realizar una función que permita ac-
tualizar en un rango dentro de la colección con un tiempo log N .
Propagación diferida (Lazy progation): Es una optimización para hacer
que las actualizaciones de rango sean más rápidas

373
ESTRUCTURA DE DATOS

Cuando hay muchas actualizaciones y actualizaciones en un rango, po-


demos posponer algunas actualizaciones (evitar las llamadas recursivas en la
actualización) y hacer esas actualizaciones solo cuando sea necesario.
Recuerde que un nodo en el árbol de rangos almacena o representa el
resultado de una consulta para un rango de ı́ndices. Y si el rango de este
nodo se encuentra dentro del rango de operación de actualización, todos los
descendientes del nodo también deben actualizarse.
La idea es inicializar todos los elementos de lazy [] como 0. Un valor 0 en
lazy [i] indica que no hay actualizaciones pendientes en el nodo i en el árbol
de rangos. Un valor no nulo de perezoso [i] significa que esta cantidad debe
agregarse al nodo i en el árbol de rangos antes de realizar cualquier consulta
al nodo.
¿Hay algún cambio en la función de consulta también? Como hemos cam-
biado la actualización para posponer sus operaciones, puede haber problemas
si se realiza una consulta a un nodo que aún no se ha actualizado. Por lo
tanto, debemos actualizar nuestro método de consulta. En el método de con-
sulta ahora primero comprueba si hay una actualización pendiente y si la
hay, luego actualiza el nodo. Una vez que se asegure de que la actualización
pendiente esté completa, funciona igual que el método de consulta de un
Range Tree sin lazy progation.

13.17. Árbol biselado (Splay Tree)


Un Árbol biselado o Árbol Splay es un árbol binario de búsqueda auto-
balanceable, con la propiedad adicional de que a los elementos accedidos
recientemente se accederá más rápidamente en accesos posteriores. Realiza
operaciones básicas como pueden ser la inserción, la búsqueda y el borrado
en un tiempo del orden de O(log n). Para muchas secuencias no uniformes
de operaciones, el árbol biselado se comporta mejor que otros árboles de
búsqueda, incluso cuando el patrón especı́fico de la secuencia es desconocido.
Esta estructura de datos fue inventada por Robert Tarjan y Daniel Sleator.
Todas las operaciones normales de un árbol binario de búsqueda son com-
binadas con una operación básica, llamada biselación. Esta operación consiste
en reorganizar el árbol para un cierto elemento, colocando éste en la raı́z. Una
manera de hacerlo es realizando primero una búsqueda binaria en el árbol
para encontrar el elemento en cuestión y, a continuación, usar rotaciones de
árboles de una manera especı́fica para traer el elemento a la cima. Alternati-
vamente, un algoritmo ”de arriba a abajo”puede combinar la búsqueda y la
reorganización del árbol en una sola fase.
El buen rendimiento de un árbol biselado depende del hecho de que es

374
ESTRUCTURA DE DATOS

auto-balanceado, y además se optimiza automáticamente. Los nodos accedi-


dos con más frecuencia se moverán cerca de la raı́z donde podrán ser accedidos
más rápidamente. Esto es una ventaja para casi todas las aplicaciones, y es
particularmente útil para implementar cachés y algoritmos de recolección de
basura; sin embargo, es importante apuntar que para un acceso uniforme, el
rendimiento de un árbol biselado será considerablemente peor que un árbol
de búsqueda binaria balanceado simple.
Los árboles biselados también tienen la ventaja de ser consideradamente
más simples de implementar que otros árboles binarios de búsqueda auto-
balanceados, como pueden ser los árboles Rojo-Negro o los árboles AVL,
mientras que su rendimiento en el caso promedio es igual de eficiente. Además,
los árboles biselados no necesitan almacenar ninguna otra información adicio-
nal a parte de los propios datos, minimizando de este modo los requerimientos
de memoria. Sin embargo, estas estructuras de datos adicionales proporcio-
nan garantı́as para el peor caso, y pueden ser más eficientes en la práctica
para el acceso uniforme.
Uno de los peores casos para el algoritmo básico del árbol biselado es el
acceso secuencial a todos los elementos del árbol de forma ordenada. Esto
deja el árbol completamente des balanceado (son necesarios n accesos, cada
uno de los cuales del orden de O(log n) operaciones). Volviendo a acceder
al primer elemento se dispara una operación que toma del orden de O(n)
operaciones para volver a balancear el árbol antes de devolver este primer
elemento. Esto es un retraso significativo para esa operación final, aunque el
rendimiento se amortiza si tenemos en cuenta la secuencia completa, que es
del orden de O(log n). Sin embargo, investigaciones recientes muestran que si
aleatoriamente volvemos a balancear el árbol podemos evitar este efecto de
desbalance y dar un rendimiento similar a otros algoritmos de auto-balanceo.
Al contrario que otros tipos de árboles auto balanceados, los árboles bi-
selados trabajan bien con nodos que contienen claves idénticas. Incluso con
claves idénticas, el rendimiento permanece amortizado del orden de O(log
n). Todas las operaciones del árbol preservan el orden de los nodos idénticos
dentro del árbol, lo cual es una propiedad similar a la estabilidad de los algo-
ritmos de ordenación. Un operación de búsqueda cuidadosamente diseñada
puede devolver el nodo más a la izquierda o más a la derecha de una clave
dada.
A continuación la implementación de la estructura.

struct s p l a y t r e e
{
struct node
{

375
ESTRUCTURA DE DATOS

int key ;
node ∗ l , ∗ r ;
node ( int key = 0 , node ∗ l = 0 , node ∗ r = 0 ) :
key ( key ) , l ( l ) , r ( r ) { }
} ∗ root ;

node ∗ s p l a y ( int x , node ∗ t )


{
if (! t)
return t ;
node N, ∗ l = &N, ∗ r = &N, ∗ s ;
for ( ; ; )
{
i f ( x < t−>key )
{
i f ( t−>l && x < t−>l −>key )
s = t−>l , t−>l = s−>r , s−>r = t , t = s ;
i f ( ! t−>l )
break ;
r−>l = t , r = t , t = t−>l ;
}
e l s e i f ( x > t−>key )
{
i f ( t−>r && x > t−>r−>key )
s = t−>r , t−>r = s−>l , s−>l = t , t = s ;
i f ( ! t−>r )
break ;
l −>r = t , l = t , t = t−>r ;
}
else
break ;
}
l −>r = t−>l ;
r−>l = t−>r ;
t−>l = N. r ;
t−>r = N. l ;

return t ;
}

node ∗ f i n d ( int x )
{
root = splay (x , root ) ;
i f ( x == r o o t −>key )
return r o o t ;
else
return 0 ;
}

376
ESTRUCTURA DE DATOS

void i n s e r t ( int x )
{
i f ( ! root )
r o o t = new node ( x ) ;
else
{
root = splay (x , root ) ;
i f ( x < r o o t −>key )
{
node ∗ t = new node ( x , r o o t −>l , r o o t ) ;
r o o t −>l = 0 ;
root = t ;
}
e l s e i f ( x > r o o t −>key )
{
node ∗ t = new node ( x , r o o t , r o o t −>r ) ;
r o o t −>r = 0 ;
root = t ;
}
}
}

void e r a s e ( int x )
{
if (! find (x) )
return ;
i f ( ! r o o t −>l )
r o o t = r o o t −>r ;
else
{
node ∗ t = r o o t −>r ;
r o o t = s p l a y ( x , r o o t −>l ) ;
r o o t −>r = t ;
}
}

splay tree () : root (0) { };


};

Cada una de las operaciones tiene una complejidad de O(log N ), veamos


como funciona cada una.

Búsqueda La búsqueda de un valor de clave en un árbol biselado tiene la


caracterı́stica particular de que modifica la estructura del árbol. El descenso
se efectúa de la misma manera que un árbol binario de búsqueda, pero si se
encuentra un nodo cuyo valor de clave coincide con el buscado, se realiza una
biselación de ese nodo. Si no se encuentra, el nodo biselado será aquel que

377
ESTRUCTURA DE DATOS

visitamos por último antes de descartar la búsqueda. Ası́, la raı́z contendrá


un sucesor o predecesor del nodo buscado. En el caso del la implementación
se le pasa el elemento a buscar en caso de que exista devuelve el puntero sino
devuelve null.

Inserción Es igual que en el árbol binario de búsqueda con la salvedad de


que se realiza una biselación sobre el nodo insertado. Además, si el valor de
clave a insertar ya existe en el árbol, se bisela el nodo que lo contiene. En
la implementación solo se debe pasar el elemento que se desea adicionar al
árbol.

Extracción Esta operación requiere dos biselaciones. Primero se busca el


nodo que contiene el valor de clave que se debe extraer. Si no se encuentra, el
árbol es biselado en el último nodo examinado y no se realiza ninguna acción
adicional. Si se encuentra, el nodo se bisela y se elimina. Con esto el árbol
se queda separado en dos mitades, por lo que hay que seleccionar un nodo
que haga las veces de raı́z. Al ser un árbol binario de búsqueda y estar todos
los valores de clave ordenados, podemos elegir como raı́z el mayor valor del
subárbol izquierdo o el menor valor de clave del derecho.

Biselación (Splay) Esta operación traslada un nodo x, que es el nodo al


que se accede, a la raı́z . Para realizar esta operación debemos rotar el árbol
de forma que en cada rotación el nodo x está más cerca de la raı́z. Cada
biselación realizada sobre el nodo de interés mantiene el árbol parcialmente
equilibrado y además los nodos recientemente accedidos se encuentran en
las inmediaciones de la raı́z. De esta forma amortizamos el tiempo empleado
para realizar la biselación.
Podrı́amos distinguir 3 casos generales

1. x es hijo izquierdo o derecho de la raı́z, p. Si x es hijo izquierdo de p


entonces realizaremos una rotación simple derecha. En caso de que x
sea el derecho la rotación que deberemos realizar es simple izquierda.

2. x es hijo izquierdo de p y este a su vez hijo izquierdo de q o bien


ambos son hijos derechos. Si x es hijo y nieto izquierdo de p y q, res-
pectivamente. Entonces debemos realizar rotación doble a la derecha,
en caso de que x sea hijo y nieto derecho de p y q la rotación será doble
izquierda.

3. x es hijo izquierdo de p y este a su vez hijo derecho de q o viceversa. En


caso de que x sea hijo izquierdo de p y nieto derecho de q realizaremos

378
ESTRUCTURA DE DATOS

una rotación simple derecha en el borde entre x y p y otra simple


izquierda entre x y q. En caso contrario, x sea hijo derecho y nieto
izquierdo de q, la rotaciones simples será izquierda y después derecha.

379
ESTRUCTURA DE DATOS

13.18. Consulta de rango mı́nimo (Range Mi-


nimum Query RMQ)
Una consulta de rango mı́nimo (Range Minimum Query RMQ) resuelve
el problema de encontrar el valor mı́nimo en un sub-arreglo de un arreglo de
objetos comparables.
Dada un arreglo A [1. . . n ] de n objetos tomados de un conjunto. el rango
mı́nimo consulta RQMa ( l , r ) = arg min A [ k ] (con 1 ≤ l ≤ k ≤ r ≤ n )
devuelve la posición del elemento mı́nimo en el subarreglo A especificado [ l
... r ] .
Por ejemplo, cuando A = [0,5,2,5,4,3,1,6,3] , entonces la respuesta a la
consulta de rango mı́nimo para la sub-matriz A [3 ... 8] = [2,5 , 4,3,1,6] es 7
, como A [7] = 1

13.18.1. Solución ingenua


En una configuración tı́pica, el arreglo A es estático, es decir, los elementos
no se insertan o eliminan durante una serie de consultas, y las consultas que
deben responderse en lı́nea (es decir, el conjunto completo de consultas no se
conoce de antemano para el algoritmo ) En este caso, un preprocesamiento
adecuado del arreglo en una estructura de datos garantiza una respuesta de
consulta más rápida. Una solución ingenua es calcular previamente todas
las consultas posibles, es decir, el mı́nimo de todas los subarreglos de A
, y almacenarlas en una matriz B tal que B [ i , j ] = min ( A [ i ... j ]) ;
entonces una consulta de rango mı́nimo se puede resolver en tiempo constante
mediante la búsqueda de matriz en B. Hay O(n2 ) consultas posibles para una
matriz de longitud n , y las respuestas a estas se pueden calcular en O(n2 )
tiempo mediante programación dinámica .

13.18.2. Solución usando tiempo constante y espacio


linearithmic
Al igual que en la solución anterior, la respuesta a las consultas en tiempo
constante se logrará mediante el cálculo previo de los resultados. Sin embargo,
la matriz almacenará consultas mı́n. Calculadas previamente para todos los
elementos y solo los rangos cuyo tamaño es una potencia de dos. Hay O(logn)
tales consultas para cada posición de inicio i , por lo que el tamaño de la
tabla de programación dinámica B es O(nlogn). Cada elemento B [ i , j ]
contiene el ı́ndice del mı́nimo del rango A [ i ... i +2 j -1] . La tabla se llena
con los ı́ndices de mı́nimos usando la recurrencia

380
ESTRUCTURA DE DATOS

Si A[B[i ,j-1]] ≤ A[B[i+2j−1 ,j-1]] , entonces B [i,j] = B [i,j-1] ; de lo


contrario, B[i,j] = B[i+2j−1 , j -1] .
Ahora se puede responder una consulta RMQA (l,r) dividiéndola en dos
consultas separadas: una es la consulta precalculada con un rango de l al
lı́mite más alto menor que r . La otra es la consulta de un intervalo de la
misma longitud que tiene r como lı́mite derecho. Estos intervalos pueden
superponerse, pero como se calcula el mı́nimo en lugar de, digamos, la suma,
esto no importa. El resultado general se puede obtener en tiempo constante
porque estas dos consultas se pueden responder en tiempo constante y lo
único que queda por hacer es elegir el menor de los dos resultados.

13.18.3. Solución usando tiempo logarı́tmico y espacio


lineal
Esta solución responde a las RMQ en tiempo O (log n ) . Sus estructuras
de datos usan espacio O ( n ) y sus estructuras de datos también se pueden
usar para responder consultas en tiempo constante. La matriz primero se
divide conceptualmente en bloques de tamaño s= logn 4
Entonces, el mı́nimo
para cada bloque se puede calcular en tiempo O(n) en general y los mı́nimos
se almacenan en una nueva matriz.
Los RMQ ahora se pueden responder en tiempo logarı́tmico mirando los
bloques que contienen el lı́mite de consulta izquierdo, el lı́mite de consulta
derecho y todos los bloques intermedios:

Los dos bloques que contienen los lı́mites se pueden buscar ingenua-
mente. Los elementos fuera del lı́mite ni siquiera necesitan ser mirados.
Esto se puede hacer en tiempo logarı́tmico.

Los mı́nimos de todos los bloques que están completamente conteni-


dos en el rango, y los dos mı́nimos mencionados anteriormente, deben
compararse para responder la consulta.

Debido a que la matriz se dividió en bloques de tamaño logn


4
, hay
4n
como máximo logn bloques que están completamente contenidos en la
consulta.

Al usar la solución linearithmic se puede encontrar el mı́nimo gene-


ral entre estos bloques. Esta estructura de datos tiene un tamaño O(
n n
logn
log( logn ))=O(n).

Ahora, solo se necesitan comparar tres mı́nimos.

381
ESTRUCTURA DE DATOS

Por ejemplo, usando la matriz A = [0,5,2,5,4,3,1,6,3] y un tamaño de


bloque de 3 produce la matriz mı́nima A’ = [0,3,1] .

13.18.4. Solución usando tiempo constante y espacio


lineal
Usando la solución anterior, las subconsultas dentro de los bloques que
no están completamente contenidas en la consulta aún deben responderse
en tiempo constante. Hay como máximo dos de esos bloques: el bloque que
contiene l y el bloque que contiene r . El tiempo constante se logra almace-
nando los árboles cartesianos para todos los bloques de la matriz. Algunas
observaciones:

Los bloques con árboles cartesianos isomórficos dan el mismo resultado


para todas las consultas en ese bloque.

El número de diferentes árboles cartesianos de s nodos es Cs , el número


catalán s.

Por lo tanto, el número de diferentes árboles cartesianos para los blo-


ques está en el rango de 4s

Para cada árbol, el posible resultado para todas las consultas debe al-
macenarse. Esto se reduce a entradas s2 u O (log2 n). Esto significa que el
tamaño general de la tabla es O(n).
Para buscar resultados de manera eficiente, el árbol cartesiano (fila) co-
rrespondiente a un bloque especı́fico debe ser direccionable en tiempo cons-
tante. La solución es almacenar los resultados para todos los árboles en una
matriz y encontrar una proyección única de árboles binarios a enteros pa-
ra direccionar las entradas. Esto se puede lograr haciendo una búsqueda de
amplitud primero a través del árbol y agregando nodos hoja para que cada
nodo existente en el árbol cartesiano tenga exactamente dos hijos. El núme-
ro entero se genera representando cada nodo interno como un bit 0 y cada
hoja como un bit en una palabra de bit (atravesando nuevamente el árbol
en orden de nivel). Esto lleva a un tamaño de registron
4
para cada árbol. Para
habilitar el acceso aleatorio en tiempo constante a cualquier árbol, los árboles
no contenidos en la matriz original también deben incluirse. Una matriz con
logn
ı́ndices de logn
4
bits de longitud tiene un tamaño 2 4 = 0(n)
Los RMQ se pueden usar para resolver el problema de ancestro común
más bajo y se usan como una herramienta para muchas tareas en la coinci-
dencia de cadenas exacta y aproximada.
A continuación la implementación de esta estructura de datos.

382
ESTRUCTURA DE DATOS

int ∗buildRMQ ( int ∗a , int n ) {


int l o g n = 1 ;
f o r ( int k=1;k<n ; k∗=2) ++l o g n ;
int ∗ r = new int [ n∗ l o g n ] ;
int ∗b = r ; copy ( a , a+n , b ) ;
f o r ( int k=1;k<n ; k∗=2){
copy ( b , b+n , b+n ) ;
b+= n ;
REP( i , n−k )
b [ i ]=min ( b [ i ] , b [ i+k ] ) ;
}
return r ;
}

int minimum ( int x , int y , int ∗rmq , int n ) {


int z=y−x , k=0, e =1, s ; //y−x>=e=2ˆk
s =(( z & 0 x f f f f 0 0 0 0 ) != 0 ) <<4; z>>=s ; e<<=s ; k|= s ;
s =(( z & 0 x 0 0 0 0 f f 0 0 ) != 0 ) <<3; z>>=s ; e<<=s ; k|= s ;
s =(( z & 0 x 0 0 0 0 0 0 f 0 ) != 0 ) <<2; z>>=s ; e<<=s ; k|= s ;
s =(( z & 0 x0000000c ) != 0 ) <<1; z>>=s ; e<<=s ; k|= s ;
s =(( z & 0 x00000002 ) != 0 ) <<0; z>>=s ; e<<=s ; k|= s ;
return min ( rmq [ x+n∗k ] , rmq [ y+n∗k−e +1] ) ;
}

En esta implementación primero invocamos al método buildRMQ pasando


el arreglo de los valores con la cantidad de elementos del mismo. Este método
construye la tabla con los valores precalculado luego para cada consulta de
rango pero se invoca al método minimum donde los parámetros x y y son
los lı́mites izquierdo y derecho respectivamente del intervalo de la consulta,
rmq el arreglo que genera el método buildRMQ y n la cantidad de elementos
del arreglo original.
Si se analiza podemos modificar las estructura para en vez de buscar
dentro de un rango el menor que busque el mayor. Si a cada elemento del
arreglo inicial lo multiplicamos por menos -1 y luego el valor encontrado
por la estructura en cada consulta lo volvemos a multiplicar por menos uno
obtendremos el menor.
O cambiamos la linea

b [ i ]=min ( b [ i ] , b [ i+k ] ) ;

Por

b [ i ]=max( b [ i ] , b [ i+k ] ) ;

De igual modificamos

383
ESTRUCTURA DE DATOS

return min ( rmq [ x+n∗k ] , rmq [ y+n∗k−e +1] ) ;

por

return max( rmq [ x+n∗k ] , rmq [ y+n∗k−e +1] ) ;

Esta implementación tiene la siguiente complejidad. El método buildRMQ


es O(nlogn) siendo n la cantidad de elementos que tiene inicialmente el arre-
glo. En el caso del método minimum su complejidad es O(1).

13.19. Descomposición raı́z cuadrada(Sqrt De-


composition)
La descomposición raı́z cuadrada (Sqrt Decomposition) es un método (o
una estructura de datos) que le permite realizar algunas operaciones comunes
(encontrar la suma de los elementos de la submatriz,
√ encontrar el elemento
mı́nimo / máximo, etc.) en operaciones O( N ) , que es mucho más rápido
que O(N ) para el algoritmo trivial.
Primero vamos a describir la estructura de datos para una de las aplica-
ciones más simples de esta idea, luego mostramos cómo generalizarla para
resolver otros problemas, y finalmente vemos un uso ligeramente diferente de
esta idea: dividir las solicitudes de entrada en bloques sqrt.

13.19.1. Estructura de datos basada en descomposi-


ción raı́z cuadrada
Dada un arreglo a[0 . . . N − 1], implemente una estructura de datos que
permita encontrar la suma√ de los elementos a[l . . . r] para valores arbitrarios
de l y r en operaciones O( N )
La idea básica de la descomposición raı́z cuadrada es el preprocesamiento.

Dividiremos en arreglo a en bloques de longitud aproximadamente N , y
para cada bloque i calcularemos previamente la suma de los elementos que
contiene en b[i].
Podemos suponer √ que tanto el tamaño del bloque como el número de
bloques son iguales a N redondeados:

S = d Ne
Luego, el arreglo a se divide en bloques de la siguiente manera:

384
ESTRUCTURA DE DATOS

a[0], a[1], . . . , a[S − 1], a[S], . . . , a[2S − 1], . . . , a[(S − 1) · s], . . . , a[N ]
| {z } | {z } | {z }
b[0] b[1] b[S-1]

El último bloque puede tener menos elementos que los otros (si N no
es un múltiplo de S ), no es importante para la discusión (ya que se puede
manejar fácilmente). Por lo tanto, para cada bloque k, conocemos la suma
de elementos en él b[k]:
mı́n (n−1,(k+1)·s−1)
X
b[k] = a[i]
i=k·s

Entonces, hemos calculado los valores de b[k] (esto requirió operaciones


O(N )). ¿Cómo pueden ayudarnos a responder cada consulta [l; r] ? Tenga
en cuenta que si el intervalo [l; r] es lo suficientemente largo, contendrá va-
rios bloques completos, y para esos bloques podemos encontrar la suma de
elementos en ellos en una sola operación. Como resultado, el intervalo [l; r]
contendrá partes de solo dos bloques, y tendremos que calcular la suma de
elementos en estas partes de manera trivial.
Por lo tanto, para calcular la suma de elementos en el intervalo [l; r] solo
necesitamos sumar los elementos de las dos colas: [l . . . (k + 1) · s − 1] y
[p · s . . . r] , y suma los valores b[i] en todos los bloques desde k + 1 hasta
p − 1:
r (k+1)·s−1 p−1 r
X X X X
a[i] = a[i] + b[i] + a[i]
i=l i=l i=k+1 i=p·s

Nota: Cuando k = p, es decir, l y r pertenecen al mismo bloque, la fórmula


no se puede aplicar y la suma se debe calcular de manera trivial.
Este enfoque nos permite reducir significativamente el número de opera-
ciones. De hecho, el tamaño de cada colas no excede la longitud del bloque
s, y √
el número de bloques en la suma no excede s. Como hemos elegido
S ≈ n, el número total de operaciones √ requeridas para encontrar la suma
de elementos en el intervalo [l; r] es O( N )

13.19.2. Implementación
Comencemos con la implementación más simple:

// d a t o s de e n t r a d a
int n ;

385
ESTRUCTURA DE DATOS

v e c t o r <int> a ( n ) ;

// p r e r p o c e s a m i e n t o
int l e n = ( int ) s q r t ( n +.0) +1; // l o n g i t u d y c a n t i d a d de b l o q u e s
v e c t o r <int> b ( l e n ) ;
f o r ( int i =0; i <n ; ++i )
b [ i / l e n ]+=a [ i ] ;

// r e s p o n d e r l a s c o n s u l t a s
for ( ; ; ) {
int l , r ;
// l e e r l o s d a t o s de e n t r a d a s para l a s i g u i e n t e c o n s u l t a
int sum = 0 ;
f o r ( int i=l ; i<=r ; )
i f ( i % l e n==0 && i+l e n −1 <= r ) {
// s i todo e l b l o q u e que comienza en i p e r t e n e c e a [ l ; r ]
sum += b [ i / l e n ] ;
i += l e n ;
} else {
sum += a [ i ] ;
++i ;
}
}

Esta implementación tiene irrazonablemente muchas operaciones de di-


visión (que son mucho más lentas que otras operaciones aritméticas). En
cambio, podemos calcular los ı́ndices de los bloques cl y cr que contienen los
ı́ndices l y r, y recorrer los bloques cl +1 . . . cr −1 con un procesamiento sepa-
rado de las colas en los bloques cl y cr . Este enfoque corresponde a la última
fórmula en la descripción y hace que el caso cl = cr sea un caso especial.

int sum=0;
int c l=l / l e n , c r=r / l e n ;
i f ( c l==c r )
f o r ( int i=l ; i<=r ; ++i )
sum += a [ i ] ;
else {
f o r ( int i=l , end=( c l +1)∗ l e n −1; i<=end ; ++i )
sum += a [ i ] ;
f o r ( int i=c l +1; i<=c r −1; ++i )
sum += b [ i ] ;
f o r ( int i=c r ∗ l e n ; i<=r ; ++i )
sum += a [ i ] ;
}

386
ESTRUCTURA DE DATOS

13.19.3. Otros problemas


Hasta ahora estábamos discutiendo el problema de encontrar la suma de
elementos de un subconjunto continuo. Este problema puede ampliarse para
permitir actualizar elementos de un arreglo de forma individual. Si
un elemento a[i] cambia, es suficiente actualizar el valor de b[k] para el bloque
al que pertenece este elemento (k = i/s) en una operación:

b[k]+ = anew [i] − aold [i]


Por otro lado, la tarea de encontrar la suma de los elementos se puede
reemplazar con la tarea de encontrar el elemento mı́nimo / máximo de una
submatriz. Si este problema también tiene que abordar las actualizaciones
de elementos individuales, también es posible actualizar el valor de b[k], pero
requerirá iterar
√ a través de todos los valores del bloque k en operaciones
O(s) = O( n).
La descomposición raı́z cuadrada se puede aplicar de manera similar a
toda una clase de otros problemas: encontrar el número de elementos cero,
encontrar el primer elemento distinto de cero, contar elementos que satisfacen
una determinada propiedad, etc.
Aparece otra clase de problemas cuando necesitamos actualizar los ele-
mentos del arreglos a intervalos: incremente los elementos existentes o re-
emplácelos con un valor dado.
Por ejemplo, supongamos que podemos hacer dos tipos de operaciones
en el arreglo: adicionar un valor dado δ a todos los elementos del arreglo en
el intervalo [l; r] o consultar el valor del elemento a[i]. Guardemos el valor
que debe adicionarse a todos los elementos del bloque k en b[k] (inicialmente
todos b[k] = 0). Durante cada operación de suma necesitamos agregar δ a
b[k] para todos los bloques que pertenecen al intervalo [l; r] y agregar δ a
a[i] para todos los elementos que pertenecen a las colas del intervalo . La
respuesta a una consulta i es simplemente √ a[i] + b[i/s]. De esta manera, la
operación suma tiene complejidad O( n), y responder una consulta tiene
complejidad O(1).
Finalmente, esas dos clases de problemas se pueden combinar si la tarea
requiere realizar actualizaciones de elementos en un intervalo√ y consultas en
un intervalo. Ambas operaciones se pueden hacer con O( n) complejidad.
Esto requerirá dos matrices de bloques b y c: una para realizar un seguimiento
de las actualizaciones de elementos y otra para realizar un seguimiento de
las respuestas a la consulta.
Existen otros problemas que pueden resolverse utilizando la descomposi-
ción raiz cuadrada, por ejemplo, un problema sobre el mantenimiento de un
conjunto de números que permitirı́a agregar / eliminar números, verificar si

387
ESTRUCTURA DE DATOS

un número pertenece al conjunto y encontrar el k-ésimo número más gran-


de. Para resolverlo, uno tiene que√almacenar los números en orden creciente,
divididos en varios bloques con N números en cada uno. Cada vez que se
agrega / elimina un número, los bloques deben reequilibrarse moviendo los
números entre el comienzo y el final de los bloques adyacentes.

13.20. Algoritmo de Mo(Mo’s algorithm)


Una idea similar, basada en la descomposición raı́z cuadrada, puede usar-
se para responder consultas de √ rango (Q) fuera del orden dado (Offline es
el térimo usado) en O((N + Q) N ). Esto puede sonar mucho peor que los
métodos de la sección anterior, ya que esta es una complejidad ligeramente
peor que la que tenı́amos antes y no puede actualizar los valores entre dos
consultas. Pero en muchas situaciones este método tiene ventajas. Durante
una descomposición raı́z cuadrada normal, tenemos que calcular previamen-
te las respuestas para cada bloque y fusionarlas durante las consultas de
respuesta. En algunos problemas, este paso de fusión puede ser bastante pro-
blemático. P.ej. cuando cada consulta solicita encontrar el modo de su rango
(el número que aparece con más frecuencia). Para esto, cada bloque tendrı́a
que almacenar el recuento de cada número en él en algún tipo de estructu-
ra de datos, y ya no podemos realizar el paso de fusión lo suficientemente
rápido. El algoritmo de Mo utiliza un enfoque completamente diferente, que
puede responder a este tipo de consultas rápidamente, ya que solo realiza un
seguimiento de una estructura de datos, y las únicas operaciones con ella son
fáciles y rápidas.
La idea es responder las consultas en un orden especial basado en los
ı́ndices. Primero responderemos todas las consultas que tengan el ı́ndice iz-
quierdo en el bloque 0, luego contestaremos todas las consultas que hayan
dejado el ı́ndice en el bloque 1 y ası́ sucesivamente. Y también tendremos
que responder a las consultas de un bloque en un orden especial, es decir,
ordenadas por el ı́ndice correcto de las consultas.
Como ya se dijo, utilizaremos una única estructura de datos. Esta es-
tructura de datos almacenará información sobre el rango. Al principio este
rango estará vacı́o. Cuando queremos responder a la siguiente consulta (en el
orden especial), simplemente ampliamos o reducimos el rango, agregando /
eliminando elementos en ambos lados del rango actual, hasta que lo transfor-
mamos en el rango de consulta. De esta manera, solo necesitamos agregar o
eliminar un solo elemento una vez a la vez, lo que deberı́a ser una operación
bastante fácil en nuestra estructura de datos.
Dado que cambiamos el orden de respuesta de las consultas, esto solo

388
ESTRUCTURA DE DATOS

es posible cuando se nos permite responder las consultas en modo fuera del
orden establecido.

13.20.1. Implementación
En el algoritmo de Mo utilizamos dos funciones para agregar un ı́ndice y
para eliminar un ı́ndice del rango que actualmente mantenemos.

void remove ( i d x ) ; //TODO: E l im i na a l v a l o r que e s t a en l a p o i s c i o n


i d x de l a e s t r u c t u r a
void add ( i d x ) ; //TODO: A d i c i o n a a l v a l o r que e s t a en l a p o i s c i o n
i d x de l a e s t r u c t u r a
int g e t a n s w e r ( ) ; //TODO: Devuelve l a r e s p u e s t a a c t u a l para l a
e s t r u c t u r a de de d a t o s

int b l o c k s i z e ;

struct Query {
int l , r , i d x ;
bool operator <(Query o t h e r ) const {
return m a k e p a i r ( l / b l o c k s i z e , r ) <
make pair ( other . l / b l o c k s i z e , other . r ) ;
}
};

v e c t o r <int> m o s a l g o r i t h m ( v e c t o r <Query> q u e r i e s ) {
v e c t o r <int> a ns w er s ( q u e r i e s . s i z e ( ) ) ;
s o r t ( q u e r i e s . b e g i n ( ) , q u e r i e s . end ( ) ) ;

// TODO: I n i c i a l i z a r l a e s t r u c t u r a

int c u r l = 0 ;
int c u r r = −1;
// i n v a r i a n t e : La e s t r u c t u r a de d a t o s s i e m p r e r e f l e j a e l
rango [ c u r l , c u r r ]
f o r ( Query q : q u e r i e s ) {
while ( c u r l > q . l ) {
c u r l −−;
add ( c u r l ) ;
}
while ( c u r r < q . r ) {
c u r r ++;
add ( c u r r ) ;
}
while ( c u r l < q . l ) {
remove ( c u r l ) ;
c u r l ++;
}

389
ESTRUCTURA DE DATOS

while ( c u r r > q . r ) {
remove ( c u r r ) ;
c u r r −−;
}
a ns w er s [ q . i d x ] = g e t a n s w e r ( ) ;
}
return a ns w er s ;
}

Según el problema, podemos usar una estructura de datos diferente y


modificar las funciones agregar / quitar / obtener respuesta según corres-
ponda. Por ejemplo, si se nos pide que busquemos consultas de suma de
rango, entonces usamos un entero simple como estructura de datos, que es 0
al principio. La función de agregar simplemente agregará el valor de la posi-
ción y posteriormente actualizará la variable de respuesta. Por otro lado, la
función remove restará el valor en la posición y posteriormente actualizará
la variable de respuesta. Y get answer solo devuelve el entero.
Para responder consultas de modo, podemos usar un árbol de búsqueda
binario (por ejemplo, map <int, int>) para almacenar la frecuencia con la
que cada número aparece en el rango actual, y un segundo árbol de búsqueda
binario (por ejemplo, set <pair <int, int> ¿) para mantener los recuentos
de los números (por ejemplo, como pares de recuento-número) en orden El
método add elimina el número actual del segundo BST, aumenta el recuento
en el primero e inserta el número nuevamente en el segundo. remove hace lo
mismo, solo disminuye el conteo. Y get answer solo mira el segundo árbol y
devuelve el mejor valor en O (1).

13.20.2. Complejidad
Ordenar todas las consultas tomará O(Q log Q).
¿Qué hay de las otras operaciones? ¿Cuántas veces se llamará agregar y
quitar?
Digamos que el tamaño del bloque es S.
Si miramos solo miramos todas las consultas que con el ı́ndice izquierdo
en el mismo bloque. Las consultas se ordenan por el ı́ndice derecho. Por lo
tanto, llamaremos a add (cur r) y remove (cur r) solo O O(N ) veces para
todas estas consultas combinadas. Esto proporciona llamadas O( NS N ) para
todos los bloques.
El valor de cur l puede cambiar como máximo O(S) durante entre dos
consultas. Por lo tanto, tenemos un adicional O(SQ) llamadas de add (cur l)
y remove (cur√l). √
Para S ≈ N esto da operaciones O((N + Q) N ) en total. Por lo tanto,

390
ESTRUCTURA DE DATOS


la complejidad es O((N + Q)F N ) donde O(F ) es la complejidad de las
funciones agregar y quitar.

Consejos para mejorar el tiempo de ejecución:


El tamaño de bloque de √ N no siempre ofrece el mejor tiempo de
ejecución. Por ejemplo, si N = 750 , puede ocurrir que el tamaño de
bloque de 700 u 800 funcione mejor. Más importante aún, no calcules el
tamaño del bloque en tiempo de ejecución, hazlo constante. La división
por constantes está bien optimizada por los compiladores.

En bloques impares ordene el ı́ndice derecho en orden ascendente y en


bloques pares ordene en orden descendente. Esto minimizará el mo-
vimiento del puntero derecho, ya que la ordenación normal moverá el
puntero derecho desde el final hasta el principio al comienzo de cada
bloque. Con la versión mejorada, este restablecimiento ya no es nece-
sario.

bool cmp( p a i r <int , int> p , p a i r <int , int> q ) {


i f ( p . f i r s t /BLOCK SIZE!=q . f i r s t /BLOCK SIZE)
return p < q ;
return ( p . f i r s t /BLOCK SIZE & 1 ) ? ( p . second<q . s e c o n d ) : ( p .
second>q . s e c o n d ) ;
}

391
ESTRUCTURA DE DATOS

392
ESTRUCTURA DE DATOS

13.21. Árbol AVL


13.22. Árbol binario de búsqueda
13.23. Heap Binomial
13.24. Bit Queue
13.25. Circular Doubly-Linked List
13.26. Fibonacci Heap
13.27. Link-cut Tree
13.28. Pairing Heap
13.29. Proto-vEB Tree
13.30. XOR Linked List
13.31. van Emde Boas Tree
13.32. Trie
13.33. Suffix Automaton
13.34. Suffix Array
13.35. Suffix Array + LCP Array
13.36. Thurs Splay
13.37. Treap
13.38. Thurs interval
13.39. 393
Grilla de Buckets
13.40. Análisis de ejercicios
3526 - Unordered List Usar un árbol binario con alguna modificación
para en tiempo de inserción saber la cantidad de mayores al elemento a
ESTRUCTURA DE DATOS

insertar. Insertar los elementos en orden contrario a como fueron leı́dos.

2249 - Curious Robin Hood El problema se resuelve usando un árbol


binario indexado o árbol de Fenwick.

3632 - Tobby and Query La solución de problema se realiza usando


una estructura de datos que haga una consulta en el rango dado de manera
eficiente. Es por eso que para la solución utilizaremos un RangeTree con una
adecuaciones para ajustarlo para que sea parte de la solución del problema
pero antes de entrar en las adecuaciones analicemos algunos aspectos del
problema. Nos piden dado un rango ver cuántos números distintos existen
en dicho rango sabiendo que los valores oscilan entre 0 y 9, esto es muy útil
porque para cada posible rango podemos tener un arreglo booleanos de 10
posiciones donde si la posición i es true significa que esa valor está presente
en el rango y false en caso contrario. En caso de unir dos intervalos seria
realizar un OR posición a posición de los arreglos de cada intervalo. Visto
esto la solución es correcta pero tiene un detalle y es el uso de memoria ya
que por cada intervalo que genere el RangeTree se va generar un arreglo de
10 posiciones, pues bien veamos cómo podemos reducir un tanto ese gasto
de memoria pues bien resulta que el peor caso es todas las posiciones del
arreglo en true 1111111111 y el peor caso es todo en false 0000000000 y
cómo podemos ver tenemos dos números binarios en el rango de 210 − 1
a 20 que llevados a decimales es 1023 a 0 valores que caben en un int sin
problemas. Pues bien resuelto el problema de memoria es evidente que de
un arreglo de booleanos vamos a guardar la información en un int hay una
mejora en cuanto la optimización de memoria, sabiendo que la respuesta es
la cantidad de bit en uno que tiene ese valor en su representación en binario.
Veamos entonces las adecuaciones:

void I n i t T r e e ( int idx , int l e f t , int r i g h t )


{
i f ( l e f t == r i g h t )
{
ST [ i d x ] = pow ( 2 , v a l u e s [ l e f t ] ) ;
return ;
}
ST [ i d x ] = (ST [ 2 ∗ i d x ] | ST [ 2 ∗ i d x +1]) ;
}

int Query ( int idx , int l , int r , int l e f t , int r i g h t )


{
i f ( l <= l e f t && r i g h t <= r ) return ST [ i d x ] ;
ULL r e t = 0 ;

394
ESTRUCTURA DE DATOS

i f ( l <= MID) r e t = r e t | Query ( 2 ∗ idx , l , r , l e f t , MID) ;


i f ( r > MID) r e t = r e t | Query ( 2 ∗ i d x +1, l , r , MID+1, r i g h t ) ;
return r e t ;
}

La respuesta es la cantidad de bit en uno que tiene la representación en


binario del valor que devuelve el método Query.

3484 - Work for Ten Este ejercicio es similar al 3632 Tobby and Query
su solución parte de la utilización de una estructura de datos RangeTree con
sus respectivas adecuaciones. Ahora el RangeTree lo vamos hacer sobre las
habitaciones y no sobre las personas. La primera adecuación que se tiene
que hacer es cambiar el tipo de dato del RangeTree, en los casos de estudio
siempre se ha visto la estructura trabaja con int ahora vamos a cambiarlo
por un vector de int, serı́a algo como vector<int> ST[MAXTREE] donde
para cada intervalo se almacenara en un vector como máximo los 10 más
inteligentes dentro de ese rango. Las otras adecuaciones que se deben hacer
es los métodos InitTree y Query en el primero :

void I n i t T r e e ( int idx , int l e f t , int r i g h t )


{
i f ( l e f t == r i g h t )
{
/∗ Leemos l a c a n t i d a d de p e r s o n a s de e s t a h a b i t a c i \ ’ on
y luego la i n t e l i g e n c i a de cada una l a
a d i c i o n a m o s en e l v e c t o r ST [ i d x ] , por u l t i m o
ordenamos ∗/
}
/∗ Pedimos l o s v e c t o r e s o r d e n a d o s de ST [ 2 ∗ i d x ] y ST [ 2 ∗ i d x
+1] e i n s e r t a m o s en ST [ i d x ] l o s d i e z menores v a l o r e s
e n t r e l o s dos v e c t o r e s ∗/
}

En el método Query hacemos forma similar cuando está en el rango senci-


llamente se devuelve el vector correspondiente a ese rango cuando se debe
forma a partir de dos intervalos se coge los dos vectores de cada sub-rango y
se arma un nuevo vector solución con los diez menores valores entre los dos
vectores. Ojo no siempre los vectores van tener diez elementos a veces tienen
menos.

3683 - Who’s left? Who’s right? Con la utilización de set de C++ y


el uso de los metodos upper bounds y lower bound de dicha estructura basta
para resolver el problema. Solo debemos tener una estructra de datos ante-
riormente mencionada con los valores 0 y 1000000 insertados inicialemente.

395
ESTRUCTURA DE DATOS

Luego para cada Ai aplicar los metodos mencionados y capturar los itera-
dores que devuelven dichas funcionales, en caso de iterador que devuelve
upper bounds decrementarlo en uno y mandar a imprimir los valores a que
apunta esos iteradores.

3793 - Rockabye Tobby Para solucionar el problema solo debemos crear


una estructura que contenga el nombre del medicamento, la frecuencia , el
tiempo que debe ser tomada el medicamento y la prioridad del medicamen-
to. Esta estructura le podemos llamar dosis. Luego con la utilización de una
cola con prioridad (debemos reimplementar el operador menor que donde el
primer criterio es la hora a que debe ser tomada la dosis y como segundo
criterio en caso de que el anterior sea identico, es la prioridad de los medi-
camentos) inicialemente adicionamos todos los medicamentos como dosis y
vamos a sacarlo de la cola e imprimirlo mientras k sea distinto de cero. Muy
importante cada vez que se extrae de la cola una dosis se debe calcular la
proxima dosis de este medicamento y adicionarlo a la cola.

3231 - Utrka El ejercicio nos explica que en los maratones de Zagreb


que se celebra todos los años de los N participantes solo llegan N-1 y nos
piden dado los listados de los corredores que iniciaron la carrera y los que
terminaron debemos hallar cual fue el corredor que no termino la carrera. La
solución es bien sencilla basta con tener un diccionario donde la clave será
los nombres de los corredores mientras los valores asociados a estos serán la
cantidad de veces que aparecen dichos nombres en los dos listados. Luego
solo basta con recorrer el diccionario y buscar la primera clave cuyo valor
asociado sea impar e imprimir dicha clave.

3956 - Street Parade Una vez leido e interpretado el problema nos po-
demos percatar que la calle auxiliar de la que se nos habla en el problema
la podemos ver como una estructura de datos de tipo pila. Y utilizando una
estructura de este tipo podemos resolver el problema de la siguiente manera.
Bien vamos a tener una variable que su valor inicial es 1 y no vas indicar que
carro yo espero que pase. Luego voy por cada carro que entre si su chapa es
la que yo esperaba incremento en uno la variable que me indica la chapa del
carro que yo espero y paso el carro. En caso de que la chapa de carro que
analizo no sea la que espero almaceno dicho carro en la pila. Luego empiezo a
sacar los carros de la pila si cuando saco un carro de la pila su chapa coincide
con la chapa esperada, incremento en uno la chapa esperaba, caso contrario
la respuesta a ese caso es no mientras si logro sacar todos los carros de la
pila sin problema entonces la respuesta para ese caso es yes.

396
ESTRUCTURA DE DATOS

3954 - The Evarista Store Una leı́do el problema y si tuvimos el detalle


de leernos el epı́grafe de Árbol binario indexado o Thurs Fenwick, nos po-
demos percatar que dicha estructura se ”pinta”sola para ser utilizada para
resolver dicho ejercicio. Quiza la única adecuación o detalle que debemos te-
ner en cuenta es que es necesario llevar en una colección adicional para para
llevar las actualizaciones que se llevan sobre las posiciones de las colecciones.
Porque la estructura anteriormente mencionada solo nos permitir llevar la
suma acumulada para rangos.

1422 - Range Multiplication Una vez leı́do el problema si nos detuvimos


a leer el epı́grafe Propagación diferida (Lazy Propagation) podemos ver que la
solución al problema radica aplicar una estructra de este tipo. Solo debemos
hacer alguna modificación en los métodos de actualización y consulta de la
estructura de datos.

2791 - Join The Game With Plastic Soldiers Por lo descripto en el


problema se puede resolver aplicando un Range Tree con Lazy Progation.
Se debe tener en cuenta que cada vez que un jugador toma un rango una
vez hallada la suma del rango, se debe actualizar el rango antes que el otro
jugador seleccione su rango. Lo otro es llevar dos contadores que lleven la
cuentan de las rondas ganadas por cada jugador.

397
ESTRUCTURA DE DATOS

398
Capı́tulo 14

Greedy

El término “algoritmo goloso”es una traducción libre del inglés ’greedy


algorithms’, en algunos textos se refiere a este tipo de algoritmos como al-
goritmos ’avidos’. Los algoritmos golosos se utilizan para resolver problemas
de optimización y son extremadamentes sencillos, se basan en un principio
fundamental:
Un algoritmo goloso siempre toma la decisión que parece mejor
de acuerdo al estado actual de problema.
Un algoritmo goloso funciona construyendo una solución en base a una
secuencias de decisiones golosas, se dice que la decisión que toman es-
tos algoritmos es golosa porque si bien son optimas en el estado actual del
problema no implica que lleven necesariamente a una solución óptima del
problema.
Algunos algoritmos golosos son óptimos, en el sentido de que terminan
generando una solución que es óptima para el problema planteado, otros
no son óptimos pero sirven para encontrar una buena aproximación de la
solución, por último en algunos casos un algoritmo goloso puede llevar a
cualquier solución aleatoriamente alejada de la solución óptima.

14.0.1. Fundamentos
Para que un problema pueda ser solucionado en forma óptima mediante
un algoritmo goloso debe presentar dos caracterı́sticas fundamentales:

- Subestructura óptima. El problema debe poder ser descompuesto en


varios subproblemas de forma que si la solución del problema es óptima
la solución de los subproblemas también lo es. (Este requisito también
era necesario para el método de programación dinámica).

399
GREEDY

- Opción golosa. Debe ser posible demostar que una elección localmente
óptima lleva a una solución globalmente óptima. Esta es la clave de
los algoritmos golosos y en general se demuestra por inducción, en pri-
mer lugar se prueba que si existe una solución óptima entonces existe
también una solución que parte de una decisión golosa. Luego para el
subproblema restante por inducción se prueba que se puede llegar a
una solución óptima.

Dado un problema con n entradas el método consiste en obtener un sub-


conjunto de éstas que satisfaga una determinada restricción definida para el
problema. Cada uno de los subconjuntos que cumplan las restricciones dire-
mos que son soluciones prometedoras. Una solución prometedora que maxi-
mice o minimice una función objetivo la denominaremos solución óptima.
Como ayuda para identificar si un problema es susceptible de ser resuelto
por un algoritmo ávido vamos a definir una serie de elementos que han de
estar presentes en el problema:

Un conjunto de candidatos, que corresponden a las n entradas del pro-


blema.

Una función de selección que en cada momento determine el candida-


to idóneo para formar la solución de entre los que aún no han sido
seleccionados ni rechazados.

Una función que compruebe si un cierto subconjunto de candidatos


es prometedor. Entendemos por prometedor que sea posible seguir
añadiendo candidatos y encontrar una solución.

Una función objetivo que determine el valor de la solución hallada. Es


la función que queremos maximizar o minimizar.

Una función que compruebe si un subconjunto de estas entradas es


solución al problema, sea óptima o no.

Con estos elementos, podemos resumir el funcionamiento de los algoritmos


ávidos en los siguientes puntos:

1. Para resolver el problema, un algoritmo ávido tratará de encontrar un


subconjunto de candidatos tales que, cumpliendo las restricciones del
problema, constituya la solución óptima.

400
GREEDY

2. Para ello trabajará por etapas, tomando en cada una de ellas la decisión
que le parece la mejor, sin considerar las consecuencias futuras, y por
tanto escogerá de entre todos los candidatos el que produce un óptimo
local para esa etapa, suponiendo que será a su vez óptimo global para
el problema.

3. Antes de añadir un candidato a la solución que está construyendo com-


probará si es prometedora al añadilo. En caso afirmativo lo incluirá en
ella y en caso contrario descartará este candidato para siempre y no
volverá a considerarlo.

4. Cada vez que se incluye un candidato comprobará si el conjunto obte-


nido es solución.

Resumiendo, los algoritmos ávidos construyen la solución en etapas suce-


sivas, tratando siempre de tomar la decisión óptima para cada etapa.
Entonces cabe preguntarse ¿por qué no utilizarlos siempre? En primer
lugar, porque no todos los problemas admiten esta estrategia de solución.
De hecho, la búsqueda de óptimos locales no tiene por qué conducir siempre
a un óptimo global, como mostraremos en varios ejemplos de este capı́tulo.
La estrategia de los algoritmos ávidos consiste en tratar de ganar todas las
batallas sin pensar que, como bien saben los estrategas militares y los ju-
gadores de ajedrez, para ganar la guerra muchas veces es necesario perder
alguna batalla.
Desgraciadamente, y como en la vida misma, pocos hechos hay para los
que podamos afirmar sin miedo a equivocarnos que lo que parece bueno
para hoy siempre es bueno para el futuro. Y aquı́ radica la dificultad de
estos algoritmos. Encontrar la función de selección que nos garantice que el
candidato escogido o rechazado en un momento determinado es el que ha
de formar parte o no de la solución óptima sin posibilidad de reconsiderar
dicha decisión. Por ello, una parte muy importante de este tipo de algoritmos
es la demostración formal de que la función de selección escogida consigue
encontrar óptimos globales para cualquier entrada del algoritmo. No basta
con diseñar un procedimiento ávido, que seguro que será rápido y eficiente
(en tiempo y en recursos), sino que hay que demostrar que siempre consigue
encontrar la solución óptima del problema.

401
GREEDY

14.1. Intervalo de Tareas (Interval Schedu-


ling)
Entre los problemas que se puede resolver utilizando un algoritmo greedy
se encuentra aquel que te dan un grupo de actividades, programas o tareas
para las esta definido el tiempo de inicio y final de las mismas y se nos pide
determinar la cantidad de actividades ( muy importante nos piden cantidad,
no la máxima cantidad posible ) , programas o tareas que se pueden realizar
teniendo como restricciones que no se puede pasar de una otra hasta que no
se termine la primera.
El primer paso para diseñar un algoritmo greedy que solucione el problema
es ordenar de forma ascendente las tareas por su tiempo de inicio, caso de
existir dos tareas con similar tiempo de inicio se ordenan por el tiempo de
finalización de forma ascendente.
La segunda parte del algoritmo vamos recorrer la colección de tareas
ordenadas a partir de la segunda, porque voy asumir que realizare la primera
tarea de la colección. Por cada tarea que su tiempo de inicio sea mayor o
igual que el tiempo de finalización de la ultima tarea realizada voy tomar
esa tarea como la ultima realizada y aumentar en uno el contador de tareas
realizadas. A continuación la implementación del algoritmo.

#include <s t d i o . h>


#include <i o s t r e a m >
#include <a l g o r i t h m >
#define MAX N 100001
using namespace s t d ;

int n ;

struct I n t e r v a l
{
int L , R;
bool operator <(const I n t e r v a l &a ) const
{
i f (R != a .R) return (R < a .R) ;
return (L < a . L) ;
}
};
I n t e r v a l I [MAX N ] ;

i n l i n e int S c h e d u l e I n t e r v a l s ( )
{
s o r t ( I , I+n ) ;
int r e t = 1 ;

402
GREEDY

int currentEnd = I [ 0 ] . R;
f o r ( int i =1; i <n ; i ++)
{
i f ( I [ i ] . L >= currentEnd )
{
currentEnd = I [ i ] . R;
r e t ++;
}
}

return r e t ;
}

int main ( )
{
n = 4;

I [0].L = −1, I [ 0 ] . R = 1 ;
I [1].L = 0 , I [ 1 ] .R = 5;
I [2].L = 2 , I [ 2 ] .R = 3;
I [3].L = 5 , I [ 3 ] .R = 9;

p r i n t f ( " %d\n" , S c h e d u l e I n t e r v a l s ( ) ) ;
return 0 ;
}

Esta implementación tiene una complejidad de O(N log N ) donde N es la


cantidad de tareas.

14.2. El fontanero diligente


Un fontanero necesita hacer n reparaciones urgentes, y sabe de antemano
el tiempo que le va a llevar cada una de ellas: en la tarea i-ésima tardará ti
minutos. Como en su empresa le pagan dependiendo de la satisfacción del
cliente, necesita decidir el orden en el que atenderá los avisos para minimizar
el tiempo medio de espera de los clientes.
En otras palabras, si llamamos Ei a lo que espera el cliente i-ésimo hasta
ver reparada su averı́a por completo, necesita minimizar la expresión:

Deseamos diseñar un algoritmo ávido que resuelva el problema y probar


su validez, bien mediante demostración formal o con un contraejemplo que
la refute.

403
GREEDY

En primer lugar hemos de observar que el fontanero siempre tardará el


mismo tiempo global T = t1 + t2 + ... + tn en realizar todas las reparaciones,
independientemente de la forma en que las ordene. Sin embargo, los tiempos
de espera de los clientes sı́ dependen de esta ordenación.
En efecto, si mantiene la ordenación original de las tareas (1, 2, ..., n), la
expresión de los tiempos de espera de los clientes viene dada por:

Lo que queremos encontrar es una permutación de las tareas en donde se


minimice la expresión de E(n) que, basándonos en las ecuaciones anteriores,
viene dada por:

Vamos a demostrar que la permutación óptima es aquella en la que los


avisos se atienden en orden creciente de sus tiempos de reparación.
Para ello, denominemos X = (x1 ,x2 ,...,xn ) a una permutación de los ele-
mentos (1,2,...,n), y sean (s1 ,s2 ,...,sn ) sus respectivos tiempos de ejecución,
es decir, (s1 ,s2 ,...,sn ) va a ser una permutación de los tiempos orginales (t1
,t2 ,...,tn ). Supongamos que no está ordenada en orden creciente de tiempo
de reparación, es decir, que existen dos números xi < xj tales que si > sj .
Sea Y = (y1 ,y2 ,...,yn ) la permutación obtenida a partir de X intercam-
biando xi con xj , es decir, yk = xk si k 6= i y k 6= j, y i = x j , yj = xi
.
Si probamos que E(Y ) < E(X) habremos demostrado lo que buscamos,
pues mientras más ordenada (según el criterio dado) esté la permutación,
menor tiempo de espera supone. Pero para ello, basta darse cuenta que

y que por tanto

En consecuencia, el algoritmo pedido consiste en atender a las llamadas


en orden inverso a su tiempo de reparación. Con esto conseguirá minimizar
el tiempo medio de espera de los clientes, tal y como hemos probado.

404
GREEDY

14.2.1. Mas fontaneros


Supongamos que en la empresa del fontanero del apartado anterior au-
menta el número de clientes debido a su buena calidad de servicio y deciden
contratar a más personal, con lo que disponen de un total de F fontaneros
para realizar las n tareas.
Modificar el diseño del algoritmo para que realice la asignación de tareas
a fontaneros siguiendo con el criterio de calidad expuesto anteriormente.
En este caso también tenemos que minimizar el tiempo medio de espera
de los clientes, pero lo que ocurre es que ahora existen F fontaneros dando
servicio simultámeamente. Basándonos en el método utilizado anteriormente,
la forma óptima de atender los avisos va a ser la siguiente:
En primer lugar, se ordenan los avisos por orden creciente de tiempo
de reparación.
Un vez hecho esto, se van asignando los avisos por este orden, siempre
al fontanero menos ocupado. En caso de haber varios con el mismo
grado de ocupación, se escoge el de número menor.
En otras palabras, si los avisos están ordenados de forma que ti ≤ tj si
i < j, asignaremos al fontanero k los avisos k, k+F, k+2F, ...

14.3. Los ficheros y el disquete


Supongamos que disponemos de n ficheros f1 , f2 , ..., fn con tamaños
l1 , l2 , ..., ln y un disquete de capacidad d < l1 + l2 + ... + ln . Queremos
maximizar el número de ficheros que ha de contener el disquete, y para eso
ordenamos los ficheros por orden creciente de su tamaño y vamos metiendo
ficheros en el disco hasta que no podamos meter más. Determinar si este
algoritmo ávido encuentra solución óptima en todos los casos.
Supongamos los ficheros f1 , f2 , ..., fn ordenados respecto a su tamaño,
esto es, l1 ≤ l2 ≤ ... ≤ ln . Dicho de otra forma, si llamamos L a la función
que devuelve la longitud de un fichero dado, lo que tenemos es que L(f1 ) ≤
L(f2 ) ≤ ... ≤ L(fn ).
El algoritmo ávido indicado en el enunciado de este apartado sugiere ir
tomando los ficheros según están ordenados hasta que no quepa ninguno más.
Vamos a demostrar que el número de ficheros que caben de esta forma es
el óptimo. Sea m el número de ficheros
P que dice el algoritmo que caben en
un disquete
P de capacidad d. Sid ≥ L(fi ) entonces m coincide con n. Pero
si d < L(fi ), por la forma en la que trabaja el algoritmo sabemos que se
verifica la siguiente relación:

405
GREEDY

Sea entonces g1 , g2 , ..., gs otro subconjunto de ficheros que caben también


en el disquete, es decir, tal que

Veamos que s ≤ m. En primer lugar, vamos a suponer sin pérdida de


generalidad que el conjunto de los ficheros gi está también ordenado en orden
creciente de tamaño:

Como ambas descomposiciones son distintas, sea k el primer ı́ndice tal


que fk 6= gk . Podemos suponer sin perder generalidad que k=1, puesto que
si hasta fk–1 los ficheros son iguales podemos eliminarlos y restar la suma de
los tamaños de tales ficheros a la capacidad de nuestro disquete.
Por la forma en que funciona el algoritmo, si f1 6= g1 entonces L(f1 ) ≤
L(g1 ) pues f1 era el fichero de menor tamaño. Además, g1 corresponderá a un
fichero fa en la ordenación inicial, con a > 1. Análogamente, g2 corresponderá
a un fichero fb en la ordenación inicial, con b > a > 1, y por tanto b > 2,
por lo que L(g2 ) ≥ L(g2 ). Repitiendo el razonamiento, los ficheros gi se
corresponderán con ficheros de la ordenación inicial, pero siempre cumpliendo
que:

Ahora bien, por la relaciones [4.6] y [4.7] obtenemos

Pero entonces, por [4.5], s ha de ser estrictamente menor que m+1, y por
tanto s ≤ m, como querı́amos demostrar.

14.4. El camionero con prisa


Un camionero conduce desde Bilbao a Málaga siguiendo una ruta dada y
llevando un camión que le permite, con el tanque de gasolina lleno, recorrer
n kilómetros sin parar. El camionero dispone de un mapa de carreteras que
le indica las distancias entre las gasolineras que hay en su ruta. Como va
con prisa, el camionero desea pararse a repostar el menor número de veces
posible.

406
GREEDY

Deseamos diseñar un algoritmo ávido para determinar en qué gasolineras


tiene que parar y demostrar que el algoritmo encuentra siempre la solución
óptima.
Supondremos que existen G gasolineras en la ruta que sigue el camionero
entre Bilbao y Málaga, incluyendo una en la ciudad destino, y que están
numeradas del 0 (gasolinera en Bilbao) a G–1 (la situada en Málaga).
Supondremos además que disponemos de un vector con la información
que tiene el camionero sobre las distancias entre ellas. De forma que el i-
ésimo elemento del vector indica los kilómetros que hay entre las gasolineras
i–1 e i. Para que el problema tenga solución hemos de suponer que ningún
valor de ese vector es mayor que el número n de kilómetros que el camión
puede recorrer sin repostar.
Con todo esto, el algoritmo ávido pedido va a consistir en intentar recorrer
el mayor número posible de kilómetros sin repostar, esto es, tratar de ir desde
cada gasolinera en donde se pare a repostar a la más lejana posible, ası́ hasta
llegar al destino.
Para demostrar la validez de este algoritmo ávido, sean x1 ,x2 ,...,xs las
gasolineras en donde este algoritmo decide que hay que parar a repostar, y
sea 1 ,y2 ,...,yt otro posible conjunto solución de gasolineras. Llamaremos X
a un camión que sigue la primera solución, e Y a un camión que se guı́a por
la segunda. Sea N el número total de kilómetros a recorrer (distancia entre
las dos ciudades), y sea D[i] la distancia recorrida por el camionero hasta la
i-ésima gasolinera (1 ≤ i ≤ G–1). Es decir,

Lo que tenemos que demostrar es que s ≤ t, puesto que lo que querı́amos


minimizar era el número de paradas a realizar. Para probarlo, basta con
demostrar que xk ≥ yk para todo k.
En primer lugar, como ambas descomposiciones son distintas, sea k el
primer ı́ndice tal que xk 6= yk .Podemos suponer sin perder generalidad que
k=1, puesto que hasta xk–1 los viajes son iguales, y en la gasolinera xk–1
ambos camiones rellenan su tanque completamente.
Por la forma en que funciona el algoritmo, si x1 6= y1 entonces x1 > y1 ,
pues x1 era la gasolinera más alejada a donde podı́a viajar el camionero sin
repostar.
Además, también se tiene que x2 ≥ y2 , pues x2 era la gasolinera más
alejada a donde podı́a viajar desde x1 el camionero sin repostar. Para probar
este hecho, supongamos por reducción al absurdo que y2 fuera estrictamente
mayor que x2 . Pero si Y consigue ir desde y1 a y2 es que hay menos de n

407
GREEDY

kilómetros entre ellas, es decir,

Por tanto desde x1 también hay menos de n kilómetros hasta y2 , esto es,

puesto que D[y1 ] < D[x1 ]. Entonces el método no hubiera escogido x2


como siguiente gasolinera a x1 sino y2 , porque el algoritmo busca siempre la
gasolinera más alejada de entre las que alcanza.
Repitiendo el proceso, vamos obteniendo en cada paso que xk ≥ yk para
todo k, hasta llegar a la ciudad destino, lo que demuestra la hipótesis.

14.5. Análisis de ejercicios


2236 - Twins Sumar todos los elementos, ordenarlos de mayor a menor
y tomar los k primeros que su suma sea mayor que la suma de los restantes
elementos. Devolver la cantidad de k selecionados.

3401 - Find the Replacements Su explicación capı́tulo de cadena, por-


que para poder usar un greedy hay que partir de la utilización de un algoritmo
de cadena.

3735 - My Longest Palindrome Tener un simple arreglo donde en cada


posición se guardará la cantidad de veces que existe de esa letra. A la hora de
conformar el palı́ndromo vamos a recorrer el arreglo desde la a-z y mientras
la cantidad en esa posición sea mayor de dos vamos a insertar el carácter
correspondiente por el final de A y principio de B descontando 2 unidades
la cantidad en esa posición del arreglo luego recorremos de nuevamente el
arreglo y buscamos la primera posición donde su valor sea mayor de 0 y
C ponemos la letra del alfabeto correspondiente a esa posición. Se manda
imprimir ACB en ese orden.

3336 - TETA El problema vamos dividirlo en dos en aquellos platos


que no son parte del menú y los que sı́. Los que no pertenecen abonan a la
solución general directamente mientras los platos que forman parte de menú
hay que ver que es más rentable si pagar los platos por separados o pagarlo
como parte del menú.

2619 - Domino Aunque el ejercicio fue clasificado como greedy de nivel 3.


La verdad el análisis que le realice fue con algo de grafo, sobre todo utilizando

408
GREEDY

un algoritmo cuya lógica es similar a la busqueda a lo ancho de un grafo o


árbol. Pero para esto primero debemos definir algunos aspectos. Lo primero
es entender de que se trata y de que va el problema, el cual nos da una grupo
de fichas de domino y debemos determinar la menor cantidad de ficha que
debo girar 1800 para que la suma de los números de la parte superior de
las fichas sea par y a la vez la suma de los números inferiores de las fichas
también sea par. Lo segundo que hay que recordar es lo siguiente:

número impar + número impar = número par

número par + número par = número par

número impar + número par = número impar

Lo tercero que debemos tener en cuenta es que existen cuatro tipos de


fichas de domino en lo que respecta al problema. El primer tipo están las
fichas que sus números son pares, este tipo de ficha no interesa o no afecta a
la solución porque si la giramos igual seguimos sumando en ambas partes un
partes valores pares. El segundo tipo de ficha es la que ambos números son
impares estos tampoco conviene girarlos por que igual van seguir sumando
valores impares. Esto nos deja tipos fichas tres y cuatro donde en el tercer
grupo están aquellas que su valor superior es par mientras su valor inferior
es impar mientras en el cuarto el valor impar esta en la parte superior de la
ficha y el valor par está en la inferior.
A partir de esto podemos establecer algunos casos bases, situaciones sim-
plificadas de la cantidad de fichas de cada tipo que nos podemos encontrar
a las cuales le calculamos el mı́nimo de fichas que hay que girar para que las
sumas superior e inferior sean pares.

Tipo I Tipo II Tipo III Tipo IV cant. min. de giros


n 0 0 0 0
n 1 1 1 0
n 0 2 0 0
n 0 0 2 0
n 2 0 0 0
n 1 2 0 1
n 1 0 2 1
n 0 1 1 1

Como se puede observar la cantidad de fichas de tipo I no afecta en la


solución del problema. Mientras se puede decir que para determinadas canti-

409
GREEDY

dades de tipos II,III y IV hay solución siempre que se puedan descomponer


en combinaciones de los casos bases expuestos en la tabla.
Por los datos del problema sabemos que la máxima cantidad de fichas
de un tipo es 100 y el mı́nimo es 0. Este rango me dio la idea de crear
una matriz trideminsional donde el valor almacenado en board[x][y][z] es el
número mı́nimo de giros que debo hacer con x cantidad de fichas de tipo III,
y cantidad de fichas de tipo IV y z cantidad de fichas de tipo II. Es de notar
también que la minı́ma cantidad de giros para una cominación de x de tipo
III, y de tipo IV y z de tipo II es igual la cominación de y de tipo III, x de
tipo IV y z de tipo II. Esto significa que el valor de board[x][y][z] es igual
board[y][x][z].
A partir de los explicado anteriormente se puede implementar un algorit-
mo con una lógica similar a un bfs donde se parte de la cominacion (0,0,0) y
se inicializa toda la matriz con -1. Para saber de una posición cuales son los
próximos visitar se debe utilizar la tabla descrita que según la combinación
utilizada tendrá un costo. Una vez finalizado el algoritmo las posiciones de
la matriz tridimensional donde su valor -1 significa que no existe solución,
mientras en aquellas que su valor sea distinto de -1 esa será la solución. Solo
nos queda leer cada entrada y contar la cantidad de fichas de cada tipo y la
solución será board[cant. tipo III][cant. tipo IV][cant. tipo II].

3932 - Professor Farnsworth’s Gift El ejercicio es bien sencillo de resol-


ver. Lo primero es que vamos a leer el número pero como si fuera una cadena
de carácteres y no como número esto nos va ser más facil la implementación.
Lo siguiente es determinar si la cadena de carácteres es un número positivo
o negativo. Solo basta por saber que carácter esta en la primera posición. Si
es un número positivo solo basta ordenar la cadena de carácteres de menor a
mayor y en caso de que la primera posición sea igual al carácter cero se cam-
bia con la primera posición (comenzando desde 0 hasta N-1) donde el valor
sea distinto de valor mencionado anteriormente. En caso que sea negativo y
como debemos conformar el menor número posible en este caso ordenamos
de mayor a menor( tener en cuenta que si se ordena toda la cadena se ordena
tambı́en el sı́mbolo -, por lo que luego tendremos que colocarlo en la primera
posición).

2741 - Coco-Bits and Permutations Una vez leido el problema pode-


mos comprender que dada una secuencia a1 , a2 , a3 , ... ,an de N elementos nos
piden transformar la secuencia en una de las posibles permutaciones que se
generan con un conjunto de 1 a N con la mı́nima cantidad de cambios posible,
donde un cambio cuenta como un incremento o decremento en una unidad

410
GREEDY

de algún elemento de la secuencia inicial. Para dar solución al problema va-


mos a tener un arreglo booleano permutación con una dimensión mayor a N,
inicializada cada posición con valor falso y una colección para almacenar los
elementos de la secuencia a.En el proceso de lectura de la secuencia de los
elementos:
Si elemento leido esta en el rango de 1 a N y permutacion en la posición
del valor de ese elemento es falso
permutacion en esa posición se pone en verdadero
Sino
se adiciona el elemento a la colección.
Luego ordenamos los valores de la colección de menor a mayor.Por cada
permutation[i] que su valor sea false se va sumar a la respuesta |i-coleccion[j++]|
donde j es inicialmente igual a cero.

1210 - Hooligan A pesar de que este ejercicio esta clasificado en el COJ


como teorı́a de grafos 4, mi solución fue un tanto greedy. Para empezar el
problema nos plantea que de un torneo de futbol del cual se conoce la can-
tidad de equipos, la cantidad de juegos que juegan dos equipos entre sı́ ası́
como el resultado de algunos de los juegos efectuados hasta ese momento. Se
conoce que por cada victoria el equipo ganador recibe dos puntos mientras en
el empate ambos equipos reciben un punto cada uno. Se le pide determinar
conociendo todos estos datos determinar si el equipo cuyo identificador es 0
puede ser o no campeón del torneo. Para ser campeón tiene que tener mas
puntos que cualquier otro equipo. Bien para solucionar el problema primero
vamos a contar con estructura para almacenar los datos del Equipo dichos
datos son identificador, puntos acumulados, juegos ganados,empatados, per-
didos y pendientes a jugar (inicialmente los juegos ganados,empatados y per-
didos su valor es cero y mientras se vaya leyendo los juegos jugados se deben
ir incrementando mientras los juegos pendientes por jugar su valor inicial es
la cantidad de equipos menos uno por la cantidad de juegos que juegan los
equipos entre sı́), una matriz bidimensional que en la celda [i,j] va indicar
cuantos juegos les quedan por jugar los equipos cuyos identificadores son j y
i. Inicialmente en cada celda el valor colocado será la cantidad de juegos que
juegan entre sı́ los equipos excepto en las celdas donde j y i son iguales. Los
valores de las celdas deben decrementarse cada vez que los equipos que la re-
presnten jueguen entre sı́. Una vez aclarado esto la estrategia a implementar
es la siguiente:

1. Leo los resultados e los juegos efectuados y voy actualizando el estado


de los equipos y los juegos pendientes entre ellos.

411
GREEDY

2. Maximizar los puntos del equipo cuyo identificador es cero. Esto signi-
fica que voy a declarar como victorias aquellos juegos que tienen pen-
dientes por jugar el equipo 0 para sı́ y derrotas para aquellos equipos
con los cuales debı́a jugar.
3. Ordeno los equipos de acuerdo a la cantidad de puntos acumulados has-
ta el momento de forma descendente. Extraigo de la colección ordenada
de equipos el primer elemento si su identificador no es cero entonces
no es posible que el equipo 0 gane el torneo y la respuesta es N. Si el
equipo es el 0 voy a realizar los siguientes pasos mientras en la colección
exista equipo y el equipo 0 tenga posibilidad de ser campeón.
Extraigo el primer equipo de la colección si sus puntos son menor
que el del equipo 0 y no tiene juego pendientes continuo con el
procedimeinto.
Extraigo el primer equipo de la colección si sus puntos son iguales
o mayor que el del equipo 0 detengo el procedimiento y la respuesta
es N.
Extraigo el primer equipo de la colección si sus puntos son menor
que el del equipo 0 y tiene juego pendientes continuo con el proce-
dimeinto. Primero le vamos a declarar tantas victorias como pueda
se siempre y cuando la cantidad de puntos obtenidos sea menor
que la del equipo 0. Luego si continúa con juegos pendientes le
vamos a declarar tantos empates como se pueda siempre y cuando
la cantidad de puntos obtenidos por el y por el equipo que juegan
con él los empates no supere ni iguale los puntos del equipo 0.
Luego si continúa con juegos pendientes le vamos declarar tantas
derrotas como sea posible siempre y cuando la cantidad de puntos
que obtenga los equipos que le ganen no superen a los puntos del
equipo 0. Luego si equipo analizado tiene todavı́a juegos pendien-
tes entonces el equipo 0 no puede ser campeón sino reordeno los
equipos de nuevo sin tener en cuanta el equipo analizado y repito
el este procedimiento. La hora de efectuar los juegos pendientes
del equipo utilice el orden en que estan los equipos en ese momen-
to para si puede ganar que le gane a los equipos más próximo a
él, si puede empatar que sea con esos equipos luego si tiene que
perder que pierda lo menos posible con esos equipos y si con los
que están con la menor cantidad de puntos.
Si el anterior procedimientos concluyó y todos los equipos pudie-
ron efectuar sus partidos entonces el equipo 0 puede ser campeón
del torneo

412
GREEDY

2151 - Stack of Stones Dados n bultos de piedra, queremos combinarlos


todos en un solo bulto. Para realizar esta tarea, podemos seleccionar arbitra-
riamente dos bultos y fusionarlos en uno, y volver a hacer lo mismo hasta que
quede un solo bulto. Combinar dos bultos trae consigo gastar cierta energı́a,
y el costo de esta es la cantidad de piedras del bulto más pequeño, por ejem-
plo, si combinamos dos bultos con 3 y 5 piedras respectivamente, el costo de
la operación es 3 y el bulto obtenido es uno de 8 piedras. Dados la cantidad
de bultos, calcule la mı́nima cantidad de energı́a que se necesita para hacer
el trabajo.
Aplicando un algoritmo greedy, nuestra mejor opción es siempre combinar
el bulto más pequeño con otro, para gastar la menor cantidad de energı́a
en cada paso, pero si nos fijamos, la mı́nima cantidad de operaciones para
convertir n bultos de piedra en uno sólo son n-1, entonces, ¿con cuál lo
fusionamos?, aquı́ entra nuestro segundo criterio del algoritmo greedy, unir
nuestro menor bulto con el mayor, ası́ en cada paso siempre seleccionamos
la mı́nima energı́a posible y el único bulto que incrementa su tamaño es el
mayor.

3108 - Number Generator Se tiene un grupo de botones los cuales son


capaces de generar cada uno un número que esta dentro de un intervalo.
Cada botón tiene su propio intervalo. Se presiona cada botón y se suma cada
numero generado por cada botón a esta suma se llama número aleatorio.
Dado los intervalos de generación de cada botón hallar cuantos números
aleatorios se pueden generar.
La solución es :
X n
Smaxi − Smini + 1
i=1

Donde Smin es la suma de todos los extremos inferior o izquierdo de los


intervalos y Smax es la suma de todos los extremos superior o derecha de
los intervalos. A la diferencia de estas sumatorias se le debe sumar uno. La
explicación de esto viene que el menor número aleatorio posible a generar
A va ser igual a la suma del menor número que puede generar cada botón
(En otras palabras que cada botón genere como número el extremo izquierdo
del intervalo). De igual forma el máximo número aleatorio posible a generar
B va ser igual a la suma del mayor número que puede generar cada botón
(Hablando en plata que cada botón genere como número el extremo derecho
del intervalo). Una vez visto cualquier número aleatorio que se genere siempre
estará en el rango de [A-B], por lo que solo debemos saber cuantos numeros
naturales en el rango [A,B] existen incluyendo a los extremos.

413
GREEDY

DMOJ - Olivander El problema nos plantea dada una colección de varitas


y una colección de cajas para guardar dichas varas ver si existe alguna forma
tal que cada vara sea guardada en una caja y en cada caja exista una sola
vara. Además debemos sabe rque para poder almacenar una vara en una
cara su longitud debe ser menor a la longitud donde sea guardada. Dada las
longitudes de las varas y cajas ver si es posible esto. Para resolver vamos
aplicar una estrategia bien greedy. Lo primeros que haremos es ordenar las
colecciones de varas y cajas de acuerdo a su logintud de forma ascendente.
Ahora bien para que cada vara sea guardada en una caja y que en cada caja
se guarde una vara debe cumplirse que para cada varai su longitud debe
ser menor que la cajai de ser asi para cada vara de la colección ordenada
entonces la respuesta es DA sino la respuesta es NE. Cuando ordenamos las
colecciones intentamos que para la menor vara se utilices la caja de menor
tamaño y de igual forma para la segunda y tercera varas de menor longitud se
utilicen la segunda y tercera cajas de menor longitud. Si en algún momento
esto no es posible implica que para al menos una vara puede que exista una
caja para guardarla.

414
Capı́tulo 15

Teorı́a de Grafos

La teorı́a de grafo es un campo de estudio de las matemáticas y las ciencias


de la computación, que estudia las propiedades de los grafos estructuras que
constan de dos partes, el conjunto de vértices, nodos o puntos; y el conjunto
de aristas, lı́neas o lados (edges en inglés) que pueden ser orientados o no.
Por lo tanto también está conocido como análisis de redes.
La teorı́a de grafos es una rama de las matemáticas discretas y de las
matemáticas aplicadas, y es un tratado que usa diferentes conceptos de diver-
sas áreas como combinatoria, álgebra, probabilidad, geometrı́a de polı́gonos,
aritmética y topologı́a.
Existen diferentes formas de representar un grafo (simple), además de la
geométrica y muchos métodos para almacenarlos en una computadora. La
estructura de datos usada depende de las caracterı́sticas del grafo y el algo-
ritmo usado para manipularlo. Entre las estructuras más sencillas y usadas
se encuentran las listas y las matrices, aunque frecuentemente se usa una
combinación de ambas. Las listas son preferidas en grafos dispersos porque
tienen un eficiente uso de la memoria. Por otro lado, las matrices proveen
acceso rápido, pero pueden consumir grandes cantidades de memoria.
Según Robert Sedgewick en su libro Algorithms in C++, Third Edition,
Part 5: Graph Algorithms. los problemas de grafos se pueden clasificar como:

Fáciles: Un problema fácil de procesamiento de grafos es aquel que se


puede resolver utilizando un programa eficiente y elegante. Frecuente-
mente su tiempo de ejecución es lineal en el peor caso, o limitado por
un polinomio de bajo grado en el número de nodos o el número de
aristas. Generalmente, también podemos decir que el problema es fácil
si podemos desarrollar un algoritmos de fuerza bruta que aunque sea
lento para grandes grafos, es útil para grafos pequeños e inclusive de
tamaño medio. Entonces, una vez que sabemos que el problema es fácil,

415
TEORÍA DE GRAFOS

buscamos soluciones eficientes y escogemos la mejor de ellas.

Tratable: Un problema tratable de procesamiento de grafos es aquel


para el que se conoce un algoritmo que garantiza que sus requerimientos
en tiempo y espacio están limitados por una función polinomial en
el tamaño del grafo (número de nodos + número de aristas). Todo
problema fácil es tratable, pero se hace la distinción debido a que el
desarrollo de una solución eficiente para resolverlo es extremadamente
difı́cil o imposible. Las soluciones a algunos problemas intratables nunca
han sido escritas en programas, o tiempo tiempos de ejecución tan altos
que no puede contemplarse su utilización en la práctica.

Intratable: Un problema intratable de procesamiento de grafos es aquel


para el que no se conoce algoritmo que garantice obtener un solución
del problema en una cantidad razonable de tiempo. Muchos de estos
problemas tienen la caracterı́stica de que podemos utilizar un méto-
do de fuerza bruta para probar todas las posibilidades de calcular la
solución, y se consideran intratables porque existen demasiadas posi-
bilidades a considerar. Esta clase de problemas es extensa y muchos
expertos piensan que no existen algoritmos eficientes para solucionar
estos problemas. El término NP-hard describe los problemas de esta
clase, el cual representa un altı́simo nivel de dificultad.

Desconocida: Existen problemas de procesamiento de grafos cuya di-


ficultad es desconocida. No hay un algoritmo eficiente conocido para
resolverlos, ni son conocidos como NP-hard . El problema de isomor-
fismo de grafos pertenece a esta clase.

Algunos de los problemas más conocidos de grafos son:

Conectividad Simple: Consiste en estudiar si el grafo es conexo, es decir,


si existe al menos un camino entre cada par de vértices.

Detección de Ciclos: Consiste en estudiar la existencia de al menos un


ciclo en el grafo.

Camino Simple: Consiste en estudiar la existencia de un camino entre


dos vértices cualquiera.

Camino de Euler: Consiste en estudiar la existencia de un camino que


conecte dos vértices dados usando cada arista del grafo exactamente
una sola vez. Si el camino tiene como inicio y final el mismo vértice,
entonces se desea encontrar un tour de Euler.

416
TEORÍA DE GRAFOS

Camino de Hamilton: Consiste en estudiar la existencia de un camino


que conecte dos vértices dados que visite cada nodo del grafo exacta-
mente una vez. Si el camino tiene como inicio y final el mismo vértice,
entonces se desea encontrar un tour de Hamilton.
Conectividad Fuerte en Dı́grafos: Consiste en estudiar si hay un camino
dirigido conectando cada par de vértices del dı́grafo. Inclusive se puede
estudiar si existe un camino dirigido entre cada par de vértices, en
ambas direcciones.
Clausura Transitiva: Consiste en tratar de encontrar un conjunto de
vértices que pueda ser alcanzado siguiendo aristas dirigidas desde cada
vértice del dı́grafo.
Árbol de Expansión Mı́nima: Consiste en encontrar, en un grafo pesado,
el conjunto de aristas de peso mı́nimo que conecta a todos los vértices.
Caminos cortos a partir de un mismo origen: Consiste en encontrar
cuales son los caminos más cortos conectando a un vértice v cualquier
con cada uno de los otros vértices de un dı́grafo pesado. Este es un
problema que por lo general se presenta en redes de computadores,
representadas como grafos.
Planaridad: Consiste en estudiar si un grafo puede ser dibujado sin que
ninguna de las lı́neas que representan las aristas se intercepten.
Pareamiento (Matching): Dado un grafo, consiste en encontrar cual
es el subconjunto más largo de sus aristas con las propiedad de que
no haya dos conectados al mismo vértice. Se sabe que este problema
clásico es resoluble en tiempo proporcional a una función polinomial
en el número de vértices y de aristas, pero aun no existe un algoritmo
rápido que se ajuste a grandes grafos.
Ciclos Pares en Dı́grafos: Consiste en encontrar en un dı́grafo un camino
de longitud par. Este problema puede lucir simple ya que la solución
para grafos no dirigidos es sencilla. Sin embargo, aun no se conoce si
existe un algoritmo eficiente para resolverlo.
Asignación: Este problema se conoce también como pareamiento bi-
partito pesado (bipartite weigthed matching). Consiste en encontrar
un pareamiento perfecto de peso mı́nimo en un grafo bipartito. Un gra-
fo bipartito es aquel cuyos vértices se pueden separar en dos conjuntos,
de tal manera que todas las aristas conecten a un vértice en un conjunto
con otro vértice en el otro conjunto.

417
TEORÍA DE GRAFOS

Conectividad General: Consiste en encontrar el número mı́nimo de aris-


tas que al ser removidas separarán el grafo en dos partes disjuntas (co-
nectividad de aristas). También se puede encontrar el número mı́nimo
de nodos que al ser removidos separarán el grafo en dos partes disjuntas
(conectividad de nodos).

El camino más largo: Consiste en encontrar cual es el camino más largo


que conecte a dos nodos dados en el grafo. Aunque parece sencillo, este
problema es una versión del problema del tour de Hamilton y es NP-
hard.

Colorabilidad: Consiste en estudiar si existe alguna manera de asignar


k colores a cada uno de los vértices de un grafo, de tal forma de que
ninguna arista conecte dos vértices del mismo color. Este problema
clásico es fácil para k=2 pero es NP-hard para k=3.

Conjunto Independiente: Consiste en encontrar el tamaño del mayor


subconjunto de nodos de un grafo con la propiedad de que no haya
ningún par conectado por una arista. Este problema es NP-hard.

Clique: Consiste en encontrar el tamaño del clique (subgrafo completo)


más grande en un grafo dado.

Isomorfismo de grafos: Consiste en estudiar la posibilidad de hacer dos


grafos idénticos con solo renombrar sus nodos. Se conocen algoritmos
eficientes para solucionar este problema, para varios clases particulares
de grafos, pero no se tiene solución para el problema general. Este
problema es NP-hard .

15.1. Representación computacional


Existen diferentes formas de representar un grafo (simple), además de la
geométrica y muchos métodos para almacenarlos en una computadora. La
estructura de datos usada depende de las caracterı́sticas del grafo y el algo-
ritmo usado para manipularlo. Entre las estructuras más sencillas y usadas
se encuentran las listas y las matrices, aunque frecuentemente se usa una
combinación de ambas. Las listas son preferidas en grafos dispersos porque
tienen un eficiente uso de la memoria. Por otro lado, las matrices proveen
acceso rápido, pero pueden consumir grandes cantidades de memoria.
Estructura de lista

418
TEORÍA DE GRAFOS

Figura 15.1: Ejemplo de grafo

Lista de incidencia: Las aristas son representadas con un vector de


pares (ordenados, si el grafo es dirigido), donde cada par representa
una de las aristas.
Lista de adyacencia: Cada vértice tiene una lista de vértices los
cuales son adyacentes a él. Esto causa redundancia en un grafo no
dirigido (ya que A existe en la lista de adyacencia de B y viceversa),
pero las búsquedas son más rápidas, al costo de almacenamiento extra.
ListAdy={ {1,2,5}, {3,5}, {4}, {5,6} }
Lista de grados: También llamada secuencia de grados o sucesión
gráfica de un grafo no-dirigido es una secuencia de números, que co-
rresponde a los grados de los vértices del grafo.LisGra=(4,3,3,3,2,1).
Estructuras matriciales
Matriz de adyacencia: El grafo está representado por una matriz
cuadrada M de tamaño n2 , donde n es el número de vértices. Si hay
una arista entre un vértice x y un vértice y, entonces el elemento m x,
y es 1, de lo contrario, es 0.

Figura 15.2: Matriz de adyacencia del grafo

419
TEORÍA DE GRAFOS

Matriz de incidencia: El grafo está representado por una matriz de


A. (aristas) por V (vértices), donde [vértice, arista] contiene la infor-
mación de la arista (1 - conectado, 0 - no conectado).

Figura 15.3: Matriz de de incidencia del grafo

Existen varias formas representar un grafo, su representación depende en


gran medida de las caracterı́sticas propias del grafo ası́ como el algoritmo que
se quiera utilizar sobre el. A continuación veremos algunas de esas variantes.

struct Node
{
v e c t o r <int> a d j ;
};
Node g r a f [MAX N ] ;

Esta variante es quizás la mas sencilla. Un grafo va ser un arreglo de


una estructura llamada Node la cual tiene como atributo un vector con los
ı́ndices de los nodos adyacentes al nodo almacenado en la posición inesima
del arreglo. La definición de MAX N debe ser un valor igual o mayor que la
cantidad de nodos del grafo que vas a representar con esta variante. Es muy
utilizada cuando realizamos BFS y DFS.

struct Node
{
int d i s t ;
v e c t o r <int> a d j ;
v e c t o r <int> w e i g h t ;
};
Node g r a f [MAX N ] ;

Es una extensión de la primera en esta incorporamos un vector de los pesos


de las aristas de los nodos adyacentes. Otra adecuación es la variable dist
que almacenará la distancia mı́nima existen entre un nodo X y el que ocupa
la posición inesima. Esta variante es muy utilizada para un Dijkstra.

420
TEORÍA DE GRAFOS

En determinados algoritmos de grafos se hace necesario trabajar con las


aristas del grafo y para este elemento también podemos representarlo me-
diante la siguiente estructura

struct Edge
{
int a , b ;
int w e i g h t ;
};
Edge E [MAX N ] ;

En este caso las variables a y b de la estructura son los nodos que componen
la aristas mientras la variable weight contiene el valor de la aristas. E es el
arreglo de las aristas del grafo mientras MAX N tiene que ser definida con
valor igual o mayor a la cantidad de aristas del grafos. A continuación les
dejo otra variante para representar los grafos.

typedef int Weight ;


struct Edge
{
int s r c , d s t ;
Weight w e i g h t ;
Edge ( int s r c , int dst , Weight w e i g h t ) :
s r c ( s r c ) , d s t ( d s t ) , w e i g h t ( w e i g h t ) {}
};
bool operator <(const Edge & e , const Edge & f )
{
return e . w e i g h t != f . w e i g h t ? e . w e i g h t > f . w e i g h t : // ! !
INVERSE ! !
e . s r c != f . s r c ? e . s r c < f . s r c : e . d s t < f . d s t ;
}
typedef v e c t o r <Edge> Edges ;
typedef v e c t o r <Edges> Graph ;
typedef v e c t o r <Weight> Array ;
typedef v e c t o r <Array> Matrix ;

15.2. Búsqueda en profundidad (DFS)


Una búsqueda en profundidad (en inglés DFS o Depth First Search) es un
algoritmo que permite recorrer todos los nodos de un grafo o árbol (teorı́a de
grafos) de manera ordenada, pero no uniforme. Su funcionamiento consiste en
ir expandiendo todos y cada uno de los nodos que va localizando, de forma
recurrente, en un camino concreto. Cuando ya no quedan más nodos que
visitar en dicho camino, regresa (Backtracking), de modo que repite el mismo

421
TEORÍA DE GRAFOS

proceso con cada uno de los hermanos del nodo ya procesado. Análogamente
existe el algoritmo de búsqueda en anchura (BFS o Breadth First Search).
El siguiente ejemplo ilustra el funcionamiento del algoritmo DFS sobre
un grafo de ejemplo. El algoritmo comienza por el nodo 0.

Figura 15.4: Salida del DFS sobre el grafo.

El pseudocódigo seria el siguiente:

DFS( g r a f o G)
PARA CADA v e r t i c e u que p e r t e n e c e V[G] HACER
e s t a d o [ u ] = NO VISITADO
padre [ u ] = NULO
tiempo = 0
PARA CADA v e r t i c e u que p e r t e n e c e V[G] HACER
SI e s t a d o [ u ] = NO VISITADO ENTONCES
D F S V i s i t a r ( u , tiempo )

D F S V i s i t a r ( nodo u , int tiempo )


e s t a d o [ u ] = VISITADO
tiempo = tiempo + 1
d [ u ] = tiempo
PARA CADA v que p e r t e n e c e V e c i n o s [ u ] HACER
SI e s t a d o [ v ] = NO VISITADO ENTONCES
padre [ v ] = u
D F S V i s i t a r ( v , tiempo )
e s t a d o [ u ] = TERMINADO
tiempo = tiempo + 1
f [ u ] = tiempo

Como se puede observar esta variante es recursiva y es un ejemplo clásico


de un Backtracking lo cual no es muy recomendable usar para grafos con
una cantidad de nodos mayor de 9 nodos por la cantidad de operaciones
que representa (9!), es por eso que se decide sustituir el elemento recursivo

422
TEORÍA DE GRAFOS

por una estructura de datos que puede simular la “recursividad” inicial del
algoritmo y disminuye la complejidad del algoritmo. Dicha estructura es una
pila, aplicando los cambios pertinentes el algoritmo quedarı́a como se muestra
en el código.

#include <s t d i o . h>


#include <i o s t r e a m >
#include <v e c t o r >
#include <a l g o r i t h m >
#include <queue>
#include <s t a c k >
#define MAX N 5001
using namespace s t d ;

struct Node
{
v e c t o r <int> a d j ;
};
Node g r a f [MAX N ] ;
bool mark [MAX N ] ;

i n l i n e void DFS( int s t a r t )


{
s t a c k <int> d f s s t e k ;
d f s s t e k . push ( s t a r t ) ;
while ( ! d f s s t e k . empty ( ) )
{
int xt = d f s s t e k . top ( ) ;
d f s s t e k . pop ( ) ;
mark [ xt ] = true ;
f o r ( int i =0; i <g r a f [ xt ] . a d j . s i z e ( ) ; i ++)
{
i f ( ! mark [ g r a f [ xt ] . a d j [ i ] ] )
{
d f s s t e k . push ( g r a f [ xt ] . a d j [ i ] ) ;
mark [ g r a f [ xt ] . a d j [ i ] ] = true ;
}
}
}
}

Este algoritmo tiene una complejidad de O( V+E ) donde V es la cantidad


de vértices del grafo y E las aristas. El algoritmo recibe como parámetro el
nodo inicial por el cual se inicia el DFS. Una bondad de este algoritmo es
que los nodos solo se vistan una vez. Esto implica que si se salvan en alguna
estructura las aristas que se van recorriendo se obtiene un conjunto de aristas
de cubrimiento mı́nimo del grafo, lo cual se utiliza frecuentemente se utiliza

423
TEORÍA DE GRAFOS

para reducir la complejidad del grafo cuando la perdida de información de


algunas aristas no es importante. Este resultado se conoce como árbol DFS
(DFS Tree).
El DFS puede modificarse fácilmente y utilizarse para resolver proble-
mas sencillos como los de conectividad simple, detección de ciclos y camino
simple. Por ejemplo, el número de veces que se invoca a la acción DFS R
desde la acción DFS en el algoritmo anterior es exactamente el número de
componentes conexas del grafo, lo cual representa la solución al problema de
conectividad simple.
Para un grafo de pocos nodos se puede implementar un dfs recursivo
lo cual podrı́a generar todos los posibles caminos desde un nodos hasta los
otros.

15.3. Búsqueda en anchura (BFS)


En este algoritmo también se utiliza la estrategia de marcas los nodos
como “visitados”para detectar la culminación del recorrido, pero los nodos
se recorren de una manera ligeramente distinta al DFS.
De nuevo, se selecciona cualquier nodo como punto de partida (por lo
general el primer nodo del grafo) y se marcan todos los nodos del grafo como
“no visitados”. El nodo inicial se marca como “visitados”y luego se visitan
TODOS los nodos adyacentes a este, al finalizar este proceso se busca visitar
nodos más lejanos visitando los nodos adyacentes a los nodos adyacentes del
nodo inicial.
Este algoritmo puede crear menos ambientes recursivos que el anterior
porque visita mas nodos en un mismo ambiente, pero esto depende de cómo
este construido el grafo. algoritmo se conoce como el algoritmo de BFS
(Breadth-First Search).
El siguiente ejemplo ilustra el funcionamiento del algoritmo BFS sobre
un grafo de ejemplo. La secuencia de ilustraciones va de izquierda a derecha
y de arriba hacia abajo. El algoritmo comienza por el nodo 0.
Este algoritmo tiene exactamente el mismo orden en tiempo de ejecución
del algoritmo de recorrido en profundidad y también se puede obtener el con-
junto de aristas de cubrimiento mı́nimo del grafo. A continuación se muestra
el código:

#include <s t d i o . h>


#include <i o s t r e a m >
#include <v e c t o r >
#include <a l g o r i t h m >
#include <queue>

424
TEORÍA DE GRAFOS

Figura 15.5: Ejecucción del algoritmo BFS.

#include <s t a c k >


#define MAX N 5001
using namespace s t d ;

struct Node
{
v e c t o r <int> a d j ;
};
Node g r a f [MAX N ] ;
bool mark [MAX N ] ;

i n l i n e void BFS( int s t a r t )


{
queue<int> b f s q u e u e ;
b f s q u e u e . push ( s t a r t ) ;
while ( ! b f s q u e u e . empty ( ) )
{
int xt = b f s q u e u e . f r o n t ( ) ;
b f s q u e u e . pop ( ) ;
mark [ xt ] = true ;

425
TEORÍA DE GRAFOS

f o r ( int i =0; i <g r a f [ xt ] . a d j . s i z e ( ) ; i ++)


{
i f ( ! mark [ g r a f [ xt ] . a d j [ i ] ] )
{
b f s q u e u e . push ( g r a f [ xt ] . a d j [ i ] ) ;
mark [ g r a f [ xt ] . a d j [ i ] ] = true ;
}
}
}
}

Este algoritmo tiene una complejidad de O( V+E ) donde V es la cantidad


de vértices del grafo y E las aristas. El algoritmo recibe como parámetro el
nodo inicial por el cual se inicia el BFS.

15.4. Algoritmo Kruskal


El algoritmo de Kruskal es un algoritmo de la teorı́a de grafos para encon-
trar un árbol recubridor mı́nimo en un grafo conexo y ponderado. Es decir,
busca un subconjunto de aristas que, formando un árbol, incluyen todos los
vértices y donde el valor total de todas las aristas del árbol es el mı́nimo. Si
el grafo no es conexo, entonces busca un bosque expandido mı́nimo (un árbol
expandido mı́nimo para cada componente conexa). El algoritmo de Kruskal
es un ejemplo de algoritmo voraz. Funciona de la siguiente manera

- Se crea un bosque B (un conjunto de árboles), donde cada vértice del


grafo es un árbol separado.

- Se crea un conjunto C que contenga a todas las aristas del grafo.

- Mientras C es no vacı́o.

* Eliminar una arista de peso mı́nimo de C.


* Si esa arista conecta dos árboles diferentes se añade al bosque,
combinando los dos árboles en un solo árbol.
* En caso contrario, se desecha la arista

Al acabar el algoritmo, el bosque tiene un solo componente, el cual forma un


árbol de expansión mı́nimo del grafo.
El algoritmo de Prim construye un MST una arista a la vez, encontrando
una nueva arista que agregar a un MST que va creciendo en cada paso. El
algoritmo de Kruskal también construye el MST una arista a la vez, con
la diferencia que este encuentra una arista que conecte dos MST que van

426
TEORÍA DE GRAFOS

creciendo dentro de un bosque de MST crecientes, formado de los nodos del


grafo original.
El algoritmo comienza a partir de un conjunto de árboles degenerados
formados por un solo nodo, que son los nodos del grafo, y se comienzan a
combinar los árboles de dos en dos usando la arista menos costosa posible,
hasta que solo quede un solo árbol: El MST.
Dada una lista de las aristas del grafo, el primer paso del algoritmo de
Kruskal es ordenarlas por peso (usando un quicksort por ejemplo). Luego se
van procesando las aristas en el orden de su peso, agregando aristas que no
produzcan ciclos en el MST. El algoritmo de Kruskal es de O(A*log2 A),
donde A es el número de aristas del grafo.
El siguiente ejemplo ilustra el funcionamiento del algoritmo. La secuencia
de ilustraciones va de izquierda a derecha y de arriba hacia abajo.

Figura 15.6: Ejecucción del algoritmo Kruskal.

Este algoritmo fue publicado por primera vez en Proceedings of the Ame-
rican Mathematical Society, pp. 48–50 en 1956, y fue escrito por Joseph
Kruskal. A continuación se muestra un seudocódigo del algoritmo.

f u n c t i o n Kruskal (G)
Para cada v en V[G] h a c e r
Nuevo c o n j u n t o C( v ) \ t e x t l e f t a r r o w {v } .
Nuevo heap Q que c o n t i e n e t o d a s l a s a r i s t a s de G, ordenando
por su p e s o .
D e f i n o un a r b o l T ={ v a c i o }
// n e s e l numero t o t a l de v e r t i c e s
M i e n t r a s T t e n g a menos de n−1 v e r t i c e s h a c e r
( u , v ) i g u a l Q. sacarMin ( )
// p r e v i e n e c i c l o s en T . a g r e g a ( u , v ) s i u y v e s t a n
d i f e r e n t e s componentes en e l c o n j u n t o .
// Notese que C( u ) d e v u e l v e l a componente a l a que
pertenece u .
i f C( v ) d i s t i n t o C( u ) then
Agregar a r i s t a ( v , u ) a T .
Merge C( v ) y C( u ) en e l c o n j u n t o
Return a r b o l T

A continuación se muestra la implementación en C++

427
TEORÍA DE GRAFOS

#include <s t d i o . h>


#include <math . h>
#include <s t r i n g . h>
#include <i o s t r e a m >
#include <v e c t o r >
#include < l i s t >
#include <s t r i n g >
#include <a l g o r i t h m >
#include <queue>
#include <s t a c k >
#include <s e t >
#include <map>
#include <complex>
#define MAX N 100001
using namespace s t d ;
typedef long long l l d ;

int n , m;
int numComponents , r e t ;

struct Edge
{
int a , b ;
int w e i g h t ;
bool operator <(const Edge &e ) const
{
return ( w e i g h t < e . w e i g h t ) ;
}
};
Edge E [MAX N ] ;

struct Node
{
int p a r e n t ;
int rank ;
};
Node DSU[MAX N ] ;

i n l i n e void MakeSet ( int x )


{
DSU[ x ] . p a r e n t = x ;
DSU[ x ] . rank = 0 ;
}

i n l i n e int Find ( int x )


{
i f (DSU[ x ] . p a r e n t == x ) return x ;
DSU[ x ] . p a r e n t = Find (DSU[ x ] . p a r e n t ) ;

428
TEORÍA DE GRAFOS

return DSU[ x ] . p a r e n t ;
}

i n l i n e void Union ( int x , int y )


{
int xRoot = Find ( x ) ;
int yRoot = Find ( y ) ;
i f ( xRoot == yRoot ) return ;
i f (DSU[ xRoot ] . rank < DSU[ yRoot ] . rank )
{
DSU[ xRoot ] . p a r e n t = yRoot ;
}
e l s e i f (DSU[ xRoot ] . rank > DSU[ yRoot ] . rank )
{
DSU[ yRoot ] . p a r e n t = xRoot ;
}
else
{
DSU[ yRoot ] . p a r e n t = xRoot ;
DSU[ xRoot ] . rank++;
}
}

i n l i n e int Kruskal ( )
{
int r e t = 0 , numComponents = n ;
f o r ( int i =0; i <n ; i ++) MakeSet ( i ) ;
s o r t (E , E+m) ;
f o r ( int i =0; i < m && numComponents > 1 ; i ++)
{
i f ( Find (E [ i ] . a ) != Find (E [ i ] . b ) )
{
Union (E [ i ] . a , E [ i ] . b ) ;
r e t += E [ i ] . w e i g h t ;
numComponents−−;
}
}
i f ( numComponents > 1 ) return −1;
return r e t ;
}

int main ( )
{
n = 7 , m = 11;

E[0]. a = 0, E[0]. b = 1, E[0]. weight = 7;


E[1]. a = 0, E[1]. b = 3, E[1]. weight = 5;
E[2]. a = 1, E[2]. b = 2, E[2]. weight = 8;
E[3]. a = 1, E[3]. b = 3, E[3]. weight = 9;

429
TEORÍA DE GRAFOS

E[4]. a = 1, E[4]. b = 4 , E [ 4 ] . weight = 7 ;


E[5]. a = 2, E[5]. b = 4 , E [ 5 ] . weight = 5 ;
E[6]. a = 3, E[6]. b = 4 , E [ 6 ] . weight = 15;
E[7]. a = 3, E[7]. b = 5 , E [ 7 ] . weight = 6 ;
E[8]. a = 4, E[8]. b = 5 , E [ 8 ] . weight = 8 ;
E[9]. a = 4, E[9]. b = 6 , E [ 9 ] . weight = 9 ;
E[10]. a = 5 , E[10]. b = 6 , E [ 1 0 ] . weight = 11;

p r i n t f ( " %d\n" , Kruskal ( ) ) ;

return 0 ;
}

Esta implementación devuelve la longitud del árbol de expansión mı́ni-


mo que genera el algoritmo. Mientras en la segunda variante es el mismo
algoritmo pero devuelve las aristas que lo integran y su longitud.

p a i r <Weight , Edges> minimumSpanningForest ( const Graph & g )


{
int n = g . s i z e ( ) ;
UnionFind u f ( n ) ;
p r i o r i t y q u e u e <Edge> Q;
REP ( u , n ) FOR ( e , g [ u ] ) i f ( u <e−> d s t ) Q. push ( ∗ e ) ;

Weight t o t a l = 0 ;
Edges F ;
while (F . s i z e ( ) <n−1 & &! Q. empty ( ) ) {
Edge e = Q. top ( ) ; Q. pop ( ) ;
i f ( uf . unionSet ( e . src , e . dst ) ) {
F . push back ( e ) ;
t o t a l + = e . weight ;
}
}
return p a i r <Weight , Edges> ( t o t a l , F) ;
}

Para determinar la complejidad del algoritmo vamos a definir m como el


número de aristas mientras el número de nodos o vértices será n. El algoritmo
de Kruskal muestra una complejidad O(m log m) o, equivalentemente, O(m
log n), cuando se ejecuta sobre estructuras de datos simples. Los tiempos de
ejecución son equivalentes porque:

m es a lo sumo n2 y log n2 = 2 log n es O(log n)

ignorando los vértices aislados, los cuales forman su propia componente


del árbol de expansión mı́nimo, n ≤ 2m, ası́ que log n es O(log m).

430
TEORÍA DE GRAFOS

Se puede conseguir esta complejidad de la siguiente manera: primero se


ordenan las aristas por su peso usando una ordenación por comparación
(comparison sort) con una complejidad del orden de O(m log m); esto permi-
te que el paso .eliminar una arista de peso mı́nimo de C”se ejecute en tiempo
constante. Lo siguiente es usar una estructura de datos sobre conjuntos dis-
juntos (disjoint-set data structure) para controlar qué vértices están en qué
componentes. Es necesario hacer orden de O(m) operaciones ya que por cada
arista hay dos operaciones de búsqueda y posiblemente una unión de con-
juntos. Incluso una estructura de datos sobre conjuntos disjuntos simple con
uniones por rangos puede ejecutar las operaciones mencionadas en O(m log
n). Por tanto, la complejidad total es del orden de O(m log m) = O(m log
n).

15.5. Algoritmo Prim


El algoritmo de Prim es un algoritmo perteneciente a la teorı́a de los
grafos para encontrar un árbol recubridor mı́nimo en un grafo conexo, no
dirigido y cuyas aristas están etiquetadas.
El algoritmo de Prim es tal vez el algoritmo de MST1 más sencillo de
implementar y el mejor método para grafos densos. Este algoritmo puede
encontrar el MST de cualquier grafo conexo pesado.
Sea V el conjunto de nodos de un grafo pesado no dirigido. El algoritmo
de Prim comienza cuando se asigna a un conjunto U de nodos un nodo inicial
perteneciente a V, en el cual “crece” un árbol de expansión, arista por arista.
En cada paso se localiza la arista más corta (u,v) que conecta a U con V-U,
y después se agrega v, el vértice en V-U, a U. Este paso se repite hasta que
V=U. El algoritmo de Prim es de O(N2 ), donde |V | = N.
En otras palabras, el algoritmo encuentra un subconjunto de aristas que
forman un árbol con todos los vértices, donde el peso total de todas las aristas
en el árbol es el mı́nimo posible. Si el grafo no es conexo, entonces el algoritmo
encontrará el árbol recubridor mı́nimo para uno de los componentes conexos
que forman dicho grafo no conexo.
El algoritmo fue diseñado en 1930 por el matemático Vojtech Jarnik y lue-
go de manera independiente por el cientı́fico computacional Robert C. Prim
en 1957 y redescubierto por Dijkstra en 1959. Por esta razón, el algoritmo es
también conocido como algoritmo DJP o algoritmo de Jarnik.
El algoritmo incrementa continuamente el tamaño de un árbol, comen-
zando por un vértice inicial al que se le van agregando sucesivamente vértices
1
Árbol de expansión mı́nima (MST – Minimum Spanning Tree).

431
TEORÍA DE GRAFOS

cuya distancia a los anteriores es mı́nima. Esto significa que en cada paso, las
aristas a considerar son aquellas que inciden en vértices que ya pertenecen
al árbol. El árbol recubridor mı́nimo está completamente construido cuando
no quedan más vértices por agregar.
El siguiente ejemplo ilustra el funcionamiento del algoritmo. La secuencia
de ilustraciones va de izquierda a derecha y de arriba hacia abajo. La primera
imagen muestra el grafo pesado y las siguientes muestran el funcionamien-
to del algoritmo de Prim y como va cambiando el conjunto U durante la
ejecución.

Figura 15.7: Ejecucción del algoritmo Prim.

Agregar un nodo al MST es un cambio incremental, para implementar


el algoritmo de Prim debemos enfocarnos en la naturaleza de ese cambio
incremental. La clave está en notar que nuestro interés esta en la distancia
más corta de cada vértice de U a V-U. Al agregar un nodo v al árbol, el único
cambio posible para cada vértice w fuera del árbol es que agregar v coloca
a w más cerca del árbol. Esto es, no es necesario verificar la distancia de w
a todos los demás nodos del árbol, solo se necesita verificar si la adición de
v al árbol necesita actualizar dicho mı́nimo. Esto se puede lograr agregando
una estructura de datos simple para evitar repetir cálculos excesivos, y hacer
que el algoritmo sea más rápido y más simple.
El siguiente ejemplo ilustra la metodologı́a anterior, utilizando una cola
con prioridad de aristas P, en donde las aristas se van encolando en menor a
mayor según el peso de la misma. La secuencia de ilustraciones va de izquierda
a derecha.
Se comienza en el nodo 0 y se encolan las aristas adyacentes a este nodo,

432
TEORÍA DE GRAFOS

Figura 15.8: Ejecucción del algoritmo Prim paso a paso.

en orden decreciente. La arista mı́nima es la que esta en el frente de la cola,


ası́ que se elimina de la cola y se agrega al MST. Seguidamente se procede
con el nodo 1 y se encolan sus aristas adyacentes, con la diferencia de que
hay que verificar si dichas aristas representan un nuevo camino mı́nimo con
respecto a las aristas que ya están introducidas en la cola. En este caso no se
encola la arista 1-3 porque ya hay una arista que lleve a 3 con el mismo costo
en la pila. Se toma 1-2 porque esta en el frente y se procede con el nodo 2.
Cuando se va a encolar la arista 2-3, se encuentra que ya hay un camino que
lleve a 3 pero de mayor costo, ası́ que se elimina 0-3 y luego se encola 2-3.
Se agrega 2-3 al MST y termina el proceso, porque el conjunto de nodos en
el MST es igual al conjunto de vértices del grafo original.
Lo siguiente es un pseudocódigo del algoritmo que utiliza como estructura
de datos auxiliar una cola com prioridad la cual se puede implementar con
un heap

Prim ( Grafo G)
/∗ I n i c i a l i z a m o s t o d o s l o s nodos d e l g r a f o .
La d i s t a n c i a l a ponemos a i n f i n i t o y e l padre de cada
nodo a NULL
Encolamos , en una c o l a de p r i o r i d a d
donde l a p r i o r i d a d e s l a d i s t a n c i a ,
t o d a s l a s p a r e j a s <nodo , d i s t a n c i a > d e l g r a f o ∗/
por cada u en V[G] h a c e r
d i s t a n c i a [ u ] = INFINITO
padre [ u ] = NULL
A d i c i o n a r ( c o l a ,<u , d i s t a n c i a [ u] >)
d i s t a n c i a [ u]=0
mientras ! e s t a v a c i a ( cola ) hacer
// OJO: Se e n t i e n d e por mayor p r i o r i d a d a q u e l nodo
cuya d i s t a n c i a [ u ] e s menor .
u = e x t r a e r m i n i m o ( c o l a ) // d e v u e l v e e l minimo y l o
e l i m i n a de l a c o l a .
por cada v a d y a c e n t e a ’u’ h a c e r
s i ( ( v p e r t e n e c e c o l a ) && ( d i s t a n c i a [ v ] > p e s o ( u ,

433
TEORÍA DE GRAFOS

v) ) entonces
padre [ v ] = u
d i s t a n c i a [ v ] = peso (u , v )
A c t u a l i z a r ( c o l a ,<v , d i s t a n c i a [ v ] >)

A continuación una primera variante del algoritmo con C++

#include <s t d i o . h>


#include <math . h>
#include <s t r i n g . h>
#include <i o s t r e a m >
#include <v e c t o r >
#include < l i s t >
#include <s t r i n g >
#include <a l g o r i t h m >
#include <queue>
#include <s t a c k >
#include <s e t >
#include <map>
#include <complex>
#define MAX N 101
using namespace s t d ;
typedef long long l l d ;

int n ;
struct Node
{
v e c t o r <int> a d j ;
v e c t o r <int> w e i g h t ;
};
Node g r a f [MAX N ] ;
bool mark [MAX N ] ;

struct edge
{
int u , v ;
double w ;
bool operator <(const edge &a ) const
{
return (w>a . w) ;
}
};

p r i o r i t y q u e u e <edge> pq prim ;

i n l i n e int Prim ( )
{
int xt = 0 ;

434
TEORÍA DE GRAFOS

int r e t = 0 ;
int amt = 0 ;
while ( amt < n−1)
{
mark [ xt ]= true ;
f o r ( int i =0; i <g r a f [ xt ] . a d j . s i z e ( ) ; i ++)
{
i f ( ! mark [ g r a f [ xt ] . a d j [ i ] ] )
{
edge E ;
E . u = xt ;
E . v = g r a f [ xt ] . a d j [ i ] ;
E . w = g r a f [ xt ] . w e i g h t [ i ] ;
pq prim . push (E) ;
}
}
while ( ! pq prim . empty ( ) )
{
edge X = pq prim . top ( ) ;
pq prim . pop ( ) ;
i f ( ! mark [X. v ] )
{
r e t += X. w ;
xt = X. v ;
amt++;
break ;
}
}
}
return r e t ;
}

int main ( )
{
n = 7;

graf [0]. a d j . push back ( 1 ) ;


graf [0]. w e i g h t . push back ( 7 ) ;
graf [1]. a d j . push back ( 0 ) ;
graf [1]. w e i g h t . push back ( 7 ) ;

graf [0]. a d j . push back ( 3 ) ;


graf [0]. w e i g h t . push back ( 5 ) ;
graf [3]. a d j . push back ( 0 ) ;
graf [3]. w e i g h t . push back ( 5 ) ;

g r a f [ 1 ] . a d j . push back ( 2 ) ;
g r a f [ 1 ] . w e i g h t . push back ( 8 ) ;
g r a f [ 2 ] . a d j . push back ( 1 ) ;

435
TEORÍA DE GRAFOS

g r a f [ 2 ] . w e i g h t . push back ( 8 ) ;

graf [1]. a d j . push back ( 3 ) ;


graf [1]. w e i g h t . push back ( 9 ) ;
graf [3]. a d j . push back ( 1 ) ;
graf [3]. w e i g h t . push back ( 9 ) ;

graf [1]. a d j . push back ( 4 ) ;


graf [1]. w e i g h t . push back ( 7 ) ;
graf [4]. a d j . push back ( 1 ) ;
graf [4]. w e i g h t . push back ( 7 ) ;

graf [2]. a d j . push back ( 4 ) ;


graf [2]. w e i g h t . push back ( 5 ) ;
graf [4]. a d j . push back ( 2 ) ;
graf [4]. w e i g h t . push back ( 5 ) ;

graf [3]. a d j . push back ( 4 ) ;


graf [3]. w e i g h t . push back ( 1 5 ) ;
graf [4]. a d j . push back ( 3 ) ;
graf [4]. w e i g h t . push back ( 1 5 ) ;

graf [3]. a d j . push back ( 5 ) ;


graf [3]. w e i g h t . push back ( 6 ) ;
graf [5]. a d j . push back ( 3 ) ;
graf [5]. w e i g h t . push back ( 6 ) ;

graf [4]. a d j . push back ( 5 ) ;


graf [4]. w e i g h t . push back ( 8 ) ;
graf [5]. a d j . push back ( 4 ) ;
graf [5]. w e i g h t . push back ( 8 ) ;

graf [4]. a d j . push back ( 6 ) ;


graf [4]. w e i g h t . push back ( 9 ) ;
graf [6]. a d j . push back ( 4 ) ;
graf [6]. w e i g h t . push back ( 9 ) ;

graf [5]. a d j . push back ( 6 ) ;


graf [5]. w e i g h t . push back ( 1 1 ) ;
graf [6]. a d j . push back ( 5 ) ;
graf [6]. w e i g h t . push back ( 1 1 ) ;

p r i n t f ( " %d\n" , Prim ( ) ) ;

return 0 ;
}

Dicha implementación tiene una complejidad de O((V+E)log V) donde V


se corresponde con el número de vértices del grafo mientras E son el números

436
TEORÍA DE GRAFOS

de aristas.
La siguiente variante devuelve las aristas que conforman el árbol de ex-
pansión mı́nima que conforma el algoritmo.

p a i r <Weight , Edges> minimumSpanningTree ( const Graph & g , int r


= 0) {
int n = g . s i z e ( ) ;
Edges T ;
Weight t o t a l = 0 ;

v e c t o r <bool> v i s i t e d ( n ) ;
p r i o r i t y q u e u e <Edge> Q;
Q. push ( Edge (− 1 , r , 0 ) ) ;
while ( ! Q. empty ( ) )
{
Edge e = Q. top ( ) ;
Q. pop ( ) ;
i f ( v i s i t e d [ e . dst ] )
continue ;
T . push back ( e ) ;
t o t a l+=e . w e i g h t ;
v i s i t e d [ e . d s t ] = true ;
FOR ( f , g [ e . d s t ] )
i f ( ! v i s i t e d [ f −>d s t ] )
Q. push ( ∗ f ) ;
}
return p a i r <Weight , Edges> ( t o t a l , T) ;
}

Esta implementación tiene una complejidad de O (E logV), la cual recibe


como parámetros la lista de adyacencia del grafo y el nodo inicial por el cual
debe comenzar el algoritmo.

15.6. Algoritmo Dijkstra


El algoritmo de Dijkstra, también llamado algoritmo de caminos mı́nimos,
es un algoritmo para la determinación del camino más corto dado un vértice
origen al resto de los vértices en un grafo con pesos en cada arista, siempre
que los pesos sean positivos. Su nombre se refiere a Edsger Dijkstra, quien lo
describió por primera vez en 1959.
La idea subyacente en este algoritmo consiste en ir explorando todos
los caminos más cortos que parten del vértice origen y que llevan a todos
los demás vértices; cuando se obtiene el camino más corto desde el vértice
origen, al resto de vértices que componen el grafo, el algoritmo se detiene. El

437
TEORÍA DE GRAFOS

algoritmo es una especialización de la búsqueda de costo uniforme, y como


tal, no funciona en grafos con aristas de coste negativo (al elegir siempre el
nodo con distancia menor, pueden quedar excluidos de la búsqueda nodos
que en próximas iteraciones bajarı́an el costo general del camino al pasar por
una arista con costo negativo).
Teniendo un grafo dirigido ponderado de N nodos no aislados, sea x el
nodo inicial, un vector D de tamaño N guardará al final del algoritmo las
distancias desde x al resto de los nodos.

1. Inicializar todas las distancias en D con un valor infinito relativo ya que


son desconocidas al principio, exceptuando la de x que se debe colocar
en 0 debido a que la distancia de x a x serı́a 0.

2. Sea a = x (tomamos a como nodo actual).

3. Recorremos todos los nodos adyacentes de a, excepto los nodos marca-


dos, llamaremos a estos nodos no marcados vi .

4. Para el nodo actual, calculamos la distancia tentativa desde dicho nodo


a sus vecinos con la siguiente fórmula: dt(vi .) = Da . + d(a,vi .). Es de-
cir, la distancia tentativa del nodo ‘vi .’es la distancia que actualmente
tiene el nodo en el vector D más la distancia desde dicho el nodo ‘a’(el
actual) al nodo vi . Si la distancia tentativa es menor que la distan-
cia almacenada en el vector, actualizamos el vector con esta distancia
tentativa. Es decir: Si dt(vi ) < Dvi → Dvi = dt(vi )

5. Marcamos como completo el nodo a.

6. Tomamos como próximo nodo actual el de menor valor en D (puede


hacerse almacenando los valores en una cola de prioridad) y volvemos
al paso 3 mientras existan nodos no marcados.

Una vez terminado al algoritmo, D estará completamente lleno.


Orden de complejidad del algoritmo: O(|V|2 +|A|) = O(—V—2 ) sin uti-
lizar cola de prioridad, O((|A|+|V|) log |V|) = O(|A| log |V|) utilizando
cola de prioridad (por ejemplo un montı́culo). Por otro lado, si se utiliza un
Montı́culo de Fibonacci, serı́a O(|V| log |V|+|A|).
Podemos estimar la complejidad computacional del algoritmo de Dijkstra
(en términos de sumas y comparaciones). El algoritmo realiza a lo más n-1
iteraciones, ya que en cada iteración se añade un vértice al conjunto distin-
guido. Para estimar el número total de operaciones basta estimar el número
de operaciones que se llevan a cabo en cada iteración. Podemos identificar
el vértice con la menor etiqueta entre los que no están en Sk realizando n-1

438
TEORÍA DE GRAFOS

comparaciones o menos. Después hacemos una suma y una comparación para


actualizar la etiqueta de cada uno de los vértices que no están en Sk . Por
tanto, en cada iteración se realizan a lo sumo 2(n-1) operaciones, ya que no
puede haber más de n-1 etiquetas por actualizar en cada iteración. Como no
se realizan más de n-1 iteraciones, cada una de las cuales supone a lo más
2(n-1) operaciones, llegamos al siguiente teorema.
TEOREMA: El Algoritmo de Dijkstra realiza O(n2 ) operaciones (sumas
y comparaciones) para determinar la longitud del camino más corto entre dos
vértices de un grafo ponderado simple, conexo y no dirigido con n vértices.
A continuación dos pseudocódigo del algoritmo en el primero se utiliza
una estructura de datos auxiliar: Q = Estructura de datos Cola con prioridad
(se puede implementar con montı́culo)

DIJKSTRA ( Grafo G, n o d o f u e n t e s )
para u que p e r t e n e c e V[G] h a c e r
d i s t a n c i a [ u ] = INFINITO
padre [ u ] = NULL
visto [ u ] = false
distancia [ s ] = 0
adicionar ( cola , ( s , d i s t a n c i a [ s ] ) )
m i e n t r a s que c o l a no e s v a c i a h a c e r
u = extraer minimo ( cola )
v i s t o [ u ] = true
para t o d o s v p e r t e n e c e a d y a c e n c i a [ u ] h a c e r
s i no v i s t o [ v ] y d i s t a n c i a [ v ] > d i s t a n c i a [ u ] +
peso (u , v ) hacer
d i s t a n c i a [ v ] = d i s t a n c i a [ u ] + peso (u , v )
padre [ v ] = u
adicionar ( cola , ( v , d i s t a n c i a [ v ] ) )

Esta otra versión no utiliza la cola con prioridad

f u n c i o n D i j k s t r a ( Grafo G, n o d o s a l i d a s )
// Usaremos un v e c t o r para g u a r d a r l a s d i s t a n c i a s d e l nodo
salida al resto
entero distancia [ n ]
// I n i c i a l i z a m o s e l v e c t o r con d i s t a n c i a s i n i c i a l e s
booleano v i s t o [ n ]
// v e c t o r de b o l e a n o s para c o n t r o l a r l o s v e r t i c e s de l o s que ya
tenemos l a d i s t a n c i a minima
para cada w que p e r t e n e c e V[G] h a c e r
S i ( no e x i s t e a r i s t a e n t r e s y w) e n t o n c e s
d i s t a n c i a [ w ] = I n f i n i t o // puedes marcar l a c a s i l l a con
un −1 por e j e m p l o
Si no
d i s t a n c i a [ w ] = p e s o ( s , w)
fin si

439
TEORÍA DE GRAFOS

f i n para
distancia [ s ] = 0
visto [ s ] = cierto
//n e s e l numero de v e r t i c e s que t i e n e e l Grafo
m i e n t r a s que ( n o e s t e n v i s t o s t o d o s ) h a c e r
v e r t i c e = c o g e r e l m i n i m o d e l v e c t o r d i s t a n c i a y que no
este visto ;
visto [ vertice ] = cierto ;
para cada w que p e r t e n e c e s u c e s o r e s (G, v e r t i c e ) h a c e r
s i d i s t a n c i a [ w]> d i s t a n c i a [ v e r t i c e ]+ p e s o ( v e r t i c e , w)
entonces
d i s t a n c i a [ w ] = d i s t a n c i a [ v e r t i c e ]+ p e s o ( v e r t i c e , w)
fin si
f i n para
f i n mientras
f i n funcion .

En esta variante al final tenemos en el vector distancia en cada posición


la distancia mı́nima del vértice salida a otro vértice cualquiera. Para una
mejor compresión veamos la siguiente figura donde se ilustra el ejecucción
del algoritmo.

Figura 15.9: Ejecucción del algoritmo Dijkstra comenzando por el nodo s.

Veamos ahora las implementaciones del algoritmo.

#include <s t d i o . h>


#include <math . h>
#include <s t r i n g . h>

440
TEORÍA DE GRAFOS

#include <i o s t r e a m >


#include <v e c t o r >
#include < l i s t >
#include <s t r i n g >
#include <a l g o r i t h m >
#include <queue>
#include <s t a c k >
#include <s e t >
#include <map>
#include <complex>
#define MAX N 100001
#define INF 987654321
using namespace s t d ;
typedef long long l l d ;

int n ;

struct Node
{
int d i s t ;
v e c t o r <int> a d j ;
v e c t o r <int> w e i g h t ;
};
Node g r a f [MAX N ] ;
bool mark [MAX N ] ;

struct p q e n t r y
{
int node , d i s t ;
bool operator <(const p q e n t r y &a ) const
{
i f ( d i s t != a . d i s t ) return ( d i s t > a . d i s t ) ;
return ( node > a . node ) ;
}
};

i n l i n e void D i j k s t r a ( int s o u r c e )
{
p r i o r i t y q u e u e <p q e n t r y > pq ;
pq entry P;
f o r ( int i =0; i <n ; i ++)
{
i f ( i == s o u r c e )
{
graf [ i ] . dist = 0;
P . node = i ;
P. dist = 0;
pq . push (P) ;
}

441
TEORÍA DE GRAFOS

e l s e g r a f [ i ] . d i s t = INF ;
}
while ( ! pq . empty ( ) )
{
p q e n t r y c u r r = pq . top ( ) ;
pq . pop ( ) ;
int nod = c u r r . node ;
int d i s = c u r r . d i s t ;
f o r ( int i =0; i <g r a f [ nod ] . a d j . s i z e ( ) ; i ++)
{
i f ( ! mark [ g r a f [ nod ] . a d j [ i ] ] )
{
int nextNode = g r a f [ nod ] . a d j [ i ] ;
i f ( d i s + g r a f [ nod ] . w e i g h t [ i ] < g r a f [ nextNode ] .
dist )
{
g r a f [ nextNode ] . d i s t = d i s + g r a f [ nod ] . w e i g h t
[ i ];
P . node = nextNode ;
P . d i s t = g r a f [ nextNode ] . d i s t ;
pq . push (P) ;
}
}
}
mark [ nod ] = true ;
}
}

int main ( )
{
n = 4;

graf [0]. a d j . push back ( 1 ) ;


graf [0]. w e i g h t . push back ( 5 ) ;
graf [1]. a d j . push back ( 0 ) ;
graf [1]. w e i g h t . push back ( 5 ) ;

graf [1]. a d j . push back ( 2 ) ;


graf [1]. w e i g h t . push back ( 5 ) ;
graf [2]. a d j . push back ( 1 ) ;
graf [2]. w e i g h t . push back ( 5 ) ;

graf [2]. a d j . push back ( 3 ) ;


graf [2]. w e i g h t . push back ( 5 ) ;
graf [3]. a d j . push back ( 2 ) ;
graf [3]. w e i g h t . push back ( 5 ) ;

g r a f [ 3 ] . a d j . push back ( 1 ) ;
g r a f [ 3 ] . w e i g h t . push back ( 6 ) ;

442
TEORÍA DE GRAFOS

g r a f [ 1 ] . a d j . push back ( 3 ) ;
g r a f [ 1 ] . w e i g h t . push back ( 6 ) ;

Dijkstra (0) ;

p r i n t f ( " %d\n" , g r a f [ 3 ] . d i s t ) ;
return 0 ;
}

Esta implementación tiene una complejidad de O((V+E)log V) donde V


son los vértices del grafo y E las aristas del mismo. La siguiente implementa-
ción tiene una complejidad de O (E log V), además propone un mecanismo
para una vez hallado la distancia mı́nima a partir de un nodo a todos los
nodos del grafo la construcción del camino mı́nimo.

void s h o r t e s t P a t h ( const Graph & g , int s , v e c t o r <Weight> & d i s t


, v e c t o r <int> & prev )
{
int n = g . s i z e ( ) ;
d i s t . a s s i g n ( n , INF ) ;
dist [ s ] = 0;
prev . a s s i g n ( n,− 1 ) ;
p r i o r i t y q u e u e <Edge> Q; // ” e <f ” <=> ” e . weight> f .
weight ”
f o r (Q. push ( Edge(− 2 , s , 0 ) ) ; ! Q. empty ( ) ; )
{
Edge e =Q. f r o n t ( ) ;
Q. pop ( ) ;
i f ( ! prev [ e . d s t ]=−1)
continue ;
prev [ e . d s t ]= e . s r c ;
FOR( f , g [ e . d s t ] )
{
i f ( d i s t [ f −>d s t ]> e . w e i g h t+f −>w e i g h t )
{
d i s t [ f −>d s t ]= e . w e i g h t+f −>w e i g h t ;
Q. push ( Edge ( f −>s r c , f −>dst , e .
w e i g h t+f −>w e i g h t ) ) ;
}
}
}
}

v e c t o r <int> b u i l d P a t h ( const v e c t o r <int> & prev , int t )


{
v e c t o r <int> path ;
f o r ( int u=t ; u>=0; u=prev [ u ] )
path . push back ( u ) ;

443
TEORÍA DE GRAFOS

r e v e r s e ( path . b e g i n ( ) , path . end ( ) ) ;


return path ;
}

Esta última variante recibe como parámetros los siguientes datos:

Const Graph & g: Grafo sobre el cual deseamos aplicar el algoritmo

int s: Nodo inicial del cual se desea calcular la distancia mı́nima desde
el hacia cualquier nodo grafo.

Vector <Weight> & dist : Vector que va almacenar en cada posición la


distancia mı́nima entre el nodo inicial y el nodo correspondiente a esa
posición en el grafo.

Vector <int> & prev: Vector que va almacenar en la posición i el valor


del nodo que antecede al nodo i en el camino mı́nimo entre el nodo
inicial y el nodo i.

Además se cuenta con la funcionalidad buildPath que una vez ejecutamos


el algoritmo y le pasamos por parámetro el vector <int> & prev, nos devuelve
los nodos que componen el camino mı́nimo entre el nodo inicial y cualquier
otro nodo que pasemos como parámetro a la función.

15.7. Algoritmo Bellman-Ford


El algoritmo de Bellman-Ford (algoritmo de Bell-End-Ford), genera el ca-
mino más corto en un Grafo dirigido ponderado (en el que el peso de alguna
de las aristas puede ser negativo). El algoritmo de Dijkstra resuelve este mis-
mo problema en un tiempo menor, pero requiere que los pesos de las aristas
no sean negativos. Por lo que el Algoritmo Bellman-Ford normalmente se
utiliza cuando hay aristas con peso negativo. Este algoritmo fue desarrollado
por Richard Bellman, Samuel End y Lester Ford.
Si el grafo contiene un ciclo de coste negativo, el algoritmo lo detectará,
pero no encontrará el camino más corto que no repite ningún vértice. La
complejidad de este problema es al menos la del problema del camino más
largo de complejidad NP-Completo.
El algoritmo de Bellman-Ford es, en su estructura básica, muy parecido al
algoritmo de Dijkstra, pero en vez de seleccionar vorazmente el nodo de peso
mı́nimo aun sin procesar para relajarlo, simplemente relaja todas las aristas,
y lo hace |V | − 1 veces, siendo |V | el número de vértices en el grafo. Las
repeticiones permiten a las distancias mı́nimas recorrer el árbol, ya que en la

444
TEORÍA DE GRAFOS

ausencia de ciclos negativos, el camino más corto solo visita cada vértice una
vez. A diferencia de la solución voraz, la cual depende de la suposición de
que los pesos sean positivos, esta solución se aproxima más al caso general.
Existen dos versiones:

Versión no optimizada para grafos con ciclos negativos, cuyo coste de


tiempo es O(VE).

bool BellmanFord ( Grafo G, n o d o o r i g e n s )


// i n i c i a l i z a m o s e l g r a f o . Ponemos d i s t a n c i a s a
INFINITO menos e l nodo o r i g e n que
// t i e n e d i s t a n c i a 0
f o r v i n V[G] do
d i s t a n c i a [ v]=INFINITO
p r e d e c e s o r [ v]=NIL
d i s t a n c i a [ s ]=0
// r e l a j a m o s cada a r i s t a d e l g r a f o t a n t a s v e c e s como
numero de nodos −1 haya en e l g r a f o
f o r i =1 t o |V[G]| −1 do
f o r ( u , v ) i n E [G] do
i f d i s t a n c i a [ v]> d i s t a n c i a [ u ] + p e s o ( u , v )
then
d i s t a n c i a [ v ] = d i s t a n c i a [ u ] + peso (u , v )
predecesor [ v ] = u
// comprobamos s i hay c i c l o s n e g a t i v o
f o r ( u , v ) i n E [G] do
i f d i s t a n c i a [ v ] > d i s t a n c i a [ u ] + p e s o ( u , v ) then
p r i n t ( "Hay ciclo negativo " )
return FALSE
return TRUE

Versión optimizada para grafos con aristas de peso negativo, pero en


el grafo no existen ciclos de coste negativo, cuyo coste de tiempo, es
también O(VE).

bool BellmanFord Optimizado ( Grafo G, n o d o o r i g e n s )


// i n i c i a l i z a m o s e l g r a f o . Ponemos d i s t a n c i a s a
INFINITO menos e l nodo o r i g e n que
// t i e n e d i s t a n c i a 0 . Para e l l o l o hacemos
recorriendonos todos l o s v e r t i c e s del grafo
f o r v i n V[G] do
d i s t a n c i a [ v]=INFINITO
padre [ v]=NIL
d i s t a n c i a [ s ]=0
e n c o l a r ( s , Q)
e n c o l a [ s ]=TRUE
while Q!=0 then

445
TEORÍA DE GRAFOS

u = e x t r a e r (Q)
e n c o l a [ u]=FALSE
// r e l a j a m o s l a s a r i s t a s
f o r v i n ady [ u ] do
i f d i s t a n c i a [ v]> d i s t a n c i a [ u ] + p e s o ( u , v )
then
d i s t a n c i a [ v ] = d i s t a n c i a [ u ] + peso (u , v )
padre [ v ] = u
i f e n c o l a [ v]==FALSE then
e n c o l a r ( v , Q)
e n c o l a [ v]=TRUE

El la siguiente imagen se muestra la ejcucción del algoritmo. El mismo


comienza por el nodo z.

Figura 15.10: Ejecucción del algoritmo Bellman-Ford.

A continuación su implementación en C++

#include <s t d i o . h>


#include <i o s t r e a m >
#define MAX N 5001
#define MAX E 25000001
#define INF 987654321
using namespace s t d ;
typedef long long l l d ;

446
TEORÍA DE GRAFOS

int v , e ;

int d i s t [MAX N ] ;
struct Edge
{
int x , y , w e i g h t ;
};
Edge E [MAX N ] ;

i n l i n e int BellmanFord ( int s o u r c e )


{
f o r ( int i =0; i <v ; i ++)
{
i f ( i == s o u r c e ) d i s t [ i ] = 0 ;
e l s e d i s t [ i ] = INF ;
}
bool done = f a l s e ;
f o r ( int i =0; ! done && i <v ; i ++)
{
done = true ;
f o r ( int j =0; j <e ; j ++)
{
int s o = E [ j ] . x ;
int de = E [ j ] . y ;
i f ( d i s t [ s o ] + E [ j ] . w e i g h t < d i s t [ de ] )
{
d i s t [ de ] = d i s t [ s o ] + E [ j ] . w e i g h t ;
done=f a l s e ;
}
}
}
i f ( ! done ) return −1; // c i c l o n e g a t i v o d e t e c t a d o
return 0 ;
}

int main ( )
{
v = 4 , e = 8;

E [ 0 ] . x = 0 , E [ 0 ] . y = 1 , E [ 0 ] . weight = 5 ;
E [ 1 ] . x = 1 , E [ 1 ] . y = 0 , E [ 1 ] . weight = 5 ;

E [ 2 ] . x = 1 , E [ 2 ] . y = 2 , E [ 2 ] . weight = 5 ;
E [ 3 ] . x = 2 , E [ 3 ] . y = 1 , E [ 3 ] . weight = 5 ;

E [ 4 ] . x = 2 , E [ 4 ] . y = 3 , E [ 4 ] . weight = 5 ;
E [ 5 ] . x = 3 , E [ 5 ] . y = 2 , E [ 5 ] . weight = 5 ;

E [ 6 ] . x = 3 , E [ 6 ] . y = 1 , E [ 6 ] . weight = 6 ;

447
TEORÍA DE GRAFOS

E [ 7 ] . x = 1 , E [ 7 ] . y = 3 , E [ 7 ] . weight = 6 ;

BellmanFord ( 0 ) ;
p r i n t f ( " %d\n" , d i s t [ 3 ] ) ;
return 0 ;
}

15.8. Algoritmo Floyd-Warshall


En informática, el algoritmo de Floyd-Warshall, descrito en 1959 por Ber-
nard Roy, es un algoritmo de análisis sobre grafos para encontrar el camino
mı́nimo en grafos dirigidos ponderados. El algoritmo encuentra el camino
entre todos los pares de vértices en una única ejecución. El algoritmo de
Floyd-Warshall es un ejemplo de programación dinámica.
El algoritmo de Floyd-Warshall compara todos los posibles caminos a
través del grafo entre cada par de vértices. El algoritmo es capaz de hacer
esto con sólo V3 comparaciones (esto es notable considerando que puede
haber hasta V2 aristas en el grafo, y que cada combinación de aristas se
prueba). Lo hace mejorando paulatinamente una estimación del camino más
corto entre dos vértices, hasta que se sabe que la estimación es óptima.
Sea un grafo G con conjunto de vértices V, numerados de 1 a N. Sea
además una función caminoMinimo(i,j,k) que devuelve el camino mı́nimo de
i a j usando únicamente los vértices de 1 a k como puntos intermedios en el
camino. Ahora, dada esta función, nuestro objetivo es encontrar el camino
mı́nimo desde cada i a cada j usando únicamente los vértices de 1 hasta k+1.
Hay dos candidatos para este camino: un camino mı́nimo, que utiliza
únicamente los vértices del conjunto (1...k); o bien existe un camino que va
desde i hasta k+1 , y de k+1 hasta j , que es mejor. Sabemos que el camino
óptimo de i a j que únicamente utiliza los vértices de 1 hasta k está definido
por caminoMinimo(i,j,k) , y está claro que si hubiera un camino mejor de
i a k+1 a j , la longitud de este camino serı́a la concatenación del camino
mı́nimo de i a k+1 (utilizando vértices de (1...k) ) y el camino mı́nimo de
k+1 a j (que también utiliza los vértices en (1...k) ).
Para que haya coherencia numérica, Floyd-Warshall supone que no hay
ciclos negativos (de hecho, entre cualquier pareja de vértices que forme parte
de un ciclo negativo, el camino mı́nimo no está bien definido porque el ca-
mino puede ser infinitamente pequeño). No obstante, si hay ciclos negativos,
Floyd-Warshall puede ser usado para detectarlos. Si ejecutamos el algoritmo
una vez más, algunos caminos pueden decrementarse pero no garantiza que,
entre todos los vértices, caminos entre los cuales puedan ser infinitamente

448
TEORÍA DE GRAFOS

pequeños, el camino se reduzca. Si los números de la diagonal de la matriz


de caminos son negativos, es condición necesaria y suficiente para que este
vértice pertenezca a un ciclo negativo.
En código el algoritmo serı́a ası́:

i n l i n e void FloydWarshall ( )
{
f o r ( int i =1; i<=n ; i ++)
{
f o r ( int j =1; j<=n ; j ++)
{
flojd [ i ] [ j ] = dist [ i ] [ j ] ;
}
flojd [ i ] [ i ] = 0;
}
f o r ( int k=1;k<=n ; k++)
{
f o r ( int i =1; i<=n ; i ++)
{
f o r ( int j =1; j<=n ; j ++)
{
if ( flojd [ i ][ k] + flojd [k ][ j ] < flojd [ i ][ j ])
{
flojd [ i ][ j ] = flojd [ i ][ k] + flojd [k ][ j ];
}
}
}
}
}

Donde dist es la matriz de distancia del grafo, una vez ejecutado el al-
goritmo la distancia mı́nima entre cualquier par x,y va estar en flojd[x][y].
Como podemos analizar el algoritmo tiene una complejidad de N3 donde N
es la cantidad de nodos, lo que hace que este algoritmo sea factible su uti-
lización cuando el numero de de nodos sea menor igual a 100 nodos lo que
significa en un millón de operaciones por parte del algoritmo.
El algoritmo de Floyd-Warshall puede ser utilizado para resolver los si-
guientes problemas, entre otros:

1. Camino mı́nimo en grafos dirigidos (algoritmo de Floyd).

2. Cierre transitivo en grafos dirigidos (algoritmo de Warshall). Es la for-


mulación original del algoritmo de Warshall. El grafo es un grafo no
ponderado y representado por una matriz booleana de adyacencia. En-
tonces la operación de adición es reemplazada por la conjunción lógi-
ca(AND) y la operación menor por la disyunción lógica (OR).

449
TEORÍA DE GRAFOS

3. Encontrar una expresión regular dada por un lenguaje regular aceptado


por un autómata finito (algoritmo de Kleene).

4. Comprobar si un grafo no dirigido es bipartito.

5. Ruta optima. En esta aplicación es interesante encontrar el camino del


flujo máximo entre 2 vértices. Esto significa que en lugar de tomar los
mı́nimos con el pseudocodigo anterior, se coge el máximo. Los pesos
de las aristas representan las limitaciones del flujo. Los pesos de los
caminos representan cuellos de botella; por ello, la operación de adición
anterior es reemplazada por la operación mı́nimo.

6. Inversión de matrices de números reales (algoritmo de Gauss-Jordan).

Pero tener en cuenta para su utilización por su alta complejidad temporal.

15.9. Orden topológico


Una ordenación topológica (topological sort, topological ordering, topsort
o toposort en inglés) de un grafo acı́clico G dirigido es una ordenación lineal
de todos los nodos de G que conserva la unión entre vértices del grafo G
original. La condición que el grafo no contenga ciclos es importante, ya que
no se puede obtener ordenación topológica de grafos que contengan ciclos.
Usualmente, para clarificar el concepto se suelen identificar los nodos con
tareas a realizar en la que hay una precedencia a la hora de ejecutar dichas
tareas. La ordenación topológica por tanto es una lista en orden lineal en que
deben realizarse las tareas. Para poder encontrar la ordenación topológica del
grafo G deberemos aplicar una modificación del algoritmo de búsqueda en
profundidad (DFS). Los algoritmos usuales para el ordenamiento topológico
tienen un tiempo de ejecución de la cantidad de nodos más la cantidad de
aristas (O(|V | + |E|)).
Uno de los algoritmos primero descrito por Kahn (1962),trabaja eligien-
dolos vértices del mismo orden como un eventual orden topológico. Primero,
busca la lista de los ”nodos iniciales”que no tienen arcos entrantes y los in-
serta en un conjunto S; donde al menos uno de esos nodos existe si el grafo
es acı́clico.
La ordenación topológica no es única. Depende en qué orden recorras los
nodos del grafo
En codigo el algoritmo serı́a asi:

#define MAX N 5001


int n ;

450
TEORÍA DE GRAFOS

struct Node
{
v e c t o r <int> a d j ;
};
Node g r a f [MAX N ] ;
int i n d e g r e e [MAX N ] ;
int t o p o s o r t [MAX N ] ;
i n l i n e int TopoSort ( )
{
queue<int> S ;
f o r ( int i =0; i <n ; i ++) i f ( i n d e g r e e [ i ] == 0 ) S . push ( i ) ;
int i d x = 0 ;
while ( ! S . empty ( ) )
{
int i d d = S . f r o n t ( ) ;
S . pop ( ) ;
t o p o s o r t [ i d x ++] = i d d ;
f o r ( int i =0; i <g r a f [ i d d ] . a d j . s i z e ( ) ; i ++)
{
i f (−− i n d e g r e e [ g r a f [ i d d ] . a d j [ i ] ] == 0 ) S . push ( g r a f [
idd ] . adj [ i ] ) ;
}
}
i f ( idx < n)
return −1; // C i c l o d e t e c t a d o !
else
return 0 ;
}

Donde n es la cantidad de nodos que tiene el grafo sobre el cual vamos apli-
car el ordenamiento topológico, graf es lo que utilizaremos para representar
nuestro grafo es una estructura que para el nodo i tiene un vector con los
nodos adyacentes a él. El arreglo indegree se va almacenar en la inésima la
cantidad de aristas incidentes ( si la arista termina en ese nodo, no se cuenta
si la arista parte de él) sobre el inésimo nodo del grafo. El arreglo toposort
estarán los nodos en orden topológico siempre y cuando la ejecución del algo-
ritmo devuelva como valor 0 en caso de que su valor de retorno sea -1 menos
significa que el grafo tiene un ciclo lo que significa que no se puede hacer un
ordenamiento topológico sobre el grafo.
Como podemos apreciar el ordenamiento topológico solo se puede obtener
cuando el grafo no presenta ciclos. La complejidad del algoritmo el O(E+V)
donde E es la cantidad de aristas y V la cantidad de nodos del grafo. Tener
presente que un grafo puede tener multiple ordenamientos topologicos todo
depende del orden en que se seleccione los nodos del grafo.

451
TEORÍA DE GRAFOS

15.10. Componentes conexas


Un grafo no dirigido es conexo si cada par de vértices está conectado por
un camino; es decir, si para cualquier par de vértices (a, b), existe al menos
un camino posible desde a hacia b. Esto es fácil de comprobar si se aplica un
BFS o DFS sobre todo el grafo y si todos los nodos del grafo están visitados
después de la ejecucción del algoritmo de recorrido sobre el grafo que hemos
escogido entonces podemos afirmar que el grafo es conexo. Pero si no lo es,
como podemos determinar la cantidad de componentes conexas que tiene el
grafo no dirigido.
Bien la solución es bastante sencilla. Vamos asumir que inicialemente que
la cantidad de componentes conexas de un grafo es 0 y que todos los nodos
del grafo no han sido visitados. Pues dicho esto vamos a recorrer cada nodo
del grafo y cada vez que analice un nodo puede pasar dos cosas:

Nodo no visitado: El nodo no ha sido visitado, por tanto estoy en


presencia de un nodo que integra o forma parte de una componente co-
nexa del grafo aún no detectada. Por tanto se debe realizar dos acciones
inmediatamente:

1. Incrementar en uno la cantidad de componentes conexas del grafo


porque se acaba de hallar una que no se habı́a detectado hasta el
momento.
2. Recorrer dicha componente partiendo desde el nodo analizado en
profundidad. Cambiando a visitado todos los nodos que sean ana-
lizados en el recorrido en profundidad.

Nodo visitado: El nodo ha sido visitado porque forma parte de una


componente conexa detectada antes de llegar a este nodo. Por tanto no
se debe realizar nada en este caso.

Dicho esto es claro que para contar la cantidad de componentes conexas


de un grafo no dirigido es necesario realizar un DFS aunque tambien se puede
sustituir por un BFS. Aqui un ejemplo de implementación para determinar
la cantidad de componentes conexas de un grafo no dirigido utilizando un
DFS.

#include <s t d i o . h>


#include <math . h>
#include <s t r i n g . h>
#include <i o s t r e a m >
#include <v e c t o r >
#include <l i s t >

452
TEORÍA DE GRAFOS

#include <s t r i n g >


#include <a l g o r i t h m >
#include <queue>
#include <s t a c k >
#include <s e t >
#include <map>
#include <complex>
#define MAX N 5001
using namespace s t d ;
typedef long long l l d ;

int n ;

struct Node
{
v e c t o r <int> a d j ;
};
Node g r a f [MAX N ] ;
bool mark [MAX N ] ;

i n l i n e void DFS( int s t a r t )


{
s t a c k <int> d f s s t e k ;
d f s s t e k . push ( s t a r t ) ;
while ( ! d f s s t e k . empty ( ) )
{
int xt = d f s s t e k . top ( ) ;
d f s s t e k . pop ( ) ;
mark [ xt ] = true ;
f o r ( int i =0; i <g r a f [ xt ] . a d j . s i z e ( ) ; i ++)
{
i f ( ! mark [ g r a f [ xt ] . a d j [ i ] ] )
{
d f s s t e k . push ( g r a f [ xt ] . a d j [ i ] ) ;
mark [ g r a f [ xt ] . a d j [ i ] ] = true ;
}
}
}
}

i n l i n e int numComponents ( int n )


{
int r e t = 0 ;
f o r ( int i =0; i <n ; i ++)
{
i f ( ! mark [ i ] )
{
DFS( i ) ;
r e t ++;

453
TEORÍA DE GRAFOS

}
}
return r e t ;
}

int main ( )
{
n = 6;

g r a f [ 0 ] . a d j . push back ( 1 ) ;
g r a f [ 1 ] . a d j . push back ( 0 ) ;

g r a f [ 0 ] . a d j . push back ( 2 ) ;
g r a f [ 2 ] . a d j . push back ( 0 ) ;

g r a f [ 4 ] . a d j . push back ( 4 ) ;

g r a f [ 3 ] . a d j . push back ( 5 ) ;
g r a f [ 5 ] . a d j . push back ( 3 ) ;

p r i n t f ( " %d\n" , numComponents ( n ) ) ;

return 0 ;
}

Como sabemos la complejidad del DFS es igual O(V + E) siendo V y E


los nodos y aristas respectivamente del grafo, y como es llamado dentro de
un ciclo que se ejecuta n la complejidad es igual a O( n(V+E)) donde n es
igual a V. Por lo que la complejidad del algoritmo es O( N(V+E)). Pero no
se alarmen por esta complejidad, veremos que no es alta como parece.
Vamos analizar los dos casos extremos:

Un grafo con V vértices con una sola componente conexa: En


un grafo con una sola componente conexa el DFS solo se ejecutará en
la primera iteración del ciclo y en el resto de la iteraciones no será ası́
porque ya los nodos estarán visitados y el tiempo será constante O(1).
Por lo que para ese caso la complejidad es O(V+E).

Un grafo con V vértices con V componentes conexa: En un


grafo con V vértices y con V componentes conexa significa que es un
grafo sin aristas esto significa que realizar un DFS sobre cualquier nodo
de ese grafo va ser en tiempo constante OO(1) porque es un solo nodo
de esa supuesta componente conexas que no tiene arista y dada que este
procedimiento se va repetir para los V vértices del grafo la complejidad
para este caso es O(V).

454
TEORÍA DE GRAFOS

Cualquier otro caso va oscilar en este rango definido por estos dos ca-
sos explicados anteriormenente. Es por eso que en el peor de los casos este
algoritmo va tener una complejidad de O(V+E).

15.11. Diámetro del árbol


El diámetro de árbol se define como la longitud máxima que existe entre
cualquier par de nodos que integren el árbol. Para poder aplicar este algorit-
mo el árbol que se vaya analizar tiene que ser no dirigido y la aristas con un
peso no negativo.
El algoritmo en sı́ mismo es bastante simple. Se toma un nodo S de forma
aleatoria y se explora el resto del árbol usando un dfs y calculando para cada
nodo del árbol su distancia con respecto al nodo S. Una vez terminado este
primer dfs buscamos que nodo esta más alejado con respecto al nodo S que
tomamos de forma aleatoria la primera vez. Al nodo mas alejado del nodo
S lo nombraremos nodo U. Ahora a partir del nodo U realizaremos un dfs
para buscar el nodo V que va ser el nodo más alejado del nodo U. Una vez
hallado el nodo V, podemos decir con seguridad que el par de nodos U,V
son los nodos más alejados en el árbol por tanto su distancia entre ellos va a
definir el dı́ametro del árbol.
Este algoritmo tiene una complejidad de O(E) siendo E la cantidad de
aristas del árbol que por definición de un árbol va ser igual a la cantidad de
nodos del árbol menos uno.
A continuación una implementación del algoritmo:

typedef p a i r <Weight , int> R e s u l t ;

R e s u l t v i s i t ( int p , int v , const Graph &g )


{
Result r (0 , v) ;
FOR ( e , g [ v ] )
i f ( e−>d s t !=P)
{
R e s u l t t = v i s i t ( v , e−>dst , g ) ;
t . f i r s t + = e−> w e i g h t ;
i f ( r . f i r s t <t . f i r s t ) r = t ;
}
return r ;
}

Weight d i a m e t e r ( const Graph &g )


{
R e s u l t r = v i s i t (− 1 , 0 , g ) ;

455
TEORÍA DE GRAFOS

R e s u l t t = v i s i t (− 1 , r . second , g ) ;
return t . f i r s t ; // ( r . second , t . s e c o n d ) i s f a r t h e s t p a i r
}

La siguiente es otra implementación donde los nodos del grafos son enu-
merados de 0 a N-1.

struct Node
{
int m d i s t ;
v e c t o r <int> m n e i g h b o r s ;
int m dfs ;
Node ( )
{
m neighbors . c l e a r ( ) ;
m dfs =0;
m d i s t =0;
}
};

Node g r a p h s [MAX] ;

int d i a m e t e r T r e e ( int n o d e S t a r t )
{
int tDiameter =0;
int nextNode , currentNode , f a r t h e s t N o d e= n o d e S t a r t ;
g r a p h s [ n o d e S t a r t ] . m d i s t =0;
g r a p h s [ n o d e S t a r t ] . m dfs =1;

s t a c k <int> v i s i t ;
v i s i t . push ( n o d e S t a r t ) ;

while ( ! v i s i t . empty ( ) )
{
currentNode= v i s i t . top ( ) ;
v i s i t . pop ( ) ;
i f ( g r a p h s [ currentNode ] . m dist >tDiameter )
{
tDiameter=g r a p h s [ currentNode ] . m d i s t ;
f a r t h e s t N o d e=currentNode ;
}

int t N e i g h b o r s=g r a p h s [ currentNode ] . m n e i g h b o r s . s i z e ( ) ;


REP( i , t N e i g h b o r s )
{
int nextNode=g r a p h s [ currentNode ] . m n e i g h b o r s [ i ] ;
i f ( g r a p h s [ nextNode ] . m dfs <1)
{
g r a p h s [ nextNode ] . m dfs =1;

456
TEORÍA DE GRAFOS

g r a p h s [ nextNode ] . m d i s t=g r a p h s [ currentNode ] . m d i s t +1;


i f ( g r a p h s [ nextNode ] . m dist >tDiameter )
{
tDiameter=g r a p h s [ nextNode ] . m d i s t ;
f a r t h e s t N o d e=nextNode ;
}
v i s i t . push ( nextNode ) ;
}
}
}

g r a p h s [ f a r t h e s t N o d e ] . m d i s t =0;
g r a p h s [ f a r t h e s t N o d e ] . m dfs =2;
tDiameter =0;

v i s i t . push ( f a r t h e s t N o d e ) ;

while ( ! v i s i t . empty ( ) )
{
currentNode= v i s i t . top ( ) ;
v i s i t . pop ( ) ;
i f ( g r a p h s [ currentNode ] . m dist >tDiameter )
{
tDiameter=g r a p h s [ currentNode ] . m d i s t ;
f a r t h e s t N o d e=currentNode ;
}

int t N e i g h b o r s=g r a p h s [ currentNode ] . m n e i g h b o r s . s i z e ( ) ;


REP( i , t N e i g h b o r s )
{
int nextNode=g r a p h s [ currentNode ] . m n e i g h b o r s [ i ] ;
i f ( g r a p h s [ nextNode ] . m dfs <2)
{
g r a p h s [ nextNode ] . m dfs =2;
g r a p h s [ nextNode ] . m d i s t=g r a p h s [ currentNode ] .
m d i s t +1;
i f ( g r a p h s [ nextNode ] . m dist >tDiameter )
{
tDiameter=g r a p h s [ nextNode ] . m d i s t ;
f a r t h e s t N o d e=nextNode ;
}
v i s i t . push ( nextNode ) ;
}
}
}
return tDiameter ;
}

457
TEORÍA DE GRAFOS

15.12. Detención de ciclos de un grafo


Dentro de una grafo se entiende como un ciclo es una sucesión de aristas
adyacentes, donde no se recorre dos veces la misma arista, y donde se regresa
al nodo inicial. Entonces dado un grafo como determinar si ese grafo su aristas
conforman al menos un ciclo. Para solucionar esto haremos uso de uno de los
algoritmos de recorrido dentro de un grafo como es el DFS. Como es sabido
el DFS recorre el grafo en profundidad y va marcando los nodos visitados y
vamos aprovechar ese detalle para determinar si el grafo tiene un ciclo o no.
Si durante la ejecucción del DFS estamos visitando el nodo u y unos de sus
vecinos el nodo z esta visitado ya, el DFS como es sabido no lo adicionará a
la pila. Pero este detalle nos dice que existe un camino del nodo z al nodo u y
que si tomaramos las aristas que conforman ese camino y le adicionaramos la
arista que va del nodo u al nodo z estariamos en presencia de un ciclo dentro
del grafo. Es por eso que para saber si un grafo tiene o no un ciclo basta
con aplicar un DFS sobre el grafo y si algún momento el nodo que se intenta
visitar ya esta visitado significa que hallaste un ciclo en el grafo. Se puede
comenzar el DFS por cualquier nodo del grafo. Ahora bien esto se puede
aplicar cuando estamos en presencia de grafo dirigido, pero cuando estamos
en presencia de un grafo no dirigido como resolver el hecho que del nodo u
sale una arista al nodo z o de igual forma del nodo z sale la arista hacia el
nodo u. Sencillo solo debemos llevar para cada nodo que es visitado en el
DFS el nodo que provocó que lo vistarán y si el nodo hacia donde se quiere
ir esta visitado y es un nodo distinto al nodo que provocó que lo vistarán
entonces tenemos un ciclo a continación una implementación de lo anterior.

#include <s t d i o . h>


#include <math . h>
#include <s t r i n g . h>
#include <i o s t r e a m >
#include <v e c t o r >
#include < l i s t >
#include <s t r i n g >
#include <a l g o r i t h m >
#include <queue>
#include <s t a c k >
#include <s e t >
#include <map>
#include <complex>
#define MAX N 5001
using namespace s t d ;
typedef long long l l d ;

int n ;

458
TEORÍA DE GRAFOS

struct Node
{
v e c t o r <int> a d j ;
};
Node g r a f [MAX N ] ;
bool mark [MAX N ] ;

i n l i n e bool DFS( int s t a r t )


{
s t a c k <int> d f s s t e k ;
s t a c k <int> p r e v i o u s ;
d f s s t e k . push ( s t a r t ) ;
p r e v i o u s . push ( −1) ;
while ( ! d f s s t e k . empty ( ) )
{
int xt = d f s s t e k . top ( ) ;
int pt = p r e v i o u s . top ( ) ;
d f s s t e k . pop ( ) ;
p r e v i o u s . pop ( ) ;
mark [ xt ] = true ;
f o r ( int i =0; i <g r a f [ xt ] . a d j . s i z e ( ) ; i ++)
{
i f ( ! mark [ g r a f [ xt ] . a d j [ i ] ] )
{
d f s s t e k . push ( g r a f [ xt ] . a d j [ i ] ) ;
p r e v i o u s . push ( xt ) ;
mark [ g r a f [ xt ] . a d j [ i ] ] = true ;
}
e l s e i f ( g r a f [ xt ] . a d j [ i ] != pt )
return true ;
}
}
return f a l s e ;
}

i n l i n e bool h a s C y c l e ( int n )
{
f o r ( int i =0; i <n ; i ++)
{
i f ( ! mark [ i ] )
{
i f (DFS( i ) ) return true ;
}
}
return f a l s e ;
}

int main ( )

459
TEORÍA DE GRAFOS

{
n = 6;
g r a f [ 0 ] . a d j . push back ( 1 ) ;
g r a f [ 1 ] . a d j . push back ( 0 ) ;
g r a f [ 0 ] . a d j . push back ( 2 ) ;
g r a f [ 2 ] . a d j . push back ( 0 ) ;
g r a f [ 2 ] . a d j . push back ( 1 ) ;
g r a f [ 1 ] . a d j . push back ( 2 ) ;
g r a f [ 3 ] . a d j . push back ( 5 ) ;
g r a f [ 5 ] . a d j . push back ( 3 ) ;
p r i n t f ( " %d\n" , h a s C y c l e ( n ) ? 1 : 0 ) ;
return 0 ;
}

Como utilizamos un DFS la complejidad de la anterior implementación


es O(E+V) siendo E la cantidad de aristas y V la cantidad de vértices del
grafo respectivamente.

15.13. Ancestro común más bajo (LCA)


El ancestro común más bajo (Lowest Common Ancestor (LCA)) es un
concepto dentro de la Teorı́a de grafos y Ciencias de la computación. Sea T
un árbol con raı́z y n nodos. El ancestro común más bajo entre dos nodos v y
w se define como el nodo más bajo en T que tiene a v y w como descendientes
(donde se permite a un nodo ser descendiente de él mismo).
El LCA de v y w en T es el ancestro compartido de v y w que está
localizado más lejos de la raı́z. El cómputo del ancestro común más bajo
puede ser útil, por ejemplo, como parte de un procedimiento para determinar
la distancia entre pares de nodos en un árbol: la distancia de v a w puede ser
calculada como la distancia desde la raı́z hasta v, sumada con la distancia
desde la raı́z hasta w, menos dos veces la distancia desde la raı́z hasta su
ancestro común más bajo.
En una estructura de datos árbol donde cada nodo referencia a su padre,
el ancestro común más bajo puede ser determinado de forma muy simple
encontrando la primera intersección de los caminos desde v and w hasta la
raı́z. En general, el tiempo computacional requerido por este algoritmo es
O(h) donde h es la altura del árbol (longitud del camino más largo desde una
hoja hasta la raı́z). Sin embargo, existen muchos algoritmos para procesar
árboles con los que el ancestro común más bajo puede ser encontrado de
forma más rápida.
Se puede buscar en tiempo constante por pregunta después de un prepro-
cesamiento en tiempo lineal.

460
TEORÍA DE GRAFOS

Sin preprocesamiento se puede mejorar el tiempo de cómputo del algorit-


mo ingenuo hasta O(log h) almacenando los caminos a través del árbol usando
skew-binary random access lists, premitiendo aún al árbol ser extendido en
tiempo constante.

15.13.1. Idea del algoritmo


Antes de responder a las consultas, debemos preprocesar el árbol. Ha-
cemos un recorrido DFS comenzando en la raı́z y construimos una lista que
almacena el orden de los vértices que visitamos (se agrega un vértice a la lista
cuando lo visitamos por primera vez, y después del retorno de los recorridos
DFS a sus hijos) ). Esto también se llama un recorrido de Euler del árbol.
Está claro que el tamaño de esta lista será O (N). También debemos crear un
arreglo first[0..N-1] que almacene para cada vértice i su primera aparición en
euler. Es decir, la primera posición en euler tal que euler [first[i]] = i. Tam-
bién utilizando el DFS podemos encontrar la altura de cada nodo (distancia
desde la raı́z a ella) y almacenarla en el arreglo height[0..N-1].
Entonces, ¿cómo podemos responder a las consultas utilizando el recorrido
de Euler y los dos arreglos adicionales? Supongamos que la consulta es un
par de v1 y v2. Considere los vértices que visitamos en el recorrido de Euler
entre la primera visita de v1 y la primera visita de v2. Es fácil ver que el LCA
(v1, v2) es el vértice con la altura más baja en este camino. Ya notamos que
el LCA tiene que ser parte de la ruta más corta entre v1 y v2. Claramente
también tiene que ser el vértice con la altura más pequeña. Y en la gira de
Euler, esencialmente utilizamos el camino más corto, excepto que además
visitamos todos los subárboles que encontramos en el camino. Pero todos los
vértices en estos subárboles son más bajos en el árbol que el LCA y, por
lo tanto, tienen una altura mayor. Por lo tanto, el LCA (v1, v2) se puede
determinar únicamente al encontrar el vértice con la altura más pequeña en
el recorrido de Euler entre la primera (v1) y la primera (v2).
Vamos a ilustrar esta idea. Considere la siguiente imagen y el recorrido
de Euler con las alturas correspondientes:

Vertices: 1 2 5 2 6 2 1 3 1 4 7 4 1
Heights: 1 2 3 2 3 2 1 2 1 2 3 2 1

En el recorrido que comienza en el vértice 6 y termina en 4, visitamos


los vértices [6,2,1,3,1,4]. Entre esos vértices, el vértice 1 tiene la altura más
baja, por lo tanto, LCA (6, 4) = 1.
Para responder a una consulta, solo necesitamos encontrar el vértice con
la altura más pequeña en el array euler en el rango desde el primer [v1] al

461
TEORÍA DE GRAFOS

Figura 15.11: Ancestro común más bajo

primero [v2]. Por lo tanto, el problema de LCA se reduce al problema de


RMQ (encontrar el mı́nimo en un problema de rango).
Usando Sqrt-Descomposición,
√ es posible obtener una solución respondien-
do a cada consulta en O( N ) con preprocesamiento en tiempo O(N).
Usando un Range Tree, puede responder a cada consulta en O(logN)
con preprocesamiento en tiempo O(N). A continuación una implementacion
utlizando esta estructura.

struct LCA {
v e c t o r <int> h e i g h t , e u l e r , f i r s t , s e g t r e e ;
v e c t o r <bool> v i s i t e d ;
int n ;

LCA( v e c t o r <v e c t o r <int>> &adj , int r o o t = 0 ) {


n = adj . s i z e () ;
height . r e s i z e (n) ;
f i r s t . r e s i z e (n) ;
e u l e r . r e s e r v e (n ∗ 2) ;
v i s i t e d . assign (n , false ) ;
d f s ( adj , r o o t ) ;
int m = e u l e r . s i z e ( ) ;
s e g t r e e . r e s i z e (m ∗ 4 ) ;
build (1 , 0 , m − 1) ;
}

462
TEORÍA DE GRAFOS

void d f s ( v e c t o r <v e c t o r <int>> &adj , int node , int h = 0 ) {


v i s i t e d [ node ] = true ;
h e i g h t [ node ] = h ;
f i r s t [ node ] = e u l e r . s i z e ( ) ;
e u l e r . push back ( node ) ;
f o r ( auto t o : a d j [ node ] ) {
i f ( ! v i s i t e d [ to ] ) {
d f s ( adj , to , h + 1 ) ;
e u l e r . push back ( node ) ;
}
}
}

void b u i l d ( int node , int b , int e ) {


i f ( b==e ) {
s e g t r e e [ node ] = e u l e r [ b ] ;
} else {
int mid = ( b + e ) / 2 ;
b u i l d ( node << 1 , b , mid ) ;
b u i l d ( node << 1 | 1 , mid + 1 , e ) ;
int l = s e g t r e e [ node << 1 ] , r = s e g t r e e [ node << 1 | 1 ] ;
s e g t r e e [ node ] = ( h e i g h t [ l ] < h e i g h t [ r ] ) ? l : r ;
}
}

int query ( int node , int b , int e , int L , int R) {


i f ( b > R | | e < L)
return −1;
i f ( b >= L && e <= R)
return s e g t r e e [ node ] ;
int mid = ( b + e ) >> 1 ;

int l e f t = query ( node << 1 , b , mid , L , R) ;


int r i g h t = query ( node << 1 | 1 , mid + 1 , e , L , R) ;
i f ( l e f t == −1) return r i g h t ;
i f ( r i g h t == −1) return l e f t ;
return h e i g h t [ l e f t ] < h e i g h t [ r i g h t ] ? l e f t : r i g h t ;
}

int l c a ( int u , int v ) {


int l e f t = f i r s t [ u ] , r i g h t = f i r s t [ v ] ;
if ( l e f t > right )
swap ( l e f t , r i g h t ) ;
return query ( 1 , 0 , e u l e r . s i z e ( ) − 1 , l e f t , r i g h t ) ;
}
};

463
TEORÍA DE GRAFOS

15.14. Buscar los puntos de articulación de


un grafo
Un punto de articulación o vértice de corte de un grafo G=(V, E), es un
vértice v ∈ V , tal que, al eliminar v de G, y todas las aristas incidentes en
él, se divide una componente conexa en dos o más componentes conexas. Por
ejemplo en el siguiente grafo:

Los nodos C y E son puntos de articulación. Si eliminamos el nodo C


nuestro grafo quedarı́a como se observa en el grafo de la izquierda mientras
si e elimina el nodo E serı́a como se muestra en el grafo de la derecha

En ambos casos nuestro grafo deja de ser conexo. En un grafo no dirigido,


se dice que es una arista puente si al eliminarla se desconecta el grafo G
(aumenta en uno la cantidad de componentes conexas del mismo). En nuestro
caso la arista que conecta los nodos C y E es una arista puente.
Bien veamos como dado un grafo no dirigido podemos buscar los puntos de
articulación del mismo. Una primera idea es por cada nodo del grafo ponerlo
como marcado y hacer un bien un BFS o DFS y chequear si el resto de los
nodos quedaron en la misma componentes conexa si esto ocurre entonces
el nodo seleccionado previamente no es un punto de articulación. Esta idea
tiene una complejidad de O (N2 +NE) donde N es la cantidad de nodos y
E es la cantidad de arista. Si se toma cualquier punto de articulación como
punto de partida del DFS, se forma un árbol del recorrido cuya raı́z tiene, al
menos, dos hijos.

464
TEORÍA DE GRAFOS

Esto nos conduce a plantear que sea el grafo G=(V, E) conexo y no diri-
gido. La raı́z tiene, al menos, dos hijos en árbol abarcador en profundidad es
un punto de articulación. Lo anterior es se demuestra de la siguiente manera.

Si el nodo C no tiene hijos: no es punto de articulación por definición


(el grafo resultante, tras eliminar ese vértice, es vacı́o).
Si el nodo C tiene un único hijo: sea la arista < C, B > la arista de
árbol que se forma entre el nodo C y su único hijo el nodo B. Sean los
A y D nodos cualesquiera diferentes de C . Como es conexo, entonces,
existe un camino simple entreA y D en que no pasa por C ya que si
fuera ası́, recorrerı́a dos veces y ya no serı́a simple, por tanto, el grafo
no se desconecta al eliminar y C no es punto de articulación.

Si el nodo C tiene 2 o más hijos. Sean B y E dos hijos cualesquiera


de C . Cualquier camino entre B y E tiene que pasar por C (en G no
existe otra arista que conecte a un vértice de un subárbol con uno del
otro subárbol) por lo que C es punto de articulación.

Bueno lo anterior es una buena idea de hallar un punto de articulación


aplicando un DFS pero tiene un simple detalle se asume que el se inicia el
DFS sobre un nodo que es un punto de articulación grafo, curioso no?

465
TEORÍA DE GRAFOS

Bueno vamos hacer un análisis para cuando se inicia la búsqueda sobre


un nodo que no tiene que ser un punto de articulación del grafo. Seguiremos
con la estrategia de generar un arbol de recorrido en profundidad pero ahora
a partir del nodo B del grafo original.

Donde las aristas rojas forman parte del árbol de recorrido en profundidad
mientras.
En este esquema nos podemos percatar que los nodos C y E que son
los puntos de articulación tienen, al menos, un hijo, tal que; no existe una
arista de retroceso (lı́neas negras discontinua) desde desde ese hijo, o desde
cualquiera de sus descendientes, hacia un ancestro de C o E en el árbol de
recorrido en profundidad.
Esta observación nos permite decir que sea G=(V, E) conexo y no dirigido.
Un vértice v que no sea raı́z del árbol abarcador en profundidad es punto de
articulación tiene, al menos, un hijo w tal que no existe una arista de retroceso
desde w , o desde cualquiera de sus descendientes, hacia un ancestro de v.

466
TEORÍA DE GRAFOS

Como v es punto de articulación, entonces divide al grafo conexo G en,


al menos, dos componentes conexas al quitar v , y sus aristas adyacentes, de
G. En una de estas componentes (CC1), estarán los ancestros de v en G (y
posiblemente, algunos de sus descendientes en G).

Toda arista de retroceso en la otra componente conexa (CC2) tiene que


conectar, necesariamente, a descendientes de v entre si, o con el propio v
pues si no fuera ası́, existirı́a una forma (dicha arista de retroceso) de llegar
de un vértice de una componente a otra y por tanto , v no serı́a un punto de
articulación.

Con lo visto anteriormente podemos trazarnos un algoritmo que determi-


ne los puntos de articulación en un grafo:

1. Realizar una búsqueda en profundidad (DFS) del grafo e ir numerando


los vértices a medida que se van visitando. array d[v]: almacena dicho
número, 1 ≤ v ≤ |V |. d[v]: (discover time) momento de descubrimiento
de v. La raı́z se numera con 1 La numeración que se establece a partir
del array d[] permite ordenar los vértices según el recorrido en preorden
del árbol abarcador que se obtiene tras el DFS.

2. Se establece también el array low[v] tal que: low[v] = min (d[v], d[w]),
1 ≤ v ≤ |V |, w: es cualquier vértice, alcanzable desde v, al cual se
llega bajando, cero o más niveles, por cualquier rama que salga de

467
TEORÍA DE GRAFOS

v en el árbol abarcador hasta llegar a un descendiente u del propio


v (puede suceder u=v) y luego, subir por UNA arista de retroceso ¡u,
w¿Al calcular el valor de low[v] se tienen en cuenta todos los adyacentes
(hijos, determinados por aristas árbol y ancestros determinados por
aristas de retroceso) a v.
A continuación una implementación utilizando un DFS de forma recursi-
va.

struct Node{
v e c t o r <int> a d j ;
};

Node graph [MAX] ;


bool mark [MAX] ;
int d i s c o v e r y T i m e [MAX] , low [MAX] , t i m e s ;

void d f s ( int u , int p , v e c t o r <int> & n o d e s ) {


mark [ u ] = true ;
low [ u]= d i s c o v e r y T i m e [ u]= t i m e s ++;
int c h i l d r e n = 0 ;

int nadj=graph [ u ] . a d j . s i z e ( ) ;

f o r ( int g =0;g<nadj ; g++){


int v=graph [ u ] . a d j [ g ] ;
i f ( v == p ) continue ;
i f ( mark [ v ] ) low [ u]=min ( low [ u ] , d i s c o v e r y T i m e [ v ] ) ;
else {
dfs (v , u , nodes ) ;
low [ u ] = min ( low [ u ] , low [ v ] ) ;
i f ( low [ v ] >= d i s c o v e r y T i m e [ u ] && p != −1)
n o d e s . push back ( u ) ;
++c h i l d r e n ;
}
}
i f ( p == −1 && c h i l d r e n > 1 )
n o d e s . push back ( u ) ;
}

v e c t o r <int> a r t i c u l a t i o n P o i n t s ( ) {
v e c t o r <int> i n d e x P o i n t s ;
f i l l ( mark , mark+MAX, f a l s e ) ;
t i m e s =0;
d f s (1 , −1 , i n d e x P o i n t s ) ;
return i n d e x P o i n t s ;
}

468
TEORÍA DE GRAFOS

En esta variante el método articulationPoints() devuelve un vector con


las posiciones de los Nodes en el arreglo graph que son los puntos de articu-
lación del grafo. Si analizamos esta variante la misma tiene una complejidad
de O(E + V ). Otro detalle importante esta implementación es que en el an-
teriormente mencionado método articulationPoints() se hace una llamada al
método dfs pasando como primer parámetro 1, este es el valor para el caso
que los nodos del grafo se enumeren de 1 a N para el caso que la enumeración
dea de 0 a N-1 uno el valor pasado debe ser 0.
La siguiente implementación también determina la cantidad de puntos de
articulación dentro de un grafo. Pero en este caso el dfs lo hace utilizando una
pila y el grafo es representado a través de la matriz de adyacencia. Haciendo
la adecuaciones pertinentes se puede hacer que esta implementación funcione
con la representación del grafo de la implementación anterior.

#define SZ 100
bool M[ SZ ] [ SZ ] ;
int N, c o l o u r [ SZ ] , dfsNum [ SZ ] , num , pos [ SZ ] , l e a s t A n c e s t o r [ SZ ] ,
p a r e n t [ SZ ] ;

int d f s ( int u ) {
int ans = 0 , c o n t = 0 , v ;

s t a c k <int> S ;
S . push ( u ) ;

while ( ! S . empty ( ) ) {
v=S . top ( ) ;
i f ( c o l o u r [ v]== 0 ) {
colour [ v ]=1;
dfsNum [ v]=num++;
l e a s t A n c e s t o r [ v]=num ;
}

f o r ( ; pos [ v]<N;++pos [ v ] ) {
i f (M[ v ] [ pos [ v ] ] && pos [ v ] ! = p a r e n t [ v ] ) {
i f ( c o l o u r [ pos [ v]]== 0 ) {
p a r e n t [ pos [ v ] ] = v ;
S . push ( pos [ v ] ) ;
i f ( v==u )++c o n t ;
break ;
} else
l e a s t A n c e s t o r [ v ] < ? = dfsNum [ pos [ v ] ] ;
}
}
i f ( pos [ v]==N) {
colour [ v ]=2;

469
TEORÍA DE GRAFOS

S . pop ( ) ;
i f ( v!=u ) l e a s t A n c e s t o r [ p a r e n t [ v ] ] < ? = l e a s t A n c e s t o r [ v
];
}
}

i f ( cont >1){
++ans ;
p r i n t f ( " %d\n" , u ) ;
}

f o r ( int i =0; i <N;++ i ) {


i f ( i==u ) continue ;
f o r ( int j =0; j <N; j ++)
i f (M[ i ] [ j ] && p a r e n t [ j ] == i && l e a s t A n c e s t o r [ j ] >=
dfsNum [ i ] ) {
p r i n t f ( " %d\n" , i ) ;
++ans ;
break ;
}
}
return ans ;
}

void A r t i c u l a t i o n points () {
memset ( c o l o u r , 0 , sizeof ( colour ) ) ;
memset ( pos , 0 , s i z e o f ( pos ) ) ;
memset ( parent , −1, s i z e o f ( p a r e n t ) ) ;
num = 0 ;

int t o t a l = 0 ;
f o r ( int i = 0 ; i < N; ++i )
i f ( c o l o u r [ i ] == 0 ) t o t a l += d f s ( i ) ;

p r i n t f ( "# Articulation Points : %d\n" , t o t a l ) ;


}

15.15. Grafo bipartido


Un grafo bipartito es un grafo cuyos vértices se pueden dividir en dos
conjuntos disjuntos para que cada arista conecte dos vértices de diferentes
conjuntos (es decir, no hay aristas que conecten vértices del mismo conjunto).
Estos conjuntos generalmente se llaman lados.

470
TEORÍA DE GRAFOS

15.15.1. Comprobar si el grafo es bipartido

Vamos a ver para un grafo no dirigido como comprobar si el mismo es


bipartido.
Para resolver esto vamos apoyarnos en la existencia de un teorema que
afirma que un grafo es bipartito si y solo si todos sus ciclos tienen una longitud
par. Sin embargo, en la práctica es más conveniente usar una formulación
diferente de la definición: un grafo es bipartito si y solo si es de dos colores.
Usemos una serie de búsquedas a lo ancho, comenzando por cada vértice
que aún no se ha visitado. En cada búsqueda, asigne el vértice desde el cual
comenzamos al lado 1. Cada vez que visitamos a un vecino aún no visitado de
un vértice asignado a un lado, lo asignamos al otro lado. Cuando intentamos
ir a un vecino de un vértice asignado a un lado que ya ha sido visitado,
verificamos que se haya asignado al otro lado; Si se ha asignado al mismo
lado, concluimos que el grafo no es bipartito. Una vez que hemos visitado
todos los vértices y los hemos asignado con éxito a los lados, sabemos que el
grafo es bipartito y hemos construido su partición.

int n ;
v e c t o r <v e c t o r <int>> a d j ;

v e c t o r <int> s i d e ( n , −1) ;
bool i s b i p a r t i t e = true ;
queue<int> q ;
f o r ( int s t =0; s t <n;++ s t ) {
i f ( s i d e [ s t ]==−1){
q . push ( s t ) ;
side [ st ] = 0;
while ( ! q . empty ( ) ) {
int v=q . f r o n t ( ) ;
q . pop ( ) ;
f o r ( int u : a d j [ v ] ) {
i f ( s i d e [ u]==−1){
side [u] = side [ v ] ˆ 1
q . push ( u ) ;
} else {
i s b i p a r t i t e &= s i d e [ u ] != s i d e [ v ] ;
}
}
}
}
}

c o u t << ( i s b i p a r t i t e ? "YES" : "NO" ) << e n d l ;

471
TEORÍA DE GRAFOS

472
TEORÍA DE GRAFOS

15.15.2. Máximo pareo de un grafo bipartido


15.15.3. Edge-colored bipartite graph
15.15.4. Minimum weight maximum matching of bi-
partite graph

15.16. Buscar los puentes de un grafo


15.17. K enésimo camino más corto
15.18. Dinic’s Algorithm
15.19. Edmonds-Karp Algorithm
15.20. Ford-Fulkerson Algorithm
15.21. Heavy-Light Decomposition
15.22. Tarjan’s SCC Algorithm
15.23. Minimum diameter spanning tree (Cuninghame-
Green)
15.24. Minimum spanning directed tree (Chu-
Liu - Edmond)
15.25. Minimum Steiner tree (Dreyfus-Wagner)
15.26. Maximum flow (Goldberg-Tarjan)
15.27. Minimum cost flow (Primal-Dual)
15.28. Thurs Gomory-Hu
15.29. Undirected graph a minimum cut across
(Nagamochi-Ibaraki
473 - Stoer-Wagner)
15.30. Maximum matching (Edmonds)
15.31. Maximum matching of bipartite graph
TEORÍA DE GRAFOS

de conocer que elementos de ella son parte del borde exterior y cuanto aporta
este al perı́metro.

3005 - Decorating the Pastures Pintado de grafo con un DFS teniendo


en cuenta que el grafo puede tener varias componentes conexas y voy tomar
de cada una de ellas la cantidad de la letra que más se repite y las voy
acumulando en una variable que imprimo al final, en caso de que un DFS no
me permita pintar como explica el problema se retorna -1.

2713 - Poisonous Gas Un simple BFS comenzando por el nodo uno cuyo
costo es uno a los vecinos a este es dos y ası́ sucesivamente tener cuidado
porque puede existir nodos que no estén conectado con la componente conexa
a la que está asociada el nodo #1 para estos nodos el valor es -1.

2943 - Find Phones Aplicar un BFS comenzando por aquellos estudiantes


(nodos) de los cuales se conocen sus respectivos números de teléfono hacia
aquellos que son vecinos propagando el número de teléfono. Y finalmente se
imprime para cada estudiante el número de teléfono una vez concluido todos
los BFS.

1611 - The Cats and the Mouse Determinar la cantidad mı́nima de


pasos que necesita el ratón a cada casilla que representa el borde del tablero
con BFS, de similar manera hacer los mismo para los gatos y tener los resul-
tados en dos matrices [m,n] una para el costo mı́nimo del ratón y otra para
costo mı́nimo de los gatos y después chequear que no exista casilla una celda
del borde que el costo mı́nimo del ratón que el de los gatos en caso de existir
el ratón se puede escapar, sino es capturado siempre por los gatos.

1040 - Pick up Sticks Se resuelve aplicando un ordenamiento topológico


al grafo dirigido dado.

2239 - Turquino Peak Se resuelve aplicando el algoritmo de los k enési-


mos caminos más cortos entre dos vértices.

3462 - Precipitous Promotion La solución radica en contar la cantidad


de componentes conexas que del grafo que se forma siendo los estudiantes
los nodos y las relación de seguimiento entre estos las aristas, el grafo que
se forma es dirigido, haremos un DFS para cada estudiante que no sigue a
nadie, y luego haremos DFS para aquellos no visitados con la diferencia que

474
TEORÍA DE GRAFOS

en este DFS visitaremos de cada estudiante al estudiante que él sigue (sino
no está visitado) y luego a los estudiantes que lo siguen.

3022 - Gopher Family La solución del problema es modelar la situación


como un grafo bipartido donde las ardillas son un conjunto de vértices mien-
tras el otro conjunto de vértices son los huecos y la arista entre un nodo del
conjunto de las ardillas y un nodo del conjunto de los huecos va existir siem-
pre que el tiempo que demora esa ardilla para llegar a ese hueco es menor que
el S que dan. Modelando de esta manera el problema es evidente que parte
de la solución es utilizar el algoritmo de máximo pareo en un grafo bipartido.
Y finalmente la solución es la cantidad de ardillas menos el máximo pareo
del grafo bipartido planteado.

3715 - Network Loops La solución del problema se modela realizando


un árbol de expansión mı́nima sobre el grafo dado donde los switch son los
nodos y las conexiones entre ellos las aristas realizando un Kruskal con la
variación de seleccionar las aristas no por la menor peso sino por la mayor
peso, si se puede conformar un árbol de expansión mı́nima la solución es la
arista de menor pesos de las escogidas, sino la solución será IMPOSIBLE.

1137 - ¿Es un árbol? El ejercicio se reduce a dada una colección indeter-


minadas de pares (A, B) saber si se pude forma un árbol o no sabiendo que
el par (A, B) significa que A y B son nodos unidos por una arista direccional
desde A a B. Vamos inicialmente asumir que es un árbol y por tanto a medida
que leamos los pares se mantendrá tal afirmación o se hará falsa. Lo primero
que tenemos que verificar a medidas que sea lea los pares es que no exista un
nodo con dos aristas que conduzcan a él, esto se puede controlar con un mapa
de <int,int> donde el primer int es el id del nodo y el segundo la cantidad de
aristas incidentes en el cada vez leamos un par se debe plantear soons[B]++
y si después de esto si soons[B]>1 entonces ya no es un árbol automática-
mente y solo debemos leer las restantes aristas del caso. Lo otro que se debe
controlar es la cantidad de raı́ces las cuales deben ser una sola si es un árbol
para esto tendremos un set de <int> llamado root donde insertaremos todos
los A que cumpla la siguiente condición soons[A]==0 significa que no es hijo
de nadie hasta ese momento de la lectura y en dicho set eliminaremos todos
los B leı́dos estén o no. Lo otro que debemos llevar la cuenta es la cantidad
de aristas leı́das. Una vez leı́da todas las arista si la cantidad de raı́ces es
mayor de 1 entonces no es árbol o si la cantidad de raı́ces es cero pero la
cantidad de aristas es mayor que uno entonces tampoco es un árbol porque
no existe por definición árbol sin raı́z y con arista, en todo caso sin raı́z y sin

475
TEORÍA DE GRAFOS

arista que en un árbol nulo o vacio.

3092 - Research Project for Competitions Ejemplo clásico de asigna-


ción de tareas el cual plantea la existencia de n trabajadores para realizar
n tareas donde cada trabajador cobra una cantidad determinada por reali-
zar determinada tarea, el problema consiste en determinar el menor salario
a pagar a los trabajadores de forma que cada uno realiza una tarea y cada
tarea es realizada por un único trabajador (Casi siempre se trata de minimi-
zar en este caso se busca primero maximizar y luego minimizar). Se resuelve
aplicando el algoritmo húngaro o algoritmo Kuhn-Munkres como también es
conocido.

3190 - Flowery Trails El problema consiste en hallar la suma de las


longitudes de todas las aristas que pertenecen a al menos un camino mı́nimo
entre el nodo de salida y el de llegada. Una arista (u; v) de peso w pertenece
a un camino mı́nimo entre el nodo S y el T si:
dist(S; u) + w + dist(v; T ) = dist(S; T )
Donde dist(x; y) es la distancia mı́nima entre el nodo x y el nodo y.
Como el grafo es no dirigido, tenemos además que dist(v; T) = dist(T; v),
de modo que podemos usar la fórmula anterior para controlar si una arista
pertenece a un camino mı́nimo de S a T en O(1), siempre y cuando hayamos
precalculado todos los posibles valores de dist(S; x) y dist(T; x). Podemos
hacer esto utilizando dos veces el algoritmo de Dijkstra, empezando una vez
desde S y otra desde T. La solución resulta entonces O(N logN + E), con N
la cantidad de nodos y E la cantidad de aristas del grafo.

1094 - Virtual Friends Para solucionar el problema se necesita del cono-


cimiento de la estructura Disjoint Set Union, para cada par (A,B) donde A
y B son nombre se debe imprimir cuanto amigos existen entre ellos y la es-
tructura anteriormente se adapta perfectamente, ahora si has trabajado con
la estructura sabes que la estructura trabaja con números y no con string es
por eso que se hace necesario usar un mapa donde la clave es el nombre y el
valor es un int a medida que se va leyendo los nombres se le da id secuenciales
empezando desde cero y de esta manera se lee A y B se busca los idA y idB
y con esos números invoco la función unionFriend el cual tiene la siguiente
adecuaciones :

void u n i o n F r i e n d ( int i , int j )


{
int i i = lookup ( i ) ;

476
TEORÍA DE GRAFOS

int j j = lookup ( j ) ;
i f ( i i == j j )
return ;
if ( ii < jj )
{
relations [ jj ] = ii ;
cantFriends [ i i ] += c a n t F r i e n d s [ j j ] ;
}
else
{
relations [ ii ] = jj ;
cantFriends [ j j ] += c a n t F r i e n d s [ i i ] ;
}
}

Donde relations es el arreglo de la estructura mientras cantFriends es un


arreglo donde en la posición X guardo la cantidad de amigos que tiene la
persona cuyo Id sea X, inicialmente cada posición de ese arreglo el valor será
1. El resto de los métodos de la estructura Disjoint Set Union se mantienen
igual. Entonces se lee el par de nombre se busca los id de los nombre se
invoca el método unionFriend y luego imprimo cantFriends en la posición de
cualquiera de los dos id.

2738 - Coco-Bits and the Spies Network Una vez leı́do el problema
no cabe duda que nos estan pidiendo el árbol de expasión mı́nima del grafo
donde los nodos son los puntos de los espı́as y las aristas las conexiones entre
los espı́as donde el peso de las aristas es la distancia euclidiana entre los dos
puntos conocidos las coordenadas de cada uno. El algoritmo ideal para esto
es el Kruskal pero hay un detalle. El problema te obliga que determinados
espı́as esten conectados de la forma punto a punto como se dice en el argot
informático, eso significa que mi árbol de expasión minı́ma debe incluir dichas
aristas aunque inicialemnte las puede bien no considerar. Es por eso que se
debe realizar determinada adecuaciones al Kruskal. Lo primero es tomar
para K1 y K2 las distancia entre ellos y acumularla para la respuesta y si no
están en la misma componente conexa hasta ese momento, ponerlos (Usar la
estructura Disjoint Set Union que usa el Kruskal), luego con el resto de las
aristas realizar el Kruskal hasta que la cantidad de componentes conexa del
árbol de expasión mı́nima sea una. Devolver la cantidad de longitud deseada.

2571 - Crazy Frog Para asegurar que el salto de la Rana Loca sea lo
mı́nimo posible debemos construir un árbol de expasión mı́nimo del grafo
donde los nodos son las piedras y las aristas las conexiones entre las piedras
donde el peso de las aristas es la distancia euclidiana entre los dos puntos

477
TEORÍA DE GRAFOS

conocidos las coordenadas de cada piedra. Una vez construido el árbol de


expansión minı́ma, la respuesta para A y B es la arista de mayor peso de
todas las que conforman el camino entre A y B. Esto se puede determinar
aplicando un BFS. Ojo como dato para la optimización se debe tener en
cuenta que la respuesta para el par(A,B) es la misma que para el par(B,A),
aquı́ vamos a introducir algo de dinámica para mejor el tiempo de ejecucción
con una matriz donde el cost[i][j] voy almacenar la respuesta para el camino
desde el nodo i hasta el nodo j de igual forma cost[j][i]. Inicialmente esta
matriz cada celda su valor será ∞ excepto donde i==j donde es 0, cuando se
haga el Kruskal la matriz se debe actualizar para cada arista que conforma el
árbol de expansión minı́mo. Entonces para cada par(A,B) si cost[A][B]==∞
realizo el BFS para determinar y actualizo cost[A][B] y cost[B][A], detalle en
el BFS si para ir de A a B paso por C cuando llegue a C acabo de calcular de
paso la forma optima de llegar de A a C asi que deberı́a actualizar cost[A][C]
y cost[C][A] por si luego me hace falta.

3709 - Multistory Labyrinth Un ejemplo de los problemas sobre tablero


que su solución recae en la utilización de un BFS con la única diferencia que
este tablero no tiene dos dimensiones como es constumbre, sino tres.

3256 - Basic Work with Alphabets A pesar de que esta clasificado


como greedy. La solución que le determine fue desde la teorı́a de grafos.
Tomemos cada sı́mbolo del supuesto alfabeto que nos dan como un nodo del
grafo el cual tendrá un lazo con el mismo y con cada nodo restante tendrá
una arista desde el hacia los demas nodos y desde cada uno de los nodos hacia
el. Una vez hecho el diagrama tendremos una grafo dirigido , completo y con
lazo. Ahora el problema nos pide la mayor secuencia de simbolos de forma
que no exista un par de simbolo concecutivos repetidos, llevada esta idea al
grafo serı́a como describir el máximo camino sobre el grafo dado sin repetir
una arista en el camino. Exacto lo que estamos determinando el maximo de
aristas del camino euleriano sobre el grafo descripto, la cual coindice con la
cantidad de aristas del grafo. Por lo que para solucionar el problema solo
basta saber para un grafo de N nodos donde cada nodo tiene un lazo con el
mismo y tiene dos aristas una entrante y otra saliente con cada uno de los
restantes nodos del grafo la cantidad de aristas + 1 y esa será la solución del
problema.

2081 - Saving Money Para responder cada query del problema basta con
realizar previamente un floyd sobre el grafo dado. Tenga en cuenta que puede
existir multiples aristas entre dos nodos.

478
TEORÍA DE GRAFOS

2547 - Walking Among Mountains II El ejercicio se resuelve aplicando


un bfs desde la celda inicial. La unica modificación es la cola del bfs que en
vez de usar una clásica, usar una cola de prioridad en la cual la prioridad
es el costo de llegar a una celda determinada del mapa. Y siempre se debe
volver a insertar la celda en la cola siempre que se llegue a ella con un costo
menor que se tenı́a hasta ese momento.

3008 - Flip Five Si modelamos el problema como un grafo donde de los


nodos del mismo son cada una de las configuraciones posibles del tablero,
la aristas en entre dichos nodos va ser direccional y tiene el significado que
de la configuración k se puede generar la configuración j y no de de j se
pude generar k. Note que cada nodo/configuración genera 9 nodos con los
cuales esta conectado. Una vez hecha esta modelación y sabiendo que todas
las aristas tienen el mismo peso(1) es evidente que aplicando un BFS sobre
dicho grafo llegamos a la solución.

1451 - A Knights Tale Si representamos graficamente la situación deta-


llada en el problema nos podemos percartar que podemos construir un grafo
donde tenemos dos conjuntos de vertices(posiciones de los caballos, posicio-
nes hacia los cuales se quiere llevar los caballos) los cuales los vértices de un
conjunto no se relacionan entre ellos pero si con los vértices del otro conjunto.
Con la anterior dicho, es claro que modelamos un grafo bipartido del cual
debemos hacer un pareo con el menor costo posible donde el costo entre dos
nodos es la cantidad de pasos mı́nimo que debe hacer un caballo de ajedrez
(no es necesario hacer bfs, existe un algoritmo con complejidad O(1) que
lo calcula) para ir de una posición a otra. Si se determina el maximo grafo
bipartido con el menor peso, solo debemos sumar las aristas de dicho grafo y
será la solución .... eso supongo porque realmente no lo resolvı́ asi. Este fue
el primer ánalisis que hice y no le ejecute por falta del algoritmo en sı́ aunque
creo que el ánalisis si es correcto. Mi segunda modelación del problema fue
de la siguiente: Supongamos que contamos con n trabajadores (posiciones
iniciales de los caballos) disponibles para realizar n tareas (posiciones finales
hacia donde se quiere llevar los caballos), con la restricción de un trabajador
solo puede realizar una tarea (a una posición final solo puede ir un caballo )
queremos realizar todas la tareas con el menor tiempo (llevar los caballos a
las posiciones finales con la menor cantidad de movimientos), si lo vemos ası́
estamos frente a un ejemplo clásico de asignación de tareas el cual plantea
la existencia de n trabajadores para realizar n tareas donde cada trabajador
cobra una cantidad determinada por realizar determinada tarea, el problema
consiste en determinar el menor salario a pagar a los trabajadores de forma

479
TEORÍA DE GRAFOS

que cada uno realiza una tarea y cada tarea es realizada por un único traba-
jador. Se resuelve aplicando el algoritmo húngaro o algoritmo Kuhn-Munkres
como también es conocido.

3758 - Horseshoes Para la solución de este problema podemos abordarlo


con DFS recursivo que tiene varias modificaciones para determinar la mayor
secuencia de parentisis balaceando. Tener en cuenta que la secuencia que se
busca tiene sus caracteristicas como son :

La longitud de la secuencia siempre va ser par.

La cantidad de parentisis izquierdo ’(’ va ser igual a la cantidad de


parentisis derecho ’)’.

En la secuencia del camino no puede existir un parentisis izquierdo ’(’


posterior a un parentisis derecho ’)’.

Teniendo en cuenta estos aspectos con la codificación de un DFS recur-


sivo se puede determinar la solución del problema. Para llevar los tipos de
parentesis recorrido en la secuencia que genera el dfs se puede utilizar una
pila.

2501 - Kastenlauf Una vez leı́do el problema es evidente que la situa-


ción se puede modelar a traves de una grafo donde los nodos son la casa,
Bergkirchweih y las tiendas donde se puede comprar cervezas. Mientras las
aristas entre los nodos van existir siempre y cuando la distancia Manhattan
entre los dos nodos sea menor igual a 1000(distancia máxima que pueden
caminar antes que se le acabe toda la cerveza). Una vez construido el grafo
con esas restricciones solo resta realizar algún algoritmo de recorrido sobre
el grafo (BFS o DFS) y si al final de la ejecucción del algoritmo seleccionado
el nodo que representa al Bergkirchweih esta marcado o visitado la solución
es “happy”en caso contrario “sad”.

3816 - WiFi Zones La solución del problema radica en aplicar el algoritmo


Kruskal y devolver la distancia del árbol de expansión mı́nima que genera
el algoritmo. En este caso los nodos son los puntos mientras las distancia
euclidiana entre ellos será el peso de las aristas que conectan a los nodos.

3813 - Traveling in the City La solución del problema radica en realizar


dos dfs uno teniendo como inicio el nodo A y el otro como inicio el nodo B.
La respuesta será 1 si se visita el nodo B cuando se hace el dfs comenzando

480
TEORÍA DE GRAFOS

por el nodo A y se visita el nodo A cuando se hace el dfs comenzado por el


nodo B. En cualquier otra variante será 0.

2559 - Treasure Island El problema nos pide hallar el máximo de tiempo


que se puede demorar en la celda (1,1) de forma se pueda aún ir a la celda
(R,C) pasando por las celdas en un tiempo menor al valor de la celda que
se visita en el recorrido. La solución radica en realizar un bfs desde la celda
(1,1) hasta la celda (R,C) teniendo un tiempo adicional (X) al comienzo.
Para buscar de forma eficiente en el rango de 0 a celda[1][1]-1. Para buscar
lo hacemos con búsqueda binaria. El procedimiento seria se toma un tiempo
t=(celda[1][1]-1+0)/2 se realiza el bfs con tiempo inicial t. Si se logra el
próximo rango serı́a t+1 a celda[1][1]-1 en caso contrario el rango analizar
serı́a de 0 a t-1. La respuesta al problema serı́a el rango mı́nimo de la búsqueda
binaria una vez concluida menos uno.

3651 - Center of the City Para solucionar el problema debemos dividir


la solución en tres partes. La primera parte nos dedicaremos a conformar el
grafo según los datos de entrada y sobre este aplicaremos el algoritmo Floyd-
Warshall para calcular la distancia mı́nima entre cualquier par de nodos.
En la segunda parte de la solución buscaremos aquel o aquellos nodos cuya
distancia a sus vecinos mas lejanos sea mı́nima, estos serán los centros de la
ciudad que se hablan en el problema. Luego en la última parte de la solución
buscaremos que cuales de las dos casas esta a menor distancia de uno de los
centros de la ciudad hallados para dar una respuesta.

3928 - The Deep Dark Web La idea para solucionar el primer subpro-
blema es usar minimum spanning tree como kruskal, se ordenan las aristas
por su peso de forma descendente y para dar solución del primer subproble-
ma nos quedamos con el peso de la arista que hace que los dos nodos (S y
T) estén en la misma componente, y para el segundo subproblema se debe
hallar el camino mı́nimo entre S y T usando dijkstra donde el costo de una
arista es 0 si su valor original es mayor que el valor que se desea obtener y si
no es la diferencia de cuanto debe aumentar para alcanzar el valor deseado.

3464 - The Princess is in Another Castle Una vez leı́do el problema


debemos darnos cuenta que la cantidad de castillos y la cantidad de caminos
entre ellos, las restricciones planteadas el grafo que se construye es un árbol
no dirigido. Por los que la solución se reduce a encontrar dentro de un árbol
los dos nodos mas lejanos, en otras palabras hallar el diámetro del árbol, una
vez construido el árbol con los datos de entrada solo queda hallar el diámetro

481
TEORÍA DE GRAFOS

del árbol, incrementarlo en uno( vamos asumir que el peso de cada arista es
uno, por tanto un diámetro de 4 significa que es un camino con cinco nodos,
que en nuestro problema son los castillos ) y esa será la respuesta

1107 - Magic Squares Para solucionar el problema se puede utilizar dos


vı́as pero en ambas se necesita elementos comunes como son:

Funciones que reciban por parámetro una configuración y devuelvan


una nueva configuración aplicando los cambios explicados en el proble-
ma. Es decir una función para generar la configuración ’A’ partir de una
inicial, de similar manera debe existir para generar las configuraciones
’B’ y ’C’.

Una estructura que permita llevar el control de la configuraciones ge-


neradas para no repetir ninguna (En el peor de los casos la máxima
cantidad de configuraciones generadas sera factorial de 8).

La primera variante es construir el grafo dirigido y ponderado (todas las


aristas con peso 1) con todas las posibles configuraciones como nodos del grafo
y la arista va existir entre dos configuraciones si una configuración se puede
generar la otra. Luego de construido el grafo aplicar el algoritmo de dijkstra
teniendo como nodo inicial aquel representa la configuración inicial descrita
en el problema. Luego solo tenemos que buscar el nodo que representa la
configuración entrada por datos y ver la distancia hasta ese nodo que dio la
ejecución del dijsktra. Para determinar la secuencia solo basta con usar la
propia información adicional que genera el dijsktra.
La segunda variante parte que las configuraciones construyen grafo donde
todas las aristas tiene igual peso se puede aplicar un bfs para determinar
la mı́nima cantidad de configuraciones a realizar para obtener la entrada
por datos a partir una configuración inicial. Para determinar la secuencia
se tendrá que ’exportar’ la filosofı́a del dijsktra en cuanto a la información
adicional para luego generar la secuencia.

1660 - Knights of Ni Hacer un BFS desde la posición de Besie hacia


todas las fresas y calcular el costo minimo para llegar a cada una sin pasar
por la base de los caballeros. Luego realizar otro BFS desde la posición de la
base de los caballeros hacia todas la fresas y calcular el costo minimo para
llegar a cada. Luego buscar la fresa cuya expresión:
costo(Besie, f resai ) + costo(Base los caballeros, f resai )
Devuelva el menor valor y esa será la respuesta.

482
TEORÍA DE GRAFOS

2668 - Tom and Jerry II El problema nos plantea que dado un registro
que almacena un valor A se desea saber la menor secuencia de operaciones
aritmética (suma, resta, división entera y multiplicación) para alcanzar al-
macenar dicho registro un valor B en caso que no se puede la respuesta es
’ ?’. Para solucionar el problema vamos a realizar BFS partiendo del valor A
para llegar a B realizando suma, multiplicación y división entera. La resta
no la utilizamos porque A-A me darı́a 0 y caerı́a un valor del cual no saldrı́a
con alguna operación de las permitidas. Llevar un set o map de los números
analizados para no repetir análisis con el mismo número. En caso de existir
varias soluciones validas vamos a tomar aquella que tenga la menor secuencia
de operaciones y que lexicograficamente sea menor que las demás.

1220 - Come and Go Una vez leı́do el problema nos podemos percartar
que nos pide deteminar dado un conjunto de N intersecciones si es posible ir
entre cualquiera par de intersecciones en un sentido y en el otro. Para esto
las aristas que conecta a la intersecciones son de dos tipos, de un solo sentido
o doble sentido. Si construimos el grafo con las restricciones planteadas y
realizamos un recorrido a lo ancho sobre el grafo y todos los nodos del grafo
son visitados en una sola componente conexa. La respuesta es 1 sino es 0.

3955 - Water Among Cubes Una vez leı́do el problema nos podemos
percatar que nos piden determinar el maximo volumen de agua que se puede
contener dentro de la malla de cuadriculas donde de las cuadriculas tienen
una altura determinada. Para empezar vamos asumir que cada cuadricula de
la malla puede ser de dos tipos: MURO y CONTENEDOR. En el primer tipo
estarán aquellas cuadriculas que conforman los muros que contendrán el agua
que almacenarán , mientras en el segundo tipo están aquellas que se va verte
agua sobre ellas. Al principio vamos asumir que todas las cuadriculas de la
malla son de tipo CONTENEDOR. Luego cada cuadricula del borde de la
malla vamos a asumir que es de tipo MURO porque a priori las cuadriculas del
borde de la malla van ser parte del muro contenedor. Luego cada cuadricula
interna (no borde de la malla) vamos analizarla porque si tiene una altura
mayor o igual a la cuadriculas del borde entonces en esa cuadricula no se
puede verter agua en ella porque cuando en esa cuadricula cuando se contega
agua se habrá botado por un o alguno de los bordes.
Entonces una vez visto esto, el analisis de las cuadriculas internas debe
ser siguiente. Las alamcenos en una colección y las ordenos por sus alturas
de forma descendente. Para cada cuadricula se realiza un BFS partiendo de
la posición de dicha cuadricula y puede pasar otra posición siempre y cuando
la altura de la cuadricula hacia donde se vaya desplazar sea menor e igual

483
TEORÍA DE GRAFOS

a la altura de la cuadricula inicial. En caso que la próxima cuadricula hacia


donde vaya a moverse su altura sea mayor que la altura cuadricula inicial
entonces puede ocurrir dos cosas:
Que dicha casilla sea muro por tanto la maxima cantidad de agua que
puede verter en la casilla inicial sea igual a la diferencia de altura entre
las dos casillas.
Que dicha casilla sea un contenedor ya analizada por tanto sabe cual de
los muros que la contiene tiene la menor altura y esta altura tambien
contendrá a la casilla analizada por tanto la cantidad agua sera la
diferencia entre la alturas de la casilla analizada y la altura del muro
que contiene a la próxima cuadricula.
Luego hecho este análisis por cada cuadricula interna solo debemos re-
correr cada cuadricula de la malla y si esta es de tipo CONTENEDOR la
cantidad de agua que le aporta a la solución es igual a la diferencia entre la
altura menor de los muros que lo contienen y la altura de la cuadricula en si.
A continuación les dejo los principales elementos mencionados:

enum TypeField {WALK,CONTAINER} ;

struct F i e l d
{
TypeField m type ;
int m row ;
int m column ;
ULL m heigth ;
ULL m minHeigthWalk ;

F i e l d ( int r =0, int c =0, ULL h e i g t h =0)


{
m column= c ;
m row= r ;
m heigth= h e i g t h ;
m type= CONTAINER;
m minHeigthWalk=LIMIT ;
}

bool operator <( F i e l d f i e l d ) const


{
return ( m heigth> f i e l d . m heigth ) ;
}
};

....

484
TEORÍA DE GRAFOS

F i e l d matrix [MAX] [MAX] ;

....

bool isWalk ( F i e l d f i e l d )
{
bool i s C o n t a i n e r=true ;

C e l l at , next ;
queue<C e l l > v i s i t ;
ULL heigthMin= f i e l d . m heigth ;
v i s i t . push ( C e l l ( f i e l d . m row , f i e l d . m column ) ) ;
memset ( mark , f a l s e , s i z e o f ( mark ) ) ;

mark [ column ∗ ( f i e l d . m row−1)+ f i e l d . m column]= true ;

while ( ! v i s i t . empty ( ) && i s C o n t a i n e r )


{
a t= v i s i t . f r o n t ( ) ;
v i s i t . pop ( ) ;
i f ( isBorder ( at ) )
i s C o n t a i n e r=f a l s e ;
else
{
f o r ( int i =0; i <4; i ++)
{
next=C e l l ( a t . m row+mov r [ i ] , a t . m column+mov c [ i ] ) ;
i f ( i s F i e l d V a l i d ( next ) )
{
int i n d e x=column ∗ ( next . m row−1)+next . m column ;
i f ( ! mark [ i n d e x ] &&
matrix [ next . m row ] [ next . m column ] . m heigth <= heigthMin )
{
mark [ i n d e x ]= true ;
v i s i t . push ( next ) ;
}
e l s e i f ( ! mark [ i n d e x ] &&
matrix [ next . m row ] [ next . m column ] . m heigth >
heigthMin )
{
i f ( matrix [ next . m row ] [ next . m column ] . m type==WALK)
{
matrix [ f i e l d . m row ] [ f i e l d . m column ] . m minHeigthWalk=
min ( matrix [ f i e l d . m row ] [ f i e l d . m column ] .
m minHeigthWalk ,
matrix [ next . m row ] [ next . m column ] . m heigth ) ;
}
e l s e i f ( matrix [ next . m row ] [ next . m column ] . m type==
CONTAINER)

485
TEORÍA DE GRAFOS

{
matrix [ f i e l d . m row ] [ f i e l d . m column ] . m minHeigthWalk=
min ( matrix [ f i e l d . m row ] [ f i e l d . m column ] .
m minHeigthWalk ,
matrix [ next . m row ] [ next . m column ] . m minHeigthWalk ) ;
}
}
}
}

}
}

return ( ! i s C o n t a i n e r ) ;
}

.....

void s o l u t i o n ( )
{
ULL maxVolumen=0;

REP1 N( i , row )
{
REP1 N( j , column )
i f ( matrix [ i ] [ j ] . m type==CONTAINER)
maxVolumen+=
( matrix [ i ] [ j ] . m minHeigthWalk−matrix [ i ] [ j ] . m heigth ) ;
}
cout<<maxVolumen<<ENDL;
}

2942 - Employees Membership Se nos plantea que dentro de una or-


ganización los empleados se agrupan en grupos de trabajos y a su vez estos
grupos de trabajos se agrupan en otros grupos. Los empleados pueden perte-
necer a varios grupos. Una vez conocido esto se nos pide determinar cuantos
empleados tiene el primer grupo de la entrada de datos. Si modelamos la
situación como un grafo dirigido nos podemos percatar que solución es la
cantidad de nodos que no tienen aristas hacia otros nodos del grafo , siempre
y cuando exista un camino entre el nodo que representa el primer grupo y
nodo que no tiene aristas. Para solucionar vamos a dividir la solución en
varias partes:

Hacer un mecanismo de parseado para la entrada de datos.

486
TEORÍA DE GRAFOS

Crear un mecanismo que a cada nombre bien sea de un grupo o em-


pleado le asigne un número único como identificador. Otra variante es
trabajar con un mapa.

Luego construir un grafo dirigido donde va existir una arista dirigida


del NodoA al NodoB si el nombre asociado al nodoB integra el grupo
cuyo nombre es igual al asociado al NodoA.

Realizar un BFS comenzando por el nodo que repersenta el nombre del


primer grupo de la entrada del caso y contar si repetir aquellos nodos
que notengan aristas hacia otros nodos del grago.

La estructura donde se almacena las aristas asociadas al nodo puede


ser un un set para evitar aristas duplicadas.

4088 - How Long is the Path Una vez leı́do el problema podemos per-
catarnos que nos pide determinar el camino mas largo donde todos los nodos
que se visite en en ese camino sean nodos de color rojo. Si analizamos y
representamos el graficamente el caso de ejemplo que nos pones el problema
se nos reduce en encontrar aquel subárbol solo conformado por nodos rojos
cuyo dı́ametro sea mayor y devolver dicho diametro incrementado en uno y
asumiendo que el peso de cada arista es 1. Ahora veamos algunos detalles
que pueden optimizar nuestra solución.

A la hora conformar el árbol como si fuera un grafo solo debemos tener


aquellas aristas cuyos nodos son rojos ambos. Porque las aristas que
tienen al menos un nodo negro nunca va ser parte de aquel camino
máximo solución del problema.

A la hora de buscar la solución solo debemos aplicar el algoritmo del


dı́ametro del árbol sobre aquellos nodos rojos sobre los cuales no hayan
aplicado dos dfs. Si analizan el algoritmo del dı́ametro del árbol dete-
nidamente se darán cuenta que el mismo cuando termina ejecutó dos
dfs sobre todos los nodos conectados en esa misma componente conexa
donde va hallar el dı́ametro. Por lo que se puede llevar para cada nodo
en cuantos dfs se ha visto envuelto y eso nos evitarı́a realizar el algo-
ritmo sobre un nodo que ya se determino el diametro del árbol al que
el pertenece previamente.

3949 - Jumping of the Knight Una vez leı́do el problema nos podemos
percatar que nos piden determinar si dada dos posiciones en un tablero de
ajedrez se puede ir de una otra casilla utilizando una pieza de tipo caballo

487
TEORÍA DE GRAFOS

pero con la restricción de movimiento que la pieza solo puede ir a una celda
cuya fila sea mayor que la fila de la posición actual. Para resolver este pro-
blema solo debemos aplicar un BFS tomando como posición inicial la celda
inicial y luego de terminada la ejecucción del BFS ver si la celda donde debe
terminar la pieza fue visitada. Se debe tener en cuenta a la hora de generar
las celdas vecinas la restricción de movimiento de la pieza.

4111 - Training the Deivi Una vez leı́do el problema nos podemos per-
catar que se puede modelar a través de una grafo donde los laboratorios
serı́an los nodos y los pasillos las aristas. Ahora una vez hecha esta modela-
ción debemos ver el problema radica en encontrar el camino mas corto o con
el mı́nimo costo desde el labortorio 1 hasta el laboratorio N. Aplicando un
Dijkstra desde el laboratorio 1 hacia los demás laboratorios. Una vez con-
cluido la ejecucción del algoritmo solo debemos imprimir la distancia mı́nima
calculada por el algoritmo para el nodo N.

2800 - Find a Path Dado un conjunto de puntos y un grupo de segmentos


comprendidos entre estos saber si se puede ir de un punto a otro si se conoce
que para ir de un punto a otro punto debe existir un secuencia de segmentos
que lo conecten entre sı́. Si dos puntos están en la misma posición de espacio
los dos puntos están conectados. Si dos segmentos se intersectan entonces
entre los cuatros puntos que integran los dos segmentos existe una conexión
entre todos ellos. Una visto estos detalles podemos ver que la solución al
problema radica en hallar las componentes conexa de un grafo que se va
construir tomando a los puntos como nodos del grafo y las aristas entre dos
nodos va existir si los puntos asociados a esos nodos entre ellos conforman
un segmento, o forman parte de segmentos que se intersectan u ocupan la
misma posición en el plano. Luego de hallada las componentes conexa del
grafo la respuesta para los puntosi y puntosj será YES si pertenecen a la
misma componente conexa y NO en caso contrario.

3440 - Tea-Serving Robot Dado un robot dentro de un laberinto su


posición inicial y una posición final se quiere saber la cantidad mı́nima de
pasos para ir de la posición inicial a la final de ser posible. Se sabe que el
laberinto tiene obstáculos que el robot no puede superar. Por lo descrito el
robot cuando trata de acceder a una posición ocupada por un obstáculo gira
90 grados en sentido contrario a las manecillas del reloj. El robot siempre
comienza caminando hacia el norte. Para resolver el problema simular el
proceso como un BFS sobre tablero. Controlar que a una celda solo se puede
entrar cuatro veces una vez por cada punto cardinal.

488
TEORÍA DE GRAFOS

1942 - Acquapia El problema nos explica que dado un grupo de ciudades


que se comunican a través de rı́os que se bifurcan en varios afluentes. Se
necesita determinar para cada par de ciudades si están en una misma afluente
de rı́o o no se comunican o tienen una ciudad donde se debe cambiar de
afluente para poder ir de una ciudad a otra.
Para solucionar el problema vamos aplicar un LCA sobre el árbol que
se forma cuando modelamos las ciudades y los rı́os. Adicionalmente vamos
adicionar al árbol el nodo 0 el cual lo vamos a poner como padre directo de
todas las ciudades de donde comienza a fluir los rı́os. Esto nos harı́a hacer
un LCA sobre un árbol de N + 1 nodos.
Para cada consulta de un par de vértice a nodos u, v del LCA la respuesta
serı́a:

Si lca(u, v) == 0 → −1 No existe comunicación entre las dos ciudades.

Si lca(u, v) == u o lca(u, v) == v → −1 Las ciudades están en una


misma trayectoria de las afluentes.

Si lca(u, v) 6= 0 y lca(u, v) 6= u y lca(u, v) 6= v y lca(u, v) = z → z es


la ciudad donde se debe cambiar de afluente para ir de una ciudad a
otra.

4133 - Icy Perimeter Se tiene un mapa de hasta 1000x1000 donde se


va a representar figuras utilizando el sı́mbolo ]. se quiere hallar el mayor
de las figuras formada en caso de varias figuras se escoge el área de menor
perı́metro, en caso huecos se cuenta a la hora de hallar el perı́metro.
Para solucionar el problema primero debemos delimitar las areas que
se encuentran en el mapa para esto sabemos que dos celdas son vecinas si
comparten un lado común. Bien para hallar la cantidad de areas podemos
aplicar el algoritmo de hallar la cantidad de componentes conexas del tablero
con el uso de un BFS donde cada celda que forma parte de la componente
conexa aporta al área de esa componente una unidad mientras al perı́metro
aporta la cantidad de celdas vecinas que su sı́mbolo es distinto a ].

2917 - Heart of the Country El problema nos plantea que en cierta


nación es en guerra, que dicha nación cuenta con un grupo de ciudades donde
en cada una de ellas existe un grupo de soldados para protegerla, también
nos explica que una ciudad solo puede recibir refuerzo en cuanto a soldados
de ciudades vecinas. Se conoce que para defender cualquier ciudad de la
nación se requiere al menos K soldados. Se conoce que el enemigo ataca una
ciudad a la vez y que si la ciudad es tomada los soldados que estaban ahi

489
TEORÍA DE GRAFOS

son capturados y no pueden participar en la defensas de las ciudades vecinas.


Dado el número de ciudades la cantidad de soldados acuartelados en cada
una ellas ası́ como las ciudades vecinas hallar la cantidad de ciudades de
la nación que pueden ser defendidas y los soldados que participan en dicha
defensa.
Para resolver el problema vamos a partir de modelar la situación como
grafo no dirigido y no ponderado donde cada ciudad será un nodo y tendrá
una arista con aquella ciudad que sea su vecina. El primer paso será calcular
para cada ciudad la cantidad de soldados que cuenta para ser defendida para
ser este cálculo se debe tomar en cuenta la cantidad de soldados acuartela-
dos en dicha ciudad más los soldados acuartelados en las ciudades vecinas.
Luego de haber realizado esta operación vamos buscar aquellas ciudades que
cuentan para su defensa con una cantidad de soldados menor que K. Estas
ciudades por supuesto no van ser defendidas pero sus soldados tampoco van
poder audar a defender las ciudades vecinas por lo que debemos quitar de
la cantidad de soldados para defender de cada ciudad vecina la cantidad de
soldados de la ciudad que va ser tomada por el enemigo.
Para hacer este procedimiento realizo una especie de BFS primero busco
la ciudades que no pueden ser defendidas y las adiciona a la cola. Luego
mientras la cola no este vacı́a por cada ciudad en la cola busco sus vecinas
que pueden ser defendidas y les quito de la cantidad de soldados con que
cuentan para defenderse la cantidad de soldados de la ciudad que saque de
la cola. Si después de restar dicha la ciudad vecina que puede ser defendida
si con la nueva cantidad la ciudad ya no puede ser defendida la adiciono a la
cola y marco la ciudad como no defendida.
Una vez realizado el anterior procedimiento solo debemos recorrer las
ciudades y contar aquellas marcadas como defendidas y sumar los soldados
acuartelados en dichas y esan serán las respuestas.

2575 - Trees El problema nos pide hallar la cantidad de árboles que existen
en un grafo del cual conocemos la cantidad de nodos y las aristas que lo
componen. Para resolver el problema vamos hallar todas las componentes
conexas que tiene el mismo. De las componentes conexas halladas vamos a
contar aquellas componentes conexas cuya estructura forman un árbol. Para
verificar si la componente conexa forma un árbol basta con chequear que no
exista ciclo en ella. Para agilizar este proceso podemos utilizando un DFS
para determinar la componente conexa y a su vez utilizarlo para detectar
si existe ciclos en ella. Luego solo debemos imprimir la cantidad de árboles-
componente conexas halladas en el formato especificado en el ejercicio.

490
Capı́tulo 16

Misceláneas

16.1. Trabajo con fechas

Aunque no es costumbre que salgan en concursos ejercicios que su temáti-


ca sea el trabajo con fechas nunca esta de más tener al menos unos conoci-
mientos básicos sobre este tema por si las dudas. Aunque no salgan muchos
en concursos si existen un número considerable de ejercicios publicados en
jurados online que abarcan este tema. A continuación abordaremos algunos
tópicos que permiten resolver algunos de estos ejercicios.

16.1.1. Calendario Juliano

El calendario juliano es el antecesor del calendario gregoriano y se basa


en el movimiento aparente del sol para medir el tiempo. Desde su implan-
tación en el 46 a.C., se adoptó gradualmente en los paı́ses europeos y sus
colonias hasta la implantación de la reforma gregoriana, del papa Gregorio
XIII, en 1582. Sin embargo, en los paı́ses de religión ortodoxa se mantuvo
hasta principios del siglo XX: en Bulgaria hasta 1916, en Rusia hasta 1918, en
Rumanı́a hasta 1919 y en Grecia hasta 1923. A pesar de que en sus paı́ses el
calendario gregoriano es el oficial, hoy en dı́a algunas de las iglesias ortodoxas
(por ejemplo, la de Jerusalén, la de Rusia o la de Serbia) siguen utilizando el
calendario juliano, o modificaciones de él diferentes al calendario gregoriano,
para su liturgia y otras (por ejemplo la de Constantinopla, la de Grecia y la
de Finlandia) usan el calendario gregoriano. La Iglesia copta también sigue
utilizando el calendario juliano.

491
MISCELÁNEAS

16.1.2. Calendario Gregoriano


El calendario gregoriano es un calendario originario de Europa, actual-
mente utilizado de manera oficial en casi todo el mundo. Ası́ denominado
por ser su promotor el papa Gregorio XIII, vino a sustituir en 1582 al ca-
lendario juliano, utilizado desde que Julio César lo instaurara en el año 46
a. C. El papa promulgó el uso de este calendario por medio de la bulaInter
Gravissimas.
El germen del calendario gregoriano fueron dos estudios realizados en 1515
y 1578 por cientı́ficos de la Universidad de Salamanca, que fueron remitidos a
la Iglesia. Del primero se hizo caso omiso y del segundo finalmente fructificó
el actual calendario mundial.

16.1.3. Java: Trabajo con fecha


El package java.util tiene otras clases interesantes para aplicaciones de
distinto tipo, entre ellas algunas destinadas a considerar todo lo relacionado
con fechas y horas. A continuación se consideran algunas de dichas clases.

Clase Date
La clase Date representa un instante de tiempo dado con precisión de
milisegundos. La información sobre fecha y hora se almacena en un entero
long de 64 bits que contiene los milisegundos transcurridos desde las 00:00:00
del 1 de enero de 1970 GMT (Greenwich mean time). Ya se verá que otras
clases permiten a partir de un objeto Date obtener información del año, mes,
dı́a, horas, minutos y segundos. A continuación se muestran los métodos de
la clase Date, habiéndose eliminado los métodos declarados obsoletos (depre-
cated) en el JDK 1.2:

public c l a s s j a v a . u t i l . Date extends j a v a . l a n g . Object implements


j a v a . i o . S e r i a l i z a b l e , j a v a . l a n g . C l o n e a b l e , j a v a . l a n g . Comparable
{
public j a v a . u t i l . Date ( ) ;
public j a v a . u t i l . Date ( long ) ;
public boolean a f t e r ( j a v a . u t i l . Date ) ;
public boolean b e f o r e ( j a v a . u t i l . Date ) ;
public j a v a . l a n g . Object c l o n e ( ) ;
public int compareTo ( j a v a . l a n g . Object ) ;
public int compareTo ( j a v a . u t i l . Date ) ;
public boolean e q u a l s ( j a v a . l a n g . Object ) ;
public long getTime ( ) ;
public int hashCode ( ) ;
public void setTime ( long ) ;

492
MISCELÁNEAS

public j a v a . l a n g . S t r i n g t o S t r i n g ( ) ;
}

El constructor por defecto Date() crea un objeto a partir de la fecha y


hora actual del ordenador. El segundo constructor crea el objeto a partir
de los milisegundos transcurridos desde el 01/01/1970, 00:00:00 GMT. Los
métodos after() y before() permiten saber si la fecha indicada como argumen-
to implı́cito (this) es posterior o anterior a la pasada como argumento. Los
métodos getTime() y setTime() permiten obtener o establecer los milisegun-
dos transcurridos desde el 01/01/1970, 00:00:00 GMT para un determinado
objeto Date. Otros métodos son consecuencia de las interfaces implementadas
por la clase Date.
Los objetos de esta clase no se utilizan mucho directamente, sino que se
utilizan en combinación con las clases que se vana ver a continuación.

Clases Calendar y GregorianCalendar


La clase Calendar es una clase abstract que dispone de métodos para
convertir objetos de la clase Date en enteros que representan fechas y ho-
ras concretas. La clase GregorianCalendar es la única clase que deriva de
Calendar y es la que se utilizará de ordinario.
Java tiene una forma un poco particular para representar las fechas y
horas:

1. Las horas se representan por enteros de 0 a 23 (la hora o va de las


00:00:00 hasta la 1:00:00), y los minutos y segundos por enteros entre
0 y 59.

2. Los dı́as del mes se representan por enteros entre 1 y 31 (lógico).

3. Los meses del año se representan mediante enteros de 0 a 11 (no tan


lógico).

4. Los años se representan mediante enteros de cuatro dı́gitos. Si se re-


presentan con dos dı́gitos, se resta 1900. Por ejemplo, con dos dı́gitos
el año 2000 es para Java el año 00.

La clase Calendar tiene una serie de variables miembro y constantes (va-


riables final) que pueden resultar muy útiles:

La variable int AM PM puede tomar dos valores: las constantes enteras


AM y PM.

493
MISCELÁNEAS

La variable int DAY OF WEEK puede tomar los valores int SUNDAY,
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY y SA-
TURDAY.

La variable int MONTH puede tomar los valores int JANUARY, FE-
BRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEP-
TEMBER, OCTOBER, NOVEMBER, DECEMBER. Para hacer los
programas más legibles es preferible utilizar estas constantes simbóli-
cas que los correspondientes números del 0 al 11.

La variable miembro HOUR se utiliza en los métodos get() y set() para


indicar la hora de la mañana o de la tarde (en relojes de 12 horas, de
0 a 11). La variable HOUR OF DAY sirve para indicar la hora del dı́a
en relojes de 24 horas (de 0 a 23).

Las variables DAY OF WEEK, DAY OF WEEK IN MONTH, DAY OF MONTH


(o bien DATE), DAY OF YEAR, WEEK OF MONTH, WEEK OF YEAR
tienen un significado evidente.

Las variables ERA, YEAR, MONTH, HOUR, MINUTE, SECOND,


MILLISECOND tienen también un significado evidente.

Las variables ZONE OFFSET y DST OFFSET indican la zona horaria


y el desafı́e en milisegundos respecto a la zona GMT.

La clase Calendar dispone de un gran número de métodos para establecer


u obtener los distintos valores de la fecha y/u hora. Algunos de ellos se
muestran a continuación. Para más información, se recomienda utilizar la
documentación de JDK 1.2.

public abstract c l a s s j a v a . u t i l . Calendar extends j a v a . l a n g .


Object implements
java . io . S e r i a l i z a b l e , java . lang . Cloneable {
protected long time ;
protected boolean i s T i m e S e t ;
protected j a v a . u t i l . Calendar ( ) ;
protected j a v a . u t i l . Calendar ( j a v a . u t i l . TimeZone , j a v a . u t i l . L o c a l e
);
public abstract void add ( int , int ) ;
public boolean a f t e r ( j a v a . l a n g . Object ) ;
public boolean b e f o r e ( j a v a . l a n g . Object ) ;
public f i n a l void c l e a r ( ) ;
public f i n a l void c l e a r ( int ) ;
protected abstract void computeTime ( ) ;
public boolean e q u a l s ( j a v a . l a n g . Object ) ;

494
MISCELÁNEAS

public f i n a l int g e t ( int ) ;


public int getFirstDayOfWeek ( ) ;
public s t a t i c synchronized j a v a . u t i l . Calendar g e t I n s t a n c e ( ) ;
public s t a t i c synchronized j a v a . u t i l . Calendar g e t I n s t a n c e ( j a v a .
u t i l . Locale ) ;
public s t a t i c synchronized j a v a . u t i l . Calendar g e t I n s t a n c e ( j a v a .
u t i l . TimeZone ) ;
public s t a t i c synchronized j a v a . u t i l . Calendar g e t I n s t a n c e ( j a v a .
u t i l . TimeZone ,
java . u t i l . Locale ) ;

public f i n a l j a v a . u t i l . Date getTime ( ) ;

protected long g e t T i m e I n M i l l i s ( ) ;
public j a v a . u t i l . TimeZone getTimeZone ( ) ;
public f i n a l boolean i s S e t ( int ) ;
public void r o l l ( int , int ) ;
public abstract void r o l l ( int , boolean ) ;
public f i n a l void s e t ( int , int ) ;
public f i n a l void s e t ( int , int , int ) ;
public f i n a l void s e t ( int , int , int , int , int ) ;
public f i n a l void s e t ( int , int , int , int , int , int ) ;
public f i n a l void setTime ( j a v a . u t i l . Date ) ;
public void setFirstDayOfWeek ( int ) ;
protected void s e t T i m e I n M i l l i s ( long ) ;
public void setTimeZone ( j a v a . u t i l . TimeZone ) ;
public j a v a . l a n g . S t r i n g t o S t r i n g ( ) ;
}

La clase GregorianCalendar añade las constante BC y AD para la ERA,


que representan respectivamente antes y después de Jesucristo. Añade además
varios constructores que admiten como argumentos la información correspon-
diente a la fecha/hora y –opcionalmente- la zona horaria.
A continuación se muestra un ejemplo de utilización de estas clases. Se
sugiere al lector que cree y ejecute el siguiente programa, observando los
resultados impresos en la consola.

public c l a s s PruebaFechas {
public s t a t i c void main ( S t r i n g a r g [ ] ) {
Date d = new Date ( ) ;
G r e g o r i a n C a l e n d a r gc = new G r e g o r i a n C a l e n d a r ( ) ;
gc . setTime ( d ) ;
System . out . p r i n t l n ( "Era: "+gc . g e t ( Calendar .ERA) ) ;
System . out . p r i n t l n ( "Year: "+gc . g e t ( Calendar .YEAR) ) ;
System . out . p r i n t l n ( " Month : "+gc . g e t ( Calendar .MONTH) ) ;
System . out . p r i n t l n ( "Dia del mes: "+gc . g e t ( Calendar .DAY OF MONTH)
);
System . out . p r i n t l n ( "D de la S en mes:" +gc . g e t ( Calendar .

495
MISCELÁNEAS

DAY OF WEEK IN MONTH) ) ;


System . out . p r i n t l n ( "No de semana : "+gc . g e t ( Calendar .WEEK OF YEAR
));
System . out . p r i n t l n ( " Semana del mes: "+gc . g e t ( Calendar .
WEEK OF MONTH) ) ;
System . out . p r i n t l n ( " Fecha : "+gc . g e t ( Calendar .DATE) ) ;
System . out . p r i n t l n ( "Hora: "+gc . g e t ( Calendar .HOUR) ) ;
System . out . p r i n t l n ( " Tiempo del dia: "+gc . g e t ( Calendar .AM PM) ) ;
System . out . p r i n t l n ( "Hora del dia: "+gc . g e t ( Calendar .HOUR OF DAY)
);
System . out . p r i n t l n ( " Minuto : "+gc . g e t ( Calendar .MINUTE) ) ;
System . out . p r i n t l n ( " Segundo : "+gc . g e t ( Calendar .SECOND) ) ;
System . out . p r i n t l n ( "Dif. horaria : "+gc . g e t ( Calendar . ZONE OFFSET)
);
}
}

Clases DateFormat y SimpleDateFormat


DateFormat es una clase abstract que pertenece al package java.text y no
al package java.util, como las vistas anteriormente. La razón es para facilitar
todo lo referente a la internacionalización, que es un aspecto muy importante
en relación con la conversión, que permite dar formato a fechas y horas de
acuerdo con distintos criterios locales. Esta clase dispone de métodos static
para convertir Strings representando fechas y horas en objetos de la clase
Date, y viceversa.
La clase SimpleDateFormat es la única clase derivada de DateFormat. Es
la clase que conviene utilizar. Esta clase se utiliza de la siguiente forma: se
le pasa al constructor un String definiendo el formato que se desea utilizar.
Por ejemplo:

import j a v a . u t i l . ∗ ;
import j a v a . t e x t . ∗ ;
c l a s s SimpleDateForm {
public s t a t i c void main ( S t r i n g a r g [ ] ) throws P a r s e E x c e p t i o n {
SimpleDateFormat s d f 1 = new SimpleDateFormat ( "dd -MM -yyyy hh:mm:
ss" ) ;
SimpleDateFormat s d f 2 = new SimpleDateFormat ( "dd -MM -yy" ) ;
Date d = s d f 1 . p a r s e ( "12 -04 -1968 11:23:45 " ) ;
S t r i n g s = s d f 2 . format ( d ) ;
System . out . p r i n t l n ( s ) ;
}
}

La documentación de la clase SimpleDateFormat proporciona abundante

496
MISCELÁNEAS

información al respecto, incluyendo algunos ejemplos.

Clases TimeZone y SimpleTimeZone


La clase TimeZone es también una clase abstract que sirve para definir
la zona horaria. Los métodos de esta clase son capaces de tener en cuenta el
cambio de la hora en verano para ahorrar energı́a. La clase SimpleTimeZone
deriva de TimeZone y es la que conviene utilizar.
El valor por defecto de la zona horaria es el definido en el ordenador en
que se ejecuta el programa. Los objetos de esta clase pueden ser utilizados
con los constructores y algunos métodos de la clase Calendar para establecer
la zona horaria.

16.1.4. Dı́a de la semana


16.1.5. Cantidad de dı́as entre dos fechas
16.1.6. Año bisiesto
Año bisiesto es una expresión que deriva del latı́n bis sextus dies ante
calendas martii (sexto dı́a antes del mes de marzo repetido), que correspondı́a
a un dı́a extra intercalado entre el 23 y el 24 de febrero por Julio César. En
el calendario gregoriano, calendario hecho por el papa Gregorio XIII, este
dı́a extra se colocó al final de mes (29 de febrero). El 24 de febrero era el
sexto dı́a antes de las calendas (primer dı́a del mes) de marzo. Los romanos
no contaban los dı́as del mes del 1 al 31, sino que tomaban tres fechas de
referencia: calendas, nonas e idus. Para contar se incluı́a el dı́a de referencia
(en este caso, el 1 de marzo).
Este dı́a se añade para corregir el desfase que existe entre la duración
del año trópico: 365 dı́as 5 h 48 min 45,25 s (365,242190402 dı́as) y el año
calendario de 365 dı́as. Esto requiere que cada cuatro años se corrija el año
calendario por una acumulación no contabilizada de aproximadamente 1/4
de dı́a por año que equivale a un dı́a extra.
En el calendario juliano se consideraban bisiestos los años divisibles entre
cuatro. Ası́, el año juliano duraba 365 dı́as + 0,25 = 365,25 dı́as, más que el
año trópico, que consta de 365,242190402 dı́as.

bool y e a r B i s i e s t o C a l e n d a r J u l i a n ( int year ){


bool p=( y e a r %4==0) ;
return p ;
}

497
MISCELÁNEAS

Según el calendario gregoriano, la regla para los años bisiestos es la si-


guiente:

Un año es bisiesto si es divisible entre 4, a menos que sea divisible entre


100. Sin embargo, si un año es divisible entre 100 y además es divisible
entre 400, también resulta bisiesto. Obviamente, esto elimina los años
finiseculares (últimos de cada siglo, que ha de terminar en 00) cuyo
siglo no es múltiplo de 4.

La siguiente implementación comprueba si un determinado año en el ca-


lendario gregoriano que es pasado por parámetro es bisiesto o no. Devuelve
verdadero en caso de ser bisiesto falso en caso contrario.

bool y e a r B i s i e s t o C a l e n d a r G r e g o r i a n ( int year ){


bool p=( y e a r %4==0) ;
bool q=( y e a r %100==0) ;
bool r =( y e a r %400==0) ;
return ( p and ( ( not q ) or r ) ) ;
}

16.2. Ajedrez
16.2.1. Rey
16.2.2. Dama
16.2.3. Torre
16.2.4. Alfil
El alfil es una pieza menor del ajedrez occidental de valor aproximado de
tres peones. Se mueve en diagonal y no pueden saltar piezas intervinientes,
y captura tomando el lugar ocupado por la pieza adversaria. Debido a las
caracterı́sticas de su movimiento tiene la deficiencia de la debilidad del color
donde su movimiento queda limitado al color del escaque en la que se inicia
la partida.

Cantidad máxima de alfiles en un tablero con dimesión n sin ata-


carse
Entre los problemas relacionados con esta pieza esta el de hallar la máxi-
ma cantidad de piezas de este tipo en un tablero de dimensión (nx n) sin que

498
MISCELÁNEAS

Figura 16.1: Movimiento del alfil, los cı́rculos indican donde se permite el
movimiento.

ninguna se ataque. Para la dimensiones de tablero 1 y 2 las respuestas van


hacer precisamente esos números pero para cuando n sea mayor e igual que 3
que sucede ?. Vamos a colocar en la fila n del tablero alfiles y marcar las casi-
llas que son atacadas por las piezas. Observarán que a medida que aumenten
las dimensiones del tablero la cantidad de casillas disponibles aumentan en
n-2. Resulta que la máxima cantidad de alfiles que puedo colocar sin que
se ataquen entre ellos y con los que estaban anteriormente en estas casillas
disponibles es precisamente n-2. Esto nos conduce a la siguiente ecuación
solución para el problema. Para n igual a 1 ó 2 la respuesta va ser 1 y 2
respectivamente mientras para un n mayor que 2 la solución es 2n − 2.

16.2.5. Caballo
El caballo es una pieza menor del ajedrez occidental de un valor aproxi-
mado de tres peones. Tiene un movimiento semejante a una L y, a diferencia
de otras piezas, puede saltar piezas intermedias. Captura tomando el escaque
ocupado por la pieza adversaria.

Cantidad mı́nima de pasos de caballo en un tablero


El problema parte de saber la cantidad mı́nima de pasos que debe dar
una la pieza de ajedrez de tipo caballo para poder ir de una posición x a una
posición y del tablero. La primera idea que no viene a la cabeza es realizar un

499
MISCELÁNEAS

Figura 16.2: Los escaques marcados indican donde se permite el movimiento.

BFS. Pero que hacer cuando dicho algoritmo es ineficiente producto de que
no conocemos las dimensiones del tablero o este es infito. Por suerte existe un
algoritmo que dado dos posiciones del tablero el cual se asume que es infito
determina la cantidad de mı́nima que debe dar un caballo para ir de una a
otra posición. A continuación les dejo el código.

#include <cmath>
#include <math . h>
#include <a l g o r i t h m >
#define LL long long

LL d i s t (LL x1 , LL y1 , LL x2 , LL y2 )
{
LL dx=abs ( x2−x1 ) ;
LL dy=abs ( y2−y1 ) ;
LL l b =(dx+1) / 2 ;
l b=max( lb , ( dy+1) / 2 ) ;
l b=max( lb , ( dy+dx+2) / 3 ) ;

while ( ( l b %2) != ( ( dx+dy ) %2) )


l b ++;
i f ( abs ( dx )==1 && ! dy )
return 3 ;
i f ( abs ( dy )==1 && ! dx )
return 3 ;
i f ( abs ( dx )==2 && abs ( dy )==2)
return 4 ;

500
MISCELÁNEAS

return l b ;
}

Donde las variables x1 y y1 se corresponde con la posición inicial siendo


x1 las filas y y1 las columnas, de igual manera son x2 y y2 para la posición
final.

Peón

16.3. Jurado Online


Los jurados en lı́nea o judge online son sistemas informáticos que permiten
enviar, compilar, ejecutar y evaluar programas implementados en determina-
dos lenguajes de programación que dan solución a determinados problemas
publicados en el jurado. Pero como lo hace?.
Todo el proceso comienza cuando el usuario o equipo envı́a su solución al
sistema. El sistema compila el código enviado utilizando el compilador aso-
ciado al lenguaje de programación seleccionado por el usuario en el momento
del envı́o. Si el código no compila el sistema lo informa a través del veredicto
Error de compilación. Pero si el código compila perfectamente el sistema
pasa al siguiente paso.
El siguiente paso es ejecutar el código y pasarle unos juegos de datos que
se correspoden con las especificaciones de los datos de entrada descriptos en
el problema. En caso que durante la ejecucción su programa se detiene por
algún error producto a una instrucción como puede ser la división por cero,
se acceda a una posición de un arreglo o alguna memoria reservada que ya
no exista el veredicto del sistema será Error de ejecucción. Si el programa
se ejecutó sin ninguna interrupción entonces guarda la salida que produjo la
solución enviada para cada juegos de datos probados. Se hace el siguiente
análisis por parte del sistema.
Se comprueba primero si la solución enviada cumple con la restricción
de código fuente del problema, en otras palabras comprueba que la cantidad
de caracteres utilizados en la implementación de la solución no sobrepase
el lı́mite permitido para el problema. Si la solución sobrepasa el lı́mite el
veredicto es Lı́mite de código fuente excedido. Este error algunos jurados
lo obvian o lo que hacen es que no permiten que el usuario envı́e una solución
que su código no cumpla con la restrición de este tipo.
Se comprueba que el uso de la memoria que realiza la solución y si se
hace un uso superior al permitido el veredicto de jurado será Lı́mite de
memoria excedido.
Otras de las comprobaciones que se realiza es la del tiempo que se demora

501
MISCELÁNEAS

la solución para resolver cada caso de prueba y aquı́ puede venir dos posibles
veredictos de acuerdo a jurado que se utilice. Si tu solución en algún caso
especificó se demora más de lo permitido el veredicto será Lı́mite de tiempo
excedido, algunos jurados se toma además el tiempo total que se demoró
tu solución para solucionar todos los casos. Si ese tiempo es superior a una
cota definida para el problema entonces el veredicto será el de Lı́mite de
tiempo total excedido.
Luego si despues de todas estas comprobaciones el jurado no ha lanzado
ninguna de los anteriores veredictos el sistema va comparar las salidas que
produjo la solución y la comparará con las salidas de muestras que tiene para
los juegos de datos que se probarón en la solución y si ambas salidas son iden-
ticas en tonces el verdicto es Aceptado sino será Respuesta Incorrecta.
Pueden existir otro grupo de posibles veredictos que lancen los jurados
pero son propios ese sistema en sı́, los anteriormentes expuestos son los más
comunes. De igual forma existen jurados que te dan una evaluación general
de tu solución a partir de la evaluación de tu solución para cada caso probado
e incluso te dicen la evaluación para cada caso probado.

16.4. Estrategias de competencia


Para las competencias o cuncursos de programación los equipos deben
establecer una estrategia de competencia previamente la cual deben aplicar
durante el concurso o competencia. Pero como lograr encontrar la estrate-
gia de competencia adecuada para usted y su equipo ?. La solución es bien
sencilla, compita. Solo compitiendo podrá encontrarla. Lo primero es definir
cuales son las competencias o concursos fundamentales que su equipo tiene
en mente. Una vez realizado esto, aproveche el resto de las comptencias como
laboratorios de ensayo para buscar y ajustar su estrategia. Una estrategia de
competencia no es vitalicia en un equipo la misma pueda cambiar producto
que los integrantes del equipo amplian o fortalecen sus áreas de conocimien-
tos o porque la composición del equipo sufre cambios. A nuestro modo de
ver existen algunos elementos que se debe tener en cuenta para confeccionar
una estrategia de competencia como son:
Trate de llegar en tiempo: La estrategia debe trazarse no desde
el comienzo de la competencia sino desde momentos antes que bien
puede ser 30 minutos antes o hasta un dı́a antes. Llegar en tiempo a la
competencia lo prepara a usted para enfrentar la tarea que se avecina,
además de darle apoyo moral y psicológico a sus compañeros. El hecho
que esté todo el equipo reunido, en tiempo, para el comienzo de la justa
les proporciona calma.

502
MISCELÁNEAS

Siempre atienda las orientaciones y aclaraciones del jurado: En


ocasiones todos hemos sido vı́ctimas de este mal que afecta a muchos
estudiantes y, en general, a personas que no son capaces de lograr una
medida serenidad y calma en momentos como estos. El jurado puede
aclarar algún aspecto relevante que en un futuro puede ser una duda
para usted.

Saber las potencialidades de cada uno de los miembros de su


equipo: Este es uno de los aspectos más relevantes e importantes que
debe usted tener presente si desea lograr un eficiente y correcto ”trabajo
en equipo”; lo normal y necesario, según la variedad de los problemas,
es que en cada conjunto haya un buen programador, un matemático
y otro individuo que tenga un conocimiento general y variado sobre
las restantes ramas del conocimiento (o por lo menos las más que se
puedan, es completamente inviable abarcar todo el conocimiento), es
válido aclarar que idóneo fuese que todos dominen todas las técnicas,
aunque en ocasiones afecta el trabajo en colectivo. Lo primero que
deben hacer todos los integrantes es leerse bien, detenidamente, todos
y cada uno de los problemas propuestos por el jurado, para a partir
de ahı́ asignar ejercicios según el binomio: “complejidad” y “rama del
conocimiento” al que pertenece el problema y según las potencialidades
del estudiante que intentará resolverlo.

Leer todos los problemas: Bajo ningún concepto usted como concur-
sante debe abandonar o terminar una competencia sin leer y entender
los problemas que su equipo intento resolver sin exito, y aquellos que ni
siquiera se penso en una solución. Muchos son los casos en que equipos
han salido o terminado una competencia y luego se han enterado que
hubo uno o un grupo de problemas que estaban a su alcance y sencilla-
mente no lo hicieron porque era un problema con un texto demasiado
largo. Tomesé los primeros 20-30 minutos de la competencia para leer
y entender todos los problemas, eso lo va ayudar a determinar el orden
y la distribución de los problemas en el equipo.

En la máquina solo debe estar programando uno, mientras


el resto trabaja en los demás problemas: Bajo ningún concepto
usted debe entretenerse en otra cosa que no sea la competencia que está
teniendo lugar en ese momento; la lid tiene un tiempo de duración de 4
horas, pero estas son solo la intersección del tiempo de cada integrante
del equipo: el total de tiempo del equipo se multiplica para la cantidad
de sus integrantes, si el conjunto tiene 3 estudiantes pues entonces

503
MISCELÁNEAS

el tiempo de aprovechamiento máximo será de 12 horas. !Traten de


aprovecharlas todas!.

Defina la complejidad de los ejercicios y sus verdaderas posi-


bilidades de resolverlos: por las caracterı́sticas de esta competencia,
usted tendrá un ejercicio aceptado si está correcto un 100 %, no vale
de nada que sus posibilidades sean resolver los 6 problemas al 98 %, al
final todos estarı́an incompletos y el saldo final de aceptados es 0 aun-
que çasi.o ”por poquito”pueda resolverlos. Defina la complejidad de los
ejercicios, trate de comenzar por los más fáciles (a su consideración),
comenzar la competencia con algún aceptado siempre sube la moral y
entrega de su equipo.

Mire las estadı́sticas de aceptados por problemas: La cantidad


de soluciones correctas por problemas le da a usted una medida de la
complejidad del mismo; si más de 10 equipos han resuelto un problema
determinado, pues entonces hay una mayor ”probabilidad”de que sea
un problema fácil de hacer (aunque a veces este no es un modelo fiable
), este consejo queda a su consideración, pues en ocasiones hay un pro-
blema con 2 aceptados y al final de la jornada no era tan dificil, además
no le ponga techo a sus conocimientos y posibilidades, sencillamente su
equipo puede ser el 3ro en resolverlo!.

No te retires de la competencia hasta el último momento:


Por lo general la mayorı́a de los problemas de concursos de este tipo
tienen una solución elegante, fácil de resolver e implementar (a no ser
que sea un problema de geometrı́a computacional! ), aunque te queden
5 minutos no pierdas las esperanzas, además, el mejor y más puro
momento de una competencia es el final, !no te la pierdas!

Si en una competencia no puedes resolver algún problema, no


te desilusiones... será para la próxima por lo general, los jurados
de las competencias tienen modelos de selección de problemas para la
competencia; este es el llamado 2-2-2 (2 problemas fáciles que todos
puedan resolver, 2 de complejidad media y 2 que nadie pueda resolver
, vamos, que no es nada personal, es cuestión de respeto ). El mérito
primario es la participación en la competencia, un poco más allá es
poder resolver un ejercicio. Si no puedes resolver ningún problema en
una competencia, no se desilusione, será para la próxima!

504
MISCELÁNEAS

16.5. Equipo
A la hora de crear un equipo de tres estudiantes para los concurso ACM-
ICPC existen dos variantes:

Equipo beta: Se integran por libre albedrı́o, sin ningún procedimiento


o criterio técnico especı́fico. En estos equipos es usual que exista una
alta colaboración y cooperación entre sus miembros, pero al mismo
tiempo pueden tener similares deficiencias o debilidades.

Equipo alfa: Se constituyen a partir de algún basamento o meca-


nismo con criterios técnicos especı́ficos. En estos equipos puede que
inicialmente entre sus miembros la colaboración y cooperación no sean
tan altas, pero el conocimiento previo de las fortalezas y debilidades de
cada concursante debe derivar en equipos más fuertes desde un inicio.

16.6. Código Gray


El código binario reflejado o código Gray, nombrado ası́ en honor del
investigador Frank Gray, es un sistema de numeración binario en el que dos
valores sucesivos difieren solamente en uno de sus dı́gitos.
El código Gray fue diseñado originalmente para prevenir señales ilegales
(señales falsas o viciadas en la representación) de los switches electromecáni-
cos, y actualmente es usado para facilitar la corrección de errores en los
sistemas de comunicaciones, tales como algunos sistemas de televisión por
cable y la televisión digital terrestre.
El investigador de Laboratorios Bell Frank Gray inventó el término código
binario reflejado cuando lo patentó en 1947, remarcando que éste ”no tenı́a
nombre reconocido aún”. Él creó el nombre basándose en el hecho de que
el código ”puede ser construido a partir del código binario convencional por
una suerte de ’proceso reflejante’”.
El código binario reflejado fue aplicado para acertijos matemáticos antes
de ser usado para la ingenierı́a. El ingeniero francés Émile Baudot le dio una
aplicación al código de Gray en 1878 en telegrafı́a.
Hasta la primera mitad de los años 1940 los circuitos lógicos digitales se
realizaban con válvulas de vacı́o y dispositivos electromecánicos. Los con-
tadores necesitaban potencias muy elevadas a la entrada y generaban picos
de ruido cuando varios bits cambiaban simultáneamente. Tomando esto en
cuenta, Frank Gray inventó un método para convertir señales analógicas a
grupos de código binario reflejado utilizando un aparato diseñado con válvu-

505
MISCELÁNEAS

las de vacı́o, con lo cual garantizó que en cualquier transición variarı́a tan
sólo un bit.
En la actualidad, el código Gray se emplea como parte del algoritmo de
diseño de los mapas de Karnaugh, los cuales son, a su vez, utilizados como
”herramienta de diseño.en la implementación de circuitos combinacionales y
circuitos secuenciales. La vigencia del código Gray se debe a que un diseño
digital eficiente requerirá transiciones más simples y rápidas entre estados
lógicos (0 ó 1), por ello es que se persiste en su uso, a pesar de que los
problemas de ruido y potencia se hayan reducido con la tecnologı́a de estado
sólido de los circuitos integrados.
Utilizando el código Gray es posible también resolver el problema de
las Torres de Hanói. Se puede incluso formar un ciclo hamiltoniano o un
hipercubo, en el que cada bit se puede ver como una dimensión.
Debido a las propiedades de distancia de Hamming que posee el código
Gray, es usado en ocasiones en algoritmos genéticos.
Las computadoras antiguas indicaban posiciones abriendo y cerrando in-
terruptores. Utilizando tres interruptores como entradas usando Base 2, estas
dos posiciones estarı́an una después de la otra. El problema con el código bi-
nario en base 2 es que con interruptores mecánicos, es realmente difı́cil que
todos los interruptores cambien al mismo tiempo. En la transición de los dos
estados mostrados arriba, tres interruptores cambian de sitio. En el lapso
en el que los interruptores están cambiando, se pueden presentar salidas de
información espurias. Si las salidas mencionadas alimentan un circuito se-
cuencial, probablemente el sistema presentará un error en entrada de datos.
El código gray resuelve este problema cambiando solamente un dı́gito a la
vez, ası́ que no existe este problema. Tienes que tener en cuenta que para
convertir de binarios a Gray los valores que deben ser sumados en base 2 to-
man los siguientes valores 1+1=0, 0+0=0 , 1+0=1 y 0+1=1 esta operación
de forma vertical
Para convertir un número binario (en Base 2) a código Gray, simplemente
se le aplica una operación XOR con el mismo número desplazado un bit a la
derecha, sin tener en cuenta el acarreo.
Para relaizar la operación inversa es decir llevar de código gray a número
binario (Base 2) realizams el siguiente algoritmo:
Definimos un vector g conteniendo los dı́gitos en gray y otro vector b
destinado a contener los dı́gitos en Base 2
g0 es el dı́gito que se encuentra en el extremo izquierdo de la represen-
tación en código gray.
b0 es el dı́gito de mayor peso y que se encuentra en el extremo izquierdo
en la representación en Base 2.

506
MISCELÁNEAS

Luego resulta que: bn+1 = gn+1 ⊕ bn con la excepción de que b0 = g0 , la


cual se puede resumir como:
Con el número g = 1001 en código Gray.
Lo primero es decir que: b0 = g0 , por lo que para este caso: b0 = 1 .
Luego siguiendo con el algoritmo: bn+1 = gn+1 ⊕ bn resulta que:
b0 = g 0 = 1
b1 = g 1 ⊕ b0 = 0 ⊕ 1 = 1
b2 = g 2 ⊕ b1 = 0 ⊕ 1 = 1
b3 = g3 ⊕ b2 = 1 ⊕ 1 = 0
Esto da como resultado b = 1110
La siguiente implementación permite hallar el valor en decimal del enesi-
mo número gray, luego solo tendrı́amos que convertir ese valor decimal que
devuelve la función en binario y tendrı́amos su valor en código gray.

int codeGray ( int thGray )


{
return thGray ˆ ( thGray>> 1 ) ;
}

En caso de que queramos realizar la operación inversa, es decir dado el


enesimo número gray su valor en decimal determinar el número original quelo
generó podemos utilizar la siguiente función:

int r e v g ( int g )
{
int n = 0 ;
f o r ( ; g ; g>> = 1 )
n ˆ = g;
return n ;
}

16.7. Parser
16.7.1. Leer una lı́nea completa con C++
Uno de los problemas que se enfrentan los concursantes sobre todo los
que programan en C++ es el de leer toda una lı́nea de datos y despues
”dividir”dicha entrada en palabras. Para solcionar esto vamos en dividir este
dos momentos:
Captura de datos: Bien para leer una lı́nea completa con espacios
incluidos en C++ existen dos variantes de acuerdo a como se lea los
datos. Si usamos la función scanf serı́a de la siguiente manera:

507
MISCELÁNEAS

s c a n f ( " %[^\n]" , s ) ;

lee todos los caracteres que encuentra hasta que llega al carácter nueva
lı́nea ’
n’. Esta sentencia puede utilizarse por tanto para leer lı́neas completas,
con blancos incluidos.
Mientras en caso que utilizemos cin se puede utilizar lo siguiente:

string line
g e t l i n e ( cin , l i n e )

Hay que tener en cuenta que en ambos casos que el salto de lı́nea no lo
captura y si vuelves a leer otra cadena seguido solo vas a leer el salto
de lı́nea.

Parsear la entrada: Una vez almacenado la entrada de toda una


lı́nea como parsear ”splitear.en palabras. Para eso se puede usar uno los
siguientes algoritmos:

v e c t o r <s t r i n g > s p l i t A l l ( s t r i n g s )
{
stringstream ss ( s ) ;
v e c t o r <s t r i n g > v ;
while ( s s >> b )
{
v . push back ( b ) ;
}

return v ;
}

v e c t o r <s t r i n g > s p l i t A l l ( s t r i n g l i n e , s t r i n g
separator )
{
v e c t o r <s t r i n g > v ;
f o r ( int p =0;( p= l i n e . f i n d ( s e p a r a t o r ) ) !=
l i n e . npos ; )
{
v . push back ( l i n e . s u b s t r ( 0 , p ) ) ;
l i n e= l i n e . s u b s t r ( p + s e p a r a t o r .
size () ) ;
}
v . push back ( l i n e ) ;
return v ;

508
MISCELÁNEAS

En ambos algoritmo hay que tener en cuenta que puede existir ”palabras.o
”tokens”vacios o de longitud igual cero. Esto es bien fácil de solucionar,
solo basta con comprobar que la palabra tenga longitud distinto de cero
antes de adicionarla al vector resultante.

16.8. Puzzle 15
El puzzle 15 consiste en una matriz de 4x4 donde existen todos los núme-
ros del 1 al 15 con un espacio vacı́o. El objetivo del juego es colocar todos los
números del 1 al 15 desde de la esquina superior izquierda dejando el espacio
en la esquina inferior derecha tal y como se muestra en la figura.

Figura 16.3: Configuración ganadora del puzzle 15

Donde la X representa la posición vacı́a en el tablero. Las reglas de mo-


vimiento es bastante simple un número solo puede ser colocado en una de
las casillas con las que comparte un lado y solo si esa casilla esta casilla esta
vacı́a. En el caso de la figura anterior solo los números 15 y 12 pueden mo-
verse hacia la derecha y abajo respectivamente. Este juego fue inventado por
Noyes Chapman in 1880 bajo el nombre de Puzzle 15.
Ahora bien nuestro interés en este juego es el siguiente dada una con-
figuración inicial determinar si existe una secuencias de movimientos que
conduzcan a la configuración ganadora del juego.
Por supuesto una primera y trivial idea es realizar un algoritmo de búsque-
da a fuerza bruta y a partir de la configuración inicial del tablero explorar
todas las posibles configuraciones que se pueden generar y verificar si alguna
es la configuración ganadora.
Claro esta idea tiene un costo tanto en memoria como en tiempo enor-
me teniendo que a partir de cada configuración se puede generar entre 2 a
4 nuevas configuraciones. Bueno entonces como resolvemos el problema de

509
MISCELÁNEAS

dada una configuración del tablero del puzzle 15 saber si puede llegar a la
configuración ganadora del juego.
Bueno vamos a tomar el tablero como un arreglo unidimensional de 16
elementos y a la configuración dada en el tablero es una de las 16! permuta-
ciones que pueden generar ese conjunto de elementos en el caso de la celda
vacı́a colocaremos el valor 0.
Llamaremos N al número de inversiones( cantidad de veces que se cumple
que: ai y aj son elementos del arreglo tal que i < j, pero ai > aj ) dentro del
arreglo unidimensional.
Ahora llamaremos K el indicé de la fila donde se ubica el valor cero en el
arreglo que representa la casilla vacı́a en la configuración inicial del tablero.
Se puede calcular como K = (z − 1) div 4 + 1 siendo z la posicion dentro del
arreglo unidimensional.
Luego si N + K es un número par podemos afirmar con certeza que a
partir de esa configuración dada se puede llegar a la configuración ganadora
del juego.
Luego la implementacion serı́a ası́:

int a [ 1 6 ] ;
f o r ( int i =0; i <16; ++i )
c i n >> a [ i ] ;

int i n v = 0 ;
f o r ( int i =0; i <16; ++i )
if (a [ i ])
f o r ( int j =0; j <i ; ++j )
if (a [ j ] > a [ i ])
++i n v ;
f o r ( int i =0; i <16; ++i )
i f ( a [ i ] == 0 )
i n v += 1 + i / 4 ;

p u t s ( ( i n v & 1 ) ? "No Solution " : " Solution Exists " ) ;

16.9. Recursividad
La recursividad es una técnica de programación en la que un función,
método o subrutina hace una llamada a si mismo con el fin de resolver el
problema. La llamada a si mismo se conoce como llamada recursiva. Dicho
formalmente, un algoritmo se dice recursivo si calcula instancias de un pro-
blema en función de otras instancias del mismo problema hasta llegar a un
caso base, que suele ser una instancia pequeña del problema, cuya respuesta

510
MISCELÁNEAS

generalmente está dada en el algoritmo y no es necesario calcularla.

16.9.1. Funciones matemáticas recursivas


16.9.2. Recursividad simple
16.9.3. Recursividad ramificada
16.9.4. Recursividad con bactracking

16.10. Divide y Vencerás


16.11. Fı́sica
16.12. Probabilidades y estadı́sticas
16.13. Team Reference
16.14. Análisis de ejercicios
2650 - Easy Probability La solución de ejercicio es bastante sencilla.
La ecuación que lo resuelve es 1-(probabilidad del evento del evento A sin
B/probabilidad del evento A). Recordar que la respuesta se debe dar con dos
lugares despues de la coma.

1605 - Grey Codes II Para cada n debemos generar las primeras 2n


secuencias de códigos grey con exactamente n bits y concatenar en la salida.
Para solucionar el problema solo debemos realizar un recorrido desde 0 hasta
2n -1 determinar el enésimo grey e imprimir su representación en binario uno
detrás del otro.

3051 - Calculating Probabilities La solución es bien fácil solo debemos


imprimir el resultado con dos lugares despues de la coma de la división de la
cantidad de casos de favorables entre la cantidad total de casos.

1135 - It’s My Derivative Para poder solucionar este ejercicio debemos


tener conocimiento sobre el diseño e implementación de Interpretes y Com-
piladores para elaborar un mini-interprete de funciones polinomiales que sea
capaz de:

511
MISCELÁNEAS

Parsear la función dada en una cadena de literales y extraer los términos


que la componen.

Derivar cada término que tiene la función.

Sustituir la variable por el valor.

Calcular el resultado de cada término una vez que se sutituye la variable


por un valor dado.

Calcular el resultado de la función una vez que se sutituye la variable


por un valor dado.

2451 - Fifteen Puzzle El problema nos pide que dada una configuración
del tablero del juego Puzzle 15 determinar si se puede resolver o no. Entien-
dasé como resolver como realizar una secuencia de movimentos de forma que
se puede colocar todos los numeros en orden desde el 1 al 15 comenzando
desde la esquina superior izquierda dejando el espacio vació en la esquina
inferior derecha. Aplicando el algoritmo explicado en la sección Puzzle 15 de
esta capı́tulo se puede resolver.

2189 - A Game with Coins Aplicando las fórmulas conocidas de probi-


lidades en el problema y despejando no queda la siguiente ecuación (K ∗ 2) −
(M ∗ 2) + (K − M ) ∗ 0,5 la cual resuelve el problema planteado.

512

Вам также может понравиться