Hay un montón de preguntas acerca de cómo analizar el tiempo de ejecución de algoritmos (véase, por ejemplo, el tiempo de ejecución-análisis y algoritmo de análisis ). Muchos son similares, por ejemplo, aquellos que solicitan un análisis de costos de bucles anidados o algoritmos de dividir y conquistar, pero la mayoría de las respuestas parecen estar hechas a medida.
Por otro lado, las respuestas a otra pregunta general explican el panorama general (en particular con respecto al análisis asintótico) con algunos ejemplos, pero no cómo ensuciarse las manos.
¿Existe un método estructurado y general para analizar el costo de los algoritmos? El costo puede ser el tiempo de ejecución (complejidad del tiempo), o alguna otra medida de costo, como el número de comparaciones ejecutadas, la complejidad del espacio u otra cosa.
Se supone que esto se convertirá en una pregunta de referencia que puede usarse para señalar a los principiantes; de ahí su alcance más amplio de lo habitual. Tenga cuidado de dar respuestas generales, presentadas didácticamente, que se ilustran con al menos un ejemplo, pero que sin embargo cubren muchas situaciones. ¡Gracias!
Respuestas:
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
: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 matrizn
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:for
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,j Ci,j
i
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,8 1
Con una traducción similar a la anterior, llegamos a la siguiente fórmula:
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í.A n i j C5,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,9 A C5,9 A
A[j]
A[j+1]
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}
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:
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í²:
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 obtenerA A inv(A) A
Por suerte para nosotros, se ha determinado que el número promedio de inversiones es
cual es nuestro resultado final Tenga en cuenta que esto es exactamente la mitad del costo del peor de los casos.
i = n-1
del bucle externo que nunca hace nada no se ejecute.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
P
P
Declaraciones individuales
Dada una sola declaraciónCS(ψ)
S;
, le asigna costos . Esto será típicamente una función constante.Expresiones
Si tiene una expresión
E
de la formaE1 ∘ E2
(por ejemplo, una expresión aritmética donde∘
puede ser suma o multiplicación, suma los costos de forma recursiva:Tenga en cuenta que
entonces puede que tenga que ser flexible con esta regla.
Secuencia
Dado un programa
P
como secuencia de programasQ;R
, agrega los costos aCondicionales
Dado un programa
P
de la formaif A then Q else R end
, los costos dependen del estado: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 formulariofor x = [x1, ..., xk] do Q end
, asignar costosdonde es el estado antes de procesar el valor , es decir, después de la iteración con el establecimiento de , ..., .ψi
Q
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_for cstep_for
xi
puede ser costoso yfor
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 formulariowhile A do Q end
, asignar costosAl 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:
Al aplicar la regla, obtenemos
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,4
i
Esto resuelve con medios elementales para
reintroducir simbólicamente el estado completo; if , entonces .ψ={…,x:=5,…} ψ(x)=5
Llamadas de procedimiento
Dado un programa
P
de la formaM(x)
para algunos parámetrosx
dondeM
es un procedimiento con parámetro (nombrado)p
, asignar costosObserve 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 dep
tox
. Además,x
puede ser una expresión que (generalmente) suponemos que se evalúa antes de pasarla.Ejemplo: considere el procedimiento
Según la (s) regla (s), obtenemos:
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 resolverHemos 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.
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.
¿Cómo describir algoritmos, probarlos y analizarlos?
¿Cómo podemos suponer que las operaciones básicas en números toman tiempo constante?
¿Qué constituye una unidad de tiempo en el análisis de tiempo de ejecución?
Hay muchas preguntas etiquetadas en el análisis de algoritmos que utilizan técnicas similares a esta.
fuente
Recuentos de ejecución de declaraciones
Hay otro método, defendido por Donald E. Knuth en su serie The Art of Computer Programming . A diferencia de traducir todo el algoritmo en una fórmula , funciona independientemente de la semántica del código en el lado de "poner las cosas juntas" y permite ir a un nivel inferior solo cuando es necesario, comenzando desde una vista de "ojo de águila". Cada declaración puede analizarse independientemente del resto, lo que lleva a cálculos más claros. Sin embargo, la técnica se presta bien a un código bastante detallado, no a un seudocódigo de nivel superior.
El método
Es bastante simple en principio:
Calcular costos totales
Puede insertar estimaciones y / o cantidades simbólicas en cualquier punto, debilitando las resp. generalizando el resultado en consecuencia.
Tenga en cuenta que el paso 3 puede ser arbitrariamente complejo. Por lo general, es allí donde debe trabajar con estimaciones (asintóticas) como " " para obtener resultados.e77∈O(nlogn)
Ejemplo: búsqueda de profundidad primero
Considere el siguiente algoritmo de recorrido de gráfico:
Suponemos que el gráfico (no dirigido) viene dado por listas de adyacencia en los nodos . Sea el número de aristas.{0,…,n−1} m
Con solo mirar el algoritmo, vemos que algunas declaraciones se ejecutan con la misma frecuencia que otras. Presentamos algunos marcadores de posición , y para los recuentos de ejecución :A B C ei
En particular, ya que cada llamada recursiva en la línea 8 provoca una llamada en la línea 3 (y una es causada por la llamada original desde ). Además, porque la condición debe verificarse una vez por iteración, pero luego una vez más para abandonarla.e8=e3−1 e6=e5+e7
foo
dfs
while
Está claro que . Ahora, durante una prueba de corrección, mostraríamos que se ejecuta exactamente una vez por nodo; es decir, . Pero luego, iteramos sobre cada lista de adyacencia exactamente una vez y cada borde implica dos entradas en total (una para cada nodo incidente); obtenemos iteraciones en total. Con esto, derivamos la siguiente tabla:A=1 B=n C=2m
foo
Esto nos lleva a costos totales de exactamente
Al crear instancias de valores adecuados para el podemos derivar más costos concretos. Por ejemplo, si queremos contar los accesos a la memoria (por palabra), usaríamosCi
y obten
Otras lecturas
Ver al final de mi otra respuesta .
fuente
El análisis de algoritmos, como la demostración de teoremas, es en gran medida un arte (por ejemplo, hay programas simples (como el problema de Collatz ) que no sabemos cómo analizar). Podemos convertir un problema de complejidad del algoritmo en uno matemático, como lo respondió Raphael de manera exhaustiva , pero luego, para expresar un límite sobre el costo de un algoritmo en términos de funciones conocidas, nos quedamos para:
fuente