Perché la complessità computazionale 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++;
            }
        }
    }
}

Non capisco come quando j = i, 2i, 3i ... l'ultimo forciclo viene eseguito n volte. Immagino di non capire come siamo arrivati ​​a questa conclusione sulla base diif dichiarazione.

Modifica: so come calcolare la complessità per tutti i loop tranne per il motivo per cui l'ultimo ciclo viene eseguito i volte in base all'operatore mod ... Non vedo come sono io. Fondamentalmente, perché j% non riesco a salire su i * i piuttosto che io?


5
È possibile ridurre la complessità di questo codice da più fattori di grandi dimensioni . Suggerimento : la somma dei numeri da 1 a n è ((n + 1) * n) / 2 Suggerimento 2 : for (j = i; j < i *i; j += i)quindi non è necessario il test del modulo (perché jè garantito che sia divisibile per i).
Elliott Frisch,

1
La funzione O () è una funzione ball-park, quindi qualsiasi loop in questo esempio aggiunge complessità. Il secondo loop è in esecuzione fino a n ^ 2. le dichiarazioni if ​​vengono ignorate.
Christoph Bauer, l'

11
Le ifdichiarazioni di @ChristophBauer non sono assolutamente ignorate. Questa ifaffermazione indica che la complessità è O (n ^ 4) invece di O (n ^ 5), poiché fa sì che il ciclo più interno esegua solo i itempi anziché i i*itempi per ogni iterazione del secondo ciclo.
kaya3,

1
@ kaya3 ha completamente perso la k < n^2parte, quindi è O (n ^ 5) ma la conoscenza (comprendendo if) suggerisce O (n ^ 4).
Christoph Bauer, l'

1
Se questo non è solo un esercizio di classe, cambia il secondo ciclo in per (int j = i; j <i * i; j + = i)
Cristobol Polychronopolis

Risposte:


49

Etichettiamo i loop A, B e 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++;
            }
        }
    }
}
  • Il ciclo A itera O ( n ) volte.
  • Il ciclo B itera O ( i 2 ) volte per iterazione di A . Per ciascuna di queste iterazioni:
    • j % i == 0 viene valutato, il che richiede O (1) tempo.
    • Su 1 / i di queste iterazioni, il ciclo C itera j volte, facendo O (1) lavoro per iterazione. Poiché j è O ( i 2 ) in media, e questo viene fatto solo per iterazioni 1 / i del loop B, il costo medio è O ( i 2  /  i ) = O ( i ).

Moltiplicando tutto questo insieme, otteniamo O ( n  ×  i 2  × (1 +  i )) = O ( n  ×  i 3 ). Poiché i è in media O ( n ), questo è O ( n 4 ).


La parte difficile di questo sta dicendo che la ifcondizione è vera solo 1 / i del tempo:

Fondamentalmente, perché j% non riesco a salire su i * i piuttosto che io?

In realtà, jva fino a j < i * i, non solo fino a j < i. Ma la condizione j % i == 0è vera se e solo se jè un multiplo di i.

I multipli di iall'interno della gamma sono i, 2*i, 3*i, ..., (i-1) * i. Ce ne sono alcuni i - 1, quindi i tempi del loop C vengono raggiunti i - 1nonostante i i * i - 1tempi di iterazione del loop B.


2
In O (n × i ^ 2 × (1 + i)) perché 1 + i?
Soleil,

3
Perché la ifcondizione richiede O (1) tempo su ogni iterazione del ciclo B. Qui è dominata dal ciclo C, ma l'ho contata sopra quindi è solo "mostrando il mio lavoro".
kaya3,

16
  • Il primo ciclo consuma niterazioni.
  • Il secondo ciclo consuma n*niterazioni. Immagina il caso quando i=n, allora j=n*n.
  • Il terzo ciclo consuma niterazioni perché viene eseguito solo ivolte, dove iè limitato nnel caso peggiore.

Pertanto, la complessità del codice è O (n × n × n × n).

Spero che questo ti aiuti a capire.


6

Tutte le altre risposte sono corrette, voglio solo modificare quanto segue. Volevo vedere se la riduzione delle esecuzioni del k-loop interno fosse sufficiente per ridurre la complessità effettiva di seguito. O(n⁴).Quindi ho scritto quanto segue:

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);
}

Dopo aver eseguito ciò, diventa evidente che la complessità è in realtà n⁴. Le ultime righe di output si presentano così:

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

Ciò mostra che la differenza relativa effettiva tra effettiva n⁴e la complessità di questo segmento di codice è un fattore asintotico verso un valore intorno0.124... (in realtà 0,125). Sebbene non ci dia il valore esatto, possiamo dedurre quanto segue:

La complessità del tempo è n⁴/8 ~ f(n)dov'è la ftua funzione / metodo.

  • La pagina di Wikipedia sulla notazione Big O afferma nelle tabelle delle "Notazioni della famiglia Bachmann – Landau" che ~il limite di definizione dei due lati dell'operando è uguale. O:

    f è uguale a g asintoticamente

(Ho scelto 363 come limite superiore escluso, perché n = 362 è l'ultimo valore per il quale otteniamo un risultato ragionevole. Successivamente, superiamo lo spazio lungo e il valore relativo diventa negativo.)

L'utente kaya3 ha capito quanto segue:

La costante asintotica è esattamente 1/8 = 0.125, tra l'altro; ecco la formula esatta tramite Wolfram Alpha .


5
Certo, O (n⁴) * 0.125 = O (n⁴). Moltiplicare il tempo di esecuzione per un fattore costante positivo non cambia la complessità asintotica.
Ilmari Karonen,

Questo è vero. Tuttavia, stavo cercando di riflettere la complessità effettiva, non la stima del limite superiore. Dato che non ho trovato altra sintassi per esprimere la complessità temporale diversa dalla notazione O, sono tornato su questo. Non è tuttavia sensato al 100% scriverlo in questo modo.
TreffnonX,

Puoi usare poca notazione per dire che la complessità del tempo è n⁴/8 + o(n⁴), ma è comunque possibile dare un'espressione più rigorosa n⁴/8 + O(n³)con una grande O.
kaya3,

@TreffnonX big OH è un solido concetto matematico. Quindi quello che stai facendo è fondamentalmente sbagliato / insignificante. Naturalmente sei libero di ridefinire i concetti matematici, ma questa è una grande lattina di worm che stai aprendo allora. Il modo per definirlo in un contesto più rigoroso è quello che ha descritto kaya3, si passa un ordine "inferiore" e lo si definisce in questo modo. (Sebbene in matematica di solito usi il reciprocato).
Paul23,

Hai ragione. Mi sono corretto di nuovo. Questa volta, uso la crescita asintotica verso lo stesso limite, come definito nelle notazioni della Famiglia di Bachmann-Landau su en.wikipedia.org/wiki/Big_O_notation#Little-o_notation . Spero che questo sia ora matematicamente corretto per non incitare alla rivolta;)
TreffnonX,

2

Rimuovi ife modulo senza cambiare la complessità

Ecco il metodo 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;
}

Se sei confuso dal ifmodulo e, puoi semplicemente rimuoverlo, jsaltando direttamente da ia 2*ia 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;
}

Per semplificare ulteriormente il calcolo della complessità, è possibile introdurre una j2variabile intermedia , in modo che ogni variabile del ciclo sia incrementata di 1 ad ogni iterazione:

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;
}

È possibile utilizzare il debug o la vecchia scuola System.out.printlnper verificare che la i, j, ktripletta sia sempre la stessa in ciascun metodo.

Espressione a forma chiusa

Come menzionato da altri, puoi usare il fatto che la somma dei primi n numeri interi è uguale a n * (n+1) / 2(vedi numeri triangolari ). Se usi questa semplificazione per ogni ciclo, otterrai:

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

Ovviamente non è la stessa complessità del codice originale ma restituisce gli stessi valori.

Se si google i primi termini, si può notare che 0 0 0 2 11 35 85 175 322 546 870 1320 1925 2717 3731compaiono in "Stirling numeri del primo tipo: s (n + 2, n)". , con due 0s aggiunte all'inizio. Significa che sumè il numero Stirling del primo tipo s(n, n-2) .


0

Diamo un'occhiata ai primi due loop.

Il primo è semplice, va in loop da 1 a n. Il secondo è più interessante. Va da 1 a I al quadrato. Vediamo alcuni esempi:

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  

In totale, il i and j loopscombinato ha 1^2 + 2^2 + 3^2.
Esiste una formula per la somma dei primi n quadrati n * (n+1) * (2n + 1) / 6, che è approssimativamenteO(n^3) .

Hai un ultimo k loopche passa da 0 a jif e solo if j % i == 0. Dal momento che jva da 1 a i^2, j % i == 0è vero per i itempi. Dal momento che l' i loopiterazione termina n, hai un extra O(n).

In modo da avere O(n^3)da i and j loopse un'altra O(n)da k loopper un totale diO(n^4)


So come calcolare la complessità di tutti i loop tranne per il motivo per cui l'ultimo loop viene eseguito i volte in base all'operatore mod ... Non vedo come sono io. Fondamentalmente, perché j% non riesco a salire su i * i piuttosto che io?
user11452926

1
@ user11452926 diciamo che ero 5. j andrebbe da 1 a 25 nel 2 ° ciclo. Tuttavia, j % i == 0solo quando j è 5, 10, 15, 20 e 25. 5 volte, come il valore di i. Se annoti i numeri da 1 a 25 in 5 x 5 quadrati, solo la 5a colonna conterrà i numeri divisibili per 5. Funziona per qualsiasi numero di i. Disegna un quadrato di n per n usando i numeri da 1 a n ^ 2. L'ennesima colonna conterrà i numeri divisibili per n. Hai n righe, quindi n numeri da 1 a n ^ 2 divisibili per n.
Silviu Burcea,

Grazie! ha senso! E se fosse un numero arbitrario come 24 anziché 25, il trucco quadrato funzionerà ancora?
user11452926

25 arriva quando icolpisce 5, quindi i jloop da 1 a 25, non puoi scegliere un numero arbitrario. Se il tuo secondo ciclo passasse a un numero fisso, ad esempio 24, invece di i * iquello, sarebbe un numero costante e non sarebbe legato a n, quindi sarebbe O(1). Se stai pensando al j < i * ivs. j <= i * i, questo non avrà molta importanza, come ci saranno ne le n-1operazioni, ma nella notazione Big-oh, entrambi i mezziO(n)
Silviu Burcea,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.