Tại sao độ phức tạp tính toán 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++;
            }
        }
    }
}

Tôi không hiểu làm thế nào khi j = i, 2i, 3i ... forvòng lặp cuối chạy n lần. Tôi đoán tôi chỉ không hiểu làm thế nào chúng ta đi đến kết luận đó dựa trên iftuyên bố.

Chỉnh sửa: Tôi biết cách tính độ phức tạp cho tất cả các vòng lặp ngoại trừ lý do tại sao vòng lặp cuối thực hiện i lần dựa trên toán tử mod ... Tôi chỉ không thấy nó như thế nào. Về cơ bản, tại sao j% tôi không thể đi lên i * i chứ không phải tôi?


5
Bạn có thể giảm độ phức tạp của mã này bằng nhiều yếu tố lớn . Gợi ý : Tổng các số từ 1 đến n là ((n + 1) * n) / 2 Gợi ý 2 : for (j = i; j < i *i; j += i)sau đó bạn không cần kiểm tra mô đun (vì jđược đảm bảo chia hết cho i).
Elliott Frisch

1
Hàm O () là hàm công viên bóng nên bất kỳ vòng lặp nào trong ví dụ này đều tăng thêm độ phức tạp. Vòng lặp thứ hai đang chạy tới n ^ 2. if-statement bị bỏ qua.
Christoph Bauer

11
Các ifcâu lệnh @ChristophBauer hoàn toàn không được bỏ qua. Câu iflệnh này có nghĩa là độ phức tạp là O (n ^ 4) thay vì O (n ^ 5), vì nó làm cho vòng lặp trong cùng chỉ thực hiện ithời gian thay vì i*ithời gian cho mỗi lần lặp của vòng lặp thứ hai.
kaya3

1
@ kaya3 hoàn toàn bỏ lỡ phần. k < n^2Vì vậy, đó là O (n ^ 5) nhưng kiến ​​thức (bằng cách hiểu if) gợi ý O (n ^ 4).
Christoph Bauer

1
Nếu đây không chỉ là một bài tập trong lớp, hãy thay đổi vòng lặp thứ hai thành for (int j = i; j <i * i; j + = i)
Cristobol Poly syncopolis

Câu trả lời:


49

Hãy gắn nhãn các vòng A, B và 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++;
            }
        }
    }
}
  • Vòng lặp A lặp lại O ( n ) lần.
  • Vòng B lặp O ( i 2 ) lần mỗi lần lặp của A . Đối với mỗi lần lặp này:
    • j % i == 0 được đánh giá, mất O (1) thời gian.
    • Trên 1 / i của các lần lặp này, vòng lặp C lặp lại j lần, thực hiện O (1) mỗi lần lặp. Vì j là O ( i 2 ) trung bình và điều này chỉ được thực hiện cho 1 / i lần lặp của vòng B, nên chi phí trung bình là O ( i 2  /  i ) = O ( i ).

Nhân tất cả những thứ này lại với nhau, ta được O ( n  ×  i 2  × (1 +  i )) = O ( n  ×  i 3 ). Vì i là trung bình O ( n ), đây là O ( n 4 ).


Phần khó khăn của điều này là nói rằng ifđiều kiện này chỉ đúng 1 / i của thời gian:

Về cơ bản, tại sao j% tôi không thể đi lên i * i chứ không phải tôi?

Trong thực tế, jkhông đi lên j < i * i, không chỉ lên đến j < i. Nhưng điều kiện j % i == 0là đúng khi và chỉ khi jlà bội số của i.

Các bội số của itrong phạm vi là i, 2*i, 3*i, ..., (i-1) * i. Có i - 1những điều này, vì vậy vòng lặp C đạt được i - 1thời gian mặc dù vòng lặp B lặp lại i * i - 1.


2
Trong O (n × i ^ 2 × (1 + i)) tại sao 1 + i?
Soleil

3
Bởi vì ifđiều kiện này mất O (1) thời gian cho mỗi lần lặp của vòng lặp B. Nó bị chi phối bởi vòng lặp C ở đây, nhưng tôi đã đếm nó ở trên để nó chỉ "hiển thị công việc của tôi".
kaya3

16
  • Vòng lặp đầu tiên tiêu thụ các nlần lặp.
  • Vòng lặp thứ hai tiêu thụ n*nlặp đi lặp lại. Hãy tưởng tượng trường hợp khi i=n, sau đó j=n*n.
  • Vòng lặp thứ ba tiêu thụ các nlần lặp bởi vì nó chỉ được thực hiện ilần, trong trường hợp ibị ràng buộc ntrong trường hợp xấu nhất.

Do đó, độ phức tạp của mã là O (n × n × n × n).

Tôi hy vọng điều này sẽ giúp bạn hiểu.


6

Tất cả các câu trả lời khác là chính xác, tôi chỉ muốn sửa đổi như sau. Tôi muốn xem, nếu việc giảm các lần thực hiện của vòng lặp k bên trong là đủ để giảm độ phức tạp thực tế bên dưới O(n⁴).Vì vậy tôi đã viết như sau:

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

Sau khi thực hiện điều này, nó trở nên rõ ràng, rằng sự phức tạp là trên thực tế n⁴. Các dòng đầu ra cuối cùng trông như thế này:

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

Điều này cho thấy, sự khác biệt tương đối thực tế giữa thực tế n⁴và độ phức tạp của đoạn mã này là một yếu tố tiệm cận đối với một giá trị xung quanh 0.124...(thực tế là 0,125). Mặc dù nó không cung cấp cho chúng tôi giá trị chính xác, chúng tôi có thể suy luận như sau:

Thời gian phức tạp là n⁴/8 ~ f(n)nơi fchức năng / phương pháp của bạn.

  • Trang wikipedia về ký hiệu Big O nêu trong các bảng của 'Gia đình Bachmann dòng Landau' rằng ~định nghĩa giới hạn của hai mặt toán hạng là bằng nhau. Hoặc là:

    f bằng g tiệm cận

(Tôi đã chọn 363 là giới hạn trên bị loại trừ, vì n = 362là giá trị cuối cùng mà chúng tôi nhận được kết quả hợp lý. Sau đó, chúng tôi vượt quá không gian dài và giá trị tương đối trở thành âm.)

Người dùng kaya3 đã tìm ra những điều sau đây:

Hằng số tiệm cận là chính xác 1/8 = 0,125; đây là công thức chính xác thông qua Wolfram Alpha .


5
Tất nhiên, O (n⁴) * 0.125 = O (n⁴). Nhân thời gian chạy với một yếu tố hằng số dương không làm thay đổi độ phức tạp tiệm cận.
Ilmari Karonen

Đây là sự thật. Tuy nhiên, tôi đã cố gắng phản ánh sự phức tạp thực tế, không phải là ước tính hướng trên. Vì tôi không tìm thấy cú pháp nào khác để diễn tả độ phức tạp của thời gian ngoài ký hiệu O, tôi đã dựa vào đó. Tuy nhiên không phải là 100% hợp lý để viết nó như thế này.
TreffnonX

Bạn có thể sử dụng ký hiệu nhỏ để nói độ phức tạp của thời gian n⁴/8 + o(n⁴), nhưng dù sao cũng có thể đưa ra biểu thức chặt chẽ hơn n⁴/8 + O(n³)với chữ O lớn.
kaya3

@TreffnonX big OH là một khái niệm toán học vững chắc. Vì vậy, những gì bạn đang làm là sai về cơ bản / vô nghĩa. Tất nhiên, bạn có thể tự do xác định lại các khái niệm toán học, nhưng đó là một con giun lớn bạn đang mở. Cách để xác định nó trong một bối cảnh chặt chẽ hơn là những gì kaya3 đã mô tả, bạn đi theo thứ tự "thấp hơn" và định nghĩa nó theo cách đó. (Mặc dù trong toán học, bạn thường sử dụng đối ứng).
paul23

Bạn nói đúng. Tôi lại sửa mình. Lần này, tôi sử dụng sự tăng trưởng tiệm cận theo cùng một giới hạn, như được định nghĩa trong các ký hiệu của Gia đình Bachmann-Landau trên en.wikipedia.org/wiki/Big_O_notation#Little-o_notation . Tôi hy vọng điều này bây giờ đủ chính xác về mặt toán học để không kích động nổi dậy;)
TreffnonX

2

Loại bỏ ifvà modulo mà không thay đổi độ phức tạp

Đây là phương pháp ban đầu:

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

Nếu bạn đang nhầm lẫn bởi các ifvà modulo, bạn chỉ có thể cấu trúc lại chúng đi, với jnhảy trực tiếp từ iđể 2*iđến 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;
}

Để làm cho việc tính toán độ phức tạp trở nên dễ dàng hơn, bạn có thể đưa ra một j2biến trung gian , sao cho mỗi biến vòng lặp được tăng thêm 1 ở mỗi lần lặp:

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

Bạn có thể sử dụng gỡ lỗi hoặc trường học cũ System.out.printlnđể kiểm tra i, j, kbộ ba luôn giống nhau trong mỗi phương thức.

Biểu thức dạng đóng

Như đã đề cập bởi những người khác, bạn có thể sử dụng thực tế là tổng của các n số nguyên đầu tiên bằng n * (n+1) / 2(xem số tam giác ). Nếu bạn sử dụng đơn giản hóa này cho mỗi vòng lặp, bạn sẽ nhận được:

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

Nó rõ ràng không phức tạp như mã gốc nhưng nó trả về cùng các giá trị.

Nếu bạn google các thuật ngữ đầu tiên, bạn có thể nhận thấy 0 0 0 2 11 35 85 175 322 546 870 1320 1925 2717 3731xuất hiện trong "Số khuấy của loại thứ nhất: s (n + 2, n)." , với hai 0s được thêm vào lúc đầu. Nó có nghĩa sumsố Stirling của loại đầu tiên s(n, n-2) .


0

Chúng ta hãy nhìn vào hai vòng đầu tiên.

Cái đầu tiên rất đơn giản, nó lặp từ 1 đến n. Cái thứ hai thú vị hơn. Nó đi từ 1 đến bình phương. Hãy xem một số ví dụ:

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  

Trong tổng số, i and j loopskết hợp có 1^2 + 2^2 + 3^2.
Có một công thức tính tổng n bình phương đầu tiên n * (n+1) * (2n + 1) / 6, đại khái là O(n^3).

Bạn có một k loopvòng lặp cuối từ 0 đến jnếu và chỉ khi j % i == 0. Kể từ khi jđi từ 1 đến i^2, j % i == 0là đúng cho ithời gian. Kể từ khi i looplặp đi lặp lại n, bạn có thêm một O(n).

Vì vậy, bạn có O(n^3)từ i and j loopsvà khác O(n)từ k looptổng cộngO(n^4)


Tôi biết cách tính độ phức tạp cho tất cả các vòng lặp ngoại trừ lý do tại sao vòng lặp cuối thực hiện lần thứ i dựa trên toán tử mod ... Tôi chỉ không thấy nó như thế nào. Về cơ bản, tại sao j% tôi không thể đi lên i * i chứ không phải tôi?
dùng11452926

1
@ user11452926 giả sử i là 5. j sẽ đi từ 1 đến 25 trong vòng 2. Tuy nhiên, j % i == 0chỉ khi j là 5, 10, 15, 20 và 25. 5 lần, giống như giá trị của i. Nếu bạn viết các số từ 1 đến 25 vào ô vuông 5 x 5, chỉ cột thứ 5 sẽ chứa các số chia hết cho 5. Điều này hoạt động với mọi số i. Vẽ một hình vuông của n bằng n bằng cách sử dụng các số từ 1 đến n ^ 2. Cột thứ n sẽ chứa các số chia hết cho n. Bạn có n hàng, nên n số từ 1 đến n ^ 2 chia hết cho n.
Silviu Burcea

Cảm ơn! có ý nghĩa! Điều gì sẽ xảy ra nếu đó là một con số tùy ý như 24 chứ không phải 25, liệu mẹo vuông vẫn còn hoạt động?
dùng11452926

25 xuất hiện khi iđạt 5, vì vậy các jvòng lặp từ 1 đến 25, bạn không thể chọn một số tùy ý. Nếu vòng lặp thứ 2 của bạn sẽ đi đến một số cố định, ví dụ 24, thay vào đó i * i, đó sẽ là một số không đổi và sẽ không bị ràng buộc n, vì vậy nó sẽ như vậy O(1). Nếu bạn đang suy nghĩ về j < i * iso với j <= i * i, điều đó sẽ không quan trọng lắm, vì sẽ có nn-1hoạt động, nhưng trong ký hiệu Big-oh, cả hai phương tiệnO(n)
Silviu Burcea
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.