AlgorithmsNotesForProfessionals (Español)
AlgorithmsNotesForProfessionals (Español)
AlgorithmsNotesForProfessionals (Español)
Algoritmos
Contenido
Sobre .................................................... .................................................... .................................................... ............................. 1
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
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
Sección 8.1: Encontrar el ancestro común más bajo .................................................... .................................................... ..... 29
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
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
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 25: Algoritmo de Knuth Morris Pratt (KMP) ........................................... ............................................. 131
Sección 25.1: Ejemplo de KMP ............................................. .................................................... ........................................ 131
por combinación Sección 30.2: Implementación de la ordenación por .................................................... .................................................... 150
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
Sección 34.1: Información básica de clasificación por .................................................... .................................................... ... 162
Sección 39.3: Análisis de búsqueda lineal (peor, promedio y mejores casos) .................................. ..................... 176
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
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
Sección 47.1: Explicación de la subsecuencia común más larga ........................................... .......................................... 220
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 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
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
Sobre
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.
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
Problema: Clasificació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 }.
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.
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,
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:
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.
}
}
¡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.
}
}
¡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.
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!
}
}
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.
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
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.
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!
ÿ(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:
La definición es igual a:
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))
Lineal 10 100
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
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
Enlaces
Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein. Introducción a los Algoritmos.
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:
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)).
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)).
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".
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.
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.
} retorno máximo;
}
Estas dos asignaciones se realizan solo una vez, por lo que son 2 operaciones. Las operaciones que se repiten son:
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!
}
}
} 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 .
}
}
} 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.
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.
2. Pero sabemos que en el k-ésimo paso, el tamaño de nuestro problema debería ser:
3. De 1 y 2:
n/2k = 1 o
norte = 2k
loge n = k loge2
k = loge n / loge 2
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)
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=8
Otro muy buen ejemplo para un caso similar es el algoritmo de búsqueda binaria.
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; }
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
un = 0
b = n-1
mientras que es cierto:
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".
Conclusión
Ante divisiones sucesivas (ya sea por dos o por cualquier número), recuerda que la complejidad es logarítmica.
Capítulo 4: Árboles
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;
}
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;
}
}
}
printtree_r(raíz, 0);
}
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.
Ejemplo 1
a)
b)
Ejemplo:2
a)
b)
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 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)
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.
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.
nodo de estructura
{
datos int ;
nodo *izquierda, *derecha;
};
más
{
if(root->left == nullptr && root->right == nullptr) // Caso 1 {
libre (raíz);
raíz = punto nulo;
} // Caso 3
más {
nodo* temp = root->right;
} devuelve la raíz;
}
La complejidad temporal del código anterior es O(h), donde h es la altura del árbol.
Considere el BST:
La propiedad del árbol de búsqueda binaria se puede usar para encontrar el ancestro más bajo de los nodos
Pseudocódigo:
si (raíz == NULL)
devuelve NULL;
devolver raíz;
}
else
{ return lowerCommonAncestor(root->right, node1, node2);
}
}
devolver la raíz
volver Ninguno
más:
self.in_order_place(root.l_child) print root.val
self.in_order_place(root.r_child)
print root.val
self.pre_order_place(root.l_child)
self.pre_order_place(root.r_child)
volver Ninguno
más:
self.post_order_place(root.l_child)
self.post_order_place(root.r_child) print root.val
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))
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) .
is_BST(raíz): si
raíz == NULL:
volver verdadero
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(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
si la entrada es:
Si la entrada es:
1234567
Código:
#include<iostream>
#include<cola>
#include<malloc.h>
nodo de estructura{
datos int ;
nodo *izquierda;
nodo *derecho;
};
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();
retorno (nodo);
}
int principal(){
devolver 0;
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.
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.
nodo.
4251637
El recorrido posterior al pedido (raíz) atraviesa el subárbol izquierdo del nodo, luego el subárbol derecho y luego el nodo.
4526731
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}.
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
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
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.
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.
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.
importar java.util.Scanner;
public Represent_Graph_Adjacency_Matrix(int v) {
vértices = v;
matriz_adyacencia = new int[vértices + 1][vértices + 1];
}
prueba
matriz_adyacencia[a][desde] = borde;
prueba
return matriz_adyacencia[a][desde];
} devuelve -1;
}
a = sc.nextInt(); desde
= sc.nextInt();
} atrapar (Excepción E) {
sc.cerrar();
}
}
Ejemplo:
$ java Represent_Graph_Adjacency_Matrix
Introduzca el número de vértices:
4
Introduzca el número de aristas: 6
30001
40000
¿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.
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.
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.
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.
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.
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
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.
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
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.
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á:
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:
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.
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.
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.
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:
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));
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>
#define NUM_V 4
visitado[u]=verdadero;
recStack[u]=verdadero;
lista<int>::iterador i; for(i =
gráfico[u].begin();i!=gráfico[u].end();++i) {
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. */
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;
} 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.
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
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).
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.
// establecer como visitado para evitar visitar el mismo nodo dos veces
(*visitado)[nodo] = true;
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,
Luego actualizamos,
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
en cola y en lugar de actualizarlo con visitado, relajamos o actualizamos el nuevo borde. Veamos un ejemplo:
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:
terminar mientras
Distancia de retorno
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.
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
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:
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
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
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
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.*;
min = dist[v];
índice_min = v;
}
devuelve min_index;
}
dist[i] = Integer.MAX_VALUE;
sptSet[i] = falso;
}
dist[origen] = 0;
sptSet[u] = verdadero;
imprimirSolucion(dist, V);
}
t.dijkstra(gráfico, 0);
}
}
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.
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*
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:
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)
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
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":
Hemos calculado los valores g, h y f para todos los nodos azules. Ahora bien, ¿cuál elegimos?
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:
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:
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:
Una vez más, calculemos las heurísticas para el vecino del nodo:
Finalmente, podemos ver que tenemos una casilla ganadora a nuestro lado, así que nos movemos allí y terminamos.
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
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
f(n) = g(n) + h(n), donde g(n) es el costo requerido para alcanzar el estado actual desde
estado
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 +
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.
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.
13 _ 123
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:
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.
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.
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
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)
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
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":
Hemos calculado los valores g, h y f para todos los nodos azules. Ahora bien, ¿cuál elegimos?
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:
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:
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:
Una vez más, calculemos las heurísticas para el vecino del nodo:
Finalmente, podemos ver que tenemos una casilla ganadora a nuestro lado, así que nos movemos allí y terminamos.
Implementación en Java
}
}
} return dp[str1.longitud()][str2.longitud()];
}
Producción
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?
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 |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
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:
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 |
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 |
+---------------------------------------+---------+---------+--- ------+---------+---------+---------+
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]
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í:
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.
Ejemplo
Implementación en Java
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
Árbol recursivo
mentira(5)
\
/ fib(4) \ mentira(3) /
/ \
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
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];
}
En)
Ejemplos
Implementación en Java
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;
}
O(m*n)
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.
Aquí hay un árbol recursivo de ejemplo para fibonacci(4), tenga en cuenta los cálculos repetidos:
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]
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
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
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
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.
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;
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.
si vRoot == uRoot:
devolver
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:
si vRoot == uRoot:
devolver
si aleatorio() % 2 == 0:
vRoot.parent = uRoot más:
En la práctica, este algoritmo aleatorio junto con la compresión de ruta para la operación findSet dará como resultado
devuelve findSet(v.parent)
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.
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.
+------------------------+-----+-----+-----+-----+ -----+-----+
| 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?
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:
El pseudocódigo se parece a:
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
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
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.
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
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
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
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.
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
2. Si la actividad a realizar no comparte un tiempo común con las actividades que anteriormente
realizado, realizar la actividad.
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
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. .
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.
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).
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.
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 :
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 :
caché 2 bbbbbbaaaaaaffff
caché 3 ccccccccfddddbbb
error de caché XX xxx xxx
El esqueleto es una aplicación que resuelve el problema dependiendo de la estrategia codiciosa elegida:
#incluir <iostream>
#include <memoria>
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;
// 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 };
// restablecer
caché para (int i=0; i < cacheSize; ++i) cache[i] = originalCache[i];
int cntMisses = 0;
" "
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;
}
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é.
FIFO
FIFO() : Estrategia("FIFO") {
int mayor = 0;
si (!cacheMiss)
devolver;
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
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
LIFO
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
LRU
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
Estrategia: LRU
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
F a F mi X
b a f X
mi mi X
C mi C bbb X
LFU
volver menos;
}
más
++requestFrequency[cachePos];
}
privado:
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
Estrategia: LFU
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
LFD
privado:
volver solicitudLongitud + 1;
}
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
más lejano en el futuro. La solución del programa es igual a la solución a mano de arriba:
Estrategia: LFD
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
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.
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.
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.
#include <iostream>
#include <vector>
#include <cadena>
#include <algoritmo>
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
diffValue -= *monedValue;
contarMonedas++;
}
coinCount.push_back(countMonedas);
}
devolver 0;
}
// valores de monedas
std::vector<unsigned int> coinValues;
// lee los valores de las monedas (atención: se omite el manejo de errores) while(true) {
int valormoneda;
if(coinValue > 0)
coinValues.push_back(coinValue);
de lo
contrario romper;
// ordenar valores
sort(coinValues.begin(), coinValues.end(), std::greater<int>());
// imprime la matriz
cout << "Valores de monedas: ";
Tenga en cuenta que ahora hay verificación de entrada para mantener el ejemplo simple. Un ejemplo de salida:
Mientras 1 esté en los valores de la moneda, sabemos que el algoritmo terminará porque:
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.
El objetivo es encontrar el subconjunto máximo de trabajos compatibles entre sí. Hay varios enfoques codiciosos para este problema:
La pregunta ahora es, qué enfoque es realmente exitoso. Hora de inicio temprano definitivamente no, aquí hay un contraejemplo
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:
#include <iostream>
#include <utilidad>
#include <tupla> #include
<vector> #include
<algoritmo>
// Horas de finalización
del trabajo const int endTimes[] = { 4, 4, 3, 5, 5, 5, 8, 9, 9, 10};
int principal()
{
vector<par<int,int>> trabajos;
// paso 1: sort
sort(jobs.begin(), jobs.end(),[](pair<int,int> p1, pair<int,int> p2) { return p1.segundo <
p2.segundo; } );
// paso 3:
for(int i=0; i<jobCnt; ++i) {
{
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?
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.
importar java.util.Arrays;
importar java.util.Comparator;
trabajo de clase
{
int inicio, fin, beneficio;
this.inicio = inicio;
this.terminar = terminar;
this.profit = beneficio;
}
}
} más
hola = medio - 1;
}
devolver -1;
}
int n = trabajos.longitud;
int tabla[] = new int[n]; tabla[0] =
trabajos[0].beneficio;
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));
}
}
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
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
12
tj 1 5
DJ 3 5
3. para j=1 a n
Asignar trabajo j al intervalo [t,t+tj]
#include <iostream>
#include <utilidad>
#include <tupla> #include
<vector> #include
<algoritmo>
// Horas de finalización
del trabajo const int dueTimes[] = { 4, 7, 9, 13, 8, 17, 9, 11, 22, 25};
int principal()
{
vector<par<int,int>> trabajos;
// paso 1: sort
sort(jobs.begin(), jobs.end(),[](pair<int,int> p1, pair<int,int> p2) { return p1.segundo <
p2.segundo; } );
// paso 3:
vector<par<int,int>> jobIntervals;
jobIntervals.push_back(make_pair(t,t+jobs[i].first)); t += trabajos[i].primero;
retraso int = 0;
cout << "(" << par.primero << "," << par.segundo << ") "
<< "Retraso: " << par.segundos-trabajos[i].segundo << std::endl;
}
"
cout << "\nla tardanza máxima es << retraso << endl;
devolver 0;
}
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:
Eso significa que el retraso después del intercambio es menor o igual que antes. Esto concluye la prueba.
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.
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:
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
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
Si seguimos así, seleccionaremos la arista 8-6, 6-7 y 4-3. Nuestro subgrafo se verá así:
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.
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
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.
importar java.util.*;
i, j ; NNodes
= mat.length; LinkCost =
new int[NNodes][NNodes]; for ( i=0; i < NNodos;
i++) {
LinkCost [ i ] [ j ] = mat [ i ] [ j ] ; if
( Costo del enlace [ i ] [ j ] == 0 )
LinkCost [ i ] [ j ] = infinito ;
}
booleano hecho =
verdadero ; for ( int i = 0 ; i < r.length ; i++ ) if ( r
[ i ] == false ) return i ; retorno - 1 ;
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++)
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 );
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 ( ) ;
}
}
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
1 --> 7
0 --> 8
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:
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
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.
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.
+--------+--------+--------+--------+--------+---- ----+--------+
| 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:
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 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á:
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á:
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 .
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.
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.
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:
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
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
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
+--------+--------+--------+--------+
| Serie | 1 | 2 | 3 |
+--------+--------+--------+--------+
| Borde | 3->4 | 2->3 | 1->2 |
+--------+--------+--------+--------+
Podemos ver que nuestro proceso de relajación solo cambió d[2]. Nuestro gráfico se verá así:
Segunda iteración:
Esta vez el proceso de relajación cambió d[3]. Nuestro gráfico se verá así:
Tercera iteración:
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,
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:
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.
se dirige un dispositivo de salida para llenar estas posiciones entre los puntos finales.
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.
Se incrementa el valor de y O se
3. Calcular
Delx =| x2 – x1 |
Retraso = | y2 – y1 |
dely – delx
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:
int main()
{ int
gdriver=DETECT,gmode; int
x1,y1,x2,y2,delx,dely,p,i;
initgraph(&gdriver,&gmode,"c:\\TC\\BGI");
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; }
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)
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:
#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; }
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.
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,
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:
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:
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| |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á:
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á:
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
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²).
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?"
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.
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)
p-combinar(T,p1,r1,p2,r2,A,p3) n1 =
r1-p1+1 n2 = r2-p2+1 si n1<n2
// comprueba si n1>=n2
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
volver a cenar
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
System.out.println(obj.patternExistKMP(str.toCharArray(), pattern.toCharArray()));
}
lps[0] = 0; int
j = 0; for(int i
=1;i<str.longitud;i++){ if(str[j] == str[i])
{ lps[i] = j+1; j++;
yo+
+; }
else{ if(j!=0)
{ j = lps[j-1]; }si
no{ lps[i] = j+1; yo+
+;
}
}
volver lps;
}
}
}
}
if(j==pat.length)
devuelve
verdadero; devolver falso;
}
1. Insertar
2. Eliminar
3. Reemplazar
Por ejemplo
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 | | | | | | |
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 | | | | | | |
+---+---+---+---+---+---+---+
(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
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()];
}
O(n^2)
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
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 .
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.
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
Si no hay una constante r para la cual un algoritmo en línea A sea r-competitivo, llamamos A no competitivo.
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
Otras lecturas
Código fuente
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.
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.
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.
// 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;
devolver 0;
}
El código fuente completo está disponible aquí. Si reutilizamos el ejemplo del tema, obtenemos el siguiente resultado:
Estrategia: FWF
C C X X X
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
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
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.
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.
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
¿No hay una constante r para la cual un algoritmo en línea A sea r-competitivo, llamamos A no competitivo?
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.
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.
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).
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.
La ordenación inestable puede generar el mismo resultado que la ordenación estable, pero no siempre.
Tipo de inserción
Clasificación de raíz
ordenar tim
Ordenamiento de burbuja
Ordenar montones
Ordenación rápida
Parámetro Descripción
Estable Sí
En su lugar Sí
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:
void bubbleSort(vector<int>numbers) {
}
}
}
}
Implementación C
largo c, d, t;
/ * Intercambio */
t = lista[d];
lista[d] = lista[d+1]; lista[d+1]
= t;
}
}
}
}
largo c, d, t;
/ * Intercambio */
t = * (lista + d ); * (lista
+ d ) = * (lista + d + 1 +); 1)
* (lista
= t; + d
}
}
}
}
que se va a clasificar, compara cada par de elementos adyacentes y los intercambia si están en el orden incorrecto.
}
}
}
SortBubble(entrada);
entrada de retorno ;
}
}
lista_entrada = [10,1,2,11]
imprimir lista_entrada
public static void bubble_srt(int array[]) {// main logic int n = array.length;
intk ; para (int m = n; m >= 0; m--) {
swapNumbers(i, k, matriz);
}
} imprimirNúmeros(matriz);
}
}
temperatura
interna ; temperatura
= matriz[i]; matriz[i] =
matriz[j]; matriz[j] = temporal;
}
}
}
} } while (intercambiado);
}
var a = [3, 203, 34, 746, 200, 984, 198, 764, 9];
ordenarburbujas(a);
consola.log(a); //registros [3, 9, 34, 198, 200, 203, 746, 764, 984]
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:
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.
Estable: Sí
paquete principal
importar "fmt"
} m := (largo(a)) / 2
f := mergeSort(a[:m]) s :=
mergeSort(a[m:])
yo ++
}
}
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))
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 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#
i, j ; var n1
= m - l + 1; var n2 = r - m;
yo = 0;
j = 0;
var k = l;
entrada[k] = izquierda[i];
yo++;
}
más {
entrada[k] = derecha[j]; j++;
} k++;
}
entrada[k] = derecha[j]; j+
+; k++;
}
}
si (l < r) {
int m = l + (r - l) / 2;
OrdenarCombinar(entrada, l, m);
OrdenarCombinar(entrada, m + 1, r);
Combinar (entrada, l, m, r);
}
}
A continuación se muestra la implementación en Java utilizando un enfoque genérico. Es el mismo algoritmo, que se presenta
arriba.
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);
}
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++) {
}
} else if (j > alto && i <= medio) { b[k] = a[i++]; }
else if (i > mid && j <= high) { b[k] = a[j++];
}
}
}}}
def mergeSort(A): if
len(A) <= 1: devuelve
A if len(A) ==
2: devuelve
ordenado(A)
si __nombre__ == "__principal__":
# Genera 20 números aleatorios y ordénalos
A = [randint(1, 100) for i in xrange(20)] print mergeSort(A)
pública MergeSortBU() { }
combinación de vacío estático privado (Comparable [] arrayToSort, Comparable [] aux, int lo, int mid, int hi) {
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);
}
}
}
System.out.println(búfer);
}
}
}
cubeta[i - minValue].Add(i);
}
if (b.Cuenta > 0) {
foreach (int t en b) {
entrada[k] = t;
k++;
}
}
}
}
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.
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.
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.
Partición:
yo := yo + 1
hacer:
j := j - 1
mientras que A[j] > pivote hacer
si i >= j entonces
devuelve j
ordenación rápida del vacío estático público (int [] ar, int bajo, int alto) {
si (bajo<alto) {
devolver yo;
}
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.
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
para x en la
entrada: salida[cuenta[tecla(x)]]
= x cuenta[tecla(x)] += 1 salida
de retorno
si (mayor != i) {
SortHeap(entrada, entrada.Longitud);
entrada de retorno ;
}
}
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.
heapify(a,count) end
<- count - 1 while
end -> 0 do
swap(a[end],a[0]) end<-
end-1 restore(a, 0, end)
regreso salida
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.
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]
Implementación:
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 ;
}
}
SortOddEven(entrada, entrada.Longitud);
entrada de retorno ;
}
}
Selección.sort([100,4,10,6,9,3])
|> IO.inspeccionar
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. .
entrada[minId] = entrada[i];
entrada[i] = temperatura;
}
}
SortSelection(entrada, entrada.Longitud);
entrada de retorno ;
}
}
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.
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]).
if (x == a[mid]) { return
(mid); } else if (x
< a[mid]) { binsearch(a,
x, low, mid - 1); } else
{ binsearch(a, x, mid + 1, high);
}
}
// busca el patrón
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)
} 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.
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.
2. Caso promedio
3. Mejor Caso
#incluir <stdio.h>
// 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;
}
int principal()
{
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;
}
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
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
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
bajo = 0;
alto = N -1;
mientras (bajo < alto) {
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:
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.
¿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.
+-------+---+---+---+---+---+---+---+---+
| Í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.
+-------+--+--+--+--+--+--+--+--+--+--+--+--+--+-- +--+--+--+--+--+--+--+--+--+
| Í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 |
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
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
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.
+---------+---+---+---+---+---+---+---+---+---+--- +---+---+
| Í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 |
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í:
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.
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.
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:
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:
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:
¡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:
Hachís de retorno
Recálculo de hash:
Coincidencia de cadenas:
Devolver verdadero
Rabin Karp:
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).
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
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.
j += 1
delimitador += 1
return prefix_table
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
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)
Ejemplos:
Aporte:
producción:
Aporte:
producción:
#incluir<cadena.h>
#incluir<stdlib.h>
if (pat[j] == txt[i]) {
j++;
yo++;
}
si (j == M) {
yo = yo+1;
}
largo int = 0; // longitud del sufijo de prefijo más largo anterior int i;
if (pat[i] == pat[len]) {
len++;
lps[i] = largo; yo+
+;
si (largo ! = 0) {
} más // si (len == 0) {
lps[i] = 0; yo+
+;
}
}
}
}
Producción:
Referencia:
http://www.geeksforgeeks.org/searching-for-patterns-set-2-kmp-algorithm/
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.
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.
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.
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:
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.
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|
+-----+ +-----+ +-----+
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á:
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á:
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.
nivel serán arreglos 2D. Para cada nodo, consideraremos todos los movimientos posibles. Para encontrar la distancia a un nodo específico, también
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.
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.
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)
terminara si
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.
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.
boolean isConnected(Graph g)
{ BFS(v)//v es un nodo fuente aleatorio.
si (todos los visitados (g)) {
devolver
verdadero; }
más devuelve falso; }
#include<stdio.h>
#include<stdlib.h> #define
MAXVERTICES 100
intv ;
nodo de estructura *siguiente;
};
int principal()
{
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
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");
}
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() {
si (Qfrente == NULL) {
} 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;
}
}
ent yo;
int i,vértice;
visitado[v] = 'Y'; poner
en cola (v); while((vértice
= deque()) != -1) {
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.
ent yo;
for(i = 0;i < noOfVertices;++i) {
if(visitado[i] == 'N')
BFS(gráfico,i,nºDeVertices);
}
}
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).
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
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,
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).
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.
Búsqueda de caminos.
Clasificación topológica.
booleano
SByte
RuntimeHelpers.GetHashCode(esto);
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:
C;
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>
Formación
intret = 0 ; for
(int i = (Longitud >= 8 ? Longitud - 8 : 0); i < Longitud; i++) {
Referencias
funciones hash es determinista. h(x) siempre debe devolver el mismo valor para una x dada
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).
Funciones hash utilizadas en tablas hash para calcular el índice en una matriz de ranuras. La tabla hash es una estructura de datos para
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
Los siguientes métodos se utilizan para calcular las secuencias de sonda requeridas para el direccionamiento abierto
Método Fórmula
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.
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.
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
mínimo de salida
¡ 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.
(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.
Esta línea puede ser un poco confusa, así que repasemos lentamente:
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.
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.
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:
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))
$ 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.
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 {
k[i, b] = k[i - 1, b];
}
}
int n = valores.Longitud;
return Mochila(nItems, pesos, valores, n);
}
}
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-
i, j ; while(!
rootFound){ for(i=0; i<n;
i++){ Nx[i]=b[i]; // cálculo
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;
}
}
}
}
volver ;
}
i, j ; para(i=0;
i<n; i++){ // inicialización
Nx[i]=x[i];
}
while(!rootFound){ for(i=0;
i<n; i++){ Nx[i]=b[i]; // cálculo
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;
}
}
}
}
volver ;
}
} printf("\n\n");
volver ;
}
int main(){ //
inicialización de la ecuación //
número de variables int n=3;
// 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;
}
JacobisMethod(n, x, b, a); imprimir
(n, x);
x[i]=0;
}
GaussSeidalMethod(n, x, b, a); imprimir
(n, x);
devolver 0;
}
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
Implementación en C:
/ **
* Toma dos valores iniciales y acorta la distancia por ambos lados. **/ double
BisectionMethod(){ double root=0;
int bucleContador=0;
if(f(a)*f(b) < 0){ while(1)
{ loopCounter++;
c=(a+b)/2;
a=c;
}
devolver raíz;
}
/ **
* Toma dos valores iniciales y acorta la distancia por un solo lado. **/ double FalsePosition()
{ double root=0;
int bucleContador=0;
if(f(a)*f(b) < 0){ while(1)
{ 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+
+;
x1=x2;
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+
+;
x=g(x);
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+
+;
x2 = ((x0*f(x1))-(x1*f(x0))) / (f(x1)-f(x0));
x0=x1;
x1=x2;
devolver raíz;
}
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;
}
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.
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.
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.
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 | | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
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
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| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
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| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
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|
+-----+-----+-----+-----+-----+-----+-----+-----+
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:
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.
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.
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:
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
m ÿ yo
terminar si
terminar para
Parte 2:
poner
un tiempo A(m) > 1 do i ÿ
mÿ1
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
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], ÿ)
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])
Algoritmo iterativo:
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
MAIN(A[1..n]): L =
LIS(A[1..n]) devuelve
el valor máximo en L
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}.
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.
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.
En nuestro caso, todos los valores son cero, por lo que es un anagrama.
// 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);
}
});
}
if(hashMap[i] !== 0)
{ isAnagram = false;
descanso; } else
{ isAnagram = true;
}
}
volver esAnagrama;
}
}
})();
imprimirf(" "); +
+contar;
}
while(k != 2*i-1) {
}
más {
++cuenta1;
printf("%d ", (i+k-2*cuenta1));
} ++ k;
} cuenta1 = cuenta = k = 0;
imprimirf("\n");
}
Producción
1232
34543
456765456
7898765
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
}
}
impresión cuadrada(6,4);
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.
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
[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].
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].
|1 1 | X | f(n) | = | f(n+1) |
|1 0| | f(n-1) | | f(n) |
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:
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
estado en B.
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:
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:
En breve:
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:
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.
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.
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)
X <- G.getAllVerticiesArrangedDescendinglyByDegree()
para v en x hacer
List<Vértice> vértices adyacentes1 <- G.getAdjacent(v)
C.añadir(v)
para el vértice en C do
C.remove(vértice)
volver C
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)
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}
Primero, definimos la distancia entre dos puntos, d(x, y) donde x e y representan los dos puntos. Dejar,
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,
+------+------+------+------+------+------+------+ ------+
| | 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,
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á:
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.
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
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.
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:
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.
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.
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.
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>
devolver verdadero;
}
// Variables enteras
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)
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
}
DosPi_NP = DosPi_N*P;
//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)
//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;
//CAdd (pHi, &TEMP, pHi); pHi- // Encuentra un nuevo Hivalue (suma compleja)
>Re = (pHi->Re + TEMP.Re);
pHi->Im = (pHi->Im + TEMP.Im);
}
}
}
}
1. Encuentre el complejo conjugado de los datos del dominio de la frecuencia invirtiendo el componente imaginario para todos
instancias de k
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.
#incluir <matemáticas.h>
x = 0;
EPS = 0;
throw "rad2InverseFFT(): N debe ser una potencia de 2 para Radix 2 Inverse FFT";
}
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
}
}
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
incremento def
devolver n + 1
Sea incr(n) = n + 1
son todos bastante claros, por lo que puede usarlos. Trate de no ser ambiguo con una afectación variable
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