Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Capítulo 4
Dividir y Conquistar
4.1 Generalidades
“Dividir y conquistar” (DyC) es una técnica de diseño de algoritmos que consiste en
descomponer la instancia a resolver en un número de subinstancias más pequeñas del
mismo problema, resolver sucesiva e independientemente cada una de estas
subinstancias, y luego combinar las subsoluciones calculadas de ese modo para obtener la
solución a la instancia original. Generalmente resulta en un algoritmo más eficiente que el
original.
Para el punto 2 se puede aplicar nuevamente la técnica DyC, hasta que se llegue a
subinstancias de tamaño suficientemente pequeño para ser resueltas inmediatamente.
F.C.A.D – U.N.E.R. 46
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
− function DyC(x)
− if x es lo bastante pequeño o simple then
− return adhoc(x)
− else
− Descomponer x en subinstancias x1, x2, …, xl más pequeñas.
− for i ← 1 to l do
− yi ← DyC(xi)
− end for
− Recomponer los yi para obtener una solución y para x.
− return y
− end if
El número de subinstancias, l, por lo general es pequeño e independiente de la
instancia particular a resolver.
Para que un algoritmo DyC sea valioso, se deben cumplir tres condiciones:
Si g(n) es el tiempo requerido por DyC en instancias de tamaño n, sin contar el tiempo
necesario para las llamadas recursivas, entonces el tiempo total t(n) utilizado por este
algoritmo DyC es algo de la forma:
f (n ) si n es lo suficientemente pequeño
t DyC (n ) =
lt DyC (n ÷ b ) + g (n ) en caso contrario
Si existe un entero k tal que g(n) ∈ Θ(nk), entonces se puede concluir que
( )
Θ n k si l < bk
( )
t (n ) ∈ Θ n k log n si l = bk
( )
Θ n
log b l
si l > bk
F.C.A.D – U.N.E.R. 47
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
La elección del umbral se complica por el hecho de que el valor óptimo en general no
depende sólo del algoritmo en cuestión, sino también de la implementación particular.
Más aún, para una implementación particular no hay un valor preciso para un umbral
óptimo. No podemos hablar entonces de un “umbral óptimo”, sino de un umbral
aproximadamente óptimo.
n 2 mseg si n ≤ n0
t DyC (n ) =
3t DyC (n / 2) + 16n mseg en caso contrario
4.1.4 Correctitud
F.C.A.D – U.N.E.R. 48
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
r = (w + x) × (y + z) = wy + (wz +xy) + xz
Luego de esa única multiplicación obtenemos la suma de los tres términos necesarios
para calcular el producto deseado. Así que podemos proceder de la siguiente forma
p = wy = 09 × 12 = 108
q = xz = 81 × 34 = 2754
r = (w +x) × (y +z) = 90 × 46 = 4140
y finalmente
F.C.A.D – U.N.E.R. 49
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
En el ejemplo aplicamos DyC una sola vez, pero en general es necesario aplicar
recursivamente la estrategia hasta llegar a operandos lo bastante pequeños. Por lo tanto,
para el tiempo del algoritmo se genera una recurrencia de la forma
t DyC (n ) =
( )
Θ n 2 si n es pequeño
3t DyC (n ÷ 2 ) + Θ(n ) si n es grande
Para mejorar este tiempo se puede utilizar el clásico algoritmo de búsqueda binaria,
que probablemente sea la aplicación más simple de DyC. Estrictamente hablando, no se
trata de un algoritmo DyC, ya que el número de subinstancias siempre es 1. A esta suerte
de degeneración de los algoritmos DyC se los denomina simplificación.
F.C.A.D – U.N.E.R. 50
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
− function recBin(T[i..j], x)
− if i = j then
− return i
− end if
− k ← (i + j) ÷ 2
− if x ≤ T[k] then
− return recBin(T[i..k], x)
− else
− return recBin(T[k+1..j], x)
− end if
1 2 3 4 5 6 7 8 9 10 11
−5 −2 0 3 8 9 12 12 12 26 31 ¿ x ≤ T[k] ?
i k j no
i k j si
i k j si
i-k j no
i-j i = j : stop
a si m = 1
t (m ) =
b + t (m 2) si no
Resolviendo, se obtiene que t(m) ∈ Θ(lg m) en el peor caso, y el mismo resultado aún
en el mejor caso.
− function iterBin(T[1..n], x)
− if x > T[n] then
− return n + 1
− end if
− i ← 1; j ← n
F.C.A.D – U.N.E.R. 51
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
− while i < j do
− k ← (i + j) ÷ 2
− if x ≤ T[k] then
− j←k
− else
− i←k+1
− end if
− end while
− return i
4.4 Ordenamiento
Sea T [1..n] un arreglo de n elementos. Nuestro problema es ordenar estos elementos
en orden ascendente. Ya vimos que este problema puede ser resuelto con los algoritmos de
ordenamiento por selección o inserción, o mediante heapsort. Recordemos que este último
método toma un tiempo en el Θ(n log n), tanto para el peor caso como para el caso
promedio, mientras que los dos primeros métodos toman tiempo cuadrático. Existen varios
algoritmos clásicos de ordenamiento que siguen el modelo Dividir y Conquistar.
Estudiaremos ahora dos de ellos: mergesort y quicksort.
F.C.A.D – U.N.E.R. 52
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
− mezcla(U, V, T)
− end if
Nótese que tanto U como V tienen un elemento más que el necesario. Esto se debe a
que el algoritmo de mezcla utiliza esta posición adicional al final del arreglo como
centinela, para que los subíndices dentro del ciclo for no caigan fuera de rango. La técnica
funciona si se puede asignar al centinela un valor mayor a cualquier elemento de los
arreglos U y V , lo que en el algoritmo siguiente aparece como “∞”.
Este algoritmo de ordenamiento ilustra bien todas las facetas de DyC. Cuando el
número de elementos a ordenar es pequeño, se usa un algoritmo relativamente simple. Por
otro lado, cuando el número de elementos lo justifica, mergesort separa la instancia en dos
subinstancias aproximadamente iguales, resuelve cada una recursivamente, y luego
combina las dos mitades ordenadas para obtener la solución a la instancia original. La
Figura 4.2 muestra un ejemplo, con un arreglo de 13 elementos.
Sea t(n) el tiempo utilizado por este algoritmo para ordenar un arreglo de n elementos.
La separación de T en U y V toma tiempo lineal. Es fácil ver que mezcla(U, V, T) también
toma tiempo lineal. En consecuencia, t(n) = t(n/2) + t(n/2) + g(n), donde g(n) ∈ Θ(n).
Esta recurrencia, que si n es par se transforma en t(n) = 2t(n/2)+g(n), es un caso especial
de nuestro análisis general para algoritmos DyC. En este caso, l = 2, b = 2 y k = 1. Como
l = bk, se aplica el segundo caso y por lo tanto se obtiene que t(n) ∈ Θ(n log n).
F.C.A.D – U.N.E.R. 53
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
Mergesort
3 1 4 1 5 9 2 6 5 3 5 8 9
Mergesort Mergesort
3 1 4 1 5 9 2 6 5 3 5 8 9
Mezcla
3 5 8 9
Mezcla
2 3 5 5 6 8 9
Mezcla
1 1 2 3 3 4 5 5 5 6 8 9 9
Como primer paso, este algoritmo elige como pívot uno de los elementos del arreglo a
ordenar. Luego el arreglo se divide a cada lado del pívot: los elementos se mueven de
forma tal que los que son mayores que el pívot están a su derecha, mientras que los
menores están a su izquierda. Si ahora las secciones del arreglo a cada lado del pívot se
F.C.A.D – U.N.E.R. 54
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
Para balancear los tamaños de las subinstancias a ordenar, podríamos querer usar el
elemento mediano como pívot. Desafortunadamente, encontrar el elemento mediano lleva
demasiado tiempo. Por esta razón, simplemente usamos un elemento arbitrario del arreglo
como pívot, esperando que sea el mejor.
F.C.A.D – U.N.E.R. 55
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
− end while
− intercambiar T[i] y T[l]
Arreglo a ordenar
3 1 4 1 5 9 2 6 5 3 5 8 9
El arreglo se pivotea por su primer elemento p = 3
3 1 4 1 5 9 2 6 5 3 5 8 9
Se encuentra el primer elemento mayor que el pívot (naranja)
y el último elemento no mayor que el pívot (celeste)
3 1 4 1 5 9 2 6 5 3 5 8 9
Se intercambian esos elementos (negrita)
3 1 3 1 5 9 2 6 5 4 5 8 9
Se busca de nuevo en ambas direcciones
3 1 3 1 5 9 2 6 5 4 5 8 9
Se intercambian
3 1 3 1 2 9 5 6 5 4 5 8 9
Se busca de nuevo
3 1 3 1 2 9 5 6 5 4 5 8 9
Los punteros se cruzaron (el naranja está a la derecha del celeste):
se intercambian el pívot con el celeste
2 1 3 1 3 9 5 6 5 4 5 8 9
El pivoteo está completo.
Se ordenan recursivamente los subarreglos a cada lado del pívot
1 1 2 3 3 4 5 5 5 6 8 9 9
Al final, el arreglo está ordenado
F.C.A.D – U.N.E.R. 56
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
Por otro lado, si el arreglo a ordenar está inicialmente en orden aleatorio, es probable
que la mayoría de las veces las subinstancias a ordenar estén bastante bien balanceadas.
Para determinar el tiempo promedio requerido por quicksort para ordenar un arreglo de n
elementos, debemos asumir cuál es la distribución de probabilidad de todas las instancias
de tamaño n. Lo más natural es asumir que los elementos de T son distintos y que cada una
de las n! posibles permutaciones iniciales de los elementos es igualmente probable. Es
necesario notar, sin embargo, que esta suposición puede ser inadecuada –o directamente
errónea– para algunas aplicaciones, en cuyo caso no se puede aplicar el análisis siguiente.
Este es el caso, por ejemplo, si la aplicación con frecuencia necesita ordenar arreglos que
ya están casi ordenados.
Sea t(m) el tiempo promedio utilizado por una llamada a quicksort(T[i..j]), donde
m = j – i + 1 es el número de elementos en el subarreglo. En particular, quicksort(T[1..n])
requiere el tiempo t(n) para ordenar todos los n elementos del arreglo. Por nuestra
suposición sobre la distribución de probabilidad de la instancia, el pívot elegido por el
algoritmo cuando se le pide ordenar T[1..n] cae con igual probabilidad en cualquier
posición con respecto a los demás elementos de T . Por lo tanto, el valor de l devuelto por
el algoritmo de pivoteo luego de la llamada inicial pivot(T[1..n], l) puede ser cualquier
entero entre 1 y n, donde cada valor tiene igual probabilidad 1/n. Esta operación de
pivoteo toma tiempo lineal g(n) ∈ Θ(n). Nos queda ordenar recursivamente dos
subarreglos de tamaño l – 1 y n – l, respectivamente. Se puede mostrar que la distribución
de probabilidad en los subarreglos sigue siendo uniforme. Por lo tanto, el tiempo promedio
requerido para ejecutar estas llamadas recursivas es de t(l–1) + t(n–l). En consecuencia,
1 n
t (n ) = ∑ (g (n) + t (l − 1) + t (n − l ))
n l =1
siempre que n sea lo suficientemente grande como para garantizar el enfoque recursivo.
F.C.A.D – U.N.E.R. 57
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
• una mejor elección del pívot (se puede elegir como pívot el elemento mediano en
tiempo de Θ(n), pero esto no es conveniente salvo para n muy grandes).
• Se puede modificar el pivoteo para que divida el arreglo en tres subarreglos: los
elementos menores que el pívot, los elementos iguales, y los elementos mayores.
Estas “mejoras” involucran constantes ocultas que hacen que el algoritmo resultante
sea prácticamente inviable comparado con la versión original.
¿Qué puede ser más fácil que encontrar el menor elemento de T o calcular la media de
todos los elementos? Sin embargo, no es tan obvio que la mediana pueda encontrarse tan
fácilmente. El algoritmo naive para determinar la mediana de T[1..n] consiste en ordenar
el arreglo y luego extraer su entrada n/2-ésima. Si usamos heapsort o mergesort, esto
F.C.A.D – U.N.E.R. 58
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
toma un tiempo en Θ(n log n). ¿Se puede mejorar? Para responder a esta pregunta,
estudiamos la interrelación entre encontrar la mediana y elegir el s-ésimo menor elemento.
Es obvio que cualquier algoritmo para el problema de selección puede usarse para
encontrar la mediana: simplemente se selecciona el n/2-ésimo menor elemento. Lo
inverso también se cumple. Asumamos por ahora la disponibilidad de un algoritmo
mediana(T[1..n]) que devuelve la mediana de T. Dado un arreglo T y un entero s, cómo se
podría usar este algoritmo para determinar el s-ésimo menor elemento de T?. Sea p la
mediana de T. Realicemos un pivoteo de T alrededor de p, muy parecido a lo que
hacíamos en quicksort, pero usando el algoritmo pivotbis introducido al final de la sección
anterior. Recordemos que una llamada a pivotbis(T[i..j], p; var k, l) parte a T[i..j] en tres
secciones: los elementos de T[i..k] son menores que p, los elementos en T[k+1..l–1] son
iguales a p, y los elementos de T[l..j] son mayores que p. Luego de una llamada a
pivotbis(T, p, k, l), si k < s < l el problema está resuelto, ya que el s-ésimo menor elemento
de T es igual a p. Si s ≤ k, el s-ésimo menor elemento de T es ahora el s-ésimo menor
elemento de T[1..k]. Finalmente, si s ≥ l, el s-ésimo menor elemento de T es ahora el
(s – l + 1)-ésimo menor elemento de T[l..n]. En cualquier caso hemos avanzado, ya que, o
resolvimos el problema, o el subarreglo a considerar contiene menos de la mitad de los
elementos, en virtud de que p es la mediana del arreglo original.
La Figura 4.4 ilustra el proceso. Por simplicidad, la ilustración asume que pivotbis está
implementado de una forma intuitivamente simple, aún cuando una implementación
realmente eficiente podría proceder de manera diferente.
F.C.A.D – U.N.E.R. 59
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
− case
− s ≤ k: j ← k; p ← seleccion(T[i..j], s);
− k < s < l: ;
− s ≥ l: i ← l; p ← seleccion(T[i..j], s – l + 1);
− end case
− return p
p ← T[i]
F.C.A.D – U.N.E.R. 60
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
Esto hace que el algoritmo tome tiempo cuadrático en el peor caso, por ejemplo si el
arreglo está en orden decreciente y queremos encontrar el menor elemento. Sin embargo,
este algoritmo modificado corre en tiempo lineal en promedio, bajo nuestra suposición
usual de que los elementos de T son distintos y que cada una de las n! posibles
permutaciones iniciales son igualmente probables. Esto es mucho mejor que el tiempo
promedio requerido si procedemos a ordenar el arreglo, pero el comportamiento en el peor
caso es inaceptable para muchas aplicaciones.
Felizmente, este peor caso cuadrático puede evitarse sin sacrificar el comportamiento
lineal en el promedio. La idea es que el número de veces que se ejecuta el ciclo
permanezca logarítmico a condición de que el pívot se elija razonablemente cercano a la
mediana. Una buena aproximación a la mediana puede encontrarse rápidamente con un
pequeño artificio. Consideremos el siguiente algoritmo, que encuentra una aproximación a
la mediana del arreglo T.
− function seudomed(T[1..n])
− if n ≤ 5 then return adhocmed(T)
− z ← n/5
− array Z[1..z]
− for i ← 1 to z do
− Z[i] ← adhocmed(T[5i–4..5i])
− end for
− return seleccion(Z, z/2)
F.C.A.D – U.N.E.R. 61
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
p ← seudomed(T[i..j])
Sea t(n) el tiempo requerido en el peor caso por una llamada a seleccion(T[1..n], s).
Consideremos cualquier i y j tales que 1 ≤ i ≤ j ≤ n. El tiempo requerido para completar el
ciclo con estos valores para i y j es esencialmente t(m), donde m = j – i + 1 es el número de
elementos que todavía están en consideración. Cuando n > 5, el cálculo de seudomed(T)
toma un tiempo en t(n/5) + O(n) porque el arreglo Z puede construirse en tiempo lineal,
ya que cada llamada a adhocmed toma tiempo constante. La llamada a pivotbis también
toma tiempo lineal. En este punto, o hemos terminado, o tenemos que volver a ingresar al
ciclo con un máximo de (7n + 12)/10 elementos, que todavía están bajo consideración. Por
lo tanto, existe una constante d tal que
F.C.A.D – U.N.E.R. 62
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
a a12 b b12
A = 11 ; B = 11
a 21 a 22 b21 b22
dos matrices a multiplicar. Consideremos las siguientes operaciones, cada una de las
cuales involucra una única multiplicación.
m1 = (a21 + a22 – a11)(b22 – b12 + b11)
m2 = a11b11
m3 = a12b21
m4 = (a11 – a21)(b22 – b12)
m5 = (a21 + a22)(b12 – b11)
m6 = (a12 – a21 + a11 – a22)b22
F.C.A.D – U.N.E.R. 63
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
m 2 + m3 m1 + m2 + m5 + m6
C =
m1 + m2 + m4 − m7 m1 + m2 + m4 + m5
Por lo tanto, es posible multiplicar dos matrices de 2×2 usando sólo siete
multiplicaciones escalares. A primera vista, este algoritmo no parece muy interesante:
utiliza un gran número de sumas y restas comparadas con las cuatro adiciones que son
suficientes para el algoritmo clásico.
Sea t(n) el tiempo necesario par multiplicar dos matrices de n×n mediante el uso
recursivo de las multiplicaciones introducidas anteriormente. Asumamos por simplicidad
que n es una potencia de 2. Como las matrices pueden ser sumadas y restadas en un
tiempo del Θ(n2), t(n) = 7t(n/2) + g(n), donde g(n) ∈ Θ(n2). Esta recurrencia es otra
instancia de nuestro análisis general para los algoritmos DyC. Si tomamos l = 7, b = 2 y k
= 2, y como l > bk, el tercer caso da que t(n) ∈ Θ(nlg7). Las matrices cuadradas cuyo
tamaño no es una potencia de 2 se pueden manejar fácilmente llenándolas de ceros hasta
que alcancen un tamaño apropiado, lo que no afecta el tiempo de corrida asintótico. Como
lg 7 < 2.81, entonces es posible multiplicar dos matrices de n×n en un tiempo en el
O(n2.81), si suponemos que las operaciones escalares son elementales.
F.C.A.D – U.N.E.R. 64
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
Pasó casi una década antes de que Pan descubriera una forma de multiplicar dos
matrices de 70×70 con 143640 multiplicaciones escalares –comparemos esto con las
343000 requeridas por el algoritmo clásico– y de hecho log70 143640 es un poquito menor
que lg 7. Este descubrimiento desató la llamada guerra decimal. Sucesivamente se fueron
descubriendo numerosos algoritmos, asintóticamente cada vez más eficientes. Por
ejemplo, a finales de 1979 era conocido que las matrices se podían multiplicar en un
tiempo en el O(n2.521813); imaginemos la emoción en enero de 1980 cuando esto se mejoró
al O(n2.521801). El algoritmo de multiplicación de matrices más rápido hoy conocido data de
1986, cuando Coppersmith y Winograd descubrieron que era posible, al menos en teoría,
multiplicar dos matrices de n×n en un tiempo en el O(n2.376). Sin embargo, debido a las
constantes ocultas involucradas, ninguno de los algoritmos encontrados después del de
Strassen tiene mucho uso práctico.
4.7 Exponenciación
Sean a y n dos números enteros. Queremos calcular la exponenciación x = an.
Asumiremos que n > 0. Si n es pequeño, entonces se puede aplicar el algoritmo obvio.
− function exposec(a, n)
− r←a
− for i ← 1 to n – 1 do
− r←a×r
− end for
− return r
Este algoritmo toma un tiempo en el Θ(n), suponiendo que las multiplicaciones se
consideran como operaciones elementales. Sin embargo, en la mayoría de las
computadoras, incluso valores pequeños de a y n causan que este algoritmo produzca un
desbordamiento. Por ejemplo, 1517 no cabe en un entero de 64 bits.
F.C.A.D – U.N.E.R. 65
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
cuando se usa el algoritmo exposec es de T(m, n) ∈ Θ(m2n2). Si por otro lado usamos el
algoritmo de multiplicación de enteros grandes descrito en este mismo capítulo, es decir el
que se basa en DyC, se puede probar que T(m, n) ∈ Θ(mlg 3n2).
La observación clave para mejorar exposec es que an = (an/2)2 cuando n es par. Esto es
interesante porque an/2 se puede calcular aproximadamente cuatro veces más rápido que an
con exposec, y un único cálculo del cuadrado (que es una multiplicación) es suficiente
para obtener el resultado deseado partiendo de an/2. Esto da lugar a la siguiente
recurrencia.
a si n = 1
an = (an/2)2 si n es par
a × an–1 en caso contrario
Por ejemplo,
F.C.A.D – U.N.E.R. 66
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
Resolviendo esta recurrencia, se puede probar que N(n) está en el Θ(log n). Por lo
tanto, usando este criterio, concluimos que este algoritmo es más eficiente que exposec,
que utilizaba un número de multiplicaciones en el Θ(n). Queda por ver si expoDyC
también es significativamente mejor cuando se tiene en cuenta el tiempo insumido en esas
multiplicaciones. Nuevamente, el tiempo dependerá de si esas multiplicaciones se ejecutan
utilizando el algoritmo clásico o el algoritmo DyC. En el primer caso, el tiempo insumido
por las multiplicaciones es del Θ(m2n2), mientras que usando el algoritmo DyC ese tiempo
se reduce al Θ(mlg 3nlg 3).
Resumiendo, la siguiente tabla muestra el tiempo requerido para calcular an, donde m
es el tamaño de a, dependiendo de si se usa exposec o expoDyC, y de si se usa el algoritmo
de multiplicación clásico o el algoritmo DyC.
Multiplicación
Clásica DyC
exposec Θ(m2n2) Θ(mlg 3n2)
expoDyc Θ(m2n2) Θ(mlg 3nlg 3)
Como en el caso de la búsqueda binaria, expoDyC requiere sólo una llamada recursiva
sobre una instancia más pequeña. Por lo tanto es un ejemplo de simplificación más que de
Dividir y Conquistar. Existe una versión iterativa, aunque no muy eficiente, del algoritmo
de exponenciación.
− function expoiter(a, n)
− i ← n; r ← 1; x ← a
− while i > 0 do
− if i es impar then r ← rx
− x ← x2
− i←i÷2
− end while
− return r
F.C.A.D – U.N.E.R. 67
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
Hay aplicaciones para las cuales es razonable contar todas las multiplicaciones de una
exponenciación con el mismo costo. Este es el caso si estamos interesados en la aritmética
modular, es decir en el cálculo de an módulo algún tercer entero z. Recordemos que
x mod z denota el resto de la división entera de x por z. Por ejemplo, 25 mod 7 = 4 porque
25 = 3 × 7 + 4. Si x e y son dos enteros entre 0 y z – 1, y si z es un entero de tamaño m, la
multiplicación modular xy mod z involucra una multiplicación entera ordinaria de dos
enteros de tamaño máximo m, dando un entero de tamaño máximo 2m, seguida por una
división del producto por z, un entero de tamaño m, para calcular el resto de la división.
Por lo tanto, el tiempo insumido por cada multiplicación modular es más bien insensible a
los dos números realmente involucrados. Se usarán dos propiedades elementales de la
aritmética modular:
F.C.A.D – U.N.E.R. 68
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
Esta forma de encarar la criptografía se ha usado con más o menos éxito a lo largo de
la historia. Su requerimiento de que las partes deben compartir información secreta antes
de la comunicación puede ser aceptable para militares y diplomáticos, pero no para los
ciudadanos comunes. En la era de las comunicaciones electrónicas, es deseable que
cualquier par de personas puedan comunicarse privadamente sin coordinación previa.
¿Pueden comunicarse secretamente A y B, a la vista de una tercera parte, si no comparten
un secreto antes de establecer la comunicación? La era de la criptografía de clave pública
comenzó a mediados de los ‘70, cuando Diffie, Hellman y Merkle vieron que esto podía
ser posible. Aquí se presenta una solución asombrosamente simple descubierta unos pocos
años más tarde por Rivest, Shamir y Adleman, que se hizo conocida como el sistema de
criptografía RSA por los nombres de sus inventores.
F.C.A.D – U.N.E.R. 69
Algoritmos y Complejidad Unidad 4: Dividir y Conquistar
existe, B tiene que elegir aleatoriamente otro valor para n; cada intento tiene una buena
probabilidad de éxito. El teorema clave es que ax mod z = a siempre que 0 ≤ a < z y
x mod φ = 1.
Para permitir que A o cualquier otra parte se comunique con él privadamente, B hace
pública su elección de z y n, pero mantiene s en secreto. Sea m un mensaje que A le quiere
transmitir a B. Usando codificación estándar, como ASCII, A transforma su mensaje en
una cadena de bits, que interpreta como un número a. Asumamos por simplicidad que
0 ≤ a ≤ z – 1; en caso contrario, tendrá que dividir su mensaje en porciones del tamaño
apropiado. Luego, A usa el algoritmo expomod para calcular c = an mod z, que es lo que le
envía a B a través del canal inseguro. Usando su conocimiento privado de s, B obtiene a, y
por lo tanto el mensaje m de A, con una llamada a expomod(c, s, z). Esto funciona porque
Ahora consideremos la tarea del espía. Asumiendo que ha interceptado todas las
comunicaciones entre A y B, sabe cuáles son z, n y c. Su propósito es determinar el
mensaje a de A, que es el único número entre 0 y z – 1 tal que c = an mod z. Es decir que
tiene que calcular la raíz enésima de c módulo z. No se conoce ningún algoritmo eficiente
para este cálculo: las exponenciaciones modulares se pueden calcular eficientemente con
expomod, pero resulta que el proceso inverso es inviable. El mejor método conocido hasta
ahora es el obvio: factorear z en p y q, calcular φ como (p – 1)(q – 1), calcular s a partir de
n y φ, y calcular a = cs mod z exactamente como debería haberlo hecho B. Todos los pasos
de este intento son viables, salvo el primero: factorear un número de 200 dígitos está más
allá del alcance de la tecnología actual. Entonces, la ventaja de B para descifrar los
mensajes dirigidos a él está en el hecho de que sólo él conoce los factores de z, que son
necesarios para calcular φ y s. Este conocimiento no proviene de sus habilidades para
factorear, sino más bien porque él eligió esos factores de z primero, y luego calculó z a
partir de ellos.
F.C.A.D – U.N.E.R. 70