¿Cuál es el tiempo de ejecución de este algoritmo recursivo?

8

Hice el siguiente programa de Haskell (sin golf) para el desafío del código de golf de calcular los primeros valores de A229037 .n

Esta es mi propuesta de solución para calcular el º valor:n

a n | n<1        = 0 
    | n<3        = 1
    | otherwise  = head (goods n)

goods n = [x | x <- [1..], isGood x n]

isGood x n = and [ x - a(n-k) /= a(n-k) - a(n-k-k) || a(n-k-k) == 0 | k <- [1..n] ]

Tenga en cuenta que Haskell no almacena en caché ni memoriza automáticamente estos valores.

La página OEIS para la secuencia da el hecho de que , por lo que podría ser reemplazado por , ya que el algoritmo nunca alcanzará una mayor que .a(n)(n+1)/2[1..][1..(n+1)/2]xn+12

Intentando contar las llamadas a funciones, deduje el siguiente límite superior , el número de llamadas a funciones que el algoritmo toma para una entrada :T(n)n

T(n)=x=1(n+1)/2k=1n2 T(nk)+2 T(n2k)x=1(n+1)/2k=1n T(nk)x=1(n+1)/2k=1n4 T(n1)x=1(n+1)/24 n T(n1)4 n T(n1) n+122 n (n+1) T(n1))

Conecté la fórmula final en Mathematica:

RSolve[{T[n] == 2*T[n - 1]*n*(n + 1), T[1] == 1}, T[n], n]

Y obtuve, después de una pequeña simplificación:T(n) 2n n! (n+1)!

La proporción promedio entre esto y el tiempo de ejecución del programa Haskell, para es y la desviación estándar de las proporciones es de alrededor de . (Curiosamente, el diagrama de registro de las proporciones parece ser una línea recta).n[12,20]2.010396.01039

Las relaciones con la primera línea, que define , tienen una desviación media y estándar de y , respectivamente, pero su trama salta mucho.T(n)4.81061.8106

¿Cómo puedo conocer mejor la complejidad temporal de este algoritmo?

Aquí está el algoritmo en C válido (menos las declaraciones directas), que creo que es más o menos equivalente al código Haskell:

int a(int n){
    if (n < 1) {
        return 0;
    } else if (n < 3) {
        return 1;
    } else {
        return lowestValid(n);
    }
}

int lowestValid(int n){
    int possible = 1; // Without checking, we know that this will not exceed (n+1)/2

    while (notGood(possible, n)) {
        possible++;
    }
    return possible;
}

int notGood(int possible, int n){
    int k = 1;

    while (k <= n) {
        if ( ((possible - a(n-k)) == (a(n-k) - a(n-2*k))) && (a(n-2*k) != 0) ) {
            return 1;
        } else {
            k++;
        }
    }
    return 0;
}

La versión C tarda unos 5 minutos en calcular y la versión Haskell tarda aproximadamente lo mismo en .a(17)a(19)

Los primeros tiempos de las versiones:

Haskell: [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0e-2,3.0e-2,9.0e-2,0.34,1.42,11.77,71.68,184.37,1815.91]
C:       [2.0e-6, 1.0e-6, 1.0e-6, 2.0e-6, 1.0e-6, 6.0e-6, 0.00003,0.00027, 0.002209, 0.005127, 0.016665, 0.080549, 0.243611, 0.821537, 4.56265, 24.2044, 272.212]
Michael Klein
fuente
He cambiado las etiquetas y el título para dejar en claro que este es un análisis de algoritmo, no una pregunta de teoría de complejidad. "Suponiendo que la multiplicación y la suma son insignificantes", ¿puedes ? Enserio ? Todavía es mejor decir lo que estás contando, porque es probable que no estés contando la mayoría de las cosas. Vea también nuestra pregunta de referencia .
Raphael
¿Has intentado graficar tu resultado (con algún factor constante) frente a los tiempos reales de medición? (A menudo es más informativo trazar la relación y adivinar si converge con algo en .) Dicho esto, me resulta difícil ayudar aquí ya que el ansatz para depende de los detalles de Haskell, que no todos hablan aquí. . Específicamente, ¿cómo se evalúa esta comprensión del conjunto? ¿Se está memorizando? Puede obtener mejores respuestas (¡o cualquier otra, de verdad!) Si incluye una versión de pseudocódigo que expone la mayor parte de lo que realmente sucede como sea necesario para un análisis riguroso. O(1)Ta
Raphael
Finalmente, usar métodos de ajuste para derivar los límites de Landau es probablemente inútil. Cualquiera de estas funciones solo puede ajustarse a un conjunto fijo de funciones; Supongo que Mathematica usó en los peores modelos exponenciales allí, por lo que hizo un mal trabajo al capturar un crecimiento súper exponencial.
Raphael
@Raphael Tus comentarios fueron muy útiles. Lo investigaré más cuando tenga algo de tiempo. Además, el vino de ajustar los logaritmos de los valores a una línea, que era más un tiro en la oscuridad que cualquier otra cosa. O(n22)
Michael Klein

Respuestas:

2

Puede escribir su recurrencia como En particular, . Esto significa que la secuencia crece muy rápidamente, y en particular Por lo tanto, Esto significa que Y entonces Esto mejora en su límite por una raíz cuadrada.

T(n)=(n+1)(T(n1)+2T(n2)+T(n3)+2T(n4)+).
T(n)(n+1)T(n1)T(n)
T(n1)+2T(n2)+T(n1)[1+2n+1n(n1)+2n(n1)(n2)+]=(1+O(1/n))T(n1).
T(n)(n+O(1))T(n1).
T(n)=O((n+O(1))!),
T(n)=O(nO(1)(n/e)n).
Yuval Filmus
fuente