220726-Ada-Notes 1 5
220726-Ada-Notes 1 5
Camilo Rocha
Email address: [email protected]
Para Laura.
© Derechos de autor 2019-2022 Camilo Rocha.
Última actualización 30 de octubre de 2022.
Versión 0.0
cbna
Esta obra está bajo una licencia de Creative Commons
Reconocimiento-NoComercial-CompartirIgual 4.0 Internacional.
Este trabajo puede ser copiado y distribuı́do libremente, como copia electrónica o en
papel. No puede ser vendido por un valor mayor a su costo actual de reproducción,
almacenamiento o transmisión.
Índice general
Capı́tulo 0. Preliminares 1
0.1. Arreglos: notación y convenciones 1
0.2. Problemas algorı́tmicos y su especificación 4
0.3. El concepto de algoritmo 11
0.4. Especificación de algoritmos 16
Notas del capı́tulo y referencias 18
7
8 Índice general
Bibliografı́a 161
Preliminares
Definición 0.1.1
1
2 0. Preliminares
Nota 0.1.1
Se usa una notación similar a la de rangos en los números reales, con paréntesis
circulares ( ) y llaves cuadradas [ ], para especificar arreglos y definir opera-
ciones (de indexación y proyección) sobre ellos.
La expresión A[0..N ) indica que A es un arreglo de N elementos con
ı́ndices en el rango 0..N − 1.
Con A[0..N ) y n un número natural, la indexación A[n] denota el valor
de A en la posición n siempre y cuando 0 ≤ n < N . Si n ≥ N , entonces
la expresión es un error.
Por ejemplo, con A = [1, 0, 25, −1, 8] se tiene A[3] = −1 y A[6] es un
error.
Un arreglo sin ı́ndices se llama vacı́o.
Con A[0..N ) y a, b números naturales, la expresión A[a..b) denota la sec-
ción (o el subarreglo) de A, la cual es también un arreglo que:
• cuando 0 ≤ a < b ≤ N , re-indexa desde 0 y hasta b − (a + 1),
respetando su orden, los elementos de A entre a y b − 1 (i.e., b no se
incluye), y
• de lo contrario, el arreglo vacı́o.
Por ejemplo, A[1..3) = [0, 25] con A = [1, 0, 25, −1, 8]; A[2..2) denota
el arreglo vacı́o.
El tamaño (o, equivalentemente, la cantidad de ı́ndices) de un arreglo o
una sección de un arreglo se denota como | |.
Nota 0.1.2
0.1. Arreglos: notación y convenciones 3
Ejercicios
2. Investigue sobre árboles binarios casi llenos (e.g., aquellos que se utilizan en
la implementación de HeapSort como implementación de colas de prioridad).
Explique brévemente cómo representar un árbol binario casi lleno (e.g., de
números) en un arreglo. ¿Qué relación hay entre los ı́ndices del arreglo y la
profundidad de los elementos del árbol?
Nota 0.2.1
Definición 0.2.1
Nota 0.2.2
Entrada: ...
Salida: ...
Ejemplo 0.2.1
Definición 0.2.2
Definición 0.2.3
Ejemplo 0.2.2
Ejemplo 0.2.3
Definición 0.2.4
Ejemplo 0.2.4
Vale la pena llamar la atención del lector acerca de un detalle que puede ser
importante en la Definición 0.2.4. Allı́ se hace explı́cito el hecho de que pueden
existir varias soluciones diferentes para una misma instancia de un problema al-
gorı́tmico dado y, por ende, para un problema algorı́tmico dado. A continuación se
presenta un problema algorı́tmico en el cual su única instancia puede tener más de
una solución.
8 0. Preliminares
Ejemplo 0.2.5
Ejemplo 0.2.6
Ejemplo 0.2.7
Ejercicios
b) El Teorema de Fermat.
c) La Conjetura de Goldbach.
d) El problema de satsifacibilidad proposicional.
e) El problema de la parada.
7. Clasifique los problemas indicados en el Ejercicio 1 de acuerdo a si tienen ins-
tancias con solución única o no. Ilustre su respuesta con ejemplos.
8. Clasifique los problemas indicados en el Ejercicio 6 de acuerdo a si tienen ins-
tancias con solución única o no. Ilustre su respuesta con ejemplos.
9. ¿Cuántas soluciones tiene el problema de las 8 reinas en el Ejemplo 0.2.5?
Investigue y formule tres soluciones.
10. Considere la siguiente problema:
De acuerdo con Donald Knuth, uno de los cientı́ficos más destacados y prolı́ficos
de la informática, los algoritmos son los hilos que permiten relacionar y asociar
diferentes disciplinas de las ciencias de la computación. Dada su importancia, definir
qué se entiende por algoritmo ha sido una de las primeras tareas abordadas desde
las matemáticas y desde las ciencias de la computación. Esta sección presenta una
definición de algoritmo, los asocia a problemas algorı́tmicos e identifica algunas de
sus propiedades principales.
12 0. Preliminares
Nota 0.3.1
Definición 0.3.1
Nota 0.3.2
Definición 0.3.2
Definición 0.3.3
Ejemplo 0.3.1
Ejercicios
Nota 0.4.1
los pasos de un algoritmo se describen con frases de palabras, que algunas veces
contienen fórmulas. A pesar de ser una opción muy intuitiva, se ha de propender
por ser preciso con las palabras porque de lo contrario describir un algoritmo podrı́a
fácilmente resultar en pasos ambigüos, como usualmente sucede en las recetas de
cocina. Usar pseudo-código o código puede ayudar a eliminar fuentes de ambigüea-
dad gracias a la sintaxis técnica de los lenguajes de programación, como se explicó
anteriormente. De esta forma, un algoritmo es o está muy cerca de ser un progra-
ma de computador. La relativa desventaja con esta opción es que la utilidad del
algoritmo está supeditada a la semántica del lenguaje de programación elegido y se
puede perder –entre tanto detalle técnico– la idea primordial detrás de la solución
que encarna. Por ello, a lo largo de este texto se usará esta opción simpre y cuando
la especificación del algoritmo no resulte en demasiado detalle técnico o sea extre-
madamente dependiente de la semántica de Pyhton. Finalmente, al usar notación
matemática se elimina de raı́z el problema de la ambigüedad, pero la especificación
puede resultar extraña a los ojos poco entrenados o desconocedores de la notación
elegida.
Ejercicios
guardados (en inglés, guarded command language) para especificar algoritmos ite-
rativos; este puede ser considerado un lenguaje matemático para la especificación
de algoritmos.
Capı́tulo 1
Análisis asintótico
21
22 1. Análisis asintótico
Nota 1.1.1
Ejemplo 1.1.1
Nota 1.1.2
Nota 1.1.3
Note que las funciones TA presentadas en la Nota 1.1.3 tienen como dominio
los números naturales y rango los números reales no negativos. Esto obedece a
que el tamaño de una instancia se mide, generalmente, en unidades enteras (e.g.,
cantidad de elementos, cantidad de bits), y a que la cantidad de operaciones, tiempo
o memoria nunca es negativa (por cuestiones técnicas, se prefiere que sea un número
real y no uno natural).
Abstraer la cantidad de recursos empleados por un algoritmo con una función
matemática tiene varios beneficios. Primero, la noción de función es un concepto
básico no solo en matemáticas sino en cualquier ciencia. Segundo, hay herramientas
disponibles desde las matemáticas para analizar funciones, i.e., para clasificar al-
goritmos en función de la cantidad de recursos que requieren. Tercero, esta brinda
un nivel de abstracción conveniente para comparar algoritmos, dejando un poco de
lado la velocidad del procesador o de la memoria de una máquina concreta en la
cual se implanten.
Ejercicios
Definición 1.2.1
Nota 1.2.1
Nota 1.2.2
Ejemplo 1.2.1
Ejemplo 1.2.2
Definición 1.2.2
Nota 1.2.3
Definición 1.2.3
Ejemplo 1.2.3
Ejercicios
Demostración
Demostración
Ejemplo 1.3.1
Demostración
Teorema 1.3.4
Sean f, g : N → R≥0 :
1. O(f ) = O(g) sii f ∈ O(g) y g ∈ O(f ).
2. O(f ) ⊂ O(g) sii f ∈ O(g) y g ∈
/ O(f ).
Demostración
Sean f, f1 , g, g1 : N → R≥0 :
1. O(f + g) = O(f ↑ g), en donde la suma y el máximo se interpretan punto
a punto.
2. Si f1 ∈ O(f ) y g1 ∈ O(g), entonces f1 g1 ∈ O(f g), en donde el producto
se interpreta punto a punto.
Ejercicios
El caso base en una función simple indica que una vez el tamaño de la entra-
da es suficientemente pequeño como para no hacer llamados recurrentes, entonces
el problema se puede resolver en tiempo/espacio constante (i.e., O(1)). El caso
recurrente asume que se recurre sobre a instancias similares a la instancia dada,
pero de tamaño reducido en una fración b, y que estos llamados requieren O(nk )
tiempo/espacio para ser consolidados como respuesta de la instancia inicial.
Ejemplo 1.4.1
último nivel de la recurrencia, es decir, está acotada por O(nlogb a ) (sabiendo que
nlogb a = alogb n , lo cual se propone como ejercicio para el lector).
A continuación se presentan ejemplos del uso del Teorema Maestro.
Ejemplo 1.4.4
Considere una función simple cuyo caso recurrente está dado por:
T (n) = 2 · T (n/3) + O(n).
Luego, a = 2, b = 3 y k = 1. Note que:
a = 2 < 3 = 31 = bk ,
lo cual corresponde al segundo caso del Teorema Maestro. En consecuencia,
T (n) ∈ O(n).
1.4. El Teorema Maestro 39
Ejercicios
1. Use el Teorema Maestro para calcular T (n) en cada uno de los siguientes casos:
a) T (n) = T (n/2) + 1
b) T (n) = 3T (n/3) + 8n
c) T (n) = 2T (4n/5) + n3
2. Use el Teorema Maestro para calcular T (n) en cada uno de los siguientes casos:
a) T (n) = 2T (n/3) + n3
b) T (n) = 3T (n/3) + 5n
c) T (n) = 6T (4n/5) + 4n2
3. Justifique por qué, para analizar con ayuda del Teorema Maestro el orden de
la cantidad de recursos que emplea un algoritmo, no es necesario contar con los
valores exactos de los casos base de la función simple asociada.
4. Sean a, b ∈ R tales que a ≥ 1 y b > 1. Demuestre, para n ∈ N, que nlogb a =
alogb n .
5. Investigue acerca del Algoritmo de Strassen para la multiplicación de matrices.
Explique brevemente en qué consiste, cómo reduce la cantidad de llamados
recurrentes de 8 a 7 e ilustre su funcionamiento con un ejemplo.
6. Existe una versión más general del Teorema Maestro en la cual se pueden
analizar funciones recurrentes T : N → R≥0 cuyos casos bases son O(1) y cuyos
casos recurrentes son de la forma
T (n) ≤ a · T (n/b) + Θ(nk ),
para a, b, k ∈ R≥0 tales que a ≥ 1 y b > 1. Investigue acerca de esta versión del
Teorema Maestro y enúncielo.
7. Los árboles de recurrencia son una herramienta visual para resolver recurrencias
más generales que las admitidas para funciones simples. Investigue acerca de los
árboles de recurrencia, elabore una breve descripción y presente un ejemplo de
cómo usarlos para analizar funciones recurrentes más generales que las simples.
8. Use árboles de recurrencia (ver Ejercicio 7) para resolver cada una de las si-
guientes recurrencias:
√
a) T (n) = 2T (n/4) + n
b) T (n) = 2T (n/4) + n
c) T (n) = 2T (n/4) + n2
9. Use árboles de recurrencia (ver Ejercicio 7) para resolver cada una de las si-
guientes recurrencias:
a) T (n) = T (n/2) + T (n/3) + T (n/6) + n
b) T (n) = T (n/2) + 2T (n/3) + 3T (n/4) + n2
c) T (n) = 2T (n/2) + O(n log n)
Notas del capı́tulo y referencias 41
Esta sección tiene dos propósitos. Uno es mostrar cómo la técnica de dividir,
conquistar y combinar permite resolver el problema de la teselación de tableros de
ajedrez. El otro es establecer (de manera informal) la relación que existe entre esta
técnica algorı́tmica y la demostración por inducción matemática.
Nota 2.1.1
43
44 2. Dividir, conquistar y combinar
A continuación se indican las tres partes que forman parte de una demostra-
ción por inducción matemática:
Casos base: son aquellos casos que no dependen de otros casos y que se
pueden resolver directamente.
Hipótesis inductiva: son aquellos casos que se pueden suponer resueltos
(con base en un ordenamiento de las instancias del problema).
Casos inductivos: son aquellos casos que se apoyan en la hipótesis in-
ductiva para poder llegar al objetivo de la demostración.
Esta es una sobresimplificación (un poco abusiva) del principio de inducción
matemática. Sin embargo, es útil para el propósito de explicar la técnica de
dividir, conquistar y combinar.
22
El problema se plantea de manera general, i.e., para cualquier tablero que cum-
pla con las indicaciones de la entrada. Como N no está acotado, esto significa que
la cantidad de tableros es infinita y que no es posible tratar de construir tesela-
ciones enumerando explı́citamente todos los tableros. Si se formulase el problema
directamente a modo de una fórmula matemática (e.g., ∀N.N ≥ 1 · · · ), serı́a claro
que una opción es proceder por inducción matemática.
2.1. Teselación de tableros de ajedrez 45
Algoritmo 2.1.1
Cada uno de estos tableros se puede teselar con una ficha en forma
de L. Esto concluye el caso base o, más bien, los casos base.
Los demás tableros tienen lado 2N , con N ≥ 2. Se supondrá, a modo
de hipótesis inductiva, que todo tablero de lado 2n , con n ≥ 1, se puede
teselar (tal cual como se procede por inducción matemática sobre N ).
Tenga en cuenta que cada uno de estos tableros tiene exactamente un
hueco. La idea es entonces encontrar una forma de teselar un tablero
de lado 2n+1 que tiene exactamente un hueco. Note también que si un
tablero de lado 2n+1 se divide en 4 partes iguales, resultan 4 tableros de
lado 2n (enumerados de 1 a 4 en la gráfica a continuación) y se acerca
a algo parecido a la hipótesis inductiva. Estos dos hechos se representan
gráficamente a continuación:
2n+1 2n 2n
(1) (2)
2n
2n+1
2n
(4) (3)
2n (HI) (HI)
2n (HI) (HI)
(4) (3)
Ejercicios
...
...
0 N
(i.e., combinar ), que a su vez nos permitirán identificar cuál deberı́a ser la hipótesis
inductiva (i.e., dividir ). Por supuesto, dependiendo de cómo se haga este análisis,
diferentes algoritmos resultarán.
Algoritmo 2.2.1
Ejemplo 2.2.1
5 A = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
6 sort(A, 0, len(A))
2.2. Ordenamiento de arreglos 51
7 print(A)
8
9 A = [ ]
10 sort(A, 0, len(A))
11 print(A)
12
13 A = [ 5 ]
14 sort(A, 0, len(A))
15 print(A)
El resultado de esta ejecución es el siguiente:
1 [-50, -10, -2, 2, 3, 4, 8, 8, 10]
2 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
3 []
4 [5]
Se han mostrado ejemplos que sugieren indicar que funciona bien para algunas
instancias del problema, mas no una demostración de que el algoritmo funciona
correctamente (i.e., resuelve adecuadamente todas las instancias del problema).
Para ello es necesario demostrar que en realidad el código hace lo que debe hacer, es
decir, ordenar un arreglo ascendentemente. Dicho de otra forma: se desea demostrar
que sort ordena ascendentemente cualquier arreglo de números.
Demostración
Demostración
Demostración
Es importante aclarar que en el Teorema 2.2.3 se está suponiendo que las varia-
bles que se crean en cada llamado recurrente son independientes entre los llamados.
Sin embargo, si se pudieran reutilizar de tal forma que fuera una cantidad constante
para todos los llamados recurrentes, la complejidad espacial del algoritmo serı́a de
orden constante.
Ejercicios
Nota 2.3.1
Antes de continuar con el diseño de los casos del algoritmo, es clave pensar en
cuál serı́a la situación ideal que –con base en los invariantes dados– garantizará que
el arreglo A está ordenado. En este sentido, el invariante P1 es clave, pues cuando n
sea N (lo cual es permitido por el invariante P2 ), se tendrı́a la siguiente situación:
A[0..N ) tiene los n elementos de A[0..N ) más pequeños y ordenados ascen-
dentemente.
Es decir, A[0..N ) tendrı́a sus elementos ordenados ascendentemente; no faltarı́a ni
sobrarı́a elemento alguno gracias al invariante P0 . De esta forma, se sugiere que
la estrategia es ir avanzando n, que puede iniciar desde 0, hasta que su valor sea
N , mientras se mantienen los invariantes formulados. Un representación gráfica
de los invariantes P1 y P2 , y de esta idea de solución algorı́tmica, se muestra a
continuación:
A
0 n N
Con base en los invariantes y en la idea gráfica de diseño, se propone el Algo-
ritmo 2.3.1 para ordenar iterativa y ascendentemente un arreglo de números.
Algoritmo 2.3.1
Para n = 0, 1, . . . , N − 1:
1. identificar el ı́ndice n ≤ m < N con el mı́nimo valor en A[n..N ) e
2. intercambiar A[n] y A[m].
1 def sortiter(A):
2 N = len(A)
3 # P0 ∧ P1 ∧ P2
4 for n in range(0, N):
5 m = n
2.3. Una versión iterativa del ordenamiento 57
Nota 2.3.2
Teorema 2.3.1
Demostración
Antes de la primera iteración del ciclo, las variables n y N tienen los valores
0 y len(A), respectivamente. Note que:
Dado que el arreglo A no ha sido modificado, claramente P0 es cierto.
58 2. Dividir, conquistar y combinar
Teorema 2.3.2
Demostración
Ejercicios
ordenado ordenado
A
0 low mid hi N
merge
A
0 low ordenado hi N
Note que la idea de ordenar el arreglo dado /in-situ/ se captura con la condición
C0 . La condición C1 brinda pistas de cómo se puede avanzar con low y hi ordenando
por partes:
Para entender cómo encajan las piezas del algoritmo, se proponen los siguientes
invariantes para el segundo ciclo de la función merge:
P0 : A[low..n) es un ordenamiento de tmp[low..l) y tmp[mid..r).
P1 : low ≤ l ≤ mid ≤ r ≤ hi.
P2 : low ≤ n ≤ hi.
tmp
0 low l mid r hi len(tmp)
¿?
ordenado
A
0 low n hi N
Antes de proceder a demostrar que merge y mergesort son correctos con respec-
to a su especificación, se presentan ejemplos de cómo el algoritmo ordena algunos
arreglos de números.
Ejemplo 2.4.1
6 A = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
2.4. Mergesort: un ordenamiento de arreglos más eficiente 63
7 mergesort(A, 0, len(A))
8 print(A)
9
10 A = [ ]
11 mergesort(A, 0, len(A))
12 print(A)
13
14 A = [ 5 ]
15 mergesort(A, 0, len(A))
16 print(A)
El resultado de esta ejecución es el siguiente:
1 [-50, -10, -2, 2, 3, 4, 8, 8, 10]
2 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
3 []
4 [5]
Teorema 2.4.1
Demostración
Teorema 2.4.2
Demostración
Teorema 2.4.3
Demostración
Teorema 2.4.4
66 2. Dividir, conquistar y combinar
Demostración
Ejercicios
Demuestre que esta igualdad es cierta. Concluya que, cuando n = hi y con base
en el invariante P1 , l = mid y r = hi.
2. Demuestre que la desigualdad low < mid < hi es cierta en mergesort.
3. Demuestre el Teorema 2.4.3.
4. Demuestre el Teorema 2.4.4.
5. Considere la función merge que es llamada desde la función mergesort en la
Implementación 2.4.1. Allı́ se usa la variable global tmp para almacenar una
copia de partes de A en cada llamado. Suponiendo que dicha variable no es global
y que en cada llamado recurrente se crea un arreglo de tamaño hi − low para la
copia temporal, demuestre que mergesort(A, 0, N) usa espacio O(N log N ).
Justifique su respuesta.
6. La cantidad de inversiones de un arreglo es un indicador de qué tan desordenado
está: si este valor es 0, entonces el arreglo está completamente ordenado. Si
el arreglo está ordenado descendentemente, entonces este valor es el máximo.
Formalmente, una inversión en en un arreglo A[0..N ), N ≥ 0, es una pareja de
ı́ndices 0 ≤ i < j < N tales que A[i] > A[j]. Por ejemplo, en el arreglo [3, 1, 2]
hay dos inversiones: (3, 1) y (3, 2).
a) Diseñe un algoritmo que calcule la cantidad de inversiones en A[0..N ) en
tiempo O(N log N ).
b) Demuestre que el algoritmo propuesto es correcto con respecto a la espe-
cificación.
2.5. Búsqueda binaria 67
A
0 low hi N
El algoritmo mantiene dos centinelas low y hi que definen las fronteras del espa-
cio de búsqueda activo A[low..hi) para el valor x. Las otras dos porciones A[0..low)
y A[hi..N ) han sido descartadas y se tiene certeza de que no es necesario seguir
buscando allı́. Este diseño usa las siguientes condiciones:
A
0 low mid hi N
Algoritmo 2.5.1
Teorema 2.5.1
Demostración
qué?). En cada uno de los casos se recurre sobre la “mitad” que tiene el
potencial de contener a x.
En consecuencia, el llamado binsearch(A, x, 0, N) resuelve el problema de
búsqueda de x en el arreglo A ordenado ascendentemente.
Teorema 2.5.2
Demostración
Teorema 2.5.3
Demostración
Ejercicios
13. Considere un arreglo A[0..N ) que ha sido rotado circularmente k pasos, con
1 ≤ k < N . Por ejemplo, el arreglo [3, 7, 10, 1, 2] está rotado 3 pasos.
a) Diseñe y analice un algoritmo que en tiempo O(log N ) calcule la cantidad
de pasos que ha sido rotado A[0..N ).
b) Diseñe y analice un algoritmo que en tiempo O(log N ) determine si un
número x está en A[0..N ).
Programación dinámica
Nota 3.0.1
75
76 3. Programación dinámica
La programación dinámica es una técnica que puede ser útil para resolver pro-
blemas algorı́tmicos. Sin embargo, no en todos los problemas algorı́tmicos tiene sen-
tido usarla. Aquellos problemas que son susceptibles de abordar usando la técnica
exhiben ciertas propiedades en común que relacionan la solución de instancias con
subinstancias.
Nota 3.1.1
Ejemplo 3.1.1
Ejemplo 3.1.2
78 3. Programación dinámica
A 1 B
1
D 1
Ejercicios
Definición 3.2.1
80 3. Programación dinámica
Calcular fib es en el fondo un problema de conteo. Hay dos casos bases: para
N = 0 o N = 1, se tiene que fib(N ) = N . El caso inductivo se plantea para
cualquier otro valor de N (recuerde que es un número natural) recurriendo sobre
los dos valores inmediatamente anteriores (por eso son necesarios los dos casos
base). Note que el Problema 3.2.1 cuenta con las propiedades de subestructura y
solapamiento. Por un lado, el cálculo recurrente de la función depende de sı́ misma,
con valores más pequeños (en el orden usual de los números). Por otro lado, dos
llamados recurrentes distintos dependen de un mismo suproblema. Por ejemplo, en
el cálculo de fib(4) se recalcula fib(2):
fib(4)
fib(2) fib(3)
0 1 1 fib(0) fib(1)
0 1
1 def fib(n):
2 ans = None
3 if n<=1: ans = n
4 else: ans = fib(n-2)+fib(n-1)
5 return ans
Ejemplo 3.2.1
Hoy dos formas usualmente empleadas para diseñar un algoritmo con pro-
gramación dinámica y que permiten implementar eficientemente la función
recurrente asociada a su solución:
Memorización: se calcula la función objetivo por demanda de modo tal
que los valores intermedios, resultado de los llamados recurrentes, se van
calculando en la medida que sea necesario. Generalmente, resultan algo-
ritmos recurrentes muy parecidos a la función objetivo. Hay una memoria
compartida que se usa: para una instancia especı́fica del problema se con-
sulta si ha sido resuelta antes (i.e., el valor está en la memoria compar-
tida), entonces se usa el valor registrado en la memoria; de lo contrario,
se calcula el valor correspondiente a dicha instancia, ya bien sea directa
o recurrentemente, y se almacena en la memoria compartida para que
pueda ser usado posteriormente de ser necesario.
Tabulación: se calcula la función objetivo exhaustivamente considerando
todos los casos posibles para implementar la función dada con base en
los parámetros de interés. La memoria compartida se va “llenando” incre-
mentalmente con todos estos casos hasta obtener el valor deseado. Gene-
ralmente, resultan algoritmos iterativos.
Teorema 3.2.1
Demostración
Resta por hacer explı́cito cómo fib_memo resuelve el problema planteado ini-
cialmente.
Teorema 3.2.2
Demostración
fib_memo(100, mem) de tal forma que en el segundo llamado se cuente con algunos
valores previamente (y correctamente) calculados en mem.
7 mem = dict()
8 print('fib(100):', fib_memo(100, mem))
9 print(mem)
1 fib(0): 0
2 fib(1): 1
3 fib(2): 1
4 fib(10): 55
5 fib(15): 610
6 fib(100): 354224848179261915075
7 {0: 0, 1: 1, 2: 1, 3: 2, 4: 3, 5: 5, 6: 8, 7: 13, 8: 21, 9: 34, 10: 55,
8 11: 89, 12: 144, 13: 233, 14: 377, 15: 610, 16: 987, 17: 1597,
9 18: 2584, 19: 4181, 20: 6765, 21: 10946, 22: 17711, 23: 28657,
10 24: 46368, 25: 75025, 26: 121393, 27: 196418, 28: 317811, 29: 514229,
11 30: 832040, 31: 1346269, 32: 2178309, 33: 3524578, 34: 5702887,
12 35: 9227465, 36: 14930352, 37: 24157817, 38: 39088169, 39: 63245986,
13 40: 102334155, 41: 165580141, 42: 267914296, 43: 433494437,
14 44: 701408733, 45: 1134903170, 46: 1836311903, 47: 2971215073,
15 48: 4807526976, 49: 7778742049, 50: 12586269025, 51: 20365011074,
16 52: 32951280099, 53: 53316291173, 54: 86267571272, 55: 139583862445,
17 56: 225851433717, 57: 365435296162, 58: 591286729879, 59: 956722026041,
18 60: 1548008755920, 61: 2504730781961, 62: 4052739537881,
19 63: 6557470319842, 64: 10610209857723, 65: 17167680177565,
20 66: 27777890035288, 67: 44945570212853, 68: 72723460248141,
21 69: 117669030460994, 70: 190392490709135, 71: 308061521170129,
22 72: 498454011879264, 73: 806515533049393, 74: 1304969544928657,
23 75: 2111485077978050, 76: 3416454622906707, 77: 5527939700884757,
24 78: 8944394323791464, 79: 14472334024676221, 80: 23416728348467685,
25 81: 37889062373143906, 82: 61305790721611591, 83: 99194853094755497,
26 84: 160500643816367088, 85: 259695496911122585, 86: 420196140727489673,
27 87: 679891637638612258, 88: 1100087778366101931,
28 89: 1779979416004714189, 90: 2880067194370816120, 91: 4660046610375530309,
86 3. Programación dinámica
Teorema 3.2.3
Demostración
tab 0 1 ?
0 n-2 n-1 n N
1 def fib_iter(N):
2 ans = None
3 if N<=1: ans = N
4 else:
5 tab,n = [ None for _ in range(N+1) ],2
6 tab[0],tab[1] = 0,1
7 # P0 ∧ P1
88 3. Programación dinámica
8 while n!=N+1:
9 tab[n],n = tab[n-2]+tab[n-1],n+1
10 ans = tab[N]
11 return ans
Teorema 3.2.4
Demostración
Teorema 3.2.5
Demostración
Ejercicios
en donde n es valor que se desea calcular. También se indicó que esta expresión
resulta de resolver la ecuación de diferencia X 2 − X − 1 = 0 con variable X.
90 3. Programación dinámica
Nota 3.3.1
Estos pasos se ilustrarán con cada uno de los ejemplos que se presentan en el
resto del capı́tulo.
Ejercicios
El caso base indica que no hay ganancia alguna cuando no hay nada que vender.
El caso inductivo indica que se escoge aquella opción que resulta de cortar el
3.4. Suma máxima de un subarreglo 93
Ejemplo 3.4.1
Este problema se puede resolver trivialmente para algunas instancias. Por ejem-
plo, si el arreglo dado únicamente tiene números no negativos, la respuesta es la
suma de sus elementos. También, si todos sus elementos son números negativos,
la respuesta es 0, pues el arreglo vacı́o es subarreglo de cualquier arreglo y tiene
suma 0. El caso general se puede resolver exhaustiva pero ineficientemente, como
se presenta en el Algoritmo 3.4.1.
94 3. Programación dinámica
Algoritmo 3.4.1
La complejidad temporal del Algoritmo 3.4.1 es O(N 3 ) pues calcular todas las
parejas (i, j) toma tiempo O(N 2 ) y para cada una de ellas la suma de A[i..j) toma
tiempo O(N ). Esta complejidad se puede reducir a O(N 2 ) con un pre-proceso que
toma tiempo y espacio O(N ). En particular, se pueden calcular las sumas de los
prefijos (o sufijos) de A[0..N ) de tal forma que la suma de cada subarreglo A[i..j)
se obtiene en tiempo O(1), reduciendo de O(N 3 ) a O(N 2 ) el tiempo que toma la
solución global.
Aplicando la técnica dividir, conquistar y combinar se puede obtener una solu-
ción más eficiente (o menos ineficiente).
Algoritmo 3.4.2
T (N ) = 2T (N/2) + O(N ).
Es decir, bajo la suposición hecha, el Algoritmo 3.4.2 toma tiempo O(N log N ) en
resolver el problema de suma máxima de un subarreglo.
Si se cuenta con un algoritmo relativamente eficiente que resuelve el problema
sin usar programación dinámica, ¿qué hace este problema en este capı́tulo? Pues
bien, lo que sucede es que se puede diseñar una solución que toma tiempo O(N ) si
se usa programación dinámica. Ese es el objetivo de lo que resta de esta sección:
derivar dicha solución.
Nota 3.4.1
Teorema 3.4.1
3.4. Suma máxima de un subarreglo 97
Demostración
tab A[0] ?
0 n N
P2 : ans = (↑ i | 0 ≤ i ≤ n : φ(i)).
Algoritmo 3.4.3
1 def mss(A):
2 ans,N = 0,len(A)
3 if N!=0:
4 tab = [ None for _ in range(N) ]
5 tab[0],ans = A[0],max(ans, A[0])
6 # P0 ∧ P1
7 for n in range(1, N):
8 tab[n] = max(A[n], tab[n-1]+A[n])
9 # P0 ∧ P1 ∧ P2
10 for n in range(N):
11 ans = max(ans, tab[n])
12 return ans
El caso en el cual el arreglo dado es vacı́o, se trata de manera especial con ayuda
de una instrucción condicional. Es necesario ahora demostrar que mss es correcta
con respecto a la especificación dada, y determinar sus complejidades temporal y
espacial.
Teorema 3.4.2
El llamado mss(A):
1. Calcula la suma máxima de un subarreglo de A[0..N ).
2. Toma tiempo O(N ).
3. Toma espacio O(N ).
Demostración
Algoritmo 3.4.4
1 def mss_opt(A):
2 ans,N = 0,len(A)
3 if N!=0:
4 prev,ans = A[0],max(ans, A[0])
5 # P1 ∧ P2 ∧ P3
6 for n in range(1, N):
7 prev = max(A[n], prev+A[n])
8 ans = max(ans, prev)
9 return ans
Teorema 3.4.3
El llamado mss_opt(A):
1. Calcula la suma máxima de un subarreglo de A[0..N ).
2. Toma tiempo O(N ).
3. Toma espacio O(1).
Demostración
de números es 0 (en este problema tiene todo el sentido pues se opera sobre números
naturales y el 0 es el elemento neutro del máximo sobre este conjunto), entonces
no hay ningún problema si se decide iniciar las iteraciones del ciclo con n = 0. La
transformación del Algoritmo 3.4.4 bajo las observaciones anteriores resulta en el
Algoritmo de Kadane, el cual se presenta en el Algoritmo 3.4.5.
1 def kadane(A):
2 N,curr,ans = len(A),0,0
3 for n in range(N):
4 curr = max(A[n], curr+A[n])
5 ans = max(ans, curr)
6 return ans
Ejercicios
1. Diseñe una función mss_bf en Python que implemente el diseño del Algorit-
mo 3.4.1. Corra la implementación con arreglos de números generados aleato-
riamente de tamaños 10, 50, 100 y 250. ¿Cuál es la diferencia en tiempo entre
las ejecuciones?
3. Diseñe una función mss_dcc en Python que implemente el diseño del Algorit-
mo 3.4.2. Corra la implementación con arreglos de números generados aleato-
riamente de tamaños 10, 100, 1000, 10000 y 100000. ¿Cuál es la diferencia en
tiempo entre las ejecuciones?
(↑ n | 0 ≤ n < N : φ(n)) ↑ 0.
102 3. Programación dinámica
Nota 3.5.1
De forma general, dada una colección de elementos, cada uno con un peso y
un valor asociados, el problema consiste en determinar cuál es la suma máxima de
valores que se puede obtener al tomar algunos de los elementos sin que su suma
exceda una restricción de peso global.
La condición de salida del problema supone que el valor y el peso del i-ésimo
elemento están dados por V [i] y W [i], respectivamente. La naturaleza combinatoria
del problema del morral se revela al notar que se requiere una exploración sobre
los subconjuntos de ı́ndices de A. Para cualquier S ⊆ {0, . . . , N − 1}, se definen las
funciones value y weight:
value(S) = (+i | i ∈ S : V [i]) y weight(S) = (+i | i ∈ S : W [i]).
La expresión value(S) representa el valor total de los elementos indexados por S,
mientras que weight(S) el peso total de los elementos indexados por S. Ası́, la salida
del problema del morarral corresponde a la expresión:
(↑ S | S ⊆ {0, . . . , N − 1} ∧ weight(S) ≤ X : value(S)).
Como hay 2N subconjuntos S de ı́ndices de A[0..N ) (¿por qué?), se dice que el pro-
blema del morral es de naturaleza combinatoria, pues debe considerar todos aquellos
subconjuntos S para determinar la mejor combinación posible bajo la restricción
dada. Debe ser claro, entonces, que un algoritmo de fuerza bruta para resolver el
problema del morral tomará tiempo exponencial en la cantidad de elementos entre
los cuales se elige.
Ejemplo 3.5.1
104 3. Programación dinámica
No hay elementos para escoger (i.e., n = 0). Luego, sin importar la capacidad
x del morral, lo máximo (y único) que se puede obtener es 0.
Hay al menos un elemento para escoger (i.e., n > 0). Entonces están las opciones
de que el elemento n−1 (este es el n-ésimo elemento) pese más que la capacidad
x del morral o que no:
3.5. El problema del morral 105
Teorema 3.5.1
Demostración
Sea S ⊆ {0, . . . , n − 1} una solución óptima para el problema del morral con
los primeros n elementos y capacidad x. Se procede por casos sobre S.
Si S = ∅, entonces no hay elementos para escoger (i.e., n = 0) o todos los
elementos disponibles tienen un peso mayor a x (i.e., W [i] > x para 0 ≤ i < n).
En cualquiera de los dos casos φ(n, x) = 0, lo cual coincide con el hecho de
que S = ∅ (¿por qué?).
106 3. Programación dinámica
Luego,
value(S 0 ) = φ(n, x) = φ(n − 1, x).
Como (n − 1) ∈ S, note que
value(S) = value(S \ {n − 1}) + V [n − 1].
Por la misma observación del caso anterior, la optimalidad de φ(n − 1, x −
W [n − 1]) implica que
value(S \ {n − 1}) ≤ φ(n − 1, x − W [n − 1]).
Entonces se tiene:
value(S) = value(S \ {n − 1}) + V [n − 1] ((n − 1) ∈ S)
≤ φ(n − 1, x − W [n − 1]) + V [n − 1] (optimalidad de φ)
≤ φ(n − 1, x) (suposición)
= φ(n, x) (suposición)
0
= value(S ) (definición de S 0 )
< value(S) (suposición inicial).
objetivo
tab[0..N][0..X]
X 0
.
.
.
.
x 0 ?
. .
. .
. .
.
.
0 0
0 n N
objetivo
tab[0..N][0..X]
X 0
.
.
.
.
x 0 ?
. .
. .
. .
.
.
0 0
0 n N
Para implementar el algoritmo de tabulación, basta con seguir los invariantes y tener
clara la imagen mental de ellos en el diagrama de necesidades. La idea es entonces,
una vez creada la tabla e inicializada su primera columna, iniciar a llenarla desde
la columna n = 1. Esto se hará como con las máquinas de escribir: se llena una
columna, se procesa la siguiente. Intencionalmente se permite que la variable x
llegue a X + 1, aunque esta fila no haga parte de la tabla: con x = X + 1 se sabe
que se ha llenado la columna actual y se procede a iniciar la siguiente. Algo similar
sucede con n: se habrá llenado la tabla por completo cuando se tenga n = N + 1.
Algoritmo 3.5.1
1 >>> V = [4, 6, 5, 1]
2 >>> W = [3, 5, 5, 2]
3 >>> print(ks_tab(V, W, 15))
4 16
5 >>> print(ks_tab(V, W, 11))
6 11
7 >>> print(ks_tab(V, W, 5))
8 6
9 >>> print(ks_tab(V, W, 1))
10 0
Teorema 3.5.2
Demostración
x ?
prev curr
Algoritmo 3.5.2
el presente en la columna 1. El cuerpo del ciclo tiene dos partes: una de cambio
de columna (lı́nea 7) y otra de cambio de fila (lı́neas 8-12). El cambio de columna
se hace de manera similar al algoritmo sin optimización, incrementando n en una
unidad y asignando 0 a x. Las variables prev y curr invierten sus valores (¿por
qué la resta funciona?). Se pudo optar por intercambiar directamente los valores de
estas dos variables apelando a la sustitución “simulatánea” ofrecida por Python.
Sin embargo, esto no es posible en lenguajes de programación imperativos como
C, C++ o Java. Por esto, se prefiere hacer el “complemento” con 1, dado que esta
operación aritmética se puede implementar con una asignación individual sin la
necesidad de una variable intermedia/temporal. El cuerpo del ciclo se encarga de
encontrar el mejor valor posible para φ(n, x) de acuerdo con su definición recurrente.
Observe el valor que se retorna está al final de la columna prev y no en la columna
curr (¿por qué?).
Se ilustra el uso de la función ks_tab_opt1 con algunas instancias del problema:
1 >>> V = [4, 6, 5, 1]
2 >>> W = [3, 5, 5, 2]
3 >>> print(ks_tab_opt1(V, W, 15))
4 16
5 >>> print(ks_tab_opt1(V, W, 11))
6 11
7 >>> print(ks_tab_opt1(V, W, 5))
8 6
9 >>> print(ks_tab_opt1(V, W, 1))
10 0
El diseño de la tabulación para ks_tab_opt1 permite reducir el espacio de la
tabulación a O(2X) = O(X), mientras que el tiempo de ejecución se mantiene en
O(N X).
Teorema 3.5.3
Demostración
114 3. Programación dinámica
X
presente
x
pasado
tab
Como la parte inferior de tab[0..X] debe tener disponibles los valores del pasado,
necesariamente la forma de llenarla debe iniciar por su parte superior. Los siguientes
invariantes hacen explı́cita esta y las observaciones anteriores.
El invariante R0 especifica que el subarreglo tab[0..x] tiene los valores del pasado
inmediato (i.e., desde φ(n − 1, 0) hasta φ(n − 1, x)). El invariante R1 , a su vez,
especifica que el subarreglo tab[x + 1..X] tiene los valores del presente (i.e., desde
φ(n, x + 1) hasta φ(n, X)). Los invariantes P2 y P3 son los mismos de las dos
versiones anteriores de los algoritmos de tabulación de φ.
3.5. El problema del morral 115
Algoritmo 3.5.3
Teorema 3.5.4
Demostración
Ejercicios
15. Hay una variante del problema del morral en la cual de cada elemento hay una
cantidad ilimitada de copias (y se permite llevar cuantas copias sean deseadas
de cada elemento).
a) Especifique esta variante del problema del morral.
b) Diseñe una solución con tabulación para el problema especificado usando
la metodologı́a propuesta en la Sección 3.3. Si es posible reducir el espacio
de la tabulación, redúzcalo al máximo.
16. La siguiente especificación corresponde al problema, comúnmente denominado,
suma exacta de un subconjunto (en inglés, Subset Sum):
Nota 3.6.1
Nota 3.6.2
El problema del agente viajero (en inglés, travelling salesman problem o TSP )
se preocupa de responder la siguiente pregunta: dada una lista de ciudades
y las distancias entre cada par de ellas, ¿cuál es la ruta más corta posible
que visita cada ciudad exactamente una vez y al finalizar regresa a la ciudad
origen?
Este problema fue formulado por primera vez a finales de los años 1920s
y ha sido estudiado exhaustivamente en las áreas de optimización y teoria de
la complejidad.
Es decir, cost(C) es la suma de los pesos de los arcos en C. Con base en estas
definiciones, se formula la salida del problema del agente viajero de la siguiente
3.6. El problema del agente viajero 119
manera:
(↓ C | C ⊆ E ∧ circuit(C) : cost(C)).
Esta fórmula expresa que se desea calcular el mı́nimo costo entre todos los circuitos
en G. Note que la cantidad de posibles circuitos está acotada por 2|E| . Más preci-
|E|
samente, por aquellos subconjuntos de |E| de tamaño |V |; es decir, por 2(|V |) , en
donde
|E|
|V |
denota la cantidad de subconjuntos de tamaño |V | en un conjunto de tamaño |E|. En
esta observación yace la naturaleza combinatoria del problema del agente viajero.
Ejemplo 3.6.1
8
6
a 7
7
b 8
c
8
6
a 7
7
b 8
c
subestructura óptima que se “explotará” para formular una función objetivo que
ayude a resolver el problema del agente viajero usando programación dinámica.
Función objetivo. Dado que en un circuito no importa desde dónde se inicie
el recorrido, se fija arbitrariamente un vértice s ∈ V como punto de partida. Se usa
el hecho de que el grafo G no es vacı́o (i.e., V 6= ∅); de lo contrario el problema es
trivial ya que dicho circuito no existe.
Para X ⊆ V y u ∈ X:
Entre todos los posibles caminos que inician en s, incluyen todos los demás vértices
del grafo y terminan en u, se desea calcular aquel camino que sumando la ruta de
regreso directa de u a s sea de costo mı́nimo. Note que al tener que w(e) ≥ 0 para
cualquier arco e ∈ E, es natural que el valor asociado a la minimatoria anterior sea
0 cuando V tiene exactamente un vértice (i.e., V = {s}).
Definición recurrente. Una ruta de costo mı́nimo de s a cualquier otro vértice
debe estar conformado por subrutas óptimas. Es decir, cada ruta que inicie en s en
dicho camino, también debe ser de costo mı́nimo.
La definición formal de φ se presenta para X ⊆ V \ {s} y u ∈ V \ {s}:
+∞
, si x ∈
/ X,
φ(u, X) = w(s, u) , si X = {u},
(↓ v | v ∈ X \ {u} : φ(v, X \ {u}) + w(v, u)) , si u ∈ X ∧ |X| ≥ 2.
Se consideran tres casos para definir φ. Hay dos opciones entre u y X: que u sea
elemento de X o no y, cuando lo es, que sea el único o no. Es imposible que haya un
camino que visite todos los elementos de X y que termine en u cuando u ∈ / X; la
forma de expresar que esto es imposible es con el valor +∞ dado que es la identidad
del mı́nimo. Si u es el único elemento de X, solo hay un camino de s a u: el camino
directo entre s y u, el cual tiene costo w(s, u). Cuando en X hay otros elementos en
adición a u, se apuesta por todos lo caballos: se prefiere aquél vértice v ∈ X \ {u}
3.6. El problema del agente viajero 121
que en dicho camino conecte directamente con u y para el cual el camino desde s
permita construir un camino hasta u de costo mı́nimo.
Teorema 3.6.1
Demostración
La función φ es distinta a las que se han usado para resolver los problemas
anteriores. En particular, tiene como parámetro un conjunto. Como tal, esta defini-
ción se puede implementar directamente en un lenguaje de programación (usando
conjuntos disponibles en sus librerı́as o implementando la estructura de datos co-
rrespondiente). No resulta fácil pensar cómo tabular cuando en una de las dos
dimensiones hay un conjunto. Tampoco resulta fácil pensar cómo implementar una
memorización eficiente cuando el acceso a los datos depende de una colección y que
esta colección puede “mutar” entre los distintos llamados recurrentes. En realidad,
estas son noticias parcialmente malas. La noticia parcialmente buena es que bajo
ciertas suposiciones acerca de la cantidad de vértices en el grafo, los conjuntos se
pueden representar con números naturales y las operaciones básicas sobre ellos (al
menos las que se requieren para calcular la función objetivo) se pueden realizar
en tiempo constante. Entonces, antes de proponer una solución con programación
dinámica, se estudia cómo representar conjuntos y algunas de sus operaciones con
números naturales.
Ejemplo 3.6.2
El llamado is_elt(n, X) es cierto cuando el n-ésimo bit del código binario del
número natural X es 1, suponiendo que n es una posición en la máscara de bits
correspondiente a X.
De una manera muy similar, se puede apagar el n-ésimo bit en una máscara de
bits.
El Algoritmo 3.6.1 presenta las funciones phi_memo y tsp que resuelven el pro-
blema del agente viajero para un grafo de N vértices y función w de peso en los
arcos. La función principal es tsp, la cual recibe estos dos parámetros (se pudo
también optar por tener estos dos parámetros como variables globales, simplifican-
do –pero oscureciendo en este caso en particular– el código). La función phi_memo
es la solución por memorización que implementa la función φ. Note que el vértice
0 juega el papel del vértice s en la especificación de la función φ.
Algoritmo 3.6.1
1 INF = float('inf')
2
Ejemplo 3.6.3
8
6
a 7
7
b 8
c
Demostración
Demostración
Ejercicios
Algoritmos voraces
Los algoritmos voraces (en inglés, greedy algorithms) sirven comúnmente para
resolver problemas de optimización. Están basados en la premisa de que la elección
reiterativa de óptimos locales garantiza, al final del proceso de elección, un óptimo
global. Es decir, para resolver un problema de optimización, un algoritmo voraz
una y otra vez toma una mejor opción local (sin fijarse en sus consecuencias) de
tal forma que la colección de elecciones hecha finalmente constituye una solución
óptima global.
Sin embargo, en la práctica, la optimización local rara vez conduce a un óptimo
global. Entonces, ¿por qué estudiar este tipo de algoritmos? La razón principal
es que cuando los algoritmos voraces funcionan, son muy eficientes, sencillos de
programar y elegantes. Hay una frase anónima que resume muy bien esta situación
que parece paradójica:
Los algoritmos voraces no funcionan, pero cuando sı́, lo hacen muy bien.
Autor desconocido.
131
132 4. Algoritmos voraces
Nota 4.1.1
Ejemplo 4.1.1
Demostración
Algoritmo 4.1.1
Demostración
Ejemplo 4.1.2
0 0 x
x x
x x x
0 x 0 x
1 3 1
x 2 x x 2
x x x x 5
0 x x 0 x x
1 3 x 1 3 x
x 2 x 4 x 2 4
4.1. Agendamiento de actividades 137
función objetivo:
0 , si n = N,
φ(n, t) = φ(n + 1, t) , si n 6= N ∧ A[n][0] < t,
1 + φ(n + 1, A[n][1]) , si n =6 N ∧ A[n][0] ≥ t.
El caso base corresponde a la situación en la cual no hay actividades para agendar;
el agendamiento óptimo tiene tamaño 0. El caso inductivo se divide en dos, siempre
suponiendo que hay al menos una actividad que potencialmente puede ser agendada
(i.e., n 6= N ). Si hay conflicto entre la actividad n y el intervalo “protegido” (i.e.,
A[n][0] < t), entonces esta se ignora y se recurre con el resto de las actividades. De
lo contrario, si no hay conflicto entre la actividad n y el intervalo protegido (i.e.,
A[n][0] ≥ t), entonces se selecciona la actividad y se recurre con las demás que están
pendientes por explorar, actualizando a (0..A[n][1]) el intervalo protegido (i.e., con
el cual se quiere evitar un conflicto).
Teorema 4.1.3
Demostración
Algoritmo 4.1.2
Ejemplo 4.1.3
Teorema 4.1.4
Demostración
140 4. Algoritmos voraces
Ejercicios
Definición 4.2.1
Ejemplo 4.2.1
1
a b
1 1
Este grafo tiene tres árboles de cubrimiento mı́nimo, dados por los siguientes
conjuntos de arcos, cada uno con suma de pesos 2: {(a, b), (b, c)}, {(a, b), (a, c)}
y {(a, c), (b, c)}.
1. A = ∅.
2. Mientras A no sea un MST de G:
a) Encontrar un arco seguro en E para extender A (e.g., e)
b) extender A con e.
3. Retornar A.
Nota 4.2.1
Sea A ⊆ E tal que cada uno de sus arcos hace parte de un MST de G y
(S, V \ S) un corte de V que respeta a A. Si (u, v) ∈ E es un arco ligero que
cruza (S, V \ S), entonces (u, v) es seguro para A.
Demostración
Finalmente, con ayuda del Teorema 4.2.1 se puede obtener la correctitud del
Algoritmo 4.2.1. Esto quiere decir que, al concretar las nociones de corte y arco
146 4. Algoritmos voraces
ligero resulta un algoritmo para calcular el MST del grafo G con función de peso
w.
Demostración
Ejercicios
Decidibilidad y completitud
147
148 5. Decidibilidad y completitud
Ejemplo 5.1.1
Ejemplo 5.1.2
Ejercicios
Definición 5.2.1
Definición 5.2.2
Sea Σ un alfabeto:
El complemento L de un lenguaje L ⊆ Σ∗ es el conjunto
L = Σ∗ \ L.
La concatenación L1 L2 de dos lenguajes L1 , L2 ⊆ Σ∗ es el conjunto
L1 L2 = {s1 s2 | s1 ∈ L1 ∧ s2 ∈ L2 }.
La concatenación generalizada Lk , con k ≥ 0, de un lenguaje L ⊆ Σ∗ es
el conjunto definido inductivamente, para cualquier n ∈ N, de la siguiente
manera:
L0 = {λ}
Ln+1 = Ln L.
Las operaciones de unión (i.e., ∪ ), intersección (i.e., ∩ ) y potencia (i.e.,
P( )) son las usuales.
5.2. Un marco universal basado en lenguajes 153
Ejemplo 5.2.1
Nota 5.2.1
La inclinación por usar Σ∗ para denotar el conjunto de todas las cadenas sobre
Σ no es caprichosa. Obedece al hecho de que este conjunto corresponde a la
clausura de Kleene de Σ, escrita Σ∗ , y definida como:
[
Σ∗ = Σn ,
n∈N
una función inyectiva que identifica cualquier elemento o tupla de elementos (de un
universo contable) con una secuencia binaria en {0, 1}∗ .
Nota 5.2.2
Ejercicios
Ejemplo 5.3.1
Definición 5.3.2
Teorema 5.3.1
5.3. Aceptación y decisión 157
Demostración
de una función pero decidido solo por una única (¿por qué?). Finalmente, cual-
quier función total necesariamente es una función de decisión; el converso también
es cierto, es decir, cualquier función de decisión es total. Esta última observación,
en el marco de los lenguajes formales, es especialmente importante porque asocia
unı́vocamente la noción de decisión de un lenguaje a la de totalidad de una función.
Teorema 5.3.2
Demostración
Ejercicios
1. Proponga condiciones suficientes para que la siguiente igualdad sea cierta para
A : Σ∗ → {0, 1}:
[BB88] Gilles Brassard and Paul Bratley, Algorithms: Theory and practice, Prentice
Hall, 1988.
[Bei13] Wolfgang Bein, Advanced techniques for dynamic programming, pp. 41–92,
Springer, New York, NY, 2013.
[Bel84] Richard Bellman, Eye of the hurricane: an autobiography, World Scientific,
1984.
[Bha15] Harsh Bhasin, Algorithms: Design and analysis, Oxford University Press, 2015.
[Boh06] Jaime Bohórquez, Diseño efectivo de programas correctos, Escuela Colombiana
de Ingenierı́a, 2006.
[CLRS09] Thomas Cormen, Charles Leiserson, Ronald Rivest, and Clifford Stein, Intro-
duction to algorithms, 3rd ed., MIT Press, 2009.
[Coh90] Edward Cohen, Programming in the 1990s: an introduction to the calculation
of programs, Springer-Verlag, 1990.
[Dij76] Edsger Wybe Dijkstra, A discipline of programming, Prentice-Hall, 1976.
[Eri19] Jeff Erickson, Algorithms, 2019.
[Gri81] David Gries, The science of programming, Springer-Verlag, 1981.
[HHE20] Steven Halim, Felix Halim, and Suhendry Effendy, Competitive programming
4: Book I, LuLu, 2020.
[Kal90] Anne Kaldewaij, Programming: the derivation of algorithms, Prentice-Hall,
1990.
[KET06] Jon Kleinberg and Éva Tardos, Algorithm design, Pearson, 2006.
[Knu88] Donald Knuth, The art of computer programming: Volume 3 / sorting and
searching, 2nd ed., Addison-Wesley, 1988.
[Lev12] Anany Levitin, Introduction to the design and analysis of algorithms, 3rd ed.,
Pearson, 2012.
161
162 Bibliografı́a
163
164 Índice alfabético
notación asintótica
O, 25
Ω, 28
Θ, 28
problema, 4
algorı́tmico, 4
árbol de cubrimiento mı́nimo, 142
cálculo de números de Fibonacci, 80