Чому обчислювальна складність 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++;
            }
        }
    }
}

Я не розумію, як при j = i, 2i, 3i ... останній forцикл працює n разів. Я думаю, я просто не розумію, як ми дійшли цього висновку на основі ifзаяви.

Редагувати: Я знаю, як обчислити складність для всіх циклів, за винятком того, чому останній цикл виконується i раз на основі мод-оператора ... Я просто не бачу, як це я. В основному, чому j% я не можу перейти до i * i, а не до i?


5
Ви можете зменшити складність цього коду за допомогою декількох великих факторів. Підказка : Сума чисел від 1 до n дорівнює ((n + 1) * n) / 2 Підказка 2 : for (j = i; j < i *i; j += i)тоді тест на модуль вам не потрібен (тому що jвін гарантовано ділиться на i).
Елліот Фріш

1
Функція O () - це функція паркування кулі, тому будь-яка петля в цьому прикладі додає складності. Другий цикл працює до n ^ 2. if-заяви ігноруються.
Крістоф Бауер

11
@ChristophBauer ifзаяви абсолютно ігноруються. Це ifтвердження означає, що складність є O (n ^ 4) замість O (n ^ 5), оскільки вона змушує внутрішню петлю виконувати лише iрази замість i*iразів для кожної ітерації другого циклу.
kaya3

1
@ kaya3 повністю пропустив цю k < n^2частину. Отже, це O (n ^ 5), але знання (розуміючи if) підказує O (n ^ 4).
Крістоф Бауер

1
Якщо це не просто заняття з класу, змініть другу петлю на на (int j = i; j <i * i; j + = i)
Cristobol Polychronopolis

Відповіді:


49

Позначимо петлі A, B і 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++;
            }
        }
    }
}
  • Цикл A повторює O ( n ) разів.
  • Цикл B ітерацію O ( я 2 ) раз в ітерації . Для кожної з цих ітерацій:
    • j % i == 0 оцінюється, що займає О (1) час.
    • На 1 / i з цих ітерацій цикл C повторюється j разів, виконуючи роботу O (1) за ітерацію. Оскільки j в середньому O ( i 2 ), і це робиться лише для 1 / i ітерацій циклу B, середня вартість O ( i 2  /  i ) = O ( i ).

Помноживши все це разом, отримаємо O ( n  ×  i 2  × (1 +  i )) = O ( n  ×  i 3 ). Оскільки i в середньому O ( n ), це O ( n 4 ).


Хитра частина цього говорить про те, що ifумова справджується лише 1 / i часу:

В основному, чому j% я не можу перейти до i * i, а не до i?

Насправді, jдійсно j < i * i, не просто j < i. Але ця умова j % i == 0справедлива, якщо і лише якщо jкратна i.

У кратні iмежах є i, 2*i, 3*i..., (i-1) * i. Їх існує i - 1, тому цикл C досягається i - 1разів, незважаючи на i * i - 1періоди повторення циклу B.


2
В O (n × i ^ 2 × (1 + i)) чому 1 + i?
Солей

3
Тому що ifумова займає час O (1) для кожної ітерації циклу B. Тут переважає цикл C, але я перерахував її вище, тому це просто "показ моєї роботи".
kaya3

16
  • Перший цикл споживає nітерації.
  • Друга петля споживає n*nітерації. Уявіть собі випадок, коли i=nтоді j=n*n.
  • Третій цикл використовує nітерації, оскільки він виконується лише iраз, де в гіршому випадку iобмежений n.

Таким чином, складність коду дорівнює O (n × n × n × n).

Сподіваюся, це допоможе вам зрозуміти.


6

Усі інші відповіді правильні, я просто хочу внести зміни до наступних. Я хотів побачити, чи скорочення виконання внутрішнього k-циклу було достатнім для зменшення фактичної складності нижче. O(n⁴).Тому я написав наступне:

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

Після виконання цього стає очевидним, що складність насправді є n⁴. Останні рядки виводу виглядають приблизно так:

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

Це свідчить про те, що фактична відносна різниця між фактичною n⁴та складністю цього кодового сегмента є асимптотичним фактором щодо значення навколо 0.124...(фактично 0,125). Хоча це не дає нам точного значення, ми можемо зробити наступне:

Складність Час , n⁴/8 ~ f(n)коли fваша функція / метод.

  • Сторінка вікіпедії на нотації Big O в таблицях «Родини Бахмана – Ландауських позначень» ~зазначає, що межа двох операндних сторін визначається рівним. Або:

    f дорівнює g асимптотично

(Я вибрав 363 як виключену верхню межу, тому що n = 362це останнє значення, за яке ми отримуємо розумний результат. Після цього ми перевищуємо пробіл довгого простору і відносне значення стає негативним.)

Користувач kaya3 з'ясував таке:

Асимптотична константа, до речі, рівно 1/8 = 0,125; ось точна формула за допомогою Wolfram Alpha .


5
Звичайно, O (n⁴) * 0,125 = O (n⁴). Помноження часу виконання на позитивний постійний коефіцієнт не змінює асимптотичну складність.
Ільмарі Каронен

Це правда. Однак я намагався відобразити фактичну складність, а не верхню оцінку. Оскільки я не знайшов іншого синтаксису для вираження часової складності, окрім O-позначення, я відмовився від цього. Однак писати це так не на 100%.
TreffnonX

Ви можете використовувати позначення "мало-о", щоб сказати, що складність у часі є n⁴/8 + o(n⁴), але n⁴/8 + O(n³)з великим O в будь-якому випадку можна дати більш жорсткий вираз .
kaya3

@TreffnonX big OH - це математичне тверде поняття. Тож те, що ти робиш, є фундаментально неправильним / безглуздим. Звичайно, ви можете переосмислити математичні поняття, але це велика банка глистів, яку ви відкриваєте тоді. Спосіб визначення його в більш жорсткому контексті - це те, що описано kaya3, ви йдете на порядок "нижче" і визначаєте його таким чином. (Хоча в математиці ти зазвичай використовуєш зворотну відповідь).
paul23

Ви праві. Я знову виправився. Цього разу я використовую асимптотичний ріст до тієї ж межі, що визначена в нотаціях сімейства Бахмана-Ландау на en.wikipedia.org/wiki/Big_O_notation#Little-o_notation . Я сподіваюся, що це тепер математично досить правильно, щоб не підбурювати бунт;)
TreffnonX

2

Видаліть ifі модуль, не змінюючи складності

Ось оригінальний метод:

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

Якщо ви збентежені ifі по модулю, ви можете просто реорганізувати їх, з jстрибаючи безпосередньо від 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;
}

Щоб зробити його простіше обчислити складність, ви можете ввести проміжну j2змінну, так що кожна змінна петля збільшується на 1 при кожній ітерації:

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

Ви можете використовувати налагодження або old-school System.out.println, щоб перевірити, чи i, j, kтриплет завжди однаковий у кожному методі.

Вираз закритої форми

Як зазначають інші, можна використовувати той факт, що сума перших n цілих чисел дорівнює n * (n+1) / 2(див. Трикутні числа ). Якщо ви використовуєте це спрощення для кожного циклу, ви отримуєте:

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

Очевидно, це не така ж складність, як оригінальний код, але він повертає ті самі значення.

Якщо ви перейдете в Google перші терміни, ви можете помітити, що 0 0 0 2 11 35 85 175 322 546 870 1320 1925 2717 3731відображаються у "Стірлінг номерів першого виду: s (n + 2, n)." , з двома 0доданими на початку. Це означає, що sumце номер Стірлінга першого виду s(n, n-2) .


0

Давайте розглянемо перші дві петлі.

Перший простий, це циклічне від 1 до n. Другий цікавіший. Він іде від 1 до я в квадрат. Давайте подивимось кілька прикладів:

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  

Загалом i and j loopsкомбіновані мають 1^2 + 2^2 + 3^2.
Існує формула для суми перших n квадратів n * (n+1) * (2n + 1) / 6, яка є приблизно O(n^3).

У вас є одна остання, k loopяка циклів від 0 до, jякщо і тільки якщо j % i == 0. Оскільки jйде від 1 до i^2, j % i == 0це вірно для iчасів. Оскільки i loopітерації закінчуються n, у вас є одна додаткова O(n).

Так що у вас є O(n^3)від i and j loopsі інший O(n)з k loopза великий ціломуO(n^4)


Я знаю, як обчислити складність для всіх циклів, за винятком того, чому останній цикл виконується i раз на основі оператора мод ... Я просто не бачу, як це я. В основному, чому j% я не можу перейти до i * i, а не до i?
користувач11452926

1
@ user11452926 скажімо, що я був 5. j у другому циклі піде від 1 до 25. Однак j % i == 0лише тоді, коли j дорівнює 5, 10, 15, 20 і 25. 5 разів, як значення i. Якщо ви записуєте числа від 1 до 25 в 5 х 5 квадратних, то лише 5-й стовпець міститиме числа, ділені на 5. Це працює для будь-якого числа i. Намалюйте квадрат n на n, використовуючи числа 1 до n ^ 2. П'ятий стовпець буде містити числа, ділені на n. У вас n рядків, тому n чисел від 1 до n ^ 2 ділиться на n.
Сільвіу Бурча

Дякую! має сенс! Що робити, якщо це буде довільне число на зразок 24, а не 25, квадратний трюк все ще буде працювати?
користувач11452926

25 приходить, коли звертається до i5, тому jциклів від 1 до 25 ви не можете вибрати довільне число. Якщо ваш 2-й цикл переходитиме до фіксованого числа, наприклад 24, а не 24, i * iце було б постійним числом і не було б прив’язане до нього n, так було б O(1). Якщо ви думаєте про j < i * ivs. j <= i * i, це не матиме великого значення, як будуть nі n-1операції, але в нотації Big-Oh, обидва засобиO(n)
Silviu Burcea,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.