Pourquoi la complexité de calcul O (n ^ 4)?

50
int sum = 0;
for(int i = 1; i < n; i++) {
    for(int j = 1; j < i * i; j++) {
        if(j % i == 0) {
            for(int k = 0; k < j; k++) {
                sum++;
            }
        }
    }
}

Je ne comprends pas comment quand j = i, 2i, 3i ... la dernière forboucle s'exécute n fois. Je suppose que je ne comprends tout simplement pas comment nous sommes arrivés à cette conclusion sur la base de la ifdéclaration.

Edit: je sais comment calculer la complexité de toutes les boucles, sauf pourquoi la dernière boucle exécute i fois en fonction de l'opérateur de mod ... Je ne vois tout simplement pas comment c'est i. Fondamentalement, pourquoi j% i ne peut-il pas aller jusqu'à i * i plutôt que i?

user11452926
la source
5
Vous pouvez réduire la complexité de ce code par plusieurs grands facteurs. Astuce : La somme des nombres 1 à n est ((n + 1) * n) / 2 Astuce 2 : for (j = i; j < i *i; j += i)alors vous n'avez pas besoin du test de module (car il jest garanti d'être divisible par i).
Elliott Frisch
1
La fonction O () est une fonction de parcage de balle, donc toute boucle dans cet exemple ajoute à la complexité. La deuxième boucle fonctionne jusqu'à n ^ 2. Les instructions if sont ignorées.
Christoph Bauer
11
Les ifdéclarations @ChristophBauer ne sont absolument pas ignorées. Cette ifinstruction signifie que la complexité est O (n ^ 4) au lieu de O (n ^ 5), car elle oblige la boucle la plus interne à exécuter uniquement des itemps au lieu de i*ifois pour chaque itération de la deuxième boucle.
kaya3
1
@ kaya3 a totalement raté la k < n^2partie. C'est donc O (n ^ 5) mais la connaissance (en comprenant le if) suggère O (n ^ 4).
Christoph Bauer
1
Si ce n'est pas seulement un exercice en classe, changez la deuxième boucle en for (int j = i; j <i * i; j + = i)
Cristobol Polychronopolis

Réponses:

49

Étiquetons les boucles A, B et C:

int sum = 0;
// loop A
for(int i = 1; i < n; i++) {
    // loop B
    for(int j = 1; j < i * i; j++) {
        if(j % i == 0) {
            // loop C
            for(int k = 0; k < j; k++) {
                sum++;
            }
        }
    }
}
  • La boucle A itère O ( n ) fois.
  • Boucle B itère O ( i 2 ) fois par itération d'une . Pour chacune de ces itérations:
    • j % i == 0 est évalué, ce qui prend du temps O (1).
    • Sur 1 / i de ces itérations, la boucle C itère j fois, faisant O (1) travail par itération. Puisque j est O ( i 2 ) en moyenne, et cela n'est fait que pour 1 / i itérations de la boucle B, le coût moyen est O ( i 2  /  i ) = O ( i ).

En multipliant tout cela ensemble, nous obtenons O ( n  ×  i 2  × (1 +  i )) = O ( n  ×  i 3 ). Puisque i est en moyenne O ( n ), c'est O ( n 4 ).


La partie délicate de cela est de dire que la ifcondition n'est vraie que 1 / i du temps:

Fondamentalement, pourquoi j% i ne peut-il pas aller jusqu'à i * i plutôt que i?

En fait, jne monte pas j < i * i, pas seulement j < i. Mais la condition j % i == 0est vraie si et seulement si jest un multiple de i.

Les multiples de l' iintérieur de la gamme sont i, 2*i, 3*i, ..., (i-1) * i. Il y en a i - 1, donc la boucle C est atteinte i - 1fois malgré les i * i - 1temps d' itération de la boucle B.

kaya3
la source
2
En O (n × i ^ 2 × (1 + i)) pourquoi 1 + i?
Soleil
3
Parce que la ifcondition prend du temps O (1) à chaque itération de la boucle B. Elle est dominée par la boucle C ici, mais je l'ai comptée ci-dessus donc c'est juste "montrer mon travail".
kaya3
16
  • La première boucle consomme des nitérations.
  • La deuxième boucle consomme des n*nitérations. Imaginez le cas quand i=n, alors j=n*n.
  • La troisième boucle consomme des nitérations car elle n'est exécutée que des ifois, où elle iest limitée ndans le pire des cas.

Ainsi, la complexité du code est O (n × n × n × n).

J'espère que cela vous aide à comprendre.

Mohammed Deifallah
la source
6

Toutes les autres réponses sont correctes, je veux juste modifier ce qui suit. Je voulais voir si la réduction des exécutions de la boucle k interne était suffisante pour réduire la complexité réelle ci-dessous. O(n⁴).J'ai donc écrit ce qui suit:

for (int n = 1; n < 363; ++n) {
    int sum = 0;
    for(int i = 1; i < n; ++i) {
        for(int j = 1; j < i * i; ++j) {
            if(j % i == 0) {
                for(int k = 0; k < j; ++k) {
                    sum++;
                }
            }
        }
    }

    long cubic = (long) Math.pow(n, 3);
    long hypCubic = (long) Math.pow(n, 4);
    double relative = (double) (sum / (double) hypCubic);
    System.out.println("n = " + n + ": iterations = " + sum +
            ", n³ = " + cubic + ", n⁴ = " + hypCubic + ", rel = " + relative);
}

Après avoir exécuté cela, il devient évident que la complexité est en fait n⁴. Les dernières lignes de sortie ressemblent à ceci:

n = 356: iterations = 1989000035, n³ = 45118016, n = 16062013696, rel = 0.12383254507467704
n = 357: iterations = 2011495675, n³ = 45499293, n = 16243247601, rel = 0.12383580700180696
n = 358: iterations = 2034181597, n³ = 45882712, n = 16426010896, rel = 0.12383905075183874
n = 359: iterations = 2057058871, n³ = 46268279, n = 16610312161, rel = 0.12384227647628734
n = 360: iterations = 2080128570, n³ = 46656000, n = 16796160000, rel = 0.12384548432498857
n = 361: iterations = 2103391770, n³ = 47045881, n = 16983563041, rel = 0.12384867444612208
n = 362: iterations = 2126849550, n³ = 47437928, n = 17172529936, rel = 0.1238518469862343

Ce que cela montre, c'est que la différence relative réelle entre le réel n⁴et la complexité de ce segment de code est un facteur asymptotique vers une valeur autour 0.124...(en fait 0,125). Bien qu'il ne nous donne pas la valeur exacte, nous pouvons en déduire ce qui suit:

La complexité du temps est n⁴/8 ~ f(n)où se ftrouve votre fonction / méthode.

  • La page wikipedia sur la notation Big O indique dans les tableaux de la «famille des notations Bachmann – Landau» que la ~définition de la limite des deux côtés de l'opérande est égale. Ou:

    f est égal à g asymptotiquement

(J'ai choisi 363 comme borne supérieure exclue, car n = 362c'est la dernière valeur pour laquelle nous obtenons un résultat sensible. Après cela, nous dépassons l'espace long et la valeur relative devient négative.)

L'utilisateur kaya3 a compris ce qui suit:

La constante asymptotique est exactement 1/8 = 0,125, soit dit en passant; voici la formule exacte via Wolfram Alpha .

TreffnonX
la source
5
Bien sûr, O (n⁴) * 0,125 = O (n⁴). La multiplication du temps d'exécution par un facteur constant positif ne change pas la complexité asymptotique.
Ilmari Karonen
C'est vrai. Cependant, j'essayais de refléter la complexité réelle, pas l'estimation supérieure. Comme je n'ai trouvé aucune autre syntaxe pour exprimer la complexité temporelle autre que la notation O, je me suis replongé là-dessus. Il n'est cependant pas sensé à 100% de l'écrire ainsi.
TreffnonX
Vous pouvez utiliser la notation little-o pour dire que la complexité temporelle est n⁴/8 + o(n⁴), mais il est tout de même possible de donner une expression plus stricte n⁴/8 + O(n³)avec un grand O.
kaya3
@TreffnonX big OH est un concept mathématique solide. Donc ce que vous faites est fondamentalement faux / vide de sens. Bien sûr, vous êtes libre de redéfinir les concepts mathématiques, mais c'est une grosse boîte de vers que vous ouvrez alors. La façon de le définir dans un contexte plus strict est ce que kaya3 a décrit, vous allez un ordre "inférieur" et le définissez de cette façon. (Bien qu'en mathématiques, vous utilisez généralement la réciproque).
paul23
Vous avez raison. Je me corrigeai à nouveau. Cette fois, j'utilise la croissance asymtotique vers la même limite, telle que définie dans la famille des notations de Bachmann-Landau sur en.wikipedia.org/wiki/Big_O_notation#Little-o_notation . J'espère que c'est maintenant mathématiquement correct pour ne pas inciter à la révolte;)
TreffnonX
2

Supprimer ifet modulo sans changer la complexité

Voici la méthode originale:

public static long f(int n) {
    int sum = 0;
    for (int i = 1; i < n; i++) {
        for (int j = 1; j < i * i; j++) {
            if (j % i == 0) {
                for (int k = 0; k < j; k++) {
                    sum++;
                }
            }
        }
    }
    return sum;
}

Si vous êtes confus par le ifet modulo, vous pouvez simplement les refactoriser, en jsautant directement de ià 2*ià 3*i...:

public static long f2(int n) {
    int sum = 0;
    for (int i = 1; i < n; i++) {
        for (int j = i; j < i * i; j = j + i) {
            for (int k = 0; k < j; k++) {
                sum++;
            }
        }
    }
    return sum;
}

Pour faciliter encore plus le calcul de la complexité, vous pouvez introduire une j2variable intermédiaire , de sorte que chaque variable de boucle est incrémentée de 1 à chaque itération:

public static long f3(int n) {
    int sum = 0;
    for (int i = 1; i < n; i++) {
        for (int j2 = 1; j2 < i; j2++) {
            int j = j2 * i;
            for (int k = 0; k < j; k++) {
                sum++;
            }
        }
    }
    return sum;
}

Vous pouvez utiliser le débogage ou la vieille école System.out.printlnafin de vérifier que le i, j, ktriplet est toujours le même dans chaque méthode.

Expression de forme fermée

Comme mentionné par d'autres, vous pouvez utiliser le fait que la somme des premiers n entiers est égale à n * (n+1) / 2(voir les nombres triangulaires ). Si vous utilisez cette simplification pour chaque boucle, vous obtenez:

public static long f4(int n) {
    return (n - 1) * n * (n - 2) * (3 * n - 1) / 24;
}

Ce n'est évidemment pas la même complexité que le code d'origine mais il renvoie les mêmes valeurs.

Si vous recherchez sur Google les premiers termes, vous pouvez remarquer qu'ils 0 0 0 2 11 35 85 175 322 546 870 1320 1925 2717 3731apparaissent dans "Numéros de Stirling du premier type: s (n + 2, n)". , avec deux 0s ajoutés au début. Cela signifie que sumc'est le nombre de Stirling du premier type s(n, n-2) .

Eric Duminil
la source
0

Jetons un coup d'œil aux deux premières boucles.

Le premier est simple, il est en boucle de 1 à n. Le second est plus intéressant. Cela va de 1 à i au carré. Voyons quelques exemples:

e.g. n = 4    
i = 1  
j loops from 1 to 1^2  
i = 2  
j loops from 1 to 2^2  
i = 3  
j loops from 1 to 3^2  

Au total, les i and j loopscombinés ont 1^2 + 2^2 + 3^2.
Il existe une formule pour la somme des n premiers carrés n * (n+1) * (2n + 1) / 6, qui est approximativement O(n^3).

Vous avez une dernière k loopboucle de 0 à jif et seulement if j % i == 0. Depuis jva de 1 à i^2, j % i == 0est vrai pour les itemps. Puisque l' i loopitération est terminée n, vous en avez un de plus O(n).

Vous avez donc O(n^3)de i and j loopset un autre à O(n)partir k looppour un grand total deO(n^4)

Silviu Burcea
la source
Je sais comment calculer la complexité de toutes les boucles, sauf pourquoi la dernière boucle exécute i fois en fonction de l'opérateur mod ... Je ne vois tout simplement pas comment c'est i. Fondamentalement, pourquoi j% i ne peut-il pas aller jusqu'à i * i plutôt que i?
user11452926
1
@ user11452926 disons que j'étais 5. j passerait de 1 à 25 dans la 2ème boucle. Cependant, j % i == 0seulement lorsque j vaut 5, 10, 15, 20 et 25. 5 fois, comme la valeur de i. Si vous notiez les nombres de 1 à 25 dans un carré de 5 x 5, seule la 5e colonne contiendrait les nombres divisibles par 5. Cela fonctionne pour n'importe quel nombre de i. Tracez un carré de n par n en utilisant les nombres 1 à n ^ 2. La nième colonne contiendra les nombres divisibles par n. Vous avez n lignes, donc n nombres de 1 à n ^ 2 divisibles par n.
Silviu Burcea
Merci! logique! Et si c'était un nombre arbitraire comme 24 plutôt que 25, l'astuce carrée fonctionnera-t-elle toujours?
user11452926
25 vient quand ifrappe 5, donc les jboucles de 1 à 25, vous ne pouvez pas choisir un nombre arbitraire. Si votre 2ème boucle devait aller à un nombre fixe, par exemple 24, au lieu de i * i, ce serait un nombre constant et ne serait pas lié n, alors ce serait le cas O(1). Si vous pensez à j < i * ivs j <= i * i, cela n'aura pas beaucoup d'importance, car il y aura netn-1 opérations, mais dans la notation Big-oh, les deux moyensO(n)
Silviu Burcea