Quel est le temps d'exécution de cet algorithme récursif?

8

J'ai fait le programme Haskell (non golfé) suivant pour le défi de golf de code de calculer les premières valeurs de A229037 .n

Voici ma solution proposée pour calculer la ème valeur: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] ]

Notez que Haskell ne met pas en cache ni ne mémorise automatiquement ces valeurs.

La page OEIS pour la séquence donne le fait que , donc le pourrait être remplacé par , car l'algorithme n'atteindra jamais un supérieur à .une(n)(n+1)/2[1..][1..(n+1)/2]Xn+12

En essayant de compter les appels de fonction, j'ai dérivé la limite supérieure , le nombre d'appels de fonction que l'algorithme prend pour une entrée :T(n)n

T(n)=X=1(n+1)/2k=1n2 T(n-k)+2 T(n-2k)X=1(n+1)/2k=1n T(n-k)X=1(n+1)/2k=1n4 T(n-1)X=1(n+1)/24 n T(n-1)4 n T(n-1) n+122 n (n+1) T(n-1))

J'ai branché la formule finale dans Mathematica:

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

Et obtenu, après une petite simplification:T(n) 2n n! (n+1)!

Le rapport moyen entre cela et le temps d'exécution du programme Haskell, pour est de et l'écart-type des ratios est d'environ . (Curieusement, le diagramme logarithmique des ratios semble être une ligne droite).n[12,20]2.0dix396.0dix39

Les rapports avec la première ligne, définissant , ont une moyenne et un écart type de et , respectivement, mais son tracé saute beaucoup.T(n)4.8dix61,8dix6

Comment puis-je obtenir une meilleure limite sur la complexité temporelle de cet algorithme?

Voici l'algorithme en C valide (moins les déclarations avancées), qui je crois est à peu près équivalent au code 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 version C prend environ 5 minutes pour calculer et la version Haskell prend environ la même chose pour .une(17)une(19)

Les premières fois des versions:

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
la source
J'ai changé les balises et le titre pour préciser qu'il s'agit d'une analyse d'algorithme, pas d'une question de théorie de la complexité. "En supposant que la multiplication et l'addition sont négligeables" - pouvez-vous ? Vraiment ? Il vaut toujours mieux dire ce que vous comptez, car il est probable que vous ne comptez pas la plupart des choses. Voir aussi notre question de référence .
Raphael
Avez-vous essayé de tracer votre résultat (avec un facteur constant) par rapport aux durées de mesure réelles? (Il est souvent plus instructif de tracer le rapport et de deviner s'il converge vers quelque chose dansO(1).) Cela dit, j'ai du mal à aider ici depuis l'ansatz de Tdépend des détails de Haskell, dont tout le monde ici ne parle pas. Plus précisément, comment cette compréhension d'ensemble est-elle évaluée? Est amémorisé? Vous pouvez obtenir de meilleures réponses (ou n'importe laquelle, vraiment!) Si vous incluez une version de pseudo-code qui expose autant de ce qui se passe réellement que nécessaire pour une analyse rigoureuse.
Raphael
Enfin, l'utilisation de méthodes d'ajustement pour dériver des bornes de Landau est probablement futile. Une telle fonction ne peut tenir que contre un ensemble fixe de fonctions; Je suppose que Mathematica a utilisé les pires modèles exponentiels là-bas, donc a fait un mauvais travail en capturant une croissance super-exponentielle.
Raphael
@Raphael Vos commentaires ont été très utiles. J'y reviendrai plus quand j'aurai du temps. ÉgalementO(n22)est venu d'ajuster les logarithmes des valeurs à une ligne, qui était plus un coup dans l'obscurité qu'autre chose.
Michael Klein

Réponses:

2

Vous pouvez écrire votre récurrence comme

T(n)=(n+1)(T(n-1)+2T(n-2)+T(n-3)+2T(n-4)+).
En particulier, T(n)(n+1)T(n-1). Cela signifie que la séquenceT(n) croît très rapidement, et en particulier
T(n-1)+2T(n-2)+T(n-1)[1+2n+1n(n-1)+2n(n-1)(n-2)+]=(1+O(1/n))T(n-1).
Donc
T(n)(n+O(1))T(n-1).
Cela signifie que
T(n)=O((n+O(1))!),
et donc
T(n)=O(nO(1)(n/e)n).
Cela améliore votre lié par une racine carrée.
Yuval Filmus
la source