0% encontró este documento útil (0 votos)
309 vistas257 páginas

AlgorithmsNotesForProfessionals (Español)

Descargar como pdf o txt
Descargar como pdf o txt
Descargar como pdf o txt
Está en la página 1/ 257

Machine Translated by Google

Algoritmos

Notas para algoritmos profesionales


Notas para profesionales

más de 200 páginas


de consejos y trucos profesionales

GolKicker.com Libros Descargo


de responsabilidad Este es un libro gratuito no oficial creado con fines
de Programación Gratis educativos y no está asociado con grupos o compañías oÿciales de algo
Todas las marcas comerciales y marcas registradas
son propiedad de sus respectivos dueños
Machine Translated by Google

Contenido
Sobre .................................................... .................................................... .................................................... ............................. 1

Capítulo 1: Primeros pasos con los algoritmos ........................................... .................................................... ....... 2

Sección 1.1: Ejemplo de un problema algorítmico ........................................... .................................................... ..................... 2


Sección 1.2: Primeros pasos con el algoritmo simple Fizz Buzz en Swift .................................................... ..................... 2

Capítulo 2: Complejidad del algoritmo ............................................... .................................................... .......................... 5

Sección 2.1: Notación Big-Theta .................................................... .................................................... ................................ 5


Sección 2.2: Comparación de las notaciones asintóticas .................................................... ............................................ 6
Sección 2.3: Notación Big-Omega .................................................... .................................................... ............................ 6

Capítulo 3: Notación Big-O .................................................... .................................................... ..................................... 8

Sección 3.1: Un bucle simple ............................................... .................................................... ............................................. 9


Sección 3.2: Un bucle anidado ............................................. .................................................... ............................................. 9
Sección 3.3: Tipos de algoritmos O(log n) ....................................... .................................................... .......................... 10
Sección 3.4: Un ejemplo de O(log n) .................................. .................................................... .................................... 12
Capítulo 4: Árboles .................................................... .................................................... .................................................... ... 14

Sección 4.1: Representación típica de un árbol anario ........................................... .................................................... ............ 14


Sección 4.2: Introducción .................................................... .................................................... ............................................. 14
Sección 4.3: Para verificar si dos árboles binarios son iguales o no .................................................... .................................... 15

Capítulo 5: Árboles de búsqueda binarios.................................................... .................................................... ...................... 18

Sección 5.1: Árbol de búsqueda binaria - Inserción (Python) ....................................... .................................................... ...... 18
Sección 5.2: Árbol de búsqueda binaria - Eliminación (C++) ....................................... .................................................... ............ 20
Sección 5.3: Antepasado común más bajo en un BST .................................................... .................................................... .. 21
Sección 5.4: Árbol de búsqueda binaria - Python ........................................... .................................................... ..................... 22

Capítulo 6: Verificar si un árbol es BST o no Sección .................................................... .................................................... ...... 24

6.1: Algoritmo para verificar si un árbol binario dado es BST Sección .................................................... ................................ 24
6.2: Si un árbol de entrada dado sigue o no la propiedad del árbol de búsqueda binario .................................................... ..... 25
....................................................
Capítulo 7: Recorridos de árboles binarios Sección 7.1: Orden de niveles transversal ....................................................
- .................. 26

Implementación ............................................................. .......................................... 26


Sección 7.2: Recorrido de un árbol binario en preorden, en orden y post orden .................................................... ........ 27

Capítulo 8: El antepasado común más bajo de un árbol binario .................................................... ....................... 29

Sección 8.1: Encontrar el ancestro común más bajo .................................................... .................................................... ..... 29

Capítulo 9: Gráfico ....................................... .................................................... .................................................... .............. 30

Sección 9.1: Almacenamiento de gráficos (matriz de adyacencia) .................................. .................................................... ............ 30


Sección 9.2: Introducción a la teoría de grafos ........................................... .................................................... .......... 33
Sección 9.3: Almacenamiento de gráficos (Lista de adyacencia) .................................. .................................................... .......... 37
Sección 9.4: Ordenación .................................................... .................................................... .................................. 39
topológica Sección 9.5: Detección de un ciclo en un gráfico dirigido utilizando Profundidad .................................................... 40
Primero Traversal Sección 9.6: Algoritmo de Thorup ........... .................................................... .................................................... ... 41
Capítulo 10: Gráficos transversales .................................................... .................................................... .......................... 43

Sección 10.1: Función transversal de búsqueda en profundidad primero .................................................... ................................................ 43

Capítulo 11: Algoritmo de Dijkstra ............................................... .................................................... .......................... 44

Sección 11.1: Algoritmo de ruta más corta de Dijkstra ........................................... .................................................... .......... 44
Capítulo 12: A* Pathfinding .................................................. .................................................... .............................................
49
Sección 12.1: Introducción a A* .................................................... .................................................... ............................... 49
Sección 12.2: A* Pathfinding a través de un laberinto sin obstáculos .................................................... .......................... 49
Sección 12.3: Resolviendo el problema de 8 acertijos usando el algoritmo A* ........... .................................................... ........ 56
Machine Translated by Google

Capítulo 13: Algoritmo de búsqueda de caminos A* .................................. .................................................... ....................


59
Sección 13.1: Ejemplo simple de A* Pathfinding: Un laberinto sin obstáculos .................................................... ........ 59

Capítulo 14: Programación dinámica .............................................. .................................................... .................... 66


Sección 14.1: Editar distancia .................................................... .................................................... ............................................. 66

Sección 14.2: Algoritmo de programación de trabajos ponderados .................................. .................................................... ..... 66


Sección 14.3: Subsecuencia común más larga .................................................. .................................................... ............ 70
Sección 14.4: Número de Fibonacci .................................................... .................................................... ............................. 71

Sección 14.5: Subcadena común más larga ............................................... .................................................... .................... 72

Capítulo 15: Aplicaciones de la Programación Dinámica ........................................... ..................................... 73


Sección 15.1: Números de Fibonacci .................................................... .................................................... ............................ 73

Capítulo 16: Algoritmo de Kruskal ............................................... .................................................... .......................... 76


Sección 16.1: Implementación óptima basada en conjuntos disjuntos .................................. ............................................... 76
Sección 16.2: Implementación simple, más detallada ........................................... .................................................... ... 77
Sección 16.3: Implementación simple basada en conjuntos disjuntos .................................. ............................................. 77
Sección 16.4: Implementación simple y de alto nivel ........................................... .................................................... .......... 77

Capítulo 17: Algoritmos codiciosos ............................................... .................................................... ............................. 79


Sección 17.1: Codificación de Huÿman ........................................... .................................................... .......................................... 79
Sección 17.2: Problema de selección de actividades .................................................... .................................................... ................ 82

Sección 17.3: Problema de creación de cambios .................................. .................................................... ............................. 84

Capítulo 18: Aplicaciones de la técnica Greedy ........................................... .......................................................... 86

Sección 18.1: Almacenamiento en caché fuera de línea .................................. .................................................... .......................................... 86


Sección 18.2: Boleto automático .................................................... .................................................... .................................... 94

Sección 18.3: Programación de intervalos ............................................. .................................................... ............................... 97


Sección 18.4: Minimización de los retrasos .................................................... .................................................... ........................ 101

Capítulo 19: Algoritmo de Prim ............................................... .................................................... ............................... 105

Sección 19.1: Introducción al algoritmo de Prim ........................................... .................................................... .......... 105

Capítulo 20: Algoritmo de Bellman-Ford ........................................... .................................................... ............... 113

Sección 20.1: Algoritmo de ruta más corta de fuente única (dado que hay un ciclo negativo en un gráfico) .................. 113

Sección 20.2: Detección de ciclos negativos en un gráfico .................................. .................................................... .... 116
Sección 20.3: ¿Por qué necesitamos relajar todos los bordes la mayoría de las veces (V-1)? .................................................... ........ 118

Capítulo 21: Algoritmo de línea ............................................... .................................................... .....................................121


Sección 21.1: Algoritmo de dibujo lineal de Bresenham ........................................... .................................................... ..... 121

Capítulo 22: Algoritmo de Floyd-Warshall ........................................... .................................................... ............. 124

Sección 22.1: Algoritmo de ruta más corta para todos los pares .................................. .................................................... ............ 124

Capítulo 23: Algoritmo del Número Catalán ........................................... .................................................... ......... 127

Sección 23.1: Información básica del algoritmo numérico catalán .................................................... ............................... 127

Capítulo 24: Algoritmos de subprocesos múltiples ....................................... .................................................... .......... 129


Sección 24.1: Multiproceso de multiplicación de matriz cuadrada .................................................... ............................................. 129

Sección 24.2: Multiproceso de vector de matriz de multiplicación .................................................... ........................................ 129


.................................................... .................................................... ..................
Sección 24.3: Multiproceso de combinación y clasificación 129

Capítulo 25: Algoritmo de Knuth Morris Pratt (KMP) ........................................... ............................................. 131
Sección 25.1: Ejemplo de KMP ............................................. .................................................... ........................................ 131

Capítulo 26: Editar algoritmo dinámico de distancia ........................................... ............................................... 133


Sección 26.1: Ediciones mínimas requeridas para convertir la cadena 1 en la cadena 2 .................................................... .................. 133

Capítulo 27: Algoritmos en línea ............................................. .................................................... ............................. 136


Sección 27.1: Paginación (almacenamiento en caché en línea) .................................. .................................................... ........................ 137

Capítulo 28: Ordenar .............................................. .................................................... .................................................. 143


Sección 28.1: Estabilidad en la clasificación ........................................... .................................................... ............................. 143
Machine Translated by Google

Capítulo 29: Clasificación de burbujas .................................................... .................................................... ..................................... 144


Sección 29.1: Clasificación por burbuja .................................................... .................................................... ...................................... 144

Sección 29.2: Implementación en C y C++ Sección .................................................... .................................................... ........... 144

29.3: Implementación en C# Sección 29.4: .................................................... .................................................... ..................... 145

Implementación de Python .............................. .................................................... .................................... 146


Sección 29.5: Implementación en Java Sección .................................................... .................................................... .................. 147

29.6: Implementación en Javascript ...................................... .................................................... ................... 147


Capítulo 30: Clasificación por fusión .................................................... .................................................... ..................................... 149

Sección 30.1: Fundamentos de la ordenación .................................................... .................................................... .......................... 149

por combinación Sección 30.2: Implementación de la ordenación por .................................................... .................................................... 150

combinación en Go Sección 30.3: Implementación de la ordenación por .................................................... .......................................... 150

combinación en C y C# Sección 30.4: Implementación de la ordenación .................................................... ............................................................. 152

por combinación en Java Sección 30.5: Implementación de la ordenación por combinación en Python .......... .................................................... .............................153

Sección 30.6: Implementación de Java de abajo hacia arriba .................................. .................................................... ....... 154

Capítulo 31: Clasificación por .................................................... .................................................... .................................. 156

inserción Sección 31.1: Implementación de Haskell ........................................... .................................................... ............................. 156

Capítulo 32: Clasificación de .................................................... .................................................... ..................................... 157

depósito Sección 32.1: Implementación de C# ........................................... .................................................... .................................... 157


Capítulo 33: Clasificación rápida ............................................... .................................................... ............................................. 158

Sección 33.1: Conceptos básicos de .................................................... .................................................... ............................... 158

Quicksort Sección 33.2: Quicksort en Python ........................................... .................................................... .................................... 160


Sección 33.3: Implementación Java de la partición Lomuto ........................................... .................................................... 160
Capítulo 34: Clasificación de conteo .................................................... .................................................... ............................... 162

Sección 34.1: Información básica de clasificación por .................................................... .................................................... ... 162

conteo Sección 34.2: Implementación de pseudocódigo .................................. .................................................... ..................... 162


Capítulo 35: Ordenar montones .................................................... .................................................... ............................................. 164

Sección 35.1: Implementación de C# ............................................... .................................................... ............................... 164


Sección 35.2: Información básica de la clasificación en .................................................... .................................................... ......... 164
montón Capítulo 36: Clasificación cíclica
.................................................... .................................................... ............................................. 166

Sección 36.1: Implementación de pseudocódigo ............................................. .................................................... ............. 166


Capítulo 37: Clasificación par-impar .................................................... .................................................... ............................... 167
Sección 37.1: Información básica de clasificación par-impar .................................................... .................................................... .. 167
Capítulo 38: Clasificación de selección .................................................... .................................................... ............................... 170

Sección 38.1: Implementación de Elixir ............................................. .................................................... .......................... 170


Sección 38.2: Información básica de clasificación de selección .................................................... .................................................... .. 170

Sección 38.3: Implementación de la ordenación por selección en .................................................... .......................................... 172

C# Capítulo 39: Búsqueda ........................................... .................................................... .................................................... ... 174

Sección 39.1: Búsqueda binaria .................................................... .................................................... .................................... 174

Sección 39.2: Rabin Karp ........................................... .................................................... ................................................ 175

Sección 39.3: Análisis de búsqueda lineal (peor, promedio y mejores casos) .................................. ..................... 176

Sección 39.4: Búsqueda binaria: en números ordenados .................................................... .......................................................... 178


Sección 39.5: Búsqueda lineal .................................................... .................................................... .................................... 178
Capítulo 40: Búsqueda de subcadenas .................................................... .................................................... ....................... 180

Sección 40.1: Introducción al algoritmo Knuth-Morris-Pratt (KMP) .................................. ............................... 180


Sección 40.2: Introducción al Algoritmo de Rabin-Karp ........................................... .................................................... .. 183
Sección 40.3: Implementación de Python del algoritmo KMP ........................................... ............................................. 186
Sección 40.4: Algoritmo KMP en C Capítulo .................................................... .................................................... ........................ 187
41: Búsqueda en amplitud .................................................... .................................................... ................ 190
Machine Translated by Google

Sección 41.1: Encontrar la ruta más corta desde el origen a otros nodos Sección .................................................... ................ 190

41.2: Encontrar la ruta más corta desde el origen en un gráfico 2D ........... ................................................ 196

Sección 41.3: Componentes conectados de un gráfico no dirigido utilizando BFS .................................................... ........... 197

....................................................
Capítulo 42: Búsqueda en profundidad primero Sección 42.1: Introducción a la búsqueda .................................................... ..................... 202

en profundidad primero .................................................... .......................................................... 202

Capítulo 43: Funciones hash Sección .................................................... .................................................... ............................ 207

43.1: Códigos hash para tipos comunes en C# Sección 43.2: .................................................... ............................................. 207
Introducción a las funciones hash .................................................... .................................................... ...... 208

Capítulo 44: Vendedor viajero Sección 44.1: .................................................... .................................................... ................ 210

Algoritmo de fuerza bruta ....................................... .................................................... ............................... 210


Sección 44.2: Algoritmo de programación dinámica ........................................... .................................................... ..... 210
Capítulo 45: Problema de la mochila .................................................... .................................................... ..................... 212

Sección 45.1: Fundamentos del problema de la .................................................... .................................................... .............. 212

mochila Sección 45.2: Solución implementada en C# .................................................... .................................................... .......... 212

Capítulo 46: Resolución de ecuaciones ............................................... .................................................... ............................. 214


Sección 46.1: Ecuación lineal ............................................... .................................................... .................................... 214
Sección 46.2: Ecuación no lineal ............................................... .................................................... ............................. 216

Capítulo 47: Subsecuencia común más larga .................................................. ................................................ 220

Sección 47.1: Explicación de la subsecuencia común más larga ........................................... .......................................... 220

Capítulo 48: Subsecuencia Creciente Máxima .................................................. ............................................. 225

Sección 48.1: Información básica de la subsecuencia creciente más larga .................................................... ..................... 225

Capítulo 49: Comprobar que dos cadenas son anagramas ........................................... ................................................ 228

Sección 49.1: Ejemplo de entrada y salida ........................................... .................................................... ....................... 228


Sección 49.2: Código Genérico para Anagramas ........................................... .................................................... .......... 229

Capítulo 50: Triángulo de Pascal ............................................... .................................................... ............................. 231

Sección 50.1: Triángulo de Pascal en C .................................................... .................................................... ......................... 231

Capítulo 51: Algo: - Imprima la matriz am * n en forma cuadrada


.................................................... .......................... 232

Sección 51.1: Ejemplo de muestra ............................................. .................................................... .................................... 232


Sección 51.2: Escriba el código genérico .................................................... .................................................... ................... 232

Capítulo 52: Exponenciación de matrices ............................................... .................................................... ................... 233

Sección 52.1: Exponenciación de matrices para resolver problemas de ejemplo .................................................... ....................... 233

Capítulo 53: algoritmo polinomial acotado en el tiempo para la cobertura mínima de vértices
........................ 237

Sección 53.1: Algoritmo Pseudo Código .................................................... .................................................... .................. 237

Capítulo 54: Deformación dinámica del tiempo ........................................... .................................................... ................ 238

Sección 54.1: Introducción a la deformación dinámica del tiempo .................................. .......................................... 238
Capítulo 55: Transformada rápida de Fourier
.................................................... .................................................... .......... 242
Sección 55.1: Radix 2 FFT .................................................... .................................................... ...................................... 242
Sección 55.2: Radix 2 FFT inversa .................................................... .................................................... ........................ 247

Apéndice A: Pseudocódigo .................................................... .................................................... .................................... 249


Apartado A.1: Afectaciones variables .................................................... .................................................... ........................ 249
Sección A.2: Funciones .................................................... .................................................... .......................................... 249
Créditos .................................................... .................................................... .................................................... ...................... 250

También te puede interesar.................................................... .................................................... .................................................... 252


Machine Translated by Google

Sobre

No dude en compartir este PDF con cualquier persona de forma


gratuita, la última versión de este libro se puede descargar desde:
https://goalkicker.com/AlgorithmsBook

Este libro Notas de algoritmos para profesionales está compilado a partir de la documentación de
desbordamiento de pila , el contenido está escrito por la hermosa gente de Stack Overflow.
El contenido del texto se publica bajo Creative Commons BY-SA, vea los créditos al final de este libro

que contribuyeron a los diversos capítulos. Las imágenes pueden ser propiedad de sus respectivos
propietarios a menos que se especifique lo contrario

Este es un libro gratuito no oficial creado con fines educativos y no está afiliado con grupos o
compañías oficiales de algoritmos ni con Stack Overflow. Todas las marcas comerciales y marcas
comerciales registradas son propiedad de sus respectivos
dueños de la empresa

No se garantiza que la información presentada en este libro sea correcta ni precisa, utilícela bajo
su propio riesgo.

Envíe comentarios y correcciones a web@petercv.com

GoalKicker.com – Notas de algoritmos para profesionales 1


Machine Translated by Google

Capítulo 1: Introducción a los algoritmos


Sección 1.1: Ejemplo de un problema algorítmico
Un problema algorítmico se especifica describiendo el conjunto completo de instancias en las que debe trabajar y su salida después de ejecutarse en una de estas

instancias. Esta distinción, entre un problema y una instancia de un problema, es fundamental. El problema algorítmico conocido como clasificación se define de

la siguiente manera: [Skiena:2008:ADM:1410219]

Problema: Clasificación

Entrada: Una secuencia de n teclas, a_1, a_2, ..., Salida: El un.


reordenamiento de la secuencia de entrada tal que a'_1 <= a'_2 <= ... <= a'_{n-1} <= a'_n

Una instancia de clasificación podría ser una matriz de cadenas, como { Haskell, Emacs } o una secuencia de números
como { 154, 245, 1337 }.

Sección 1.2: Primeros pasos con el algoritmo simple Fizz Buzz en


Swift
Para aquellos de ustedes que son nuevos en la programación en Swift y aquellos que provienen de diferentes bases de programación, como Python o Java, este

artículo debería ser bastante útil. En esta publicación, discutiremos una solución simple para implementar algoritmos rápidos.

zumbido efervescente

Es posible que haya visto Fizz Buzz escrito como Fizz Buzz, FizzBuzz o Fizz-Buzz; todos se refieren a lo mismo. Esa "cosa" es el principal tema de discusión hoy.

Primero, ¿qué es FizzBuzz?

Esta es una pregunta común que surge en las entrevistas de trabajo.

Imagina una serie de un número del 1 al 10.

1 2 3 4 5 6 7 8 9 10

Fizz y Buzz se refieren a cualquier número que sea múltiplo de 3 y 5 respectivamente. En otras palabras, si un número es divisible por 3, se sustituye por fizz; si un

número es divisible por 5, se sustituye por zumbido. Si un número es simultáneamente un múltiplo de 3 Y 5, el número se reemplaza con "fizz buzz". En esencia,

emula el famoso juego infantil "fizz buzz".

Para trabajar en este problema, abra Xcode para crear un nuevo patio de recreo e inicialice una matriz como la siguiente:

// por ejemplo ,
sea número = [1,2,3,4,5] // aquí
3 es efervescencia y 5 es zumbido

Para encontrar toda la efervescencia y el zumbido, debemos iterar a través de la matriz y verificar qué números son efervescentes y cuáles son zumbidos. Para

hacer esto, crea un bucle for para iterar a través de la matriz que hemos inicializado:

para num en numero {


// El cuerpo y el cálculo van aquí
}

Después de esto, simplemente podemos usar la condición "if else" y el operador de módulo en forma rápida, es decir, -% para ubicar la efervescencia y el zumbido.

GoalKicker.com – Notas de algoritmos para profesionales 2


Machine Translated by Google

for num in number { if


num % 3 == 0 { print("\
(num) fizz") } else
{ print(num)

}
}

¡Excelente! Puede ir a la consola de depuración en el área de juegos de Xcode para ver el resultado. Encontrará que las "efervescencias" se
han ordenado en su matriz.

Para la parte Buzz, usaremos la misma técnica. Probémoslo antes de desplazarnos por el artículo: puede comparar sus resultados con este
artículo una vez que haya terminado de hacer esto.

for num in number { if


num % 3 == 0 { print("\
(num) fizz") } else if num
% 5 == 0 { print("\(num)
buzz") } else { print(num)

}
}

¡Compruebe la salida!

Es bastante sencillo: dividiste el número por 3, fizz y dividiste el número por 5, buzz. Ahora, aumenta los números en la matriz.

sea numero = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]

Aumentamos el rango de números del 1 al 10 al 1 al 15 para demostrar el concepto de "efervescencia". Dado que 15 es un múltiplo de 3 y 5, el
número debe reemplazarse con "fizz buzz". ¡Pruébelo usted mismo y compruebe la respuesta!

Aquí está la solución:

for num in number { if


num % 3 == 0 && num % 5 == 0 { print("\
(num) fizz buzz")
} else if num % 3 == 0 { print("\
(num) fizz") } else if num %
5 == 0 { print("\(num) buzz") }
else { print(num)

}
}

Espera... ¡aunque no ha terminado! Todo el propósito del algoritmo es personalizar el tiempo de ejecución correctamente. Imagínese si el rango
aumenta de 1-15 a 1-100. El compilador verificará cada número para determinar si es divisible por 3 o por 5. Luego, revisará los números
nuevamente para verificar si los números son divisibles por 3 y 5. El código esencialmente tendría que ejecutarse a través de cada número en la
matriz. dos veces: primero tendría que ejecutar los números por 3 y luego ejecutarlos por 5. Para acelerar el proceso, simplemente podemos
decirle a nuestro código que divida los números por 15 directamente.

Aquí está el código final:

para num en numero {

GoalKicker.com – Notas de algoritmos para profesionales 3


Machine Translated by Google

if num % 15 == 0 { print("\
(num) zumbido de efervescencia")
} else if num % 3 == 0 { print("\
(num) fizz") } else if num %
5 == 0 { print("\(num) buzz") }
else { print(num)

}
}

Tan simple como eso, puede usar cualquier idioma de su elección y comenzar

Disfruta de la codificación

GoalKicker.com – Notas de algoritmos para profesionales 4


Machine Translated by Google

Capítulo 2: Complejidad del algoritmo


Sección 2.1: notación Big-Theta
A diferencia de la notación Big-O, que representa solo el límite superior del tiempo de ejecución de algún algoritmo, Big-Theta es un
atado apretado; tanto el límite superior como el inferior. El límite estrecho es más preciso, pero también más difícil de calcular.

La notación Big-Theta es simétrica: f(x) = ÿ(g(x)) <=> g(x) = ÿ(f(x))

Una forma intuitiva de entenderlo es que f(x) = ÿ(g(x)) significa que las gráficas de f(x) y g(x) crecen a la misma velocidad, o
que los gráficos 'se comportan' de manera similar para valores suficientemente grandes de x.

La expresión matemática completa de la notación Big-Theta es la siguiente:


ÿ(f(x)) = {g: N0 -> R y c1, c2, n0 > 0, donde c1 < abs(g(n) / f(n)), para cada n > n0 y abs es el absoluto valor }

Un ejemplo

Si el algoritmo para la entrada n toma 42n^2 + 25n + 4 operaciones para terminar, decimos que es O(n^2), pero también es O(n^3)
y O(n^100). Sin embargo, es ÿ(n^2) y no es ÿ(n^3), ÿ(n^4) etc. El algoritmo que es ÿ(f(n)) también es O(f(n)), pero
¡no viceversa!

Definición matemática formal

ÿ(g(x)) es un conjunto de funciones.

ÿ(g(x)) = {f(x) tal que existen constantes positivas c1, c2, N tales que 0 <= c1*g(x) <= f(x)
<= c2*g(x) para todo x > N}

Debido a que ÿ(g(x)) es un conjunto, podríamos escribir f(x) ÿ ÿ(g(x)) para indicar que f(x) es un miembro de ÿ(g(x)). En cambio, nosotros
normalmente escribirá f(x) = ÿ(g(x)) para expresar la misma noción - esa es la forma común.

Cada vez que aparece ÿ(g(x)) en una fórmula, lo interpretamos como una función anónima que no conocemos.
cuidado de nombrar. Por ejemplo, la ecuación T(n) = T(n/2) + ÿ(n), significa T(n) = T(n/2) + f(n) donde f(n) es un
función en el conjunto ÿ(n).

Sean f y g dos funciones definidas en algún subconjunto de los números reales. Escribimos f(x) = ÿ(g(x)) como
x->infinito si y solo si hay constantes positivas K y L y un número real x0 tal que se cumple:

K|g(x)| <= f(x) <= L|g(x)| para todo x >= x0.

La definición es igual a:

f(x) = O(g(x)) y f(x) = ÿ(g(x))

Un método que utiliza límites

si límite(x->infinito) f(x)/g(x) = c ÿ (0,ÿ) es decir, el límite existe y es positivo, entonces f(x) = ÿ(g(x))

Clases de complejidad comunes

Notación de nombre norte = 10 n = 100


ÿ constante(1) 1 1

Logarítmico ÿ(log(n)) ÿ(n) 3 7

Lineal 10 100

GoalKicker.com – Notas de algoritmos para profesionales 5


Machine Translated by Google

Linearítmico ÿ(n*log(n)) 30 700

Cuadrático ÿ(n^2) 100 10 000

Exponencial ÿ(2^n) 1 024 1.267650e+ 30

Factorial ÿ(n!) 3 628 800 9.332622e+157

Sección 2.2: Comparación de las notaciones asintóticas


Sean f(n) y g(n) dos funciones definidas sobre el conjunto de los números reales positivos, c, c1, c2, n0 son reales positivos
constantes

f(n) = f(n) =
Notación f(n) = O(g(n)) f(n) = ÿ(g(n)) f(n) = ÿ(g(n))
o(g(n)) ÿ(g(n))
ÿc>
ÿc> 0, ÿ
0, ÿ n0 >
n0 > 0 0:ÿ
Formal ÿ c1, c2 > 0, ÿ n0 > 0 : ÿ norte ÿ n0, 0 ÿ c1 g(n) ÿ : ÿ norte norte ÿ
ÿ c > 0, ÿ n0 > 0 : ÿ norte ÿ n0, 0 ÿ f(n) ÿ c g(n) ÿ c > 0, ÿ n0 > 0 : ÿ norte ÿ n0, 0 ÿ c g(n) ÿ f(n)
definición f(n) ÿ c2 g(n) ÿ n0, n0, 0
0ÿ ÿ do

f(n) < g(n)


cg (n) <
f(n)

Analogía
Entre los
asintótico
comparación un ÿ segundo un ÿ segundo un = segundo un < ba > b

de f, g y
numeros reales
un, b
7n^2
5n^2 = =
Ejemplo 7n + 10 = O(n^2 + n - 9) n^3 - 34 = ÿ(10n^2 - 7n + 1) 1/2 n^2 - 7n = ÿ(n^2)
o(n^3)
ÿ(n)

Gráfico
interpretación

Las notaciones asintóticas se pueden representar en un diagrama de Venn de la siguiente manera:

Enlaces

Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein. Introducción a los Algoritmos.

Sección 2.3: Notación Big-Omega


La notación ÿ se utiliza para el límite inferior asintótico.

GoalKicker.com – Notas de algoritmos para profesionales 6


Machine Translated by Google

Definicion formal

Sean f(n) y g(n) dos funciones definidas sobre el conjunto de los números reales positivos. Escribimos f(n) = ÿ(g(n)) si existen
constantes positivas c y n0 tales que:

0 ÿ c g(n) ÿ f(n) para todo n ÿ n0.

notas

f(n) = ÿ(g(n)) significa que f(n) crece asintóticamente no más lento que g(n). También podemos decir sobre ÿ(g(n)) cuando el
análisis del algoritmo no es suficiente para afirmar sobre ÿ(g(n)) o / y O(g(n)).

De las definiciones de notaciones se sigue el teorema:

Para dos funciones cualesquiera f(n) y g(n) tenemos f(n) = ÿ(g(n)) si y solo si f(n) = O(g(n)) y f(n) = ÿ (g(n)).

Gráficamente, la notación ÿ se puede representar de la siguiente manera:

Por ejemplo, tengamos f(n) = 3n^2 + 5n - 4. Entonces f(n) = ÿ(n^2). También es correcto f(n) = ÿ(n), o incluso f(n) = ÿ(1).

Otro ejemplo para resolver el algoritmo de coincidencia perfecta: si el número de vértices es impar, se genera "No hay coincidencia
perfecta", de lo contrario, intente todas las coincidencias posibles.

Nos gustaría decir que el algoritmo requiere un tiempo exponencial pero, de hecho, no puede probar un límite inferior de ÿ (n
^ 2) utilizando la definición habitual de ÿ ya que el algoritmo se ejecuta en tiempo lineal para n impar. En su lugar, deberíamos
definir f(n)=ÿ(g(n)) diciendo que para alguna constante c>0, f(n)ÿ c g(n) para una cantidad infinita de n. Esto da una buena
correspondencia entre los límites superior e inferior: f(n)=ÿ(g(n)) iff f(n) != o(g(n)).

Referencias

La definición formal y el teorema están tomados del libro "Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein. Introducción
a los algoritmos".

GoalKicker.com – Notas de algoritmos para profesionales 7


Machine Translated by Google

Capítulo 3: Notación Big-O


Definición

La notación Big-O es, en esencia, una notación matemática, utilizada para comparar la tasa de convergencia de funciones.
Sean n -> f(n) y n -> g(n) funciones definidas sobre los números naturales. Entonces decimos que f = O(g) si y sólo si
f(n)/g(n) está acotado cuando n tiende a infinito. En otras palabras, f = O(g) si y solo si existe una constante A, tal que
para todo n, f(n)/g(n) <= A.

En realidad, el alcance de la notación Big-O es un poco más amplio en matemáticas, pero por simplicidad lo he reducido a lo que se usa en el
análisis de complejidad de algoritmos: funciones definidas en los naturales, que tienen valores distintos de cero, y el caso de n creciente hasta el
infinito.

Qué significa ?

Tomemos el caso de f(n) = 100n^2 + 10n + 1 y g(n) = n^2. Está bastante claro que ambas funciones tienden a infinito cuando
n tiende a infinito. Pero a veces conocer el límite no es suficiente, y también queremos saber la velocidad a la que las funciones
se acercan a su límite. Nociones como Big-O ayudan a comparar y clasificar funciones por su velocidad de
convergencia.

Averigüemos si f = O(g) aplicando la definición. Tenemos f(n)/g(n) = 100 + 10/n + 1/n^2. Como 10/n es 10 cuando n es 1 y
es decreciente, y como 1/n^2 es 1 cuando n es 1 y también es decreciente, tenemos ÿf(n)/g(n) <= 100 + 10 + 1 = 111. La
definición se cumple porque hemos encontrado un límite de f(n)/g(n) (111) y entonces f = O(g) (decimos que f es un Big-O de
n^2).

Esto significa que f tiende al infinito aproximadamente a la misma velocidad que g. Ahora bien, esto puede parecer algo extraño de decir, porque
lo que hemos encontrado es que f es como máximo 111 veces más grande que g, o en otras palabras, cuando g crece en 1, f crece como máximo
en 111. Puede parecer que al crecer 111 veces más rápido no es "aproximadamente la misma velocidad". Y, de hecho, la notación Big-O no es una
forma muy precisa de clasificar la velocidad de convergencia de funciones, razón por la cual en matemáticas usamos la relación de equivalencia
cuando queremos una estimación precisa de la velocidad. Pero a los efectos de separar algoritmos en clases de gran velocidad, Big-O es suficiente.
No necesitamos separar funciones que crecen un número fijo de veces más rápido que otras, sino solo funciones que crecen infinitamente más
rápido que otras.
Por ejemplo, si tomamos h(n) = n^2*log(n), vemos que h(n)/g(n) = log(n) que tiende a infinito con n, por lo que h no es O(n^ 2), porque h crece
infinitamente más rápido que n^2.

Ahora necesito hacer una nota al margen: es posible que haya notado que si f = O (g) y g = O (h), entonces f = O (h).
Por ejemplo, en nuestro caso, tenemos f = O(n^3) y f = O(n^4)... En el análisis de complejidad de algoritmos, con frecuencia
decimos f = O(g) para indicar que f = O( g) y g = O(f), que puede entenderse como "g es el Big-O más pequeño para f". En
matemáticas decimos que tales funciones son Big-Thetas entre sí.

Cómo se usa ?

Al comparar el rendimiento de un algoritmo, nos interesa el número de operaciones que realiza un algoritmo. Esto se llama complejidad
temporal. En este modelo, consideramos que cada operación básica (suma, multiplicación, comparación, asignación, etc.) toma una
cantidad fija de tiempo y contamos el número de tales operaciones. Por lo general, podemos expresar este número como una función del
tamaño de la entrada, que llamamos n. Y, lamentablemente, este número suele crecer hasta el infinito con n (si no es así, decimos que el
algoritmo es O(1)). Separamos nuestros algoritmos en clases de gran velocidad definidas por Big-O: cuando hablamos de un "algoritmo
O(n^2)", queremos decir que el número de operaciones que realiza, expresado en función de n, es un O( n^2). Esto dice que nuestro algoritmo
es aproximadamente tan rápido como un algoritmo que haría un número de operaciones igual al cuadrado del tamaño de su entrada, o más
rápido. La parte "o más rápida" está ahí porque usé Big-O en lugar de Big-Theta, pero generalmente la gente dice Big-O para referirse a Big-
Theta.

GoalKicker.com – Notas de algoritmos para profesionales 8


Machine Translated by Google

Cuando contamos operaciones, solemos considerar el peor de los casos: por ejemplo, si tenemos un bucle que puede ejecutarse como
máximo n veces y que contiene 5 operaciones, el número de operaciones que contamos es 5n. También es posible considerar la
complejidad promedio del caso.

Nota rápida: un algoritmo rápido es aquel que realiza pocas operaciones, por lo que si el número de operaciones crece hasta el infinito
más rápido, entonces el algoritmo es más lento: O(n) es mejor que O(n^2).

A veces también estamos interesados en la complejidad espacial de nuestro algoritmo. Para ello consideramos el número de bytes
en memoria ocupado por el algoritmo en función del tamaño de la entrada, y usamos Big-O de la misma manera.

Sección 3.1: Un bucle simple


La siguiente función encuentra el elemento máximo en una matriz:

int find_max(const int *array, size_t len) { int max = INT_MIN;


for (size_t i = 0; i < len; i++) { if (max < array[i]) { max =
array[i];

} retorno máximo;
}

El tamaño de entrada es el tamaño de la matriz, que llamé len en el código.

Contemos las operaciones.

int máx = INT_MIN;


tamaño_t i = 0;

Estas dos asignaciones se realizan solo una vez, por lo que son 2 operaciones. Las operaciones que se repiten son:

si (max < array[i]) i++;


máx = matriz[i]

Dado que hay 3 operaciones en el ciclo, y el ciclo se realiza n veces, agregamos 3n a nuestras 2 operaciones ya existentes para
obtener 3n + 2. Entonces, nuestra función requiere 3n + 2 operaciones para encontrar el máximo (su complejidad es 3n + 2). Este es un
polinomio donde el término de más rápido crecimiento es un factor de n, por lo que es O(n).

Probablemente habrás notado que "operación" no está muy bien definida. Por ejemplo, dije que si (max < array[i]) fuera una operación,
pero dependiendo de la arquitectura, esta declaración puede compilarse, por ejemplo, en tres instrucciones: una lectura de memoria, una
comparación y una rama. También he considerado todas las operaciones como iguales, aunque, por ejemplo, las operaciones de memoria
serán más lentas que las demás, y su rendimiento variará enormemente debido, por ejemplo, a los efectos de caché. También he ignorado
por completo la declaración de retorno, el hecho de que se creará un marco para la función, etc. Al final, no importa para el análisis de
complejidad, porque sea cual sea la forma que elija para contar las operaciones, solo cambiará el coeficiente. del factor n y la constante, por
lo que el resultado seguirá siendo O(n).
La complejidad muestra cómo el algoritmo escala con el tamaño de la entrada, ¡pero no es el único aspecto del rendimiento!

Sección 3.2: Un bucle anidado


La siguiente función verifica si una matriz tiene duplicados tomando cada elemento, luego iterando sobre toda la matriz para ver si el
elemento está allí

GoalKicker.com – Notas de algoritmos para profesionales 9


Machine Translated by Google

_Bool contiene_duplicados(const int *array, size_t len) {


for (int i = 0; i < len - 1; i++) { for (int j = 0; j
< len; j++) { if (i != j && array[i] ==
array[j]) { return 1;

}
}

} devuelve 0;
}

El ciclo interno realiza en cada iteración un número de operaciones que es constante con n. El ciclo externo también realiza
algunas operaciones constantes y ejecuta el ciclo interno n veces. El bucle exterior en sí se ejecuta n veces. Entonces, las
operaciones dentro del ciclo interno se ejecutan n ^ 2 veces, las operaciones en el ciclo externo se ejecutan n veces y la
asignación a i se realiza una vez. Por lo tanto, la complejidad será algo así como an^2 + bn + c, y dado que el término más alto
es n^2, la notación O es O(n^2).

Como habrás notado, podemos mejorar el algoritmo evitando hacer las mismas comparaciones varias veces.
Podemos comenzar desde i + 1 en el bucle interno, porque todos los elementos anteriores ya se habrán verificado con todos
los elementos de la matriz, incluido el del índice i + 1. Esto nos permite descartar la verificación i == j .

_Bool más rápido_contiene_duplicados(const int *array, size_t len) {


para (int i = 0; i < len - 1; i++) {
for (int j = i + 1; j < len; j++) { if (matriz[i] ==
matriz[j]) { return 1;

}
}

} devuelve 0;
}

Obviamente, esta segunda versión hace menos operaciones y por lo tanto es más eficiente. ¿Cómo se traduce eso a la
... 1polinomio
notación Big-O? Bien, ahora el cuerpo del bucle interno se ejecuta + 2 + + n -de
1 segundo
= n(n-1)/2grado,
veces.por
Este
lo que
sigue
sigue
siendo
siendo
un solo O
(n ^ 2). Claramente hemos reducido la complejidad, ya que dividimos aproximadamente por 2 el número de operaciones que
estamos haciendo, pero todavía estamos en la misma clase de complejidad definida por Big-O. Para bajar la complejidad a una
clase más baja necesitaríamos dividir el número de operaciones por algo que tiende a infinito con n.

Sección 3.3: Tipos de algoritmos O(log n)


Digamos que tenemos un problema de tamaño n. Ahora, para cada paso de nuestro algoritmo (que necesitamos escribir), nuestro problema
original se convierte en la mitad de su tamaño anterior (n/2).

Entonces, en cada paso, nuestro problema se convierte en la mitad.

Paso Problema
1 n/2
2 n/4
3 n/8
4 n/16

Cuando el espacio del problema se reduce (es decir, se resuelve por completo), no se puede reducir más (n se vuelve igual a 1) después de
salir de la condición de verificación.

GoalKicker.com – Notas de algoritmos para profesionales 10


Machine Translated by Google

1. Digamos en el k-ésimo paso o número de operaciones:

tamaño del problema = 1

2. Pero sabemos que en el k-ésimo paso, el tamaño de nuestro problema debería ser:

tamaño del problema = n/2k

3. De 1 y 2:

n/2k = 1 o

norte = 2k

4. Tome registro en ambos lados

loge n = k loge2

k = loge n / loge 2

5. Usando la fórmula logx m / logx n = logn m

k = log2 norte

o simplemente k = log n

Ahora sabemos que nuestro algoritmo puede ejecutarse al máximo hasta log n, por lo tanto, la complejidad del tiempo se presenta como

O (registro n)

Un ejemplo muy simple en el código para admitir el texto anterior es:

para(int i=1; i<=n; i=i*2) {

// realizar alguna operación


}

Entonces, si alguien le pregunta si n es 256, cuántos pasos ejecutará ese bucle (o cualquier otro algoritmo que reduzca el tamaño del problema a
la mitad), puede calcularlo fácilmente.

k = log2 256

k = log2 2 8 ( => logaa = 1)

k=8

Otro muy buen ejemplo para un caso similar es el algoritmo de búsqueda binaria.

GoalKicker.com – Notas de algoritmos para profesionales 11


Machine Translated by Google

int bSearch(int arr[],int size,int item){


int bajo=0;
int alto=tamaño-1;

while(bajo<=alto)
{ medio=bajo+(alto-bajo)/2;
if(arr[mid]==item) return
mid; else
if(arr[mid]<item) low=mid+1;
más alto = medio-1; }

return –1; // Resultado fallido


}

Sección 3.4: Un ejemplo de O(log n)


Introducción

Considere el siguiente problema:

L es una lista ordenada que contiene n enteros con signo ( siendo n lo suficientemente grande), por ejemplo [-5, -2, -1, 0, 1, 2, 4] (aquí, n
tiene un valor de 7). Si se sabe que L contiene el entero 0, ¿cómo puede encontrar el índice de 0?

enfoque ingenuo

Lo primero que viene a la mente es simplemente leer cada índice hasta encontrar 0. En el peor de los casos, el número de
operaciones es n, por lo que la complejidad es O(n).

Esto funciona bien para valores pequeños de n, pero ¿hay alguna forma más eficiente?

Dicotomía

Considere el siguiente algoritmo (Python3):

un = 0
b = n-1
mientras que es cierto:

h = (a+b)//2 ## // es la división entera, entonces h es un entero


si L[h] == 0:
volver h
elif L[h] > 0:
segundo = h

elif L[h] < 0:


un = h

a y b son los índices entre los cuales se encuentra 0. Cada vez que ingresamos al ciclo, usamos un índice entre un
yb y utilícelo para reducir el área de búsqueda.

En el peor de los casos, tenemos que esperar hasta que a y b sean iguales. Pero, ¿cuántas operaciones requiere eso? No n, porque
cada vez que entramos en el bucle, dividimos la distancia entre ayb por aproximadamente dos. Más bien, la complejidad es O(log
norte).

Explicación

Nota: Cuando escribimos "log", nos referimos al logaritmo binario, o log base 2 (que escribiremos "log_2"). Como O(log_2 n) = O(log
n) (usted puede hacer los cálculos) usaremos "log" en lugar de "log_2".

GoalKicker.com – Notas de algoritmos para profesionales 12


Machine Translated by Google

Llamemos x al número de operaciones: sabemos que 1 = n / (2^x).

Entonces 2^x = n, entonces x = log n

Conclusión

Ante divisiones sucesivas (ya sea por dos o por cualquier número), recuerda que la complejidad es logarítmica.

GoalKicker.com – Notas de algoritmos para profesionales 13


Machine Translated by Google

Capítulo 4: Árboles

Sección 4.1: Representación típica de un árbol anario


Por lo general, representamos un árbol anario (uno con hijos potencialmente ilimitados por nodo) como un árbol binario (uno con exactamente
dos hijos por nodo). El "próximo" hijo es considerado como un hermano. Tenga en cuenta que si un árbol es binario, esta representación crea
nodos adicionales.

Luego iteramos sobre los hermanos y recursimos a los hijos. Como la mayoría de los árboles son relativamente poco profundos (muchos
hijos pero solo unos pocos niveles de jerarquía), esto da lugar a un código eficiente. Tenga en cuenta que las genealogías humanas
son una excepción (muchos niveles de ancestros, solo unos pocos hijos por nivel).

Si es necesario, se pueden mantener punteros traseros para permitir que se ascienda el árbol. Estos son más difíciles de mantener.

Tenga en cuenta que es típico tener una función para llamar a la raíz y una función recursiva con parámetros adicionales, en este caso, la
profundidad del árbol.

nodo de estructura

{
nodo de estructura
*siguiente; nodo de
estructura * hijo; std::cadena de datos;
}

void printtree_r(struct nodo *nodo, int profundidad) {

ent yo;

mientras (nodo)
{
if(nodo->hijo) {

for(i=0;i<profundidad*3;i++)
printf(" "); printf("{\n"):
printtree_r(nodo->hijo,
profundidad +1); for(i=0;i<profundidad*3;i++)
printf(" "); imprimirf("{\n"):

for(i=0;i<profundidad*3;i++)
printf(" "); printf("%s\n",
nodo->datos.c_str());

nodo = nodo->siguiente;
}
}
}

void printtree(nodo *raíz) {

printtree_r(raíz, 0);
}

Sección 4.2: Introducción


Árboles son un subtipo de la estructura de datos de gráfico de borde de nodo más general.

GoalKicker.com – Notas de algoritmos para profesionales 14


Machine Translated by Google

Para ser un árbol, un grafo debe cumplir dos requisitos:

es acíclico. No contiene ciclos (o "bucles").


Esta conectado. Para cualquier nodo dado en el gráfico, todos los nodos son accesibles. Todos los nodos son accesibles a través
de una ruta en el gráfico.

La estructura de datos de árbol es bastante común dentro de la informática. Los árboles se utilizan para modelar muchas estructuras
de datos algorítmicos diferentes, como árboles binarios ordinarios, árboles rojo-negro, árboles B, árboles AB, árboles 23, Heap e intentos.

es común referirse a un árbol como un árbol enraizado por:

eligiendo 1 celda para llamarla `Raíz`


pintando la `Raíz` en la parte superior
creando una capa inferior para cada celda en el gráfico dependiendo de su distancia desde la raíz -cuanto mayor
sea la distancia, más bajas serán las celdas (ejemplo anterior)

símbolo común para los árboles: T

Sección 4.3: Para verificar si dos árboles binarios son iguales o no


1. Por ejemplo, si las entradas son:

Ejemplo 1

a)

GoalKicker.com – Notas de algoritmos para profesionales 15


Machine Translated by Google

b)

La salida debe ser verdadera.

Ejemplo:2

Si las entradas son:

a)

b)

La salida debe ser falsa.

Pseudocódigo para el mismo:

booleano mismoÁrbol(nodo raíz1, nodo raíz2){

GoalKicker.com – Notas de algoritmos para profesionales dieciséis


Machine Translated by Google

if(root1 == NULL && root2 == NULL) devuelve


verdadero;

if(root1 == NULL || root2 == NULL) return false;

if(root1->data == root2->data &&


sameTree(root1->left,root2->left)
&& mismoÁrbol(raíz1->derecha, raíz2->derecha)) devuelve
verdadero;

GoalKicker.com – Notas de algoritmos para profesionales 17


Machine Translated by Google

Capítulo 5: Árboles de búsqueda binarios


El árbol binario es un árbol en el que cada nodo tiene un máximo de dos hijos. El árbol de búsqueda binaria (BST) es un árbol binario cuyos elementos
se colocan en un orden especial. En cada BST, todos los valores (es decir, clave) en el subárbol izquierdo son menores que los valores en el subárbol
derecho.

Sección 5.1: Árbol de búsqueda binario - Inserción (Python)


Esta es una implementación simple de la inserción de árbol de búsqueda binaria usando Python.

A continuación se muestra un ejemplo:

Siguiendo el fragmento de código, cada imagen muestra la visualización de la ejecución, lo que facilita la visualización de cómo funciona este código.

nodo de clase :
def __init__(self, val): self.l_child =
Ninguno self.r_child =
Ninguno self.data = val

def insertar (raíz, nodo): si la raíz


es Ninguno:
raíz = nodo
más: si raíz.datos >
nodo.datos:
si root.l_child es Ninguno:
root.l_child = nodo
más:
insertar (root.l_child, nodo)
más:
si root.r_child es Ninguno:

GoalKicker.com – Notas de algoritmos para profesionales 18


Machine Translated by Google
root.r_child = nodo
más:
insertar (root.r_child, nodo)

def in_order_print(root): si no es
root:
return
in_order_print(root.l_child) print
root.data in_order_print (root.r_child)

def pre_order_print(root): si no es
root:
return
print root.data
pre_order_print(root.l_child)
pre_order_print(root.r_child)

GoalKicker.com – Notas de algoritmos para profesionales 19


Machine Translated by Google

Sección 5.2: Árbol de búsqueda binaria - Eliminación (C++)

Antes de comenzar con la eliminación, solo quiero aclarar qué es un árbol de búsqueda binario (BST). Cada nodo en un BST puede
tener un máximo de dos nodos (hijo izquierdo y derecho). El subárbol izquierdo de un nodo tiene un clave menor o igual que la clave
de su nodo principal. El subárbol derecho de un nodo tiene una clave mayor que la clave de su nodo principal.

Eliminar un nodo en un árbol manteniendo su propiedad de árbol de búsqueda binaria.

Hay tres casos a considerar al eliminar un nodo.

Caso 1: El nodo a eliminar es el nodo hoja. (Nodo con valor 22).


Caso 2: el nodo a eliminar tiene un hijo (nodo con valor 26).
Caso 3: El nodo a eliminar tiene ambos hijos. (Nodo con valor 49).

Explicación de casos:

1. Cuando el nodo a eliminar es un nodo hoja, simplemente elimine el nodo y pase nullptr a su nodo principal.
2. Cuando un nodo que se va a eliminar solo tiene un hijo, copie el valor del hijo en el valor del nodo y elimine el hijo
(Convertido al caso 1)
3. Cuando un nodo que se va a eliminar tiene dos hijos, el mínimo de su subárbol derecho se puede copiar al nodo y luego el
valor mínimo se puede eliminar del subárbol derecho del nodo (Convertido al Caso 2)

Nota: El mínimo en el subárbol derecho puede tener un máximo de un hijo y ese hijo también derecho si tiene el hijo izquierdo, eso
significa que no es el valor mínimo o que no sigue la propiedad BST.

GoalKicker.com – Notas de algoritmos para profesionales 20


Machine Translated by Google

La estructura de un nodo en un árbol y el código para la Eliminación:

nodo de estructura

{
datos int ;
nodo *izquierda, *derecha;
};

nodo* eliminar_nodo(nodo *raíz, datos int ) {

if(root == nullptr) return root; else if(datos


< raíz->datos) raíz->izquierda = delete_node(raíz->izquierda, datos); else if(datos > raíz->datos)
raíz->derecha = delete_node(raíz->derecha, datos);

más
{
if(root->left == nullptr && root->right == nullptr) // Caso 1 {

libre (raíz);
raíz = punto nulo;

} else if(root->left == nullptr) { // Caso 2

nodo* temporal = raíz;


raíz= raíz->derecha;
libre (temporario);

} else if(root->right == nullptr) { // Caso 2

nodo* temporal = raíz;


raíz = raíz->izquierda;
libre (temporario);

} // Caso 3
más {
nodo* temp = root->right;

while(temp->left != nullptr) temp = temp->left;

raíz->datos = temp->datos; root-


>right = delete_node(root->right, temp->data);
}

} devuelve la raíz;
}

La complejidad temporal del código anterior es O(h), donde h es la altura del árbol.

Sección 5.3: Antepasado común más bajo en un BST

Considere el BST:

GoalKicker.com – Notas de algoritmos para profesionales 21


Machine Translated by Google

El antepasado común más bajo de 22 y 26 es 24

El antepasado común más bajo de 26 y 49 es 46

El antepasado común más bajo de 22 y 24 es 24

La propiedad del árbol de búsqueda binaria se puede usar para encontrar el ancestro más bajo de los nodos

Pseudocódigo:

antepasado común más bajo (raíz, nodo1, nodo2){

si (raíz == NULL)
devuelve NULL;

else if(nodo1->datos == raíz->datos || nodo2->datos== raíz->datos)


devolver raíz;

else if((nodo1->datos <= raíz->datos && nodo2->datos > raíz->datos)


|| (nodo2->datos <= raíz->datos && nodo1->datos > raíz->datos)){

devolver raíz;
}

else if(raíz->datos > max(nodo1->datos,nodo2->datos)){


return lowerCommonAncestor(root->left, node1, node2);
}

else
{ return lowerCommonAncestor(root->right, node1, node2);
}
}

Sección 5.4: Árbol de búsqueda binaria - Python


clase Nodo(objeto): def
__init__(self, val): self.l_child =
Ninguno self.r_child =
Ninguno self.val = val

clase BinarySearchTree (objeto):


def insert(auto, raíz, nodo):

GoalKicker.com – Notas de algoritmos para profesionales 22


Machine Translated by Google
si la raíz es Ninguno:
nodo de retorno

si raíz.val < nodo.val:


root.r_child = self.insert(root.r_child, node) else: root.l_child =
self.insert(root.l_child, node)

devolver la raíz

def in_order_place(self, root): si no es root:

volver Ninguno
más:
self.in_order_place(root.l_child) print root.val
self.in_order_place(root.r_child)

def pre_order_place(self, root): si no es root:


devuelve Ninguno más:

print root.val
self.pre_order_place(root.l_child)
self.pre_order_place(root.r_child)

def post_order_place(self, root): si no es root:

volver Ninguno
más:
self.post_order_place(root.l_child)
self.post_order_place(root.r_child) print root.val

""" Crear un nodo diferente e insertar datos en él"""

r = Nodo(3) nodo
= BinarySearchTree() nodeList = [1, 8,
5, 12, 14, 6, 15, 7, 16, 8]

para nd en nodeList:
nodo.insertar(r, Nodo(nd))

print "------En orden ---------" print


(node.in_order_place(r)) print "------Pre order
---------" print (node.pre_order_place(r)) print "------
Post order ---------" print (node.post_order_place(r))

GoalKicker.com – Notas de algoritmos para profesionales 23


Machine Translated by Google

Capítulo 6: Comprobar si un árbol es BST o no

Sección 6.1: Algoritmo para verificar si un árbol binario dado es BST


Un árbol binario es BST si cumple cualquiera de las siguientes condiciones:

1. Está vacío 2.
No tiene subárboles

3. Para cada nodo x en el árbol, todas las claves (si las hay) en el subárbol izquierdo deben ser menores que la clave (x) y todas las claves (si
las hay) en el subárbol derecho deben ser mayores que la clave (x) .

Entonces, un algoritmo recursivo directo sería:

is_BST(raíz): si
raíz == NULL:
volver verdadero

// Comprobar valores en el subárbol izquierdo


si root->left != NULL:
max_key_in_left = find_max_key(root->left) if max_key_in_left
> root- >key: devuelve falso

// Comprobar valores en el subárbol derecho


si raíz->derecha != NULL: min_key_in_right =
find_min_key(root->right) if min_key_in_right < raíz->clave: return
false

return is_BST(raíz->izquierda) && is_BST(raíz->derecha)

El algoritmo recursivo anterior es correcto pero ineficiente, porque atraviesa cada nodo varias veces.

Otro enfoque para minimizar las visitas múltiples de cada nodo es recordar los valores mínimos y máximos posibles de las claves en el subárbol
que estamos visitando. Deje que el valor mínimo posible de cualquier clave sea K_MIN y el valor máximo sea K_MAX. Cuando empezamos desde
la raíz del árbol, el rango de valores en el árbol es [K_MIN,K_MAX]. Sea x la clave del nodo raíz. Entonces el rango de valores en el subárbol
izquierdo es [K_MIN,x) y el rango de valores en el subárbol derecho es (x,K_MAX]. Usaremos esta idea para desarrollar un algoritmo más eficiente.

is_BST(raíz, min, max): si raíz ==


NULL:
volver verdadero

// ¿La clave del nodo actual está fuera de rango? si raíz-


>clave < min || root->key > max: devuelve falso

// comprobar si el subárbol izquierdo y derecho es BST


return is_BST(root->left,min,root->key-1) && is_BST(root->right,root->key+1,max)

Se llamará inicialmente como:

is_BST(my_tree_root,KEY_MIN,KEY_MAX)

Otro enfoque será hacer un recorrido en orden del árbol binario. Si el recorrido en orden produce una secuencia ordenada de claves,
entonces el árbol dado es un BST. Para verificar si la secuencia en orden está ordenada, recuerde el valor de

GoalKicker.com – Notas de algoritmos para profesionales 24


Machine Translated by Google

nodo visitado anteriormente y compararlo con el nodo actual.

Sección 6.2: Si un árbol de entrada dado sigue la propiedad del árbol


de búsqueda binaria o no
Por ejemplo

si la entrada es:

La salida debe ser falsa:

Como 4 en el subárbol izquierdo es mayor que el valor raíz (3)

Si la entrada es:

La salida debe ser verdadera

GoalKicker.com – Notas de algoritmos para profesionales 25


Machine Translated by Google

Capítulo 7: Recorridos de árboles binarios


Visitar un nodo de un árbol binario en un orden particular se llama recorridos.

Sección 7.1: Travesía de Orden de Nivel - Implementación


Por ejemplo, si el árbol dado es:

El recorrido del orden de nivel será

1234567

Impresión de datos de nodos nivel por nivel.

Código:

#include<iostream>
#include<cola>
#include<malloc.h>

utilizando el espacio de nombres estándar;

nodo de estructura{

datos int ;
nodo *izquierda;
nodo *derecho;
};

void levelOrder(estructura nodo *raíz){

si (raíz == NULL) devolver;

cola<nodo *> Q;
Q.push(raíz);

while(!Q.vacío()){
estructura nodo* curr = Q.front(); cout<<
actual->datos <<" "; if(curr->left != NULL)
Q.push(curr-> left);
if(curr->right != NULL) Q.push(curr-> right);

Q.pop();

GoalKicker.com – Notas de algoritmos para profesionales 26


Machine Translated by Google

} struct nodo* nuevoNodo(int datos) {

nodo de estructura* nodo = (nodo de estructura*)


malloc(tamañode( nodo de estructura));
nodo->datos = datos;
nodo->izquierda = NULL;
nodo->derecho = NULL;

retorno (nodo);
}

int principal(){

struct nodo *root = newNode(1); raíz-


>izquierda = nuevoNodo(2); raíz->derecha
= nuevoNodo(3);
raíz->izquierda->izquierdaraíz->izquierda-
= newNode(4);
>derecha = newNode(5); root->right->left =
newNode(6); root->right->right = newNode(7);

printf("El recorrido del orden de niveles del árbol binario es \n");


nivelOrden(raíz);

devolver 0;

La estructura de datos de la cola se utiliza para lograr el objetivo anterior.

Sección 7.2: Recorrido de un árbol binario en preorden, en


orden y post orden
Considere el árbol binario:

El recorrido de pedido previo (raíz) atraviesa el nodo, luego el subárbol izquierdo del nodo y luego el subárbol derecho del nodo.

Entonces, el recorrido de pre-pedido del árbol anterior será:

1245367

El recorrido en orden (raíz) está recorriendo el subárbol izquierdo del nodo, luego el nodo y luego el subárbol derecho del nodo.

GoalKicker.com – Notas de algoritmos para profesionales 27


Machine Translated by Google

nodo.

Entonces, el recorrido en orden del árbol anterior será:

4251637

El recorrido posterior al pedido (raíz) atraviesa el subárbol izquierdo del nodo, luego el subárbol derecho y luego el nodo.

Entonces, el recorrido posterior al pedido del árbol anterior será:

4526731

GoalKicker.com – Notas de algoritmos para profesionales 28


Machine Translated by Google

Capítulo 8: El antepasado común más bajo de un


Árbol binario
El ancestro común más bajo entre dos nodos n1 y n2 se define como el nodo más bajo en el árbol que tiene ambos n1
y n2 como descendientes.

Sección 8.1: Encontrar el ancestro común más bajo


Considere el árbol:

El antepasado común más bajo de los nodos con valor 1 y 4 es 2

El antepasado común más bajo de los nodos con valor 1 y 5 es 3

El antepasado común más bajo de los nodos con valor 2 y 4 es 4

El antepasado común más bajo de los nodos con valor 1 y 2 es 2

GoalKicker.com – Notas de algoritmos para profesionales 29


Machine Translated by Google

Capítulo 9: Gráfico
Un gráfico es una colección de puntos y líneas que conectan algún subconjunto (posiblemente vacío) de ellos. Los puntos de un gráfico se denominan vértices

del gráfico, "nodos" o simplemente "puntos". De manera similar, las líneas que conectan los vértices de un gráfico se denominan bordes de gráfico, "arcos" o

"líneas".

Un grafo G se puede definir como un par (V,E), donde V es un conjunto de vértices y E es un conjunto de aristas entre los vértices E ÿ {(u,v) | u, v ÿ V}.

Sección 9.1: Almacenamiento de gráficos (matriz de adyacencia)


Para almacenar un gráfico, dos métodos son comunes:

Matriz de adyacencia

Lista de adyacencia

Una matriz de adyacencia es una matriz cuadrada utilizada para representar un gráfico finito. Los elementos de la matriz indican si los pares de

vértices son adyacentes o no en el gráfico.

Adyacente significa 'junto a o contiguo a otra cosa' o estar al lado de algo. Por ejemplo, sus vecinos están junto a usted. En la teoría de grafos, si podemos ir

al nodo B desde el nodo A, podemos decir que el nodo B es adyacente al nodo A. Ahora aprenderemos cómo almacenar qué nodos son adyacentes a cuál

a través de la matriz de adyacencia. Esto significa que representaremos qué nodos comparten borde entre ellos. Aquí matriz significa matriz 2D.

Aquí puedes ver una tabla al lado del gráfico, esta es nuestra matriz de adyacencia. Aquí Matrix[i][j] = 1 representa que hay un borde entre i y j. Si no hay

borde, simplemente ponemos Matrix[i][j] = 0.

Estos bordes se pueden ponderar, como puede representar la distancia entre dos ciudades. Luego pondremos el valor en Matrix[i][j] en lugar de poner 1.

El gráfico descrito anteriormente es bidireccional o no dirigido, lo que significa que si podemos ir al nodo 1 desde el nodo 2, también podemos ir al nodo 2

desde el nodo 1. Si el gráfico fuera Dirigido, habría un signo de flecha en uno lado del gráfico. Incluso entonces, podríamos representarlo usando una matriz

de adyacencia.

GoalKicker.com – Notas de algoritmos para profesionales 30


Machine Translated by Google

Representamos los nodos que no comparten borde por infinito. Una cosa a tener en cuenta es que, si el gráfico no está dirigido, la matriz se vuelve
simétrica.

El pseudocódigo para crear la matriz:

Procedimiento Matriz de adyacencia (N): // N representa el número de nodos


Matriz[N][N]
para i de 1 a N para j
de 1 a N
Tomar entrada -> Matrix[i][j] endfor
endfor

También podemos poblar Matrix usando esta forma común:

Procedimiento Matriz de adyacencia (N, E): // N -> número de nodos


Matriz[N][E] // E -> numero de aristas
para i de 1 a E
entrada -> n1, n2, costo
Matriz[n1][n2] = costo
Matriz[n2][n1] = coste final
para

Para gráficos dirigidos, podemos eliminar Matriz[n2][n1] = línea de costo.

Los inconvenientes de usar Adjacency Matrix:

La memoria es un gran problema. No importa cuántos bordes haya, siempre necesitaremos una matriz de tamaño N * N donde N es el número de
nodos. Si hay 10000 nodos, el tamaño de la matriz será 4 * 10000 * 10000 alrededor de 381 megabytes.
Esta es una gran pérdida de memoria si consideramos gráficos que tienen algunos bordes.

Supongamos que queremos saber a qué nodo podemos ir desde un nodo u. Tendremos que comprobar toda la fila de u, lo que cuesta mucho tiempo.

El único beneficio es que podemos encontrar fácilmente la conexión entre los nodos uv y su costo usando Adjacency Matrix.

Código Java implementado usando el pseudocódigo anterior:

importar java.util.Scanner;

clase pública Represent_Graph_Adjacency_Matrix {

vértices int finales privados ;

GoalKicker.com – Notas de algoritmos para profesionales 31


Machine Translated by Google

privado int[][] adjacency_matrix;

public Represent_Graph_Adjacency_Matrix(int v) {

vértices = v;
matriz_adyacencia = new int[vértices + 1][vértices + 1];
}

public void makeEdge(int to, int from, int edge) {

prueba

matriz_adyacencia[a][desde] = borde;

} captura (índice ArrayIndexOutOfBoundsException ) {

System.out.println("Los vértices no existen");


}
}

public int getEdge(int a, int desde) {

prueba

return matriz_adyacencia[a][desde];

} captura (índice ArrayIndexOutOfBoundsException ) {

System.out.println("Los vértices no existen");

} devuelve -1;
}

public static void main(String args[]) {

int v, e, cuenta = 1, hasta = 0, desde = 0; Escáner


sc = nuevo Escáner (System.in); Gráfico
Represent_Graph_Adjacency_Matrix; prueba {

System.out.println("Ingrese el número de vértices: "); v = sc.nextInt();


System.out.println("Ingrese el número de aristas: "); e = sc.nextInt();

gráfico = new Representar_Graph_Adjacency_Matrix(v);

System.out.println("Ingrese los bordes: <a> <desde>"); mientras


(cuenta <= e) {

a = sc.nextInt(); desde
= sc.nextInt();

graph.makeEdge(a, desde, 1);


contar++;
}

System.out.println("La matriz de adyacencia para el gráfico dado es: "); Sistema.out.print("


"); para (int i = 1; i <= v; i++)

Sistema.out.print(i + " ");


Sistema.salida.println();

GoalKicker.com – Notas de algoritmos para profesionales 32


Machine Translated by Google

para (int i = 1; i <= v; i++) {

Sistema.out.print(i + " "); para (int


j = 1; j <= v; j++)
System.out.print(graph.getEdge(i, j) + " ");
Sistema.salida.println();
}

} atrapar (Excepción E) {

System.out.println("Algo salió mal");


}

sc.cerrar();
}
}

Ejecutando el código: Guarde el archivo y compile usando javac Represent_Graph_Adjacency_Matrix.java

Ejemplo:

$ java Represent_Graph_Adjacency_Matrix
Introduzca el número de vértices:
4
Introduzca el número de aristas: 6

Introduzca los bordes:


11342314241
2

La matriz de adyacencia para el gráfico dado es: 1 2 3 4 1


110120011

30001
40000

Sección 9.2: Introducción a la teoría de grafos


Teoría de grafos es el estudio de los gráficos, que son estructuras matemáticas que se utilizan para modelar relaciones por pares entre
objetos.

¿Sabías que casi todos los problemas del planeta Tierra se pueden convertir en problemas de Carreteras y Ciudades, y solucionarse?
La teoría de grafos se inventó hace muchos años, incluso antes de la invención de la computadora. Leonhard Euler escribió un artículo
sobre los Siete Puentes de Königsberg que se considera como el primer artículo de teoría de grafos. Desde entonces, la gente se ha
dado cuenta de que si podemos convertir cualquier problema en este problema City-Road, podemos resolverlo fácilmente mediante la
teoría de grafos.

La teoría de grafos tiene muchas aplicaciones. Una de las aplicaciones más comunes es encontrar la distancia más corta entre una
ciudad y otra. Todos sabemos que para llegar a su PC, esta página web tuvo que recorrer muchos enrutadores desde el servidor.
Graph Theory lo ayuda a encontrar los enrutadores que deben cruzarse. Durante la guerra, qué calle debe ser bombardeada para
desconectar la ciudad capital de otras, eso también se puede averiguar utilizando la teoría de grafos.

GoalKicker.com – Notas de algoritmos para profesionales 33


Machine Translated by Google

Primero aprendamos algunas definiciones básicas sobre la teoría de grafos.

Grafico:

Digamos que tenemos 6 ciudades. Las marcamos como 1, 2, 3, 4, 5, 6. Ahora conectamos las ciudades que tienen carreteras entre sí.

Este es un gráfico simple donde se muestran algunas ciudades con las carreteras que las conectan. En teoría de grafos, llamamos a cada una de
estas ciudades nodo o vértice y las carreteras se llaman borde. Graph es simplemente una conexión de estos nodos y aristas.

Un nodo puede representar muchas cosas. En algunos gráficos, los nodos representan ciudades, algunos representan aeropuertos, algunos
representan un cuadrado en un tablero de ajedrez. Edge representa la relación entre cada nodo. Esa relación puede ser el tiempo para ir de un
aeropuerto a otro, los movimientos de un caballo de una casilla a todas las demás casillas, etc.

GoalKicker.com – Notas de algoritmos para profesionales 34


Machine Translated by Google

Camino del Caballero en un Tablero de Ajedrez

En palabras simples, un Nodo representa cualquier objeto y Edge representa la relación entre dos objetos.

Nodo adyacente:

Si un nodo A comparte un borde con el nodo B, se considera que B es adyacente a A. En otras palabras, si dos nodos están
conectados directamente, se denominan nodos adyacentes. Un nodo puede tener múltiples nodos adyacentes.

Gráfico dirigido y no dirigido:

En los gráficos dirigidos, los bordes tienen signos de dirección en un lado, lo que significa que los bordes son unidireccionales.
Por otro lado, los bordes de los gráficos no dirigidos tienen signos de dirección en ambos lados, lo que significa que son bidireccionales.
Por lo general, los gráficos no dirigidos se representan sin signos a ambos lados de los bordes.

Supongamos que hay una fiesta en marcha. Las personas del grupo están representadas por nodos y hay una ventaja entre dos
personas si se dan la mano. Entonces este gráfico no está dirigido porque cualquier persona A le da la mano a la persona B si y
solo si B también le da la mano a A. Por el contrario, si los bordes de una persona A a otra persona B corresponden a la admiración
de A por B, entonces este gráfico está dirigido , porque la admiración no es necesariamente recíproca. El primer tipo de gráfico se
llama gráfico no dirigido y los bordes se llaman bordes no dirigidos, mientras que el último tipo de gráfico se llama gráfico dirigido y los
bordes se llaman bordes dirigidos.

Gráfico ponderado y no ponderado:

Un gráfico ponderado es un gráfico en el que se asigna un número (el peso) a cada borde. Dichos pesos pueden representar, por
ejemplo, costos, longitudes o capacidades, según el problema en cuestión.

GoalKicker.com – Notas de algoritmos para profesionales 35


Machine Translated by Google

Un gráfico no ponderado es simplemente lo contrario. Suponemos que el peso de todos los bordes es el mismo (presumiblemente 1).

Sendero:

Un camino representa una forma de ir de un nodo a otro. Consiste en una secuencia de aristas. Puede haber múltiples

caminos entre dos nodos.

En el ejemplo anterior, hay dos rutas de A a D. A->B, B->C, C->D es una ruta. El costo de este camino es 3 + 4 + 2 = 9. De nuevo, hay
otro camino A->D. El costo de este camino es 10. El camino que cuesta menos se llama camino más corto.

La licenciatura:

El grado de un vértice es el número de aristas que están conectadas a él. Si hay algún borde que se conecta al vértice en ambos
extremos (un bucle) se cuenta dos veces.

GoalKicker.com – Notas de algoritmos para profesionales 36


Machine Translated by Google

En grafos dirigidos, los nodos tienen dos tipos de grados:

En grado: el número de aristas que apuntan al nodo.


Grado de salida: el número de aristas que apuntan desde el nodo a otros nodos.

Para grafos no dirigidos, simplemente se les llama grado.

Algunos algoritmos relacionados con la teoría de grafos

Algoritmo de Bellman-Ford
Algoritmo de Dijkstra
Algoritmo de Ford-Fulkerson
Algoritmo de Kruskal
Algoritmo de vecino más cercano
algoritmo de Prim
Búsqueda en profundidad
Búsqueda en amplitud

Sección 9.3: Almacenamiento de gráficos (Lista de adyacencia)

lista de adyacencia es una colección de listas desordenadas usadas para representar un gráfico finito. Cada lista describe el conjunto de
vecinos de un vértice en un gráfico. Se necesita menos memoria para almacenar gráficos.

Veamos un grafo, y su matriz de adyacencia:

Ahora creamos una lista usando estos valores.

GoalKicker.com – Notas de algoritmos para profesionales 37


Machine Translated by Google

Esto se llama lista de adyacencia. Muestra qué nodos están conectados a qué nodos. Podemos almacenar esta información
usando una matriz 2D. Pero nos costará la misma memoria que Adjacency Matrix. En su lugar, vamos a utilizar la memoria
asignada dinámicamente para almacenar esta.

Muchos idiomas admiten Vector o Lista , que podemos usar para almacenar la lista de adyacencia. Para estos, no necesitamos
especificar el tamaño de la Lista. Solo necesitamos especificar el número máximo de nodos.

El pseudocódigo será:

Procedimiento Adyacencia-Lista(maxN, E): // maxN indica el número máximo de nodos


borde[maxN] = Vector() para i de 1 a E // E denota el número de aristas

entrada -> x, y // Aquí x, y denota que hay un borde entre x, y


borde[x].empujar(y)
borde[y].empujar(x)
fin para
Borde de retorno

Dado que este es un gráfico no dirigido, si hay una arista de x a y, también hay una arista de y a x. Si fuera un gráfico dirigido,
omitiríamos el segundo. Para gráficos ponderados, también necesitamos almacenar el costo. Crearemos otro vector o lista llamada
cost[] para almacenarlos. El pseudocódigo:

Procedimiento Adyacencia-Lista(maxN, E):


borde[maxN] = Vector() costo[maxN] =
Vector() para i de 1 a E

entrada -> x, y, w
edge[x].push(y)
cost[x].push(w) end
for Return edge, cost

A partir de este, podemos averiguar fácilmente el número total de nodos conectados a cualquier nodo y cuáles son estos nodos.

GoalKicker.com – Notas de algoritmos para profesionales 38


Machine Translated by Google

Lleva menos tiempo que Adjacency Matrix. Pero si necesitáramos averiguar si hay un borde entre u y v, hubiera sido más fácil si
mantuviéramos una matriz de adyacencia.

Sección 9.4: Clasificación topológica


Una ordenación topológica, o una clasificación topológica, ordena los vértices de un gráfico acíclico dirigido en una línea, es decir, en una
lista, de modo que todas las aristas dirigidas van de izquierda a derecha. Tal orden no puede existir si el gráfico contiene un ciclo dirigido porque
no hay forma de que pueda continuar en una línea y aún así regresar a donde comenzó.

Formalmente, en un grafo G = (V, E), entonces una ordenación lineal de todos sus vértices es tal que si G contiene una arista (u, v)
ÿ E del vértice u al vértice v entonces u precede a v en la ordenación.

Es importante tener en cuenta que cada DAG tiene al menos un tipo topológico.

Existen algoritmos conocidos para construir un ordenamiento topológico de cualquier DAG en tiempo lineal, un ejemplo es:

1. Llame a depth_first_search(G) para calcular los tiempos de finalización vf para cada vértice v 2.
A medida que finaliza cada vértice, insértelo al principio de una lista enlazada 3. la lista enlazada
de vértices, tal como está ahora ordenada.

Una ordenación topológica se puede realizar en (V + E) tiempo, ya que el algoritmo de búsqueda en profundidad toma (V + E)
tiempo y toma ÿ(1) (tiempo constante) para insertar cada uno de |V| vértices al frente de una lista enlazada.

Muchas aplicaciones utilizan gráficos acíclicos dirigidos para indicar precedencias entre eventos. Usamos ordenación topológica para obtener
una ordenación para procesar cada vértice antes que cualquiera de sus sucesores.

Los vértices de un gráfico pueden representar tareas a realizar y los bordes pueden representar restricciones de que una tarea debe
realizarse antes que otra; un ordenamiento topológico es una secuencia válida para realizar el conjunto de tareas descritas en V.

Ejemplo de problema y su solución.

Deje que un vértice v describa una Tarea (horas para completar: int), es decir , Tarea (4) describe una Tarea que tarda 4 horas
en completarse, y un borde e describe un Enfriamiento (horas: int) tal que Cooldown (3) describe una duración de tiempo para
refrescarse después de una tarea completada.

Dejemos que nuestro grafo se llame dag (ya que es un grafo acíclico dirigido), y que contenga 5 vértices:

A <- dag.add_vertex(Tarea(4)); B <-


dag.add_vertex(Tarea(5)); C <-
dag.add_vertex(Tarea(3)); D <-
dag.add_vertex(Tarea(2)); E <-
dag.add_vertex(Tarea(7));

donde conectamos los vértices con aristas dirigidas de manera que el gráfico es acíclico,

// A ---> C ----+
// | // | |
v // v v
B ---> D --> E
dag.add_edge(A, B, Cooldown(2));
dag.add_edge(A, C, Enfriamiento(2));
dag.add_edge(B, D, Enfriamiento(1));
dag.add_edge(C, D, Enfriamiento(1));
dag.add_edge(C, E, Enfriamiento(1));
dag.add_edge(D, E, Enfriamiento(3));

GoalKicker.com – Notas de algoritmos para profesionales 39


Machine Translated by Google

entonces hay tres ordenaciones topológicas posibles entre A y E,

1. A -> B -> D -> E


2. A -> C -> D -> E
3. A -> C -> E

Sección 9.5: Detección de un ciclo en un gráfico dirigido utilizando


Profundidad Primero Traversal

Existe un ciclo en un gráfico dirigido si se descubre un borde posterior durante un DFS. Un borde posterior es un borde de un nodo a sí
mismo o uno de los ancestros en un árbol DFS. Para un gráfico desconectado, obtenemos un bosque DFS, por lo que debe iterar a través
de todos los vértices del gráfico para encontrar árboles DFS disjuntos.

Implementación en C++:

#incluir <iostream>
#incluir <lista>

utilizando el espacio de nombres estándar;

#define NUM_V 4

bool helper(list<int> *graph, int u, bool* visitado, bool* recStack) {

visitado[u]=verdadero;
recStack[u]=verdadero;
lista<int>::iterador i; for(i =
gráfico[u].begin();i!=gráfico[u].end();++i) {

if(recStack[*i]) //si el vértice v se encuentra en la pila de recursión de este recorrido DFS


devolver
verdadero; else if(*i==u) //si hay una arista desde el vértice hacia sí mismo
devuelve verdadero; else if(!visitado[*i]) { if(helper(graph, *i, visited, recStack))

devolver verdadero;
}

} recStack[u]=falso;
falso retorno;

}/
* / La función contenedora llama a la función auxiliar en cada vértice que no ha sido visitado. Ayudante
La función devuelve verdadero si detecta un borde posterior en el subgráfico (árbol) o falso. */

bool isCyclic(lista<int> *grafica, int V) {

bool visitado[V]; //matriz para rastrear vértices ya visitados bool recStack[V]; //


matriz para rastrear vértices en la pila de recursión del recorrido.

for(int i = 0;i<V;i++)
visitó[i]=falso, recStack[i]=falso; //inicializar todos los vértices como no visitados y no
recursiva

for(int u = 0; u < V; u++) //Comprueba iterativamente si todos los vértices han sido visitados { if(visited[u]==false)
{ if(helper(graph, u, visited, recStack)) // comprueba si el árbol DFS desde el vértice

contiene un ciclo
devolver verdadero;

GoalKicker.com – Notas de algoritmos para profesionales 40


Machine Translated by Google

} devuelve falso;

} /*
Función del conductor
*/
int principal()
{
list<int>* gráfico = new list<int>[NUM_V];
gráfico[0].push_back(1); gráfico[0].push_back(2);
gráfico[1].push_back(2); gráfico[2].push_back(0);
gráfico[2].push_back(3); gráfico[3].push_back(3); bool
res = isCyclic(gráfico, NUM_V); cout<<res<<endl;

Resultado: como se muestra a continuación, hay tres bordes posteriores en el gráfico. Uno entre el vértice 0 y 2; entre el vértice 0, 1 y 2; y vértice
3. La complejidad temporal de la búsqueda es O(V+E) donde V es el número de vértices y E es el número de aristas.

Sección 9.6: Algoritmo de Thorup


El algoritmo de Thorup para la ruta más corta de fuente única para gráficos no dirigidos tiene una complejidad de tiempo O (m), más baja que la
de Dijkstra.

Las ideas básicas son las siguientes. (Lo siento, no intenté implementarlo todavía, por lo que podría perderme algunos detalles menores. Y el
documento original tiene un muro de pago, por lo que intenté reconstruirlo a partir de otras fuentes que hacen referencia a él. Elimine este
comentario si puede verificarlo).

Hay formas de encontrar el árbol de expansión en O(m) (no se describen aquí). Necesita "hacer crecer" el árbol de expansión desde el
borde más corto al más largo, y sería un bosque con varios componentes conectados antes

GoalKicker.com – Notas de algoritmos para profesionales 41


Machine Translated by Google

Completamente crecido.

Seleccione un número entero b (b>=2) y solo considere los bosques extensos con límite de longitud b^k. Combine
los componentes que son exactamente iguales pero con k diferente, y llame al k mínimo el nivel del componente.
Luego, lógicamente, convierta los componentes en un árbol. u es el padre de v iff u es el componente más pequeño distinto de
v que contiene completamente v. La raíz es el gráfico completo y las hojas son vértices individuales en el gráfico original (con
el nivel de infinito negativo). El árbol todavía tiene solo O(n) nodos.
Mantenga la distancia de cada componente a la fuente (como en el algoritmo de Dijkstra). La distancia de un componente
con más de un vértice es la distancia mínima de sus hijos no expandidos. Establezca la distancia del vértice de origen
en 0 y actualice los ancestros en consecuencia.
Considera las distancias en base b. Cuando visite un nodo en el nivel k por primera vez, coloque sus elementos secundarios
en cubos compartidos por todos los nodos del nivel k (como en la ordenación de cubos, reemplazando el montón en el
algoritmo de Dijkstra) por el dígito k y más alto de su distancia. Cada vez que visite un nodo, considere solo sus primeros b
cubos, visite y elimine cada uno de ellos, actualice la distancia del nodo actual y vuelva a vincular el nodo actual a su propio
padre usando la nueva distancia y espere la próxima visita para lo siguiente baldes
Cuando se visita una hoja, la distancia actual es la distancia final del vértice. Expanda todos los bordes del gráfico original y
actualice las distancias en consecuencia.
Visite el nodo raíz (gráfico completo) repetidamente hasta llegar al destino.

Se basa en el hecho de que no hay una arista con una longitud inferior a l entre dos componentes conectadas del bosque expansivo
con limitación de longitud l, por lo que, comenzando en la distancia x, puede concentrarse solo en una componente conectada hasta
llegar a la distancia x + l. Visitará algunos vértices antes de que se visiten todos los vértices con una distancia más corta, pero eso no
importa porque se sabe que no habrá un camino más corto hasta aquí desde esos vértices. Otras partes funcionan como el ordenamiento
por cubo / ordenamiento por radix MSD y, por supuesto, requiere el árbol de expansión O(m).

GoalKicker.com – Notas de algoritmos para profesionales 42


Machine Translated by Google

Capítulo 10: Gráficos transversales


Sección 10.1: Función transversal de búsqueda en profundidad primero

La función toma el argumento del índice de nodo actual, la lista de adyacencia (almacenada en el vector de vectores en este
ejemplo) y el vector de booleano para realizar un seguimiento de qué nodo ha sido visitado.

void dfs(int nodo, vector<vector<int>>* gráfico, vector<bool>* visitado) {


// comprobar si el nodo ha sido visitado antes if((*visitado)
[nodo])
devolver;

// establecer como visitado para evitar visitar el mismo nodo dos veces
(*visitado)[nodo] = true;

// realizar alguna acción aquí cout


<< nodo;

// atravesar los nodos adyacentes en profundidad primero for(int i = 0;


i < (*graph)[node].size(); ++i) dfs((*graph)[node][i], gráfico, visitado);

GoalKicker.com – Notas de algoritmos para profesionales 43


Machine Translated by Google

Capítulo 11: Algoritmo de Dijkstra


Sección 11.1: Algoritmo de ruta más corta de Dijkstra
Antes de continuar, se recomienda tener una breve idea sobre Adjacency Matrix y BFS

Algoritmo de Dijkstra se conoce como algoritmo de ruta más corta de fuente única. Se utiliza para encontrar los caminos más cortos
entre los nodos de un gráfico, que puede representar, por ejemplo, redes de carreteras. Fue concebido por Edsger W.
Dijkstra en 1956 y publicado tres años después.

Podemos encontrar la ruta más corta usando el algoritmo de búsqueda Breadth First Search (BFS). Este algoritmo funciona bien, pero el
problema es que asume que el costo de recorrer cada ruta es el mismo, lo que significa que el costo de cada borde es el mismo. El algoritmo
de Dijkstra nos ayuda a encontrar el camino más corto donde el costo de cada camino no es el mismo.

Primero veremos cómo modificar BFS para escribir el algoritmo de Dijkstra, luego agregaremos la cola de prioridad para convertirlo en
un algoritmo de Dijkstra completo.

Digamos que la distancia de cada nodo desde la fuente se mantiene en la matriz d[] . Como en, d[3] representa que se tarda d[3] tiempo en
llegar al nodo 3 desde la fuente. Si no conocemos la distancia, almacenaremos el infinito en d[3]. Además, deje que cost[u][v] represente el
costo de uv. Eso significa que cuesta[u][v] ir del nodo u al nodo v .

Necesitamos entender la relajación de borde. Digamos, desde tu casa, esa es la fuente, toma 10 minutos para ir al lugar A. Y toma 25
minutos para ir al lugar B. Tenemos,

d[A] = 10
d[B] = 25

Ahora digamos que se tarda 7 minutos en ir del lugar A al lugar B, eso significa:

costo[A][B] = 7

Entonces podemos ir al lugar B desde la fuente yendo al lugar A desde la fuente y luego desde el lugar A, yendo al lugar B, lo que tomará
10 + 7 = 17 minutos, en lugar de 25 minutos. Asi que,

d[A] + coste[A][B] < d[B]

Luego actualizamos,

d[B] = d[A] + coste[A][B]

Esto se llama relajación. Iremos del nodo u al nodo v y si d[u] + cost[u][v] < d[v] entonces actualizaremos d[v] = d[u] + cost[u][v].

En BFS, no necesitábamos visitar ningún nodo dos veces. Solo verificamos si un nodo es visitado o no. Si no fue visitado, empujamos el
nodo en cola, lo marcamos como visitado e incrementamos la distancia en 1. En Dijkstra, podemos empujar un nodo

GoalKicker.com – Notas de algoritmos para profesionales 44


Machine Translated by Google

en cola y en lugar de actualizarlo con visitado, relajamos o actualizamos el nuevo borde. Veamos un ejemplo:

Supongamos que el Nodo 1 es la Fuente. Después,

d[1] = 0
d[2] = d[3] = d[4] = infinito (o un valor grande)

Ajustamos d[2], d[3] y d[4] al infinito porque aún no conocemos la distancia. Y la distancia de la fuente es, por supuesto , 0. Ahora, vamos
a otros nodos desde la fuente y, si podemos actualizarlos, los colocaremos en la cola.
Digamos, por ejemplo, que atravesaremos el borde 1-2. Como d[1] + 2 < d[2] , lo que hará que d[2] = 2. De manera similar, atravesaremos el borde
1-3 , lo que hará que d[3] = 5.

Podemos ver claramente que 5 no es la distancia más corta que podemos cruzar para ir al nodo 3. Entonces, atravesar un nodo solo una vez,
como BFS, no funciona aquí. Si vamos del nodo 2 al nodo 3 usando el borde 2-3, podemos actualizar d[3] = d[2] + 1 = 3. Entonces podemos ver
que un nodo puede actualizarse muchas veces. ¿Cuántas veces preguntas? El número máximo de veces que se puede actualizar un nodo es el
número de grados de entrada de un nodo.

Veamos el pseudocódigo para visitar cualquier nodo varias veces. Simplemente modificaremos BFS:

procedimiento BFSmodificado(G, fuente):


Q = cola() distancia[] = infinito
Q.encolar(fuente) distancia[fuente]=0
mientras que Q no está vacío u <- Q.pop()
para todos los bordes de u a v en
G.adjacentEdges(v) do if distancia[u] +
costo[u][v] < distancia[v] distancia[v] =
distancia[u] + costo[u][v] end if end for

terminar mientras
Distancia de retorno

GoalKicker.com – Notas de algoritmos para profesionales 45


Machine Translated by Google

Esto se puede usar para encontrar la ruta más corta de todos los nodos desde la fuente. La complejidad de este código no es tan buena.

Este es el por qué,

En BFS, cuando vamos del nodo 1 a todos los demás nodos, seguimos el método por orden de llegada . Por ejemplo, pasamos al nodo 3 desde el origen antes de

procesar el nodo 2. Si vamos al nodo 3 desde el origen, actualizamos el nodo 4 como 5 + 3 = 8.

Cuando actualizamos nuevamente el nodo 3 desde el nodo 2, ¡necesitamos actualizar el nodo 4 como 3 + 3 = 6 nuevamente! Entonces el nodo 4 se actualiza
dos veces.

Dijkstra propuso que, en lugar de optar por el método Primero en llegar, primero en servir , si primero actualizamos los nodos más cercanos, se necesitarán menos

actualizaciones. Si procesamos el nodo 2 antes, entonces el nodo 3 se habría actualizado antes, y después de actualizar el nodo 4 en consecuencia, ¡obtendríamos

fácilmente la distancia más corta! La idea es elegir de la cola, el nodo, que está más cerca de la fuente. Así que usaremos Priority Queue aquí para que cuando abramos

la cola, nos traiga el nodo u más cercano a la fuente. ¿Cómo hará eso? Verificará el valor de d[u] con él.

Veamos el pseudocódigo:

procedimiento dijkstra(G, fuente): Q =


prioridad_cola() distancia[] = infinito
Q.encolar(fuente) distancia[fuente] = 0
mientras Q no está vacío u <- nodos
en Q con distancia mínima[] eliminar u
de la Q para todos los bordes de u a v
en G.adjacentEdges(v) do if distancia[u] + costo[u]
[v] < distancia[v] distancia[v] = distancia[u] + costo[u]
[v ]

Q.enqueue(v)
fin si fin por fin
mientras

Distancia de retorno

El pseudocódigo devuelve la distancia de todos los demás nodos desde la fuente. Si queremos saber la distancia de un solo nodo v, simplemente podemos devolver el

valor cuando v se extrae de la cola.

Ahora bien, ¿funciona el algoritmo de Dijkstra cuando hay un borde negativo? Si hay un ciclo negativo, se producirá un bucle infinito, ya que seguirá reduciendo el costo

cada vez. Incluso si hay un borde negativo, Dijkstra no funcionará, a menos que regresemos justo después de que se reventa el objetivo. Pero entonces, no será un

algoritmo de Dijkstra. Necesitaremos el algoritmo de Bellman-Ford para procesar el ciclo/flanco negativo.

Complejidad:

La complejidad de BFS es O(log(V+E)) donde V es el número de nodos y E es el número de aristas. Para Dijkstra, la complejidad es similar, pero la clasificación de

Priority Queue toma O (logV). Entonces la complejidad total es: O(Vlog(V)+E)

A continuación se muestra un ejemplo de Java para resolver el algoritmo de ruta más corta de Dijkstra utilizando matriz de adyacencia

importar java.util.*;
importar java.lang.*;
importar java.io.*;

clase ruta más corta {

int final estático V=9; int


minDistance(int dist[], Boolean sptSet[]) {

GoalKicker.com – Notas de algoritmos para profesionales 46


Machine Translated by Google

int min = Integer.MAX_VALUE, min_index=-1;

for (int v = 0; v < V; v++) if


(sptSet[v] == false && dist[v] <= min) {

min = dist[v];
índice_min = v;
}

devuelve min_index;
}

void printSolution(int dist[], int n) {

System.out.println(" Distancia del vértice desde la fuente"); para


(int i = 0; i < V; i++)
System.out.println(i+" \t\t "+dist[i]);
}

void dijkstra(int gráfico[][], int src) {

Boolean sptSet[] = new Boolean[V];

para (int i = 0; i < V; i++) {

dist[i] = Integer.MAX_VALUE;
sptSet[i] = falso;
}

dist[origen] = 0;

for (int conteo = 0; conteo < V-1; conteo++) {

int u = minDistance(dist, sptSet);

sptSet[u] = verdadero;

para (int v = 0; v < V; v++)

if (!sptSet[v] && gráfico[u][v]!=0 && dist[u] !=


Integer.MAX_VALUE && dist[u]+graph[u]
[v] < dist[v])
dist[v] = dist[u] + gráfico[u][v];
}

imprimirSolucion(dist, V);
}

public static void main (String[] args) { int graph[]


[] = new int[][]{{0, 4, 0, 0, 0, 0, 0, 8, 0}, {4, 0, 8, 0,
0, 0, 0, 11, 0}, {0, 8, 0, 7, 0, 4, 0, 0, 2}, {0, 0, 7, 0, 9, 14, 0, 0, 0}, {0, 0, 0,
9, 0, 10, 0, 0, 0}, {0, 0, 4, 14, 10, 0,
2, 0, 0}, {0, 0, 0, 0, 0, 2, 0, 1, 6}, {8,
11, 0, 0, 0, 0, 1, 0, 7}, {0, 0, 2, 0, 0,
0, 6, 7, 0} };

ShortestPath t = new ShortestPath();

GoalKicker.com – Notas de algoritmos para profesionales 47


Machine Translated by Google

t.dijkstra(gráfico, 0);
}
}

El resultado esperado del programa es

Distancia del vértice desde la fuente


0 0
1 4
2 12
3 19
4 21
5 11
6 9
7 8
8 14

GoalKicker.com – Notas de algoritmos para profesionales 48


Machine Translated by Google

Capítulo 12: A* Pathfinding


Sección 12.1: Introducción a A*

A* (una estrella) es un algoritmo de búsqueda que se utiliza para encontrar la ruta de un nodo a otro. Por lo que se puede comparar con
Búsqueda primero en amplitud, o algoritmo de Dijkstra, o Primera búsqueda en profundidad, o Mejor primera búsqueda. El algoritmo A* es ampliamente utilizado

en la búsqueda de gráficos por ser mejor en eficiencia y precisión, donde el preprocesamiento de gráficos no es una opción.

A* es una especialización de Best First Search , en el que la función de evaluación f se define de una manera particular.

f(n) = g(n) + h(n) es el coste mínimo desde el nodo inicial hasta los objetivos acondicionados para pasar por el nodo n.

g(n) es el costo mínimo desde el nodo inicial hasta n.

h(n) es el costo mínimo de n al objetivo más cercano a n

A* es un algoritmo de búsqueda informado y siempre garantiza encontrar el camino más pequeño (camino con costo mínimo) en
el menor tiempo posible (si utiliza heurísticas admisibles). Por lo tanto, es completo y óptimo. La siguiente animación
demuestra la búsqueda A*

Sección 12.2: A* Búsqueda de caminos a través de un laberinto sin obstáculos


Digamos que tenemos la siguiente cuadrícula de 4 por 4:

GoalKicker.com – Notas de algoritmos para profesionales 49


Machine Translated by Google

Supongamos que esto es un laberinto. Sin embargo, no hay paredes/obstáculos. Solo tenemos un punto de partida (el
cuadrado verde) y un punto final (el cuadrado rojo). Supongamos también que para pasar de verde a rojo, no podemos
movernos en diagonal. Entonces, comenzando desde el cuadrado verde, veamos a qué cuadrados podemos movernos y resáltalos en
azul:

GoalKicker.com – Notas de algoritmos para profesionales 50


Machine Translated by Google

Para elegir a qué casilla movernos a continuación, debemos tener en cuenta 2 heurísticas:

1. El valor "g": indica qué tan lejos está este nodo del cuadrado verde.
2. El valor "h": indica qué tan lejos está este nodo del cuadrado rojo.
3. El valor "f": esta es la suma del valor "g" y el valor "h". Este es el número final que nos dice qué
nodo al que moverse.

Para calcular estas heurísticas, esta es la fórmula que usaremos: distancia = abs(from.x - to.x) + abs(from.y - to.y)

Esto se conoce como la "Distancia de Manhattan" fórmula.

Calculemos el valor "g" para el cuadrado azul inmediatamente a la izquierda del cuadrado verde: abs(3 - 2) + abs(2 - 2) = 1

¡Excelente! Tenemos el valor: 1. Ahora, intentemos calcular el valor "h": abs(2 - 0) + abs(2 - 0) = 4

Perfecto. Ahora, obtengamos el valor "f": 1 + 4 = 5

Entonces, el valor final para este nodo es "5".

Hagamos lo mismo con todos los demás cuadrados azules. El número grande en el centro de cada cuadrado es el valor "f", mientras que el
número en la parte superior izquierda es el valor "g" y el número en la parte superior derecha es el valor "h":

GoalKicker.com – Notas de algoritmos para profesionales 51


Machine Translated by Google

Hemos calculado los valores g, h y f para todos los nodos azules. Ahora bien, ¿cuál elegimos?

Cualquiera que tenga el valor f más bajo.

Sin embargo, en este caso, tenemos 2 nodos con el mismo valor de f, 5. ¿Cómo elegimos entre ellos?

Simplemente, elija uno al azar o tenga un conjunto de prioridades. Por lo general, prefiero tener una prioridad como esta: "Derecha> Arriba>
Abajo> Izquierda"

Uno de los nodos con el valor f de 5 nos lleva en la dirección "Abajo", y el otro nos lleva a la "Izquierda". Dado que Abajo tiene una prioridad más
alta que Izquierda, elegimos el cuadrado que nos lleva "Abajo".

Ahora marco los nodos para los que calculamos las heurísticas, pero a los que no nos movimos, como naranja, y el nodo que elegimos como
cian:

GoalKicker.com – Notas de algoritmos para profesionales 52


Machine Translated by Google

Muy bien, ahora calculemos la misma heurística para los nodos alrededor del nodo cian:

Nuevamente, elegimos el nodo que baja del nodo cian, ya que todas las opciones tienen el mismo valor f:

GoalKicker.com – Notas de algoritmos para profesionales 53


Machine Translated by Google

Calculemos las heurísticas para el único vecino que tiene el nodo cian:

Muy bien, ya que seguiremos el mismo patrón que hemos estado siguiendo:

GoalKicker.com – Notas de algoritmos para profesionales 54


Machine Translated by Google

Una vez más, calculemos las heurísticas para el vecino del nodo:

Vamos a movernos allí:

GoalKicker.com – Notas de algoritmos para profesionales 55


Machine Translated by Google

Finalmente, podemos ver que tenemos una casilla ganadora a nuestro lado, así que nos movemos allí y terminamos.

Sección 12.3: Resolviendo un problema de 8 acertijos usando el algoritmo A*

Definición del problema:

Un rompecabezas de 8 es un juego simple que consiste en una cuadrícula de 3 x 3 (que contiene 9 cuadrados). Uno de los cuadrados
está vacío. El objeto es moverse a cuadrados alrededor en diferentes posiciones y mostrar los números en el "estado objetivo".

Dado un estado inicial de un juego de 8 rompecabezas y un estado final por alcanzar, encuentre el camino más rentable para llegar al estado
final desde el estado inicial.

Estado inicial:

_ 13
425
786

GoalKicker.com – Notas de algoritmos para profesionales 56


Machine Translated by Google

Estado definitivo:

123
456
78 _

Heurística a asumir:

Consideremos la distancia de Manhattan entre el estado actual y el final como la heurística para este problema.
declaración.

h(n) = | x - p | + | y - q |
donde x e y son coordenadas de celda en el estado actual
p y q son coordenadas de celda en el estado final

Función de costo total:

Entonces la función de costo total f(n) está dada por,

f(n) = g(n) + h(n), donde g(n) es el costo requerido para alcanzar el estado actual desde
estado

Solución al problema de ejemplo:

Primero encontramos el valor heurístico requerido para alcanzar el estado final desde el estado inicial. La función de costo, g(n) = 0, como
están en el estado inicial

h(n) = 8

El valor anterior se obtiene, ya que 1 en el estado actual está a 1 distancia horizontal del 1 en el estado final. Mismo
va por 2, 5, 6. _ está a 2 distancias horizontales y 2 distancias verticales. Entonces el valor total para h(n) es 1 + 1 + 1 + 1 +

2 + 2 = 8. La función de costo total f(n) es igual a 8 + 0 = 8.

Ahora, se encuentran los posibles estados a los que se puede llegar desde el estado inicial y sucede que podemos movernos hacia la derecha o _

hacia abajo.

Entonces, los estados obtenidos después de mover esos movimientos son:

1 3_4 413
25 _ 25
786 786
(1) (2)

Nuevamente, la función de costo total se calcula para estos estados utilizando el método descrito anteriormente y resulta ser
6 y 7 respectivamente. Elegimos el estado con costo mínimo que es el estado (1). Los siguientes movimientos posibles pueden ser Izquierda,
Derecha o Abajo. No nos moveremos a la izquierda como estábamos anteriormente en ese estado. Entonces, podemos movernos hacia la derecha o hacia abajo.

Nuevamente encontramos los estados obtenidos de (1).

13 _ 123

GoalKicker.com – Notas de algoritmos para profesionales 57


Machine Translated by Google

425 4 _ 5
786 786
(3) (4)

(3) conduce a una función de costo igual a 6 y (4) conduce a 4. Además, consideraremos (2) obtenido antes de que tenga un costo

función igual a 7. Elegir el mínimo de ellos conduce a (4). Los siguientes movimientos posibles pueden ser Izquierda, Derecha o Abajo.

Obtenemos estados:

123 123 123


_ 45 45 _ 485
786 786 76_

(5) (6) (7)

Obtenemos costos iguales a 5, 2 y 4 para (5), (6) y (7) respectivamente. Además, tenemos estados anteriores (3) y (2) con 6 y 7

respectivamente. Elegimos el estado de costo mínimo que es (6). Los siguientes movimientos posibles son Arriba, Abajo y claramente Abajo

nos llevará al estado final que conduce al valor de la función heurística igual a 0.

GoalKicker.com – Notas de algoritmos para profesionales 58


Machine Translated by Google

Capítulo 13: Algoritmo de búsqueda de rutas A*


Este tema se centrará en el algoritmo A* Pathfinding, cómo se usa y por qué funciona.

Nota para futuros colaboradores: he agregado un ejemplo para A* Pathfinding sin ningún obstáculo, en una cuadrícula de 4x4. Todavía se necesita un
ejemplo con obstáculos.

Sección 13.1: Ejemplo simple de A* Pathfinding: Un laberinto


sin obstáculos
Digamos que tenemos la siguiente cuadrícula de 4 por 4:

Supongamos que esto es un laberinto. Sin embargo, no hay paredes/obstáculos. Solo tenemos un punto de partida (el cuadrado verde) y un punto
final (el cuadrado rojo). Supongamos también que para pasar de verde a rojo, no podemos

GoalKicker.com – Notas de algoritmos para profesionales 59


Machine Translated by Google

moverse en diagonal. Entonces, comenzando desde el cuadrado verde, veamos a qué cuadrados podemos movernos y resáltalos en azul:

Para elegir a qué casilla movernos a continuación, debemos tener en cuenta 2 heurísticas:

1. El valor "g": indica qué tan lejos está este nodo del cuadrado verde.
2. El valor "h": indica qué tan lejos está este nodo del cuadrado rojo.
3. El valor "f": esta es la suma del valor "g" y el valor "h". Este es el número final que nos dice qué
nodo al que moverse.

Para calcular estas heurísticas, esta es la fórmula que usaremos: distancia = abs(from.x - to.x) + abs(from.y - to.y)

Esto se conoce como la "Distancia de Manhattan" fórmula.

Calculemos el valor "g" para el cuadrado azul inmediatamente a la izquierda del cuadrado verde: abs(3 - 2) + abs(2 - 2) = 1

¡Excelente! Tenemos el valor: 1. Ahora, intentemos calcular el valor "h": abs(2 - 0) + abs(2 - 0) = 4

Perfecto. Ahora, obtengamos el valor "f": 1 + 4 = 5

Entonces, el valor final para este nodo es "5".

Hagamos lo mismo con todos los demás cuadrados azules. El número grande en el centro de cada cuadrado es el valor "f", mientras que el
número en la parte superior izquierda es el valor "g" y el número en la parte superior derecha es el valor "h":

GoalKicker.com – Notas de algoritmos para profesionales 60


Machine Translated by Google

Hemos calculado los valores g, h y f para todos los nodos azules. Ahora bien, ¿cuál elegimos?

Cualquiera que tenga el valor f más bajo.

Sin embargo, en este caso, tenemos 2 nodos con el mismo valor de f, 5. ¿Cómo elegimos entre ellos?

Simplemente, elija uno al azar o tenga un conjunto de prioridades. Por lo general, prefiero tener una prioridad como esta: "Derecha> Arriba>
Abajo> Izquierda"

Uno de los nodos con el valor f de 5 nos lleva en la dirección "Abajo", y el otro nos lleva a la "Izquierda". Dado que Abajo tiene una prioridad más
alta que Izquierda, elegimos el cuadrado que nos lleva "Abajo".

Ahora marco los nodos para los que calculamos las heurísticas, pero a los que no nos movimos, como naranja, y el nodo que elegimos como
cian:

GoalKicker.com – Notas de algoritmos para profesionales 61


Machine Translated by Google

Muy bien, ahora calculemos la misma heurística para los nodos alrededor del nodo cian:

Nuevamente, elegimos el nodo que baja del nodo cian, ya que todas las opciones tienen el mismo valor f:

GoalKicker.com – Notas de algoritmos para profesionales 62


Machine Translated by Google

Calculemos las heurísticas para el único vecino que tiene el nodo cian:

Muy bien, ya que seguiremos el mismo patrón que hemos estado siguiendo:

GoalKicker.com – Notas de algoritmos para profesionales 63


Machine Translated by Google

Una vez más, calculemos las heurísticas para el vecino del nodo:

Vamos a movernos allí:

GoalKicker.com – Notas de algoritmos para profesionales 64


Machine Translated by Google

Finalmente, podemos ver que tenemos una casilla ganadora a nuestro lado, así que nos movemos allí y terminamos.

GoalKicker.com – Notas de algoritmos para profesionales sesenta y cinco


Machine Translated by Google

Capítulo 14: Programación dinámica


La programación dinámica es un concepto ampliamente utilizado y se usa a menudo para la optimización. Se refiere a simplificar un problema
complicado dividiéndolo en subproblemas más simples de manera recursiva, generalmente un enfoque de abajo hacia arriba. Hay dos atributos
clave que debe tener un problema para que la programación dinámica sea aplicable: "Subestructura óptima" y "Subproblemas superpuestos". Para
lograr su optimización, la programación dinámica utiliza un concepto llamado memoización.

Sección 14.1: Editar distancia


La declaración del problema es como si nos dieran dos cadenas str1 y str2, entonces, ¿cuántas operaciones mínimas se pueden realizar en
el str1 que se convierte en str2?

Implementación en Java

clase pública EditDistance {

public static void main(String[] args) {


// TODO stub de método generado automáticamente
Cadena str1 = "marcha";
Cadena str2 = "carrito";

EditDistance ed = new EditDistance();


System.out.println(ed.getMinConversions(str1, str2));
}

public int getMinConversions(String str1, String str2){


int dp[][] = new int[str1.longitud()+1][str2.longitud()+1]; for(int
i=0;i<=str1.length();i++){
for(int j=0;j<=str2.length();j++){ if(i==0) dp[i]
[j] = j; más si (j==0) dp[i][j] = i; else
if(str1.charAt(i-1) ==
str2.charAt(j-1)) dp[i][j] = dp[i-1][j-1];
else{ dp[i][j] = 1 + Math.min(dp[i-1]
[j], Math.min(dp[i][j-1], dp[i-1][j-1 ]));

}
}

} return dp[str1.longitud()][str2.longitud()];
}

Producción

Sección 14.2: Algoritmo de programación de trabajos ponderados


El algoritmo de programación de trabajo ponderado también se puede denominar algoritmo de selección de actividad ponderada.

El problema es que, dados ciertos trabajos con su hora de inicio y finalización, y una ganancia que obtienes cuando terminas el trabajo, ¿cuál es la
ganancia máxima que puedes obtener dado que no se pueden ejecutar dos trabajos en paralelo?

GoalKicker.com – Notas de algoritmos para profesionales 66


Machine Translated by Google

Este se parece a la selección de actividades usando el algoritmo codicioso, pero hay un giro adicional. Es decir, en lugar de
maximizando el número de trabajos terminados, nos enfocamos en obtener el máximo beneficio. El número de trabajos realizados.
no importa aquí

Veamos un ejemplo:

+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Nombre | A | B | C | D | mi | F |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
|(Hora de inicio, Hora de finalización)| (2,5) | (6,7) | (7,9) | (1,3) | (5,8) | (4,6) |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Lucro | 6 | 4 | 2 | 5 | 11 | 5 |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+

Los trabajos se indican con un nombre, su tiempo de inicio y finalización y la ganancia. Después de algunas iteraciones, podemos averiguar si
realizamos Job-A y Job-E, podemos obtener la ganancia máxima de 17. Ahora, ¿cómo averiguar esto usando un algoritmo?

Lo primero que hacemos es ordenar los trabajos por su hora de finalización en orden no decreciente. ¿Por qué hacemos esto? Eso es porque
si seleccionamos un trabajo que toma menos tiempo para terminar, entonces dejamos la mayor cantidad de tiempo para elegir otros trabajos. Nosotros
tener:

+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Nombre | D | A | F | B | mi | C |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
|(Hora de inicio, Hora de finalización)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Lucro | 5 | 6 | 5 | 4 | 11 | 2 |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+

Tendremos una matriz temporal adicional Acc_Prof de tamaño n (aquí, n indica el número total de trabajos). Esta voluntad
contener la ganancia máxima acumulada de la realización de los trabajos. ¿No lo entiendes? Espera y observa. Inicializaremos el
valores de la matriz con el beneficio de cada trabajo. Eso significa que Acc_Prof[i] al principio tendrá la ganancia de realizar i-th
trabajo.

+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Acc_Prof | 5 | 6 | 5 | 4 | 11 | 2 |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+

Ahora, denotemos la posición 2 con i, y la posición 1 se denotará con j. Nuestra estrategia será iterar j de 1 a
i-1 y después de cada iteración, incrementaremos i en 1, hasta que i se convierta en n+1.

j i

+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Nombre | D | A | F | B | mi | C |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
|(Hora de inicio, Hora de finalización)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Lucro | 5 | 6 | 5 | 4 | 11 | 2 |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Acc_Prof | 5 | 6 | 5 | 4 | 11 | 2 |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+

GoalKicker.com – Notas de algoritmos para profesionales 67


Machine Translated by Google

Comprobamos si Job[i] y Job[j] se superponen, es decir, si la hora de finalización de Job[j] es mayor que la hora de inicio de Job[i] , entonces estos
dos trabajos no se pueden hacer juntos. Sin embargo, si no se superponen, comprobaremos si Acc_Prof[j] + Profit[i] > Acc_Prof[i]. Si
este es el caso, actualizaremos Acc_Prof[i] = Acc_Prof[j] + Profit[i]. Eso es:

si Trabajo[j].finish_time <= Trabajo[i].start_time


if Acc_Prof[j] + Beneficio[i] > Acc_Prof[i]
Prof_Cuenta[i] = Prof_Cuenta[j] + Beneficio[i]
terminara si

terminara si

Aquí Acc_Prof[j] + Profit[i] representa la ganancia acumulada de hacer estos dos trabajos juntos. vamos a comprobarlo
nuestro ejemplo:

Aquí Job[j] se superpone con Job[i]. Entonces estos no se pueden hacer juntos. Como nuestro j es igual a i-1, incrementamos el
valor de i a i+1 que es 3. Y hacemos j = 1.

j i

+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Nombre | D | A | F | B | mi | C |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
|(Hora de inicio, Hora de finalización)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Lucro | 5 | 6 | 5 | 4 | 11 | 2 |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Acc_Prof | 5 | 6 | 5 | 4 | 11 | 2 |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+

Ahora Job[j] y Job[i] no se superponen. La cantidad total de ganancias que podemos obtener eligiendo estos dos trabajos es: Acc_Prof[j]
+ Profit[i] = 5 + 5 = 10 que es mayor que Acc_Prof[i]. Así que actualizamos Acc_Prof[i] = 10. También incrementamos j en 1.
Obtenemos,

j i

+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Nombre | D | A | F | B | mi | C |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
|(Hora de inicio, Hora de finalización)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Lucro | 5 | 6 | 5 | 4 | 11 | 2 |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Acc_Prof | 5 | 6 | 10 | 4 | 11 | 2 |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+

Aquí, Job[j] se superpone con Job[i] y j también es igual a i-1. Entonces incrementamos i en 1 y hacemos j = 1. Obtenemos,

j i

+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Nombre | D | A | F | B | mi | C |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
|(Hora de inicio, Hora de finalización)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Lucro | 5 | 6 | 5 | 4 | 11 | 2 |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Acc_Prof | 5 | 6 | 10 | 4 | 11 | 2 |

GoalKicker.com – Notas de algoritmos para profesionales 68


Machine Translated by Google
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+

Ahora, Job[j] y Job[i] no se superponen, obtenemos la ganancia acumulada 5 + 4 = 9, que es mayor que Acc_Prof[i]. Nosotros
actualice Acc_Prof[i] = 9 e incremente j en 1.

j i

+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Nombre | D | A | F | B | mi | C |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
|(Hora de inicio, Hora de finalización)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Lucro | 5 | 6 | 5 | 4 | 11 | 2 |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Acc_Prof | 5 | 6 | 10 | 9 | 11 | 2 |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+

De nuevo , Job[j] y Job[i] no se superponen. El beneficio acumulado es: 6 + 4 = 10, que es mayor que Acc_Prof[i]. Nosotros
de nuevo actualice Acc_Prof[i] = 10. Incrementamos j en 1. Obtenemos:

j i

+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Nombre | D | A | F | B | mi | C |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
|(Hora de inicio, Hora de finalización)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Lucro | 5 | 6 | 5 | 4 | 11 | 2 |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Acc_Prof | 5 | 6 | 10 | 10 | 11 | 2 |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+

Si continuamos con este proceso, después de recorrer toda la tabla usando i, nuestra tabla finalmente se verá así:

+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Nombre | D | A | F | B | mi | C |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
|(Hora de inicio, Hora de finalización)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Lucro | 5 | 6 | 5 | 4 | 11 | 2 |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
| Acc_Prof | 5 | 6 | 10 | 14 | 17 | 8 |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+

* Se han saltado algunos pasos para acortar el documento.

Si iteramos a través de la matriz Acc_Prof, ¡podemos encontrar que la ganancia máxima es 17! El pseudocódigo:

Procedimiento WeightedJobScheduling(Trabajo)
ordenar el trabajo según el tiempo de finalización en orden no decreciente
para i -> 2 a n
para j -> 1 a i-1
si Trabajo[j].finish_time <= Trabajo[i].start_time
if Acc_Prof[j] + Beneficio[i] > Acc_Prof[i]
Prof_Cuenta[i] = Prof_Cuenta[j] + Beneficio[i]

GoalKicker.com – Notas de algoritmos para profesionales 69


Machine Translated by Google
terminara si

endif
endfor endfor

maxBeneficio = 0
para i -> 1 a n si
maxBeneficio < Acc_Prof[i]
maxBeneficio = Acc_Prof[i]
return maxBeneficio

La complejidad de llenar la matriz Acc_Prof es O(n2). El recorrido del arreglo toma O(n). Entonces la complejidad total de este algoritmo es
O(n2).

Ahora, si queremos averiguar qué trabajos se realizaron para obtener el máximo beneficio, debemos recorrer la matriz en orden inverso y si
Acc_Prof coincide con maxProfit, empujaremos el nombre del trabajo en una pila y restaremos el beneficio de ese trabajo de maxProfit.
Haremos esto hasta que nuestro maxProfit > 0 o lleguemos al punto de inicio de la matriz Acc_Prof . El pseudocódigo se verá así:

Procedimiento FindingPerformedJobs(Job, Acc_Prof, maxProfit):


S = stack()
para i -> n hasta 0 y maxProfit > 0
si maxProfit es igual a Acc_Prof[i]
S.push(Trabajo[i] .nombre
maxBeneficio = maxBeneficio - Trabajo[i].beneficio
endif endfor

La complejidad de este procedimiento es: O(n).

Una cosa para recordar, si hay varios horarios de trabajo que nos pueden dar el máximo beneficio, solo podemos encontrar un horario de
trabajo a través de este procedimiento.

Sección 14.3: Subsecuencia común más larga


Si nos dan las dos cadenas, tenemos que encontrar la subsecuencia común más larga presente en ambas.

Ejemplo

LCS para las secuencias de entrada "ABCDGH" y "AEDFHR" es "ADH" de longitud 3.

LCS para las secuencias de entrada "AGGTAB" y "GXTXAYB" es "GTAB" de longitud 4.

Implementación en Java

clase pública LCS {

public static void main(String[] args) { // TODO


Método generado automáticamente stub String
str1 = "AGGTAB"; Cadena str2 = "GXTXAYB";
LCSobj = nuevo LCS(); System.out.println(obj.lcs(str1,
str2 , str1.longitud(), str2.longitud()));
System.out.println(obj.lcs2(str1, str2));

// Función recursiva public


int lcs(String str1, String str2, int m, int n){

GoalKicker.com – Notas de algoritmos para profesionales 70


Machine Translated by Google

si(m==0 || n==0)
devolver 0;
if(str1.charAt(m-1) == str2.charAt(n-1))
devuelve 1 + lcs(str1, str2, m-1, n-1);
más
return Math.max(lcs(str1, str2, m-1, n), lcs(str1, str2, m, n-1));
}

// Función iterativa
public int lcs2(String str1, String str2){
int lcs[][] = new int[str1.longitud()+1][str2.longitud()+1];

for(int i=0;i<=str1.length();i++){
para(int j=0;j<=str2.longitud();j++){
si(i==0 || j== 0){
lcs[i][j] = 0;
}
más si (str1.charAt(i-1) == str2.charAt(j-1)){
lcs[i][j] = 1 + lcs[i-1][j-1];
}más{
lcs[i][j] = Math.max(lcs[i-1][j], lcs[i][j-1]);
}
}
}

return lcs[str1.longitud()][str2.longitud()];
}

Producción

Sección 14.4: Número de Fibonacci


Enfoque ascendente para imprimir el n-ésimo número de Fibonacci usando Programación Dinámica.

Árbol recursivo

mentira(5)
\
/ fib(4) \ mentira(3) /
/ \

mentira(3) / fib(2) / mentira(2) / mentira(1)


\ fib(2) \ \

fib(1) fib(1) fib(0) fib(1) fib(0)


/ \
mentira(1) mentira(0)

Subproblemas superpuestos

Aquí fib(0),fib(1) y fib(3) son los subproblemas superpuestos. fib(0) se repite 3 veces, fib(1) se repite
repetido 5 veces y fib(3) se repite 2 veces.

Implementación

public int fib(int n){


int f[] = new int[n+1];

71
GoalKicker.com – Notas de algoritmos para profesionales
Machine Translated by Google

f[0]=0;f[1]=1;
for(int i=2;i<=n;i++)
{ f[i]=f[i-1]+f[i-2];

} devuelve f[n];
}

Complejidad del tiempo

En)

Sección 14.5: Subcadena común más larga


Dadas 2 cadenas str1 y str2, tenemos que encontrar la longitud de la subcadena común más larga entre ellas.

Ejemplos

Entrada: X = "abcdxyz", y = "xyzabcd" Salida: 4

La subcadena común más larga es "abcd" y tiene una longitud de 4.

Entrada: X = "zxabcdezy", y = "yzabcdezx" Salida: 6

La subcadena común más larga es "abcdez" y tiene una longitud de 6.

Implementación en Java

public int getLongestCommonSubstring(String str1,String str2){


int arr[][] = new int[str2.longitud()+1][str1.longitud()+1]; int max =
Integer.MIN_VALUE; for(int i=1;i<=str2.length();i++){

for(int j=1;j<=str1.length();j++)
{ if(str1.charAt(j-1) == str2.charAt(i-1)){ arr[i][j] = arr
[i-1][j-1]+1; if(arr[i][j]>max) max = arr[i][j];

}
else arr[i][j] = 0;
}

} retorno máximo;
}

Complejidad del tiempo

O(m*n)

GoalKicker.com – Notas de algoritmos para profesionales 72


Machine Translated by Google

Capítulo 15: Aplicaciones de Dynamic


Programación
La idea básica detrás de la programación dinámica es dividir un problema complejo en varios problemas pequeños y simples que se repiten. Si
puede identificar un subproblema simple que se calcula repetidamente, es probable que haya un enfoque de programación dinámica para el problema.

Como este tema se titula Aplicaciones de la programación dinámica, se centrará más en las aplicaciones que en el proceso de creación de algoritmos
de programación dinámica.

Sección 15.1: Números de Fibonacci


Números de Fibonacci son un tema principal para la programación dinámica ya que el enfoque recursivo tradicional hace muchos
cálculos repetidos. En estos ejemplos usaré el caso base de f(0) = f(1) = 1.

Aquí hay un árbol recursivo de ejemplo para fibonacci(4), tenga en cuenta los cálculos repetidos:

Complejidad de tiempo de ejecución de programación no dinámica O(2^n) , complejidad de pila O(n)

def fibonacci(n): si n
< 2:
volver 1
devuelve fibonacci(n-1) + fibonacci(n-2)

Esta es la forma más intuitiva de escribir el problema. Como mucho, el espacio de la pila será O(n) a medida que descienda por la primera rama
recursiva haciendo llamadas a fibonacci(n-1) hasta llegar al caso base n < 2.

La prueba de complejidad del tiempo de ejecución O(2^n) que se puede ver aquí: Complejidad computacional de la secuencia de Fibonacci. El punto
principal a tener en cuenta es que el tiempo de ejecución es exponencial, lo que significa que el tiempo de ejecución se duplicará para cada término
subsiguiente, fibonacci(15) tardará el doble que fibonacci(14).

Complejidad de tiempo de ejecución O(n) memorizada , complejidad de espacio O(n) , complejidad de pila O(n)

memo = []
memo.append(1) # f(1) = 1
memo.append(1) # f(2) = 1

def fibonacci(n): si
len(nota) > n: devuelve
nota[n]

GoalKicker.com – Notas de algoritmos para profesionales 73


Machine Translated by Google

resultado = fibonacci(n-1) + fibonacci(n-2)


memo.append(resultado) # f(n) = f(n-1) + f(n-2) devolver
resultado

Con el enfoque memorizado, introducimos una matriz que se puede considerar como todas las llamadas de función anteriores. La ubicación
memo[n] es el resultado de la llamada a la función fibonacci(n). Esto nos permite cambiar la complejidad del espacio de O(n) por un tiempo de
ejecución de O(n) , ya que ya no necesitamos calcular llamadas de funciones duplicadas.

Programación dinámica iterativa O(n) Complejidad de tiempo de ejecución, O(n) Complejidad de espacio, Sin pila recursiva

def fibonacci(n):
memo = [1,1] # f(0) = 1, f(1) = 1

for i in range(2, n+1):


memo.append(memo[i-1] + memo[i-2])

memorándum de devolución [n]

Si desglosamos el problema en sus elementos centrales, notará que para calcular fibonacci(n) necesitamos fibonacci(n-1) y fibonacci(n-2).
También podemos notar que nuestro caso base aparecerá al final de ese
árbol recursivo como se ve arriba.

Con esta información, ahora tiene sentido calcular la solución hacia atrás, comenzando en los casos base y trabajando hacia arriba. Ahora,
para calcular fibonacci(n) , primero calculamos todos los números de fibonacci hasta n.

El principal beneficio aquí es que ahora hemos eliminado la pila recursiva mientras mantenemos el tiempo de ejecución O(n) .
Desafortunadamente, todavía tenemos una complejidad de espacio O(n) , pero eso también se puede cambiar.

Programación dinámica iterativa avanzada O(n) Complejidad de tiempo de ejecución, O(1) Complejidad de espacio, Sin pila recursiva

def fibonacci(n):
memo = [1,1] # f(1) = 1, f(2) = 1

para i en el rango (2, n):


memo[i%2] = memo[0] + memo[1]

devolver nota[n%2]

Como se señaló anteriormente, el enfoque de programación dinámica iterativa comienza desde los casos base y trabaja hasta el resultado
final. La observación clave que se debe hacer para llegar a la complejidad del espacio a O(1) (constante) es la misma observación que hicimos
para la pila recursiva: solo necesitamos fibonacci(n-1) y fibonacci(n-2) para construir fibonacci(n). Esto significa que solo necesitamos guardar
los resultados de fibonacci(n-1) y fibonacci(n-2) en cualquier punto de nuestra iteración.

Para almacenar estos últimos 2 resultados, uso una matriz de tamaño 2 y simplemente cambio el índice al que estoy
asignando usando i % 2 , que alternará así: 0, 1, 0, 1, 0, 1, ...,
yo % 2.

Sumo ambos índices de la matriz porque sabemos que la suma es conmutativa (5 + 6 = 11 y 6 + 5 == 11). Luego, el resultado
se asigna al más antiguo de los dos puntos (indicado por i % 2). El resultado final se almacena en la posición n%2

notas

Es importante tener en cuenta que a veces puede ser mejor idear una solución memorizada iterativa para

GoalKicker.com – Notas de algoritmos para profesionales 74


Machine Translated by Google

funciones que realizan cálculos grandes repetidamente, ya que acumulará un caché de la respuesta a la
las llamadas a funciones y las llamadas subsiguientes pueden ser O(1) si ya se ha calculado.

GoalKicker.com – Notas de algoritmos para profesionales 75


Machine Translated by Google

Capítulo 16: Algoritmo de Kruskal


Sección 16.1: Implementación óptima basada en conjuntos disjuntos
Podemos hacer dos cosas para mejorar los subalgoritmos de conjuntos disjuntos simples y subóptimos:

1. Heurística de compresión de ruta: findSet no necesita manejar nunca un árbol con una altura superior a 2. Si termina
iterando dicho árbol, puede vincular los nodos inferiores directamente a la raíz, optimizando futuros recorridos;

subalgo findSet(v: un nodo): if


v.parent != v v.parent =
findSet(v.parent) return v.parent

2. Heurística de fusión basada en la altura: para cada nodo, almacene la altura de su subárbol. Al fusionar, haga que el
árbol más alto el padre del más pequeño, por lo que no aumenta la altura de nadie.

subalgo unionSet(u, v: nodos): vRoot =


findSet(v) uRoot = findSet(u)

si vRoot == uRoot:
devolver

si vRoot.height < uRoot.height:


vRoot.parent = uRoot else
if vRoot.height > uRoot.height: uRoot.parent =
vRoot else:

uRoot.parent = vRoot
uRoot.height = uRoot.height + 1

Esto lleva al tiempo O(alfa(n)) para cada operación, donde alfa es el inverso de la función de Ackermann de rápido crecimiento,
por lo tanto, es de crecimiento muy lento y puede considerarse O(1) para propósitos prácticos.

Esto hace que todo el algoritmo de Kruskal sea O(m log m + m) = O(m log m), debido a la clasificación inicial.

Nota

La compresión de ruta puede reducir la altura del árbol, por lo que comparar las alturas de los árboles durante la operación de unión
puede no ser una tarea trivial. Por lo tanto, para evitar la complejidad de almacenar y calcular la altura de los árboles, el padre
resultante se puede elegir al azar:

subalgo unionSet(u, v: nodos): vRoot =


findSet(v) uRoot = findSet(u)

si vRoot == uRoot:
devolver
si aleatorio() % 2 == 0:
vRoot.parent = uRoot más:

uRoot.parent = raíz virtual

En la práctica, este algoritmo aleatorio junto con la compresión de ruta para la operación findSet dará como resultado

GoalKicker.com – Notas de algoritmos para profesionales 76


Machine Translated by Google

rendimiento comparable, pero mucho más simple de implementar.

Sección 16.2: Implementación simple y más detallada


Para manejar eficientemente la detección de ciclos, consideramos cada nodo como parte de un árbol. Al agregar un borde, verificamos si
sus dos nodos componentes son parte de árboles distintos. Inicialmente, cada nodo forma un árbol de un nodo.

algoritmo kruskalMST(G: un grafo) ordena


las aristas de G por su valor MST = un
bosque de árboles, inicialmente cada árbol es un nodo en el grafo para cada arista e en G: si
la raíz del árbol al que pertenece e.first es no es lo mismo que la raíz del árbol al que pertenece
e.second : conecta una de las raíces con la otra, fusionando así dos árboles

devolver MST, que ahora es un bosque de un solo árbol

Sección 16.3: Implementación simple basada en conjuntos disjuntos


La metodología forestal anterior es en realidad una estructura de datos de conjuntos disjuntos, que implica tres operaciones principales:

subalgo makeSet(v: un nodo): <-


v.parent = v hacer un nuevo árbol enraizado en v

subalgo findSet(v: un nodo): if


v.parent == v: return v

devuelve findSet(v.parent)

subalgo unionSet(v, u: nodos): vRoot =


findSet(v) uRoot = findSet(u)

uRoot.parent = raíz virtual

algoritmo kruskalMST(G: un gráfico): ordenar


los bordes de Gs por su valor para cada
nodo n en G:
makeSet(n)
para cada arista e en G: if
findSet(e.primero) != findSet(e.segundo):
unionSet(e.primero, e.segundo)

Esta implementación ingenua conduce a un tiempo O(n log n) para administrar la estructura de datos del conjunto disjunto, lo que lleva a
un tiempo O(m*n log n) para todo el algoritmo de Kruskal.

Sección 16.4: Implementación simple y de alto nivel


Ordene los bordes por valor y agregue cada uno al MST en orden ordenado, si no crea un ciclo.

algoritmo kruskalMST (G: un gráfico) ordena


los bordes de G por su valor
MST = un gráfico vacío
para cada arista e en G: si
agregar e a MST no crea un ciclo:
agregar e a MST

GoalKicker.com – Notas de algoritmos para profesionales 77


Machine Translated by Google
devolver MST

GoalKicker.com – Notas de algoritmos para profesionales 78


Machine Translated by Google

Capítulo 17: Algoritmos codiciosos


Sección 17.1: Codificación Huÿman
codigo huffman es un tipo particular de código de prefijo óptimo que se usa comúnmente para la compresión de datos sin pérdidas.
Comprime datos de manera muy efectiva ahorrando del 20% al 90% de memoria, dependiendo de las características de los datos que
se comprimen. Consideramos que los datos son una secuencia de caracteres. El algoritmo codicioso de Huffman utiliza una tabla que
indica la frecuencia con la que aparece cada carácter (es decir, su frecuencia) para construir una forma óptima de representar cada carácter
como una cadena binaria. El código Huffman fue propuesto por David A. Huffman en 1951.

Supongamos que tenemos un archivo de datos de 100.000 caracteres que deseamos almacenar de forma compacta. Suponemos que
solo hay 6 caracteres diferentes en ese archivo. La frecuencia de los caracteres viene dada por:

+------------------------+-----+-----+-----+-----+ -----+-----+
| Personaje | un | segundo | do | re | mi | f |
+------------------------+-----+-----+-----+-----+ -----+-----+
|Frecuencia (en miles)| 45 | 13 | 12 | 16 | 9 | 5 |
+------------------------+-----+-----+-----+-----+ -----+-----+

Tenemos muchas opciones sobre cómo representar dicho archivo de información. Aquí, consideramos el problema de diseñar un código de
caracteres binarios en el que cada carácter está representado por una cadena binaria única, a la que llamamos palabra clave.

El árbol construido nos proporcionará:

+------------------------+-----+-----+-----+-----+ -----+-----+
| Personaje | un | segundo | do | re | mi | f |
+------------------------+-----+-----+-----+-----+ -----+-----+
| Palabra clave de longitud fija | 000 | 001 | 010 | 011 | 100 | 101 |
+------------------------+-----+-----+-----+-----+ -----+-----+
|Palabra clave de longitud variable| 0 | 101 | 100 | 111 | 1101| 1100|
+------------------------+-----+-----+-----+-----+ -----+-----+

Si usamos un código de longitud fija, necesitamos tres bits para representar 6 caracteres. Este método requiere 300.000 bits para
codificar todo el archivo. Ahora la pregunta es, ¿podemos hacerlo mejor?

GoalKicker.com – Notas de algoritmos para profesionales 79


Machine Translated by Google

Un código de longitud variable puede funcionar considerablemente mejor que un código de longitud fija, dando palabras de código cortas a los
caracteres frecuentes y palabras de código largas a los caracteres poco frecuentes. Este código requiere: (45 X 1 + 13 X 3 + 12 X 3 + 16 X 3 + 9 X 4
+ 5 X 4) X 1000 = 224000 bits para representar el archivo, lo que ahorra aproximadamente un 25 % de memoria.

Una cosa para recordar, consideramos aquí solo códigos en los que ninguna palabra clave es también un prefijo de alguna otra palabra
clave. Estos se llaman códigos de prefijo. Para la codificación de longitud variable, codificamos el archivo de 3 caracteres abc como 0.101.100 =
0101100, donde "." denota la concatenación.

Los códigos de prefijo son deseables porque simplifican la decodificación. Dado que ninguna palabra clave es un prefijo de otra, la
palabra clave que comienza un archivo codificado no es ambigua. Simplemente podemos identificar la palabra clave inicial, traducirla de nuevo al
carácter original y repetir el proceso de decodificación en el resto del archivo codificado. Por ejemplo, 001011101 se analiza de forma única como
0.0.101.1101, que se decodifica en aabe. En resumen, todas las combinaciones de representaciones binarias son únicas. Digamos, por ejemplo, que
si una letra se denota por 110, ninguna otra letra se denotará por 1101 o 1100. Esto se debe a que podría confundirse sobre si seleccionar 110 o
continuar concatenando el siguiente bit y seleccionar ese.

Técnica de compresión:

La técnica funciona mediante la creación de un árbol binario de nodos. Estos pueden almacenarse en una matriz regular, cuyo tamaño
depende del número de símbolos, n. Un nodo puede ser un nodo hoja o un nodo interno. Inicialmente, todos los nodos son nodos hoja, que
contienen el símbolo en sí, su frecuencia y, opcionalmente, un enlace a sus nodos secundarios. Como convención, el bit '0' representa el hijo
izquierdo y el bit '1' representa el hijo derecho. La cola de prioridad se utiliza para almacenar los nodos, lo que proporciona el nodo con la frecuencia
más baja cuando aparece. El proceso se describe a continuación:

1. Cree un nodo de hoja para cada símbolo y agréguelo a la cola de prioridad.


2. Mientras haya más de un nodo en la cola: 1. Quite los dos nodos
de mayor prioridad de la cola.
2. Crear un nuevo nodo interno con estos dos nodos como hijos y con frecuencia igual a la suma de
la frecuencia de los dos nodos.
3. Agregue el nuevo nodo a la cola.
3. El nodo restante es el nodo raíz y el árbol de Huffman está completo.

Para nuestro ejemplo:

GoalKicker.com – Notas de algoritmos para profesionales 80


Machine Translated by Google

El pseudocódigo se parece a:

Procedimiento Huffman(C): // C es el conjunto de n caracteres e información relacionada


n = tamaño C
Q = cola_de_prioridad() para
i = 1 a nn = nodo(C[i])

Q. empujar (n)
end for
while Q.size() no es igual a 1 Z = new node()

Z.izquierda = x = Q.pop
Z.derecha = y = Q.pop
Z.frequency = x.frequency + y.frequency Q.push(Z) end
while

Devolver Q

Aunque el tiempo lineal da una entrada ordenada, en casos generales de entrada arbitraria, el uso de este algoritmo requiere una clasificación
previa. Por lo tanto, dado que la clasificación toma un tiempo O (nlogn) en casos generales, ambos métodos tienen la misma complejidad.

Dado que aquí n es el número de símbolos en el alfabeto, que suele ser un número muy pequeño (en comparación con la longitud del mensaje a
codificar), la complejidad del tiempo no es muy importante en la elección de este algoritmo.

Técnica de descompresión:

El proceso de descompresión es simplemente una cuestión de traducir el flujo de códigos de prefijo a un valor de byte individual, generalmente
recorriendo el árbol de Huffman nodo por nodo a medida que se lee cada bit del flujo de entrada. Llegar a un nodo hoja termina necesariamente la
búsqueda de ese valor de byte en particular. El valor de la hoja representa el deseado

GoalKicker.com – Notas de algoritmos para profesionales 81


Machine Translated by Google

personaje. Por lo general, el árbol de Huffman se construye utilizando datos ajustados estadísticamente en cada ciclo de compresión,
por lo que la reconstrucción es bastante simple. De lo contrario, la información para reconstruir el árbol debe enviarse por separado. El
pseudocódigo:

Procedimiento HuffmanDecompression(root, S): // root representa la raíz de Huffman Tree n := S.length // S se refiere al flujo de bits a
descomprimir para i := 1 a n

corriente = raíz
while current.left != NULL y current.right != NULL si S[i] es igual a '0'

actual := actual.izquierda
else
actual := actual.right endif

i := i+1
endwhile imprime
actual.símbolo endfor

Explicación codiciosa:
la codificación de Huffman analiza la aparición de cada carácter y lo almacena como una cadena binaria de manera óptima.
La idea es asignar códigos de longitud variable a los caracteres de entrada de entrada, la longitud de los códigos asignados
se basa en las frecuencias de los caracteres correspondientes. Creamos un árbol binario y lo operamos de forma ascendente
para que los dos caracteres menos frecuentes estén lo más lejos posible de la raíz. De esta forma, el carácter más frecuente
obtiene el código más pequeño y el carácter menos frecuente obtiene el código más grande.

Referencias:

Introducción a los algoritmos - Charles E. Leiserson, Clifford Stein, Ronald Rivest y Thomas H. Cormen Huffman
Coding - Wikipedia Matemáticas discretas y sus aplicaciones - Kenneth H. Rosen

Sección 17.2: Problema de selección de actividades


El problema

Tienes un conjunto de cosas que hacer (actividades). Cada actividad tiene una hora de inicio y una hora de finalización. No
se le permite realizar más de una actividad a la vez. Su tarea es encontrar una manera de realizar el máximo número de actividades.

Por ejemplo, suponga que tiene una selección de clases para elegir.

Actividad No. hora de inicio hora de


finalización 1 10.20 AM 11.00 AM

2 10:30 11:30
3 11:00 12:00
4 10:00 11:30
5 9:00 11:00

Recuerda, no puedes tomar dos clases al mismo tiempo. Eso significa que no puede tomar las clases 1 y 2 porque
comparten un horario común de 10:30 a. m. a 11:00 a. m. Sin embargo, puede tomar las clases 1 y 3 porque no
comparten un horario común. Entonces, su tarea es tomar la mayor cantidad posible de clases sin superposición. Como
puedes hacer eso?

Análisis

GoalKicker.com – Notas de algoritmos para profesionales 82


Machine Translated by Google

Pensemos en la solución mediante un enfoque codicioso. En primer lugar, elegimos al azar algún enfoque y verificamos que
trabajar o no.

ordene la actividad por hora de inicio, lo que significa qué actividad comienza primero, la tomaremos primero. entonces tome primero para
último de la lista ordenada y verifique que se cruce con la actividad anterior tomada o no. Si la actividad actual no es
se cruzan con la actividad realizada anteriormente, realizaremos la actividad, de lo contrario no la realizaremos. este
enfoque funcionará para algunos casos como

N.º de actividad hora de inicio hora de finalización


1 11:00 13:30
2 11:30 12:00
3 13:30 14:00
4 10:00 11:00

el orden de clasificación será 4-->1-->2-->3. Se realizará la actividad 4--> 1--> 3 y se omitirá la actividad 2.
se realizará el máximo de 3 actividades. Sirve para este tipo de casos. pero fallará en algunos casos. vamos a aplicar
este enfoque para el caso

N.º de actividad hora de inicio hora de finalización


1 11:00 13:30
2 11:30 12:00
3 13:30 14:00
4 10:00 15:00

El orden de clasificación será 4-->1-->2-->3 y solo se realizará la actividad 4, pero la respuesta puede ser la actividad 1-->3 o 2-
->3 se realizará. Así que nuestro enfoque no funcionará para el caso anterior. Probemos otro enfoque

Ordene la actividad por duración de tiempo , lo que significa realizar primero la actividad más corta. que puede resolver lo anterior
problema . Aunque el problema no está completamente resuelto. Todavía hay algunos casos en los que puede fallar la solución.
Aplicar este enfoque en el caso de abajo.

N.º de actividad hora de inicio hora de finalización


1 6:00 11:40
2 11:30 12:00
3 23:40 14:00

si ordenamos la actividad por duración, el orden de clasificación será 2--> 3 --->1, no . y si realizamos la actividad No. 2 primero entonces
se puede realizar ninguna otra actividad. Pero la respuesta será realizar la actividad 1 y luego realizar 3 . Para que podamos realizar
actividades como máximo 2. Por lo tanto, esta no puede ser una solución a este problema. Deberíamos intentar un enfoque diferente.

La solución

Ordene la actividad por hora de finalización, lo que significa que la actividad termina primero que llega primero. se da el algoritmo
abajo

1. Ordenar las actividades por sus horas de finalización.

2. Si la actividad a realizar no comparte un tiempo común con las actividades que anteriormente
realizado, realizar la actividad.

Analicemos el primer ejemplo.

GoalKicker.com – Notas de algoritmos para profesionales 83


Machine Translated by Google

Actividad No. hora de inicio hora de


11.00 AM 1 finalización 10.20 AM

2 10:30 11:30
3 11:00 12:00
4 10:00 11:30
5 9:00 11:00

Se realizará la ordenación de la actividad , Así que el orden de clasificación será 1-->5-->2-->4-->3.. la respuesta es 1-->3 estas dos actividades
por sus horas de finalización. y esa es la respuesta. aquí está el código sudo.

1. ordenar: actividades

2. realizar la primera actividad de la lista ordenada de actividades.


3. Establezca: Actividad_actual:= primera
actividad 4. Establezca: hora_finalización:= hora_finalización
de la actividad actual 5. Vaya a la siguiente actividad si existe, .
si no existe, termine 6. Si hora_inicial de la actividad actual <= hora_finalización: realice la actividad
y vaya a 4 7. más: llegó a 5.

consulte aquí para obtener ayuda con la codificación http://www.geeksforgeeks.org/greedy-algorithms-set-1-activity-selection-problem/

Sección 17.3: Problema de creación de cambios


Dado un sistema monetario, ¿es posible dar una cantidad de monedas y cómo encontrar un conjunto mínimo de monedas
correspondiente a esta cantidad?

Sistemas monetarios canónicos. Para algunos sistemas monetarios, como los que usamos en la vida real, la solución "intuitiva"
funciona perfectamente. Por ejemplo, si las diferentes monedas y billetes de euro (excluidos los céntimos) son de 1€, 2€, 5€, 10€,
dando la moneda o billete más alto hasta llegar a la cantidad y repitiendo este procedimiento se obtendrá el conjunto mínimo de monedas. .

Podemos hacerlo recursivamente con OCaml:

(* suponiendo que el sistema monetario esté ordenado en orden decreciente


*) let change_make money_system cantidad = let rec loop cantidad dada =
if cantidad = 0 luego dado otra cosa (* encontramos el primer valor
menor o igual a la cantidad restante *) let moneda = List.find ((>=)
cantidad) sistema_dinero en bucle (moneda::dada) (cantidad -
moneda) en bucle [] cantidad

Estos sistemas están hechos para que hacer cambios sea fácil. El problema se vuelve más difícil cuando se trata de un sistema monetario
arbitrario.

Caso general. ¿Cómo dar 99€ con monedas de 10€, 7€ y 5€? Aquí dar monedas de 10€ hasta quedarnos con 9€ obviamente no tiene
solución. Peor que eso, puede que no exista una solución. Este problema es de hecho np-difícil, pero existen soluciones aceptables que
mezclan codicia y memorización . La idea es explorar todas las posibilidades y elegir la que tenga
el mínimo número de monedas.

Para dar una cantidad X > 0, elegimos una pieza P en el sistema monetario y luego resolvemos el subproblema correspondiente a XP.
Probamos esto para todas las piezas del sistema. La solución, si existe, es entonces el camino más pequeño que lleva a 0.

Aquí una función recursiva OCaml correspondiente a este método. Devuelve Ninguno, si no existe solución.

GoalKicker.com – Notas de algoritmos para profesionales 84


Machine Translated by Google

(* utilidades de opción *) let


optmin xy = emparejar x,y con

| Ninguno,a | a,Ninguno -> a


| Algunos x, Algunos y-> Algunos (min x y)

let optsucc = función


| Algunos x -> Algunos (x+1)
| Ninguno -> Ninguno

(* Problema de hacer cambios *) let


change_make money_system cantidad = let rec loop n
= let onepiece acc piece = emparejar n - pieza con |
0 -> (*problema resuelto con una moneda*)

Algunos
1 | x -> si x < 0 entonces (*no
llegamos a 0, descartamos esta solución*)

Ninguno
más (*buscamos el camino más pequeño diferente a Ninguno con las piezas restantes*) optmin (optsucc (bucle
x)) acc
en
(*llamamos onepiece a todas las piezas*)
List.fold_left onepiece Ninguno money_system
en cantidad de bucle

Nota: podemos comentar que este procedimiento puede calcular varias veces el conjunto de cambios para el mismo valor. En la
práctica, usar la memorización para evitar estas repeticiones conduce a resultados más rápidos (mucho más rápidos).

GoalKicker.com – Notas de algoritmos para profesionales 85


Machine Translated by Google

Capítulo 18: Aplicaciones de la técnica Greedy

Sección 18.1: Almacenamiento en caché fuera de línea

El problema del almacenamiento en caché surge de la limitación del espacio finito. Supongamos que nuestro caché C tiene k páginas. Ahora queremos
procesar una secuencia de m solicitudes de elementos que deben haberse colocado en la memoria caché antes de que se procesen. Por supuesto, si
m<=k , simplemente colocamos todos los elementos en la memoria caché y funcionará, pero por lo general es m> > k.

Decimos que una solicitud es un acierto de caché, cuando el elemento ya está en caché; de lo contrario, se llama pérdida de caché. En ese
caso, debemos llevar el elemento solicitado a la memoria caché y expulsar otro, suponiendo que la memoria caché esté llena. El objetivo es
un calendario de desalojos que minimice el número de desalojos.

Existen numerosas estrategias codiciosas para este problema, veamos algunas:

1. Primero en entrar, primero en salir (FIFO): se expulsa la página


más antigua 2. Último en entrar, primero en salir (LIFO) : se expulsa
la página más nueva 3. Última salida reciente (LRU): página de expulsión cuyo acceso más
reciente fue el más antiguo 4 .Solicitud con menor frecuencia (LFU): desalojar la página que se solicitó
con menos frecuencia 5. Distancia de reenvío más larga (LFD): desalojar la página en el caché que no se solicita hasta el futuro más lejano.

Atención: Para los siguientes ejemplos, desalojamos la página con el índice más pequeño, si se puede desalojar más de
una página.

Ejemplo (PEPS)

Deje que el tamaño del caché sea k = 3 el caché inicial a,b,c y la solicitud a,a,d,e,b,b,a,c,f,d,e,a,f,b,e,c :

Solicitar aadebbacfdeafbec caché 1


aaddddaaadddfffc

caché 2 bbbeeeeccceeebbb
caché 3 ccccbbbbfffaaaee
error de caché xxxxxxxxxxxx

Trece errores de caché por dieciséis solicitudes no suena muy óptimo, probemos el mismo ejemplo con otra estrategia:

Ejemplo (LFD)

Deje que el tamaño del caché sea k = 3 el caché inicial a,b,c y la solicitud a,a,d,e,b,b,a,c,f,d,e,a,f,b,e,c :

Solicitar aadebbacfdeafbec caché 1


aadeeeeeeeeeeeec

caché 2 bbbbbbaaaaaaffff
caché 3 ccccccccfddddbbb
error de caché XX xxx xxx

Ocho fallos de caché es mucho mejor.

Autoevaluación: haga el ejemplo de LIFO, LFU, RFU y observe lo que sucedió.

El siguiente programa de ejemplo (escrito en C++) consta de dos partes:

GoalKicker.com – Notas de algoritmos para profesionales 86


Machine Translated by Google

El esqueleto es una aplicación que resuelve el problema dependiendo de la estrategia codiciosa elegida:

#incluir <iostream>
#include <memoria>

utilizando el espacio de nombres estándar;

const int cacheSize const = 3;


int requestLength = 16;

const char solicitud[] char = {'a','a','d','e','b','b','a','c','f','d','e','a', 'f', 'b', 'e', 'c'}; = {'a','b','c'};
cache[]

// para
restablecer char originalCache[] = {'a','b','c'};

estrategia de clase {

público:
Estrategia(std::string nombre) : estrategiaNombre(nombre) {}
virtual ~Estrategia() = predeterminado;

// calcular qué lugar de caché debe usarse virtual int apply


(int requestIndex) = 0;

// actualiza la información que la estrategia necesita


actualización de vacío virtual (int cachePlace, int requestIndex, bool cacheMiss) = 0;

const std::string nombreEstrategia;


};

bool updateCache(int requestIndex, Estrategia* estrategia) {

// calcular dónde colocar la solicitud int


cachePlace = estrategia->apply(requestIndex);

// probar si es un acierto o un error de caché bool isMiss =


request[requestIndex] != cache[cachePlace];

// actualizar estrategia (por ejemplo, contar distancias) estrategia-


>actualizar(cachePlace, requestIndex, isMiss);

// escribe en caché
cache[cachePlace] = request[requestIndex];

volver es señorita;
}

int principal()
{
Estrategia* estrategiaseleccionada [] = { nueva FIFO, nueva LIFO, nueva LRU, nueva LFU, nueva LFD };

for (int estrato=0; estrato < 5; ++estrato) {

// restablecer
caché para (int i=0; i < cacheSize; ++i) cache[i] = originalCache[i];

endl; cout <<"\nEstrategia: " << estrategiaseleccionada[estrategia]-> nombreestrategia <<

cout << "\nCache initial: ("; for (int i=0;


i < cacheSize-1; ++i) cout << cache[i] << ",";

GoalKicker.com – Notas de algoritmos para profesionales 87


Machine Translated by Google

cout << cache[cacheSize-1] << ")\n\n";

cout << "Solicitud\t"; for (int


i=0; i < cacheSize; ++i) cout << "cache " cout << "cache miss" << endl; << yo << "\t";

int cntMisses = 0;

for(int i=0; i<requestLength; ++i) {

bool isMiss = updateCache(i, estrategiaseleccionada[estrategia]); if (isMiss) +


+cntMisses;

" "
cout << << solicitud[i] << "\t";
" "
for (int l=0; l < cacheSize; ++l) cout << cout << (isMiss ? "x" : "") << caché[l] << "\t";
<< endl;
}

"
cout<< "\nCaché total perdido: << cntMisses << endl;
}

for(int i=0; i<5; ++i) eliminar estrategiaseleccionada[i];


}

La idea básica es simple: por cada solicitud tengo dos llamadas, dos mi estrategia:

1. aplicar: la estrategia tiene que decirle a la persona que llama qué página
usar 2. actualizar: después de que la persona que llama usa el lugar, le dice a la estrategia si falló o no. Entonces la estrategia puede
actualizar sus datos internos. La estrategia LFU , por ejemplo, tiene que actualizar la frecuencia de aciertos para las páginas de
caché, mientras que la estrategia LFD tiene que recalcular las distancias para las páginas de caché.

Ahora veamos implementaciones de ejemplo para nuestras cinco estrategias:

FIFO

clase FIFO : Estrategia pública { pública:

FIFO() : Estrategia("FIFO") {

for (int i=0; i<cacheSize; ++i) edad[i] = 0;


}

int apply(int requestIndex) override {

int mayor = 0;

for(int i=0; i<cacheSize; ++i) {

if(cache[i] == request[requestIndex]) return i;

else if(edad[i] > edad[mayor]) mayor = i;

devolver el más antiguo;


}

anular la actualización (int cachePos, int requestIndex, bool cacheMiss) anular {

// nada cambió, no necesitamos actualizar las edades

GoalKicker.com – Notas de algoritmos para profesionales 88


Machine Translated by Google

si (!cacheMiss)
devolver;

// todas las paginas viejas envejecen, la nueva se vuelve 0


for(int i=0; i<cacheSize; ++i)
{
if(i != cachePos)
edad[i]++;

más
edad[i] = 0;
}
}

privado:
int edad[cacheSize];
};

FIFO solo necesita la información de cuánto tiempo está una página en el caché (y, por supuesto, solo en relación con las otras páginas). Asi que
lo único que hay que hacer es esperar a que se pierda y luego hacer las páginas, que no fueron desalojadas más antiguas. Para nuestro ejemplo
arriba la solución del programa es:

Estrategia: FIFO

Caché inicial: (a,b,c)

Solicitud caché 0 caché 1 caché 2 error de caché


a a C
a a C
d d bbb C X
mi mi C X
cama y mi cama y X
desayuno ddd mi desayuno

a a mi X
C a C X
F a C X
d C bbff X
mi mi F X
a mi a X
F ddf mi a X
b f a X
mi mi X
C C bbb mi X

Total de errores de caché: 13

Esa es exactamente la solución de arriba.

LIFO

clase LIFO : estrategia pública {


público:
LIFO() : Estrategia("LIFO")
{
for (int i=0; i<cacheSize; ++i) edad[i] = 0;
}

anular int apply(int requestIndex)


{
int más nuevo = 0;

GoalKicker.com – Notas de algoritmos para profesionales 89


Machine Translated by Google

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


{
if(cache[i] == request[requestIndex])
devolver yo;

else if(edad[i] < edad[más reciente])


más nuevo = yo;
}

volver más reciente;


}

anular la actualización (int cachePos, int requestIndex, bool cacheMiss) anular


{
// nada cambió, no necesitamos actualizar las edades
si (!cacheMiss)
devolver;

// todas las paginas viejas envejecen, la nueva se vuelve 0


for(int i=0; i<cacheSize; ++i)
{
if(i != cachePos)
edad[i]++;

más

edad[i] = 0;
}
}

privado:
int edad[cacheSize];
};

La implementación de LIFO es más o menos la misma que la de FIFO pero desalojamos la página más joven, no la más antigua. los
Los resultados del programa son:

Estrategia: LIFO

Caché inicial: (a,b,c)

Solicitud caché 0 caché 1b _ caché 2 error de caché


a a C
a a C
d d C X
mi mi bbb C X
cama y mi C
desayuno mi C
a a bbb C X
C a C
C X
f.d. f.d. bbb C X
mi mi b C X
a a C X
pensión f C X
completa bbb C
mi mi cama y C X
C mi desayuno C

Errores totales de caché: 9

LRU

GoalKicker.com – Notas de algoritmos para profesionales 90


Machine Translated by Google

clase LRU : estrategia pública {


público:
LRU() : Estrategia("LRU")
{
for (int i=0; i<cacheSize; ++i) edad[i] = 0;
}

// aquí más antiguo significa que no se usó por más tiempo


anular int apply(int requestIndex)
{
int mayor = 0;

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


{
if(cache[i] == request[requestIndex])
devolver yo;

else if(edad[i] > edad[mayor])


mayor = yo;
}

devolver el más antiguo;


}

anular la actualización (int cachePos, int requestIndex, bool cacheMiss) anular


{
// todas las paginas viejas envejecen, las usadas se vuelven 0
for(int i=0; i<cacheSize; ++i)
{
if(i != cachePos)
edad[i]++;

más
edad[i] = 0;
}
}

privado:
int edad[cacheSize];
};

En el caso de LRU , la estrategia es independiente de lo que hay en la página de caché, su único interés es el último uso. los

Los resultados del programa son:

Estrategia: LRU

Caché inicial: (a,b,c)

Solicitud caché 0 caché 1 caché 2 error de caché


a a C
a a C
d a bbd C X
mi a mi X
cama y cama y mi X
desayuno desayuno ddd mi

a b a mi X
C a C X
a C X
f.d. mejores amigos d C X
mi F mi X
a a dd mi X

GoalKicker.com – Notas de algoritmos para profesionales 91


Machine Translated by Google

F a F mi X
b a f X
mi mi X
C mi C bbb X

Total de errores de caché: 13

LFU

clase LFU : estrategia pública {


público:
LFU() : Estrategia("LFU")
{
for (int i=0; i<cacheSize; ++i) requestFrequency[i] = 0;
}

anular int apply(int requestIndex)


{
int mínimo = 0;

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


{
if(cache[i] == request[requestIndex])
devolver yo;

else if(peticiónFrecuencia[i] < peticiónFrecuencia[menor])


menos = yo;
}

volver menos;
}

anular la actualización (int cachePos, int requestIndex, bool cacheMiss) anular


{
si (señorita caché)
requestFrequency[cachePos] = 1;

más
++requestFrequency[cachePos];
}

privado:

// con qué frecuencia se usó la página


int requestFrequency[cacheSize];
};

LFU expulsa la página que se usa con menos frecuencia. Entonces, la estrategia de actualización es solo contar cada acceso. Por supuesto, después de perder el

cuenta se reinicia. Los resultados del programa son:

Estrategia: LFU

Caché inicial: (a,b,c)

Solicitud caché 0 caché 1 caché 2 error de caché


a a cama y C
a a desayuno C
d a d C X
mi a mi X
cama y a mi X
desayuno a dbb mi

a a b mi

92
GoalKicker.com – Notas de algoritmos para profesionales
Machine Translated by Google
C a b C X
a X
f.d. a f.d. X
mi a bbb mi X
a a mi

pensión a f X
completa a bbb
mi a cama y mi X
C a desayuno C X

Errores totales de caché: 10

LFD

clase LFD : estrategia pública {


público:
LFD() : Estrategia("LFD")
{
// precalcula el próximo uso antes de comenzar a completar las solicitudes
for (int i=0; i<cacheSize; ++i) nextUse[i] = calcNextUse(-1, cache[i]);
}

anular int apply(int requestIndex)


{
int último = 0;

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


{
if(cache[i] == request[requestIndex])
devolver yo;

else if(próximoUso[i] > próximoUso[último])


último = yo;
}

volver más reciente;


}

anular la actualización (int cachePos, int requestIndex, bool cacheMiss) anular


{
nextUse[cachePos] = calcNextUse(requestIndex, cache[cachePos]);
}

privado:

int calcNextUse(int requestPosition, char pageItem)


{
for(int i = posiciónsolicitud+1; i < longitudsolicitud; ++i)
{
if (solicitud[i] == elemento de página)
devolver yo;
}

volver solicitudLongitud + 1;
}

// próximo uso de la página


int nextUse[cacheSize];
};

La estrategia LFD es diferente a todas las anteriores. Es la única estrategia que utiliza las solicitudes futuras para su
decisión de a quién desalojar. La implementación usa la función calcNextUse para obtener la página cuyo próximo uso es

GoalKicker.com – Notas de algoritmos para profesionales 93


Machine Translated by Google

más lejano en el futuro. La solución del programa es igual a la solución a mano de arriba:

Estrategia: LFD

Caché inicial: (a,b,c)

Solicitud caché 0 caché 1 bb caché 2 error de caché


a a C
a a C
d a d X
mi a mi X
b a bbb mi

b a b mi

a a b mi

C a C mi X
F a F mi X
d a mi X
mi a mi

a a ddd mi

pensión mi X
completa mi X
mi fbb ddd mi

C C d mi X

Errores totales de caché: 8

La estrategia codiciosa LFD es de hecho la única estrategia óptima de las cinco presentadas. La demostración es bastante larga y puede
ser encontrado aquí o en el libro de Jon Kleinberg y Eva Tardos (consulte las fuentes en los comentarios a continuación).

Algoritmo vs Realidad

La estrategia LFD es óptima, pero hay un gran problema. Es una solución fuera de línea óptima . En la práctica, el almacenamiento en caché suele ser
un problema en línea , eso significa que la estrategia es inútil porque no podemos ahora la próxima vez que necesitemos un particular
artículo. Las otras cuatro estrategias también son estrategias en línea . Para problemas en línea necesitamos un general diferente
Acercarse.

Sección 18.2: Boleto automático


Primer ejemplo sencillo:

Dispone de un autómata de billetes que da cambio en monedas de valor 1, 2, 5, 10 y 20. La dispensación del
el intercambio puede verse como una serie de caídas de monedas hasta que se dispensa el valor correcto. Decimos que una dispensación es óptima
cuando su recuento de monedas es mínimo para su valor.

Sea M en [1,50] el precio del billete T y P en [1,50] el dinero que alguien pagó por T, siendo P >= M. Sea D=PM.
Definimos el beneficio de un paso como la diferencia entre D y Dc con c la moneda que dispensa el autómata en este
paso.

La técnica codiciosa para el intercambio es el siguiente enfoque pseudo algorítmico:

Paso 1: mientras D > 20 dispensar una moneda de 20 y programar D = D - 20

Paso 2: mientras D > 10 dispensar una moneda de 10 y programar D = D - 10

Paso 3: mientras D > 5 dispensar una moneda de 5 y configurar D = D - 5

Paso 4: mientras D > 2 dispensa una moneda de 2 y establece D = D - 2

Paso 5: mientras D > 1 dispensar una moneda de 1 y configurar D = D - 1

94
GoalKicker.com – Notas de algoritmos para profesionales
Machine Translated by Google

Luego, la suma de todas las monedas es claramente igual a D. Es un algoritmo codicioso porque después de cada paso y después de
cada repetición de un paso, se maximiza el beneficio. No podemos dispensar otra moneda con un beneficio mayor.

Ahora el autómata de tickets como programa (en C++):

#include <iostream>
#include <vector>
#include <cadena>
#include <algoritmo>

utilizando el espacio de nombres estándar;

// leer algunos valores de monedas, ordenarlos de forma


descendente, // purgar copias y garantizar que la moneda 1 está en
ellas std::vector<unsigned int> readInCoinValues();

int principal()
{
std::vector<unsigned int> coinValues; // Array de valores de monedas ascendentes int ticketPrice;
// M en el ejemplo int
dineroPagado; // P en el ejemplo

// generar valores de monedas


coinValues = readInCoinValues();

cout << "precio del boleto: "; cin >>


precio del boleto;

cout << "dinero pagado: "; cin >>


dinero pagado;

if(dineropagado <= precio del boleto) {

cout << "No hay cambio de moneda" << endl;


devolver 1;
}

int diffValue = dinero pagado - precio del boleto;

// Aqui empieza codicioso

// ahorramos cuantas monedas tenemos para dar


std::vector<unsigned int> coinCount;

for(auto coinValue = coinValues.begin(); coinValue !=


coinValues.end(); ++coinValue)
{
int cuentaMonedas = 0;

while (diffValue >= *coinValue) {

diffValue -= *monedValue;
contarMonedas++;
}

coinCount.push_back(countMonedas);
}

// imprime el resultado cout


<< "la diferencia" se paga con: " << dineroPagado - PrecioBoleto <<
<< " endl;

GoalKicker.com – Notas de algoritmos para profesionales 95


Machine Translated by Google

for(sin signo int i=0; i < valoresmonedas.tamaño(); ++i) {

if(coinCount[i] > 0) cout <<


"
coinCount[i] << monedas con valor " << coinValues[i] << endl;

devolver 0;
}

std::vector< int sin firmar> readInCoinValues() {

// valores de monedas
std::vector<unsigned int> coinValues;

// asegúrese de que 1 esté en el vectore


coinValues.push_back(1);

// lee los valores de las monedas (atención: se omite el manejo de errores) while(true) {

int valormoneda;

cout << "Valor de la moneda (<1 para detener): "; cin


>> valormoneda;

if(coinValue > 0)
coinValues.push_back(coinValue);

de lo

contrario romper;

// ordenar valores
sort(coinValues.begin(), coinValues.end(), std::greater<int>());

// borrar copias del mismo valor auto last =


std::unique(coinValues.begin(), coinValues.end()); coinValues.erase(último, coinValues.end());

// imprime la matriz
cout << "Valores de monedas: ";

for(auto i : coinValues) " ";


cout << yo <<

cout << endl;

devolver valores de monedas;


}

Tenga en cuenta que ahora hay verificación de entrada para mantener el ejemplo simple. Un ejemplo de salida:

Valor de la moneda (<1 para detener): 2


Valor de la moneda (<1 para detener): 4
Valor de la moneda (<1 para detener): 7
Valor de la moneda (<1 para detener): 9
Valor de la moneda (<1 para detener): 14
Valor de la moneda (<1 para detener): 4
Valor de la moneda (<1 para detener): 0

GoalKicker.com – Notas de algoritmos para profesionales 96


Machine Translated by Google
Valores de monedas: 14 9 7 4 2 1
precio del billete: 34
dinero pagado: 67 la
diferencia 33 se paga con: 2 monedas de
valor 14 1 monedas de valor 4 1 monedas de
valor 1

Mientras 1 esté en los valores de la moneda, sabemos que el algoritmo terminará porque:

D disminuye estrictamente con cada paso


D nunca es >0 y es menor que la moneda más pequeña 1 al mismo tiempo

Pero el algoritmo tiene dos trampas:

1. Sea C el mayor valor de la moneda. El tiempo de ejecución es solo polinomial siempre que D/C sea polinomial, porque el
la representación de D usa solo bits de registro D y el tiempo de ejecución es al menos lineal en D/C.
2. En cada paso nuestro algoritmo elige el óptimo local. Pero esto no es suficiente para decir que el algoritmo encuentra la solución
óptima global (ver más información aquí o en el Libro de Korte y Vygen).

Un contraejemplo simple: las monedas son 1,3,4 y D=6. La solución óptima es claramente dos monedas de valor 3 , pero greedy elige 4
en el primer paso, por lo que tiene que elegir 1 en los pasos dos y tres. Por lo tanto, no da una solución óptima. Un posible algoritmo
óptimo para este ejemplo se basa en la programación dinámica.

Sección 18.3: Programación de intervalos


Tenemos un conjunto de trabajos J={a,b,c,d,e,f,g}. Sea j en J un trabajo que empieza en sj y termina en fj. Dos trabajos son compatibles
si no se superponen. Una imagen como ejemplo:

El objetivo es encontrar el subconjunto máximo de trabajos compatibles entre sí. Hay varios enfoques codiciosos para este problema:

1. Hora de inicio más temprana: considere los trabajos en orden ascendente


de sj 2. Hora de finalización más temprana: considere los trabajos en orden
ascendente de fj 3. Intervalo más corto: considere los trabajos en orden
ascendente de fj-sj 4. Menos conflictos: para cada trabajo j, contar el número de trabajos en conflicto cj

La pregunta ahora es, qué enfoque es realmente exitoso. Hora de inicio temprano definitivamente no, aquí hay un contraejemplo

GoalKicker.com – Notas de algoritmos para profesionales 97


Machine Translated by Google

El intervalo más corto tampoco es óptimo

y la menor cantidad de conflictos puede sonar óptima, pero aquí hay un caso problemático para este enfoque:

Lo que nos deja con el tiempo de finalización más temprano. El pseudocódigo es bastante simple:

1. Ordene los trabajos por hora de finalización para que


f1<=f2<=...<=fn 2. Sea A un conjunto vacío 3. para j =1 an si j es
compatible con todos los trabajos en el conjunto A A=A+ {j}
4. A es un subconjunto máximo de trabajos mutuamente compatibles

O como programa C++:

#include <iostream>
#include <utilidad>
#include <tupla> #include
<vector> #include
<algoritmo>

const int jobCnt = 10;

// Horas de inicio del


trabajo const int startTimes[] = { 2, 3, 1, 4, 3, 2, 6, 7, 8, 9};

// Horas de finalización
del trabajo const int endTimes[] = { 4, 4, 3, 5, 5, 5, 8, 9, 9, 10};

utilizando el espacio de nombres estándar;

int principal()
{
vector<par<int,int>> trabajos;

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


trabajos.push_back(make_pair(startTimes[i], endTimes[i]));

// paso 1: sort
sort(jobs.begin(), jobs.end(),[](pair<int,int> p1, pair<int,int> p2) { return p1.segundo <
p2.segundo; } );

GoalKicker.com – Notas de algoritmos para profesionales 98


Machine Translated by Google

// paso 2: conjunto vacío A


vector<int> A;

// paso 3:
for(int i=0; i<jobCnt; ++i) {

auto trabajo = trabajos


[i]; bool es compatible = verdadero;

para (índice de trabajo automático : A )


{
// probar si el trabajo real y el trabajo de A son incompatibles if(trabajo.segundo >=
trabajos[índicetrabajo].primero && trabajo.primero < = trabajos[índicetrabajo].segundo)

{
esCompatible = falso;
descanso;
}
}

si (es compatible)
A.push_back(i);
}

//paso 4: imprime A
cout << "Compatible: ";

for(auto i : A) cout
<< "(" << trabajos[i].primero << "," << trabajos[i].segundo << ") "; cout << endl;

devolver 0;
}

La salida para este ejemplo es: Compatible: (1,3) (4,5) (6,8) (9,10)

La implementación del algoritmo está claramente en ÿ(n^2). Hay una implementación de ÿ(n log n) y el lector interesado puede continuar leyendo
a continuación (Ejemplo de Java).

Ahora tenemos un algoritmo codicioso para el problema de programación de intervalos, pero ¿es óptimo?

Proposición: El tiempo de finalización más temprano del algoritmo codicioso es óptimo.

Prueba: (por contradicción)

Supongamos que greedy no es óptimo y que i1,i2,...,ik denota el conjunto de trabajos seleccionados por greedy. Sea j1,j2,...,jm el conjunto de
trabajos en una solución óptima con i1=j1,i2=j2,...,ir=jr para el mayor valor posible de r.

El trabajo i(r+1) existe y finaliza antes que j(r+1) (finalización más temprana). Pero entonces j1,j2,...,jr,i(r+1),j(r+2),...,jm también es una solución
óptima y para todo k en [1,(r+1)] es jk=ik. eso es una contradicción a la maximalidad de r. Esto concluye la prueba.

Este segundo ejemplo demuestra que, por lo general, hay muchas estrategias codiciosas posibles, pero solo algunas o incluso ninguna pueden
encontrar la solución óptima en todos los casos.

A continuación se muestra un programa Java que se ejecuta en ÿ(n log n)

importar java.util.Arrays;

GoalKicker.com – Notas de algoritmos para profesionales 99


Machine Translated by Google

importar java.util.Comparator;

trabajo de clase

{
int inicio, fin, beneficio;

Trabajo( inicio int , fin int , beneficio int) {

this.inicio = inicio;
this.terminar = terminar;
this.profit = beneficio;
}
}

clase JobComparator implementa Comparator<Trabajo> {

public int compare(Trabajo a, Trabajo b) {

volver a.terminar < b.terminar ? -1 : a.terminar == b.terminar ? 0 : 1;


}
}

programación de intervalo ponderado de clase pública


{
static public int binarySearch( Trabajos[], índice int ) {

int bajo = 0, alto = índice - 1;

mientras (bajo <= hola)


{
int medio = (bajo + alto) / 2; if
(trabajos[media].terminar <= trabajos[índice].inicio) {

if (trabajos[mid + 1].finish <= trabajos[index].start) lo = mid + 1;


de lo contrario, regresa a la mitad;

} más
hola = medio - 1;
}

devolver -1;
}

programación int pública estática (trabajos de trabajo


[]) {
Arrays.sort(trabajos, nuevo JobComparator());

int n = trabajos.longitud;
int tabla[] = new int[n]; tabla[0] =
trabajos[0].beneficio;

para (int i=1; i<n; i++) {

int inclProf = trabajos[i].beneficio; int l =


binarySearch(empleos, i); if (l != -1)
inclProf += tabla[l];

tabla[i] = Math.max(inclProf, tabla[i-1]);

GoalKicker.com – Notas de algoritmos para profesionales 100


Machine Translated by Google

tabla de retorno [n-1];


}

public static void main(String[] args) {

trabajo trabajos[] = {nuevo trabajo (1, 2, 50), nuevo trabajo (3, 5, 20),
nuevo trabajo (6, 19, 100), nuevo trabajo (2, 100, 200)};

"
System.out.println("El beneficio óptimo es + horario(trabajos));
}
}

Y la salida esperada es:

La ganancia óptima es 250

Sección 18.4: Minimización de los retrasos


Existen numerosos problemas para minimizar la tardanza, aquí tenemos un solo recurso que solo puede procesar un trabajo a la vez. El
trabajo j requiere tj unidades de tiempo de procesamiento y vence en el tiempo dj. si j comienza en el tiempo sj terminará en el tiempo fj=sj+tj.
Definimos la tardanza L=max{0,fj-dh} para todo j. El objetivo es minimizar la tardanza máxima L.

123456
321432
DJ 6 8 9 9 10 11

Trabajo 3 2 2 5 5 5 4 4 4 4 1 1 1 6 6
Tiempo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

Lj -8 -5 -4 1 7 4

La solución L=7 obviamente no es óptima. Veamos algunas estrategias codiciosas:

1. Tiempo de procesamiento más corto primero: programe trabajos en orden ascendente og tiempo de
procesamiento j` 2. Fecha límite más temprana primero : programe trabajos en orden ascendente de fecha límite dj
3. Inactividad más pequeña: programe trabajos en orden ascendente de holgura dj-tj

Es fácil ver que el tiempo de procesamiento más corto primero no es óptimo, un buen contraejemplo es

12
tj 1 5
DJ 10 5

la solución de pila más pequeña tiene problemas similares

12
tj 1 5
DJ 3 5

la última estrategia parece válida, así que comenzamos con un pseudocódigo:

1. Ordene n trabajos por hora de vencimiento para que d1<=d2<=...<=dn 2.


Establezca t=0

3. para j=1 a n
Asignar trabajo j al intervalo [t,t+tj]

GoalKicker.com – Notas de algoritmos para profesionales 101


Machine Translated by Google

establecer sj=t y fj=t+tj


establecer t=t+tj

4. intervalos de retorno [s1,f1],[s2,f2],...,[sn,fn]

Y como implementación en C++:

#include <iostream>
#include <utilidad>
#include <tupla> #include
<vector> #include
<algoritmo>

const int jobCnt = 10;

// Horas de inicio del


trabajo const int processTimes[] = { 2, 3, 1, 4, 3, 2, 3, 5, 2, 1};

// Horas de finalización
del trabajo const int dueTimes[] = { 4, 7, 9, 13, 8, 17, 9, 11, 22, 25};

utilizando el espacio de nombres estándar;

int principal()
{
vector<par<int,int>> trabajos;

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


trabajos.push_back(make_pair(processTimes[i], dueTimes[i]));

// paso 1: sort
sort(jobs.begin(), jobs.end(),[](pair<int,int> p1, pair<int,int> p2) { return p1.segundo <
p2.segundo; } );

// paso 2: establecer t=0


int t = 0;

// paso 3:
vector<par<int,int>> jobIntervals;

for(int i=0; i<jobCnt; ++i) {

jobIntervals.push_back(make_pair(t,t+jobs[i].first)); t += trabajos[i].primero;

//paso 4: imprimir intervalos cout


<< "Intervalos:\n" << endl;

retraso int = 0;

for(int i=0; i<jobCnt; ++i) {

par automático = intervalos de trabajo [i];

atraso = max(atraso, par.segundos-trabajos[i].segundo);

cout << "(" << par.primero << "," << par.segundo << ") "
<< "Retraso: " << par.segundos-trabajos[i].segundo << std::endl;
}

"
cout << "\nla tardanza máxima es << retraso << endl;

GoalKicker.com – Notas de algoritmos para profesionales 102


Machine Translated by Google

devolver 0;
}

Y la salida de este programa es:

Intervalos:

(0,2) Retraso: -2
(2,5) Tardanza:-2
(5,8) Retraso: 0
(8,9) Retraso: 0
(9,12) Retraso : 3
(12,17) Retraso: 6
(17,21) Retraso: 8
(21,23) Retraso: 6
(23,25) Retraso: 3
(25,26) Retraso: 1

la tardanza máxima es 8

El tiempo de ejecución del algoritmo es obviamente ÿ(n log n) porque la clasificación es la operación dominante de este algoritmo.
Ahora tenemos que demostrar que es óptimo. Claramente, un horario óptimo no tiene tiempo de inactividad. la fecha límite más temprana primero
el horario tampoco tiene tiempo de inactividad.

Supongamos que los trabajos están numerados de modo que d1<=d2<=...<=dn. Decimos que una inversión de un programa es un par de
trabajos i y j de modo que i<j pero j está programado antes que i. Debido a su definición, el primer cronograma de la fecha límite más temprana
no tiene inversiones. Por supuesto, si un horario tiene una inversión, tiene uno con un par de trabajos invertidos programados consecutivamente.

Proposición: intercambiar dos trabajos invertidos adyacentes reduce el número de inversiones en uno y no aumenta el retraso máximo.

Prueba: Sea L el retraso antes del intercambio y M el retraso después. Como el intercambio de dos trabajos adyacentes no mueve
los otros trabajos de su posición, es Lk=Mk para todo k != i,j.

Claramente es Mi<=Li ya que el trabajo lo programé antes. si el trabajo j llega tarde, se sigue de la definición:

Mj = fi-dj <= (definición) (ya


fi-di que i y j se intercambian)
<= li

Eso significa que el retraso después del intercambio es menor o igual que antes. Esto concluye la prueba.

Proposición: El plazo más temprano primero programa S es óptimo.

Prueba: (por contradicción)

Supongamos que S* es el programa óptimo con el menor número posible de inversiones. podemos suponer que S* no tiene tiempo de inactividad.
Si S* no tiene inversiones, entonces S=S* y listo. Si S* tiene una inversión, entonces tiene una inversión adyacente. La última Proposición establece
que podemos intercambiar la inversión adyacente sin aumentar la demora pero disminuyendo el número de inversiones. Esto contradice la
definición de S*.

El problema de la minimización de los retrasos y su problema de intervalo mínimo casi relacionado , en el que se plantea la cuestión de un
horario mínimo, tienen muchas aplicaciones en el mundo real. Pero, por lo general, no tiene una sola máquina sino muchas y manejan la misma
tarea a diferentes velocidades. Estos problemas se completan NP muy rápido.

103
GoalKicker.com – Notas de algoritmos para profesionales
Machine Translated by Google

Otra pregunta interesante surge si no miramos el problema fuera de línea , donde tenemos todas las tareas y datos en
mano sino en la variante en línea , donde las tareas aparecen durante la ejecución.

GoalKicker.com – Notas de algoritmos para profesionales 104


Machine Translated by Google

Capítulo 19: Algoritmo de Prim


Sección 19.1: Introducción al algoritmo de Prim
Digamos que tenemos 8 casas. Queremos instalar líneas telefónicas entre estas casas. El borde entre las casas representa el costo
de establecer la línea entre dos casas.

Nuestra tarea es configurar las líneas de tal manera que todas las casas estén conectadas y el costo de configurar toda la
conexión sea mínimo. Ahora, ¿cómo lo averiguamos? Podemos usar el Algoritmo de Prim.

Algoritmo de Prim es un algoritmo codicioso que encuentra un árbol de expansión mínimo para un gráfico no dirigido ponderado.
Esto significa que encuentra un subconjunto de los bordes que forman un árbol que incluye todos los nodos, donde se minimiza el
peso total de todos los bordes del árbol. El algoritmo fue desarrollado en 1930 por el matemático checo Vojtÿch Jarník y luego
redescubierto y vuelto a publicar por el científico informático Robert Clay Prim en 1957 y Edsger Wybe Dijkstra en 1959. También
se le conoce como algoritmo DJP, algoritmo de Jarnik, algoritmo Prim-Jarnik o algoritmo Prim-Dijsktra.

Ahora veamos primero los términos técnicos. Si creamos un grafo, S usando algunos nodos y aristas de un grafo no dirigido G,
entonces S se llama un subgrafo del grafo G. Ahora S se llamará Spanning Tree si y solo si:

Contiene todos los nodos de G.


Es un árbol, eso significa que no hay ciclo y todos los nodos están conectados.
Hay (n-1) aristas en el árbol, donde n es el número de nodos en G.

Puede haber muchos árboles de expansión de un gráfico. El árbol de expansión mínimo de un gráfico no dirigido ponderado es
un árbol, de modo que la suma del peso de los bordes es mínima. Ahora usaremos el algoritmo de Prim para encontrar el árbol de
expansión mínimo, es decir, cómo configurar las líneas telefónicas en nuestro gráfico de ejemplo de tal manera que el costo de
configuración sea mínimo.

Al principio, seleccionaremos un nodo de origen . Digamos que el nodo 1 es nuestra fuente. Ahora agregaremos el borde del nodo
1 que tiene el costo mínimo a nuestro subgrafo. Aquí marcamos las aristas que están en el subgrafo usando el color azul. Aquí 1-5 es

GoalKicker.com – Notas de algoritmos para profesionales 105


Machine Translated by Google
Machine Translated by Google

El siguiente paso es importante. Desde el nodo 1, el nodo 2, el nodo 5 y el nodo 4, el borde mínimo es 2-4. Pero si seleccionamos
ese, creará un ciclo en nuestro subgrafo. Esto se debe a que el nodo 2 y el nodo 4 ya están en nuestro subgrafo. Asi que
tomar ventaja 2-4 no nos beneficia. Seleccionaremos los bordes de tal manera que agregue un nuevo nodo en nuestro subgrafo. Así que nosotros

seleccione el borde 4-8.

Si seguimos así, seleccionaremos la arista 8-6, 6-7 y 4-3. Nuestro subgrafo se verá así:

GoalKicker.com – Notas de algoritmos para profesionales 107


Machine Translated by Google

Este es nuestro subgrafo deseado, que nos dará el árbol de expansión mínimo. Si quitamos los bordes que no hicimos

seleccionar, obtendremos:

Este es nuestro árbol de expansión mínimo (MST). Entonces el costo de establecer las conexiones telefónicas es: 4 + 2 + 5 + 11 + 9
+ 2 + 1 = 34. Y el conjunto de casas y sus conexiones se muestran en el gráfico. Puede haber múltiples MST de un
grafico. Depende del nodo fuente que elijamos.

El pseudocódigo del algoritmo se muestra a continuación:

Procedimiento PrimsMST(Gráfico): // aquí Graph es un gráfico ponderado conectado no vacío


Vnuevo[] = {x} // Nuevo subgrafo Vnew con nodo fuente x

GoalKicker.com – Notas de algoritmos para profesionales 108


Machine Translated by Google

Enew[] = {}
while Vnew no es igual a V u -> un
nodo de Vnew v -> un nodo que
no está en Vnew tal que edge uv tiene el costo mínimo
// si dos nodos tienen el mismo peso, elige cualquiera de ellos
agregar v a Vnuevo
agregar borde (u, v) a Enuevo
terminar mientras
Devolver Vnew y Enew

Complejidad:

La complejidad temporal del enfoque ingenuo anterior es O(V²). Utiliza matriz de adyacencia. Podemos reducir la complejidad usando la cola de

prioridad. Cuando agregamos un nuevo nodo a Vnew, podemos agregar sus bordes adyacentes en la cola de prioridad. Luego saque el borde ponderado

mínimo de él. Entonces la complejidad será: O(ElogE), donde E es el número de aristas.

Nuevamente, se puede construir un montón binario para reducir la complejidad a O (ElogV).

El pseudocódigo que utiliza Priority Queue se proporciona a continuación:

Procedimiento MSTPrim(Gráfico, fuente):


para cada u en V clave[u] := inf parent[u] :=
NULL end for key[source] := 0 Q =
Priority_Queue()

Q=V
mientras Q no está vacío
u -> Q.pop para cada
v adyacente a i si v pertenece a
Q y Edge(u,v) < tecla[v] // aquí Edge(u, v) representa // costo
de edge(u, v)
padre[v] := u
tecla[v] := Borde(u, v) final
si final por final mientras

Aquí key[] almacena el costo mínimo de atravesar el nodo-v. parent[] se utiliza para almacenar el nodo principal. Es útil para recorrer e imprimir el árbol.

A continuación se muestra un programa simple en Java:

importar java.util.*;

Gráfico de clase pública


{
privado estático int infinito = 9999999; int[][] Costo
del enlace;
En t NNodos;
Gráfico(int[][] mat) {

i, j ; NNodes
= mat.length; LinkCost =
new int[NNodes][NNodes]; for ( i=0; i < NNodos;
i++) {

for ( j=0; j < NNodos; j++) {

GoalKicker.com – Notas de algoritmos para profesionales 109


Machine Translated by Google

LinkCost [ i ] [ j ] = mat [ i ] [ j ] ; if
( Costo del enlace [ i ] [ j ] == 0 )
LinkCost [ i ] [ j ] = infinito ;
}

} for ( i = 0 ; i < NNodos ; i++ ) {

for ( j = 0 ; j < NNodes ; j++ ) if


( LinkCost [ i ] [ j ] < infinite )
Sistema .out .print ( " " más + Costo del enlace [ i ] [ j ] + " " ) ;

Sistema .out .print ( "*" );


Sistema .out .println ( ) ;
}

} public int no alcanzado (booleano [ ] r ) {

booleano hecho =
verdadero ; for ( int i = 0 ; i < r.length ; i++ ) if ( r
[ i ] == false ) return i ; retorno - 1 ;

} vacío público Prim ( ) {

int i, j, k, x, y ; booleano
[ ] Alcanzado = nuevo booleano [NNodes ] ; int [ ]
predNode = new int [NNodes ] ;
Alcanzado [ 0 ] =
verdadero ; for ( k = 1 ; k < NNodos ; k+
+){
Alcanzado [ k ] = falso ;

} predNode [ 0 ] = 0 ;
printReachSet ( Alcanzado ) ; para
(k = 1 ; k < NNodos ; k++ ) {

x = y = 0 ; for
( i = 0 ; i < NNodes ; i++ ) for ( j = 0 ; {
j < NNodos ; j++)

si ( Alcanzado [ i ] && ! Alcanzado [ j ] &&


LinkCost [ i ] [ j ] < LinkCost [ x ] [ y ] )
{
x = yo ;
y=j;
}
}
System .out .println ("Min cost edge: (" + + x + "," + + y +
")" + "cost = + LinkCost
[ x ] [ y ]);
"

predNodo [ y ] = x ;
Alcanzado [ y ] =
verdadero ; printReachSet
( Alcanzado ) ; Sistema .out .println ( ) ;

} int [ ] a = predNode ;
para ( i = 0 ; i < NNodes ; i++ )
" --> "
Sistema .out .println ( a [ i ] + + yo );

} void printReachSet (booleano [ ] Alcanzado )

110
GoalKicker.com – Notas de algoritmos para profesionales
Machine Translated by Google

{
Sistema .out .print ( "ReachSet = " ); for
(int i = 0 ; i < Alcanzado.longitud ; i++ ) if ( Alcanzado
[i])
Sistema .out .print ( i + " " ); //
Sistema.out.println();
}
public static void principal (String [ ] args )
{
int [ ] [ ] conexión = { { 0 { 3 , 3 , 0 , 2 , 0 , 0 , 0 , 0 , 4 }, // 0
{ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 4 , 0 }, // 1
{ 2 , 0 , 0 , 6 , 0 , 1 , 0 , 2 , 0 }, // 2
{ 0 , 0 , 6 , 0 , 1 , 0 , 0 , 0 , 0 }, // 3
{ 0 , 0 , 0 , 1 , 0 , 0 , 0 , 0 , 8 }, // 4
{ 0 , 0 , 1 , 0 , 0 , 0 , 8 , 0 , 0 }, // 5
{ 0 , 0 , 0 , 0 , 0 , 8 , 0 , 0 , 0 }, // 6
, 4 , 2 , 0 , 0 , 0 , 0 , 0 , 0 }, // 7 0 } //
, 0, 0, 0, 8, 0, 0, 0, 8
{4};
Gráfica G = nueva Gráfica (conn ) ;
G.Prim ( ) ;
}
}

Compile el código anterior usando javac Graph.java

Producción:

$ Java Gráfico
*3*2****4
3******4*
***6*1*2*2*6*1****

***1****8
**1***8*******8***

*42******
4***8****
ReachSet = 0 Borde de costo mínimo : ( 0 , 3 ) costo = 2
AlcanceConjunto = 0 3
Borde de costo mínimo : ( 3 , 4 ) costo = 1
AlcanceConjunto = 0 3 4
Borde de costo mínimo :(10),costo = 3
AlcanceConjunto = 0 1 3
Borde de costo mínimo : ( 0 , 4 8 )costo = 4
AlcanceConjunto = 0 1 3 4 8
Margen de coste mínimo : ( 1 , 7 ) costo = 4
AlcanceConjunto = 0 1 3 4 7 8
Borde de costo mínimo :( 27), costo = 2
AlcanceConjunto = 0 1 2 3 4 7
Borde de costo mínimo : ( 2 , 8 5 )coste = 1
AlcanceConjunto = 0 1 2 3 4 5 7 8
Borde de costo mínimo :(65), costo = 8
AlcanceConjunto = 0 1 2 3 4 5 6 7 8
0 --> 0 0
--> 1
7 --> 2
0 --> 3 3
--> 4
2 --> 5
5 --> 6

GoalKicker.com – Notas de algoritmos para profesionales 111


Machine Translated by Google

1 --> 7
0 --> 8

GoalKicker.com – Notas de algoritmos para profesionales 112


Machine Translated by Google

Capítulo 20: Algoritmo de Bellman-Ford


Sección 20.1: Algoritmo de ruta más corta de fuente única (dado que hay un
ciclo negativo en un gráfico)
Antes de leer este ejemplo, se requiere tener una breve idea sobre la relajación de bordes. Puedes aprenderlo desde aquí

Bellman-Ford El algoritmo calcula las rutas más cortas desde un solo vértice de origen a todos los demás vértices
en un dígrafo ponderado. Aunque es más lento que el algoritmo de Dijkstra, funciona en los casos en que el peso
del borde es negativo y también encuentra un ciclo de peso negativo en el gráfico. El problema con el algoritmo de
Dijkstra es que, si hay un ciclo negativo, sigues repitiendo el ciclo una y otra vez y sigues reduciendo la distancia
entre dos vértices.

La idea de este algoritmo es recorrer todos los bordes de este gráfico uno por uno en algún orden aleatorio. Puede ser cualquier orden aleatorio. Pero debe

asegurarse de que si uv (donde u y v son dos vértices en un gráfico) es uno de sus órdenes, entonces debe haber un borde de u a v. Por lo general, se toma

directamente del orden de la entrada dada. Nuevamente, cualquier orden aleatorio funcionará.

Después de seleccionar el orden, relajaremos los bordes según la fórmula de relajación. Para una arista dada uv que va de u a v, la fórmula de relajación es:

si distancia[u] + costo[u][v] < d[v]


d[v] = d[u] + coste[u][v]

Es decir, si la distancia desde la fuente a cualquier vértice u + el peso de la arista uv es menor que la distancia desde la fuente a otro vértice v,

actualizamos la distancia desde la fuente a v. Necesitamos relajar las aristas como máximo (V -1) veces donde V es el número de aristas en el gráfico. ¿Por

qué (V-1) preguntas? Lo explicaremos en otro ejemplo. También vamos a realizar un seguimiento del vértice principal de cualquier vértice, es decir, cuando

relajamos un borde, estableceremos:

padre[v] = tu

Significa que hemos encontrado otro camino más corto para llegar a v a través de u. Lo necesitaremos más adelante para imprimir la ruta más corta desde el
origen hasta el vértice de destino.

Veamos un ejemplo. Tenemos un gráfico:

GoalKicker.com – Notas de algoritmos para profesionales 113


Machine Translated by Google

Hemos seleccionado 1 como vértice fuente . Queremos encontrar el camino más corto desde la fuente a todos los demás
vértices.

Al principio, d[1] = 0 porque es la fuente. Y el resto son infinitos, porque aún no conocemos su distancia.

Relajaremos los bordes en esta secuencia:

+--------+--------+--------+--------+--------+---- ----+--------+
| Serie | 1 | 2 | 3 | 4| 5 | 6 |
+--------+--------+--------+--------+--------+---- ----+--------+
| Borde | 4->5 | 3->4 | 1->3 | 1->4 | 4->6 | 2->3 |
+--------+--------+--------+--------+--------+---- ----+--------+

Puedes tomar la secuencia que quieras. Si relajamos los bordes una vez, ¿qué obtenemos? Obtenemos la distancia desde la fuente .

a todos los demás vértices del camino que utiliza como máximo 1 arista. Ahora relajemos los bordes y actualicemos los valores de d[]. Nosotros
obtener:

1. d[4] + costo[4][5] = infinito + 7 = infinito. No podemos actualizar este.

2. d[2] + coste[3][4] = infinito. No podemos actualizar este.

3. d[1] + coste[1][3] = 0 + 2 = 2 < d[2]. Entonces d[3] = 2. También padre[1] = 1.

4. d[1] + costo[1][4] = 4. Entonces d[4] = 4 < d[4]. padre[4] = 1.

5. d[4] + coste[4][6] = 9. d[6] = 9 < d[6]. padre[6] = 4.

6. d[2] + coste[2][3] = infinito. No podemos actualizar este.

No pudimos actualizar algunos vértices porque la condición d[u] + cost[u][v] < d[v] no coincidía. como hemos dicho
antes, encontramos las rutas desde la fuente a otros nodos utilizando un máximo de 1 borde.

Nuestra segunda iteración nos proporcionará la ruta usando 2 nodos. Obtenemos:

1. d[4] + coste[4][5] = 12 < d[5]. d[5] = 12. padre[5] = 4.

2. d[3] + coste[3][4] = 1 < d[4]. d[4] = 1. padre[4] = 3.

3. d[3] permanece sin cambios.

4. d[4] permanece sin cambios.

5. d[4] + coste[4][6] = 6 < d[6]. d[6] = 6. padre[6] = 4.

6. d[3] permanece sin cambios.

GoalKicker.com – Notas de algoritmos para profesionales 114


Machine Translated by Google

Nuestro gráfico se verá así:

Nuestra tercera iteración solo actualizará el vértice 5, donde d[5] será 8. Nuestro gráfico se verá así:

Después de esto, no importa cuántas iteraciones hagamos, tendremos las mismas distancias. Por lo tanto, mantendremos una bandera que verifique si se

realiza alguna actualización o no. Si no es así, simplemente romperemos el bucle. Nuestro pseudocódigo será:

Procedimiento Bellman-Ford(Gráfico, fuente):


n := número de vértices en Gráfico para i de
1 a n d[i] := infinito padre[i] := NULL final para
d[fuente] := 0 para i de Indicador 1 a
n-1 : = falso para todos los bordes desde
(u,v) en Graph si d[u] + cost[u][v] < d[v]

d[v] := d[u] + costo[u][v]


padre[v] := u indicador :=
verdadero

GoalKicker.com – Notas de algoritmos para profesionales 115


Machine Translated by Google

terminar si
end for if
flag == false break

fin para
Volver d

Para realizar un seguimiento del ciclo negativo, podemos modificar nuestro código utilizando el procedimiento descrito aquí. Nuestro
pseudocódigo completo será:

Procedimiento Bellman-Ford-With-Negative-Cycle-Detection(Graph, source): n := número de vértices en


Graph para i de 1 a n d[i] := infinity parent[i] := NULL

final para
d[fuente] := 0 para i
de 1 a n-1
flag := false para
todas las aristas desde (u,v) en Graph if d[u] +
cost[u][v] < d[v]
d[v] := d[u] + costo[u][v] padre[v] := u
bandera := verdadero fin si fin para si
bandera == falso romper

end for
para todas las aristas de (u,v) en Graph if d[u] +
cost[u][v] < d[v]
Devolver "Ciclo negativo detectado" end si finaliza
para

Volver d

Ruta de impresión:

Para imprimir la ruta más corta a un vértice, repetiremos hasta su padre hasta que encontremos NULL y luego imprimamos los vértices.
El pseudocódigo será:

Procedimiento PathPrinting(u) v :=
parent[u] if v == NULL

devolver
PathPrinting(v) imprimir
-> u

Complejidad:

*
Dado que necesitamos relajar las aristas al máximo (V-1) veces, la complejidad temporal de este algoritmo será igual a O(VE) donde E denota
el número de aristas, si usamos la lista de adyacencia para representar el gráfico. Sin embargo, si se usa una matriz de adyacencia para
representar el gráfico, la complejidad del tiempo será O(V^3). La razón es que podemos iterar a través de todos los bordes en el tiempo O (E)
cuando se usa la lista de adyacencia , pero toma el tiempo O (V ^ 2) cuando se usa la matriz de adyacencia .

Sección 20.2: Detección de ciclos negativos en un gráfico


Para entender este ejemplo, se recomienda tener una breve idea sobre el algoritmo Bellman-Ford que se puede encontrar

116
GoalKicker.com – Notas de algoritmos para profesionales
Machine Translated by Google

aquí

Usando el algoritmo de Bellman-Ford, podemos detectar si hay un ciclo negativo en nuestro gráfico. Sabemos que, para encontrar el camino más corto,

necesitamos relajar todas las aristas del grafo (V-1) veces, donde V es el número de vértices en un grafo.

Ya hemos visto que en este ejemplo, después de (V-1) iteraciones, no podemos actualizar d[], sin importar cuántas iteraciones hagamos. ¿O
podemos?

Si hay un ciclo negativo en un gráfico, incluso después de (V-1) iteraciones, podemos actualizar d[]. Esto sucede porque para cada iteración, atravesar el ciclo

negativo siempre disminuye el costo del camino más corto. Esta es la razón por la que el algoritmo de Bellman Ford limita el número de iteraciones a (V-1). Si

usáramos el Algoritmo de Dijkstra aquí, estaríamos atrapados en un bucle sin fin. Sin embargo, concentrémonos en encontrar el ciclo negativo.

Supongamos que tenemos un gráfico:

Elijamos el vértice 1 como fuente. Después de aplicar el algoritmo de ruta más corta de fuente única de Bellman-Ford al gráfico, encontraremos las distancias
desde la fuente a todos los demás vértices.

Así es como se ve el gráfico después de (V-1) = 3 iteraciones. Debería ser el resultado ya que hay 4 aristas, necesitamos como máximo 3 iteraciones para

encontrar el camino más corto. Entonces, o esta es la respuesta, o hay un ciclo de peso negativo en el gráfico. Para encontrar que, después de (V-1) iteraciones,

hacemos una iteración final más y si la distancia continúa disminuyendo, significa que definitivamente hay un ciclo de peso negativo en el gráfico.

GoalKicker.com – Notas de algoritmos para profesionales 117


Machine Translated by Google

Para este ejemplo: si marcamos 2-3, d[2] + cost[2][3] nos dará 1 que es menor que d[3]. Entonces podemos concluir que hay un ciclo negativo en

nuestro gráfico.

Entonces, ¿cómo encontramos el ciclo negativo? Hacemos una pequeña modificación al procedimiento Bellman-Ford:

Procedimiento NegativeCycleDetector(Graph, source): n := número de


vértices en Graph para i de 1 a n d[i] := extremo infinito para d[source] :=
0 for i from 1 to n-1 flag := false para todas las aristas de (u,v) en Graph
si d[u] + cost[u][v] < d[v]

d[v] := d[u] + costo[u][v] bandera :=


verdadero fin si fin para si bandera
== falso ruptura

end for
para todas las aristas de (u,v) en Graph if d[u] +
cost[u][v] < d[v]
Devuelve el final "Ciclo negativo detectado" si

fin para
Devolver "Sin ciclo negativo"

Así es como sabemos si hay un ciclo negativo en un gráfico. También podemos modificar el algoritmo de Bellman-Ford para realizar un seguimiento de los
ciclos negativos.

Sección 20.3: ¿Por qué necesitamos relajar todos los bordes la mayoría
de las veces (V-1)?
Para comprender este ejemplo, se recomienda tener una breve idea del algoritmo de ruta más corta de fuente única Bellman-Ford que se puede encontrar aquí

En el algoritmo de Bellman-Ford, para encontrar el camino más corto, necesitamos relajar todos los bordes del gráfico. Este proceso se repite como máximo

(V-1) veces, donde V es el número de vértices del gráfico.

El número de iteraciones necesarias para encontrar el camino más corto desde el origen hasta todos los demás vértices depende del orden que

seleccionemos para relajar los bordes.

Echemos un vistazo a un ejemplo:

Aquí, el vértice fuente es 1. Encontraremos la distancia más corta entre la fuente y todos los demás vértices.

Podemos ver claramente que, para llegar al vértice 4, en el peor de los casos, se necesitarán aristas (V-1) . Ahora, dependiendo del orden en que se

descubren los bordes, puede tomar (V-1) veces descubrir el vértice 4. ¿No lo entendiste? Usemos Bellman-Ford

GoalKicker.com – Notas de algoritmos para profesionales 118


Machine Translated by Google

algoritmo para encontrar el camino más corto aquí:

Vamos a usar esta secuencia:

+--------+--------+--------+--------+
| Serie | 1 | 2 | 3 |
+--------+--------+--------+--------+
| Borde | 3->4 | 2->3 | 1->2 |
+--------+--------+--------+--------+

Para nuestra primera iteración:

1. d[3] + coste[3][4] = infinito. No cambiará nada.


2. d[2] + costo[2][3] = infinito. No cambiará nada.
3. d[1] + coste[1][2] = 2 < d[2]. d[2] = 2. padre[2] = 1.

Podemos ver que nuestro proceso de relajación solo cambió d[2]. Nuestro gráfico se verá así:

Segunda iteración:

1. d[3] + coste[3][4] = infinito. No cambiará nada.


2. d[2] + coste[2][3] = 5 < d[3]. d[3] = 5. padre[3] = 2.
3. No se cambiará.

Esta vez el proceso de relajación cambió d[3]. Nuestro gráfico se verá así:

Tercera iteración:

1. d[3] + coste[3][4] = 7 < d[4]. d[4] = 7. padre[4] = 3.


2. No se cambiará.
3. No se cambiará.

Nuestra tercera iteración finalmente descubrió el camino más corto a 4 desde 1. Nuestro gráfico se verá así:

Entonces, se necesitaron 3 iteraciones para encontrar el camino más corto. Después de este, no importa cuántas veces aflojemos los bordes,

GoalKicker.com – Notas de algoritmos para profesionales 119


Machine Translated by Google

los valores en d[] seguirán siendo los mismos. Ahora, si consideramos otra secuencia:

+--------+--------+--------+--------+
| Serie | 1 | 2 | 3 |
+--------+--------+--------+--------+
| Borde | 1->2 | 2->3 | 3->4 |
+--------+--------+--------+--------+

Obtendríamos:

1. d[1] + costo[1][2] = 2 < d[2]. d[2] = 2.


2. d[2] + coste[2][3] = 5 < d[3]. d[3] = 5.
3. d[3] + coste[3][4] = 7 < d[4]. d[4] = 5.

Nuestra primera iteración ha encontrado el camino más corto desde el origen hasta todos los demás nodos. Otra secuencia 1->2,
3->4, 2->3 es posible, lo que nos dará el camino más corto después de 2 iteraciones. Podemos llegar a la decisión de que, sin importar
cómo organizamos la secuencia, no tomará más de 3 iteraciones encontrar el camino más corto desde la fuente en este
ejemplo.

Podemos concluir que, en el mejor de los casos, se necesitará 1 iteración para encontrar la ruta más corta desde la fuente. para lo peor
caso, tomará (V-1) iteraciones, por lo que repetimos el proceso de relajación (V-1) veces.

GoalKicker.com – Notas de algoritmos para profesionales 120


Machine Translated by Google

Capítulo 21: Algoritmo de línea


El dibujo de líneas se logra calculando las posiciones intermedias a lo largo de la ruta de la línea entre dos posiciones de punto final especificadas. Luego

se dirige un dispositivo de salida para llenar estas posiciones entre los puntos finales.

Sección 21.1: Algoritmo de dibujo lineal de Bresenham


Teoría de fondo: el algoritmo de dibujo de líneas de Bresenham es un algoritmo de generación de líneas de trama eficiente y preciso desarrollado por

Bresenham. Implica solo el cálculo de números enteros, por lo que es preciso y rápido. También se puede ampliar para mostrar círculos y otras curvas.

En el algoritmo de dibujo lineal de Bresenham:

Para Pendiente |m|<1:


Cualquiera de los valores de x aumenta

O tanto x como y se aumentan utilizando el parámetro de decisión.

Para Pendiente |m|>1:

Se incrementa el valor de y O se

incrementa tanto x como y utilizando el parámetro de decisión.

Algoritmo para pendiente |m|<1:

1. Introduzca dos puntos finales (x1,y1) y (x2,y2) de la línea.

2. Trace el primer punto (x1,y1).

3. Calcular

Delx =| x2 – x1 |

Retraso = | y2 – y1 |

4. Obtener el parámetro de decisión inicial como P = 2 *

dely – delx

5. Para I = 0 a delx en paso de 1

Si p < 0 entonces
x1 = x1 + 1

Bote(x1,y1)

P = p+ 2retraso

Más

x1 = x1 + 1

Y1 = y1 + 1

Parcela(x1,y1)

P = p + 2dely – 2 * delx

Terminara si

Fin para

6. FIN

Código fuente:

GoalKicker.com – Notas de algoritmos para profesionales 121


Machine Translated by Google

/ * Programa AC para implementar el algoritmo de trazado de líneas de Bresenham para |m|<1


*/ #include<stdio.h>
#incluir<conio.h>
#incluir <gráficos.h>
#incluir<matemáticas.h>

int main()
{ int
gdriver=DETECT,gmode; int
x1,y1,x2,y2,delx,dely,p,i;
initgraph(&gdriver,&gmode,"c:\\TC\\BGI");

printf("Ingrese los puntos iniciales: ");


escaneo("%d",&x1); scanf("%d",&y1);
printf("Ingrese los puntos finales: ");
escaneo("%d",&x2); scanf("%d",&y2);

putpixel(x1,y1,RED);

delx=fábricas(x2-x1);
retraso=fábricas(y2-
y1); p=(2*retraso)-
delx; for(i=0;i<delx;i++)
{ if(p<0) { x1=x1+1;
putpixel(x1,y1,RED);
p=p+(2*retraso); } más
{ x1=x1+1; y1=y1+1;
putpixel(x1,y1,RED);
p=p+(2*retraso)-(2*delx); } }
obtener(); closegraph();
devolver 0; }

Algoritmo para pendiente |m|>1:

1. Introduzca dos puntos finales (x1,y1) y (x2,y2) de la línea.


2. Trace el primer punto (x1,y1).
3. Calcular

Delx =| x2 – x1 |
Retraso = | y2 – y1 |
4. Obtenga el parámetro de decisión inicial como P = 2
* delx – retraso 5. Para I = 0 para retrasar en el
paso de 1

Si p < 0
entonces y1 = y1 + 1
Bote(x1,y1)

GoalKicker.com – Notas de algoritmos para profesionales 122


Machine Translated by Google

P = p+ 2delx

Más
x1 = x1 + 1

Y1 = y1 + 1
Parcela(x1,y1)

P = p + 2delx – 2 * retraso

Terminara si

Fin para

6. FIN

Código fuente:

/ * Programa AC para implementar el algoritmo de trazado de líneas de Bresenham para |m|


>1 */ #include<stdio.h> #include<conio.h>

#include<gráficos.h>
#include<matemáticas.h>
int main() { int
gdriver=DETECT,gmode;
int x1,y1,x2,y2,delx,dely,p,i;
initgraph(&gdriver,&gmode,"c:\\TC\
\BGI"); printf("Ingrese los puntos iniciales: ");
escaneo("%d",&x1); scanf("%d",&y1); printf("Ingrese
los puntos finales: "); escaneo("%d",&x2);
scanf("%d",&y2); putpixel(x1,y1,RED); delx=fábricas(x2-
x1); retraso=fábricas(y2-y1); p=(2*delx)-dely;
for(i=0;i<delx;i++){ if(p<0) { y1=y1+1;
putpixel(x1,y1,RED); p=p+(2*delx); } más { x1=x1+1;
y1=y1+1; putpixel(x1,y1,RED); p=p+(2*delx)-
(2*retraso); } } obtener(); closegraph(); devolver 0; }

GoalKicker.com – Notas de algoritmos para profesionales 123


Machine Translated by Google

Capítulo 22: Algoritmo de Floyd-Warshall


Sección 22.1: Algoritmo de ruta más corta para todos los pares

de Floyd-Warshall El algoritmo es para encontrar las rutas más cortas en un gráfico ponderado con pesos de borde positivos o negativos.
Una sola ejecución del algoritmo encontrará las longitudes (pesos sumados) de los caminos más cortos entre todos los pares de
vértices. Con una pequeña variación, puede imprimir la ruta más corta y puede detectar ciclos negativos en un gráfico. Floyd
Warshall es un algoritmo de programación dinámica.

Veamos un ejemplo. Vamos a aplicar el algoritmo de Floyd-Warshall en este gráfico:

Lo primero que hacemos es tomar dos matrices 2D. Estas son matrices de adyacencia. El tamaño de las matrices va a ser
el número total de vértices. Para nuestro gráfico, tomaremos matrices de 4 * 4 . La matriz de distancia va a almacenar la
distancia mínima encontrada hasta ahora entre dos vértices. Al principio, para los bordes, si hay un borde entre uv y el
distancia/peso es w, almacenaremos: distancia[u][v] = w. Para todos los bordes que no existen, vamos a poner infinito.
Path Matrix es para regenerar la ruta de distancia mínima entre dos vértices. Inicialmente, si hay un camino
entre u y v, vamos a poner path[u][v] = u. Esto significa que la mejor manera de llegar a vertex-v desde vertex-u
es usar la arista que conecta v con u. Si no hay camino entre dos vértices, vamos a poner N allí
indicando que no hay ninguna ruta disponible ahora. Las dos tablas para nuestro gráfico se verán así:

+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| |1|2|3|4| | |1| 2 |3|4|
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| 1 | 0 | 3 | 6 | 15 | | 1 | norte | 1 | 1 | 1 |
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| 2 | información | 0 | -2 | información | | 2 | norte | norte | 2 | norte |
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| 3 | información | información | 0 | 2 | | 3 | norte | norte | norte | 3 |
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| 4 | 1 | información | información | 0 | | 4 | 4 | norte | norte | norte |
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
distancia sendero

Como no hay bucle, las diagonales se establecen como N. Y la distancia desde el vértice en sí es 0.

Para aplicar el algoritmo de Floyd-Warshall, vamos a seleccionar un vértice medio k. Luego, para cada vértice i, vamos a
comprobar si podemos ir de i a k y luego k a j, donde j es otro vértice y minimizar el costo de ir de i a j. Si
la distancia actual [i][j] es mayor que distancia[i][k] + distancia[k][j], vamos a poner distancia[i][j] igual a
la suma de esas dos distancias. Y el camino[i][j] se establecerá en camino[k][j], ya que es mejor ir de i a k,

GoalKicker.com – Notas de algoritmos para profesionales 124


Machine Translated by Google

y luego k a j. Todos los vértices serán seleccionados como k. Tendremos 3 bucles anidados: para k que va de 1 a 4, yo que va de
1 a 4 y j va de 1 a 4. Vamos a comprobar:

si distancia[i][j] > distancia[i][k] + distancia[k][j]


distancia[i][j] := distancia[i][k] + distancia[k][j]
ruta[i][j] := ruta[k][j]
terminar si

Entonces, lo que básicamente estamos verificando es, para cada par de vértices, ¿obtenemos una distancia más corta al pasar por otro
¿vértice? El número total de operaciones para nuestro gráfico será 4 * 4 * 4 = 64. Eso significa que vamos a hacer esta verificación.
64 veces Veamos algunos de ellos:

Cuando k = 1, i = 2 y j = 3, distancia[i][j] es -2, que no es mayor que distancia[i][k] + distancia[k][j] = -2 + 0 = -2.


Por lo tanto, permanecerá sin cambios. De nuevo, cuando k = 1, i = 4 y j = 2, distancia[i][j] = infinito, que es mayor que
distancia[i][k] + distancia[k][j] = 1 + 3 = 4. Así que ponemos distancia[i][j] = 4, y ponemos camino[i][j] = camino[k] [j] = 1. ¿Qué
esto significa que, para pasar del vértice-4 al vértice-2, el camino 4->1->2 es más corto que el camino existente. Así es como nosotros
rellenar ambas matrices. El cálculo para cada paso se muestra aquí. Después de hacer los cambios necesarios, nuestras matrices
se vera como:

+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| |1|2|3|4| | |1| 2 |3|4|
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
|1|0|3|1|3| | 1 | norte | 1 | 2 | 3 |
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| 2 | 1 | 0 | -2 | 0 | | 2 | 4 | norte | 2 | 3 |
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
|3|3|6|0|2| | 3 | 4 | 1 | norte | 3 |
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
|4|1|4|2|0| | 4 | 4 | 1 | 2 | norte |
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
distancia sendero

Esta es nuestra matriz de distancia más corta. Por ejemplo, la distancia más corta de 1 a 4 es 3 y la distancia más corta
entre 4 a 3 es 2. Nuestro pseudocódigo será:

Procedimiento Floyd-Warshall (Gráfico):


para k de 1 a V // V denota el número de vértice
para i de 1 a V
para j de 1 a V
si distancia[i][j] > distancia[i][k] + distancia[k][j]
distancia[i][j] := distancia[i][k] + distancia[k][j]
ruta[i][j] := ruta[k][j]
terminar si
fin para
fin para
fin para

Imprimiendo la ruta:

Para imprimir la ruta, revisaremos la matriz Path . Para imprimir la ruta de u a v, comenzaremos desde ruta [u] [v]. Bien puesto
siga cambiando v = ruta [u] [v] hasta que encontremos ruta [u] [v] = u y empuje todos los valores de ruta [u] [v] en una pila. Después
al encontrar u, imprimiremos u y comenzaremos a extraer elementos de la pila e imprimirlos. Esto funciona porque la matriz de ruta
almacena el valor del vértice que comparte el camino más corto a v desde cualquier otro nodo. El pseudocódigo será:

Procedimiento PrintPath (origen, destino):

GoalKicker.com – Notas de algoritmos para profesionales 125


Machine Translated by Google

s = Pila()
S.push(destino) while
Ruta[fuente][destino] no es igual a fuente S.push(Ruta[origen]
[destino]) destino := Ruta[origen][destino] end while

imprimir -> fuente


mientras S no está vacío
imprimir -> S.pop
final mientras

Encontrar el ciclo de borde negativo:

Para saber si hay un ciclo de borde negativo, necesitaremos verificar la diagonal principal de la matriz de distancia . Si cualquier valor
en la diagonal es negativo, eso significa que hay un ciclo negativo en el gráfico.

Complejidad:

La complejidad del algoritmo de Floyd-Warshall es O(V³) y la complejidad del espacio es: O(V²).

GoalKicker.com – Notas de algoritmos para profesionales 126


Machine Translated by Google

Capítulo 23: Algoritmo numérico catalán


Sección 23.1: Información básica del algoritmo numérico catalán
El algoritmo de números catalanes es un algoritmo de programación dinámica.

En matemáticas combinatorias, los números catalanes forman una secuencia de números naturales que ocurren en varios problemas
de conteo, a menudo involucrando objetos definidos recursivamente. Los números catalanes en enteros no negativos n son un conjunto de
números que surgen en problemas de enumeración de árboles del tipo, "¿De cuántas maneras se puede dividir un n-ágono regular en n-2
triángulos si las diferentes orientaciones se cuentan por separado?"

Aplicación del algoritmo numérico catalán:

1. La cantidad de formas de apilar monedas en una fila inferior que consta de n monedas consecutivas en un plano, de modo que no se
permite colocar monedas en los dos lados de las monedas inferiores y cada moneda adicional debe estar encima de otras dos
monedas. , es el enésimo número catalán.
2. El número de formas de agrupar una cadena de n pares de paréntesis, de modo que cada paréntesis abierto tenga un
paréntesis cerrado coincidente, es el n-ésimo número catalán.
3. El número de formas de cortar un polígono convexo de n+2 lados en un plano en triángulos conectando los vértices con líneas rectas
que no se intersecan es el n-ésimo número catalán. Esta es la aplicación en la que se interesó Euler.

Usando la numeración basada en cero, el n-ésimo número catalán se da directamente en términos de coeficientes binomiales mediante
la siguiente ecuación.

Ejemplo de Número Catalán:

Aquí el valor de n = 4. (Mejor ejemplo: de Wikipedia)

Espacio Auxiliar: O(n)

GoalKicker.com – Notas de algoritmos para profesionales 127


Machine Translated by Google

Complejidad del tiempo: O(n^2)

GoalKicker.com – Notas de algoritmos para profesionales 128


Machine Translated by Google

Capítulo 24: Algoritmos de subprocesos múltiples

Ejemplos de algunos algoritmos multiproceso.

Sección 24.1: Multihilo de multiplicación de matriz cuadrada


multiplicar-matriz-cuadrada-paralelo(A, B) n =
A.líneas
C = Matrix(n,n) //crea una nueva matriz n*n paralela
para i = 1 a n
paralelo para j = 1 a n
C[i][j] = 0 para
k=1an
C[i][j] = C[i][j] + A[i][k]*B[k][j]
volver C

Sección 24.2: Matriz de multiplicación vector multihilo


matrix-vector(A,x) n =
A.lines y =
Vector(n) //crea un nuevo vector de longitud n paralelo para i =
1 a n y[i] = 0 paralelo para i = 1 a n para j = 1 a n y[i] = y[i] + A[i]
[j]*x[j] devuelve y

Sección 24.3: fusionar-clasificar multihilo


A es una matriz y los índices p y q de la matriz, como si ordenara la sub-matriz A[p..r]. B es una submatriz que se completará con la
ordenación.

Una llamada a p-merge-sort(A,p,r,B,s) ordena los elementos de A[p..r] y los coloca en B[s..s+rp].

p-merge-sort(A,p,r,B,s) n = r-
p+1 si n==1

B[s] = A[p]
más
T = new Array(n) //crear una nueva matriz T de tamaño n q =
floor((p+r)/2)) q_prime = q-p+1 spawn p-merge-sort(A,p,q, T,1) p-
merge-sort(A,q+1,r,T,q_prime+1)

sincronización p-merge(T,1,q_prime,q_prime+1,n,B,s)

Aquí está la función auxiliar que realiza la fusión en paralelo. p-merge


asume que las dos sub-matrices a fusionar están en la misma matriz pero no asume que son adyacentes en la matriz. Por eso
necesitamos p1,r1,p2,r2.

p-combinar(T,p1,r1,p2,r2,A,p3) n1 =
r1-p1+1 n2 = r2-p2+1 si n1<n2

// comprueba si n1>=n2

GoalKicker.com – Notas de algoritmos para profesionales 129


Machine Translated by Google

permutar p1 y p2
permutar r1 y r2 permutar
n1 y n2 si n1==0 //
ambos vacíos?
return
else q1 =
piso((p1+r1)/2) q2 = búsqueda
dicotómica(T[q1],T,p2,r2) q3 = p3 + (q1-p1) + (q2-p2)

A[q3] = T[q1]
generar p-combinar (T,p1,q1-1,p2,q2-1,A,p3) p-
combinar(T,q1+1,r1,q2,r2,A, q3+1) sincronización

Y aquí está la función auxiliar de búsqueda dicotómica.

x es la clave a buscar en el subarreglo T[p..r].

búsqueda dicotómica(x,T,p,r) inf = p


sup = max(p,r+1) while inf<sup
mitad = piso((inf+sup)/2) if
x<=T[half] sup = mitad más inf
= mitad+1

volver a cenar

GoalKicker.com – Notas de algoritmos para profesionales 130


Machine Translated by Google

Capítulo 25: Knuth Morris Pratt (KMP)


Algoritmo
El KMP es un algoritmo de coincidencia de patrones que busca las apariciones de una "palabra" W dentro de una "cadena de texto" principal
empleando la observación de que cuando ocurre una falta de coincidencia, tenemos la información suficiente para determinar dónde podría
comenzar la próxima coincidencia. aproveche esta información para evitar hacer coincidir los caracteres que sabemos que de todos modos
coincidirán. La complejidad del peor de los casos para buscar un patrón se reduce a O (n).

Sección 25.1: KMP-Ejemplo


Algoritmo

Este algoritmo es un proceso de dos pasos. Primero creamos una matriz auxiliar lps[] y luego usamos esta matriz para buscar el patrón.

Preprocesamiento :

1. Preprocesamos el patrón y creamos una matriz auxiliar lps[] que se usa para omitir caracteres mientras
pareo.
2. Aquí lps[] indica el prefijo propio más largo que también es un sufijo. Un prefijo adecuado es un prefijo en el que no se incluye la
“ “
cadena completa. Por ejemplo, los prefijos de la cadena ABC son "AB". ”, “A”, “AB”
Los sufijos
y “ABC”.
de laLos
cadena sonadecuados son ”, “A” y
prefijos

”, “C”, “BC” y “ABC”.

buscando

1. Seguimos haciendo coincidir los caracteres txt[i] y pat[j] y seguimos incrementando i y j mientras que pat[j] y txt[i] se mantienen
pareo.

2. Cuando vemos una falta de coincidencia, sabemos que los caracteres pat[0..j-1] coinciden con txt[i-j+1…i-1]. También sabemos que
lps[j-1] es el conteo de caracteres de pat[0…j-1] que son prefijos y sufijos adecuados. De esto podemos concluir que no necesitamos
hacer coincidir estos caracteres lps[j-1] con txt[ ij…i-1] porque sabemos que estos caracteres coincidirán de todos modos.

Implementación en Java

clase pública KMP {

public static void main(String[] args) { // TODO


Método generado automáticamente stub
String str = "abcabdabc"; Patrón de cadena =
"abc"; KMP obj = nuevo KMP();

System.out.println(obj.patternExistKMP(str.toCharArray(), pattern.toCharArray()));
}

public int[] computeLPS(char[] str){ int lps[]


= new int[str.length];

lps[0] = 0; int
j = 0; for(int i
=1;i<str.longitud;i++){ if(str[j] == str[i])
{ lps[i] = j+1; j++;

GoalKicker.com – Notas de algoritmos para profesionales 131


Machine Translated by Google

yo+
+; }
else{ if(j!=0)
{ j = lps[j-1]; }si
no{ lps[i] = j+1; yo+
+;

}
}

volver lps;
}

public boolean patternExistKMP(char[] text,char[] pat){


int[] lps = computarLPS(pat); inti
=0,j=0; while(i<texto.longitud &&
j<pat.longitud){ if(texto[i] == pat[j]){ i++; j++; }
else{ if(j!=0){ j = lps[j-1]; }más{ i++;

}
}
}

if(j==pat.length)
devuelve
verdadero; devolver falso;
}

GoalKicker.com – Notas de algoritmos para profesionales 132


Machine Translated by Google

Capítulo 26: Editar distancia dinámica


Algoritmo
Sección 26.1: Ediciones mínimas requeridas para convertir la cadena 1 a
cadena 2
La declaración del problema es como si nos dieran dos cadenas str1 y str2, ¿cuántas cantidades mínimas de
Las operaciones se pueden realizar en el str1 que se convierte en str2. Las operaciones pueden ser:

1. Insertar
2. Eliminar

3. Reemplazar

Por ejemplo

Entrada: str1 = "geek", str2 = "gesek"


Salida: 1
Solo necesitamos insertar s en la primera cadena

Entrada: str1 = "marcha", str2 = "carrito"


Salida: 3
Necesitamos reemplazar m con c y eliminar el carácter c y luego reemplazar h con t

Para resolver este problema usaremos una matriz 2D dp[n+1][m+1] donde n es la longitud de la primera cadena y m es la
longitud de la segunda cuerda. Para nuestro ejemplo, si str1 es azcef y str2 es abcdef , nuestro arreglo será dp[6][7]y
nuestra respuesta final se almacenará en dp[5][6].

(a B C D e F)
+---+---+---+---+---+---+---+
|0|1|2|3|4|5|6|
+---+---+---+---+---+---+---+
(un)| 1 | | | | | | |
+---+---+---+---+---+---+---+
(z)| 2 | | | | | | |
+---+---+---+---+---+---+---+
(c)| 3 | | | | | | |
+---+---+---+---+---+---+---+
(e)| 4 | | | | | | |
+---+---+---+---+---+---+---+
(f)| 5 | | | | | | |
+---+---+---+---+---+---+---+

Para dp[1][1] tenemos que comprobar qué podemos hacer para convertir a en a . Será 0. Para dp [1][2] tenemos que comprobar qué podemos
hacemos para convertir a en ab. Será 1 porque tenemos que insertar b. Entonces, después de la primera iteración, nuestra matriz se verá así

(a B C D e F)
+---+---+---+---+---+---+---+
|0|1|2|3|4|5|6|
+---+---+---+---+---+---+---+
(un)| 1 | 0 | 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+---+---+
(z)| 2 | | | | | | |
+---+---+---+---+---+---+---+
(c)| 3 | | | | | | |

GoalKicker.com – Notas de algoritmos para profesionales 133


Machine Translated by Google
+---+---+---+---+---+---+---+
(e)| 4 | | | | | | |
+---+---+---+---+---+---+---+
(f)| 5 | | | | | | |
+---+---+---+---+---+---+---+

Para la iteración 2

Para dp[2][1] , debemos verificar que para convertir az en a necesitamos eliminar z, por lo tanto, dp[2][1] será 1. Similar para

dp[2][2] necesitamos reemplazar z con b, por lo tanto, dp[2][2] será 1. Entonces, después de la segunda iteración, nuestra matriz dp[] se verá así.

(a B C D e F)
+---+---+---+---+---+---+---+
|0|1|2|3|4|5|6|
+---+---+---+---+---+---+---+
(un)| 1 | 0 | 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+---+---+
(z)| 2 | 1 | 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+---+---+
(c)| 3 | | | | | | |
+---+---+---+---+---+---+---+
(e)| 4 | | | | | | |
+---+---+---+---+---+---+---+
(f)| 5 | | | | | | |
+---+---+---+---+---+---+---+

Así que nuestra fórmula se verá como

si los personajes son iguales


dp[i][j] = dp[i-1][j-1];
más
dp[i][j] = 1 + Min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])

Después de la última iteración, nuestra matriz dp[] se verá como

(a B C D e F)
+---+---+---+---+---+---+---+
|0|1|2|3|4|5|6|
+---+---+---+---+---+---+---+
(un)| 1 | 0 | 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+---+---+
(z)| 2 | 1 | 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+---+---+
(c)| 3 | 2 | 2 | 1 | 2 | 3 | 4 |
+---+---+---+---+---+---+---+
(e)| 4 | 3 | 3 | 2 | 2 | 2 | 3 |
+---+---+---+---+---+---+---+
(f)| 5 | 4 | 4 | 2 | 3 | 3 | 3 |
+---+---+---+---+---+---+---+

Implementación en Java

public int getMinConversions(String str1, String str2){


int dp[][] = new int[str1.longitud()+1][str2.longitud()+1];
for(int i=0;i<=str1.length();i++){
para(int j=0;j<=str2.longitud();j++){
si(i==0)

GoalKicker.com – Notas de algoritmos para profesionales 134


Machine Translated by Google

dp[i][j] = j; más
si (j==0) dp[i][j] = i;
else
if(str1.charAt(i-1) == str2.charAt(j-1)) dp[i][j] = dp[i-1][j-1];
else{ dp[i][j] = 1 + Math.min(dp[i-1][j], Math.min(dp[i]
[j-1], dp[i-1][j-1 ]));

}
}

} return dp[str1.longitud()][str2.longitud()];
}

Complejidad del tiempo

O(n^2)

GoalKicker.com – Notas de algoritmos para profesionales 135


Machine Translated by Google

Capítulo 27: Algoritmos en línea


Teoría

Definición 1: Un problema de optimización ÿ consta de un conjunto de instancias ÿÿ. Para cada instancia ÿÿÿÿ existe un conjunto ÿÿ de
soluciones y una función objetivo fÿ : ÿÿ ÿ ÿÿ0 que asigna un valor real positivo a cada solución.
Decimos que OPT(ÿ) es el valor de una solución óptima, A(ÿ) es la solución de un Algoritmo A para el problema ÿ y wA(ÿ)=fÿ(A(ÿ)) su
valor.

Definición 2: Un algoritmo en línea A para un problema de minimización ÿ tiene una razón competitiva de r ÿ 1 si hay una constante
ÿÿÿ con

wA(ÿ) = fÿ(A(ÿ)) ÿ r ÿ OPT(&sigma) + ÿ

para todos los casos ÿÿÿÿ. A se llama un algoritmo en línea r-competitivo . Incluso

wA(ÿ) ÿ r ÿ OPT(&sigma)

para todos los casos ÿÿÿÿ entonces A se denomina algoritmo en línea estrictamente r-competitivo .

Proposición 1.3: LRU y FWF son algoritmos de marcado.

Prueba: al comienzo de cada fase (excepto la primera) , FWF tiene un error de caché y borró el caché. eso significa que tenemos k
páginas vacías. En cada fase se solicitan un máximo de k páginas diferentes, por lo que ahora habrá desalojo durante la fase. Entonces
FWF es un algoritmo de marcado.
Supongamos que LRU no es un algoritmo de marcado. Luego hay una instancia ÿ donde LRU una página marcada x en la fase i
desalojada. Sea ÿt la solicitud en la fase i donde x es desalojada. Dado que x está marcado, tiene que haber una solicitud anterior ÿt* para x
en la misma fase, por lo que t* < t. Después de t* x es la página más nueva del caché, por lo que para ser desalojado en t, la secuencia
ÿt*+1,...,ÿt tiene que solicitar al menos k de x páginas diferentes. Eso implica que la fase i ha solicitado al menos k+1 páginas diferentes, lo
que contradice la definición de la fase. Entonces LRU tiene que ser un algoritmo de marcado.

Proposición 1.4: Todo algoritmo de marcado es estrictamente k-competitivo.

Prueba: Sea ÿ una instancia del problema de paginación y l el número de fases de ÿ. Si l = 1, entonces todos los algoritmos de marcado son
óptimos y el algoritmo fuera de línea óptimo no puede ser mejor.
Suponemos que l ÿ 2. El costo de cada algoritmo de marcado, por ejemplo, ÿ está acotado desde arriba con l ÿ k porque en cada fase un
algoritmo de marcado no puede desalojar más de k páginas sin desalojar una página marcada.
Ahora tratamos de mostrar que el algoritmo fuera de línea óptimo expulsa al menos k+l-2 páginas para ÿ, k en la primera fase y al menos
una para cada fase siguiente excepto la última. Como prueba, definamos l-2 subsecuencias disjuntas de ÿ.
La subsecuencia i ÿ {1,...,l-2} comienza en la segunda posición de la fase i+1 y finaliza en la primera posición de la fase i+2.
Sea x la primera página de la fase i+1. Al comienzo de la subsecuencia i hay una página x y como máximo k-1 páginas diferentes en la
memoria caché óptima de algoritmos fuera de línea. En la subsecuencia, hay k solicitudes de página diferentes de x, por lo que el algoritmo
fuera de línea óptimo tiene que desalojar al menos una página para cada subsecuencia. Dado que al comienzo de la fase 1, el caché aún
está vacío, el algoritmo fuera de línea óptimo provoca k desalojos durante la primera fase. Eso demuestra que

wA(ÿ) ÿ lÿk ÿ (k+l-2)k ÿ OPT(ÿ) ÿ k

Corolario 1.5: LRU y FWF son estrictamente k-competitivos.

GoalKicker.com – Notas de algoritmos para profesionales 136


Machine Translated by Google

Si no hay una constante r para la cual un algoritmo en línea A sea r-competitivo, llamamos A no competitivo.

Proposición 1.6: LFU y LIFO no son competitivos.

Prueba: Sea l ÿ 2 una constante, k ÿ 2 el tamaño del caché. Las diferentes páginas de caché están numeradas 1,...,k+1. Nos fijamos
en la siguiente secuencia:

La primera página 1 se solicita l veces que la página 2 y así sucesivamente. Al final hay (l-1) solicitudes alternas de página k y k+1.

LFU y LIFO llenan su caché con páginas 1-k. Cuando se solicita la página k+1 se desaloja la página k y viceversa. Eso significa que
cada solicitud de la subsecuencia (k,k+1)l-1 expulsa una página. Además, hay errores de caché k-1 por primera vez en el uso de las páginas
1-(k-1). Entonces LFU y LIFO desalojan exactamente k-1+2(l-1) páginas.
Ahora debemos demostrar que para toda constante ÿÿÿ y toda constante r ÿ 1 existe un l tal que

que es igual a

Para satisfacer esta desigualdad solo tienes que elegir l lo suficientemente grande. Entonces LFU y LIFO no son competitivos.

Proposición 1.7: No existe un algoritmo en línea determinista r-competitivo para paginación con r < k.

Fuentes
Material básico

1. Script Online Algorithms (alemán), Heiko Roeglin, Universidad de Bonn 2.


Algoritmo de reemplazo de página

Otras lecturas

1. Computación en línea y análisis competitivo por Allan Borodin y Ran El-Yaniv

Código fuente

1. Código fuente para el almacenamiento en

caché sin conexión 2. Código fuente del juego adversario

Sección 27.1: Paginación (almacenamiento en caché en línea)

Prefacio

En lugar de comenzar con una definición formal, el objetivo es abordar este tema a través de una serie de ejemplos, introduciendo
definiciones en el camino. La sección de comentarios Teoría constará de todas las definiciones, teoremas y proposiciones para brindarle
toda la información para buscar más rápidamente aspectos específicos.

GoalKicker.com – Notas de algoritmos para profesionales 137


Machine Translated by Google

Las fuentes de la sección de comentarios consisten en el material base utilizado para este tema e información adicional para lecturas
adicionales. Además, encontrará los códigos fuente completos para los ejemplos allí. Preste atención a que para hacer que el código fuente de
los ejemplos sea más legible y más corto, se abstiene de cosas como el manejo de errores, etc. También transmite algunas características
específicas del lenguaje que oscurecerían la claridad del ejemplo, como el uso extensivo de bibliotecas avanzadas, etc.

Paginación

El problema de paginación surge de la limitación del espacio finito. Supongamos que nuestro caché C tiene k páginas. Ahora queremos
procesar una secuencia de m solicitudes de página que deben haberse colocado en la memoria caché antes de que se procesen. Por supuesto,
si m<=k , simplemente colocamos todos los elementos en el caché y funcionará, pero generalmente es m>>k.

Decimos que una solicitud es un acierto de caché, cuando la página ya está en caché; de lo contrario, se llama pérdida de caché. En ese
caso, debemos llevar la página solicitada a la memoria caché y expulsar otra, suponiendo que la memoria caché esté llena. El objetivo es un
cronograma de desalojos que minimice el número de desalojos.

Existen numerosas estrategias para este problema, veamos algunas:

1. Primero en entrar, primero en salir (FIFO): se desaloja la página


más antigua 2. Último en entrar, primero en salir (LIFO): se desaloja
la página más nueva 3. Usada menos recientemente (LRU): se desaloja la página cuyo acceso
más reciente fue el más antiguo 4 .Usado con menor frecuencia (LFU): desalojar la página que se
solicitó con menos frecuencia 5. Distancia de reenvío más larga (LFD): desalojar la página en el caché que no se solicita hasta el futuro más lejano.
6. Vaciar cuando esté lleno (FWF): borre el caché por completo tan pronto como ocurra una falta de caché

Hay dos formas de abordar este problema:

1. fuera de línea: la secuencia de solicitudes de página se conoce con anticipación


2. en línea: la secuencia de solicitudes de página no se conoce con anticipación

Enfoque fuera de línea

Para el primer enfoque, consulte el tema Aplicaciones de la técnica Greedy. Su tercer ejemplo de almacenamiento en caché sin
conexión considera las primeras cinco estrategias anteriores y le brinda un buen punto de entrada para lo siguiente.

El programa de ejemplo se amplió con la estrategia FWF :

clase FWF : Estrategia pública


{ pública:
FWF() : Estrategia("FWF") { }

int apply(int requestIndex) override {

for(int i=0; i<cacheSize; ++i) {

if(cache[i] == request[requestIndex]) return i;

// después de la primera página vacía, todas las demás deben estar vacías ;
de lo contrario, si (caché [i] == página vacía) devuelve i;

// no hay páginas libres

GoalKicker.com – Notas de algoritmos para profesionales 138


Machine Translated by Google

devolver 0;
}

anular la actualización (int cachePos, int requestIndex, bool cacheMiss) anular


{

// no hay páginas libres -> señorita -> borrar caché


if(cacheMiss && cachePos == 0)
{
for(int i = 1; i < cacheSize; ++i)
cache[i] = paginavacia;
}
}
};

El código fuente completo está disponible aquí. Si reutilizamos el ejemplo del tema, obtenemos el siguiente resultado:

Estrategia: FWF

Caché inicial: (a,b,c)

Solicitud caché 0 caché 1 caché 2 caché perdida


a a b C
a a b C
d X X X
mi dd mi X
b d mi b
b d mi b
a a X X X
C a C X
a C F
f.d. X X X
mi dd mi X
a mi a
pensión X X
completa Xb X
mi dfff b mi

C C X X X

Errores totales de caché: 5

Aunque LFD es óptimo, FWF tiene menos errores de caché. Pero el objetivo principal era minimizar el número de
desalojos y para FWF cinco fallos significan 15 desalojos, lo que la convierte en la opción más pobre para este ejemplo.

Enfoque en línea

Ahora queremos abordar el problema en línea de la paginación. Pero primero necesitamos entender cómo hacerlo.
Obviamente, un algoritmo en línea no puede ser mejor que el algoritmo fuera de línea óptimo. Pero, ¿cuánto peor es? Nosotros
necesita definiciones formales para responder a esa pregunta:

Definición 1.1: Un problema de optimización ÿ consta de un conjunto de instancias ÿÿ. Para cada caso ÿÿÿÿ hay un
conjunto ÿÿ de soluciones y una función objetivo fÿ : ÿÿ ÿ ÿÿ0 que asigna un valor real positivo a cada solución.
Decimos que OPT(ÿ) es el valor de una solución óptima, A(ÿ) es la solución de un Algoritmo A para el problema ÿ y
wA(ÿ)=fÿ(A(ÿ)) su valor.

Definición 1.2: Un algoritmo en línea A para un problema de minimización ÿ tiene una relación competitiva de r ÿ 1 si hay un
constante ÿÿÿ con

GoalKicker.com – Notas de algoritmos para profesionales 139


Machine Translated by Google

wA(ÿ) = fÿ(A(ÿ)) ÿ r ÿ OPT(ÿ) + ÿ

para todos los casos ÿÿÿÿ. A se llama un algoritmo en línea r-competitivo . Incluso

wA(ÿ) ÿ r ÿ OPT(ÿ)

para todos los casos ÿÿÿÿ entonces A se denomina algoritmo en línea estrictamente r-competitivo .

Entonces, la pregunta es qué tan competitivo es nuestro algoritmo en línea en comparación con un algoritmo fuera de línea óptimo.
En su famoso libro Allan Borodin y Ran El-Yaniv utilizaron otro escenario para describir la situación de la paginación en línea:

Hay un adversario malvado que conoce su algoritmo y el algoritmo fuera de línea óptimo. En cada paso, intenta solicitar una página que
sea peor para usted y, al mismo tiempo, mejor para el algoritmo fuera de línea. el factor competitivo de su algoritmo es el factor de qué tan
mal le fue a su algoritmo en comparación con el algoritmo fuera de línea óptimo del adversario. Si quieres intentar ser el adversario, puedes
probar el Adversary Game (trate de superar las estrategias de paginación).

Algoritmos de marcado

En lugar de analizar cada algoritmo por separado, veamos una familia especial de algoritmos en línea para el problema de
paginación llamada algoritmos de marcado.

Sea ÿ=(ÿ1,...,ÿp) una instancia para nuestro problema y k nuestro tamaño de caché, entonces ÿ se puede dividir en fases:

La fase 1 es la subsecuencia máxima de ÿ desde el inicio hasta el máximo de k páginas diferentes solicitadas
La fase i ÿ 2 es la subsecuencia máxima de ÿ desde el final del pase i-1 hasta el máximo de k solicitudes de páginas diferentes

Por ejemplo con k = 3:

Un algoritmo de marcado (implícita o explícitamente) mantiene si una página está marcada o no. Al comienzo de cada fase, todas las
páginas están sin marcar. Es una página solicitada durante una fase se marca. Un algoritmo es un algoritmo de marcado si nunca expulsa
una página marcada del caché. Eso significa que las páginas que se utilizan durante una fase no serán desalojadas.

Proposición 1.3: LRU y FWF son algoritmos de marcado.

Prueba: al comienzo de cada fase (excepto la primera) , FWF tiene un error de caché y borró el caché. eso significa que tenemos k
páginas vacías. En cada fase se solicitan un máximo de k páginas diferentes, por lo que ahora habrá desalojo durante la fase. Entonces
FWF es un algoritmo de marcado.
Supongamos que LRU no es un algoritmo de marcado. Luego hay una instancia ÿ donde LRU una página marcada x en la fase i
desalojada. Sea ÿt la solicitud en la fase i donde x es desalojada. Dado que x está marcado, tiene que haber una solicitud anterior ÿt* para x
en la misma fase, por lo que t* < t. Después de t* x es la página más nueva del caché, por lo que para ser desalojado en t, la secuencia
ÿt*+1,...,ÿt tiene que solicitar al menos k de x páginas diferentes. Eso implica que la fase i ha solicitado al menos k+1 páginas diferentes, lo
que contradice la definición de la fase. Entonces LRU tiene que ser un algoritmo de marcado.

GoalKicker.com – Notas de algoritmos para profesionales 140


Machine Translated by Google

Proposición 1.4: Todo algoritmo de marcado es estrictamente k-competitivo.

Prueba: Sea ÿ una instancia del problema de paginación y l el número de fases de ÿ. Si l = 1, entonces todos los algoritmos de marcado son
óptimos y el algoritmo fuera de línea óptimo no puede ser mejor.
Asumimos l ÿ 2. el costo de cada algoritmo de marcado, por ejemplo, ÿ está acotado superiormente con l ÿ k porque en cada fase un algoritmo
de marcado no puede desalojar más de k páginas sin desalojar una página marcada.
Ahora tratamos de mostrar que el algoritmo fuera de línea óptimo expulsa al menos k+l-2 páginas para ÿ, k en la primera fase y al menos una
para cada fase siguiente excepto la última. Como prueba, definamos l-2 subsecuencias disjuntas de ÿ.
La subsecuencia i ÿ {1,...,l-2} comienza en la segunda posición de la fase i+1 y finaliza en la primera posición de la fase i+2.
Sea x la primera página de la fase i+1. Al comienzo de la subsecuencia i hay una página x y como máximo k-1 páginas diferentes en la
memoria caché óptima de algoritmos fuera de línea. En la subsecuencia, hay k solicitudes de página diferentes de x, por lo que el algoritmo fuera
de línea óptimo tiene que desalojar al menos una página para cada subsecuencia. Dado que al comienzo de la fase 1, el caché aún está vacío,
el algoritmo fuera de línea óptimo provoca k desalojos durante la primera fase. Eso demuestra que

wA(ÿ) ÿ lÿk ÿ (k+l-2)k ÿ OPT(ÿ) ÿ k

Corolario 1.5: LRU y FWF son estrictamente k-competitivos.

Ejercicio: Muestre que FIFO no es un algoritmo de marcado, sino estrictamente k-competitivo.

¿No hay una constante r para la cual un algoritmo en línea A sea r-competitivo, llamamos A no competitivo?

Proposición 1.6: LFU y LIFO no son competitivos.

Prueba: Sea l ÿ 2 una constante, k ÿ 2 el tamaño del caché. Las diferentes páginas de caché están numeradas 1,...,k+1. Nos fijamos en la
siguiente secuencia:

La primera página 1 se solicita l veces que la página 2 y así sucesivamente. Al final, hay (l-1) solicitudes alternas de página k y k+1.

LFU y LIFO llenan su caché con páginas 1-k. Cuando se solicita la página k+1 se desaloja la página k y viceversa. Eso significa que cada
solicitud de la subsecuencia (k,k+1)l-1 expulsa una página. Además, hay errores de caché k-1 por el uso por primera vez de las páginas 1-(k-1).
Entonces LFU y LIFO desalojan exactamente k-1+2(l-1) páginas.
Ahora debemos demostrar que para toda constante ÿÿÿ y toda constante r ÿ 1 existe un l tal que

que es igual a

Para satisfacer esta desigualdad solo tienes que elegir l lo suficientemente grande. Entonces LFU y LIFO no son competitivos.

Proposición 1.7: No existe un algoritmo en línea determinista r-competitivo para paginación con r < k.

GoalKicker.com – Notas de algoritmos para profesionales 141


Machine Translated by Google

La prueba de esta última proposición es bastante larga y se basa en la afirmación de que LFD es un algoritmo fuera de línea
óptimo. El lector interesado puede buscarlo en el libro de Borodin y El-Yaniv (ver fuentes más abajo).

La pregunta es si podríamos hacerlo mejor. Para eso, tenemos que dejar atrás el enfoque determinista y comenzar a aleatorizar nuestro
algoritmo. Claramente, es mucho más difícil para el adversario castigar su algoritmo si es aleatorio.

La paginación aleatoria se discutirá en uno de los siguientes ejemplos...

GoalKicker.com – Notas de algoritmos para profesionales 142


Machine Translated by Google

Capítulo 28: Clasificación


Parámetro Descripción Un

algoritmo de clasificación es estable si conserva el orden relativo de elementos iguales después de la clasificación.
Estabilidad

Un algoritmo de clasificación está en su lugar si clasifica utilizando solo la memoria auxiliar O (1) (sin contar la matriz que debe clasificarse).
En su lugar

Un algoritmo de clasificación tiene una complejidad de tiempo en el mejor de los casos de O(T(n)) si su tiempo de ejecución es al menos
Complejidad del mejor caso
T(n) para todas las entradas posibles.

Complejidad Un algoritmo de clasificación tiene una complejidad de tiempo de caso promedio de O(T(n)) si su tiempo de ejecución, promediado sobre
promedio del caso todas las entradas posibles, es T(n).

Un algoritmo de clasificación tiene una complejidad de tiempo en el peor de los casos de O(T(n)) si su tiempo de ejecución es como máximo
Complejidad en el peor de los casos
T(n).

Sección 28.1: Estabilidad en la clasificación


La estabilidad en la clasificación significa si un algoritmo de clasificación mantiene el orden relativo de las claves iguales de la entrada original en la salida del resultado.

Por lo tanto, se dice que un algoritmo de ordenación es estable si dos objetos con claves iguales aparecen en el mismo orden en la salida ordenada que aparecen en la matriz no

ordenada de entrada.

Considere una lista de pares:

(1, 2) (9, 7) (3, 4) (8, 6) (9, 3)

Ahora ordenaremos la lista usando el primer elemento de cada par.

Una clasificación estable de esta lista generará la siguiente lista:

(1, 2) (3, 4) (8, 6) (9, 7) (9, 3)

Porque (9, 3) también aparece después de (9, 7) en la lista original.

Una clasificación inestable generará la siguiente lista:

(1, 2) (3, 4) (8, 6) (9, 3) (9, 7)

La ordenación inestable puede generar el mismo resultado que la ordenación estable, pero no siempre.

Clases estables conocidas:

Ordenar por fusión

Tipo de inserción
Clasificación de raíz

ordenar tim

Ordenamiento de burbuja

Clases inestables conocidas:

Ordenar montones

Ordenación rápida

GoalKicker.com – Notas de algoritmos para profesionales 143


Machine Translated by Google

Capítulo 29: Clasificación de burbujas

Parámetro Descripción
Estable Sí

En su lugar Sí

Complejidad del mejor caso En)

Complejidad promedio del caso O(n^2)


Complejidad en el peor de los casos O(n^2)

Complejidad del espacio O(1)

Sección 29.1: Clasificación por burbuja

BubbleSort compara cada par sucesivo de elementos en una lista desordenada e invierte los elementos si no están en orden.

El siguiente ejemplo ilustra la clasificación de burbujas en la lista {6,5,3,1,8,7,2,4} (los pares que se compararon en cada paso se encapsulan
en '**'):

{6,5,3,1,8,7,2,4}
{**5,6**,3,1,8,7,2,4} -- 5 < 6 -> intercambiar {5,*
*3,6**,1,8,7,2,4} -- 3 < 6 -> intercambiar
{5,3,**1,6**,8,7,2,4} -- 1 < 6 -> intercambiar
{5,3,1,**6,8**,7,2,4} -- 8 > 6 -> sin intercambiar
{5,3,1,6,**7,8** ,2,4} -- 7 < 8 -> intercambiar
{5,3,1,6,7,**2,8**,4} -- 2 < 8 -> intercambiar {5,3,1,6
,7,2,**4,8**} -- 4 < 8 -> intercambiar

Después de una iteración a través de la lista, tenemos {5,3,1,6,7,2,4,8}. Tenga en cuenta que el mayor valor sin ordenar de la matriz (8 en
este caso) siempre alcanzará su posición final. Por lo tanto, para asegurarnos de que la lista esté ordenada, debemos iterar n-1 veces para
listas de longitud n.

Gráfico:

Sección 29.2: Implementación en C y C++

Un ejemplo de implementación de BubbleSort en C++:

void bubbleSort(vector<int>numbers) {

for(int i = números.tamaño() - 1; i >= 0; i--) { for(int j = 1; j


<= i; j++) {
if(números[j-1] > números[j]) {
intercambio (números [j-1], números (j));

GoalKicker.com – Notas de algoritmos para profesionales 144


Machine Translated by Google

}
}
}
}

Implementación C

void bubble_sort( lista larga[], n larga ) {

largo c, d, t;

para (c = 0 ; c < ( n - 1 ); c++) {

para (d = 0 ; re < norte - c - 1; re++) {

if (lista[d] > lista[d+1]) {

/ * Intercambio */

t = lista[d];
lista[d] = lista[d+1]; lista[d+1]
= t;
}
}
}
}

Clasificación de burbujas con puntero

void pointer_bubble_sort(long * list, long n) {

largo c, d, t;

para (c = 0 ; c < ( n - 1 ); c++) {

para (d = 0 ; re < norte - c - 1; re++) {

if ( * (lista + d ) > *(lista+d+1)) {

/ * Intercambio */

t = * (lista + d ); * (lista
+ d ) = * (lista + d + 1 +); 1)
* (lista
= t; + d

}
}
}
}

Sección 29.3: Implementación en C#


La clasificación de burbuja también se conoce como clasificación de hundimiento. Es un algoritmo de clasificación simple que recorre repetidamente la lista

que se va a clasificar, compara cada par de elementos adyacentes y los intercambia si están en el orden incorrecto.

Ejemplo de clasificación de burbujas

GoalKicker.com – Notas de algoritmos para profesionales 145


Machine Translated by Google

Implementación de Bubble Sort


Utilicé el lenguaje C# para implementar el algoritmo de clasificación de burbujas

clase pública BubbleSort {

public static void SortBubble(entrada int[] ) {

for (var i = input.Length - 1; i >= 0; i--) {

for (var j = input.Length - 1 - 1; j >= 0; j--) {

si (entrada[j] <= entrada[j + 1]) continuar; var


temp = entrada[j + 1]; entrada[j + 1] = entrada[j];
entrada[j] = temperatura;

}
}
}

public static int[] Main(int[] entrada) {

SortBubble(entrada);
entrada de retorno ;
}
}

Sección 29.4: Implementación de Python


#!/ usr/ bin/ python

lista_entrada = [10,1,2,11]

for i in range(len(input_list)): for j in


range(i): if int(input_list[j]) >
int(input_list[j+1]): input_list[j],input_list[j+1] =
lista_entrada[j+1],lista_entrada[j]

imprimir lista_entrada

GoalKicker.com – Notas de algoritmos para profesionales 146


Machine Translated by Google

Sección 29.5: Implementación en Java


clase pública MyBubbleSort {

public static void bubble_srt(int array[]) {// main logic int n = array.length;
intk ; para (int m = n; m >= 0; m--) {

para (int i = 0; i < n - 1; i++) { k = i + 1; if


(matriz[i] > matriz[k]) {

swapNumbers(i, k, matriz);
}

} imprimirNúmeros(matriz);
}
}

números de intercambio vacíos estáticos privados (int i, int j, int [] matriz) {

temperatura
interna ; temperatura
= matriz[i]; matriz[i] =
matriz[j]; matriz[j] = temporal;
}

printNumbers vacío estático privado (entrada int [] ) {

for (int i = 0; i < input.length; i++)


{ System.out.print(input[i] + ", ");
}
Sistema.salida.println("\n");
}

public static void main(String[] args) { int[] input =


{ 4, 2, 9, 6, 23, 12, 34, 0, 1 }; burbuja_srt(entrada);

}
}

Sección 29.6: Implementación en Javascript


función bubbleSort(a)
{
var
intercambiado;
hacer { intercambiado
= falso; for (var i=0; i < a.longitud-1; i++) { if
(a[i] > a[i+1]) { var temp = a[i]; a[i] = a[i+1];
a[i+1] = temperatura; intercambiado
= verdadero;

} } while (intercambiado);
}

var a = [3, 203, 34, 746, 200, 984, 198, 764, 9];

GoalKicker.com – Notas de algoritmos para profesionales 147


Machine Translated by Google

ordenarburbujas(a);
consola.log(a); //registros [3, 9, 34, 198, 200, 203, 746, 764, 984]

GoalKicker.com – Notas de algoritmos para profesionales 148


Machine Translated by Google

Capítulo 30: Clasificación por fusión

Sección 30.1: Conceptos básicos de clasificación por fusión

Merge Sort es un algoritmo de divide y vencerás. Divide la lista de entrada de longitud n por la mitad sucesivamente hasta que
haya n listas de tamaño 1. Luego, los pares de listas se fusionan con el primer elemento más pequeño entre el par de listas que se
agregan en cada paso. A través de la fusión sucesiva y la comparación de los primeros elementos, se construye la lista ordenada.

Un ejemplo:

Complejidad del Tiempo: T(n) = 2T(n/2) + ÿ(n)

La recurrencia anterior se puede resolver utilizando el método de árbol de recurrencia o el método maestro. Cae en el caso II del
Método Maestro y la solución de la recurrencia es ÿ(nLogn). La complejidad de tiempo de Merge Sort es ÿ(nLogn) en los 3 casos
(peor, promedio y mejor) ya que merge sort siempre divide la matriz en dos mitades y toma un tiempo lineal para fusionar dos
mitades.

Espacio Auxiliar: O(n)

Paradigma algorítmico: divide y vencerás

GoalKicker.com – Notas de algoritmos para profesionales 149


Machine Translated by Google

Ordenar en el lugar: no en una implementación típica

Estable: Sí

Sección 30.2: Implementación de clasificación por combinación en Go

paquete principal

importar "fmt"

func mergeSort(a []int) []int { if len(a)


<2{
devolver un

} m := (largo(a)) / 2

f := mergeSort(a[:m]) s :=
mergeSort(a[m:])

volver fusionar (f, s)


}

func merge(f []int, s []int) []int { var i, j int


size := len(f) + len(s)

a := make([]int, tamaño, tamaño)

para z := 0; z < tamaño; z++


{ lenF := len(f) lenS := len(s)

si i > lenF-1 && j <= lenS-1 { a[z] =


s[j] j++

} else if j > lenS-1 && i <= lenF-1 { a[z] = f[i]

yo ++

} más si f[i] < s[j] { a[z] = f[i]


i++ } más { a[z] = s[j] j++

}
}

devolver un
}

func main()
{ a := []int{75, 12, 34, 45, 0, 123, 32, 56, 32, 99, 123, 11, 86, 33} fmt.Println(a)
fmt.Println( mergeSort(a))

Sección 30.3: Implementación de clasificación por fusión en C y C#

Ordenar por combinación C

GoalKicker.com – Notas de algoritmos para profesionales 150


Machine Translated by Google

int merge(int arr[],int l,int m,int h) {

int arr1[10],arr2[10]; // Dos matrices temporales para contener las


dos matrices que se fusionarán int n1,n2,i,j,k; n1=m-l+1; n2=hm;

for(i=0; i<n1; i++)


arr1[i]=arr[l+i];
para(j=0; j<n2; j++)
matriz2[j]=matriz[m+j+1];

matriz1[i]=9999; // Para marcar el final de cada arreglo temporal arr2[j]=9999;

yo=0; j=0; for(k=l; k<=h; k++) { //proceso de combinar dos arreglos ordenados
if(arr1[i]<=arr2[j])
arr[k]=arr1[i++]; else
arr[k]=arr2[j++];

devolver 0;
}

int merge_sort(int arr[],int bajo,int alto) {

int medio;
if(bajo<alto)
{ medio=(bajo+alto)/2;
// Divide y vencerás
merge_sort(arr,low,mid);
merge_sort(arr,mid+1,high);
// Combinar
merge(arr,bajo,medio,alto);
}

devolver 0;
}

Clasificación de combinación de C#

MergeSort de clase pública


{
Merge vacío estático (entrada int[] , int l, int m, int r) {

i, j ; var n1
= m - l + 1; var n2 = r - m;

var izquierda = new int[n1];


var derecha = new int[n2];

para (i = 0; i < n1; i++) {

izquierda[i] = entrada[l + i];


}

GoalKicker.com – Notas de algoritmos para profesionales 151


Machine Translated by Google

para (j = 0; j < n2; j++) {

derecha[j] = entrada[m + j + 1];


}

yo = 0;
j = 0;
var k = l;

mientras que (i < n1 && j < n2) {

if (izquierda[i] <= derecha[j]) {

entrada[k] = izquierda[i];
yo++;

}
más {
entrada[k] = derecha[j]; j++;

} k++;
}

mientras (yo < n1)


{
entrada[k] = izquierda[i];
yo++; k++;

mientras (j < n2) {

entrada[k] = derecha[j]; j+
+; k++;

}
}

static void SortMerge(int[] entrada, int l, int r) {

si (l < r) {

int m = l + (r - l) / 2;
OrdenarCombinar(entrada, l, m);
OrdenarCombinar(entrada, m + 1, r);
Combinar (entrada, l, m, r);
}
}

public static int[] Main(int[] input) {

SortMerge(entrada, 0, entrada.Longitud - 1); entrada


de retorno ;
}
}

Sección 30.4: Implementación de clasificación por combinación en Java

A continuación se muestra la implementación en Java utilizando un enfoque genérico. Es el mismo algoritmo, que se presenta
arriba.

GoalKicker.com – Notas de algoritmos para profesionales 152


Machine Translated by Google

interfaz pública InPlaceSort<T extiende Comparable<T>> { void sort(final


T[] elementos); }

public class MergeSort < T extiende Comparable < T >> implementa InPlaceSort < T > {

@Override
public void sort(T[] elementos) { T[] arr =
(T[]) new Comparable[elements.length]; sort(elementos, arr, 0,
elementos.longitud - 1);
}

// Verificamos ambos lados y luego los fusionamos private


void sort(T[] elementos, T[] arr, int low, int high) { if (low >= high) return; int mid =
bajo + (alto - bajo) / 2; sort(elementos, arr, bajo, medio); sort(elementos, arr,
mid + 1, high); fusionar (elementos, arr, bajo, alto, medio);

combinación de vacío privado (T [] a, T [] b, int bajo, int alto, int medio) { int i = bajo;
int j = medio + 1;

// Seleccionamos el elemento más pequeño de los dos. Y luego lo ponemos en b para (int k =
bajo; k <= alto; k++) {

if (i <= mid && j <= high) { if


(a[i].compareTo(a[j]) >= 0) { b[k] = a[j++]; }
más { b[k] = a[i++];

}
} else if (j > alto && i <= medio) { b[k] = a[i++]; }
else if (i > mid && j <= high) { b[k] = a[j++];

}
}

for (int n = bajo; n <= alto; n++) { a[n] = b[n];

}}}

Sección 30.5: Implementación de clasificación por combinación en Python

def merge(X, Y):


"
fusiona dos listas ordenadas " p1
= p2 = 0 out = [] while p1 < len(X)
and p2 < len(Y): if X[p1] < Y[p2]:
out. agregar(X[p1]) p1 += 1 más:
salir.agregar(Y[p2]) p2 += 1 salir += X[p1:]
+ Y[p2:]

GoalKicker.com – Notas de algoritmos para profesionales 153


Machine Translated by Google
regresar _

def mergeSort(A): if
len(A) <= 1: devuelve
A if len(A) ==
2: devuelve
ordenado(A)

mid = len(A) / 2 return


merge(mergeSort(A[:mid]), mergeSort(A[mid:]))

si __nombre__ == "__principal__":
# Genera 20 números aleatorios y ordénalos
A = [randint(1, 100) for i in xrange(20)] print mergeSort(A)

Sección 30.6: Implementación de Java de abajo hacia arriba


clase pública MergeSortBU {
conjunto de enteros [] estáticos privados = { 4, 3, 1, 8, 9, 15, 20, 2, 5, 6, 30, 70,
60,80,0,9,67,54,51,52,24,54,7 };

pública MergeSortBU() { }

combinación de vacío estático privado (Comparable [] arrayToSort, Comparable [] aux, int lo, int mid, int hi) {

for (int index = 0; index < arrayToSort.length; index++) { aux[index] = arrayToSort[index];

int i = lo; int j =


medio + 1; for (int k =
lo; k <= hi; k++) { if (i > mid)

arrayToSort[k] = aux[j++]; de lo
contrario si (j > hi) arrayToSort[k] = aux[i+
+]; else if (isLess(aux[i], aux[j]))
{ arrayToSort[k] = aux[i++]; } else { arrayToSort[k]
= aux[j++];

}
}

clasificación vacía estática pública (Comparable [] arrayToSort, Comparable [] aux, int lo, int hi) {
int N = arrayToSort.length; for (int sz =
1; sz < N; sz = sz + sz) { for (int low = 0; low < N; low = low
+ sz + sz) { System.out.println("Tamaño:"+ sz ); merge(arrayToSort, aux,
low, low + sz -1 ,Math.min(low + sz + sz - 1, N - 1));
imprimir(matrizParaClasificar);

}
}

public static boolean isLess(Comparable a, Comparable b) {


devuelve a.compareTo(b) <= 0;

GoalKicker.com – Notas de algoritmos para profesionales 154


Machine Translated by Google

impresión vacía estática privada (comparable[] matriz)


{http:// stackoverflow.com/ documentation/ algorithm/ 5732/ merge-sort# StringBuffer
buffer = new StringBuffer();http:// stackoverflow.com/ documentation/
algorithm/ 5732 / merge-sort# for ( Valor comparable : matriz) { buffer.append(value); búfer.append('
');

}
System.out.println(búfer);
}

public static void main(String[] args) { Comparable[]


aux = new Comparable[array.length]; imprimir (matriz);
MergeSortBU.sort(matriz, aux, 0, matriz.longitud - 1);

}
}

GoalKicker.com – Notas de algoritmos para profesionales 155


Machine Translated by Google

Capítulo 31: Clasificación por inserción

Sección 31.1: Implementación de Haskell


insertSort :: Ord a => [a] -> [a] insertSort []
= [] insertSort (x:xs) = insert x (insertSort xs)

insert :: Ord a => a-> [a] -> [a] insert n [] =


[n] insert n (x:xs) | norte <= x
= (n:x:xs)
| de lo contrario = x: insertar n xs

GoalKicker.com – Notas de algoritmos para profesionales 156


Machine Translated by Google

Capítulo 32: Clasificación de cubeta

Sección 32.1: Implementación de C#


clasificación de cubo de clase pública
{
public static void SortBucket(ref int[] entrada) {

int minValor = entrada[0]; int


maxValue = entrada[0]; int k
= 0;

for (int i = input.Length - 1; i >= 1; i--) {

if (entrada[i] > maxValor) maxValor = entrada[i]; if


(entrada[i] < minValor) minValor = entrada[i];
}

List<int>[] depósito = new List<int>[maxValue - minValue + 1];

for (int i = cubeta.Longitud - 1; i >= 0; i--) {

cubeta[i] = nueva Lista<int>();


}

foreach (int i en la entrada) {

cubeta[i - minValue].Add(i);
}

foreach (Lista<int> b en el cubo) {

if (b.Cuenta > 0) {

foreach (int t en b) {

entrada[k] = t;
k++;
}
}
}
}

public static int[] Main(int[] entrada) {

SortBucket ( entrada de referencia);


entrada de retorno ;
}
}

GoalKicker.com – Notas de algoritmos para profesionales 157


Machine Translated by Google

Capítulo 33: Clasificación rápida

Sección 33.1: Conceptos básicos de Quicksort

Ordenación rápida es un algoritmo de clasificación que elige un elemento ("el pivote") y reordena la matriz formando dos particiones
de modo que todos los elementos menores que el pivote vienen antes y todos los elementos mayores vienen después. Luego, el algoritmo
se aplica recursivamente a las particiones hasta que se ordena la lista.

1. Mecanismo de esquema de partición de Lomuto:

Este esquema elige un pivote que suele ser el último elemento de la matriz. El algoritmo mantiene el índice para poner el pivote en la variable i y cada vez que

encuentra un elemento menor o igual que el pivote, este índice se incrementa y ese elemento se colocaría antes del pivote.

la partición (A, bajo, alto) es


pivote : = A [alto] i : = bajo

for j := de bajo a alto – 1 hacer si


A[j] ÿ pivotar luego intercambiar
A[i] con A[j]
yo := yo + 1
intercambiar A[i] con A[high]
devolver i

Mecanismo de clasificación rápida:

clasificación rápida (A, bajo, alto)


es si bajo < alto entonces p :=
partición (A, bajo, alto) clasificación
rápida (A, bajo, p – 1) clasificación
rápida (A, p + 1, alto)

Ejemplo de clasificación rápida:

GoalKicker.com – Notas de algoritmos para profesionales 158


Machine Translated by Google

2. Esquema de partición de Hoare:

Utiliza dos índices que comienzan en los extremos de la matriz que se está particionando, luego se mueven uno hacia el otro, hasta que
detectan una inversión: un par de elementos, uno mayor o igual que el pivote, uno menor o igual, que están en la posición incorrecta.
orden entre sí. Luego se intercambian los elementos invertidos. Cuando los índices se encuentran, el algoritmo se detiene y devuelve el
índice final. El esquema de Hoare es más eficiente que el esquema de partición de Lomuto porque hace tres veces menos intercambios en
promedio y crea particiones eficientes incluso cuando todos los valores son iguales.

quicksort(A, lo, hola) es si lo < hi


entonces p := partición(A, lo, hola)
quicksort(A, lo, p) quicksort(A, p + 1, hola)

Partición:

la partición(A, lo, hola) es pivote :=


A[lo] i := lo - 1 j := hi + 1 loop forever
do:

yo := yo + 1

GoalKicker.com – Notas de algoritmos para profesionales 159


Machine Translated by Google

mientras que A[i] < pivote hacer

hacer:

j := j - 1
mientras que A[j] > pivote hacer

si i >= j entonces
devuelve j

intercambiar A[i] con A[j]

Sección 33.2: Quicksort en Python


def clasificación rápida(arr):
if len(arr) <= 1:
regreso _
pivote = arr[len(arr) / 2] izquierda =
[x para x en arr si x < pivote] medio = [x para x en arr
si x == pivote] derecha = [x para x en arr si x > pivote ]
volver ordenación rápida (izquierda) + centro + ordenación
rápida (derecha)

imprimir clasificación rápida ([3,6,8,10,1,2,1])

Imprime "[1, 1, 2, 3, 6, 8, 10]"

Sección 33.3: Implementación Java de la partición Lomuto


Solución de clase pública {

public static void main(String[] args) { Scanner sc = new


Scanner(System.in); int n = sc.nextInt(); int[] ar =
nuevo int[n]; for(int i=0; i<n; i++) ar[i] = sc.nextInt();
quickSort(ar, 0, ar.longitud-1);

ordenación rápida del vacío estático público (int [] ar, int bajo, int alto) {

si (bajo<alto) {

int p = partición(ar, bajo, alto); clasificación rápida


(ar, 0 , p-1); QuickSort(ar, p+1, alto);

} partición int estática pública (int[] ar, int l, int r) {

int pivote = ar[r]; int yo =


l; para(int j=l; j<r; j++) {

si(ar[j] <= pivote)


{
int t = ar[j]; ar[j] =
ar[i]; ar[i] = t; yo++;

GoalKicker.com – Notas de algoritmos para profesionales 160


Machine Translated by Google

} int t = ar[i]; ar[i]


= ar[r]; ar[r] = t;

devolver yo;
}

GoalKicker.com – Notas de algoritmos para profesionales 161


Machine Translated by Google

Capítulo 34: Clasificación de conteo

Sección 34.1: Información básica de clasificación de conteo


clasificación de conteo es un algoritmo de ordenación de enteros para una colección de objetos que ordena según las claves de
los objetos.

Pasos

1. Construya una matriz de trabajo C que tenga un tamaño igual al rango de la matriz de entrada A.
2. Iterar a través de A, asignando C[x] en función del número de veces que x apareció en A.
3. Transforme C en una matriz donde C[x] se refiere al número de valores ÿ x iterando a través de la matriz,
asignando a cada C[x] la suma de su valor anterior y todos los valores en C que vienen antes de él.
4. Iterar hacia atrás a través de A, colocando cada valor en una nueva matriz ordenada B en el índice registrado en C. Esto se hace para un
A[x] dado asignando B[C[A[x]]] a A[x ], y disminuyendo C[A[x]] en caso de que hubiera valores duplicados en la matriz original sin ordenar.

Ejemplo de clasificación por conteo

Espacio Auxiliar: O(n+k)


Complejidad de tiempo: Peor caso: O(n+k), Mejor caso: O(n), Caso promedio O(n+k)

Sección 34.2: Implementación de pseudocódigo


Restricciones:

1. Entrada (una matriz para ordenar)


2. Número de elemento en la entrada (n)
3. Teclas en el rango de 0..k-1 (k)
4. Contar (una matriz de números)

Pseudocódigo:

for x in input:
count[key(x)] += 1
total = 0 for i in range(k):
oldCount = count[i] count[i]
= total total += oldCount

GoalKicker.com – Notas de algoritmos para profesionales 162


Machine Translated by Google

para x en la
entrada: salida[cuenta[tecla(x)]]
= x cuenta[tecla(x)] += 1 salida
de retorno

GoalKicker.com – Notas de algoritmos para profesionales 163


Machine Translated by Google

Capítulo 35: Ordenar montones

Sección 35.1: Implementación de C#

clase pública HeapSort {

public static void Heapify(int[] input, int n, int i) {

int mayor = i; intl =


yo + 1 ; int r = yo +
2;

if (l < n && input[l] > input[mayor]) mayor = l;

if (r < n && input[r] > input[mayor]) mayor = r;

si (mayor != i) {

var temp = entrada[i];


entrada[i] = entrada[mayor];
entrada[mayor] = temperatura;
Heapify(entrada, n, mayor);
}
}

public static void SortHeap(int[] input, int n) {

para (var i = n - 1; i >= 0; i--) {

Heapificar (entrada, n, i);

} para (int j = n - 1; j >= 0; j--) {

var temperatura = entrada[0];


entrada[0] = entrada[j];
entrada[j] = temperatura;
Heapificar (entrada, j, 0);
}
}

public static int[] Main(int[] entrada) {

SortHeap(entrada, entrada.Longitud);
entrada de retorno ;
}
}

Sección 35.2: Información básica de clasificación de almacenamiento dinámico

Ordenar montones es una técnica de clasificación basada en comparación en la estructura de datos de montón binario. Es similar a la
ordenación por selección en la que primero encontramos el elemento máximo y lo colocamos al final de la estructura de datos. Luego repita el
mismo proceso para los elementos restantes.

Pseudocódigo para Heap Sort:

función heapsort (entrada, conteo)

GoalKicker.com – Notas de algoritmos para profesionales 164


Machine Translated by Google

heapify(a,count) end
<- count - 1 while
end -> 0 do
swap(a[end],a[0]) end<-
end-1 restore(a, 0, end)

función heapify(a, contar)


inicio <- padre(cuenta - 1) mientras
inicio >= 0 do restore(a, inicio,
cuenta - 1) inicio <- inicio - 1

Ejemplo de clasificación de montón:

Espacio Auxiliar: O(1)


Complejidad de tiempo: O (nlogn)

GoalKicker.com – Notas de algoritmos para profesionales 165


Machine Translated by Google

Capítulo 36: Clasificación cíclica

Sección 36.1: Implementación de pseudocódigo


(entrada)
salida = 0
para inicio de ciclo de 0 a longitud (matriz) - 2 elemento =
matriz [inicio de ciclo] pos = inicio de ciclo para i de
inicio de ciclo + 1 a longitud (matriz) - 1 si matriz [i]
< elemento: pos += 1 si pos == CycleStart: continuar

while elemento == matriz[pos]:


pos += 1 matriz[pos],
elemento = elemento, matriz[pos] escribe += 1
while pos != cicloInicio: pos = cicloInicio for i
from cicloInicio + 1 a longitud(matriz) - 1 si
matriz[i] < elemento: pos += 1 while
elemento == matriz[pos]: pos += 1 matriz[pos], elemento =
elemento, matriz[pos] escribe += 1

regreso salida

GoalKicker.com – Notas de algoritmos para profesionales 166


Machine Translated by Google

Capítulo 37: Clasificación par-impar


Sección 37.1: Información básica de clasificación par-impar

Una ordenación par-impar or brick sort es un algoritmo de clasificación simple, desarrollado para su uso en procesadores paralelos con
interconexión local. Funciona comparando todos los pares indexados impares/pares de elementos adyacentes en la lista y, si un par está en el
orden incorrecto, los elementos se intercambian. El siguiente paso repite esto para pares indexados pares/impares. Luego alterna entre pasos
impares/pares e pares/impares hasta que se ordena la lista.

Pseudocódigo para ordenación par-impar:

si n>2 entonces
1. aplicar la combinación impar-par (n/2) recursivamente a la subsecuencia par a0, a2, ..., la subsecuencia impar a1, a3, , ..., an-2 y al
un-1
2. comparación [i : i+1] para todo el elemento i {1, 3, 5, 7, ..., n-3} otra comparación [0 : 1]

Wikipedia tiene la mejor ilustración del tipo Impar-Par:

Ejemplo de clasificación par-impar:

GoalKicker.com – Notas de algoritmos para profesionales 167


Machine Translated by Google

Implementación:

Utilicé el lenguaje C# para implementar el algoritmo de ordenación par-impar.

clase pública OrdenaciónParImpares


{
vacío estático privado SortOddEven (entrada int [ ] , int n ) {

var ordenar = falso ;

while ( !ordenar )
{
ordenar =
verdadero ; para (var i = 1 ; i < n - 1 ; i += 2 )
{
si (entrada [ i ] <= entrada [i + 1 ]) continuar ; var
temp = entrada [ i ]; entrada [ i ] = entrada [i + 1 ];
entrada [i + 1 ] = temperatura ; ordenar = falso ;

} para (var i = 0 ; i < n - 1 ; i += 2 ) {

si (entrada [ i ] <= entrada [i + 1 ]) continuar ; var


temp = entrada [ i ]; entrada [ i ] = entrada [i + 1 ];
entrada [i + 1 ] = temperatura ; ordenar = falso ;

GoalKicker.com – Notas de algoritmos para profesionales 168


Machine Translated by Google

}
}

public static int[] Main(int[] entrada) {

SortOddEven(entrada, entrada.Longitud);
entrada de retorno ;
}
}

Espacio Auxiliar: O(n)


Complejidad de tiempo: O(n)

GoalKicker.com – Notas de algoritmos para profesionales 169


Machine Translated by Google

Capítulo 38: Clasificación de selección

Sección 38.1: Implementación de Elixir


selección de módulo def hacer

def sort(lista) when is_list(lista) do do_selection(lista, [])


end

def do_selection([head|[]], acc) do acc ++ [head] end

def hacer_selección(lista, acc) hacer


min = min(lista)
do_selection(:lists.delete(min, list), acc ++ [min]) end

defp min([primero|[segundo|[]]]) do más


pequeño(primero, segundo) fin

defp min([primero|[segundo|cola]]) hacer


min([menor(primero, segundo)|cola]) end

defp menor(e1, e2) hacer si e1 <=


e2 hacer
e1
más
e2
final
final
final

Selección.sort([100,4,10,6,9,3])
|> IO.inspeccionar

Sección 38.2: Información básica de clasificación de selección


Clasificación de selección es un algoritmo de clasificación, específicamente una clasificación de comparación en el lugar. Tiene una complejidad
de tiempo O(n2), lo que lo hace ineficiente en listas grandes y, en general, funciona peor que la ordenación por inserción similar. La ordenación
por selección se destaca por su simplicidad y tiene ventajas de rendimiento sobre algoritmos más complicados en ciertas situaciones,
particularmente donde la memoria auxiliar es limitada.

El algoritmo divide la lista de entrada en dos partes: la sublista de elementos ya ordenados, que se construye de izquierda a derecha al frente
(izquierda) de la lista, y la sublista de elementos que quedan por ordenar que ocupan el resto de la lista. lista.
Inicialmente, la sublista ordenada está vacía y la sublista no ordenada es la lista de entrada completa. El algoritmo procede encontrando
el elemento más pequeño (o el más grande, según el orden de clasificación) en la sublista sin clasificar, intercambiándolo con el elemento sin
clasificar más a la izquierda (colocándolo en orden) y moviendo los límites de la sublista un elemento a la derecha. .

Pseudocódigo para el tipo de selección:

selección de función (lista [1..n], k)


para i de 1 a k

GoalKicker.com – Notas de algoritmos para profesionales 170


Machine Translated by Google
minIndex = i
minValue = list[i] for j
from i+1 to n if list[j] <
minValue minIndex = j
minValue = list[j]
swap list[i] and
list[minIndex] return list[k]

Visualización del tipo de selección:

Ejemplo de ordenación por selección:

GoalKicker.com – Notas de algoritmos para profesionales 171


Machine Translated by Google

Espacio Auxiliar: O(n)

Complejidad del tiempo: O(n^2)

Sección 38.3: Implementación del ordenamiento por selección en C#


Utilicé el lenguaje C# para implementar el algoritmo de clasificación de selección.

clase pública SelectionSort {

privado estático vacío SortSelection(int[] entrada, int n) {

para (int i = 0; i < n - 1; i++) {

var minId = i; intj ;


para (j = i + 1; j <
n; j++) {

if (entrada[j] < entrada[minId]) minId = j;

} var temp = entrada[minId];

GoalKicker.com – Notas de algoritmos para profesionales 172


Machine Translated by Google

entrada[minId] = entrada[i];
entrada[i] = temperatura;
}
}

public static int[] Main(int[] entrada) {

SortSelection(entrada, entrada.Longitud);
entrada de retorno ;
}
}

GoalKicker.com – Notas de algoritmos para profesionales 173


Machine Translated by Google

Capítulo 39: Buscando


Sección 39.1: Búsqueda binaria
Introducción

Binary Search es un algoritmo de búsqueda Divide and Conquer. Utiliza el tiempo O(log n) para encontrar la ubicación de un elemento en un espacio
de búsqueda donde n es el tamaño del espacio de búsqueda.

La búsqueda binaria funciona reduciendo a la mitad el espacio de búsqueda en cada iteración después de comparar el valor objetivo con el valor
medio del espacio de búsqueda.

Para usar la búsqueda binaria, el espacio de búsqueda debe estar ordenado (ordenado) de alguna manera. Las entradas duplicadas (las
que se comparan como iguales según la función de comparación) no se pueden distinguir, aunque no infringen la propiedad de búsqueda binaria.

Convencionalmente, usamos menos que (<) como función de comparación. Si a < b, devolverá verdadero. si a no es menor que b y b no es menor que
a, a y b son iguales.

Pregunta de ejemplo

Eres economista, aunque bastante malo. Tienes la tarea de encontrar el precio de equilibrio (es decir, el precio donde la oferta = demanda) para el
arroz.

Recuerde que cuanto más alto se fija un precio, mayor es la oferta y menor la demanda.

Como su empresa es muy eficiente en el cálculo de las fuerzas del mercado, puede obtener instantáneamente la oferta y la demanda en unidades de
arroz cuando el precio del arroz se fija en un determinado precio p.

Su jefe quiere el precio de equilibrio lo antes posible, pero le dice que el precio de equilibrio puede ser un número entero positivo que sea como máximo
10^17 y que se garantiza que habrá exactamente 1 solución de número entero positivo en el rango. ¡Así que sigue adelante con tu trabajo antes de
que lo pierdas!

Puede llamar a las funciones getSupply(k) y getDemand(k), que harán exactamente lo que se indica en el problema.

Ejemplo Explicación

Aquí nuestro espacio de búsqueda es del 1 al 10^17. Por lo tanto, una búsqueda lineal es inviable.

Sin embargo, observe que a medida que aumenta k, aumenta getSupply ( k) y disminuye getDemand(k). Por lo tanto, para cualquier x > y,
getSupply(x) - getDemand(x) > getSupply(y) - getDemand(y). Por lo tanto, este espacio de búsqueda es monótono y podemos usar la
búsqueda binaria.

El siguiente pseudocódigo demuestra el uso de la búsqueda binaria:

alto = 100000000000000000 bajo = <- Límite superior del espacio de búsqueda


1 <- Límite inferior del espacio de búsqueda
while alta - baja > 1 media
= (alta + baja) / 2 oferta = <- Tomar el valor medio
obtener oferta (media) demanda
= obtener demanda (media) si
oferta > demanda alta = media
<- La solución está en la mitad inferior del espacio de búsqueda

GoalKicker.com – Notas de algoritmos para profesionales 174


Machine Translated by Google

otra cosa si demanda > oferta


baja = mitad otra parte <- La solución está en la mitad superior del espacio de
retorno mitad búsqueda <- oferta== condición de demanda
<- Solución encontrada

Este algoritmo se ejecuta en tiempo ~O(log 10^17) . Esto se puede generalizar al tiempo ~O(log S) donde S es el tamaño del
espacio de búsqueda ya que en cada iteración del ciclo while , reducimos a la mitad el espacio de búsqueda (de [bajo:alto] a
[bajo:medio] o [medio alto]).

C Implementación de búsqueda binaria con recursividad

int binsearch(int a[], int x, int bajo, int alto) {


int medio;

si (bajo > alto)


devuelve -1;

medio = (bajo + alto) / 2;

if (x == a[mid]) { return
(mid); } else if (x
< a[mid]) { binsearch(a,
x, low, mid - 1); } else
{ binsearch(a, x, mid + 1, high);

}
}

Sección 39.2: Rabin Karp


El algoritmo de Rabin-Karp o algoritmo de Karp-Rabin es un algoritmo de búsqueda de cadenas que utiliza hash para encontrar cualquiera de un
conjunto de cadenas de patrones en un texto. Su tiempo de ejecución promedio y en el mejor de los casos es O(n+m) en el espacio O( p), pero
su tiempo en el peor de los casos es O(nm) donde n es la longitud del texto y m es la longitud del patrón.

Implementación de algoritmo en java para coincidencia de cadenas

void RabinfindPattern ( texto de cadena, patrón de cadena) {


/*
qa número primo p
valor hash para patrón t valor
hash para texto
d es el número de caracteres únicos en el alfabeto de entrada */ int
d=128; intq =100; int n=texto.longitud(); int m=patrón.longitud(); entero
t=0,p=0; int h=1; int i,j; // función de cálculo del valor hash para
(i=0;i<m-1;i++) h = (h*d)%q; for (i=0;i<m;i++){ p = (d*p + patrón.charAt(i))
%q; t = (d*t + texto.charAt(i))%q; }

// busca el patrón

GoalKicker.com – Notas de algoritmos para profesionales 175


Machine Translated by Google

for(i=0;i<end-m;i++){ if(p==t)
{ //si el valor hash
coincide con el carácter por carácter for(j=0;j<m;j++) if(text .charAt(j+i)!
=patrón.charAt(j)) ruptura; si(j==m && i>=inicio)

System.out.println("Coincidencia de patrón encontrada en el índice "+i);

} if(i<fin-m){ t
=(d*(t - texto.charAt(i)*h) + texto.charAt(i+m))%q; si(t<0) t=t+q;

}
}
}

Mientras calculamos el valor hash, lo dividimos por un número primo para evitar la colisión. Después de dividir por un número primo, las posibilidades de colisión
serán menores, pero aún existe la posibilidad de que el valor hash sea el mismo para dos cadenas, entonces cuando obtenemos una coincidencia, tenemos que

verificarla carácter por carácter para asegurarnos de que tenemos una coincidencia adecuada.

t =(d*(t - texto.charAt(i)*h) + texto.charAt(i+m))%q;

Esto es para volver a calcular el valor hash para el patrón, primero eliminando el carácter más a la izquierda y luego agregando el nuevo carácter del texto.

Sección 39.3: Análisis de búsqueda lineal (peor, promedio


y mejores casos)
Podemos tener tres casos para analizar un algoritmo:

1. El peor de los casos

2. Caso promedio

3. Mejor Caso

#incluir <stdio.h>

// Busca linealmente x en arr[]. Si x está presente, devuelve el índice,

// de lo contrario devuelve
-1 int search(int arr[], int n, int x) {

ent yo;
para (i=0; i<n; i++) {

if (arr[i] == x)
devuelve i;
}

devolver -1;
}

/* Programa controlador para probar las funciones anteriores*/

int principal()

GoalKicker.com – Notas de algoritmos para profesionales 176


Machine Translated by Google

{
int arr[] = {1, 10, 30, 15}; entero x
= 30; int n = tamaño de (arr)/
tamaño de (arr [0]); printf("%d está presente
en el índice %d", x, search(arr, n, x));

getchar();
devolver 0;
}

Análisis del peor de los casos (generalmente hecho)

En el análisis del peor de los casos, calculamos el límite superior del tiempo de ejecución de un algoritmo. Debemos conocer el caso que hace que se ejecute el máximo

número de operaciones. Para la búsqueda lineal, el peor de los casos ocurre cuando el elemento que se busca (x en el código anterior) no está presente en la matriz.

Cuando x no está presente, la función search() lo compara con todos los elementos de arr[] uno por uno. Por lo tanto, la complejidad temporal de la búsqueda lineal en el

peor de los casos sería ÿ(n)

Análisis de casos promedio (a veces se hace)

En el análisis de casos promedio, tomamos todas las entradas posibles y calculamos el tiempo de cálculo para todas las entradas. Sume todos los valores calculados

y divida la suma por el número total de entradas. Debemos conocer (o predecir) la distribución de los casos. Para el problema de búsqueda lineal, supongamos que todos

los casos están distribuidos uniformemente (incluido el caso de que x no esté presente en el arreglo). Entonces sumamos todos los casos y dividimos la suma por (n+1). A

continuación se muestra el valor de la complejidad del tiempo de caso promedio.

Análisis del mejor caso (falso)

En el análisis del mejor de los casos, calculamos el límite inferior del tiempo de ejecución de un algoritmo. Debemos conocer el caso que hace que se ejecute un

número mínimo de operaciones. En el problema de búsqueda lineal, el mejor caso ocurre cuando x está presente en la primera ubicación. El número de operaciones en

el mejor de los casos es constante (no depende de n). Entonces, la complejidad del tiempo en el mejor de los casos sería ÿ(1) La mayoría de las veces, hacemos el

análisis del peor de los casos para analizar algoritmos. En el peor análisis, garantizamos un límite superior en el tiempo de ejecución de un algoritmo que es buena

información. El análisis de casos promedio no es fácil de hacer en la mayoría de los casos prácticos y rara vez se hace. En el análisis de casos promedio, debemos

conocer (o predecir) la distribución matemática de todas las entradas posibles. El análisis del mejor caso es falso. Garantizar un límite inferior en un algoritmo no

proporciona ninguna información ya que, en el peor de los casos, un algoritmo puede tardar años en ejecutarse.

Para algunos algoritmos, todos los casos son asintóticamente iguales, es decir, no hay mejores ni peores casos. Por ejemplo, Ordenar por combinación. Merge Sort

realiza operaciones ÿ(nLogn) en todos los casos. La mayoría de los otros algoritmos de clasificación tienen peores y mejores casos. Por ejemplo, en la implementación

típica de Quick Sort (donde el pivote se elige como un elemento de esquina), lo peor ocurre cuando la matriz de entrada ya está ordenada y lo mejor ocurre cuando los

elementos pivote siempre dividen la matriz en dos mitades. Para la ordenación por inserción, el peor de los casos ocurre cuando la matriz se ordena de forma inversa y

el mejor de los casos

GoalKicker.com – Notas de algoritmos para profesionales 177


Machine Translated by Google

ocurre cuando la matriz se ordena en el mismo orden que la salida.

Sección 39.4: Búsqueda binaria: en números ordenados


Es más fácil mostrar una búsqueda binaria en números usando pseudocódigo

int array[1000] = { lista ordenada de números }; int N = 100; //


número de entradas en el espacio de búsqueda; int alto, bajo, medio; //
nuestros temporales int x; // valor a buscar

bajo = 0;
alto = N -1;
mientras (bajo < alto) {

medio = (bajo + alto)/2;


if(arreglo[medio] < x) bajo =
medio + 1; más

alto = medio;

} if(array[low] == x) //
encontrado, el índice es bajo
else // no encontrado

No intente volver antes comparando array[mid] con x para la igualdad. La comparación adicional solo puede ralentizar el código.
Tenga en cuenta que debe agregar uno a low para evitar quedar atrapado por la división de enteros que siempre redondea hacia abajo.

Curiosamente, la versión anterior de la búsqueda binaria le permite encontrar la aparición más pequeña de x en la matriz. Si la matriz
contiene duplicados de x, el algoritmo se puede modificar ligeramente para que devuelva la mayor ocurrencia de x simplemente
agregando al condicional if:

mientras (bajo < alto) {

medio = bajo + ((alto - bajo) / 2);


if(matriz[medio] < x || (matriz[medio] == x && matriz[medio + 1] == x))
bajo = medio + 1;
más
alto = medio;
}

Tenga en cuenta que en lugar de hacer mid = (low + high) / 2, también puede ser una buena idea probar mid = low + ((high - low) / 2) para
implementaciones como las implementaciones de Java para reducir el riesgo de obtener un desbordamiento para entradas realmente
grandes.

Sección 39.5: Búsqueda lineal


La búsqueda lineal es un algoritmo simple. Recorre los elementos hasta que se encuentra la consulta, lo que lo convierte en un
algoritmo lineal: la complejidad es O (n), donde n es la cantidad de elementos por recorrer.

¿Por qué O(n)? En el peor de los casos, debe pasar por todos los n elementos.

Se puede comparar con buscar un libro en una pila de libros: los revisa todos hasta que encuentra el que desea.

A continuación se muestra una implementación de Python:

GoalKicker.com – Notas de algoritmos para profesionales 178


Machine Translated by Google

def búsqueda_lineal(lista_de_búsqueda, consulta):


for x in lista_de_búsqueda:
si consulta == x:
devuelve verdadero
volver falso

búsqueda_lineal(['manzana', 'plátano', 'zanahoria', 'higo', 'ajo'], 'higo') #devuelve Verdadero

GoalKicker.com – Notas de algoritmos para profesionales 179


Machine Translated by Google

Capítulo 40: Búsqueda de subcadenas


Sección 40.1: Introducción a Knuth-Morris-Pratt (KMP)
Algoritmo
Supongamos que tenemos un texto y un patrón. Necesitamos determinar si el patrón existe en el texto o no. Por ejemplo:

+-------+---+---+---+---+---+---+---+---+
| Índice | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+-------+---+---+---+---+---+---+---+---+
| Texto | un | segundo | do | segundo | do | gramo | yo | x |
+-------+---+---+---+---+---+---+---+---+

+--------------+---+---+---+---+
| Índice | 0 | 1 | 2 | 3 |
+--------------+---+---+---+---+
| Patrón | segundo | do | gramo | yo |
+--------------+---+---+---+---+

Este patrón existe en el texto. Entonces, nuestra búsqueda de subcadenas debería devolver 3, el índice de la posición desde la que comienza este
patrón . Entonces, ¿cómo funciona nuestro procedimiento de búsqueda de subcadenas de fuerza bruta?

Lo que solemos hacer es: comenzamos desde el índice 0 del texto y el índice 0 de nuestro *patrón y comparamos Text[0] con Pattern[0]. Como
no coinciden, vamos al siguiente índice de nuestro texto y comparamos Text[1] con Pattern[0]. Dado que esto es una coincidencia, incrementamos
el índice de nuestro patrón y también el índice del Texto . Comparamos Text[2] con Pattern[1]. También son un partido. Siguiendo el mismo
procedimiento indicado anteriormente, ahora comparamos Text[3] con Pattern[2]. Como no coinciden, partimos de la siguiente posición en la que
empezamos a encontrar la coincidencia. Ese es el índice 2 del Texto. Comparamos Text[2] con Pattern[0]. No coinciden. Luego, incrementando el
índice del Texto, comparamos el Texto[3] con el Patrón[0]. Coinciden. Nuevamente coinciden Texto[4] y Patrón[1] , Coinciden Texto[5] y
Patrón[2] y Coinciden Texto[6] y Patrón[3] . Dado que hemos llegado al final de nuestro patrón, ahora devolvemos el índice desde el que
comenzó nuestra coincidencia, es decir, 3. Si nuestro patrón era: bcgll, eso significa que si el patrón no existía en nuestro texto, nuestra búsqueda

debería devolver excepción o -1 o cualquier otro valor predefinido. Podemos ver claramente que, en el peor de los casos, este algoritmo tomaría O
(mn) tiempo donde m es la longitud del Texto y n es la longitud del Patrón. ¿Cómo reducimos esta complejidad temporal? Aquí es donde entra en
escena el algoritmo de búsqueda de subcadenas KMP.

El algoritmo de búsqueda de cadenas de Knuth-Morris-Pratt o el algoritmo KMP busca apariciones de un "patrón" dentro de un "texto" principal
empleando la observación de que cuando se produce una falta de coincidencia, la palabra en sí incorpora información suficiente para determinar
dónde podría comenzar la próxima coincidencia, evitando así la reexaminación de coincidencias anteriores caracteres. El algoritmo fue concebido en
1970 por Donuld Knuth y Vaughan Pratt e independientemente por James H. Morris. El trío lo publicó conjuntamente en 1977.

Ampliemos nuestro ejemplo de Texto y Patrón para una mejor comprensión:

+-------+--+--+--+--+--+--+--+--+--+--+--+--+--+-- +--+--+--+--+--+--+--+--+--+
| Índice |0 |1 |2 |3 |4 |5 |6 |7 |8 |9 |10|11|12|13|14|15|16|17|18|19|20|21|22|
+-------+--+--+--+--+--+--+--+--+--+--+--+--+--+-- +--+--+--+--+--+--+--+--+--+
| Texto |a |b |c |x |a |b |c |d |a |b |x |a |b |c |d |a |b |c |d |a |b |c |y |
+-------+--+--+--+--+--+--+--+--+--+--+--+--+--+-- +--+--+--+--+--+--+--+--+--+

+--------------+---+---+---+---+---+---+---+---+
| Índice | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

GoalKicker.com – Notas de algoritmos para profesionales 180


Machine Translated by Google
+--------------+---+---+---+---+---+---+---+---+
| patrón | un | segundo | do | re | un | segundo | do | y |
+--------------+---+---+---+---+---+---+---+---+

Al principio, nuestro Texto y Patrón coinciden hasta el índice 2. Texto[3] y Patrón[3] no coinciden. Por lo tanto, nuestro objetivo es no
retroceder en este Texto, es decir, en caso de que no coincidan, no queremos que nuestro emparejamiento comience nuevamente desde la
posición en la que comenzamos a emparejar. Para lograrlo, buscaremos un sufijo en nuestro Patrón justo antes de que ocurra nuestra falta de
coincidencia (subcadena abc), que también es un prefijo de la subcadena de nuestro Patrón. Para nuestro ejemplo, dado que todos los
caracteres son únicos, no hay sufijo, ese es el prefijo de nuestra subcadena coincidente. Entonces, lo que eso significa es que nuestra próxima
comparación comenzará desde el índice 0. Espera un poco, entenderás por qué hicimos esto. A continuación, comparamos Text[3] con
Pattern[0] y no coincide. Después de eso, para Texto del índice 4 al índice 9 y para Patrón del índice 0 al índice 5, encontramos una coincidencia.
Encontramos una falta de coincidencia en Text[10] y Pattern[6]. Así que tomamos la subcadena de Pattern justo antes del punto donde ocurre la
discrepancia (subcadena abcdabc), buscamos un sufijo, que también es un prefijo de esta subcadena. Podemos ver aquí que ab es tanto el sufijo
como el prefijo de esta subcadena. Lo que eso significa es que, dado que hemos emparejado hasta Text[10], los caracteres justo antes de la falta
de coincidencia son ab. Lo que podemos inferir de esto es que, dado que ab también es un prefijo de la subcadena que tomamos, no tenemos
que volver a verificar ab y la siguiente verificación puede comenzar desde Text[10] y Pattern[2]. No tuvimos que mirar hacia atrás a todo el Texto,
podemos comenzar directamente desde donde ocurrió nuestro desajuste. Ahora verificamos Text[10] y Pattern[2], ya que no coinciden, y la
subcadena antes de la discrepancia (abc) no contiene un sufijo que también es un prefijo, verificamos Text[10] y Pattern[0], no coinciden.
Después de eso, para Text del índice 11 al índice 17 y para Pattern del índice 0 al índice 6. Encontramos una discrepancia en Text[18] y
Pattern[7]. Entonces, nuevamente verificamos la subcadena antes de la discrepancia (subcadena abcdabc) y encontramos que abc es tanto el
sufijo como el prefijo. Entonces, dado que hicimos coincidir hasta Pattern[7], abc debe estar antes de Text[18]. Eso significa que no necesitamos
comparar hasta Text[17] y nuestra comparación comenzará desde Text[18] y Pattern[3]. Por lo tanto, encontraremos una coincidencia y
devolveremos 15 , que es nuestro índice inicial de la coincidencia. Así es como funciona nuestra búsqueda de subcadenas KMP utilizando
información de sufijos y prefijos.

Ahora, ¿cómo calculamos de manera eficiente si el sufijo es el mismo que el prefijo y en qué punto comenzar a verificar si hay una
discrepancia de caracteres entre el texto y el patrón? Echemos un vistazo a un ejemplo:

+--------------+---+---+---+---+---+---+---+---+
| Índice | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+--------------+---+---+---+---+---+---+---+---+
| patrón | un | segundo | do | re | un | segundo | do | un |
+--------------+---+---+---+---+---+---+---+---+

Generaremos una matriz que contenga la información requerida. Llamemos a la matriz S. El tamaño de la matriz será igual a la longitud del
patrón. Como la primera letra del Patrón no puede ser el sufijo de ningún prefijo, pondremos S[0] = 0. Tomamos i = 1 y j = 0 al principio. En cada
paso comparamos Pattern[i] y Pattern[j] e incrementamos i. Si hay una coincidencia, ponemos S[i] = j + 1 e incrementamos j, si hay una
discrepancia, verificamos la posición del valor anterior de j (si está disponible) y establecemos j = S[j-1] (si j no es igual a 0), seguimos haciendo
esto hasta que S[j] no coincida con S[i] o j no se convierta en 0. Para el último, ponemos S[i] = 0. Para nuestro ejemplo :

j i
+--------------+---+---+---+---+---+---+---+---+
| Índice | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+--------------+---+---+---+---+---+---+---+---+
| Patrón | un | segundo | do | re | un | segundo | do | un |
+--------------+---+---+---+---+---+---+---+---+

Patrón[j] y Patrón[i] no coinciden, entonces incrementamos i y como j es 0, no verificamos el valor anterior y ponemos Patrón[i] = 0. Si seguimos
incrementando i, para i = 4, obtendremos una coincidencia, entonces ponemos S[i] = S[4] = j + 1 = 0 + 1 = 1 y

GoalKicker.com – Notas de algoritmos para profesionales 181


Machine Translated by Google

incrementar j e i. Nuestra matriz se verá así:

j i
+--------------+---+---+---+---+---+---+---+---+
| Índice | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+--------------+---+---+---+---+---+---+---+---+
| Patrón | un | segundo | do | re | un | segundo | do | un |
+--------------+---+---+---+---+---+---+---+---+
| S |0|0|0|0|1| | | |
+--------------+---+---+---+---+---+---+---+---+

Dado que Pattern[1] y Pattern[5] coinciden, ponemos S[i] = S[5] = j + 1 = 1 + 1 = 2. Si continuamos, encontraremos una
discrepancia para j = 3 e i = 7. Como j no es igual a 0, ponemos j = S[j-1]. Y compararemos los caracteres en i y j si son iguales o no, ya
que son iguales, pondremos S[i] = j + 1. Nuestra matriz completa se verá así:

+--------------+---+---+---+---+---+---+---+---+
| S |0|0|0|0|1|2|3|1|
+--------------+---+---+---+---+---+---+---+---+

Esta es nuestra matriz requerida. Aquí, un valor distinto de cero de S[i] significa que hay un sufijo de longitud S[i] igual que el prefijo en esa
subcadena (subcadena de 0 a i) y la siguiente comparación comenzará desde la posición S[i] + 1 de la Patrón. Nuestro algoritmo para
generar la matriz se vería así:

Procedimiento GenerateSuffixArray(Pattern): i := 1 j := 0
n := Pattern.length while i es menor que n si Pattern[i] es
igual a Pattern[j]

S[i] := j + 1 j := j +
1 i := i + 1 más

si j no es igual a 0 j := S[j-1] sino

S[i] := 0 i :=
i + 1 final si
final si final
mientras

La complejidad del tiempo para construir esta matriz es O(n) y la complejidad del espacio también es O(n). Para asegurarse de que ha
entendido completamente el algoritmo, intente generar una matriz para el patrón aabaabaa y verifique si el resultado coincide con este una.

Ahora hagamos una búsqueda de subcadena usando el siguiente ejemplo:

+---------+---+---+---+---+---+---+---+---+---+--- +---+---+
| Índice | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |10 |11 |
+---------+---+---+---+---+---+---+---+---+---+--- +---+---+
| Texto | un | segundo | x | un | segundo | do | un | segundo | do | un | segundo | y |
+---------+---+---+---+---+---+---+---+---+---+--- +---+---+

+---------+---+---+---+---+---+---+
| Índice | 0 | 1 | 2 | 3 | 4 | 5 |

GoalKicker.com – Notas de algoritmos para profesionales 182


Machine Translated by Google
+---------+---+---+---+---+---+---+
| Patrón | un | segundo | do | un | segundo | y |
+---------+---+---+---+---+---+---+
| S |0|0|0|1|2|0|
+---------+---+---+---+---+---+---+

Tenemos un Texto, un Patrón y una matriz S precalculada utilizando nuestra lógica definida anteriormente. Comparamos Text[0] y
Pattern[0] y son iguales. Texto[1] y Patrón[1] son iguales. Text[2] y Pattern[2] no son lo mismo. Verificamos el valor en la posición justo
antes del desajuste. Dado que S[1] es 0, no hay sufijo que sea igual al prefijo en nuestra subcadena y nuestra comparación comienza en la
posición S[1], que es 0. Entonces Patrón[0] no es lo mismo que Texto[2], así que seguimos adelante. Text[3] es lo mismo que Pattern[0] y
existe una coincidencia entre Text[8] y Pattern[5]. Retrocedemos un paso en la matriz S y encontramos 2. Esto significa que hay un prefijo de
longitud 2 que también es el sufijo de esta subcadena (abcab) que es ab. Eso también significa que hay un ab antes de Text[8]. Entonces
podemos ignorar con seguridad Pattern[0] y Pattern[1] y comenzar nuestra siguiente comparación desde Pattern[2] y Text[8]. Si continuamos,
encontraremos el Patrón en el Texto. Nuestro procedimiento se verá así:

Procedimiento KMP (Texto, Patrón)


GenerateSuffixArray(Patrón) m :=
Texto.Longitud n := Patrón.Longitud
i := 0

j := 0
mientras i es menor que m
si Patrón[j] es igual a Texto[i] j := j + 1 i := i
+1

si j es igual a n
Devuelve (ji) si
i < m y Patrón[j] no es igual a t Texto[i] si j no es igual a 0 j = S[j-1]
si no

i := i + 1 fin
si fin si

terminar mientras
Retorno -1

La complejidad temporal de este algoritmo aparte del cálculo de matriz de sufijos es O(m). Dado que GenerateSuffixArray toma O(n), la
complejidad temporal total del algoritmo KMP es: O(m+n).

PD: Si desea encontrar múltiples ocurrencias de Patrón en el Texto, en lugar de devolver el valor, imprímalo/guárdelo y establezca j := S[j-1].
También mantenga una marca para rastrear si ha encontrado alguna ocurrencia o no y manéjela en consecuencia.

Sección 40.2: Introducción al Algoritmo de Rabin-Karp


Algoritmo de Rabin-Karp es un algoritmo de búsqueda de cadenas creado por Richard M. Karp y Michael O. Rabin que usa hashing para
encontrar cualquiera de un conjunto de cadenas de patrones en un texto.

Una subcadena de una cadena es otra cadena que ocurre en. Por ejemplo, ver es una subcadena de stackoverflow. No debe confundirse
con la subsecuencia porque la cubierta es una subsecuencia de la misma cadena. En otras palabras, cualquier subconjunto de letras
consecutivas en una cadena es una subcadena de la cadena dada.

En el algoritmo de Rabin-Karp, generaremos un hash de nuestro patrón que estamos buscando y verificaremos si el hash rodante de
nuestro texto coincide con el patrón o no. Si no coincide, podemos garantizar que el patrón no existe en el texto.

GoalKicker.com – Notas de algoritmos para profesionales 183


Machine Translated by Google

Sin embargo, si coincide, el patrón puede estar presente en el texto. Veamos un ejemplo:

Digamos que tenemos un texto: yeminsajid y queremos saber si el patrón nsa existe en el texto. Para calcular el
hash y hash rodante, necesitaremos usar un número primo. Este puede ser cualquier número primo. Tomemos primo = 11 para
este ejemplo Determinaremos el valor hash usando esta fórmula:

(1.ª letra) X (prima) + (2.ª letra) X (prima)¹ + (3.ª letra) X (prima)² X + ......

Denotaremos:

a -> 1 g -> 7 h m -> 13 s -> 19 y -> 25


b -> 2 -> 8 n -> 14 t -> 20 z -> 26
do -> 3 yo -> 9 o -> 15 u -> 21
re -> 4 j -> 10 p -> 16 v -> 22
e -> 5 f k -> 11 q -> 17 w -> 23
-> 6 l -> 12 r -> 18 x -> 24

El valor hash de nsa será:

14 X 11ÿ + 19 X 11¹ + 1 X 11² = 344

Ahora encontramos el hash rodante de nuestro texto. Si el hash rodante coincide con el valor hash de nuestro patrón, comprobaremos si
las cadenas coinciden o no. Dado que nuestro patrón tiene 3 letras, tomaremos las primeras 3 letras yem de nuestro texto y calcularemos
valor hash. Obtenemos:

25 X 11ÿ + 5 X 11¹ + 13 X 11² = 1653

Este valor no coincide con el valor hash de nuestro patrón. Así que la cadena no existe aquí. Ahora tenemos que considerar
el siguiente paso. Para calcular el valor hash de nuestra próxima cadena emi. Podemos calcular esto usando nuestra fórmula. Pero eso
sería bastante trivial y nos costaría más. En su lugar, utilizamos otra técnica.

Restamos el valor de la primera letra de la cadena anterior de nuestro valor hash actual. En este caso, Y. Nosotros
obtener, 1653 - 25 = 1628.
Dividimos la diferencia con nuestro primo, que es 11 para este ejemplo. Obtenemos, 1628 / 11 = 148.
Agregamos la nueva letra X (prima)ÿ¹, donde m es la longitud del patrón, con el cociente, que es i = 9.
obtener, 148 + 9 X 11² = 1237.

El nuevo valor hash no es igual al valor hash de nuestros patrones. Continuando, para n obtenemos:

Cadena anterior : emi


Primera letra de la cadena anterior: e(5)
Nueva letra: n(14)
Nueva cadena: "min"
1237 - 5 = 1232
1232 / 11 = 112
112 + 14 X 11² = 1806

No coincide. Después de eso, para s, obtenemos:

Cadena anterior : min


Primera letra de la cadena anterior: m(13)
Nueva letra: s(19)
Nueva cadena: "ins"
1806 - 13 = 1793
1793 / 11 = 163

GoalKicker.com – Notas de algoritmos para profesionales 184


Machine Translated by Google
163 + 19 X 11² = 2462

No coincide. Luego, para a, obtenemos:

Cadena anterior : ins


Primera letra de la cadena anterior: i(9)
Nueva letra: a(1)
Nueva cadena: "nsa"
2462 - 9 = 2453 2453 /
11 = 223
223 + 1 X 11² = 344

¡Es un partido! Ahora comparamos nuestro patrón con la cadena actual. Dado que ambas cadenas coinciden, la subcadena existe en
esta cadena. Y devolvemos la posición inicial de nuestra subcadena.

El pseudocódigo será:

Cálculo de hash:

Procedimiento Calculate-Hash(String, Prime, x): hash :=


0 // Aquí x denota la longitud a considerar para m de 1 a x //
para encontrar el valor hash
hash := hash + (Valor de la cadena[m])ÿ¹ fin de

Hachís de retorno

Recálculo de hash:

Procedimiento Recalculate-Hash(String, Curr, Prime, Hash): Hash :=


Hash - Valor de String[Curr] //aquí Curr denota la primera letra de la cadena anterior Hash := Hash / Prime m :=
String.length New := Corriente + m - 1

Hash := Hash + (Valor de la cadena [Nuevo])ÿ¹


Hachís de retorno

Coincidencia de cadenas:

Procedimiento String-Match(Text, Pattern, m): for i


from m to Pattern-length + m - 1
si Texto[i] no es igual a Patrón[i]
Devuelve fin falso
si finaliza para

Devolver verdadero

Rabin Karp:

Procedimiento Rabin-Karp(Texto, Patrón, Principal): m :=


Patrón.Longitud HashValue := Calcular-Hash(Patrón,
Principal, m)
CurrValue := Calculate-Hash(Pattern, Prime, m) for i from 1
to Text.length - m
si HashValue == CurrValue y String-Match(Text, Pattern, i) es verdadero
Regreso yo
terminar si

CurrValue := Recalculate-Hash(String, i+1, Prime, CurrValue) end for

GoalKicker.com – Notas de algoritmos para profesionales 185


Machine Translated by Google
Retorno -1

Si el algoritmo no encuentra ninguna coincidencia, simplemente devuelve -1.

Este algoritmo se utiliza para detectar plagio. Dado el material de origen, el algoritmo puede buscar rápidamente en un documento instancias de
oraciones del material de origen, ignorando detalles como el caso y la puntuación. Debido a la abundancia de las cadenas buscadas, los algoritmos de
búsqueda de una sola cadena no son prácticos aquí. Una vez más, el algoritmo Knuth Morris-Pratt o el algoritmo de búsqueda de cadenas de
Boyer-Moore es un algoritmo de búsqueda de cadenas de patrón único más rápido que Rabin-Karp. Sin embargo, es un algoritmo de elección para la
búsqueda de patrones múltiples. Si queremos encontrar cualquiera de los grandes números, digamos k, patrones de longitud fija en un texto, podemos
crear una variante simple del algoritmo de Rabin-Karp.

Para patrones de texto de longitud n y p de longitud combinada m, su tiempo de ejecución promedio y en el mejor de los casos es O(n+m) en el
espacio O(p), pero el tiempo en el peor de los casos es O(nm).

Sección 40.3: Implementación Python del algoritmo KMP


Haystack: la cadena en la que se debe buscar el patrón dado.
Aguja: El patrón a buscar.

Complejidad temporal: la parte de búsqueda (método strstr) tiene la complejidad O(n), donde n es la longitud del pajar, pero como la aguja también se
analiza para construir la tabla de prefijos, se requiere O(m) para construir la tabla de prefijos, donde m es la longitud de la aguja

Por lo tanto, la complejidad temporal general para KMP es O(n+m)


Complejidad espacial: O(m) debido a la tabla de prefijos en la aguja.

Nota: La siguiente implementación devuelve la posición de inicio de la coincidencia en el pajar (si hay una coincidencia); de lo contrario, devuelve -1,
para los casos extremos, como si la aguja/el pajar es una cadena vacía o la aguja no se encuentra en el pajar.

def get_prefix_table(aguja): prefix_set


= set() n = len(aguja) prefix_table
= [0]*n delimeter = 1
while(delimeter<n):
prefix_set.add(aguja[:delimeter])
j = 1 while(j <delímetro+1): si la
aguja[j:delímetro+1] en prefix_set:

prefix_table[delimitador] = delimitador - j + 1 descanso

j += 1
delimitador += 1
return prefix_table

def strstr(pajar, aguja):


# m: indica la posición dentro de S donde comienza la posible coincidencia para W # i: indica el índice
del carácter actualmente considerado en W. pajar_len = len(pajar) aguja_len = len(aguja) si (aguja_len
> pajar_len ) o (no pajar_len ) o (no aguja_len):

return -1
prefix_table = get_prefix_table(aguja) m = i = 0
while((i<aguja_largo) y (m<pajar_largo)): if pajar[m]
== aguja[i]: i += 1

GoalKicker.com – Notas de algoritmos para profesionales 186


Machine Translated by Google
m += 1
más:
si yo != 0:
i = tabla_prefijo[i-1] más:

m += 1
if i==aguja_len and pajar[m-1] == aguja[i-1]: return m - aguja_len else:

volver -1

si __nombre__ == '__principal__':
aguja = 'abcaby' pajar
= 'abxabcaby' print strstr(pajar,
aguja)

Sección 40.4: Algoritmo KMP en C


Dado un texto txt y un patrón pat, el objetivo de este programa será imprimir todas las ocurrencias de pat en txt.

Ejemplos:

Aporte:

txt[] = "ESTE ES UN TEXTO DE PRUEBA"


pat[] = "PRUEBA"

producción:

Patrón encontrado en el índice 10

Aporte:

txt[] = "AABAACAADAABAAABAA" pat[]


= "AABA"

producción:

Patrón encontrado en el índice 0


Patrón encontrado en el índice 9
Patrón encontrado en el índice 13

Implementación del lenguaje C:

// programa en C para la implementación de la búsqueda de patrones KMP //


algoritmo #include<stdio.h>

#incluir<cadena.h>
#incluir<stdlib.h>

void computarLPSArray(char *pat, int M, int *lps);

void KMPSearch(char *pat, char *txt) {

int M = strlen(pat); int N =


strlen(txt);

// crea lps[] que contendrá el sufijo de prefijo más largo

GoalKicker.com – Notas de algoritmos para profesionales 187


Machine Translated by Google

// valores para el patrón int


*lps = (int *)malloc(sizeof(int)*M); int j = 0; // índice
para pat[]

// Preprocesar el patrón (calcular la matriz lps[])


computeLPSArray(pat, M, lps);

int i = 0; // índice para txt[] while (i < N) {

if (pat[j] == txt[i]) {

j++;
yo++;
}

si (j == M) {

printf(" Patrón encontrado en el índice %d \n", ij); j = lps[j-1];

// discrepancia después de que j


coincida con otra cosa si (i < N && pat[j] !=
txt[i]) {
// No haga coincidir los caracteres lps[0..lps[j-1]], //
coincidirán de todos modos si (j != 0) j = lps[j-1]; más

yo = yo+1;
}

} libre(lps); // para evitar la pérdida de memoria


}

void computarLPSArray(char *pat, int M, int *lps) {

largo int = 0; // longitud del sufijo de prefijo más largo anterior int i;

lps[0] = 0; // lps[0] siempre es 0 i = 1;

// el ciclo calcula lps[i] para i = 1 a M-1 while (i < M) {

if (pat[i] == pat[len]) {

len++;
lps[i] = largo; yo+
+;

} else // (pat[i] != pat[len]) {

si (largo ! = 0) {

// Esto es complicado. Considere el ejemplo //


AAACAAAA e i = 7. len = lps[len-1];

// Además, tenga en cuenta que no incrementamos i aquí

GoalKicker.com – Notas de algoritmos para profesionales 188


Machine Translated by Google

} más // si (len == 0) {

lps[i] = 0; yo+
+;
}
}
}
}

// Programa controlador para probar la función


anterior int main() {

char *txt = "ABABDABACDABABCABAB";


char *pat = "ABABCABAB";
KMPSearch(pat, txt);
devolver 0;
}

Producción:

Patrón encontrado en el índice 10

Referencia:

http://www.geeksforgeeks.org/searching-for-patterns-set-2-kmp-algorithm/

GoalKicker.com – Notas de algoritmos para profesionales 189


Machine Translated by Google

Capítulo 41: Búsqueda en amplitud


Sección 41.1: Encontrar la ruta más corta desde el origen hasta
otros nodos
Búsqueda primero en amplitud (BFS) es un algoritmo para atravesar o buscar estructuras de datos de árboles o gráficos. Comienza en
la raíz del árbol (o algún nodo arbitrario de un gráfico, a veces denominado "clave de búsqueda") y explora primero los nodos vecinos,
antes de pasar a los vecinos del siguiente nivel. BFS fue inventado a fines de la década de 1950 por Edward Forrest Moore, quien lo usó
para encontrar el camino más corto fuera de un laberinto y descubierto de forma independiente por CY Lee como un algoritmo de
enrutamiento de cables en 1961.

Los procesos del algoritmo BFS funcionan bajo estos supuestos:

1. No atravesaremos ningún nodo más de una vez.


2. El nodo de origen o el nodo del que partimos está situado en el nivel 0.
3. Los nodos a los que podemos llegar directamente desde el nodo de origen son nodos de nivel 1, los nodos a los que podemos llegar directamente desde
los nodos de nivel 1 son nodos de nivel 2 y así sucesivamente.

4. El nivel indica la distancia del camino más corto desde la fuente.

Veamos un ejemplo:

Supongamos que este gráfico representa la conexión entre varias ciudades, donde cada nodo indica una ciudad y un borde entre dos
nodos indica que hay una carretera que los une. Queremos ir del nodo 1 al nodo 10. Entonces, el nodo 1 es nuestra fuente, que es el
nivel 0. Marcamos el nodo 1 como visitado. Podemos ir al nodo 2, al nodo 3 y al nodo 4 desde aquí. Entonces serán de nivel (0+1) =
nodos de nivel 1 . Ahora los marcaremos como visitados y trabajaremos con ellos.

GoalKicker.com – Notas de algoritmos para profesionales 190


Machine Translated by Google

Los nodos coloreados son visitados. Los nodos con los que estamos trabajando actualmente estarán marcados en rosa. No visitaremos el mismo
nodo dos veces. Del nodo 2, nodo 3 y nodo 4, podemos ir al nodo 6, nodo 7 y nodo 8. Vamos a marcarlos como visitados. El nivel de estos
nodos será nivel (1+1) = nivel 2.

GoalKicker.com – Notas de algoritmos para profesionales 191


Machine Translated by Google

Si no lo ha notado, el nivel de los nodos simplemente denota la distancia de ruta más corta desde la fuente. Por ejemplo:
hemos encontrado el nodo 8 en el nivel 2. Entonces, la distancia desde la fuente hasta el nodo 8 es 2.

Todavía no llegamos a nuestro nodo objetivo, que es el nodo 10. Así que visitemos los siguientes nodos. podemos ir directamente desde el nodo 6, el nodo
7 y el nodo 8.

GoalKicker.com – Notas de algoritmos para profesionales 192


Machine Translated by Google

Podemos ver que encontramos el nodo 10 en el nivel 3. Entonces, la ruta más corta desde la fuente hasta el nodo 10 es 3. Buscamos en el
graficar nivel por nivel y encontrar el camino más corto. Ahora vamos a borrar los bordes que no usamos:

GoalKicker.com – Notas de algoritmos para profesionales 193


Machine Translated by Google

Después de eliminar los bordes que no usamos, obtenemos un árbol llamado árbol BFS. Este árbol muestra la ruta más corta desde el
origen hasta todos los demás nodos.

Así que nuestra tarea será pasar de la fuente a los nodos de nivel 1 . Luego de los nodos de nivel 1 a nivel 2 y así sucesivamente hasta
llegar a nuestro destino. Podemos usar queue para almacenar los nodos que vamos a procesar. Es decir, para cada nodo con el que vamos a
trabajar, empujaremos todos los demás nodos que se pueden atravesar directamente y que aún no se han recorrido en la cola.

La simulación de nuestro ejemplo:

Primero empujamos la fuente en la cola. Nuestra cola se verá así:

frente
+-----+
|1|
+-----+

El nivel del nodo 1 será 0. level[1] = 0. Ahora comenzamos nuestro BFS. Al principio, sacamos un nodo de nuestra cola. Obtenemos el
nodo 1. Podemos ir al nodo 4, al nodo 3 y al nodo 2 desde este. Hemos llegado a estos nodos desde el nodo 1. Así que nivel[4] =
nivel[3] = nivel[2] = nivel[1] + 1 = 1. Ahora los marcamos como visitados y los colocamos en la cola.

frente
+-----+ +-----+ +-----+
|2| |3| |4|
+-----+ +-----+ +-----+

GoalKicker.com – Notas de algoritmos para profesionales 194


Machine Translated by Google

Ahora sacamos el nodo 4 y trabajamos con él. Podemos ir al nodo 7 desde el nodo 4. level[7] = level[4] + 1 = 2. Marcamos el nodo 7
como visitado y empújelo en la cola.

frente
+-----+ +-----+ +-----+
|7| |2| |3|
+-----+ +-----+ +-----+

Desde el nodo 3, podemos ir al nodo 7 y al nodo 8. Como ya marcamos el nodo 7 como visitado, marcamos el nodo 8 como
visitado, cambiamos nivel[8] = nivel[3] + 1 = 2. Empujamos el nodo 8 en la cola.

frente
+-----+ +-----+ +-----+
|6| |7| |2|
+-----+ +-----+ +-----+

Este proceso continuará hasta que lleguemos a nuestro destino o la cola se quede vacía. La matriz de nivel nos proporcionará
con la distancia del camino más corto desde la fuente. Podemos inicializar la matriz de nivel con valor infinito , que marcará
que los nodos aún no han sido visitados. Nuestro pseudocódigo será:

Procedimiento BFS (Gráfico, fuente):


Q = cola ();
nivel[] = infinito
nivel[fuente] := 0
Q.push(fuente)
mientras que Q no está vacío
u -> Q.pop()
para todos los bordes de u a v en la lista de adyacencia
si nivel[v] == infinito
nivel[v] := nivel[u] + 1
Q. empujar (v)
terminar si
fin para
terminar mientras
Nivel de retorno

Al iterar a través de la matriz de niveles , podemos averiguar la distancia de cada nodo desde la fuente. Por ejemplo: el
la distancia del nodo 10 desde la fuente se almacenará en el nivel [10].

En ocasiones, es posible que necesitemos imprimir no solo la distancia más corta, sino también el camino por el que podemos ir a nuestro
nodo de destino desde el origen. Para esto, necesitamos mantener una matriz principal . padre[fuente] será NULL. Para cada
actualización en la matriz de niveles , simplemente agregaremos parent[v] := u en nuestro pseudocódigo dentro del bucle for. Después de terminar BFS,
para encontrar la ruta, recorreremos la matriz principal hasta llegar a la fuente , que se indicará con un valor NULL.
El pseudocódigo será:

Procedimiento PrintPath(u): //recursivo si padre[u] no es | Procedimiento PrintPath(u): // iterativo


igual a nulo | S = Pila()
PrintPath(padre[u]) | mientras que padre[u] no es igual a nulo S.push(u)
finaliza si imprime -> u
| tu := padre[u ]
| | terminar mientras
| mientras que S no está vacío
| imprimir -> S.pop
| terminar mientras

GoalKicker.com – Notas de algoritmos para profesionales 195


Machine Translated by Google

Complejidad:

Hemos visitado cada nodo una vez y cada borde una vez. Entonces la complejidad será O(V + E) donde V es el número de nodos y E es el número de

aristas.

Sección 41.2: Encontrar la ruta más corta desde la fuente en un gráfico 2D


La mayoría de las veces, necesitaremos encontrar la ruta más corta desde una sola fuente hasta todos los demás nodos o un nodo específico en un gráfico
2D. Digamos, por ejemplo: queremos averiguar cuántos movimientos se requieren para que un caballo llegue a una determinada casilla en un tablero de
ajedrez, o tenemos una matriz donde algunas celdas están bloqueadas, tenemos que encontrar el camino más corto de una celda a otra. . Sólo podemos
movernos horizontal y verticalmente. Incluso los movimientos diagonales también pueden ser posibles.
Para estos casos, podemos convertir los cuadrados o celdas en nodos y resolver estos problemas fácilmente usando BFS. Ahora nuestro visitado, padre y

nivel serán arreglos 2D. Para cada nodo, consideraremos todos los movimientos posibles. Para encontrar la distancia a un nodo específico, también

comprobaremos si hemos llegado a nuestro destino.

Habrá una cosa adicional llamada matriz de dirección. Esto simplemente almacenará todas las combinaciones posibles de direcciones a las que
podemos ir. Digamos, para movimientos horizontales y verticales, nuestras matrices de dirección serán:

+----+-----+-----+-----+-----+
| dx | 1 | -1 | 0 | 0 |
+----+-----+-----+-----+-----+
| dy | 0 | 0 | 1 | -1 |
+----+-----+-----+-----+-----+

Aquí dx representa el movimiento en el eje x y dy representa el movimiento en el eje y. De nuevo, esta parte es opcional. También puedes escribir todas las
combinaciones posibles por separado. Pero es más fácil manejarlo usando una matriz de dirección. Puede haber más combinaciones e incluso diferentes
para movimientos diagonales o movimientos de caballo.

La parte adicional que debemos tener en cuenta es:

Si alguna de las celdas está bloqueada, para todos los movimientos posibles, comprobaremos si la celda está bloqueada o no.
También comprobaremos si nos hemos salido de los límites, es decir, si hemos cruzado los límites de la matriz.
Se dará el número de filas y columnas.

Nuestro pseudocódigo será:

Procedimiento BFS2D (Gráfico, signo de bloque, fila,


columna): para i de 1 a fila para j de 1 a columna

visitado[i][j] := fin falso para

final
para visitado[fuente.x][fuente.y] :=
verdadero nivel[fuente.x][fuente.y] := 0 Q
= cola()
Q.push(fuente)
m := dx.size
mientras Q no está
vacío top := Q.pop
para i de 1 a m
temp.x := top.x + dx[i]
temp.y := top.y + dy[i] si la
temperatura está dentro de la fila y la columna y la parte superior no es igual al signo
de bloque visitado[temp.x][temp.y] := nivel verdadero[temp.x][temp.y] := nivel[ top.x]
[top.y] + 1 Q.push(temp)

GoalKicker.com – Notas de algoritmos para profesionales 196


Machine Translated by Google

terminara si

fin por fin


mientras
Nivel de retorno

Como hemos discutido anteriormente, BFS solo funciona para gráficos no ponderados. Para gráficos ponderados, necesitaremos el
algoritmo de Dijkstra. Para ciclos de flanco negativo, necesitamos el algoritmo de Bellman-Ford. Nuevamente, este algoritmo es un
algoritmo de ruta más corta de fuente única. Si necesitamos averiguar la distancia de cada nodo a todos los demás nodos, necesitaremos
el algoritmo de Floyd Warshall.

Sección 41.3: Componentes conectados de un gráfico no


dirigido usando BFS
BFS se puede usar para encontrar los componentes conectados de un gráfico no dirigido. También podemos encontrar si la gráfica dada es
conexa o no. Nuestra discusión posterior asume que estamos tratando con gráficos no dirigidos. La definición de un gráfico conectado es:

Un grafo es conexo si existe un camino entre cada par de vértices.

El siguiente es un gráfico conectado.

El siguiente gráfico no es conexo y tiene 2 componentes conexos:

1. Componente conectado 1: {a,b,c,d,e}


2. Componente conectado 2: {f}

GoalKicker.com – Notas de algoritmos para profesionales 197


Machine Translated by Google

BFS es un algoritmo de recorrido de grafos. Entonces, comenzando desde un nodo de origen aleatorio, si al terminar el algoritmo, se visitan
todos los nodos, entonces el gráfico está conectado, de lo contrario, no está conectado.

Pseudocódigo para el algoritmo.

boolean isConnected(Graph g)
{ BFS(v)//v es un nodo fuente aleatorio.
si (todos los visitados (g)) {

devolver
verdadero; }
más devuelve falso; }

Implementación de C para encontrar si un gráfico no dirigido está conectado o no:

#include<stdio.h>
#include<stdlib.h> #define
MAXVERTICES 100

void poner en cola(int);


int deque(); int
isConnected(char **graph,int noOfVertices); void BFS(char
**graph,int vertex,int noOfVertices); conteo int = 0; //El nodo de cola
representa un solo elemento de cola //NO es un nodo de gráfico. nodo
de estructura {

intv ;
nodo de estructura *siguiente;
};

typedef struct nodo Nodo; typedef


estructura nodo *Nodeptr;

Nodoptr Qfront = NULL;


Nodoptr Qrear = NULL; char
*visitado;//matriz que realiza un seguimiento de los vértices visitados.

int principal()

GoalKicker.com – Notas de algoritmos para profesionales 198


Machine Translated by Google

{
int n,e;//n es el número de vértices, e es el número de aristas. int i,j; char
**grafo;//matriz de adyacencia

printf("Ingrese el número de vértices:");


escaneo("%d",&n);

if(n < 0 || n > MAXVERTICES)


{ fprintf(stderr, "Ingrese un entero
positivo válido de 1 a %d",MAXVERTICES); devolver -1; }

gráfico = malloc (n * tamaño de (char *)); visitado


= malloc(n*sizeof(char));

para(i = 0;i < n;++i) {

gráfico[i] = malloc(n*tamaño(int)); visited[i] =


'N';//inicialmente no se visitan todos los vértices. for(j = 0;j < n;++j) gráfico[i][j] = 0;

printf("Ingrese el número de aristas y luego ingréselas en pares:"); escaneo("%d",&e);

para(i = 0;i < e;++i) {

int u,v;
scanf("%d%d",&u,&v);
gráfico[u-1][v-1] = 1;
gráfico[v-1][u-1] = 1;
}

if(isConnected(graph,n)) printf("La
gráfica está conectada");
else printf("La gráfica NO es conexa\n");
}

void poner en cola ( vértice int ) {

si (Qfrente == NULL) {

Qfront = malloc(tamaño(Nodo));
Qfrente->v = vértice;
Qfrente->siguiente = NULL;
Qtrasero = Qdelantero;

} más
{
Nodeptr newNode = malloc(sizeof(Node)); nuevoNodo-
>v = vértice; nuevoNodo->siguiente = NULL;

Qposterior->siguiente = nuevoNodo;
Qrear = newNode;
}
}

int deque() {

GoalKicker.com – Notas de algoritmos para profesionales 199


Machine Translated by Google

si (Qfrente == NULL) {

printf("Q esta vacio, devolviendo -1\n"); devolver -1;

} más
{
int v = Qfront->v;
Nodeptr temp= Qfrente;
if(Qfrente == Qtrasero) {

Qfrente = Qfrente->siguiente;
Qtrasero = NULL;

} más
Qfrente = Qfrente->siguiente;

libre
(temporario); volver v;
}
}

int isConnected(char **graph,int noOfVertices) {

ent yo;

//dejar que el vértice de origen aleatorio sea el vértice 0;


BFS(gráfico,0,nºDeVertices);

for(i = 0;i < noOfVertices;++i) if(visited[i] ==


'N') return 0;//0 implica false;

return 1;//1 implica verdadero;


}

void BFS(char **graph,int v,int noOfVertices) {

int i,vértice;
visitado[v] = 'Y'; poner
en cola (v); while((vértice
= deque()) != -1) {

for(i = 0;i < noOfVertices;++i) if(grafo[vértice]


[i] == 1 && visitado[i] == 'N') {

poner en
cola(i); visitado[i] = 'Y';
}
}
}

Para encontrar todos los componentes conectados de un gráfico no dirigido, solo necesitamos agregar 2 líneas de código a la función BFS.
La idea es llamar a la función BFS hasta que se visiten todos los vértices.

Las lineas a agregar son:

printf("\nComponente conectado % d\n",++cuenta); //count


es una variable global inicializada a 0 // agregue esto como
primera línea a la función BFS

GoalKicker.com – Notas de algoritmos para profesionales 200


Machine Translated by Google

printf("%d ", vértice+1);


agregue esto como la primera línea del ciclo while en BFS

y definimos la siguiente función:

void listConnectedComponents(char **graph,int noOfVertices) {

ent yo;
for(i = 0;i < noOfVertices;++i) {

if(visitado[i] == 'N')
BFS(gráfico,i,nºDeVertices);

}
}

GoalKicker.com – Notas de algoritmos para profesionales 201


Machine Translated by Google

Capítulo 42: Primera búsqueda en profundidad

Sección 42.1: Introducción a la búsqueda en profundidad primero


Búsqueda en profundidad es un algoritmo para atravesar o buscar estructuras de datos de árboles o gráficos. Uno comienza
en la raíz y explora lo más lejos posible a lo largo de cada rama antes de retroceder. Se investigó una versión de la búsqueda
en profundidad en el matemático francés del siglo XIX Charles Pierre Trémaux como estrategia para resolver laberintos.

La búsqueda en profundidad es una forma sistemática de encontrar todos los vértices accesibles desde un vértice de origen. Al igual que la
búsqueda en amplitud, los DFS atraviesan un componente conectado de un gráfico determinado y definen un árbol de expansión. La idea básica de
la búsqueda en profundidad es explorar metódicamente cada borde. Empezamos de nuevo desde un vértice diferente según sea necesario. Tan
pronto como descubrimos un vértice, DFS comienza a explorar desde él (a diferencia de BFS, que pone un vértice en una cola para que lo explore
más tarde).

Veamos un ejemplo. Recorreremos este gráfico:

Recorreremos la gráfica siguiendo estas reglas:

Empezaremos desde la fuente.


Ningún nodo será visitado dos veces.

Los nodos que aún no visitamos serán de color blanco.


El nodo que visitamos, pero no visitamos todos sus nodos secundarios, será de color gris.
Los nodos completamente recorridos se colorearán de negro.

Veámoslo paso a paso:

GoalKicker.com – Notas de algoritmos para profesionales 202


Machine Translated by Google

GoalKicker.com – Notas de algoritmos para profesionales 203


Machine Translated by Google

GoalKicker.com – Notas de algoritmos para profesionales 204


Machine Translated by Google

Podemos ver una palabra clave importante. Eso es backedge. Puedes ver. 5-1 se llama backedge. Esto se debe a que aún no hemos terminado con el

nodo 1, por lo que pasar de otro nodo al nodo 1 significa que hay un ciclo en el gráfico. En DFS, si podemos pasar de un nodo gris a otro, podemos
estar seguros de que el gráfico tiene un ciclo. Esta es una de las formas de detectar el ciclo en un gráfico. Según el nodo de origen y el orden de los
nodos que visitamos, podemos encontrar cualquier borde en un ciclo como backedge. Por ejemplo: si pasamos a 5 desde 1 primero, habríamos encontrado

2-1 como backedge.

El borde que tomamos para pasar del nodo gris al nodo blanco se llama borde de árbol. Si solo mantenemos los bordes del árbol y eliminamos otros,

obtendremos el árbol DFS.

En un gráfico no dirigido, si podemos visitar un nodo ya visitado, ese debe ser un backedge. Pero para gráficos dirigidos, debemos verificar los
colores. Si y solo si podemos pasar de un nodo gris a otro nodo gris, eso se llama backedge.

En DFS, también podemos mantener marcas de tiempo para cada nodo, que se pueden usar de muchas maneras (p. ej., clasificación topológica).

1. Cuando un nodo v cambia de blanco a gris, el tiempo se registra en d[v].

GoalKicker.com – Notas de algoritmos para profesionales 205


Machine Translated by Google

2. Cuando un nodo v cambia de gris a negro, el tiempo se registra en f[v].

Aquí d[] significa tiempo de descubrimiento y f[] significa tiempo de finalización. Nuestro pseudo-código se verá así:

Procedimiento DFS(G):
para cada nodo u en V[G] color[u] :=
white parent[u] := NULL

end for
time := 0 para
cada nodo u en V[G]
si color[u] == blanco
DFS-Visit(u)
finaliza si
fin para

Procedimiento DFS-Visit(u):
color[u] := gray time := time + 1
d[u] := time para cada nodo v
adyacente a u if color[v] == white
parent[v] := u

DFS-Visit(v) end
if end for color[u] :=
black time := time + 1 f[u] :=
time

Complejidad:

Cada nodo y borde se visita una vez. Entonces, la complejidad de DFS es O(V+E), donde V denota el número de nodos y E denota el número de aristas.

Aplicaciones de la primera búsqueda en profundidad:

Encontrar el camino más corto de todos los pares en un gráfico no dirigido.

Detección de ciclo en un gráfico.

Búsqueda de caminos.

Clasificación topológica.

Probar si un grafo es bipartito.

Encontrar un componente fuertemente conectado.

Resolviendo acertijos con una solución.

GoalKicker.com – Notas de algoritmos para profesionales 206


Machine Translated by Google

Capítulo 43: Funciones hash


Sección 43.1: Códigos hash para tipos comunes en C#
Los códigos hash producidos por el método GetHashCode() para integrado y los tipos comunes de C# del espacio de nombres del
sistema se muestran a continuación.

booleano

1 si el valor es verdadero, 0 en caso contrario.

Byte, UInt16, int32, UInt32, Único

Valor (si es necesario convertido a Int32).

SByte

((int)m_valor ^ (int)m_valor << 8);


Carbonizarse

(int)m_valor ^ ((int)m_valor << 16);


Int16

((int)((ushort)m_value) ^ (((int)m_value) << 16));


int64, Doble

Xor entre los 32 bits inferiores y superiores de un número de 64 bits

(sin marcar ((int)((long)m_value)) ^ (int)(m_value >> 32));


UInt64, Fecha y hora, Espacio de
tiempo ((int)m_valor) ^ (int)(m_valor >> 32);
Decimal

((((int *)&dbl)[0]) & 0xFFFFFFF0) ^ ((int *)&dbl)[1];


Objeto

RuntimeHelpers.GetHashCode(esto);

La implementación predeterminada se utiliza índice de bloque de sincronización.

Cuerda

El cálculo del código hash depende del tipo de plataforma (Win32 o Win64), la característica de usar hashing de cadenas aleatorias,
modo de depuración/liberación. En el caso de la plataforma Win64:

int hash1 = 5381; int


hash2 = hash1; intc ;
char *s = origen;
while ((c = s[0]) != 0)
{ hash1 = ((hash1 << 5) +
hash1) ^ c = s[1]; si (c == 0) romper; C;
hash2 = ((hash2 << 5) + hash2) ^ s += 2;

C;

GoalKicker.com – Notas de algoritmos para profesionales 207


Machine Translated by Google

devolver hash1 + (hash2 * 1566083941);

Tipo de valor

El primer campo no estático es buscar y obtener su código hash. Si el tipo no tiene campos no estáticos, se devuelve el código hash del tipo. El código
hash de un miembro estático no se puede tomar porque si ese miembro es del mismo tipo que el tipo original, el cálculo termina en un bucle infinito.

Anulable<T>

devolver tiene valor ? valor.GetHashCode() : 0;

Formación

intret = 0 ; for
(int i = (Longitud >= 8 ? Longitud - 8 : 0); i < Longitud; i++) {

ret = ((ret << 5) + ret) ^ comparador.GetHashCode(GetValue(i));


}

Referencias

GitHub .Net Core CLR

Sección 43.2: Introducción a las funciones hash


La función hash h() es una función arbitraria que asigna datos x ÿ X de tamaño arbitrario al valor y ÿ Y de tamaño fijo: y = h(x). Las buenas
funciones hash tienen las siguientes restricciones:

Las funciones hash se comportan como una distribución uniforme.

funciones hash es determinista. h(x) siempre debe devolver el mismo valor para una x dada

cálculo rápido (tiene tiempo de ejecución O(1))

En el caso general, el tamaño de la función hash es menor que el tamaño de los datos de entrada: |y| < |x|. Las funciones hash no son
reversibles o, en otras palabras, pueden ser colisiones: ÿ x1, x2 ÿ X, x1 ÿ x2: h(x1) = h(x2). X puede ser un conjunto finito o infinito e Y es un
conjunto finito.

Las funciones hash se utilizan en muchas partes de la informática, por ejemplo, en ingeniería de software, criptografía, bases de datos, redes, aprendizaje
automático, etc. Hay muchos tipos diferentes de funciones hash, con diferentes propiedades específicas de dominio.

A menudo, hash es un valor entero. Existen métodos especiales en los lenguajes de programación para el cálculo de hash. Por ejemplo, en el método
GetHashCode() de C# para todos los tipos, se devuelve el valor Int32 (número entero de 32 bits). En Java , cada clase proporciona el método hashCode() que
devuelve int. Cada tipo de datos tiene implementaciones propias o definidas por el usuario.

Métodos hash

Hay varios enfoques para determinar la función hash. Sin pérdida de generalidad, sean x ÿ X = {z ÿ ÿ: z ÿ 0} números enteros positivos. A
menudo , m es primo (no demasiado cerca de una potencia exacta de 2).

Método Función hash


método de división h(x) = x módulo m

Método de multiplicación h(x) = ÿm (xA mod 1)ÿ, A ÿ {z ÿ ÿ: 0 < z < 1}


Tabla de picadillo

Funciones hash utilizadas en tablas hash para calcular el índice en una matriz de ranuras. La tabla hash es una estructura de datos para

GoalKicker.com – Notas de algoritmos para profesionales 208


Machine Translated by Google

implementar diccionarios (estructura clave-valor). Las buenas tablas hash implementadas tienen tiempo O (1) para la siguiente
operaciones: insertar, buscar y borrar datos por clave. Más de una clave puede codificar en la misma ranura. Hay dos
maneras de resolver la colisión:

1. Encadenamiento: la lista enlazada se usa para almacenar elementos con el mismo valor hash en la ranura

2. Direccionamiento abierto: cero o un elemento se almacena en cada ranura

Los siguientes métodos se utilizan para calcular las secuencias de sonda requeridas para el direccionamiento abierto

Método Fórmula

sondeo lineal h(x, i) = (h'(x) + i) módulo m


Sondeo cuadrático h(x, i) = (h'(x) + c1*i + c2*i^2) mod m
Hashing doble h(x, i) = (h1(x) + i*h2(x)) mod m

Donde i ÿ {0, 1, ..., m-1}, h'(x), h1(x), h2(x) son funciones hash auxiliares, c1, c2 son funciones auxiliares positivas
constantes

Ejemplos

Sea x ÿ U{1, 1000}, h = x mod m. La siguiente tabla muestra los valores hash en caso de no primo y primo. en negrita
el texto indica los mismos valores hash.

xm = 100 (no primo) m = 101 (primo)


723 23 dieciséis

103 3 2

738 38 31

292 92 90

61 61 61

87 87 87

995 95 86

549 49 44

991 91 82

757 57 50

920 20 11

626 26 20

557 57 52

831 31 23

619 19 13

Enlaces

Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein. Introducción a los Algoritmos.

Descripción general de las tablas hash

Wolfram MathWorld - Función hash

GoalKicker.com – Notas de algoritmos para profesionales 209


Machine Translated by Google

Capítulo 44: Vendedor ambulante


Sección 44.1: Algoritmo de fuerza bruta
Un camino a través de cada vértice exactamente una vez es lo mismo que ordenar los vértices de alguna manera. Por lo tanto, para calcular el
costo mínimo de viajar a través de cada vértice exactamente una vez, podemos aplicar fuerza bruta a cada uno de los N! permutaciones de los
números del 1 al N.

pseudocódigo

mínimo = INF
para todas las permutaciones P

actual = 0

para i de 0 a N-2
actual = actual + costo[P[i]][P[i+1]] <- Suma el costo de pasar de 1 vértice al siguiente

actual = actual + costo[P[N-1]][P[0]] primero <- Agregue el costo de ir desde el último vértice hasta el

si actual < mínimo <- Actualizar mínimo si es necesario


mínimo = corriente

mínimo de salida

Complejidad del tiempo

¡ Hay N! permutaciones por recorrer y el costo de cada ruta se calcula en O(N), por lo tanto, este algoritmo tarda O(N * N!) Tiempo para generar la
respuesta exacta.

Sección 44.2: Algoritmo de programación dinámica


Note que si consideramos el camino (en orden):

(1,2,3,4,6,0,5,7)

y el camino

(1,2,3,5,0,6,7,4)

El costo de pasar del vértice 1 al vértice 2 al vértice 3 sigue siendo el mismo, entonces, ¿por qué debe recalcularse? Este resultado se puede guardar
para su uso posterior.

Deje que dp[máscara de bits][vértice] represente el costo mínimo de viajar a través de todos los vértices cuyo bit correspondiente en la máscara de
bits se establece en 1 y termina en el vértice. Por ejemplo:

pd[12][2]

12 = 1100
^^

vértices: 3 2 1 0

Dado que 12 representa 1100 en binario, dp[12][2] representa pasar por los vértices 2 y 3 en el gráfico con el camino que termina en el vértice 2.

GoalKicker.com – Notas de algoritmos para profesionales 210


Machine Translated by Google

Por lo tanto, podemos tener el siguiente algoritmo (implementación en C++):

costo int [N][N]; int // Ajuste el valor de N si es necesario


memo[1 << N][N]; int // Establecer todo aquí en -1
TSP(int máscara de bits, int pos){
costo int = INF;
if (máscara de bits == ((1 << N) - 1)) // Todos los vértices han sido explorados
{ coste de retorno [pos][0]; // Costo para volver
}
if (memo[máscara de bits][pos] != -1) // Si esto ya ha sido calculado
{ return memo[máscara de bits][pos]; // Solo devuelve el valor, no es necesario volver a calcular
}
para (int i = 0; i < N; ++i){ // Por cada vértice
if ((bitmask & (1 << i)) == 0){ //Si el vértice no ha sido visitado
costo = min(costo,TSP(máscara de bits | (1 << i) , i) + costo[pos][i]); //Visita el vértice
}
}
memo[máscara de bits][pos] = costo; // Guardar el resultado
costo de devolución ;
}
// Llamar TSP(1,0)

Esta línea puede ser un poco confusa, así que repasemos lentamente:

costo = min(costo,TSP(máscara de bits | (1 << i) , i) + costo[pos][i]);

Aquí, máscara de bits | (1 << i) establece el i-ésimo bit de la máscara de bits en 1, lo que representa que se ha visitado el i-ésimo vértice. los
i después de la coma representa el nuevo pos en esa llamada de función, que representa el nuevo "último" vértice.
cost[pos][i] es sumar el costo de viajar del vértice pos al vértice i.

Por lo tanto, esta línea es para actualizar el valor del costo al valor mínimo posible de viajar a cualquier otro vértice que
aún no ha sido visitado.

Complejidad del tiempo

La función TSP(bitmask,pos) tiene 2^N valores para bitmask y N valores para pos. Cada función tarda un tiempo O(N) en
ejecutar (el bucle for ). Por lo tanto, esta implementación toma O (N ^ 2 * 2 ^ N) tiempo para generar la respuesta exacta.

GoalKicker.com – Notas de algoritmos para profesionales 211


Machine Translated by Google

Capítulo 45: Problema de la mochila


Sección 45.1: Fundamentos del problema de la mochila
El problema: Dado un conjunto de artículos donde cada artículo contiene un peso y un valor, determine el número de cada
uno para incluir en una colección de modo que el peso total sea menor o igual a un límite dado y el valor total sea lo más
grande posible.

Pseudocódigo para el problema de la mochila

Dado:

1. Valores (matriz v)
2. Pesos (matriz w)
3. Número de artículos distintos (n)
4. Capacidad (W)

para j de 0 a W hacer:
m[0, j] := 0 para i de
1 a n hacer:
para j de 0 a W hacer: si
w[i] > j entonces: m[i,
j] := m[i-1, j] sino:

m[i, j] := max(m[i-1, j], m[i-1, jw[i]] + v[i])

Una implementación simple del pseudocódigo anterior usando Python:

def mochila(W, wt, val, n):


K = [[0 para x en el rango (W+1)] para x en el rango (n+1)]
para i en el rango (n+1): para w en el rango (W+1): si i==0 o
w==0:

K[i][w] = 0 elif
wt[i-1] <= w:
K[i][w] = max(val[i-1] + K[i-1][w-wt[i-1]], K[i-1][w]) más:

K[i][w] = K[i-1][w]
devuelve K[n]
[W] val = [60, 100, 120]
wt = [10, 20, 30]
ancho = 50

n = len(valor)
print(mochila(W, wt, val, n))

Ejecutando el código: Guárdelo en un archivo llamado knapSack.py

$ python knapSack.py
220

Complejidad temporal del código anterior: O(nW) donde n es el número de artículos y W es la capacidad de la mochila.

Sección 45.2: Solución implementada en C#

problema de mochila de clase pública


{

GoalKicker.com – Notas de algoritmos para profesionales 212


Machine Translated by Google

privado estático int Mochila(int w, int[] peso, int[] valor, int n) {

ent yo;
int[,] k = nuevo int[n + 1, w + 1]; para (i
= 0; i <= n; i++) {

intb ;
para (b = 0; b <= w; b++) {

si (i==0 || b==0) {

k[yo, b] = 0;

} más si (peso[i - 1] <= b) {

k[i, b] = Math.Max(valor[i - 1] + k[i - 1, b - peso[i - 1]], k[i - 1, b]);

}
más {
k[i, b] = k[i - 1, b];
}
}

} devuelve k[n, w];


}

public static int Main(int nItems, int[] pesos, int[] valores) {

int n = valores.Longitud;
return Mochila(nItems, pesos, valores, n);
}
}

GoalKicker.com – Notas de algoritmos para profesionales 213


Machine Translated by Google

Capítulo 46: Resolución de ecuaciones

Sección 46.1: Ecuación lineal


Hay dos clases de métodos para resolver ecuaciones lineales:

1. Métodos directos: Las características comunes de los métodos directos son que transforman la ecuación original en ecuaciones
equivalentes que se pueden resolver más fácilmente, lo que significa que se resuelve directamente a partir de una ecuación.

2. Método iterativo: métodos iterativos o indirectos, comienzan con una suposición de la solución y luego refinan repetidamente la solución
hasta que se alcanza un cierto criterio de convergencia. Los métodos iterativos son generalmente menos eficientes que los métodos
directos porque se requiere una gran cantidad de operaciones. Ejemplo: método de iteración de Jacobi, método de iteración de Gauss-
Seidal.

Implementación en C-

// Implementación del Método de Jacobi void


JacobisMethod(int n, double x[n], double b[n], double a[n][n]){ double Nx[n]; //forma modificada
de las variables int rootFound=0; //bandera

i, j ; while(!
rootFound){ for(i=0; i<n;
i++){ Nx[i]=b[i]; // cálculo

for(j=0; j<n; j++){ if(i!=j)


Nx[i] = Nx[i]-a[i][j]*x[j];
}
Nx[i] = Nx[i] / a[i][i];
}

rootFound=1; // verificación
for(i=0; i<n; i++){ if(!( (Nx[i]-
x[i])/x[i] > -0.000001 && (Nx[i]-x[i])/x [i] < 0.000001 )){
rootFound=0;
descanso;
}
}

for(i=0; i<n; i++){ x[i]=Nx[i]; // evaluación

}
}

volver ;
}

// Implementación del Método Gauss-Seidal void


GaussSeidalMethod(int n, double x[n], double b[n], double a[n][n]){ double Nx[n]; //forma modificada
de las variables int rootFound=0; //bandera

i, j ; para(i=0;
i<n; i++){ // inicialización
Nx[i]=x[i];
}

GoalKicker.com – Notas de algoritmos para profesionales 214


Machine Translated by Google

while(!rootFound){ for(i=0;
i<n; i++){ Nx[i]=b[i]; // cálculo

for(j=0; j<n; j++){ if(i!=j)


Nx[i] = Nx[i]-a[i][j]*Nx[j];
}
Nx[i] = Nx[i] / a[i][i];
}

rootFound=1; // verificación
for(i=0; i<n; i++){ if(!( (Nx[i]-
x[i])/x[i] > -0.000001 && (Nx[i]-x[i])/x [i] < 0.000001 )){
rootFound=0;
descanso;
}
}

for(i=0; i<n; i++){ x[i]=Nx[i]; // evaluación

}
}

volver ;
}

// Imprimir matriz con separación de comas void


print(int n, double x[n]){ int i; for(i=0; i<n; i++)
{ printf("%lf, ", x[i]);

} printf("\n\n");

volver ;
}

int main(){ //
inicialización de la ecuación //
número de variables int n=3;

doble x[n]; // variables

doble b[n], a[n] // constantes //


[n]; argumentos

// asignar valores
a[0][0]=8; un[0][1]=2; a[0][2]=-2; b[0]=8; un[1][0]=1; a[1] //8xÿ+2xÿ-2xÿ+8=0
[1]=-8; un[1][2]=3; b[1]=-4; //xÿ-8xÿ+3xÿ-4=0 a[2][0]=2; un[2][1]=1; un[2][2]=9; b[2]=12;
// 2xÿ+xÿ+9xÿ+12=0

ent yo;

for(i=0; i<n; i++){ x[i]=0; // inicialización

}
JacobisMethod(n, x, b, a); imprimir
(n, x);

para(i=0; i<n; i++){ // inicialización

GoalKicker.com – Notas de algoritmos para profesionales 215


Machine Translated by Google

x[i]=0;
}
GaussSeidalMethod(n, x, b, a); imprimir
(n, x);

devolver 0;
}

Sección 46.2: Ecuación no lineal


Una ecuación del tipo f(x)=0 es algebraica o trascendental. Este tipo de ecuaciones se pueden resolver usando dos tipos de métodos

1. Método Directo: Este método da el valor exacto de todas las raíces directamente en un número finito de pasos.

2. Método indirecto o iterativo: los métodos iterativos son los más adecuados para que los programas de computadora resuelvan un problema.

ecuación. Se basa en el concepto de aproximación sucesiva. En el método iterativo hay dos formas de resolver una ecuación

Método de horquillado: tomamos dos puntos iniciales donde la raíz se encuentra entre ellos. Ejemplo de método de bisección,
método de posición falsa.

Método de extremo abierto: tomamos uno o dos valores iniciales donde la raíz puede estar en cualquier lugar. Ejemplo Método de

Newton-Raphson, método de aproximación sucesiva, método de la secante.

Implementación en C:

/// Aquí define diferentes funciones para trabajar #define f(x)


( ((x)*(x)*(x)) - (x) - 2 ) #define f2(x) ( (3*(x) *(x)) - 1 ) #define
g(x) ( cbrt( (x) + 2 ) )

/ **
* Toma dos valores iniciales y acorta la distancia por ambos lados. **/ double
BisectionMethod(){ double root=0;

doble a=1, b=2;


doble c=0;

int bucleContador=0;
if(f(a)*f(b) < 0){ while(1)
{ loopCounter++;
c=(a+b)/2;

si(f(c)<0.00001 && f(c)>-0.00001){


raíz=c;
descanso;
}

si((f(a))*(f(c)) < 0){ b=c; }más{

a=c;
}

GoalKicker.com – Notas de algoritmos para profesionales 216


Machine Translated by Google

} printf(" Tomó %d bucles.\n", loopCounter);

devolver raíz;
}

/ **
* Toma dos valores iniciales y acorta la distancia por un solo lado. **/ double FalsePosition()
{ double root=0;

doble a=1, b=2; doble


c=0;

int bucleContador=0;
if(f(a)*f(b) < 0){ while(1)
{ loopCounter++;

c=(a*f(b) - b*f(a)) / (f(b) - f(a));

/ */ printf("%lf\t %lf \n", c, f(c));/ **//// prueba si(f(c)<0.00001 &&


f(c)>-0.00001){ raíz=c; descanso;

si((f(a))*(f(c)) < 0){ b=c; }


más{ a=c;

}
}

} printf(" Tomó %d bucles.\n", loopCounter);

devolver raíz;
}

/ **
* Utiliza un valor inicial y gradualmente acerca ese valor al real. **/ doble NewtonRaphson(){ doble raíz=0;

doble x1=1;
doble x2=0;

int bucleContador=0;
while(1){ bucleContador+
+;

x2 = x1 - (f(x1)/f2(x1)); /*/ printf("%lf


\t %lf \n", x2, f(x2));/ **//// prueba

if(f(x2)<0.00001 && f(x2)>-0.00001){ raíz=x2;


descanso;

GoalKicker.com – Notas de algoritmos para profesionales 217


Machine Translated by Google

x1=x2;

} printf(" Tomó %d bucles.\n", loopCounter);

devolver raíz;
}

/ **
* Utiliza un valor inicial y gradualmente acerca ese valor al real. **/ double FixedPoint(){ double root=0;
doble x=1;

int bucleContador=0;
while(1){ bucleContador+
+;

si( (xg(x)) <0.00001 && (xg(x)) >-0.00001){


raíz = x;
descanso;
}

/ */ printf("%lf \t %lf \n", g(x), x-(g(x)));/ **//// prueba

x=g(x);

} printf(" Tomó %d bucles.\n", loopCounter);

devolver raíz;
}

/ **
* usa dos valores iniciales y ambos valores se aproximan a la raíz. **/ doble secante()
{ doble raíz=0;

doble x0=1;
doble x1=2;
doble x2=0;

int bucleContador=0;
while(1){ bucleContador+
+;

/ */ printf("%lf \t %lf \t %lf \n", x0, x1, f(x1));/ **//// prueba

if(f(x1)<0.00001 && f(x1)>-0.00001){ raíz=x1;


descanso;

x2 = ((x0*f(x1))-(x1*f(x0))) / (f(x1)-f(x0));

x0=x1;
x1=x2;

} printf(" Tomó %d bucles.\n", loopCounter);

devolver raíz;
}

GoalKicker.com – Notas de algoritmos para profesionales 218


Machine Translated by Google

int principal(){
raíz doble ;

root = BisectionMethod();
printf("Usando el método de bisección, la raíz es: %lf \n\n", raíz);

raíz = PosiciónFalsa();
printf("Usando el método de posición falsa, la raíz es: %lf \n\n", raíz);

raíz = NewtonRaphson();
printf("Usando el método Newton-Raphson la raíz es: %lf \n\n", root);

root = PuntoFijo();
printf("Usando el método de punto fijo, la raíz es: %lf \n\n", root);

raíz = secante();
printf("Usando el método de la secante, la raíz es: %lf \n\n", root);

devolver 0;
}

GoalKicker.com – Notas de algoritmos para profesionales 219


Machine Translated by Google

Capítulo 47: Común más largo


subsecuencia
Sección 47.1: Explicación de la subsecuencia común más larga
Una de las implementaciones más importantes de la programación dinámica es descubrir el común más largo
subsecuencia. Primero definamos algunas de las terminologías básicas.

Subsecuencia:

Una subsecuencia es una secuencia que se puede derivar de otra secuencia eliminando algunos elementos sin
cambiando el orden de los elementos restantes. Digamos que tenemos una cadena ABC. Si borramos cero o uno o más de
un carácter de esta cadena obtenemos la subsecuencia de esta cadena. Entonces las subsecuencias de la cadena ABC serán
{"A", "B", "C", "AB", "AC", "BC", "ABC", " "}. Incluso si eliminamos todos los caracteres, la cadena vacía también será un
subsecuencia Para averiguar la subsecuencia, para cada carácter de una cadena, tenemos dos opciones: tomamos la
carácter, o no lo hacemos. Entonces, si la longitud de la cadena es n, hay 2n subsecuencias de esa cadena.

Subsecuencia común más larga:

Como sugiere el nombre, de todas las subsecuencias comunes entre dos cadenas, la subsecuencia común más larga (LCS)
es el que tiene la longitud máxima. Por ejemplo: Las subsecuencias comunes entre "HELLOM" y "HMLD"
son "H", "HL", "HM" , etc. Aquí "HLL" es la subsecuencia común más larga que tiene una longitud de 3.

Método de fuerza bruta:

Podemos generar todas las subsecuencias de dos cadenas utilizando backtracking. Entonces podemos compararlos para averiguar el
subsecuencias comunes. Después tendremos que averiguar cuál tiene la longitud máxima. Ya lo hemos visto,
hay 2n subsecuencias de una cadena de longitud n. Llevaría años resolver el problema si nuestra n cruza 20-25.

Método de programación dinámica:

Acerquémonos a nuestro método con un ejemplo. Supongamos que tenemos dos cadenas abcdaf y acbcf. denotemos
estos con s1 y s2. Entonces, la subsecuencia común más larga de estas dos cadenas será "abcf", que tiene una longitud de 4.
Nuevamente les recuerdo, las subsecuencias no necesitan ser continuas en la cadena. Para construir "abcf", ignoramos "da" en s1
y "c" en s2. ¿Cómo descubrimos esto usando Programación Dinámica?

Comenzaremos con una tabla (una matriz 2D) que tiene todos los caracteres de s1 en una fila y todos los caracteres de s2 en una columna.
Aquí la tabla está indexada a 0 y ponemos los caracteres del 1 en adelante. Recorreremos la tabla de izquierda a derecha
por cada fila. Nuestra tabla se verá así:

0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| ch' | | un | segundo | do | re | un | F |
+-----+-----+-----+-----+-----+-----+-----+-----+
0| | | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | un | | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
2 | do | | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
3 | segundo | | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
4 | do | | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+

GoalKicker.com – Notas de algoritmos para profesionales 220


Machine Translated by Google

5|f| | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+

Aquí cada fila y columna representan la longitud de la subsecuencia común más larga entre dos cadenas si
tome los caracteres de esa fila y columna y agréguelos al prefijo anterior. Por ejemplo: Table[2][3] representa el
longitud de la subsecuencia común más larga entre "ac" y "abc".

La 0-ésima columna representa la subsecuencia vacía de s1. De manera similar, la 0-ésima fila representa el vacío
subsecuencia de s2. Si tomamos una subsecuencia vacía de una cadena e intentamos emparejarla con otra cadena, no importa
cuánto mide la longitud de la segunda subcadena, la subsecuencia común tendrá una longitud de 0. Entonces podemos llenar el 0-
th filas y 0-th columnas con 0's. Obtenemos:

0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| ch' | | un | segundo | do | re | un | F |
+-----+-----+-----+-----+-----+-----+-----+-----+
0| |0|0|0|0|0|0|0|
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | un | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
2 | do | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
3 | segundo | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
4 | do | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
5|f|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+

Vamos a empezar. Cuando llenamos la Tabla[1][1], nos preguntamos si teníamos una cadena a y otra cadena a y
nada más, ¿cuál será la subsecuencia común más larga aquí? La longitud del LCS aquí será 1. Ahora vamos a
mire la Tabla [1] [2]. Tenemos la cadena ab y la cadena a. La longitud de la LCS será 1. Como puedes ver, el resto de la
los valores también serán 1 para la primera fila, ya que considera solo la cadena a con abcd, abcda, abcdaf. Así se verá nuestra mesa
me gusta:

0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| ch' | | un | segundo | do | re | un | F |
+-----+-----+-----+-----+-----+-----+-----+-----+
0| |0|0|0|0|0|0|0|
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | un | 0 | 1 | 1 |1| 1 |1| 1 |
+-----+-----+-----+-----+-----+-----+-----+-----+
2 | do | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
3 | segundo | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
4 | do | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
5|f|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+

Para la fila 2, que ahora incluirá c. Para la Tabla[2][1] tenemos ac en un lado y a en el otro lado. Entonces la longitud de
el LCS es 1. ¿De dónde sacamos este 1? Desde arriba, que denota el LCS a entre dos subcadenas. Y qué
lo que decimos es que si s1[2] y s2[1] no son iguales, entonces la longitud del LCS será el máximo de la longitud de

221
GoalKicker.com – Notas de algoritmos para profesionales
Machine Translated by Google

LCS en la parte superior o a la izquierda. Tomar la longitud del LCS en la parte superior indica que no tomamos la corriente

personaje de s2. De manera similar, tomar la longitud del LCS a la izquierda denota que no tomamos la corriente

carácter de s1 para crear el LCS. Obtenemos:

0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| ch' | | un | segundo | do | re | un | F |
+-----+-----+-----+-----+-----+-----+-----+-----+
0| |0|0|0|0|0|0|0|
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | un | 0 | 1 | 1 |1| 1 |1| 1 |
+-----+-----+-----+-----+-----+-----+-----+-----+
2 | do | 0 | 1 | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
3 | segundo | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
4 | do | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
5|f|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+

Así que nuestra primera fórmula será:

si s2[i] no es igual a s1[j]


Tabla[i][j] = max(Tabla[i-1][j], Tabla[i][j-1]
terminara si

Continuando, para Table[2][2] tenemos las cadenas ab y ac. Como c y b no son iguales, ponemos el máximo de la parte superior o

dejado aquí. En este caso, es nuevamente 1. Después de eso, para Table[2][3] tenemos las cadenas abc y ac. Esta vez los valores actuales de

tanto la fila como la columna son iguales. Ahora la longitud de LCS será igual a la longitud máxima de LCS hasta ahora + 1.

¿Cómo obtenemos la longitud máxima de LCS hasta ahora? Comprobamos el valor de la diagonal, que representa la mejor coincidencia
entre ab y a. A partir de este estado, para los valores actuales, agregamos un carácter más a s1 y s2 que

pasó a ser el mismo. Entonces, la duración de LCS, por supuesto, aumentará. Pondremos 1 + 1 = 2 en Table[2][3]. Obtenemos,

0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| ch' | | un | segundo | do | re | un | F |
+-----+-----+-----+-----+-----+-----+-----+-----+
0| |0|0|0|0|0|0|0|
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | un | 0 | 1 | 1 |1| 1 |1| 1 |
+-----+-----+-----+-----+-----+-----+-----+-----+
2 | do | 0 | 1 | 1 |2| | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
3 | segundo | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
4 | do | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
5|f|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+

Así que nuestra segunda fórmula será:

si s2[i] es igual a s1[j]


Tabla[i][j] = Tabla[i-1][j-1] + 1
terminara si

GoalKicker.com – Notas de algoritmos para profesionales 222


Machine Translated by Google

Hemos definido ambos casos. Usando estas dos fórmulas, podemos llenar toda la tabla. Después de llenar el
tabla, se verá así:

0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| ch' | | un | segundo | do | re | un | F |
+-----+-----+-----+-----+-----+-----+-----+-----+
0| |0|0|0|0|0|0|0|
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | un | 0 | 1 | 1 |1| 1 |1| 1 |
+-----+-----+-----+-----+-----+-----+-----+-----+
2 | do | 0 | 1 | 1 |2|2|2|2|
+-----+-----+-----+-----+-----+-----+-----+-----+
3 | segundo | 0 | 1 | 2 | 2 | 2 | 2 | 2 |
+-----+-----+-----+-----+-----+-----+-----+-----+
4 | do | 0 | 1 | 2 | 3 | 3 | 3 | 3 |
+-----+-----+-----+-----+-----+-----+-----+-----+
5|f|0|1|2|3|3|3|4|
+-----+-----+-----+-----+-----+-----+-----+-----+

La longitud del LCS entre s1 y s2 será Table[5][6] = 4. Aquí, 5 y 6 son la longitud de s2 y s1


respectivamente. Nuestro pseudocódigo será:

Procedimiento LCSlongitud(s1, s2):


Tabla[0][0] = 0
para i de 1 a s1.longitud
Tabla[0][i] = 0
final para
para i de 1 a s2.longitud
Tabla[i][0] = 0
final para
para i de 1 a s2.longitud
para j de 1 a s1.longitud
si s2[i] es igual a s1[j]
Tabla[i][j] = Tabla[i-1][j-1] + 1
más
Tabla[i][j] = max(Tabla[i-1][j], Tabla[i][j-1])
terminara si

final para
final para
Tabla de retorno [s2.longitud][s1.longitud]

La complejidad temporal de este algoritmo es: O(mn) donde m y n indican la longitud de cada cadena.

¿Cómo encontramos la subsecuencia común más larga? Comenzaremos desde la esquina inferior derecha. Comprobaremos
de donde viene el valor. Si el valor viene de la diagonal, eso es si Tabla[i-1][j-1] es igual a
Table[i][j] - 1, presionamos s2[i] o s1[j] (ambos son iguales) y nos movemos en diagonal. Si el valor viene de arriba,
eso significa que si Table[i-1][j] es igual a Table[i][j], nos movemos hacia arriba. Si el valor viene de la izquierda, eso significa que si
Table[i][j-1] es igual a Table[i][j], nos movemos hacia la izquierda. Cuando llegamos a la columna más a la izquierda o más arriba, nuestra búsqueda
termina Luego extraemos los valores de la pila y los imprimimos. El pseudocódigo:

Procedimiento ImprimirLCS(LCSlongitud, s1, s2)


temp := LCSlongitud
S = pila ()
i := s2.longitud
j := s1.longitud
mientras i no es igual a 0 y j no es igual a 0
si Tabla[i-1][j-1] == Tabla[i][j] - 1 y s1[j]==s2[i]

GoalKicker.com – Notas de algoritmos para profesionales 223


Machine Translated by Google

S.push(s1[j]) //o S.push(s2[i]) i := i - 1 j := j


- 1 else if Table[i-1][j] == Table[i] [j]

yo := i-1 más

j := j-1
endif endwhile
mientras S no está
vacío print(S.pop) endwhile

Punto a tener en cuenta: si tanto la Tabla[i-1][j] como la Tabla[i][j-1] son iguales a la Tabla[i][j] y la Tabla[i-1][j-1] no lo es
igual a Table[i][j] - 1, puede haber dos LCS para ese momento. Este pseudocódigo no considera esta situación. Tendrá que
resolver esto recursivamente para encontrar múltiples LCS.

La complejidad temporal de este algoritmo es: O(max(m, n)).

GoalKicker.com – Notas de algoritmos para profesionales 224


Machine Translated by Google

Capítulo 48: Aumento más largo


subsecuencia
Sección 48.1: Información básica de la subsecuencia
creciente más larga
La subsecuencia creciente más larga el problema es encontrar la subsecuencia de la secuencia de entrada dada en la que los
elementos de la subsecuencia se ordenan de menor a mayor orden. Todas las subsecuencias no son contiguas ni únicas.

Aplicación de la subsecuencia creciente más larga:

Los algoritmos como la subsecuencia creciente más larga, la subsecuencia común más larga se utilizan en sistemas de control
de versiones como Git, etc.

Forma simple de algoritmo:

1. Busque líneas únicas que sean comunes a ambos documentos.


2. Tome todas las líneas del primer documento y ordénelas según su aparición en el segundo
documento.
3. Calcule el LIS de la secuencia resultante (haciendo una clasificación de paciencia), obtener la secuencia coincidente más larga
de líneas, una correspondencia entre las líneas de dos documentos.
4. Repita el algoritmo en cada rango de líneas entre las ya emparejadas.

Ahora consideremos un ejemplo más simple del problema LCS. Aquí, la entrada es solo una secuencia de enteros distintos
a1,a2,...,an., y queremos encontrar la subsecuencia creciente más larga en ella. Por ejemplo, si la entrada es 7,3,8,4,2,6 , entonces
la subsecuencia creciente más larga es 3,4,6.

El enfoque más sencillo es ordenar los elementos de entrada en orden creciente y aplicar el algoritmo LCS a las secuencias originales
y ordenadas. Sin embargo, si observa la matriz resultante, notará que muchos valores son iguales y la matriz se ve muy repetitiva.
Esto sugiere que el problema LIS (subsecuencia creciente más larga) se puede resolver con un algoritmo de programación dinámica
utilizando solo una matriz unidimensional.

Pseudocódigo:

1. Describe una matriz de valores que queremos calcular.


Para 1 <= i <= n, sea A(i) la longitud de una secuencia creciente de entrada más larga. Tenga en cuenta que la longitud
que nos interesa en última instancia es max{A(i)|1 ÿ i ÿ n}.
2. Dar una recurrencia.
Para 1 <= i <= n, A(i) = 1 + max{A(j)|1 ÿ j < i y input(j) < input(i)}.
3. Calcule los valores de A.
4. Encuentra la solución óptima.

El siguiente programa usa A para calcular una solución óptima. La primera parte calcula un valor m tal que A(m) es la longitud de una
subsecuencia creciente óptima de entrada. La segunda parte calcula una subsecuencia creciente óptima, pero por conveniencia la
imprimimos en orden inverso. Este programa se ejecuta en el tiempo O(n), por lo que todo el algoritmo se ejecuta en el tiempo O(n^2).

Parte 1:

mÿ1
para i : 2..n si
A(i) > A(m) entonces

GoalKicker.com – Notas de algoritmos para profesionales 225


Machine Translated by Google

m ÿ yo
terminar si
terminar para

Parte 2:

poner
un tiempo A(m) > 1 do i ÿ
mÿ1

mientras que not(ai < am y A(i) = A(m)ÿ1) sí


yo ÿ yoÿ1
terminar mientras

m ÿ yo

poner
fin mientras

Solución recursiva:

Enfoque 1:

LIS(A[1..n]): si (n =
0) entonces devuelve 0 m = LIS(A[1..
(n ÿ 1)])
B es una subsecuencia de A[1..(n ÿ 1)] con solo elementos menores que a[n] (* sea h el tamaño de B, h ÿ
n-1 *) m = max(m, 1 + LIS( B[1..h]))

Salida m

Complejidad del tiempo en el Enfoque 1: O(n*2^n)

Enfoque 2:

LIS(A[1..n], x): si (n = 0)
entonces devuelve 0 m = LIS(A[1..(n
ÿ 1)], x) si (A[n] < x) entonces m =
máx(m, 1 + LIS(A[1..(n ÿ 1)], A[n]))

Salida m

PRINCIPAL(A[1..n]):
devuelve LIS(A[1..n], ÿ)

Complejidad de tiempo en el enfoque 2: O (n ^ 2)

Enfoque 3:

LIS(A[1..n]): si (n =
0) devuelve 0 m = 1

para i = 1 a n ÿ 1 hacer
si (A[i] < A[n]) entonces m =
max(m, 1 + LIS(A[1..i]))
volver m

PRINCIPAL(A[1..n]):
devuelve LIS(A[1..i])

GoalKicker.com – Notas de algoritmos para profesionales 226


Machine Translated by Google

Complejidad de tiempo en el enfoque 3: O (n ^ 2)

Algoritmo iterativo:

Calcula los valores iterativamente de forma ascendente.

LIS(A[1..n]):
Matriz L[1..n]
(* L[i] = valor del final de LIS (A[1..i]) *) for i = 1 to n do
L[i] = 1 for j = 1 to i ÿ 1 do

si (A[j] < A[i]) hacer


L[i] = máx(L[i], 1 + L[j])
volver L

MAIN(A[1..n]): L =
LIS(A[1..n]) devuelve
el valor máximo en L

Complejidad del tiempo en el enfoque iterativo: O(n^2)

Espacio Auxiliar: O(n)

Tomemos {0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15} como entrada. Entonces, la subsecuencia creciente más larga para la entrada
dada es {0, 2, 6, 9, 11, 15}.

GoalKicker.com – Notas de algoritmos para profesionales 227


Machine Translated by Google

Capítulo 49: Comprueba que dos cadenas son anagramas

Dos cadenas con el mismo conjunto de caracteres se llaman anagrama. He usado javascript aquí.

Crearemos un hash de str1 y aumentaremos el conteo +1. Haremos un bucle en la segunda cadena y comprobaremos que todos los caracteres están allí
en hash y disminuiremos el valor de la clave hash. Verifique que todos los valores de la clave hash sean cero, será un anagrama.

Sección 49.1: Muestra de entrada y salida


Ej1:

let str1 = 'desbordamiento de pila';


let str2 = 'florovstack';

Estas cadenas son anagramas.

// Crear Hash a partir de str1 y aumentar una cuenta.

hashMap =
{ s : 1, t :
1, a : 1,
c : 1, k :
1, o : 2,
v : 1, e :
1, r : 1,
f : 1, l :
1, w : 1

Puede ver que hashKey 'o' contiene el valor 2 porque o es 2 veces en la cadena.

Ahora recorra str2 y verifique que cada carácter esté presente en hashMap, si es así, disminuya el valor de hashMap Key, de lo contrario, devuelva falso
(lo que indica que no es un anagrama).

hashMap =
{ s : 0, t :
0, a : 0,
c : 0, k :
0, o : 0,
v : 0, e :
0, r : 0,
f : 0, l : 0,

w:0
}

Ahora, recorra el objeto hashMap y verifique que todos los valores sean cero en la clave de hashMap.

GoalKicker.com – Notas de algoritmos para profesionales 228


Machine Translated by Google

En nuestro caso, todos los valores son cero, por lo que es un anagrama.

Sección 49.2: Código Genérico para Anagramas


(función(){

var hashMap = {};

función es Anagrama (str1, str2) {

if(str1.longitud !== str2.longitud){ return


false;
}

// Crea un mapa hash del carácter str1 y aumenta el valor uno (+1).
createStr1HashMap(str1);

// Comprobar que el carácter str2 es clave en el mapa hash y disminuir el valor en uno (-1); var
valueExist = createStr2HashMap(str2);

// Verifique que todos los valores de las claves hashMap sean cero, por lo que será un
anagrama. return isStringsAnagram(valueExist);
}

function createStr1HashMap (str1)


{ [].map.call(str1, function(value, index, array){ hashMap[value]
= valor en hashMap ? (hashMap[value] + 1) : 1; valor devuelto ;

});
}

función crear Str2HashMap (str2) {


var valueExist = [].every.call(str2, function(value, index, array){
if(valor en hashMap)
{ hashMap[valor] = hashMap[valor] - 1;

} valor devuelto en hashMap;


});
valor de retorno Existe;
}

función isStringsAnagram (valueExist) { if(!valueExist)


{ return valueExist; } else { var isAnagram; para
(var i en hashMap) {

if(hashMap[i] !== 0)
{ isAnagram = false;
descanso; } else
{ isAnagram = true;

}
}

volver esAnagrama;
}
}

isAnagram('desbordamiento de pila', 'florovstack'); // true


isAnagram('stackoverflow', 'flowervvstack'); // falso

GoalKicker.com – Notas de algoritmos para profesionales 229


Machine Translated by Google

})();

Complejidad del tiempo: 3n, es decir, O(n).

GoalKicker.com – Notas de algoritmos para profesionales 230


Machine Translated by Google

Capítulo 50: Triángulo de Pascal


Sección 50.1: Triángulo de Pascal en C
int i, espacio, filas, k=0, cuenta = 0, cuenta1 = 0; fila=5;
for(i=1; i<=filas; ++i) {

for(espacio=1; espacio <= filas-i; ++espacio) {

imprimirf(" "); +
+contar;
}

while(k != 2*i-1) {

si (contar <= filas-1) {

printf("%d ", i+k); +


+contar;

}
más {
++cuenta1;
printf("%d ", (i+k-2*cuenta1));

} ++ k;

} cuenta1 = cuenta = k = 0;

imprimirf("\n");
}

Producción

1232
34543
456765456
7898765

GoalKicker.com – Notas de algoritmos para profesionales 231


Machine Translated by Google

Capítulo 51: Algo: - Imprima la matriz am * n en forma cuadrada

Verifique la entrada y salida de muestra a continuación.

Sección 51.1: Ejemplo de muestra

Aporte:

14 15 16 17 18 21 19
10 20 11 54 36 64 55
44 23 80 39 91 92 93
94 95 42

Salida:
imprimir valor en índice
14 15 16 17 18 21 36 39 42 95 94 93 92 91 64 19 10 20 11 54 80 23 44 55

o imprima el índice
00 01 02 03 04 05 15 25 35 34 33 32 31 30 20 10 11 12 13 14 24 23 22 21

Sección 51.2: Escriba el código genérico


function noOfLooping(m,n) { if(m >
n) { valor más pequeño = n; }
else { valor más pequeño
= m;

if(valor más pequeño % 2 == 0)


{ return valor más pequeño/
2; } else { return (menorValor+1)/2;

}
}

función squarePrint(m,n) { var


looping = noOfLooping(m,n); for(var i =
0; i < bucle; i++) {
for(var j = i; j < m - 1 - i; j++) { console.log(i+''+j);

} for(var k = i; k < n - 1 - i; k++) { console.log(k+''+j);

} for(var l = j; l > i; l--)


{ console.log(k+''+l);

} for(var x = k; x > i; x--)


{ console.log(x+''+l);
}
}
}

impresión cuadrada(6,4);

GoalKicker.com – Notas de algoritmos para profesionales 232


Machine Translated by Google

Capítulo 52: Exponenciación de matrices

Sección 52.1: Exponenciación de matrices para resolver el ejemplo


Problemas
Encuentre f(n): enésimo número de Fibonacci. El problema es bastante fácil cuando n es relativamente pequeño. Podemos usar la recursividad simple,
f(n) = f(n-1) + f(n-2), o podemos usar el enfoque de programación dinámica para evitar el cálculo de la misma función
una y otra vez. Pero, ¿qué harás si el problema dice, Dado 0 < n < 10ÿ, encuentra f(n) mod 999983? Dinámica
la programación fallará, entonces, ¿cómo abordamos este problema?

Primero, veamos cómo la exponenciación de matrices puede ayudar a representar una relación recursiva.

requisitos previos:

Dadas dos matrices, saber encontrar su producto. Además, dada la matriz producto de dos matrices, y
uno de ellos, saber cómo encontrar la otra matriz.

Dada una matriz de tamaño d X d, saber encontrar su n-ésima potencia en O(d3log(n)).

Patrones:

Primero necesitamos una relación recursiva y queremos encontrar una matriz M que nos pueda llevar al estado deseado desde un

conjunto de estados ya conocidos. Supongamos que conocemos los k estados de una relación de recurrencia dada y queremos
encontrar el (k+1)-ésimo estado. Sea M una matriz k X k , y construimos una matriz A:[k X 1] a partir de los estados conocidos de la

relación de recurrencia, ahora queremos obtener una matriz B:[k X 1] que representará el conjunto de los siguientes estados, es decir, MXA = B
Como se muestra abajo:

| f(n) | | f(n+1) |
| f(n-1) | | f(n) |
México | f(n-2) | = | f(n-1) |
| ...... | | ...... |
| f(nk) | |f(n-k+1)|

Entonces, si podemos diseñar M en consecuencia, ¡nuestro trabajo estará hecho! La matriz se utilizará entonces para representar la recurrencia
relación.

Tipo 1:
Comencemos con el más simple, f(n) = f(n-1) + f(n-2)
Obtenemos, f(n+1) = f(n) + f(n-1).
Supongamos que conocemos f(n) y f(n-1); Queremos averiguar f(n+1).
A partir de la situación anterior, la matriz A y la matriz B se pueden formar como se muestra a continuación:

Matriz A Matriz B

| f(n) | | f(n-1) | f(n+1) |


| | f(n) |

[Nota: la matriz A siempre se diseñará de tal manera que todos los estados de los que depende f(n+1) estén presentes]
Ahora, necesitamos diseñar una matriz M de 2X2 tal que satisfaga MXA = B como se indicó anteriormente.
El primer elemento de B es f(n+1) que en realidad es f(n) + f(n-1). Para obtener esto, de la matriz A, necesitamos, 1 X f(n) y 1
Xf(n-1). Así que la primera fila de M será [1 1].

|11| | X | f(n) | = | f(n+1) |


----- | | f(n-1) | | ------ |

GoalKicker.com – Notas de algoritmos para profesionales 233


Machine Translated by Google

[Nota: ----- significa que no nos preocupa este valor.]

De manera similar, el segundo elemento de B es f(n) que se puede obtener simplemente tomando 1 X f(n) de A, por lo que la segunda fila de M es [1 0].

| ----- | X | f(n) | = | ------ |


|1 0| | f(n-1) | | f(n) |

Entonces obtenemos nuestra matriz M deseada de 2 X 2 .

|1 1 | X | f(n) | = | f(n+1) |
|1 0| | f(n-1) | | f(n) |

Estas matrices se derivan simplemente mediante la multiplicación de matrices.

Tipo 2:

Hagámoslo un poco complejo: encuentre f(n) = a X f(n-1) + b X f(n-2), donde a y b son constantes.
Esto nos dice, f(n+1) = a X f(n) + b X f(n-1).
Hasta aquí debe quedar claro que la dimensión de las matrices será igual al número de dependencias, es decir
en este ejemplo particular, nuevamente 2. Entonces, para A y B, podemos construir dos matrices de tamaño 2 X 1:

Matriz A | Matriz B
f(n) | | f(n-1) | | f(n+1) |
| f(n) |

Ahora para f(n+1) = a X f(n) + b X f(n-1), necesitamos [a, b] en la primera fila de la matriz objetiva M. Y para la 2da
elemento en B, es decir, f(n) ya lo tenemos en la matriz A, así que solo tomamos eso, que lleva, la segunda fila de la matriz M
a [1 0]. Esta vez obtenemos:

| segundo | X | f(n) | = | f(n+1) |


un | 1 0| | f(n-1) | | f(n) |

Bastante simple, ¿eh?

Tipo 3:

Si has sobrevivido hasta esta etapa, has envejecido mucho, ahora enfrentemos una relación un poco compleja: encuentra f(n) =
a X f(n-1) + c X f(n-3)?
¡Uy! Hace unos minutos, todo lo que vimos eran estados contiguos, pero aquí falta el estado f(n-2) . ¿Ahora?

En realidad esto ya no es un problema, podemos convertir la relación de la siguiente manera: f(n) = a X f(n-1) + 0 X f(n-2) +
c X f(n-3), deduciendo f(n+1) = a X f(n) + 0 X f(n-1) + c X f(n-2). Ahora, vemos que, esto es en realidad una forma
descrito en el Tipo 2. Así que aquí la matriz objetivo M será 3 X 3, y los elementos son:

| un 0c | _ | f(n) | | f(n+1) |
| 1 0 0 | X | f(n-1) | = | f(n) |
| 0 1 0 | | f(n-2) | | f(n-1) |

Estos se calculan de la misma manera que el tipo 2, si te resulta difícil, pruébalo con lápiz y papel.

Tipo 4:

La vida se está volviendo increíblemente compleja, y el Sr. Problema ahora le pide que encuentre f(n) = f(n-1) + f(n-2) + c donde c es cualquiera
constante.

Ahora bien, este es uno nuevo y todo lo que hemos visto en el pasado, después de la multiplicación, cada estado en A se transforma en su siguiente

GoalKicker.com – Notas de algoritmos para profesionales 234


Machine Translated by Google

estado en B.

f(n) = f(n-1) + f(n-2) + c


f(n+1) = f(n) + f(n-1) + c
f(n+2) = f(n+1) + f(n) + c
..................... etc. _

Entonces, normalmente no podemos obtenerlo de la manera anterior, pero ¿qué tal si agregamos c como un estado?

| f(n) | | f(n+1) |
México | f(n-1) | = | f(n) |
| C ||| C

Ahora, no es muy difícil diseñar M. Así es como se hace, pero no olvide verificar:

|11 1 | | f(n) | | f(n+1) |


| 1 0 0 | X | f(n-1) | = | f(n) |
|001|| | | C | C

Tipo 5:

Pongámoslo todo junto: encuentre f(n) = a X f(n-1) + c X f(n-3) + d X f(n-4) + e. Dejémoslo como ejercicio para
tú. Primero intente averiguar los estados y la matriz M. Y verifique si coincide con su solución. Encuentre también la matriz A y
B.

| un 0 cd 1 |
|10000|
|01000|
|00100|
|00001|

Tipo 6:

A veces la recurrencia se da así:

f(n) = f(n-1) -> si n es impar


f(n) = f(n-2) -> si n es par

En breve:

f(n) = (n&1) X f(n-1) + (!(n&1)) X f(n-2)

Aquí, podemos dividir las funciones en base a par impar y mantener 2 matrices diferentes para ambas y calcular
ellos por separado.

Tipo 7:

¿Te sientes demasiado confiado? Bien por usted. A veces podemos necesitar mantener más de una recurrencia, donde
están interesados. Por ejemplo, sea una recurrencia re;atopm:

g(n) = 2g(n-1) + 2g(n-2) + f(n)

Aquí, la recurrencia g(n) depende de f(n) y esto se puede calcular en la misma matriz pero de mayor
dimensiones. A partir de estos, primero diseñemos las matrices A y B.

GoalKicker.com – Notas de algoritmos para profesionales 235


Machine Translated by Google

Matriz A | Matriz B
g(n) | | g(n-1) | g(n+1) |
| | f(n+1) | | | g(n) |
f(n) | | f(n+2) |
| f(n+1) |

Aquí, g(n+1) = 2g(n-1) + f(n+1) y f(n+2) = 2f(n+1) + 2f(n). Ahora, utilizando los procesos mencionados anteriormente,
puede encontrar que la matriz objetivo M es:

|2210|
|1000|
|0022|
|0010|

Entonces, estas son las categorías básicas de las relaciones de recurrencia que se utilizan para resolver mediante esta sencilla técnica.

GoalKicker.com – Notas de algoritmos para profesionales 236


Machine Translated by Google

Capítulo 53: algoritmo polinomial acotado en el tiempo para


la cobertura mínima de vértices
Variable Sentido
GRAMO
Gráfico no dirigido conectado a la entrada
X Conjunto de vértices

C Conjunto final de vértices

Este es un algoritmo polinómico para obtener la cobertura mínima de vértices de un gráfico no dirigido conectado. La complejidad temporal de este
algoritmo es O(n2)

Sección 53.1: Algoritmo Pseudo Código


Algoritmo PMinVertexCover (gráfico G)

Gráfico conectado de entrada G

Conjunto de cobertura de vértice mínimo de salida C

Conjunto C <- nuevo Conjunto<Vértice>()

Conjunto X <- nuevo Conjunto<Vértice>()

X <- G.getAllVerticiesArrangedDescendinglyByDegree()

para v en x hacer
List<Vértice> vértices adyacentes1 <- G.getAdjacent(v)

si !C contiene cualquiera de los Vertices1 adyacentes entonces

C.añadir(v)

para el vértice en C do

Lista<vértice> vértices2 adyacentes <- G.vérticesadyacentes(vértice)

si C contiene cualquiera de los Vertices2 adyacentes entonces

C.remove(vértice)

volver C

C es la cobertura mínima de vértice del gráfico G

podemos usar la ordenación por cubos para ordenar los vértices según su grado porque el valor máximo de los grados es (n-1) donde n
es el número de vértices, entonces la complejidad temporal de la clasificación será O(n)

GoalKicker.com – Notas de algoritmos para profesionales 237


Machine Translated by Google

Capítulo 54: Deformación dinámica del tiempo

Sección 54.1: Introducción a la deformación dinámica del tiempo


Deformación dinámica del tiempo (DTW) es un algoritmo para medir la similitud entre dos secuencias temporales que pueden
variar en velocidad. Por ejemplo, las similitudes al caminar podrían detectarse usando DTW, incluso si una persona estuviera caminando.
más rápido que el otro, o si hubo aceleraciones y desaceleraciones durante el curso de una observación. Puede ser
se utiliza para hacer coincidir un comando de voz de muestra con otro comando, incluso si la persona habla más rápido o más lento que el
muestra de voz pregrabada. DTW se puede aplicar a secuencias temporales de datos de video, audio y gráficos; de hecho,
cualquier dato que pueda convertirse en una secuencia lineal puede analizarse con DTW.

En general, DTW es un método que calcula una coincidencia óptima entre dos secuencias dadas con ciertas
restricciones Pero centrémonos en los puntos más simples aquí. Digamos que tenemos dos secuencias de voz Sample y Test, y
queremos comprobar si estas dos secuencias coinciden o no. Aquí la secuencia de voz se refiere a la señal digital convertida de
tu voz. Puede ser la amplitud o la frecuencia de su voz lo que denota las palabras que dice. Asumamos:

Muestra = {1, 2, 3, 5, 5, 5, 6}
Prueba = {1, 1, 2, 2, 3, 5}

Queremos encontrar la coincidencia óptima entre estas dos secuencias.

Primero, definimos la distancia entre dos puntos, d(x, y) donde x e y representan los dos puntos. Dejar,

d(x, y) = |x - y| // diferencia absoluta

Vamos a crear una tabla de matriz 2D usando estas dos secuencias. Calcularemos las distancias entre cada punto de
Muestra con todos los puntos de prueba y encuentra la combinación óptima entre ellos.

+------+------+------+------+------+------+------+ ------+
| | 0| 1| 1| 2| 2|3| 5|
+------+------+------+------+------+------+------+ ------+
| 0| | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 1| | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 2| | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 3| | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 5| | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 5| | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 5| | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 6| | | | | | | |
+------+------+------+------+------+------+------+ ------+

Aquí, Table[i][j] representa la distancia óptima entre dos secuencias si consideramos la secuencia hasta
Sample[i] y Test[j], considerando todas las distancias óptimas que observamos antes.

Para la primera fila, si no tomamos valores de Sample, la distancia entre este y Test será infinito. Así que ponemos
infinito en la primera fila. Lo mismo ocurre con la primera columna. Si no tomamos valores de Test, la distancia entre este
uno y Sample también serán infinitos. Y la distancia entre 0 y 0 será simplemente 0. Obtenemos,

GoalKicker.com – Notas de algoritmos para profesionales 238


Machine Translated by Google

+------+------+------+------+------+------+------+ ------+
| | 0| 1| 1| 2| 2|3| 5|
+------+------+------+------+------+------+------+ ------+
| 0| 0 | información | información | información | información | información | información |
+------+------+------+------+------+------+------+ ------+
| 1 | información | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 2 | información | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 3 | información | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 5 | información | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 5 | información | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 5 | información | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 6 | información | | | | | | |
+------+------+------+------+------+------+------+ ------+

Ahora, para cada paso, consideraremos la distancia entre cada punto en cuestión y la sumaremos con el mínimo
distancia que encontramos hasta ahora. Esto nos dará la distancia óptima de dos secuencias hasta esa posición. Nuestra fórmula
estarán,

Tabla[i][j] := d(i, j) + min(Tabla[i-1][j], Tabla[i-1][j-1], Tabla[i][j-1])

Para el primero, d(1, 1) = 0, Table[0][0] representa el mínimo. Entonces el valor de Table[1][1] será 0 + 0 = 0. Para
el segundo, d(1, 2) = 0. Table[1][1] representa el mínimo. El valor será: Table[1][2] = 0 + 0 = 0. Si
continúe de esta manera, después de terminar, la tabla se verá así:

+------+------+------+------+------+------+------+ ------+
| | 0| 1| 1| 2| 2|3| 5|
+------+------+------+------+------+------+------+ ------+
| 0| 0 | información | información | información | información | información | información |
+------+------+------+------+------+------+------+ ------+
| 1 | información | 0| 0| 1| 2|4|8|
+------+------+------+------+------+------+------+ ------+
| 2 | información | 1| 1| 0|0| 1|4|
+------+------+------+------+------+------+------+ ------+
| 3 | información | 3| 3| 1| 1|0| 2|
+------+------+------+------+------+------+------+ ------+
| 5 | información | 7| 7|4|4| 2|0|
+------+------+------+------+------+------+------+ ------+
| 5 | información | 11 | 11 | 7| 7|4|0|
+------+------+------+------+------+------+------+ ------+
| 5 | información | 15 | 15 | 10 | 10 | 6 | 0 |
+------+------+------+------+------+------+------+ ------+
| 6 | información | 20 | 20 | 14 | 14 | 9 | 1|
+------+------+------+------+------+------+------+ ------+

El valor en la Tabla [7] [6] representa la distancia máxima entre estas dos secuencias dadas. Aquí 1 representa
la distancia máxima entre la muestra y la prueba es 1.

Ahora, si retrocedemos desde el último punto, hasta el punto inicial (0, 0) , obtenemos una línea larga que
se mueve horizontal, vertical y diagonalmente. Nuestro procedimiento de backtracking será:

si Tabla[i-1][j-1] <= Tabla[i-1][j] y Tabla[i-1][j-1] <= Tabla[i][j-1]

GoalKicker.com – Notas de algoritmos para profesionales 239


Machine Translated by Google
yo := yo - 1
j := j - 1
de lo contrario, si Tabla[i-1][j] <= Tabla[i-1][j-1] y Tabla[i-1][j] <= Tabla[i][j-1]
yo := yo - 1
else
j := j - 1 fin si

Continuaremos esto hasta llegar a (0, 0). Cada movimiento tiene su propio significado:

Un movimiento horizontal representa la eliminación. Eso significa que nuestra secuencia de prueba se aceleró durante este intervalo.

Un movimiento vertical representa la inserción. Eso significa que la secuencia de prueba se desaceleró durante este intervalo.

Un movimiento diagonal representa coincidencia. Durante este período, la prueba y la muestra fueron las mismas.

Nuestro pseudocódigo será:

Procedimiento DTW(Muestra, Prueba): n :=


Muestra.longitud m := Prueba.longitud Crear
Tabla[n + 1][m + 1] para i de 1 a n Tabla[i]
[0] := infinito

final para
para i de 1 a m
Table[0][i] := extremo infinito para

Tabla[0][0] := 0 para i de
1 a n para j de 1 a m

Tabla[i][j] := d(Muestra[i], Prueba[j]) + mínimo(Tabla[i-1]


[j-1], //
Tabla[i][j-1], coincidencia //
Cuadro [i-1][j]) inserción // eliminación
fin para
fin para
Tabla de retorno [n + 1][m + 1]

También podemos agregar una restricción de localidad. Es decir, requerimos que si Sample[i] coincide con Test[j], entonces |i - j|
no es mayor que w, un parámetro de ventana.

GoalKicker.com – Notas de algoritmos para profesionales 240


Machine Translated by Google

Complejidad:

*
La complejidad de calcular DTW es O(m Las n) donde m y n representan la longitud de cada secuencia. Más rápido
técnicas para calcular DTW incluyen PrunedDTW, SparseDTW y FastDTW.

Aplicaciones:

Reconocimiento de palabras habladas

Análisis de poder de correlación

GoalKicker.com – Notas de algoritmos para profesionales 241


Machine Translated by Google

Capítulo 55: Transformada rápida de Fourier


La forma real y compleja de DFT (transformadas discretas de Fourier ) se puede utilizar para realizar análisis o síntesis de frecuencia para
cualquier señal discreta y periódica. La FFT (Fast Fourier Transform) es una implementación de la DFT que se puede realizar rápidamente en
las CPU modernas.

Sección 55.1: Radix 2 FFT


El método más simple y quizás mejor conocido para calcular la FFT es el algoritmo Radix-2 Decimation in Time.
El Radix-2 FFT funciona descomponiendo una señal de dominio de tiempo de N puntos en N señales de dominio de tiempo, cada una
compuesta por un solo punto

La descomposición de la señal, o 'diezmación en el tiempo', se logra invirtiendo los índices de la matriz de datos en el dominio del tiempo. Por
lo tanto, para una señal de dieciséis puntos, la muestra 1 (0001 binario) se intercambia con la muestra 8 (1000), la muestra 2 (0010) se
intercambia con 4 (0100) y así sucesivamente. El intercambio de muestras utilizando la técnica de inversión de bits se puede lograr
simplemente en software, pero limita el uso de Radix 2 FFT a señales de longitud N = 2^M.

El valor de una señal de 1 punto en el dominio del tiempo es igual a su valor en el dominio de la frecuencia, por lo que esta matriz de puntos
únicos descompuestos en el dominio del tiempo no requiere transformación para convertirse en una matriz de puntos en el dominio de la
frecuencia. Los N puntos individuales; sin embargo, deben reconstruirse en un espectro de frecuencia de N puntos. La reconstrucción
óptima del espectro de frecuencias completo se realiza mediante cálculos de mariposa. Cada etapa de reconstrucción en Radix-2 FFT realiza
una serie de mariposas de dos puntos, utilizando un conjunto similar de funciones de ponderación exponencial, Wn^R.

GoalKicker.com – Notas de algoritmos para profesionales 242


Machine Translated by Google

La FFT elimina los cálculos redundantes en la transformada discreta de Fourier al explotar la periodicidad de Wn^R.
La reconstrucción espectral se completa en etapas log2(N) de cálculos de mariposa dando X[K]; los datos reales e imaginarios en el dominio de la
frecuencia en forma rectangular. Para convertir a magnitud y fase (coordenadas polares) se requiere encontrar el valor absoluto, ÿ(Re2 + Im2), y el
argumento, tan-1(Im/Re).

El diagrama de flujo de mariposa completo para una Radix 2 FFT de ocho puntos se muestra a continuación. Tenga en cuenta que las señales de
entrada se han reordenado previamente de acuerdo con el procedimiento de diezmado en el tiempo descrito anteriormente.

GoalKicker.com – Notas de algoritmos para profesionales 243


Machine Translated by Google

La FFT normalmente opera con entradas complejas y produce una salida compleja. Para señales reales, la parte imaginaria puede establecerse
en cero y la parte real establecerse en la señal de entrada, x[n], sin embargo, son posibles muchas optimizaciones que involucran la transformación
de datos solo reales. Los valores de Wn^R utilizados a lo largo de la reconstrucción se pueden determinar utilizando la ecuación de ponderación
exponencial.

El valor de R (el poder de ponderación exponencial) se determina la etapa actual en la reconstrucción espectral y el cálculo actual dentro de una
mariposa en particular.

Ejemplo de código (C/C++)

El ejemplo de código AC/C++ para calcular la FFT de Radix 2 se puede encontrar a continuación. Esta es una implementación simple que
funciona para cualquier tamaño N donde N es una potencia de 2. Es aproximadamente 3 veces más lenta que la implementación FFTw más rápida,
pero sigue siendo una muy buena base para futuras optimizaciones o para aprender cómo funciona este algoritmo.

#incluir <matemáticas.h>

#define PI 3.1415926535897932384626433832795 #define TWOPI // PI para cálculos de seno/coseno


6.283185307179586476925286766559
0.017453292519943295769236907684886
#define Deg2Rad // // 2*PI para cálculos de seno/coseno
Degrees to Radians factor #define Rad2Deg 57.295779513082320876798154814105 // Radians to Degrees factor #define
log10_2 0.30102999566398119521373889472449 // Log10 of 2 #define log10_2_INV
1/Log10(2)
3.3219280948873623478703194294948 //

// estructura variable compleja (doble precisión) struct complex { public:


double Re, Im;

// No es tan complicado después de todo


};

// Devuelve verdadero si N es una potencia de 2 bool


isPwrTwo(int N, int *M) {

*M = (int)ceil(log10((doble)N) * log10_2_INV);// M es el número de etapas a realizar. 2^M = N int NN = (int)pow(2.0, *M);

GoalKicker.com – Notas de algoritmos para profesionales 244


Machine Translated by Google

if ((NN != N) || (NN == 0)) // Comprueba que N es una potencia de 2.


falso retorno;

devolver verdadero;
}

void rad2FFT(int N, complejo *x, complejo *DFT)


{
int M = 0;

// Comprobar si es potencia de dos. Si no, salga si (!


isPwrTwo(N, &M))
throw "Rad2FFT(): N debe ser una potencia de 2 para Radix FFT";

// Variables enteras

int BSep; int // BSep es espacio de memoria entre mariposas


BAncho; int P; // BWidth es el espacio de memoria de los extremos opuestos de la mariposa
intj ; etapa int = // P es el número de Wn similares que se usarán en esa etapa
1; (1 a M). // j se usa en un bucle para realizar todos los cálculos en cada etapa
// etapa es el número de etapa de la FFT. Hay M etapas en total

int HiIndex; // HiIndex es el índice de la matriz DFT para el valor superior de cada
cálculo de mariposa
int sin firmar iaddr; int ii; int // máscara de bits para inversión de bits
MM1 = M - 1; // Campo de bits entero para inversión de bits (Diezmado en el tiempo)

int sin firmar i;


int l;
int sin signo nMax = ( int sin signo) N;

// Variables de doble precisión


doble TwoPi_N = DOSPI / (doble)N; doble TwoPi_NP; // constante para ahorrar tiempo computacional. = 2*PI/N

// Variables complejas (Ver 'estructura compleja')


complejo WN; // Wn es la función de ponderación exponencial en la forma a + jb
complejo TEMP; // TEMP se usa para guardar el cálculo en el cálculo de mariposas
complejo *pDFT = DFT; // Puntero a los primeros elementos en la matriz DFT
complejo *pLo; // Puntero para el valor bajo/alto de los cálculos de mariposa
complejo *pHi;
complejo *pX; // Puntero a x[n]

// Decimación en el tiempo - x[n] clasificación de muestras


para (i = 0; i < nMáx; i++, DFT++)
{
pX = x + yo; // // Calcular el x[n] actual a partir de la dirección base *x y el índice i.
ii = 0; Restablecer nueva dirección para DFT[n]
iaddr = yo; // Copia i para manipulaciones
for (l = 0; l < M; l++) // Bit invertir i y almacenar en ii...
{
if (iaddr & 0x01) ii += (1 // Determinar el bit menos significativo
<< (MM1 - l)); // Incrementar ii en 2^(M-1-l) si lsb era 1
idir >>= 1; // desplazamiento a la derecha iaddr para probar el siguiente bit. uso lógico
operaciones de aumento de velocidad
si (!iaddr)
descanso;
}
DFT = pDFT + ii; índice // Calcular DFT actual[n] a partir de la dirección base *pDFT y bit
invertido ii

GoalKicker.com – Notas de algoritmos para profesionales 245


Machine Translated by Google

DFT->Re = pX->Re; // Actualice la matriz compleja con la señal de dominio de tiempo ordenada por dirección
x[n]
DFT->Im = pX->Im; // NB: lo imaginario siempre es cero
}

// Cálculo FFT por cálculo de mariposa


for (etapa = 1; etapa <= M; etapa++) // Bucle para M etapas, donde 2^M = N
{
BSep = (int)(pow(2, etapa)); // Separación entre mariposas = 2^etapa
P = N / BSep; // Wn similares en esta etapa = N/Bsep
BAncho = BSep / 2; // Ancho de mariposa (espacio entre puntos opuestos) = Separación /
2.

DosPi_NP = DosPi_N*P;

for (j = 0; j < BWidth; j++) // Bucle para j cálculos por mariposa


{
si (j != 0) { // Ahorre en el cálculo si R = 0, como WN^0 = (1 + j0)

//WN.Re = cos(TwoPi_NP*j)
WN.Re = cos(DosPi_N*P*j); // Calcular Wn (Real e Imaginario)
WN.Im = -sin(DosPi_N*P*j);
}

for (HiIndex = j; HiIndex < N; HiIndex += BSep) // Bucle para HiIndex Step BSep
mariposas por etapa
{
pHi = pDFT + índice alto; // Apunta a un valor más alto
pLo = pHi + BAncho; para // Apunta al valor más bajo (Nota: VC++ ajusta
el espaciado entre elementos)

si (j != 0) { // Si la potencia exponencial no es cero...

//CMult(pLo, &WN, &TEMP); // Realiza la multiplicación compleja de Lovalue


con Wn
TEMP.Re = (pLo->Re * WN.Re) - (pLo->Im * WN.Im);
TEMP.Im = (pLo->Re * WN.Im) + (pLo->Im * WN.Re);

//CSub(pHi, &TEMP, pLo);


pLo->Re = pHi->Re - TEMP.Re; pLo- // Encuentra nuevo Lovalue (resta compleja)
>Im = pHi->Im - TEMP.Im;

//CAdd (pHi, &TEMP, pHi); pHi- // Encuentra un nuevo Hivalue (suma compleja)
>Re = (pHi->Re + TEMP.Re);
pHi->Im = (pHi->Im + TEMP.Im);
}
más
{
TEMP.Re = pLo->Re;
TEMP.Im = pLo->Im;

//CSub(pHi, &TEMP, pLo);


pLo->Re = pHi->Re - TEMP.Re; pLo- // Encuentra nuevo Lovalue (resta compleja)
>Im = pHi->Im - TEMP.Im;

//CAdd (pHi, &TEMP, pHi); pHi- // Encuentra un nuevo Hivalue (suma compleja)
>Re = (pHi->Re + TEMP.Re);
pHi->Im = (pHi->Im + TEMP.Im);
}
}
}
}

GoalKicker.com – Notas de algoritmos para profesionales 246


Machine Translated by Google

pLo = 0; // Anular todos los punteros


pHi = 0;
pDFT = 0;
EPS = 0;
pX = 0;
}

Sección 55.2: Radix 2 FFT inversa


Debido a la fuerte dualidad de la transformada de Fourier, ajustar la salida de una transformada directa puede producir la FFT inversa. Los datos en el dominio de la

frecuencia se pueden convertir al dominio del tiempo mediante el siguiente método:

1. Encuentre el complejo conjugado de los datos del dominio de la frecuencia invirtiendo el componente imaginario para todos
instancias de k

2. Realice la FFT directa en los datos del dominio de frecuencia conjugada.

3. Divida cada salida del resultado de esta FFT por N para obtener el verdadero valor en el dominio del tiempo.

4. Encuentre el complejo conjugado de la salida invirtiendo el componente imaginario de los datos en el dominio del tiempo para
todas las instancias de n.

Nota: tanto los datos de dominio de frecuencia como de tiempo son variables complejas. Normalmente, el componente imaginario de la señal en el dominio del

tiempo que sigue a una FFT inversa es cero o se ignora como error de redondeo. El aumento de la precisión de las variables de flotante de 32 bits a doble de 64 bits

o doble largo de 128 bits reduce significativamente los errores de redondeo producidos por varias operaciones FFT consecutivas.

Ejemplo de código (C/C++)

#incluir <matemáticas.h>

#define PI 3.1415926535897932384626433832795 #define TWOPI // PI para cálculos de seno/coseno


6.283185307179586476925286766559
0.017453292519943295769236907684886
#define Deg2Rad // // 2*PI para cálculos de seno/coseno
Degrees to Radians factor #define Rad2Deg 57.295779513082320876798154814105 // Radians to Degrees factor #define
log10_2 0.30102999566398119521373889472449 // Log10 of 2 #define log10_2_INV
1/Log10(2)
3.3219280948873623478703194294948 //

// estructura variable compleja (doble precisión) struct complex { public:


double Re, Im;

// No es tan complicado después de todo


};

void rad2InverseFFT(int N, complejo *x, complejo *DFT) {

// M es el número de etapas a realizar. 2^M = N doble Mx =


(log10((doble)N) / log10((doble)2)); int a = (int)(ceil(pow(2.0, Mx))); estado
int = 0; if (a != N) // Comprueba que N es una potencia de 2 {

x = 0;
EPS = 0;
throw "rad2InverseFFT(): N debe ser una potencia de 2 para Radix 2 Inverse FFT";
}

complejo *pDFT = DFT; // Restablecer vector para punteros DFT


complejo *pX = x; doble NN // Restablece el vector para el puntero x[n]
= 1 / (doble)N; // Factor de escala para la FFT inversa

GoalKicker.com – Notas de algoritmos para profesionales 247


Machine Translated by Google

para (int i = 0; i < N; i++, DFT++)


DFT->Im *= -1; // Encuentra el conjugado complejo del espectro de frecuencia

DFT = pDFT; // Restablecer puntero de dominio de frecuencia


rad2FFT(N, DFT, x); // Calcular la FFT directa con variables conmutadas (tiempo y frecuencia)

ent yo;
complejo* x;
para ( i = 0, x = pX; i < N; i++, x++){
x->Re *= NN; x- // Divida el dominio del tiempo por N para la escala de amplitud correcta
>Im *= -1; // Cambiar el signo de ImX
}
}

GoalKicker.com – Notas de algoritmos para profesionales 248


Machine Translated by Google

Apéndice A: Pseudocódigo
Apartado A.1: Afectaciones variables
Podrías describir la afectación variable de diferentes maneras.

mecanografiado

int a = 1
int a := 1
sea int a = 1
en un < - 1

Sin tipo
a=1
a := 1
sea a = 1
un <- 1

Sección A.2: Funciones


Mientras el nombre de la función, la declaración de devolución y los parámetros sean claros, está bien.

incremento def
devolver n + 1

Sea incr(n) = n + 1

función incr (n)


devuelve n + 1

son todos bastante claros, por lo que puede usarlos. Trate de no ser ambiguo con una afectación variable

GoalKicker.com – Notas de algoritmos para profesionales 249


Machine Translated by Google

Créditos
Muchas gracias a todas las personas de Stack Overflow Documentation que ayudaron a proporcionar este contenido.
se pueden enviar más cambios a web@petercv.com para que se publique o actualice nuevo contenido

Abdul Karim Capítulo 1


afeldespato capitulo 43
Ahmad Faiyaz capitulo 28
Alberto Tadrous capitulo 53
Anagh Hegde Capítulos 29 y 39
andrii artamonov capitulo 27
AnukuL capitulo 40
Bajtiar Hasan Capítulos 9, 11, 14, 17, 19, 20, 22, 40, 41, 42, 47, 52 y 54
benson lin Capítulos 14, 39 y 44
Brijs capitulo 39
cris Capítulo 15
Juan creativo Capítulos 49 y 51
Dian Bakti Capítulo 10
Didgeridoo Capítulos 2 y 43
Dipesh Poudel capitulo 21
Dr. ABT capitulo 55
EsmaeelE Capítulos 2, 29, 30, 39 y 50
Filip Allberg Capítulos 1 y 9
ghilesZ capitulo 17
goeddek gran Capítulos 18 y 27
lobo Capítulo 5
Invisal de capitulo 29
Ijaz Khan capitulo 31
Isha Agarwal Capítulos 4, 5, 6, 7 y 8
Ishit Mehta Capítulo 5
IVlad Capítulos 16 y 28
Iwán capitulo 30
Janaky Murthy Capítulo 6
JJTO Julien Capítulo 9
Rousé Juxhin capitulo 24
Metaj Keyur Capítulos 2 y 30
Ramoliya Capítulos 23, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 45, 47, 48 y 50
Khaled.K capitulo 39
kiner_shah Capítulo 12
lambda capitulo 38
luv agarwal capitulo 30
Lymphatus capitulo 31
MS Hossain capitulo 17
Malav capitulo 33
malcolm mclean Capítulos 4 y 39
Martín franco capitulo 21
mehedi hasan Capítulo 5
miljen mikic Capítulos 2, 28 y 39
Minhas Kamal Capítulos 12 y 46
mnoronha Capítulos 23, 29, 31, 32, 33, 34, 35, 36 y 45
mensaje capitulo 39
Nick Larsen Capitulo 2

GoalKicker.com – Notas de algoritmos para profesionales 250


Machine Translated by Google
Nick el codificador Capítulo 3
optimistanoop Capítulos 29 y 33
pedro k Capitulo 2
Rashik Hasnat capitulo 40
roberto fernandez Capítulo 12
Samgak capitulo 29
samuel pedro Capítulo 3
santiagogil capitulo 30
Sayakiss Capítulos 9 y 14
SHARMA capitulo 30
ShreePiscina capitulo 39
Shubham capitulo 16
sumeet singh Capítulos 20 y 41
TajyMuchos Capítulos 12 y 13
Tejus Prasad Capítulos 2, 5, 9, 11, 18, 19 y 45
theJollySin capitulo 17
umop apisdn capitulo 39
Usuario0911 capitulo 29
usuario23013 Capítulo 9
VermillionAzure Capítulos 4 y 9
Vishwas Capítulos 14, 25 y 26
WitVault Capítulo 3
xenteros Capítulos 17, 29 y 39
Yair Twito Capitulo 2
yd1 Capítulo 4
Yerken Capítulos 16 y 20
JovenHobbit capitulo 29

GoalKicker.com – Notas de algoritmos para profesionales


251
Machine Translated by Google

También te puede interesar

También podría gustarte