Traducción de código a las matemáticas
Dada una (más o menos) semántica operativa formal , puede traducir el código de un algoritmo (pseudo-) literalmente en una expresión matemática que le da el resultado, siempre que pueda manipular la expresión en una forma útil. Esto funciona bien para los aditivos medidas costo, tales como el número de comparaciones, permutas, las declaraciones, los accesos a memoria, los ciclos de algunas necesidades máquina abstracta, y así sucesivamente.
Ejemplo: comparaciones en Bubblesort
Considere este algoritmo que clasifica una matriz dada A
:
bubblesort(A) do 1
n = A.length; 2
for ( i = 0 to n-2 ) do 3
for ( j = 0 to n-i-2 ) do 4
if ( A[j] > A[j+1] ) then 5
tmp = A[j]; 6
A[j] = A[j+1]; 7
A[j+1] = tmp; 8
end 9
end 10
end 11
end 12
Digamos que queremos realizar el análisis de algoritmo de clasificación habitual, es decir, contar el número de comparaciones de elementos (línea 5). Notamos de inmediato que esta cantidad no depende del contenido de la matriz A
, solo de su longitud . Entonces podemos traducir los bucles (anidados) literalmente en sumas (anidadas); la variable de bucle se convierte en la variable de suma y el rango se transfiere. Obtenemos:nfor
Ccmp(n)=∑i=0n−2∑j=0n−i−21=⋯=n(n−1)2=(n2) ,
donde es el costo por cada ejecución de la línea 5 (que contamos).1
Ejemplo: Swaps en Bubblesort
Denotaré por el subprograma que consiste en líneas hacia y por los costos para ejecutar este subprograma (una vez).Pi,ji
j
Ci,j
Ahora supongamos que queremos contar los intercambios , es decir con qué frecuencia se ejecuta . Este es un "bloque básico", que es un subprograma que siempre se ejecuta atómicamente y tiene un costo constante (aquí, ). Contratar tales bloques es una simplificación útil que a menudo aplicamos sin pensar o hablar de ello.P6,81
Con una traducción similar a la anterior, llegamos a la siguiente fórmula:
Cswaps(A)=∑i=0n−2∑j=0n−i−2C5,9(A(i,j)) .
A(i,j) denota el estado de la matriz antes de la iteración de .(i,j)P5,9
Tenga en cuenta que uso lugar de como parámetro; pronto veremos por qué. No agrego y como parámetros de ya que los costos no dependen de ellos aquí (en el modelo de costo uniforme , es decir); en general, tal vez sí.AnijC5,9
Claramente, los costos de dependen del contenido de (los valores y , específicamente) por lo que tenemos que dar cuenta de eso. Ahora nos enfrentamos a un desafío: ¿cómo "desenvolvemos" ? Bueno, podemos hacer explícita la dependencia del contenido de :P5,9AA[j]
A[j+1]
C5,9A
C5,9(A(i,j))=C5(A(i,j))+{10,A(i,j)[j]>A(i,j)[j+1],else .
Para cualquier matriz de entrada dada, estos costos están bien definidos, pero queremos una declaración más general; Necesitamos hacer suposiciones más fuertes. Investiguemos tres casos típicos.
El peor caso
Simplemente mirando la suma y notando que , podemos encontrar un límite superior trivial para el costo:C5,9(A(i,j))∈{0,1}
Cswaps(A)≤∑i=0n−2∑j=0n−i−21=n(n−1)2=(n2) .
Pero, ¿ puede suceder esto , es decir, hay una para alcanzar este límite superior? Resulta que sí: si ingresamos una matriz ordenada inversamente de elementos distintos por pares, cada iteración debe realizar un intercambio¹. Por lo tanto, hemos derivado el número exacto de permutas de Bubblesort en el peor de los casos .A
El mejor caso
Por el contrario, hay un límite inferior trivial:
Cswaps(A)≥∑i=0n−2∑j=0n−i−20=0 .
Esto también puede suceder: en una matriz que ya está ordenada, Bubblesort no ejecuta un solo intercambio.
El caso promedio
El peor y el mejor caso abren una gran brecha. Pero, ¿cuál es el número típico de intercambios? Para responder a esta pregunta, necesitamos definir qué significa "típico". En teoría, no tenemos ninguna razón para preferir una entrada sobre otra, por lo que generalmente asumimos una distribución uniforme sobre todas las entradas posibles, es decir, cada entrada es igualmente probable. Nos restringimos a matrices con elementos distintos por pares y, por lo tanto, asumimos el modelo de permutación aleatoria .
Entonces, podemos reescribir nuestros costos así²:
E[Cswaps]=1n!∑A∑i=0n−2∑j=0n−i−2C5,9(A(i,j))
Ahora tenemos que ir más allá de la simple manipulación de sumas. Al observar el algoritmo, notamos que cada intercambio elimina exactamente una inversión en (solo intercambiamos vecinos³). Es decir, el número de permutas realizadas en es exactamente el número de inversiones de . Por lo tanto, podemos reemplazar las dos sumas internas y obtenerAAinv(A)A
E[Cswaps]=1n!∑Ainv(A) .
Por suerte para nosotros, se ha determinado que el número promedio de inversiones es
E[Cswaps]=12⋅(n2)
cual es nuestro resultado final Tenga en cuenta que esto es exactamente la mitad del costo del peor de los casos.
- Tenga en cuenta que el algoritmo se formuló cuidadosamente para que "la última" iteración
i = n-1
del bucle externo que nunca hace nada no se ejecute.
- " " es una notación matemática para "valor esperado", que aquí es solo el promedio.E
- En el camino, aprendemos que ningún algoritmo que solo intercambia elementos vecinos puede ser asintóticamente más rápido que Bubblesort (incluso en promedio): el número de inversiones es un límite inferior para todos estos algoritmos. Esto se aplica, por ejemplo, al orden de inserción y al orden de selección .
El metodo general
Hemos visto en el ejemplo que tenemos que traducir la estructura de control a las matemáticas; Presentaré un conjunto típico de reglas de traducción. También hemos visto que el costo de cualquier subprograma puede depender del estado actual , es decir (aproximadamente) los valores actuales de las variables. Dado que el algoritmo (generalmente) modifica el estado, el método general es un poco engorroso de observar. Si comienza a sentirse confundido, le sugiero que vuelva al ejemplo o invente el suyo.
Denotamos con el estado actual (imagínelo como un conjunto de asignaciones variables). Cuando ejecutamos un programa que comienza en el estado , terminamos en el estado ( termina siempre ).ψP
ψψ/PP
Declaraciones individuales
Dada una sola declaración S;
, le asigna costos . Esto será típicamente una función constante.CS(ψ)
Expresiones
Si tiene una expresión E
de la forma E1 ∘ E2
(por ejemplo, una expresión aritmética donde ∘
puede ser suma o multiplicación, suma los costos de forma recursiva:
CE(ψ)=c∘+CE1(ψ)+CE2(ψ) .
Tenga en cuenta que
- el costo de operación puede no ser constante pero depende de los valores de y yc∘E1E2
- la evaluación de expresiones puede cambiar el estado en muchos idiomas,
entonces puede que tenga que ser flexible con esta regla.
Secuencia
Dado un programa P
como secuencia de programas Q;R
, agrega los costos a
CP(ψ)=CQ(ψ)+CR(ψ/Q) .
Condicionales
Dado un programa P
de la forma if A then Q else R end
, los costos dependen del estado:
CP(ψ)=CA(ψ)+{CQ(ψ/A)CR(ψ/A),A evaluates to true under ψ,else
En general, la evaluación A
puede muy bien cambiar el estado, de ahí la actualización de los costos de las sucursales individuales.
For-Loops
Dado un programa P
del formulario for x = [x1, ..., xk] do Q end
, asignar costos
CP(ψ)=cinit_for+∑i=1kcstep_for+CQ(ψi∘{x:=xi})
donde es el estado antes de procesar el valor , es decir, después de la iteración con el establecimiento de , ..., .ψiQ
xi
x
x1
xi-1
Tenga en cuenta las constantes adicionales para el mantenimiento del bucle; se debe crear la variable de bucle ( ) y asignarle sus valores ( ). Esto es relevante ya quecinit_forcstep_for
- calcular el próximo
xi
puede ser costoso y
- un
for
bucle con cuerpo vacío (por ejemplo, después de simplificar en el mejor de los casos con un costo específico) no tiene costo cero si realiza iteraciones.
Mientras-Loops
Dado un programa P
del formulario while A do Q end
, asignar costos
CP(ψ) =CA(ψ)+{0CQ(ψ/A)+CP(ψ/A;Q),A evaluates to false under ψ, else
Al inspeccionar el algoritmo, esta recurrencia a menudo se puede representar muy bien como una suma similar a la de for-loops.
Ejemplo: considere este breve algoritmo:
while x > 0 do 1
i += 1 2
x = x/2 3
end 4
Al aplicar la regla, obtenemos
C1,4({i:=i0;x:=x0}) =c<+{0c+=+c/+C1,4({i:=i0+1;x:=⌊x0/2⌋}),x0≤0, else
con algunos costos constantes para las declaraciones individuales. Suponemos implícitamente que estos no dependen del estado (los valores de y ); Esto puede o no ser cierto en la "realidad": ¡piense en desbordamientos!c…i
x
Ahora tenemos que resolver esta recurrencia para . Notamos que ni el número de iteraciones ni el costo del cuerpo del bucle dependen del valor de , por lo que podemos descartarlo. Nos quedamos con esta recurrencia:C1,4i
C1,4(x)={c>c>+c+=+c/+C1,4(⌊x/2⌋),x≤0, else
Esto resuelve con medios elementales para
C1,4(ψ)=⌈log2ψ(x)⌉⋅(c>+c+=+c/)+c> ,
reintroducir simbólicamente el estado completo; if , entonces .ψ={…,x:=5,…}ψ(x)=5
Llamadas de procedimiento
Dado un programa P
de la forma M(x)
para algunos parámetros x
donde M
es un procedimiento con parámetro (nombrado) p
, asignar costos
CP(ψ)=ccall+CM(ψglob∘{p:=x}) .
Observe nuevamente la constante adicional (que de hecho podría depender de !). Las llamadas a procedimientos son costosas debido a cómo se implementan en máquinas reales y, a veces, incluso dominan el tiempo de ejecución (por ejemplo, evaluar la recurrencia del número de Fibonacci ingenuamente).ccallψ
Paso por alto algunos problemas semánticos que podría tener con el estado aquí. Deberá distinguir el estado global y las llamadas locales a procedimientos. Supongamos que solo pasamos el estado global aquí y obtenemos M
un nuevo estado local, inicializado estableciendo el valor de p
to x
. Además, x
puede ser una expresión que (generalmente) suponemos que se evalúa antes de pasarla.
Ejemplo: considere el procedimiento
fac(n) do
if ( n <= 1 ) do 1
return 1 2
else 3
return n * fac(n-1) 4
end 5
end
Según la (s) regla (s), obtenemos:
Cfac({n:=n0})=C1,5({n:=n0})=c≤+{C2({n:=n0})C4({n:=n0}),n0≤1, else=c≤+{creturncreturn+c∗+ccall+Cfac({n:=n0−1}),n0≤1, else
Tenga en cuenta que no tenemos en cuenta el estado global, ya que fac
claramente no accede a ninguno. Esta recurrencia particular es fácil de resolver
Cfac(ψ)=ψ(n)⋅(c≤+creturn)+(ψ(n)−1)⋅(c∗+ccall)
Hemos cubierto las características del lenguaje que encontrará en el pseudocódigo típico. Tenga cuidado con los costos ocultos al analizar pseudocódigo de alto nivel; En caso de duda, despliegue. La notación puede parecer engorrosa y ciertamente es una cuestión de gustos; Sin embargo, los conceptos enumerados no pueden ser ignorados. Sin embargo, con un poco de experiencia podrá ver de inmediato qué partes del estado son relevantes para qué medida de costo, por ejemplo, "tamaño del problema" o "número de vértices". El resto se puede soltar, ¡esto simplifica las cosas significativamente!
Si cree que esto es demasiado complicado, tenga en cuenta: ¡lo es ! Derivar los costos exactos de los algoritmos en cualquier modelo que esté tan cerca de las máquinas reales como para permitir predicciones de tiempo de ejecución (incluso las relativas) es una tarea difícil. Y eso ni siquiera está considerando el almacenamiento en caché y otros efectos desagradables en máquinas reales.
Por lo tanto, el análisis de algoritmos a menudo se simplifica hasta el punto de ser matemáticamente manejable. Por ejemplo, si no necesita costos exactos, puede sobreestimar o subestimar en cualquier punto (para límites superiores o inferiores): reduzca el conjunto de constantes, elimine los condicionales, simplifique las sumas, etc.
Una nota sobre el costo asintótico
Lo que generalmente encontrará en la literatura y en las webs es el "análisis Big-Oh". El término apropiado es análisis asintótico , lo que significa que en lugar de derivar los costos exactos como lo hicimos en los ejemplos, solo da los costos hasta un factor constante y en el límite (en términos generales, "para grande ").n
Esto es (a menudo) justo ya que las declaraciones abstractas tienen algunos costos (generalmente desconocidos) en realidad, dependiendo de la máquina, el sistema operativo y otros factores, y los tiempos de ejecución cortos pueden estar dominados por el sistema operativo que configura el proceso en primer lugar y otras cosas. Entonces te perturba, de todos modos.
Así es como el análisis asintótico se relaciona con este enfoque.
Identifique las operaciones dominantes (que inducen costos), es decir, las operaciones que ocurren con mayor frecuencia (hasta factores constantes). En el ejemplo de Bubblesort, una opción posible es la comparación en la línea 5.
Alternativamente, limite todas las constantes para operaciones elementales por su máximo (desde arriba) resp. su mínimo (desde abajo) y realice el análisis habitual.
- Realice el análisis utilizando los recuentos de ejecución de esta operación como costo.
- Al simplificar, permita estimaciones. Tenga cuidado de permitir solo estimaciones desde arriba si su objetivo es un límite superior ( ) resp. desde abajo si desea límites inferiores ( ).OΩ
Asegúrese de comprender el significado de los símbolos de Landau . Recuerde que tales límites existen para los tres casos ; el uso de no implica un análisis del peor de los casos.O
Otras lecturas
Hay muchos más desafíos y trucos en el análisis de algoritmos. Aquí hay algunas lecturas recomendadas.
Hay muchas preguntas etiquetadas en el análisis de algoritmos que utilizan técnicas similares a esta.