Чи існує система, що стоїть за магією аналізу алгоритму?


159

Є багато запитань щодо того, як проаналізувати час виконання алгоритмів (див., Наприклад, та ). Багато з них схожі, наприклад, ті, хто запитує аналіз витрат вкладених циклів або алгоритмів ділення та підкорення, але, здається, більшість відповідей зроблені спеціально.

З іншого боку, відповіді на інше загальне запитання пояснюють більшу картину (зокрема, стосовно асимптотичного аналізу) на деяких прикладах, але не про те, як забруднити руки.

Чи існує структурований загальний метод аналізу вартості алгоритмів? Вартість може бути часом роботи (складністю часу) або іншим показником витрат, таким як кількість виконаних порівнянь, складність простору чи щось інше.

Це має стати еталонним питанням, яке можна використовувати для вказівки на початківців; отже, її більш широка, ніж зазвичай, сфера застосування. Будь ласка, не забудьте дати загальні, дидактично представлені відповіді, які проілюстровані хоча б одним прикладом, але, тим не менш, охоплюють багато ситуацій. Дякую!


3
Дякую авторам ( StackEdit) за те, що вони зручно писати такі довгі повідомлення, а моїм бета-читачам FrankW , Juho , Gilles та Sebastian за те, що вони допомогли мені виправити ряд недоліків, які мали попередні чернетки.
Рафаель

1
Гей, @Raphael, це чудові речі. Я думав, що запропонував би скласти це у форматі PDF для поширення? Такі речі можуть стати справді корисними орієнтирами.
hadsed

1
@hadsed: Дякую, я радий, що тобі це корисно! Поки що я вважаю за краще, щоб посилання на цю посаду було розповсюджено навколо. Однак вміст користувача SE "ліцензується під cc by-sa 3.0 з необхідною атрибуцією" (див. Колонтитул сторінки), тому кожен може створити з нього PDF-файл, доки буде надано атрибуцію.
Рафаель

2
Я не особливо компетентний щодо цього, але чи нормально, що в жодному анвері немає посилання на теорему Майстра ?
babou

1
@babou Я не знаю, що тут означає "нормальний". З моєї точки зору, теорема Мастера тут не має жодної справи: мова йде про аналіз алгоритмів, магістерська теорема - дуже специфічний інструмент для вирішення (деяких) рецидивів (і дуже приблизно в цьому). Оскільки математика була розглянута в інших місцях (наприклад, тут ), я вирішив охопити лише частину від алгоритму до математики. Я даю посилання на пости, які стосуються роботи з математикою у своїй відповіді.
Рафаель

Відповіді:


134

Переклад коду на математику

Враховуючи (більш-менш) формальну оперативну семантику, ви можете перевести алгоритм (псевдо-) код досить буквально в математичний вираз, який дає результат, за умови, що ви можете маніпулювати виразом у корисній формі. Це добре підходить для додаткових заходів витрат, таких як кількість порівнянь, свопів, операторів, доступу до пам'яті, циклів деяких абстрактних машинних потреб тощо.

Приклад: Порівняння в Bubblesort

Розглянемо цей алгоритм, який сортує заданий масив A:

 bubblesort(A) do                   1
  n = A.length;                     2
  for ( i = 0 to n-2 ) do           3
    for ( j = 0 to n-i-2 ) do       4
      if ( A[j] > A[j+1] ) then     5
        tmp    = A[j];              6
        A[j]   = A[j+1];            7
        A[j+1] = tmp;               8
      end                           9
    end                             10
  end                               11
end                                 12

Скажімо, ми хочемо провести звичайний аналіз алгоритму сортування, тобто підрахувати кількість порівнянь елементів (рядок 5). Відразу зазначимо, що ця величина не залежить від вмісту масиву A, лише від його довжини . Таким чином, ми можемо перекласти (вкладені) -крутки буквально буквально на (вкладені) суми; змінна циклу стає змінною підсумовування і діапазон переноситься. Ми отримуємо:нfor

,Сcmp(н)=i=0н-2j=0н-i-21==н(н-1)2=(н2)

де - вартість кожного виконання рядка 5 (який ми рахуємо).1

Приклад: Обміни в Bubblesort

Я позначу через підпрограму, що складається з рядків до та C i , j витрати на виконання цієї підпрограми (один раз).Пi,jijСi,j

Тепер скажімо, що ми хочемо порахувати свопи , тобто часто виконується . Це "базовий блок", тобто підпрограма, яка завжди виконується атомно і має деяку постійну вартість (тут, 1 ). Укладання таких блоків - це одне корисне спрощення, яке ми часто застосовуємо, не задумуючись і не розмовляючи про це.П6,81

З аналогічним перекладом, як описано вище, ми приходимо до наступної формули:

.Ссвопи(А)=i=0н-2j=0н-i-2С5,9(А(i,j))

позначає стан масиву перед ( i , j ) -ю ітерацією P 5 , 9 .А(i,j)(i,j)П5,9

Зауважте, що я використовую замість n як параметр; ми скоро зрозуміємо, чому. Я не додаю i і j як параметри C 5 , 9, оскільки витрати тут не залежать від них (в єдиній моделі витрат , тобто); загалом, вони просто можуть.АнijС5,9

Зрозуміло, що витрати на залежать від змісту A (значення та , зокрема), тому ми маємо це враховувати. Зараз перед нами стоїть виклик: як ми «розмотуємо» C 5 , 9 ? Що ж, ми можемо зробити залежність від змісту A явною:П5,9АA[j]A[j+1]С5,9А

.С5,9(А(i,j))=С5(А(i,j))+{1,А(i,j)[j]>А(i,j)[j+1]0,ще

Для будь-якого даного вхідного масиву ці витрати чітко визначені, але ми хочемо більш загальне твердження; нам потрібно зробити сильніші припущення. Давайте дослідимо три типові випадки.

  1. Найгірший випадок

    Тільки переглянувши суму і зазначивши, що , ми можемо знайти тривіальну верхню межу вартості:C5,9(A(i,j)){0,1}

    .Cswaps(A)i=0n2j=0ni21=n(n1)2=(n2)

    Але це може статися , тобто чи досягнуто для цієї верхньої межі? Як виявляється, так: якщо ми вводимо зворотно відсортований масив попарно відмінних елементів, кожна ітерація повинна здійснювати своп¹. Таким чином, ми вивели точну найгірше число свопів BubbleSort.А

  2. Найкращий випадок

    І навпаки, існує тривіальна нижня межа:

    .Ссвопи(А)i=0н-2j=0н-i-20=0

    Це також може статися: на масиві, який вже відсортований, Bubblesort не виконує жодного свопу.

  3. Середній випадок

    Найгірший і найкращий випадок відкриває досить прогалину. Але яка типова кількість свопів? Для того, щоб відповісти на це запитання, нам потрібно визначити, що означає «типовий». Теоретично у нас немає причин віддавати перевагу одному входу над іншим, тому ми зазвичай припускаємо рівномірний розподіл на всіх можливих входах, тобто кожен вхід є однаково вірогідним. Обмежимось масивами з парно розрізненими елементами і, таким чином, припустимо модель випадкової перестановки .

    Тоді ми можемо переписати наші витрати так:

    Е[Ссвопи]=1н!Аi=0н-2j=0н-i-2С5,9(А(i,j))

    Тепер нам належить вийти за рамки простого маніпулювання сумами. Дивлячись на алгоритм, ми зазначимо, що кожен своп видаляє рівно одну інверсію в (ми лише коли-небудь міняємо сусіди³). Тобто, число свопів , виконаних на А саме число інверсій INV ( A ) від . Таким чином, ми можемо замінити внутрішні дві суми і отриматиААінв(А)А

    .Е[Ссвопи]=1н!Аінв(А)

    Пощастило для нас, середня кількість інверсій визначена

    E[Cсвопи]=12(н2)

    який наш кінцевий результат. Зауважте, що це рівно половина найгірших витрат.


  1. Зауважимо, що алгоритм був ретельно сформульований так, що "остання" ітерація i = n-1зовнішнього циклу, який ніколи нічого не робить, не виконується.
  2. " " - математичне позначення для "очікуваного значення", яке тут просто середнє.Е
  3. Ми довідаємось, що жоден алгоритм, який замінює лише сусідні елементи, не може бути асимптотично швидшим, ніж Bubblesort (навіть у середньому) - кількість інверсій є нижньою межею для всіх таких алгоритмів. Це стосується, наприклад, сортування вставки та сортування вибору .

Загальний метод

На прикладі ми бачили, що нам потрібно перевести керуючу структуру в математику; Я представлю типовий ансамбль правил перекладу. Ми також бачили, що вартість будь-якої заданої підпрограми може залежати від поточного стану , тобто (приблизно) від поточних значень змінних. Оскільки алгоритм (як правило) модифікує стан, загальний метод трохи громіздкий для нотації. Якщо ви починаєте розгублено, пропоную вам повернутися до прикладу або скласти свій власний.

Позначимо з поточний стан (уявимо це як набір змінних призначень). Коли ми виконуємо програму, починаючи з стану ψ , ми закінчуємо стан ψ / P (за умови припинення).ψPψψ/ПP

  • Індивідуальні заяви

    Враховуючи лише одне твердження S;, ви присвоюєте йому вартість . Зазвичай це буде постійною функцією.СS(ψ)

  • Вирази

    Якщо у вас є вираз Eформи E1 ∘ E2(скажімо, арифметичний вираз, де може бути додавання чи множення, ви складаєте витрати рекурсивно:

    .СЕ(ψ)=c+СЕ1(ψ)+СЕ2(ψ)

    Зауважте, що

    • експлуатаційні витрати можуть бути не постійними, але залежать від значень E 1 і E 2 іcЕ1Е2
    • оцінка виразів може змінити стан багатьох мов,

    тому вам, можливо, доведеться бути гнучкими з цим правилом.

  • Послідовність

    Давши програму Pяк послідовність програм Q;R, ви додаєте витрати до

    .СП(ψ)=СQ(ψ)+СR(ψ/Q)

  • Умовні умови

    Враховуючи програму Pформи if A then Q else R end, витрати залежать від держави:

    СП(ψ)=СА(ψ)+{СQ(ψ/А),А оцінює до істинного під ψСR(ψ/А),ще

    Загалом, оцінка Aможе дуже змінити стан, отже, оновлення витрат окремих галузей.

  • For-Loops

    Враховуючи програму Pформи for x = [x1, ..., xk] do Q end, призначте витрати

    СП(ψ)=cinit_for+i=1кcstep_for+СQ(ψi{х: =хi})

    ψiQxixx1xi-1

    cinit_forcstep_for

    • обчислення наступних xiможе бути дорогим і
    • a for-loop з порожнім корпусом (наприклад, після спрощення в найкращому випадку з конкретною вартістю) не має нульової вартості, якщо він виконує ітерації.
  • Хоча-петлі

    Враховуючи програму Pформи while A do Q end, призначте витрати

    СП(ψ) =СА(ψ)+{0,А оцінює до хибного під ψСQ(ψ/А)+СП(ψ/А;Q), ще

    Перевіряючи алгоритм, ця повторюваність часто може бути представлена ​​красиво як сума, подібна до тієї для for-циклів.

    Приклад: Розглянемо цей короткий алгоритм:

    while x > 0 do    1
      i += 1          2
      x = x/2         3
    end               4
    

    Застосовуючи правило, ми отримуємо

    С1,4({i: =i0;х: =х0}) =c<+{0,х00c+=+c/+С1,4({i: =i0+1;х: =х0/2}), ще

    cix

    С1,4i

    С1,4(х)={c>,х0c>+c+=+c/+С1,4(х/2), ще

    Це вирішується елементарними засобами до

    С1,4(ψ)=журнал2ψ(х)(c>+c+=+c/)+c>

    ψ={,х: =5,}ψ(х)=5

  • Процедурні дзвінки

    З огляду на програму Pформи M(x)для деяких параметрів, xде Mце процедура з (іменованим) параметром p, призначте витрати

    СП(ψ)=cдзвінок+СМ(ψглобус{p: =х})

    cдзвінокψ

    Я розглядаю деякі смислові питання, які у вас можуть виникнути з державою тут. Ви хочете розрізнити глобальний стан та такі локальні для процедури дзвінки Давайте припустимо , що ми тільки пройти глобальнестан тут і Mотримують нове локальне стан, ініційовані, встановивши значення pдля x. Крім того, це xможе бути вираз, який ми (як правило) вважаємо оціненим перед його передачею.

    Приклад: Розглянемо процедуру

    fac(n) do                  
      if ( n <= 1 ) do         1
        return 1               2
      else                     3
        return n * fac(n-1)    4
      end                      5
    end                        
    

    Згідно з правилами, ми отримуємо:

    Сфак({н: =н0})=С1,5({н: =н0})=c+{С2({н: =н0}),н01С4({н: =н0}), ще=c+{cповернення,н01cповернення+c+cдзвінок+Сфак({н: =н0-1}), ще

    Зауважте, що ми нехтуємо глобальною державою, оскільки facявно не отримуємо жодного доступу. Цей конкретний повтор легко вирішити

    Сфак(ψ)=ψ(н)(c+cповернення)+(ψ(н)-1)(c+cдзвінок)

Ми розглянули мовні особливості, з якими ви зіткнетеся в типовому псевдокоді. Остерігайтеся прихованих витрат при аналізі псевдокоду високого рівня; якщо сумніваєтесь, розгортайте. Позначення можуть здатися громіздкими і, безумовно, питання смаку; перелічені поняття, однак, не можна ігнорувати. Однак, маючи певний досвід, ви зможете відразу побачити, які частини держави є релевантними, для якої міри витрат, наприклад, "розмір проблеми" чи "кількість вершин". Решту можна скинути - це значно спрощує речі!

Якщо ви зараз думаєте , що це занадто складно, порадьте: вона є ! Отримати точні витрати на алгоритми в будь-якій моделі, настільки близькій до реальних машин, що дозволяє передбачити час виконання (навіть відносні) - важке починання. І це навіть не враховуючи кешування та інші неприємні ефекти на реальних машинах.

Тому аналіз алгоритмів часто спрощується до того, що він може бути відстежений математично. Наприклад, якщо вам не потрібні точні витрати, ви можете завищувати або недооцінювати в будь-якій точці (для верхньої відповідно нижньої межі): зменшуйте набір констант, позбавляйтесь від умовних умов, спрощуйте суми тощо.

Примітка про асимптотичну вартість

н

Це (найчастіше) справедливо, оскільки абстрактні висловлювання фактично мають деякі (як правило, невідомі) витрати, залежно від машини, операційної системи та інших факторів, і короткий час виконання може домінувати в операційній системі, яка налаштовує процес в першу чергу і багато чого іншого. Таким чином, ви все одно отримаєте деяке збурення.

Ось як асимптотичний аналіз стосується цього підходу.

  1. Визначте домінуючі операції (що спричиняють витрати), тобто операції, які відбуваються найчастіше (аж до постійних факторів). У прикладі Bubblesort одним із можливих варіантів є порівняння у рядку 5.

    Крім того, зв'язати всі константи для елементарних операцій їх максимальним (зверху) відповідним. їх мінімум (знизу) та виконують звичайний аналіз.

  2. Виконайте аналіз, використовуючи кількість операцій цієї операції як вартість.
  3. ОΩ

О

Подальше читання

В аналізі алгоритму є ще багато проблем і хитрощів. Ось кілька рекомендованих для читання.

Існує багато питань, позначених використанням тегів подібних до цього.


1
можливо, деякі посилання та приклади до основної теореми (та її розширень ) для асимптотичного аналізу
Нікос М.

@NikosM Тут поза сферою (див. Також коментарі до питання вище). Зауважимо, що я посилаюсь на наш довідковий пост про розв’язання рецидивів, який справді представляють теоретики магістра та ін.
Рафаель

@Nikos M: мій $ 0,02: хоча головна теорема працює для декількох повторень, вона не буде для багатьох інших; існують стандартні методи вирішення рецидивів. Є алгоритми, для яких у нас навіть не буде повторень, що дають час роботи; можуть знадобитися деякі передові методи підрахунку. Для тих, хто має гарний математичний досвід, я пропоную чудову книгу Седжевіка та Флайолета «Аналіз алгоритмів», в якій є такі розділи, як «відносини рецидиву», «генеруючі функції» та «асимптотичні наближення». Структури даних відображаються як випадкові приклади, а увага зосереджена на методах!
Джей

@Raphael Я не можу знайти будь-яку згадку в Інтернеті для цього методу "Переклад коду на математику" на основі оперативної семантики. Чи можете ви надати будь-яку посилання на книгу, папір або статтю, яка стосується цього більш формально ?. Або у випадку, якщо це було розроблено вами, чи є у вас щось більш глибоке?
Wyvern666

1
@ Wyvern666 На жаль, ні. Я склав це сам, наскільки хтось може стверджувати, що складається щось подібне. Можливо, я колись напишу цитовану роботу. Це означає, що весь корпус роботи з аналітичної комбінаторики (Флайолет, Седжвік та багато інших) є основою цього. Вони не турбуються з формальною семантикою «коду» більшу частину часу, але надають математику для вирішення додаткових витрат «алгоритмів» взагалі. Я чесно вважаю, що поняття, викладені тут, не дуже глибокі - хоча математика, до якої можна потрапити, є.
Рафаель

29

Кількість виписок про виконання

Існує ще один метод, який підтримує Дональд Е. Кнут у серії "Мистецтво комп'ютерного програмування ". На відміну від перекладу всього алгоритму в одну формулу , він працює незалежно від семантики коду на стороні "складання речей" і дозволяє перейти на нижчий рівень лише в разі необхідності, починаючи з погляду "орлиного ока". Кожне твердження можна аналізувати незалежно від решти, що веде до більш чітких розрахунків. Однак техніка добре піддається досить детальному коду, не так сильно псевдокоду вищого рівня.

Метод

У принципі це досить просто:

  1. Призначте кожному оператору ім'я / номер.
  2. SiСi
  3. Siеi
  4. Обчисліть загальні витрати

    С=iеiСi

Ви можете вставити оцінки та / або символічні величини в будь-яку точку, послаблюючи відповідність. узагальнення результату відповідно.

е77О(нжурналн)

Приклад: Перший глибинний пошук

Розглянемо наступний алгоритм переходу графіків:

dfs(G, s) do
  // assert G.nodes contains s
  visited = new Array[G.nodes.size]     1
  dfs_h(G, s, visited)                  2
end 

dfs_h(G, s, visited) do
  foo(s)                                3
  visited[s] = true                     4

  v = G.neighbours(s)                   5
  while ( v != nil ) do                 6
    if ( !visited[v] ) then             7
      dfs_h(G, v, visited)              8
    end
    v = v.next                          9
  end
end

{0,,н-1}м

АБСеi

i123456789еiААББББ+ССБ-1С

Зокрема, е8=е3-1foodfsе6=е5+е7while

А=1fooБ=нС=2м

i123456789еi11ннн2м+н2мн-12м

Це призводить нас до загальних витрат точно

С(н,м)=(С1+С2-С8)+ н(С3+С4+С5+С6+С8)+ 2м(С6+С7+С9).

Сi

i123456789Сiн00110101

і дістати

Спам(н,м)=3н+4м

Подальше читання

Дивіться внизу моєї іншої відповіді .


8

Аналіз алгоритмів, як і доказ теореми, є значною мірою мистецтвом (наприклад, є прості програми (наприклад, проблема Колатца ), які ми не знаємо, як проаналізувати). Ми можемо перетворити задачу складності алгоритму в математичну, на яку всебічно відповів Рафаель , але тоді для того, щоб виразити обмеженість вартості алгоритму з точки зору відомих функцій, ми залишаємо:

  1. Використовуйте методи, які ми знаємо з існуючих аналізів, таких як пошук меж на основі повторень, які ми розуміємо, або суми / інтегралів, які ми можемо обчислити.
  2. Змініть алгоритм на щось, що ми знаємо, як проаналізувати.
  3. Придумайте абсолютно новий підхід.

1
Напевно, я не бачу, як це додає нічого корисного та нового, як і вище інших відповідей. Методи вже описані в інших відповідях. Це для мене більше схоже на коментар, ніж на відповідь на запитання.
DW

1
Смію сказати, що інші відповіді доводять, що це не мистецтво. Ви, можливо, не зможете це зробити (тобто математика), і деяка креативність (щодо того, як застосовувати відому математику) може знадобитися, навіть якщо ви є, але це справедливо для будь-якого завдання. Я припускаю, що ми не прагнемо створити тут нову математику. (Насправді це питання, відповідно, його відповіді мали на меті демістифікувати весь процес.)
Рафаель

4
@Raphael Ari розповідає про створення розпізнаваної функції як зв'язаної, а не про "кількість вказівок, виконаних програмою" (саме на це і відповідає ваша відповідь). Загальний випадок - це мистецтво - не існує алгоритму, який би міг придумати нетривіальну межу для всіх алгоритмів. Однак загальним випадком є ​​сукупність відомих методик (наприклад, головна теорема).
Жиль

@Gilles Якби все, для чого не існує алгоритму, було мистецтвом, майстри (зокрема програмісти) платили б гірше.
Рафаель

1
@AriTrachlenberg робить важливий момент, проте існує безліч способів оцінки складності алгоритму в часі. Визначення великих O позначень самі натякають або безпосередньо констатують їх теоретичну природу залежно від автора. "Найгірший сценарій" очевидно залишає відкритим місце для здогадок та чи нових фактів серед будь-яких людей, які надають обговорення в кімнаті. Не кажучи вже про саму природу асимптотичних оцінок, що є чимось ... цілком невиправданим.
Брайан Огден
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.