Dlaczego złożoność obliczeniowa 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++;
            }
        }
    }
}

Nie rozumiem, jak kiedy j = i, 2i, 3i ... ostatnia forpętla działa n razy. Chyba po prostu nie rozumiem, jak doszliśmy do tego wniosku na podstawie tego ifoświadczenia.

Edycja: Wiem, jak obliczyć złożoność dla wszystkich pętli, z wyjątkiem tego, dlaczego ostatnia pętla wykonuje czasy i w oparciu o operator mod ... Po prostu nie rozumiem, jak to jest. Zasadniczo, dlaczego j% nie mogę przejść do i * i zamiast ja?


5
Możesz zmniejszyć złożoność tego kodu o wiele dużych czynników. Wskazówka : Suma liczb od 1 do n wynosi ((n + 1) * n) / 2 Wskazówka 2 : for (j = i; j < i *i; j += i)wtedy nie potrzebujesz testu modułu (ponieważ jgwarantuje się podzielność przez i).
— Elliott Frisch

1
Funkcja O () jest funkcją ball-parkowania, więc każda pętla w tym przykładzie zwiększa złożoność. Druga pętla działa do n ^ 2. instrukcje if są ignorowane.
— Christoph Bauer

11
@ChristophBauer ifwypowiedzi są nie całkowicie ignorowane. Ta ifinstrukcja oznacza, że ​​złożoność wynosi O (n ^ 4) zamiast O (n ^ 5), ponieważ powoduje, że wewnętrzna pętla wykonuje tylko iczasy zamiast i*iczasów dla każdej iteracji drugiej pętli.
— kaya3

1
@ kaya3 całkowicie przegapił tę k < n^2część, więc jest to O (n ^ 5), ale wiedza (przez zrozumienie if) sugeruje O (n ^ 4).
— Christoph Bauer,

1
Jeśli nie jest to tylko ćwiczenie klasowe, zmień drugą pętlę na for (int j = i; j <i * i; j + = i)
— Cristobol Polychronopolis

Odpowiedzi:


49

Oznaczmy pętle A, B i 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++;
            }
        }
    }
}
  • Pętla A iteruje O ( n ) razy.
  • Pętla B iteracje O ( : i 2 ) razy od iteracji . Dla każdej z tych iteracji:
    • j % i == 0 jest obliczany, co zajmuje czas O (1).
    • Na 1 / i tych iteracji pętla C iteruje j razy, wykonując pracę O (1) na iterację. Ponieważ j wynosi średnio O ( i 2 ), i jest to wykonywane tylko dla iteracji 1 / i pętli B, średni koszt wynosi O ( i 2  /  i ) = O ( i ).

Mnożąc to wszystko razem, otrzymujemy O ( n  ×  i 2  × (1 +  i )) = O ( n  ×  i 3 ). Ponieważ i wynosi średnio O ( n ), jest to O ( n 4 ).


Problem polega na tym, że ifwarunek jest spełniony tylko 1 / i czasu:

Zasadniczo, dlaczego j% nie mogę przejść do i * i zamiast ja?

W rzeczywistości, jnie idź do j < i * i, nie tylko do j < i. Ale warunek j % i == 0jest spełniony tylko wtedy, gdy jjest wielokrotnością i.

Wielokrotności iw przedziale są i, 2*i, 3*i, ..., (i-1) * i. Są i - 1takie, więc i - 1czasy pętli C są osiągane pomimo i * i - 1czasów iteracji w pętli B.


2
W O (n × i ^ 2 × (1 + i)) dlaczego 1 + i?
— Soleil

3
Ponieważ ifwarunek zajmuje czas O (1) przy każdej iteracji pętli B. Jest tutaj zdominowany przez pętlę C, ale policzyłem ją powyżej, więc po prostu „pokazuje moją pracę”.
— kaya3

16
  • Pierwsza pętla zużywa niteracje.
  • Druga pętla zużywa n*niteracje. Wyobraź sobie wtedy i=n, kiedy j=n*n.
  • Trzecia pętla zużywa niteracje, ponieważ jest wykonywana tylko irazy, gdzie ijest ograniczona nw najgorszym przypadku.

Zatem złożoność kodu wynosi O (n × n × n × n).

Mam nadzieję, że to pomoże ci zrozumieć.


6

Wszystkie pozostałe odpowiedzi są poprawne, chcę tylko zmienić następujące. Chciałem zobaczyć, czy ograniczenie wykonania wewnętrznej pętli k było wystarczające, aby zmniejszyć rzeczywistą złożoność poniżej. O(n⁴).Napisałem więc:

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

Po wykonaniu tego staje się oczywiste, że złożoność jest w rzeczywistości n⁴. Ostatnie wiersze danych wyjściowych wyglądają tak:

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

To pokazuje, że faktyczna względna różnica między faktyczną n⁴a złożonością tego segmentu kodu jest czynnikiem asymptotycznym względem wartości wokół 0.124...(faktycznie 0,125). Chociaż nie daje nam to dokładnej wartości, możemy wywnioskować, co następuje:

Złożoność czasu jest n⁴/8 ~ f(n)tam, gdzie fjest twoja funkcja / metoda.

  • Strona Wikipedii na temat notacji Big O stwierdza w tabelach „Rodzina notacji Bachmanna – Landaua”, że ~granica dwóch stron operandu jest równa. Lub:

    f jest równe g asymptotycznie

(Wybrałem 363 jako wykluczoną górną granicę, ponieważ n = 362jest to ostatnia wartość, dla której otrzymujemy rozsądny wynik. Następnie przekraczamy długą przestrzeń, a wartość względna staje się ujemna.)

Użytkownik kaya3 zorientował się, co następuje:

Nawiasem mówiąc, stała asymptotyczna wynosi dokładnie 1/8 = 0,125; Oto dokładna formuła za pośrednictwem Wolfram Alpha .


5
Oczywiście O (n⁴) * 0,125 = O (n⁴). Pomnożenie środowiska wykonawczego przez dodatni stały współczynnik nie zmienia złożoności asymptotycznej.
— Ilmari Karonen

To prawda. Próbowałem jednak odzwierciedlić rzeczywistą złożoność, a nie górne oszacowanie. Ponieważ nie znalazłem żadnej innej składni do wyrażania złożoności czasowej innej niż notacja O, wróciłem do tego. Jednak pisanie go w ten sposób nie jest w 100% sensowne.
— TreffnonX

Możesz użyć notacji „o-o”, aby powiedzieć, że złożoność czasu jest n⁴/8 + o(n⁴), ale możliwe jest nadanie bardziej surowego wyrażenia za n⁴/8 + O(n³)pomocą dużej litery „ O”.
— kaya3

@TreffnonX big OH to matematyczna solidna koncepcja. Więc to, co robisz, jest z założenia niewłaściwe / bez znaczenia. Oczywiście masz swobodę redefiniowania pojęć matematycznych, ale wtedy otwierasz dużą puszkę robaków. Sposób na zdefiniowanie go w bardziej rygorystycznym kontekście jest taki, jak opisał Kaya3, przechodzisz o „niższą” kolejność i definiujesz w ten sposób. (Chociaż w matematyce zwykle używasz odwrotności).
— paul23

Masz rację. Poprawiłem się ponownie. Tym razem wykorzystuję wzrost asymetryczny w kierunku tego samego limitu, jak zdefiniowano w notacjach Rodziny Bachmanna-Landaua na en.wikipedia.org/wiki/Big_O_notation#Little-o_notation . Mam nadzieję, że jest to teraz poprawne matematycznie, aby nie podżegać do buntu;)
— TreffnonX

2

Usuń ifi modulo bez zmiany złożoności

Oto oryginalna metoda:

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

Jeśli jesteś zdezorientowany przez ifi modulo, możesz po prostu je refaktoryzować, jskacząc bezpośrednio z ido 2*ina 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;
}

Aby jeszcze łatwiej obliczyć złożoność, możesz wprowadzić j2zmienną pośrednią , aby każda zmienna pętli była zwiększana o 1 przy każdej iteracji:

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

Możesz użyć debugowania lub starej szkoły System.out.println, aby sprawdzić, czy i, j, ktryplet jest zawsze taki sam w każdej metodzie.

Forma zamknięta

Jak wspomnieli inni, możesz użyć faktu, że suma pierwszych n liczb całkowitych jest równa n * (n+1) / 2(patrz liczby trójkątne ). Jeśli skorzystasz z tego uproszczenia dla każdej pętli, otrzymasz:

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

Oczywiście nie ma takiej samej złożoności jak oryginalny kod, ale zwraca te same wartości.

Jeśli google pierwsze warunki, można zauważyć, że 0 0 0 2 11 35 85 175 322 546 870 1320 1925 2717 3731pojawiają się w „liczb Stirlinga pierwszego rodzaju: s (n + 2, n).” , z 0dodanymi dwoma s na początku. Oznacza to, że sumjest to liczba Stirlinga pierwszego rodzaju s(n, n-2) .


0

Rzućmy okiem na pierwsze dwie pętle.

Pierwszy jest prosty, zapętla się od 1 do n. Drugi jest bardziej interesujący. Zaczyna się od 1 do kwadratu. Zobaczmy kilka przykładów:

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  

W sumie i and j loopspołączone mają 1^2 + 2^2 + 3^2.
Istnieje wzór na sumę pierwszych n kwadratów n * (n+1) * (2n + 1) / 6, która jest z grubsza O(n^3).

Masz ostatnią, k loopktóra zapętla się od 0 do jwtedy i tylko wtedy j % i == 0. Ponieważ jtrwa od 1 do i^2, j % i == 0jest prawdziwe dla iczasów. Ponieważ i loopiteracja się kończy n, masz jeden dodatkowy O(n).

Więc masz O(n^3)z i and j loopsi inny O(n)z k loopw sumieO(n^4)


Wiem, jak obliczyć złożoność dla wszystkich pętli, z wyjątkiem tego, dlaczego ostatnia pętla wykonuje czasy i w oparciu o operator mod ... Po prostu nie rozumiem, jak to jest. Zasadniczo, dlaczego j% nie mogę przejść do i * i zamiast ja?
— user11452926

1
@ user11452926 powiedzmy, że miałem 5 lat. W drugiej pętli zmieniłbym się z 1 na 25. Jednak j % i == 0tylko, gdy j wynosi 5, 10, 15, 20 i 25. 5 razy, podobnie jak wartość i. Jeśli zapisałbyś liczby od 1 do 25 w kwadracie 5 x 5, tylko piąta kolumna zawierałaby liczby podzielne przez 5. Działa to dla dowolnej liczby i. Narysuj kwadrat n przez n, używając liczb od 1 do n ^ 2. N-ta kolumna będzie zawierać liczby podzielne przez n. Masz n wierszy, więc n liczb od 1 do n ^ 2 dzieli się przez n.
— Silviu Burcea

Dzięki! ma sens! Co się stanie, jeśli będzie to dowolna liczba, np. 24 zamiast 25, czy sztuczka kwadratowa nadal będzie działać?
— user11452926

25 pojawia się, gdy itrafi 5, więc w jpętlach od 1 do 25 nie można wybrać dowolnej liczby. Jeśli twoja druga pętla przejdzie do stałej liczby, np. 24, zamiast i * i, byłaby to stała liczba i nie byłaby przywiązana n, więc tak by było O(1). Jeśli myślisz o j < i * ikontra j <= i * i, nie będzie to miało większego znaczenia, jak będzien i n-1operacje, ale w notacji Big-oh oba oznaczająO(n)
— Silviu Burcea
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.