Mengapa kompleksitas komputasional 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++;
            }
        }
    }
}

Saya tidak mengerti bagaimana ketika j = i, 2i, 3i ... forloop terakhir berjalan n kali. Saya kira saya hanya tidak mengerti bagaimana kita sampai pada kesimpulan berdasarkan pada ifpernyataan itu.

Sunting: Saya tahu bagaimana menghitung kompleksitas untuk semua loop kecuali untuk mengapa loop terakhir mengeksekusi i kali berdasarkan operator mod ... Saya hanya tidak melihat bagaimana itu saya. Pada dasarnya, mengapa saya tidak bisa naik ke i * i daripada saya?

pengguna11452926
sumber
5
Anda dapat mengurangi kompleksitas kode ini dengan banyak faktor besar . Petunjuk : Jumlah angka 1 hingga n adalah ((n + 1) * n) / 2 Petunjuk 2 : for (j = i; j < i *i; j += i)maka Anda tidak memerlukan tes modulus (karena jdijamin dapat dibagi oleh i).
Elliott Frisch
1
Fungsi O () adalah fungsi ball-park sehingga setiap loop dalam contoh ini menambah kompleksitas. Loop kedua berjalan hingga n ^ 2. jika-pernyataan diabaikan.
Christoph Bauer
11
ifPernyataan @ChristophBauer sama sekali tidak diabaikan. ifPernyataan ini berarti kompleksitasnya adalah O (n ^ 4), bukan O (n ^ 5), karena menyebabkan loop terdalam hanya mengeksekusi iwaktu alih-alih i*iwaktu untuk setiap iterasi dari loop kedua.
kaya3
1
@ kaya3 benar-benar ketinggalan bagian. k < n^2Jadi itu O (n ^ 5) tetapi pengetahuan (dengan memahami if) menyarankan O (n ^ 4).
Christoph Bauer
1
Jika ini bukan hanya latihan kelas, ubah loop kedua menjadi untuk (int j = i; j <i * i; j + = i)
Cristobol Polychronopolis

Jawaban:

49

Mari beri label loop A, B dan 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++;
            }
        }
    }
}
  • Loop A berulang O ( n ) kali.
  • Lingkaran B iterates O ( i 2 ) kali per iterasi A . Untuk setiap iterasi ini:
    • j % i == 0 dievaluasi, yang membutuhkan O (1) waktu.
    • Pada 1 / i iterasi ini, lingkaran C iterates j kali, melakukan O (1) kerja per iterasi. Karena j adalah rata-rata O ( i 2 ), dan ini hanya dilakukan untuk iterasi I / i dari loop B, biaya rata-rata adalah O ( i 2  /  i ) = O ( i ).

Mengalikan semua ini bersama-sama, kita mendapatkan O ( n  ×  i 2  × (1 +  i )) = O ( n  ×  i 3 ). Karena saya adalah rata-rata O ( n ), ini adalah O ( n 4 ).


Bagian yang sulit dari ini adalah mengatakan bahwa ifkondisinya hanya benar 1 / i saat itu:

Pada dasarnya, mengapa saya tidak bisa naik ke i * i daripada saya?

Bahkan, jtidak naik ke atas j < i * i, bukan hanya sampai j < i. Tetapi kondisinya j % i == 0benar jika dan hanya jika jmerupakan kelipatan i.

Kelipatan dari idalam rentang yang i, 2*i, 3*i, ..., (i-1) * i. Ada i - 1ini, sehingga loop C tercapai i - 1kali meskipun loop B i * i - 1berulang kali.

kaya3
sumber
2
Dalam O (n × i ^ 2 × (1 + i)) mengapa 1 + i?
Soleil
3
Karena ifkondisi membutuhkan O (1) waktu pada setiap iterasi dari loop B. Ini didominasi oleh loop C di sini, tapi saya menghitungnya di atas sehingga hanya "menunjukkan pekerjaan saya".
kaya3
16
  • Loop pertama mengkonsumsi niterasi.
  • Loop kedua mengkonsumsi n*niterasi. Bayangkan kasusnya kapan i=n, lalu j=n*n.
  • Loop ketiga mengkonsumsi niterasi karena hanya dieksekusi iberkali-kali, di mana iterikat npada kasus terburuk.

Dengan demikian, kompleksitas kode adalah O (n × n × n × n).

Saya harap ini membantu Anda mengerti.

Mohammed Deifallah
sumber
6

Semua jawaban lain benar, saya hanya ingin mengubah yang berikut. Saya ingin melihat, jika pengurangan eksekusi k-loop dalam cukup untuk mengurangi kompleksitas aktual di bawah ini O(n⁴).Jadi saya menulis yang berikut:

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

Setelah melaksanakan ini, menjadi jelas, bahwa kerumitan itu sebenarnya n⁴. Baris terakhir dari output terlihat seperti ini:

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

Apa ini menunjukkan, bahwa perbedaan relatif aktual antara aktual n⁴dan kompleksitas segmen kode ini adalah faktor asimtotik terhadap nilai sekitar 0.124...(sebenarnya 0,125). Meskipun tidak memberi kami nilai yang tepat, kami dapat menyimpulkan, berikut ini:

Kompleksitas waktu adalah di n⁴/8 ~ f(n)mana ffungsi / metode Anda.

  • Halaman wikipedia pada notasi O Besar menyatakan dalam tabel 'Family of Bachmann-Landau notations' yang ~mendefinisikan batas kedua sisi operan adalah sama. Atau:

    f sama dengan g asimptotik

(Saya memilih 363 sebagai batas atas yang dikecualikan, karena n = 362nilai terakhir yang kami dapatkan hasilnya masuk akal. Setelah itu, kami melampaui ruang panjang dan nilai relatif menjadi negatif.)

Pengguna kaya3 menemukan yang berikut:

Omong kosong asimptotik persis 1/8 = 0,125, omong-omong; inilah rumus yang tepat melalui Wolfram Alpha .

TreffnonX
sumber
5
Tentu saja, O (n⁴) * 0.125 = O (n⁴). Mengalikan runtime dengan faktor konstan positif tidak mengubah kompleksitas asimptotik.
Ilmari Karonen
Ini benar. Namun saya mencoba untuk mencerminkan kompleksitas yang sebenarnya, bukan perkiraan upperbound. Karena saya tidak menemukan sintaks lain untuk mengekspresikan kompleksitas waktu selain notasi-O, saya kembali ke situ. Namun tidak masuk akal untuk menulisnya seperti ini 100%.
TreffnonX
Anda dapat menggunakan notasi kecil-kecilan untuk mengatakan kompleksitas waktu n⁴/8 + o(n⁴), tetapi mungkin untuk memberikan ekspresi yang lebih ketat n⁴/8 + O(n³)dengan huruf O besar.
kaya3
@TreffnonX big OH adalah konsep matematika yang solid. Jadi apa yang Anda lakukan pada dasarnya salah / tidak berarti. Tentu saja Anda bebas mendefinisikan ulang konsep matematika, tetapi itu adalah kaleng besar cacing yang Anda buka saat itu. Cara untuk mendefinisikannya dalam konteks yang lebih ketat adalah apa yang dijelaskan kaya3, Anda menjalankan perintah "lebih rendah" dan mendefinisikannya seperti itu. (Meskipun dalam matematika Anda biasanya menggunakan membalas).
paul23
Anda benar. Saya memperbaiki diri lagi. Kali ini, saya menggunakan pertumbuhan asimtotik menuju batas yang sama, seperti yang didefinisikan dalam notasi Family of Bachmann-Landau di en.wikipedia.org/wiki/Big_O_notation#Little-o_notation . Saya harap ini sekarang secara matematis cukup benar untuk tidak memicu pemberontakan;)
TreffnonX
2

Hapus ifdan modulo tanpa mengubah kompleksitas

Inilah metode aslinya:

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

Jika Anda bingung dengan ifdan Modulo, Anda hanya bisa refactor mereka pergi, dengan jmelompat langsung dari ike 2*ike 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;
}

Untuk membuatnya lebih mudah untuk menghitung kompleksitas, Anda dapat memperkenalkan j2variabel perantara , sehingga setiap variabel loop bertambah 1 pada setiap iterasi:

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

Anda dapat menggunakan debugging atau old-school System.out.printlnuntuk memeriksa bahwa i, j, ktriplet selalu sama di setiap metode.

Ekspresi bentuk tertutup

Seperti yang disebutkan oleh orang lain, Anda dapat menggunakan fakta bahwa jumlah n bilangan bulat pertama sama dengan n * (n+1) / 2(lihat angka segitiga ). Jika Anda menggunakan penyederhanaan ini untuk setiap loop, Anda mendapatkan:

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

Jelas bukan kompleksitas yang sama dengan kode asli tetapi mengembalikan nilai yang sama.

Jika Anda menggunakan istilah pertama di Google, Anda dapat melihat yang 0 0 0 2 11 35 85 175 322 546 870 1320 1925 2717 3731muncul di "Angka pengadukan dari jenis pertama: s (n + 2, n)." , dengan dua 0s ditambahkan di awal. Ini berarti itu sumadalah angka Stirling dari jenis pertama s(n, n-2) .

Eric Duminil
sumber
0

Mari kita lihat dua loop pertama.

Yang pertama sederhana, perulangan dari 1 ke n. Yang kedua lebih menarik. Mulai dari 1 hingga kuadrat. Mari kita lihat beberapa contoh:

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  

Secara total, i and j loopsgabungan tersebut memiliki 1^2 + 2^2 + 3^2.
Ada rumus untuk jumlah n kotak pertama n * (n+1) * (2n + 1) / 6, yang kira-kira O(n^3).

Anda memiliki yang terakhir k loopdari 0 hingga jjika dan hanya jika j % i == 0. Sejak jberalih dari 1 ke i^2, j % i == 0benar untuk ikali. Sejak i loopiterates berakhir n, Anda memiliki satu ekstra O(n).

Jadi, Anda memiliki O(n^3)dari i and j loopsdan yang lain O(n)dari k loopuntuk totalO(n^4)

Silviu Burcea
sumber
Saya tahu bagaimana menghitung kompleksitas untuk semua loop kecuali untuk mengapa loop terakhir dieksekusi i kali berdasarkan operator mod ... Saya hanya tidak melihat bagaimana itu saya. Pada dasarnya, mengapa saya tidak bisa naik ke i * i daripada saya?
user11452926
1
@ user11452926 katakanlah saya adalah 5. j akan pergi dari 1 hingga 25 di loop ke-2. Namun, j % i == 0hanya ketika j adalah 5, 10, 15, 20 dan 25. 5 kali, seperti nilai i. Jika Anda menuliskan angka-angka dari 1 hingga 25 dalam 5 x 5 persegi, hanya kolom ke-5 yang berisi angka-angka yang dapat dibagi 5. Ini berfungsi untuk sejumlah i. Gambarkan sebuah kotak n dengan n menggunakan angka 1 hingga n ^ 2. Kolom ke-n akan berisi angka-angka yang dapat dibagi oleh n. Anda memiliki n baris, sehingga n angka dari 1 hingga n ^ 2 habis dibagi n.
Silviu Burcea
Terima kasih! masuk akal! Bagaimana jika itu nomor yang sewenang-wenang seperti 24 daripada 25, apakah trik kuadrat masih berfungsi?
user11452926
25 datang ketika iklik 5, jadi jloop dari 1 hingga 25, Anda tidak dapat memilih nomor acak. Jika loop kedua Anda akan menuju ke nomor tetap, misalnya 24, bukannya i * i, itu akan menjadi angka konstan dan tidak akan terikat n, jadi itu akan menjadi O(1). Jika Anda berpikir tentang j < i * ivs. j <= i * i, itu tidak masalah, karena akan ada ndan n-1operasi, tetapi dalam notasi Big-oh, keduanya berartiO(n)
Silviu Burcea