ADA Lecture Notes
ADA Lecture Notes
ALGORITMOS
Departamento de Informática y Computación -
Universidad Tecnológica Metropolitana de Chile
Índice general i
1 Introducción 1
2 Fundamentos 3
2.1. Notación asintótica . . . . . . . . . . . . . . . . . . . . . . . . 3
2.2. Recurrencias en general . . . . . . . . . . . . . . . . . . . . . . 5
2.3. Recurrencias divide and conquer . . . . . . . . . . . . . . . . . 5
2.4. Recurrencias reduce and conquer . . . . . . . . . . . . . . . . 6
2.5. Método maestro para resolver recurrencias . . . . . . . . . . . 7
2.6. Modelo de computación WRAM . . . . . . . . . . . . . . . . . 8
3 Dividir y conquistar 9
3.1. Búsqueda binaria . . . . . . . . . . . . . . . . . . . . . . . . . 9
3.2. Merge sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.3. El par de puntos más cercano . . . . . . . . . . . . . . . . . . 12
3.4. Algoritmo de Strassen . . . . . . . . . . . . . . . . . . . . . . . 14
3.5. Envoltura convexa . . . . . . . . . . . . . . . . . . . . . . . . . 16
4 Algoritmos codiciosos 19
4.1. Selección de actividades . . . . . . . . . . . . . . . . . . . . . 19
4.2. Algoritmo de Kruskal . . . . . . . . . . . . . . . . . . . . . . . 21
4.3. Códigos de Huffman . . . . . . . . . . . . . . . . . . . . . . . 24
5 Algoritmos de ordenamiento 28
5.1. Insertion sort . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
5.2. Quick sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
5.3. Heap sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
5.4. Cota inferior para el ordenamiento . . . . . . . . . . . . . . . 34
5.5. Counting sort . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
5.6. Radix sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
6 Programación dinámica 38
6.1. SRTBOT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
6.2. Corte de una varilla . . . . . . . . . . . . . . . . . . . . . . . . 39
6.3. Bowling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
I
Índice general
6.4. Parentización . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
6.5. Subsecuencia común más larga . . . . . . . . . . . . . . . . . . 46
6.6. Suma de subconjuntos . . . . . . . . . . . . . . . . . . . . . . 48
7 Algoritmos en grafos 50
7.1. Grafos direccionados y no direccionados . . . . . . . . . . . . . 50
7.2. Definiciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
7.3. Búsqueda en anchura . . . . . . . . . . . . . . . . . . . . . . . 51
7.4. Búsqueda en profundidad . . . . . . . . . . . . . . . . . . . . . 53
7.5. Algoritmo de Dijkstra . . . . . . . . . . . . . . . . . . . . . . . 54
7.6. Algoritmo de Bellman-Ford . . . . . . . . . . . . . . . . . . . . 57
Bibliografía 72
II
CAPÍTULO 1
Introducción
1
de caminos más cortos desde un origen mediante la búsqueda en profundidad
y anchura, como también al problema de caminos de menor peso desde un
origen con valores positivos mediante el algoritmo de Dijkstra y con valores
negativos mediante el algoritmo de Bellman-Ford. El capítulo 8 las clases P,
NP, NP-C, NP-H, define las distintas clases de complejidad, la clasificación de
problemas NP-Hard y NP-Completo y distintas reducciones de problemas. El
capítulo 9 algoritmos para machine learning, se presenta el algoritmo de los K
vecinos más cercanos para el problema de clasificación, el algoritmo de gradiente
descendiente para la minimización de funciones convexas y el algoritmo de
backpropagation para el entrenamiento de redes neuronales.
Existen referencias adicionales en los pie de página a sitios web de interés
con material adicional.
2
CAPÍTULO 2
Fundamentos
60 a
log(n)
n
n log(n)
40 n2
an
20
0
0 2 4 6 8
Figura 2.1: Crecimiento asintótico de distintas funciones.
3
2.1. Notación asintótica
Notación big Oh
La notación O(g(n)) nos da un límite superior para la razón de crecimiento
de una función T (n). Para dos funciones no negativas T (n) y g(n), diremos que:
si y sólo si:
T (n)
lı́m < ∞. (2.2)
n→∞ g(n)
si y sólo si:
T (n)
lı́m > 0. (2.4)
n→∞ g(n)
Se define formalmente T (n) = Ω(g(n)) como: existen dos constantes c y n0
tal que 0 ≤ cg(n) ≤ T (n) para todo n ≥ n0 .
T (n)
T (n) T (n)
4
2.2. Recurrencias en general
Árbol de recursión
En un árbol de recursión, cada nodo representa el costo de cada subproblema
en el conjunto de llamadas recursivas. Cada nivel tiene a ramas por cada nodo,
con un total de logb (n) niveles. Se suman los costos en cada nivel y el costo final
T (n) corresponde a la suma de todos los costos por nivel. Así, para la recurrencia
T (n) = 2T (n/2) + n, cada nivel l divide el problema en 2l (bl ) trabajos de n/2l
(costo por nivel n) y cada nodo genera a = 2 ramas en el nivel siguiente.
n nivel l = 0
n n
2 2 costo n - nivel l = 1
n n n n ..
4 4 4 4 costo n .
.. .. .. .. .. .. .. .. costo n - nivel l = logb n
. . . . . . . .
Figura 2.3: Árbol de recursión.
5
2.4. Recurrencias reduce and conquer
Método de sustitución
El método de sustitución requiere obtener una solución (por ejemplo desde
un árbol de recursión) y comprobar la exactitud mediante inducción matemática,
lo cual permite obtener una solución más fiable. En general se obtiene la solución
en dos pasos:
Método de Akra-Bazzi
El método de Akra-Bazzi resuelve recurrencias de la forma (más complejas
que la forma general):
k
X
T (n) = f (n) + ai T (bi n). (2.7)
i=1
donde ai > 0, 0 < bi < 1 y |f ′ (n)| < xc (f (n) es polinomial). Para la recurrencia
T (n) = 2T (n/2) + n se obtiene k = 1, a1 = 2, b1 = 1/2, p = 1, |f ′ (n)| < xc
(f (n) = n es un polinomio), la integral reulta en log(n) (al sustituir x = n)
dando como resltado T (n) = O(n log(n)).
6
2.5. Método maestro para resolver recurrencias
log (a)
Θ(n b )
si f (n) = O(nlogb (a)−ϵ )
T (n) = Θ(nlogb (a) logk+1 (n)) si f (n) = Θ(nlogb (a) logk (n)) (2.11)
Θ(f (n)) si f (n) = Ω(nlogb (a)+ϵ )
para k ≥ 0 y ϵ > 0. Además, para el caso f (n) = Ω(nlogb (a)+ϵ ) se debe cumplir
con la condición de regularidad af (n/b) ≤ cf (n) para una constante c < 1 y
cualquier n lo suficientemente grande. Una versión simplificada permite obtener
soluciones cerradas con cotas superiores para las recurrencias del tipo divide
and conquer. Con a > 0 y b > 1, si f (n) está en O(nm ) para m ≥ 0:
m
O(n )
si m > logb (a)
m
T (n) = O(n log(n)) si m = logb (a) (2.12)
O(nlogb (a) ) si m < logb (a)
De manera similar, existe una versión que permite obtener soluciones cerradas
con cotas superiores para las recurrencias del tipo reduce and conquer. Con
a > 0 y b > 0, si f (n) está en O(nm ) para m ≥ 0:
m
O(n )
si a < 1
T (n) = O(nm+1 ) si a = 1 (2.13)
m n/b
O(n a ) si a > 1
7
2.6. Modelo de computación WRAM
8
CAPÍTULO 3
Dividir y conquistar
1 LINEAR-SEARCH(A, n, x)
2 i = 0
3 while i < n and A[i] ̸= x
4 i = i + 1
5 if i > n - 1
6 return None
7 else
8 return i
9
3.1. Búsqueda binaria
correspondiente al índice ⌊(p + r)/2⌋, donde p y r son los índices del primer
y último elemento del arreglo, y luego verificar recursivamente si x es igual,
mayor o menor que el pivote. Si es mayor, se puede estar seguro (ya que el
arreglo está ordenado) que de encontrarse x en A debe estar entre los índices
⌊(p + r)/2⌋ + 1 y r. Por el contrario, si x es menor que el pivote, debe estar
entre los índices p y ⌊(p + r)/2⌋ − 1. En cada etapa de la recursión (donde la
nueva llamada a la función actualiza los valores de p y r), si x es igual al pivote
se retorna el índice del pivote (similar a un caso base) y en el caso de que el
valor no se encuentre en el arreglo el subproblema corresponde a un arreglo
vacío. La rutina BINARY-SEARCH implementa el algoritmo recursivo.
1 BINARY-SEARCH(A, x, p, r)
2 if p > r
3 return None
4 mid = floor((p + r) / 2)
5 if x == A[mid]
6 return mid
7 elseif x > A[mid]
8 return BINARY-SEARCH(A, x, mid + 1, r)
9 else
10 return BINARY-SEARCH(A, x, p, mid - 1)
n → O(1) - nivel l = 0
10
3.2. Merge sort
[1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18] O(1) - nivel 0
n 3 9 10 1 8 7 5 2
n/2 3 9 10 1 8 7 5 2
Dividir
n/4 3 9 10 1 8 7 5 2
n/8 3 9 10 1 8 7 5 2
O(n) 3 9 1 10 7 8 2 5
Combinar
O(n) 1 3 9 10 2 5 7 8
O(n) 1 2 3 5 7 8 9 10
11
3.3. El par de puntos más cercano
1 MERGE-SORT(A, p, r):
2 if p < r
3 q = floor((p+r)/2)
4 MERGE-SORT(A, p, q)
5 MERGE-SORT(A, q+1, r)
6 MERGE(A, p, q, r)
7 MERGE(A, p, q, r)
8 L = CONCAT(A[p:q], ∞)
9 R = CONCAT(A[q+1:r], ∞)
10 i = j = 1
11 for k = p to r
12 if L[i] ≤ R[j]
13 A[k] = L[i]
14 i = i + 1
15 else
16 A[k] = R[j]
17 j = j + 1
12
3.3. El par de puntos más cercano
descartando los índices iguales (distancia igual a 0), y al encontrar una distancia
menor a la anterior (que comienza en ∞) se actualiza el resultado.
1 CLOSEST_PAIR(A, n)
2 d_min = ∞
3 for i = 0 to n - 1
4 for j = 0 to n - 1
5 d = abs(A[j] - A[i])
6 if d < d_min and i ̸= j
7 d_min = d
8 return d_min
1 CLOSEST-PAIR-DC(A, p, r)
2 MERGE-SORT(A,p,r)
3 if p < r
4 q = floor((p+r)/2)
5 l_min = CLOSEST-PAIR-DC(A, p, q)
6 r_min = CLOSEST-PAIR-DC(A, q+1, r)
7 return min(l_min, r_min, abs(A[q+1] - A[q]))
8 else return ∞
1 CLOSEST-PAIR-IT(A, p, r)
2 MERGE-SORT(A,p,r)
3 d_min = ∞
4 for i in 0 to (r - p) - 1
5 d = abs(A[i] - A[i+1])
6 if d < d_min
7 d_min = d
13
3.4. Algoritmo de Strassen
1 MATRIX-MULT(A, B, C, n)
2 for i = 0 to n - 1
3 for j = 0 to n - 1
4 for k = 0 to n -1
5 c_{ij} = c_{ij} + a_{ik} b_{kj}
C11 C12 A11 A12 B11 B12
= (3.2a)
C21 C22 A21 A22 B21 B22
C11 C12 A11 · B11 + A12 · B21 A11 · B12 + A12 · B22
= (3.2b)
C21 C22 A21 · B11 + A22 · B21 A21 · B12 + A22 · B22
donde A[aij ], B[bij ] y C[cij ] son los cuadrantes de tamaño n/2 de las matrices
originales. La rutina MATRIX-MULT-DC resuelve el problema de manera recursiva.
1 MATRIX-MULT-DC(A, B, C, n)
2 if n == 1
3 c_11 = c_11 + a_11 * b_11
4 else
5 MATRIX-MULT-DC(A11, B11, C11, n/2)
6 MATRIX-MULT-DC(A11, B12, C12, n/2)
7 MATRIX-MULT-DC(A21, B11, C21, n/2)
8 MATRIX-MULT-DC(A21, B12, C22, n/2)
9 MATRIX-MULT-DC(A12, B21, C11, n/2)
10 MATRIX-MULT-DC(A12, B22, C12, n/2)
11 MATRIX-MULT-DC(A22, B21, C21, n/2)
12 MATRIX-MULT-DC(A22, B22, C22, n/2)
14 19 1 2 2 5
= (3.3a)
6 7 0 1 6 7
14
3.4. Algoritmo de Strassen
14 19 1·2+2·6 1·5+2·7
= (3.3b)
6 7 0·2+1·6 0·5+1·7
1 STRASSEN(A, B, C, n)
2 if n == 1
3 c_11 = c_11 + a_11 * b_11
4 else
5 STRASSEN(A11, B12-B22, P1, n/2)
6 STRASSEN(A11+A12, B22, P2, n/2)
7 STRASSEN(A12+A22, B11, P3, n/2)
8 STRASSEN(A22, B21-B11, P4, n/2)
9 STRASSEN(A11+A22, B11+B22, P5, n/2)
10 STRASSEN(A12-A22, B21+B22, P6, n/2)
11 STRASSEN(A11-A21, B11+B12, P7, n/2)
12 C11 = C11 + P5 + P4 - P2 + P6
13 C12 = C12 + P1 + P2
14 C21 = C21 + P3 + P4
15 C22 = C22 + P5 + P1 - P3 - P7
15
3.5. Envoltura convexa
a2
a1
a7
a3
EC(S) = a1 a2 a3 a4 a5
a6
a5 a4
a2 a2
a1 a1
a7 a7
a3 a3
a6 a6
a5 a4 a5 a4
16
3.5. Envoltura convexa
1 CONVEX-HULL(S, n)
2 EC = {}
3 for i = 1 to n
4 for j = 1 to n
5 side_a = side_b = 0
6 for k = 1 to n
7 d=(S[k].x-S[i].x)(S[j].y-S[i].y)-
8 (S[k].y-S[i].y)(S[j].x-S[i].x)
9 if d < 0
10 side_a = side_a+1
11 else if d > 0
12 side_b = side_b+1
13 if (side_a == n - 2) or (side_b == n - 2)
14 EC ∪ {S[i], S[j]}
15 return CLOCKWISE(EC)
1 TWO-FINGER(S, p, q)
2 i = j = 1
3 while (y(i, j+1) > y(i, j) or y(i-1, j) > y(i, j))
4 if (y(i, j+1) > y(i, j))
5 j = j+1 (mod q) % clockwise
6 else
7 i = i-1 (mod p) % anti-clockwise
8 return (a_i, b_j)
1 [Link]
resources/lecture-2-notes/
17
3.5. Envoltura convexa
b1
b0
a3
a2 b2
a0 b3
a1
b1
b0
a2 b2
a3
b3
a0
a1
b1
b0
a2 b2
a3
b3
a0
a1
18
CAPÍTULO 4
Algoritmos codiciosos
ai a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11
si 1 3 0 5 3 5 6 8 8 2 12
fi 4 5 6 7 9 9 10 11 12 14 16
Cuadro 4.1: Ejemplo de la selección de actividades.
19
4.1. Selección de actividades
1 GREEDY-SA(s, f, n)
2 A = {a_1}
3 k = 1
4 for m = 2 to n
5 if s[m] ≥ f[k] % esta a_m en S_k?
6 A = A ∪ {a_m} % si, seleccionarlo
7 k = m % continuar desde m
8 return A
9 GREEDY-RECURSIVO-SA(s, f, k, n)
10 m = k + 1
11 while m ≤ n and s[m] < f[k] % primero en Sk en terminar
12 m = m + 1
13 if m ≤ n
14 return {a_m} ∪ GREEDY-RECURSIVO-SA(s, f, m, n)
15 else
16 return ∅
20
4.2. Algoritmo de Kruskal
8 7
b c d
4 9
2
a 11 i 14 e
4
7 6
8 10
h g f
1 2
La Figura 4.2 muestra la solución al problema T = {(a, b), (b, c), (c, d), (d, e),
(c, f ), (c, i), (f, g), (g, h)} con peso mínimo w(T ) = 37. Como T es acíclico
y conecta todos los nodos, debe formar un árbol, el cual se conoce co-
mo árbol recubridor mínimo (MST por las siglas minimum spanning tree).
Cabe destacar que el MST no es único, por ejemplo otra solución es
T = {(a, b), (a, h), (c, d), (d, e), (c, f ), (c, i), (f, g), (g, h)} intercambiando la aris-
ta (b, c) por (a, h).
Para encontrar la solución al problema se deben definir algunos conceptos. Un
corte (S, V − S) de un grafo G = (V, E) es una división de V en dos conjuntos
S y V − S. Una arista (u, v) ∈ E cruza el corte (S, V − S) si uno de sus nodos
pertenece a S y el otro pertenece a V − S. Un corte respeta un conjunto de
aristas si ninguna de ellas cruza el corte. Una arista es ligera al satisfacer una
propiedad si su peso es el mínimo de todas las aristas con la misma propiedad.
21
4.2. Algoritmo de Kruskal
4
8 7
b c d b
8
4 9
c
2
2 4 7
a 11 i 14 e i f d
4
2 9
7 6
g e
8 10
1
h g f
1 2 h
8 7
b c d
4 9
2
S↑ a 11 i 14 e
4
7 6
8 10
V −S ↓ h g f
1 2
22
4.2. Algoritmo de Kruskal
(u, v) hace posible llegar desde u a v recorriendo el MST, por lo que agregar
(u, v) a una nueva solución A ∪ (u, v) ∈ T ′ necesariamente genera un ciclo. Esto
indica que hay una arista (x, y) ∈ / A en el camino entre u y v que cruza el
corte. Quitar (x, y) de la solución T , la divide en dos árboles que se pueden
volver a unir agregando (u, v) a una nueva solución T ′ = (T − (x, y)) ∪ (u, v).
Como (u, v) es una arista ligera al cruzar el corte, remover (x, y) y agregar
(u, v) mantiene el peso mínimo w(T ′ ) ≤ w(T ) ■
u
y
El teorema nos permite iterar sobre |E| agregando en cada ciclo la arista
de valor mínimo que no genere un ciclo en la rutina MST-KRUSKAL. El ciclo
for con la subrutina MAKE-SET genera |V | conjuntos disconexos (reemplazables
por estructuras de datos tipo heaps para el análisis) en tiempo O(|V |). La
subrutina FIND-SET(u) ̸= FIND-SET(u) realiza dos búsquedas en los conjuntos
disconexos (o heaps) en O(log(|V |)) para verificar que no existan ciclos, luego
lo agrega al conjunto de solución en tiempo O(1) y la subrutina UNION junta los
conjuntos disconexos (o heaps) en una estructura en O(log(|V |)). El tiempo final
del algoritmo es T (n) = |V | + |E| log(|V |) → T (n) = O(|E| log(|V |)). Cabe
destacar que la implementación del algoritmo de Kruskal mejora bastante
en la mayoría de los casos utilizando estructuras de datos avanzadas como
disjoint-sets.
1 MST-KRUSKAL(G, w)
2 A = ∅
3 for v in_s G.V
4 MAKE-SET(v)
5 edges = G.E
6 [Link](key = w)
7 for (u,v) in edges
8 ̸
if ( FIND-SET(u) =
9 FIND-SET(v) )
10 A = A ∪ {(u, v)}
11 UNION(u, v)
12 return A
23
4.3. Códigos de Huffman
4 9 4 9
2 2
a 11 i 14 e a 11 i 14 e
4 4
7 6 7 6
8 10 8 10
h g f h g f
1 2 1 2
8 7 8 7
b c d b c d
4 9 4 9
2 2
a 11 i 14 e a 11 i 14 e
4 4
7 6 7 6
8 10 8 10
h g f h g f
1 2 1 2
8 7 8 7
b c d b c d
4 9 4 9
2 2
a 11 i 14 e a 11 i 14 e
4 4
7 6 7 6
8 10 8 10
h g f h g f
1 2 1 2
8 7 8 7
b c d b c d
4 9 4 9
2 2
a 11 i 14 e a 11 i 14 e
4 4
7 6 7 6
8 10 8 10
h g f h g f
1 2 1 2
Caracteres a b c d e f
Frecuencia (k) 45 13 12 16 9 5
Cuadro 4.2: Frecuencia de cada carácter en la secuencia.
24
4.3. Códigos de Huffman
caracteres más frecuentes códigos cortos y a los menos frecuentes códigos largos.
El Cuadro 4.3 muestra la representación de largo fijo y utilizando códigos de
largo variable con un total de 45 × 1 + 13 × 3 + 12 × 3 + 16 × 3 + 9 × 4 + 5 × 4
= 224.000 bits.
Caracteres a b c d e f
Frecuencia (k) 45 13 12 16 9 5
Códigos de largo fijo 000 001 010 011 100 101
Códigos de largo variable 0 101 100 111 1101 1100
Cuadro 4.3: Representación en códigos de largo fijo y variable.
100
0 1
86 14
0 1 0
58 28 14
0 1 0 1 0 1
a : 45 b : 13 c : 12 d : 16 e:9 f :5
100
0 1
a : 45 55
0 1
25 30
0 1 0 1
c : 12 b : 13 14 d : 16
0 1
f :5 e:9
25
4.3. Códigos de Huffman
x y
1 HUFFMAN(C)
2 n = |C|
3 Q = C
4 for i = 1 to n - 1
5 New z
6 x = EXTRACT-MIN(Q)
7 y = EXTRACT-MIN(Q)
8 [Link] = x
9 [Link] = y
10 [Link] = [Link] + [Link]
11 INSERT(Q, z)
12 return EXTRACT-MIN(Q)
14
0 1
f :5 e:9 c : 12 b : 13 d : 16 a : 45 c : 12 b : 13 f :5 e:9 d : 16 a : 45
30
0 1
14 25 25 14 d : 16
0 1 0 1 0 1 0 1
f :5 e:9 d : 16 c : 12 b : 13 a : 45 c : 12 b : 13 f :5 e:9 a : 45
100
55 0 1
0 1 a : 45 55
25 30
0 1
25 30
0 1 0 1
0 1 0 1
c : 12 b : 13 14 d : 16 c : 12 b : 13 14 d : 16
0 1 0 1
a : 45 f :5 e:9 f :5 e:9
26
4.3. Códigos de Huffman
27
CAPÍTULO 5
Algoritmos de ordenamiento
1 INSERTION-SORT(A, n)
2 for i = 2 to n
3 key = A[i]
4 j = i - 1
5 while j > 0 and A[j] > key
6 A[j + 1] = A[j]
7 j = j - 1
8 A[j + 1] = key
El análisis simple del tiempo de ejecución, permite ver que el ciclo for de
la línea 2, se ejecuta n − 1 veces ya que ninguna sentencia actualiza el valor
de i. En cada iteración se realizan tres actualizaciones de valores (líneas 3,
4 y 8) en tiempo constante O(1) y luego un ciclo while en la línea 5. Esta
iteración en el peor caso se ejecuta 1, 2, . . . , n − 1 veces en cada repetición del
ciclo for, realizando dos actualizaciones de valores (líneas 6 y 7) en tiempo
constante O(1). Esto permite calcular el tiempo de ejecución como la suma
Pn−1 2
i=1 i que da como resultado un tiempo asintótico T (n) = O(n ). Este peor
28
5.2. Quick sort
1 QUIC-KSORT(A, p, r)
2 if p < r
3 q = PARTITION(A, p, r)
4 QUICKSORT(A, p, q - 1)
5 QUICKSORT(A, q + 1, r)
6 PARTITION(A, p, r)
7 x = A[r]
8 i = p - 1
9 for j = p to r - 1
10 if A[j] ≤ x
11 i = i + 1
12 swap A[i] with A[j]
13 swap A[i + 1] with A[r]
14 return i + 1
i ↓
a) A 5 2 4 6 1 3 b) A 2 5 4 6 1 3
j ↑
i ↓ i +1 ↓
c) A 2 1 4 6 5 3 d) A 2 1 3 6 5 4
j ↑ r ↑
Figura 5.1: Intercambios o swap de la rutina PARTITION con x = 3.
29
5.2. Quick sort
q
a) b)
6 5 4 3 2 1 1 5 4 3 2 6
q q
c) d)
5 4 3 2 6 2 4 3 5
q q
e) f)
4 3 5 3 4
Figura 5.2: Pivote q de la rutina QUICK-SORT en el peor caso.
En este caso, se aprecia que en cada iteración sólo hay una llamada recursiva
(el pivote se omite, esta en la primera o la última posición y solo se utiliza la parte
izquierda o la derecha) y la parte no recursiva, la subrutina PARTITION, realiza
O(n) iteraciones. Esta recurrencia T (n) = T (n − 1) + n tiene como solución
T (n) = O(n2 ) que corresponde al tiempo de QUICK-SORT en el peor caso.
Para los casos donde el arreglo no está ordenado de mayor a menor,
se aprecia claramente que existen dos llamadas recursivas pero no es tan
claro en qué razón se dividió el arreglo en cada iteración, pero si se
aprecia que el trabajo no recursivo de la subrutina PARTITION se realiza en
tiempo O(n), ya que cada ciclo for recorre cada partición recursiva en su
totalidad. En el mejor caso el pivote siempre divide el arreglo en mitades,
resultando en una recurrencia T (n) = 2T (n/2) + n con tiempo de ejecución
T (n) = O(n log(n)). Incluso si la partición está desbalanceada en razón 9 es a 1,
la recurrencia T (n) = T (n/10) + T (9n/10) + n entrega un tiempo de ejecución
T (n) = O(n log(n)). Una forma simple de distribuir la partición de las llamadas
recursivas es escoger un pivote aleatorio. La rutina RANDOMIZED-QUICKSORT
intercambia el último elemento que corresponde al pivote en QUICK-SORT por
una posición aleatoria (sumando solo una operación de tiempo constante que
no afecta el tiempo de ejecución) lo que en la práctica se traduce en un tiempo
de ejecución de T (n) = O(n log(n)).
1 RANDOMIZED-QUICKSORT(A, p, r)
2 if p < r
3 q = RANDOMIZED-PARTITION(A, p, r)
4 RANDOMIZED-QUICKSORT(A, p, q - 1)
5 RANDOMIZED-QUICKSORT(A, q + 1, r)
6 RANDOMIZED-PARTITION(A, p, r)
7 i = RANDOM(p, r)
8 swap A[r] with A[i]
9 return PARTITION(A, p, r)
30
5.3. Heap sort
16
1
2 3
14 10
4 5 6 7
8 7 9 3
8 9 10
2 4 1
1 HEAP-SORT(A, n)
2 BUILD-MAX-HEAP(A, n)
3 for i = n downto 2
4 swap A[1] with A[i]
5 [Link]-size -= 1
6 MAX-HEAPIFY(A, 1)
7 BUILD-MAX-HEAP(A, n)
8 [Link]-size = n
9 for i = floor(n/2) down to 1
10 MAX-HEAPIFY(A, i)
11 MAX-HEAPIFY(A, i)
12 s = [Link]-size
13 l = LEFT(i)
14 r = RIGHT(i)
15 if l ≤ s and A[l] > A[i]
16 m = l
17 else m = i
18 if r ≤ s and A[r] > A[m]
19 m = r
20 if m ̸= i
21 swap A[i] with A[m]
22 MAX-HEAPIFY(A, m)
31
5.3. Heap sort
a)
4 4
1 1
2 3 2 3
1 3 1 3
4 5 6 7 4 5 6 7
2 16 9 10 2 16 9 10
8 9 8 9
10 10
14 8 7 14 8 7
b) c)
4 4
1 1
2 3 2 3
1 3 1 10
4 5 6 7 4 5 6 7
14 16 9 10 14 16 9 3
8 9 10 8 9 10
2 8 7 2 8 7
d) e)
4 16
1 1
2 3 2 3
16 10 14 10
4 5 6 7 4 5 6 7
14 7 9 3 8 7 9 3
8 9 10 8 9 10
2 8 1 2 4 1
32
5.3. Heap sort
padre. Así podemos ver que en la primera iteración a) no hay intercambio (el
valor máximo es m que es igual a i), en la segunda iteración b) hay intercambio
entre i y m que es igual a l y en la tercera iteración c) hay intercambio entre i
y m que es igual a r. En las dos últimas iteraciones donde hay intercambio de
valores, la llamada recursiva a MAX-HEAPIFY termina ya que los valores de l y
r para m son mayores al heap-size. En la cuarta iteración d) hay intercambio
entre i y m que es igual a r y recursivamente este ya cumple con la condición
del heap máximo. En la última iteración e) hay intercambio entre i y m que es
igual a l, recursivamente este se intercambia por su nodo l y este a su vez por
su nodo r. El tiempo de ejecución de la rutina BUILD-MAX-HEAP corresponde a
un ciclo for de ⌊n/2⌋ iteraciones (y tiempo O(\)) donde se ejecuta la rutina
MAX-HEAPIFY. Esta rutina en el peor caso recorre el árbol desde el nodo raíz
hasta el nivel inferior el cual tiene altura log2 (n), realizando comparaciones
y swap en tiempo constante O(1). Así, el tiempo de ejecución de la rutina
BUILD-MAX-HEAP corresponde a T (n) = O(n log(n)).
a)
16
14
1
2 3 1
2 3
14 10
8 10
4 5 6 7
4 5 6 7
8 7 9 3
4 7 9 3
8 9
8 9 10
2 4 1
2 1 16
10
b) c)
10 9
1 1
2 3 2 3
8 9 8 3
4 5 6 7 4 5 6 7
4 7 1 3 4 7 1 2
8 9 10 8 9 10
2 14 16 10 14 16
33
5.4. Cota inferior para el ordenamiento
d) e)
8 7
1 1
2 3 2 3
7 3 4 3
4 5 6 7 4 5 6 7
4 2 1 9 1 2 8 9
8 9 10 8 9 10
10 14 16 10 14 16
f) g)
4 3
1 1
2 3 2 3
2 3 2 1
4 5 6 7 4 5 6 7
1 7 8 9 4 7 8 9
8 9 10 8 9 10
10 14 16 10 14 16
h) i)
2 1
1 1
2 3 2 3
1 3 2 3
4 5 6 7 4 5 6 7
4 7 8 9 4 7 8 9
8 9 10 8 9 10
10 14 16 10 14 16
34
5.5. Counting sort
A = {2, 5, 3}: comenzando desde el nodo raiz la comparacion 1:2 (indices 1:2 y
valores 2:3) resulta 2 ≤ 3 y se continúa al nodo hijo izquierdo, la comparación
2:3 resulta 5 > 3 (se intercambian los valores resultando en la permutación
[1,3,2]) y se continúa al nodo hijo derecho, finalmente la comparación 1:3 resulta
2 ≤ 5 y el algoritmo termina en la permutación [1,3,2] que corresponde al arreglo
ordenado A′ = {2, 3, 5}.
1:2
≤ >
2:3 1:3
≤ > ≤ >
≤ > ≤ >
Figura 5.6: Árbol de decisión para insertion sort sobre el arreglo A = {2, 5, 3}.
Como la altura del árbol binario es log2 (n!), comenzando desde el nodo raíz
un algoritmo correcto debe llegar a alguna de las permutaciones que pueden
estar en el nivel más bajo. A partir de esto, la mínima cantidad de comparaciones
y por ende la cota inferior para los algoritmos de ordenamiento de comparación
es Ω(log(n!)) → Ω(n log(n)) correspondiente a la altura del árbol de decisión.
A partir de este resultado, los algoritmos merge sort y heap sort son algoritmos
de ordenamiento asintóticamente óptimos (quick sort entrega el mismo tiempo
pero en promedio debido a la selección aleatoria del pivote).
1 COUNTING-SORT(A, n, k)
2 crear B[1 : n], C [0 : k]
3 for i = 0 to k
4 C[i] = 0
5 for j = 1 to n
6 C[A[j]] = C[A[j]] + 1
7 for i = 1 to k
8 C[i] = C[i] + C[i - 1]
9 for j = n downto 1
10 B[C[A[j]]] = A[j]
11 C[A[j]] = C[A[j]] - 1
35
5.6. Radix sort
A[8] = 3 A[7] = 0
C[A[8]] = 7 C[A[7]] = 2
B = 3 B = 0 3
C = 2 2 4 6 7 8 C = 1 2 4 6 7 8
A[6] = 3 A[5] = 2
C[A[6]] = 6 C[A[5]] = 4
B = 0 3 3 B = 0 2 3 3
C = 1 2 4 5 7 8 C = 1 2 3 5 7 8
A[4] = 0 A[3] = 3
C[A[4]] = 1 C[A[3]] = 5
B = 0 0 2 3 3 B = 0 0 2 3 3 3
C = 0 2 3 5 7 8 C = 0 2 3 4 7 8
A[2] = 5 A[1] = 2
C[A[2]] = 8 C[A[1]] = 3
B = 0 0 2 3 3 3 5 B = 0 0 2 2 3 3 3 5
C = 0 2 3 4 7 7 C = 0 2 2 4 7 7
Figura 5.7: Iteraciones del último ciclo del algoritmo counting sort.
36
5.6. Radix sort
arreglo de ejemplo A = {329, 457, 657, 839, 436, 720, 355} con d = 3 y k = 839
obteniendo la versión ordenada A′ = {329, 355, 436, 457, 657, 720, 839}.
1 RADIX-SORT(A, n, d)
2 for i = 1 to d
3 COUNTING-SORT A en base al digito i
37
CAPÍTULO 6
Programación dinámica
6.1. SRTBOT
La sigla SRTBOT se refiere al paradigma de diseño de algoritmos recursivos
usando programación dinámica y memoización. Sus elementos se definen como:
38
6.2. Corte de una varilla
i 1 2 3 4 5 6 7
p(i) 1 10 13 18 20 31 32
Cuadro 6.1: Largos de varillas y sus valores.
El algoritmo ingenuo para resolver el problema itera sobre los valores posibles
i con los que se puede cortar n y resuelve recursivamente sumando el valor y
reduciendo el problema en n − i con caso base n = 0 con costo 0. La rutina
CUT-ROD implementa esta solución y la Figura 6.1 muestra el árbol de recursión
del algoritmo.
1 CUT-ROD(p, n)
2 if n == 0
3 return 0
4 q = -∞
5 for i = 1 to n
6 q = max{q, p(i) + CUT-ROD(p, n - i)}
7 return q
0 1 2 3 4 5 6
1 10
3 2 13 1 0
18
1 1
2 10 1 0 1 0 0
13 10 1
1 1 0 0 0
10 1 1
1 0
Figura 6.1: Árbol de recursión del algoritmo ingenuo para el problema del corte
de una varilla.
39
6.2. Corte de una varilla
1 PD-CUT-ROD(p, n, r) % r = [- ∞, - ∞, . . . ]
2 if r[n] ≥ 0
3 return r[n]
4 if n == 0
5 q = 0
6 else
7 q = -∞
8 for i = 1 to n
9 q = max{q, p(i) + PD-CUT-ROD(p, n - i, r)}
10 r[n] = q
11 return q
40
6.2. Corte de una varilla
0 1 2 3 4 5 6
1 10
3 2 13 1 0
18
1 1
2 10 1 0 1 0
13 10
1 0
Figura 6.2: Árbol de recursión del algoritmo PD para el problema del corte de
una varilla.
1 BUPD-CUT-ROD(p, n)
2 r[0 : n], s[1 : n]
3 r[0] = 0
4 for j = 1 to n
5 q = -∞
6 for i = 1 to j
7 if q < p[i] + r[j - i]
8 q = p[i] + r[j - i]
9 s[j] = i
10 r[j] = q
11 return r, s
n 0 1 2 3 4 5 6 7
p(n) 0 1 10 13 18 20 31 32
r(n) 0 1 10 13 20 23 30 33
s(n) 0 1 2 3 2 2 2 3
Cuadro 6.2: Soluciones parciales del problema del corte de una varilla.
Una forma de visualizar las soluciones parciales y los cortes de cada solución
óptima es a partir del grafo direccionado acíclico (DAG) que muestra el camino
desde los valores de n hasta el caso base y los valores óptimos. La lectura se
41
6.3. Bowling
32
31
20 20
18 18
13 13 13
10 10 10
7 1 6 1 5 1 4 1 3 1 2 1 1 1 0
10 10 10
13 13
18 18
20
31
Figura 6.3: DAG del algoritmo PD para el problema del corte de una varilla.
6.3. Bowling
El problema consiste en un juego creado por el Dr. Erik Demaine1 , en el
cual se tienen n pines 1, 2, . . . , n y cada pin i tiene valor vi . Solo se puede pasar
un pin, derribar un pin o dos pines en cada tiro, si se derriba el pin i se suman
vi puntos, si se derriban dos pines i e i + 1 se suman vi · vi+1 puntos y pasar
no suma puntos. El objetivo es obtener el mayor puntaje posible.
1 1 9 9 2 -5 -5
i 1 2 3 4 5 6 7
v(i) 1 1 9 9 2 -5 -5
Cuadro 6.3: Valores de cada pin del problema de bowling para n = 7.
1 [Link]
006s20_lec15/
42
6.3. Bowling
1 PD-BOWLING(v, n, r) % r = [- ∞, - ∞, . . . ]
2 if r[n] ≥ 0
3 return r[n]
4 if n == 0
5 q = 0
6 else
7 q = -∞
8 if i > 1
9 q = max{q, BOWLING(v, i-1, r),
10 BOWLING(v, i-1, r) + v(i),
11 BOWLING(v, i-2, r) + v(i)*v(i-1)}
12 else
13 q = max{q, BOWLING(v, i-1, r),
14 BOWLING(v, i-1, r) + v(i)}
15 r[n] = q
16 return q
La Figura 6.5 muestra el árbol de recursión, donde los nodos hijos derechos
corresponden a los subproblemas i − 2, los nodos hijos centrales corresponden
a los subproblemas i − 1 y los nodos hijos izquierdos corresponden a los
subproblemas i − 1 sin contar el valor (pasar). El tiempo de ejecución del
algoritmo se puede calcular como la cantidad de nodos del árbol de recursión, ya
que todo el trabajo no recursivo corresponde a operaciones de tiempo constante,
resultando en un tiempo de ejecución T (n) = 2 + 3n → T (n) = O(n). La rutina
BUPD-BOWLING implementa la versión iterativa con los cálculos de las soluciones
parciales, donde se aprecia más claramente el tiempo de ejecución T (n) = O(n)
por el ciclo for de n iteraciones con operaciones de tiempo constante.
43
6.3. Bowling
7
0 25
6 6 -5 5
0
5 5 4 4 4 3 18
0 -5 -10 2
0 3 3 2 81
9
2 2 1 1 1 0
0 9 9 0 1 1
0 0 0 1
1 BUPD-BOWLING(v, n, r, s)
2 r[0 : n], s[1 : n]
3 r[0] = 0
4 q = -∞
5 for i = 1 to n
6 if i > 1
7 q = max{q, r[i-1], r[i-1] + v(i),
8 r[i-2] + v(i)*v(i-1)}
9 s[i] = argmax{r[i-1], r[i-1] + v(i),
10 r[i-2] + v(i)*v(i-1)}
11 else
12 q = max{q, r[i-1], r[i-1] + v(i)}
13 s[i] = argmax{r[i-1], r[i-1] + v(i)}
14 r[n] = q
15 return r, s
La Figura 6.6 muestra el DAG y el Cuadro 6.4 los valores de las soluciones
parciales del problema.
9 18 25
0 1 1 1 2 9 3 9 4 2 5 -5 6 -5 7
0 0 0 0 0 0 0
1 81 -10
i 0 1 2 3 4 5 6 7
v(i) 0 1 1 9 9 2 -5 -5
r(i) 0 1 2 11 83 85 85 110
s(i) 0 1 1 1 2 1 1 2
Cuadro 6.4: Soluciones parciales del problema de bowling.
44
6.4. Parentización
6.4. Parentización
El problema de parentización corresponde a encontrar la ubicación de
paréntesis (de manera consistente) para obtener el máximo valor de una fórmula
a0 ∗1 a1 ∗2 . . . ∗n−1 an−1 , donde ak ∈ N y ∗k ∈ {+, ×}. Ejemplo: 7 + 4 × 3 + 5
tiene como resultado máximo (7 + 4) × (3 + 5) = 88. Para resolver el problema,
los subproblemas deben ser desarrollados como substrings, ya que los elementos
ai y ai+2 no se relacionan (no hay operación + o × entre ellos). El SRTBOT
para el problema de bowling se define como:
1 PD-PARENTHESIZATION(a, s, i, j) % r = [- ∞, - ∞, . . ]
.
2 if r[i,j] ≥ 0
3 return r[i,j]
4 if i == j
5 q = a[i]
6 else
7 q = -∞
8 for k = i to j-1
9 q = max{q, PD-PARENTHESIZATION(a, s, i, k) s[k+1]
10 PD-PARENTHESIZATION(a, s, k+1, j) }
11 r[i,j] = q
12 return q
k 0 1 2 3
ak 7 4 3 5
∗k + × +
Cuadro 6.5: Arreglos del problema de parentización.
45
6.5. Subsecuencia común más larga
7 + 4 × 3 + 5
j\i 0 1 2 3
7 0 7 0 0 0
+
4 1 11 4 0 0
×
3 2 33 12 3 0
+
5 3 88 32 8 5
1 BUPD-PARENTHESIZATION(a, s, n):
2 r[0,0 : n,n]
3 for j = 0 to n - 1
4 for i = 0 to n - j - 1
5 q = -∞
6 if j == 0
7 q = a[i]
8 for k = 0 to j - 1
9 q = max{q, r[i][i + k] s[i + k + 1] +
10 r[i + k + 1][i + j] }
11 if q > r[i][i + j]:
12 r[i][i + j] = q
13 return r
n 0 1 2 3 4
A T H E I R
B H A B I T
Cuadro 6.6: Cadenas de texto del problema LCS.
46
6.5. Subsecuencia común más larga
1 BUPD-LCS(a, b):
2 r = r[0,0 : |A|,|B|]
3 for i = |A| - 1 to 0
4 for j = |B| - 1 to 0
5 if a[i] == b[j]:
6 r[i][j] = 1 + r[i+1][j+1]
7 else:
8 r[i][j] = max(r[i][j+1], r[i+1][j]])
9 return r
47
6.6. Suma de subconjuntos
T H E I R
j\i 0 1 2 3 4 5
H 0 2 2 0 0 0 0
A 1 1 1 1 1 0 0
B 2 1 1 1 1 0 0
I 3 1 1 1 1 0 0
T 4 1 0 0 0 0 0
5 0 0 0 0 0 0
Figura 6.8: DAG y soluciones parciales del algoritmo PD para el problema LCS.
i 0 1 2 3
A 3 4 3 1
Cuadro 6.7: Conjunto de ejemplo del problema.
48
6.6. Suma de subconjuntos
3 4 3 1
t\i 0 1 2 3 4
4 1 1 1 0 0
3 1 1 1 0 0
2 0 0 0 0 0
1 1 1 1 1 0
0 1 1 1 1 1
1 BUPD-SUBSET-SUM(a, t, n)
2 r = [0,0 : (t + 1),(n + 1)]
3 for i = 0 to n
4 r[i][0] = 1
5 for i = n - 1 to 0
6 for j = t to 1:
7 if a[i] > j
8 r[i][j] = r[i+1][j]
9 else
10 r[i][j] = OR{r[i+1][j], r[i+1][j-a[i]]}
11 return r
49
CAPÍTULO 7
Algoritmos en grafos
11 11
a b a b
3 5 1 2 5
d c d c
3 3
a) b)
Figura 7.1: a) Grafo no direccionado y b) grafo direccionado.
50
7.2. Definiciones
7.2. Definiciones
Un par de vértices son vecinos si existe una arista entre ellos. Para grafos diri-
gidos se define el conjunto de vértices de salida adj + (u) = {v ∈ V | (u, v) ∈ E} y
el conjunto de vértices de entrada adj − (u) = {v ∈ V | (v, u) ∈ E}. Para grafos no
dirigidos los vecinos de entrada y salida son iguales adj + (u) = adj − (u) = adj(u).
Un camino de largo k es una secuencia (no un conjunto) de vértices
P = (v1 , v2 , . . . , vk ) donde (vi , vi+1 ) ∈ E para i desde 1 a k, donde el largo
o distancia d(v1 , vk ) = k. Un conjunto de nivel k corresponde a todos los
vértices que se encuentran a distancia k, es decir los últimos vértices de todos
los caminos de largo k, de un vértice s de origen Lk = {v ∈ V | d(s, v) = k}.
1 BFS(G, s)
2 L_{0} = {s}
3 d(s,s) = 0
4 P(s) = {}
5 i = 1
6 while L_{i-1} ̸= {}
7 for u in L_{i-1} and (v in adj+(u) - w in L_{0:i-1})
8 L_{i} = L_{i} ∪ v
9 d(s,v) = i
10 P(v) = u
11 i += 1
51
7.3. Búsqueda en anchura
c d f
s a b g
s L0
a L1
b L2
c d e L3
g f L4
52
7.4. Búsqueda en profundidad
V = s a b c d e f g
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
a s a b b b d d
↓ ↓ ↓ ↓ ↓
b c f g e
↓ ↓
d g
↓
e
Figura 7.4: Lista de adyacencia del grafo de ejemplo.
constante O(1). El proceso continúa recorriendo cada columna para cada vértice
solo una vez, ya que los vértices que ya pertenecen a un conjunto de nivel
son omitidos en la iteración. Como la lista de adyacencia para grafos dirigidos
tiene tamaño |V | + |E| y para grafos no dirigidos 2(|V | + |E|), su recorrido
realizando operaciones de tiempo constante resulta en un tiempo de ejecución
T (n) = O(|V | + |E|).
1 DFS(G,s)
2 P(s) = {0}
3 visit(s)
4 visit(u)
5 for v in adj+(u)
6 if P(v) = {}
7 P(v) = u
8 visit(v)
53
7.5. Algoritmo de Dijkstra
El resultado entrega el camino entre los vértices conexos pero no entrega las
distancias. La recursión busca en profundidad el camino desde el origen al nodo
más lejano y luego realiza un backtracking actualizando los predecesores de cada
uno de los vértices. A diferencia de la rutina BFS, la búsqueda en profundidad
verifica si el vértice tiene un predecesor y omite recorrer sus vecinos. Esto logra
que la rutina DFS recorra todas las |E| aristas actualizando en cada llamada
recursiva el predecesor correspondiente en tiempo constante O(1). El tiempo de
ejecución final es T (n) = O(|E|). Esta mejora en el tiempo de ejecución sobre
la búsqueda en anchura, permite generar una nueva versión que calcula los
vértices conexos considerando todos los vértices como origen. Esto tiene una
aplicación muy común que es buscar los predecesores para grafos no conexos.
La rutina FULL-DFS implementa esta versión modificada.
1 FULL-DFS(G)
2 for v in V
3 if P(v) = {}
4 DFS(G,v)
c d f
s a b g
54
7.5. Algoritmo de Dijkstra
para el nodo de origen u e ∞ para todos los otros nodos. Luego se define el
proceso de relajación para dos nodos u y v donde (u, v) ∈ E que corresponde a
actualizar el valor de prioridad del vértice v si su prioridad es mayor a la suma
valor de prioridad del vértice u con el peso w(u, v). Las rutinas INITIALIZE y
RELAX implementan los procesos de inicialización y relajación respectivamente.
1 INITIALIZE(G, s)
2 for v in V
3 v.d = ∞
4 Q = Q ∪ {v}
5 s.d = 0
5 Q = Q ∪ {s}
6 return Q, {}
8 RELAX(u, v, Q)
9 if v.d > u.d + w(u, v)
10 v.d = u.d + w(u, v)
11 P(v) = u
1 DIJKSTRA(G, s)
2 Q, S = INITIALIZE(G, s) % S = {}, Q = {{s,0}, {a, ∞ }, . . }
.
3 P(s) = {}
4 ̸ {}
while Q =
5 u = EXTRACT-MIN(Q)
6 S = S ∪ {u}
7 for v in adj+(u)
8 RELAX(u, v, Q)
55
7.5. Algoritmo de Dijkstra
a)
1 x, ∞ 1 x, ∞
t, ∞ t, 10
10 10
9 9
2 3 2 3
s, 0 5 4 6 s, 0 5 4 6
y, ∞ y, 5
2 2
z, ∞ z, ∞
7 7
b) c)
1 1
t, 8 x, 14 t, 8 x, 13
10 10
9 9
2 3 2 3
s, 0 5 4 6 s, 0 5 4 6
y, 5 y, 5
2 2
z, 7 z, 7
7 7
d) e)
1 1
t, 8 x, 9 t, 8 x, 9
10 10
9 9
2 3 2 3
s, 0 5 4 6 s, 0 5 4 6
y, 5 y, 5
2 2
z, 7 z, 7
7 7
56
7.6. Algoritmo de Bellman-Ford
1 BELLMAN-FORD(G, s)
2 Q = INITIALIZE(G, s) %Q = {{s,0}, {a, ∞ }, . . }
.
3 for i = 1 to |V| - 1
4 for (u, v) in E
5 RELAX(u, v, Q)
6 for (u, v) in E
7 if v.d > u.d + w(u, v)
8 return FALSE
9 return TRUE
a) b)
5 5
t, 6 x, ∞ t, 6 x, 4
-2 -2
6 6
-3 -3
8 8
s, 0 7 -4 7 s, 0 7 -4 7
y, 7 y, 7
9 9
2 z, ∞ 2
z, 2
c) d)
5 5
t, 2 x, 4 t, 2 x, 4
-2 -2
6 6
-3 -3
8 8
s, 0 -4 s, 0 7 -4 7
7 7
y, 7 y, 7
9 9
2 2
z, 2 z, −2
57
7.6. Algoritmo de Bellman-Ford
58
CAPÍTULO 8
8.1. Motivación
Un análisis simple nos permite concluir que la mayoría de los problemas
de decisión son en realidad indecidibles, es decir ∈ / R donde R es el conjunto
de problemas que se pueden resolver en tiempo finito. La noción es pensar en
un programa como una cadena de bits finita (por ejemplo transformándolo a
lenguaje de máquina) que a su vez es un número ∈ N (transformando la cadena
de bits finita a un decimal ∈ N). Por otro lado un problema de decisión es una
función que transforma una entrada, que corresponde cadena de bits infinita
ya que existen infinitas entradas para cada problema, en una salida ∈ {0, 1}.
Se puede pensar en un problema de decisión como una cadena de bits infinita
{b0 , b1 , . . . bi , . . .} donde bi ∈ {0, 1} es la salida y el índice i es la entrada. La
cantidad de programas es un conjunto infinito numerable, se puede contar de
uno en uno los números enteros de cero al infinito. Por otro lado, la cantidad
de problemas de decisión es un conjunto infinito no numerable, ya que cada
problema de decisión es en sí una cadena de bits infinita y no existe una noción
de conteo para múltiples infinitos. Este análisis se puede resumir en que la
cantidad de programas infinitos es mucho menor que la cantidad de problemas
de decisión.
8.2. La clase P
Definimos un algoritmo de la clase P si a partir de una entrada de tamaño
n se obtiene la solución en el peor caso en O(nk ) para alguna constante
k ≥ 1. Recordamos el caso especial denominado pseudopolinomial con el
problema de la suma de subconjuntos: problema de decisión, es decir que tiene
respuesta es True o False, que dado un conjunto A = {a0 , a1 , . . . , an−1 } de
n enteros y un valor t donde ai , t ∈ N busca responder la pregunta ¿Existe
algún subconjunto S ⊆ A donde la suma de sus valores ai sea igual a t?
59
8.3. La clase NP
8.3. La clase NP
La clase NP es el conjunto de problemas de decisión donde una solución
se puede verificar en tiempo polinomial. Como ejemplo, para la suma de
subconjuntos A = {3, 4, 3, 1} y T = 4, verificar que cualquier solución (por
ejemplo r = {3,P1}) es correcta (resultado True) se puede realizar con la
operación (s = ai ∈r ai ) == t ∧ ai ∈ A en O(n). No nos dice nada sobre
cómo se obtuvo la solución, que ya sabemos que por ejemplo para la suma de
subconjuntos encontrar la solución podría no estar en P.
NP-H
NP
↑ Complejidad
NP-C
Figura 8.1: Clases de complejidad para algoritmos.
60
8.6. El problema SAT y 3-SAT
61
8.7. 3-SAT y la suma de subconjuntos
62
8.8. 3-SAT y el problema clique
- x1 x2 x3 c1 c2 c3 c4
v1 = 1 0 0 1 0 0 1
v1′ = 1 0 0 0 1 1 0
v2 = 0 1 0 0 0 0 1
v2′ = 0 1 0 1 1 1 0
v3 = 0 0 1 0 0 1 1
v3′ = 0 0 1 1 1 0 0
s1 = 0 0 0 1 0 0 0
s′1 = 0 0 0 2 0 0 0
s2 = 0 0 0 0 1 0 0
s′2 = 0 0 0 0 2 0 0
s3 = 0 0 0 0 0 1 0
s′3 = 0 0 0 0 0 2 0
s4 = 0 0 0 0 0 0 1
s′4 = 0 0 0 0 0 0 2
t= 1 1 1 4 4 4 4
Cuadro 8.1: Tabla de reducción de 3-SAT a la suma de subconjuntos.
2 3 2 3
1 4 4
0 5 5
a) b)
Figura 8.2: a) Grafo no dirigido de ejemplo y b) un clique.
63
8.8. 3-SAT y el problema clique
c1 x1 ¬x2 ¬x3
c2 ¬x1 x1 c3
x2 x2
x3 x3
64
8.9. Clique y vertex cover
u v u v
z w z w
y x y x
a) b)
Figura 8.4: a) Grafos no dirigido de ejemplo G y b) Ḡ.
65
CAPÍTULO 9
66
9.1. K vecinos más cercanos
x1 x2 Distancia Clase
40 36 20.4 Virus
30 30 31.6 Virus
32 45 28.4 Virus
37 55 27.5 Virus
40 47 21.2 Virus
52 40 8.0 Virus
48 59 22.5 Bacteria
60 67 27.0 Bacteria
68 50 12.8 Bacteria
40 65 32.0 Bacteria
55 55 15.8 Bacteria
58 68 28.1 Bacteria
Cuadro 9.1: Conjunto de datos de ejemplo.
Virus Virus
Bacteria Bacteria
60 60
40 40
40 60 40 60
67
9.2. Gradiente descendiente
Virus Virus
Bacteria Bacteria
60 60
40 40
40 60 40 60
0
1
↑
0,8 Mínimo
local
−0,5
f (x)
f (x)
0,6
0,4
−1
↑
0,2 Mínimo Mínimo
global global
↓
0 −1,5
−1 −0,5 0 0,5 1 −10 −5 0 5 10
x x
a) b)
Figura 9.3: Ejemplos de a) función convexas y b) no convexa.
1 1DGD(x,f,f',e,n)
2 r_old = x
3 r_new = r_old - n*f'(r_old)
4 while |f(r_new) - f(r_old)| > e
5 r_old = r_new
6 r_new = r_old - n*f'(r_old)
7 return r_new
68
9.3. Backpropagation
0,2 0,2
0 0
−0,2 −0,2
−0,4 −0,4
f (x)
f (x)
−0,6 −0,6
−0,8 −0,8
−1 −1
−10 −5 0 5 10 −10 −5 0 5 10
x x
a) b)
Figura 9.4: Ejemplos de a) mínimo local y b) mínimo global.
9.3. Backpropagation
Backpropagation es una metodología para entrenar redes neuronales basada
en la actualización de parámetros utilizando la propagación del error de una
solución inicial mediante versión estocástica del gradiente descendiente. Podemos
definir de manera simple una red neuronal para predecir un valor único ŷ como
una secuencia de operaciones matemáticas bien definidas, donde cada capa
corresponde a una multiplicación de matrices y vectores
x1 • w1 1
w5
w
2
3
w3
w6
x2 • w4 2
69
9.3. Backpropagation
(
0
1 w1 w2 2 w5 1 x si x > 0,
A = x1 x2 , W = , W = , f (x) =
w3 w4 w6 0 si x ≤ 0
y f 2 (x) = x.
(9.3)
∇W Loss(yˆi , y i ) (9.4)
la cual busca minimizar una función global
n
nX o
mı́n Loss(ŷ i , y i ) (9.5)
W
1
70
9.3. Backpropagation
∂A1 ∂A1 ∂u
→ (9.10)
∂w1 ∂u ∂w1
para u = 1w1 + 0w3 aplica una nueva regla de la cadena. El valor
(
∂A1 1 si u > 0,
= (9.11)
∂u 0 si u ≤ 0
71
Bibliografía
Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2022). Introduction
to Algorithms, Fourth Edition, (4rd ed.). The MIT Press.
72