Перевод кода в математику
Учитывая (более или менее) формальную операционную семантику, вы можете буквально перевести (псевдо) код алгоритма в математическое выражение, которое дает вам результат, при условии, что вы можете манипулировать выражением в полезной форме. Это хорошо работает для показателей аддитивной стоимости, таких как число сравнений, перестановок, операторов, обращений к памяти, циклов, в которых нуждается некоторая абстрактная машина, и так далее.
Пример: сравнения в 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
, только от его длины . Таким образом, мы можем буквально перевести (вложенные) циклы в (вложенные) суммы; переменная цикла становится переменной суммирования, и диапазон переносится. Мы получили:nfor
Ccmp(n)=∑i=0n−2∑j=0n−i−21=⋯=n(n−1)2=(n2) ,
где - стоимость каждого исполнения строки 5 (которую мы считаем).1
Пример: свопы в Bubblesort
Я обозначу через подпрограмма , которая состоит из линий до и по расходы на выполнение этой подпрограммы (один раз). C i , jPi,ji
j
Ci,j
Теперь предположим, что мы хотим считать свопы , то есть как часто выполняется . Это «базовый блок», то есть подпрограмма, которая всегда выполняется атомарно и имеет некоторую постоянную стоимость (здесь, ). Заключение таких блоков является одним из полезных упрощений, которое мы часто применяем, не задумываясь и не говоря об этом. 1P6,81
С переводом, аналогичным приведенному выше, мы приходим к следующей формуле:
Cswaps(A)=∑i=0n−2∑j=0n−i−2C5,9(A(i,j)) .
( i , j ) P 5 , 9A(i,j) обозначает состояние массива перед -й итерацией .(i,j)P5,9
Обратите внимание, что я использую вместо качестве параметра; мы скоро увидим почему. Я не добавляю и качестве параметров поскольку затраты здесь не зависят от них (то есть в модели однородных затрат ); в общем, они просто могут.AnijC5,9
Очевидно, что затраты на зависят от содержания (значения и , в частности), поэтому мы должны это учитывать. Теперь перед нами стоит задача: как нам «развернуть» ? Ну, мы можем сделать зависимость от содержимого явной:P5,9AA[j]
A[j+1]
C5,9A
C5,9(A(i,j))=C5(A(i,j))+{10,A(i,j)[j]>A(i,j)[j+1],else .
Для любого входного массива эти затраты четко определены, но мы хотим более общее утверждение; нам нужно сделать более сильные предположения. Давайте рассмотрим три типичных случая.
Худший случай
Просто взглянув на сумму и заметив, что , мы можем найти тривиальную верхнюю оценку стоимости:C5,9(A(i,j))∈{0,1}
Cswaps(A)≤∑i=0n−2∑j=0n−i−21=n(n−1)2=(n2) .
Но может ли это произойти , то есть есть ли для этой верхней границы? Как оказалось, да: если мы вводим отсортированный в обратном порядке массив попарно различных элементов, каждая итерация должна выполнять свопинг. Таким образом, мы вывели точное число наихудших вариантов свопов Bubblesort.A
Лучший случай
И наоборот, существует тривиальная нижняя граница:
Cswaps(A)≥∑i=0n−2∑j=0n−i−20=0 .
Это также может произойти: в массиве, который уже отсортирован, Bubblesort не выполняет ни одного обмена.
Средний случай
В худшем и лучшем случае откроется довольно большой разрыв. Но каково типичное количество свопов? Чтобы ответить на этот вопрос, нам нужно определить, что означает «типичный». Теоретически, у нас нет причин предпочитать один вход другому, поэтому мы обычно предполагаем равномерное распределение по всем возможным входам, то есть каждый вход одинаково вероятен. Мы ограничимся массивами с попарно различными элементами и, таким образом, примем модель случайной перестановки .
Затем мы можем переписать наши расходы следующим образом²:
E[Cswaps]=1n!∑A∑i=0n−2∑j=0n−i−2C5,9(A(i,j))
Теперь нам нужно выйти за рамки простых манипуляций с суммами. Рассматривая алгоритм, мы отмечаем, что каждый своп удаляет ровно одну инверсию в (мы всегда обмениваем только соседей3). То есть, число свопов , выполненных на именно число инверсий из . Таким образом, мы можем заменить внутренние две суммы и получитьAAinv(A)A
E[Cswaps]=1n!∑Ainv(A) .
К счастью для нас, среднее число инверсий было определено как
E[Cswaps]=12⋅(n2)
что является нашим конечным результатом. Обратите внимание, что это ровно половина стоимости наихудшего случая.
- Обратите внимание, что алгоритм был тщательно сформулирован, так что «последняя» итерация
i = n-1
внешнего цикла, который ничего не делает, не выполняется.
- « » - это математическое обозначение «ожидаемого значения», которое здесь является просто средним.E
- Таким образом, мы узнаем, что ни один алгоритм, который меняет местами только соседние элементы, не может быть асимптотически быстрее, чем Bubblesort (даже в среднем) - число инверсий является нижней границей для всех таких алгоритмов. Это относится, например , к Вставки Сортировка и выбора Сортировка .
Общий метод
Мы видели в примере, что мы должны перевести управляющую структуру в математику; Я представлю типичный ансамбль правил перевода. Мы также видели, что стоимость любой данной подпрограммы может зависеть от текущего состояния , то есть (приблизительно) текущих значений переменных. Поскольку алгоритм (обычно) изменяет состояние, общий метод немного громоздок для записи. Если вы начинаете чувствовать смущение, я предлагаю вам вернуться к примеру или придумать свой.
Мы обозначаем текущее состояние (представьте его как набор переменных). Когда мы выполняем программу, начинающуюся в состоянии , мы в состоянии (при условии, что завершается).ψP
ψψ/PP
Индивидуальные заявления
Учитывая только одно утверждение S;
, вы назначаете его стоит . Обычно это будет постоянная функция.CS(ψ)
Выражения
Если у вас есть выражение E
формы E1 ∘ E2
(скажем, арифметическое выражение, где ∘
может быть сложение или умножение, вы рекурсивно складываете расходы:
CE(ψ)=c∘+CE1(ψ)+CE2(ψ) .
Обратите внимание, что
- стоимость операции может быть не постоянной, а зависеть от значений и иc∘E1E2
- оценка выражений может изменить состояние на многих языках,
так что вам, возможно, придется проявить гибкость с этим правилом.
Последовательность
Учитывая программу P
как последовательность программ Q;R
, вы добавляете затраты к
CP(ψ)=CQ(ψ)+CR(ψ/Q) .
Conditionals
Учитывая программу P
вида if A then Q else R end
, расходы зависят от состояния:
CP(ψ)=CA(ψ)+{CQ(ψ/A)CR(ψ/A),A evaluates to true under ψ,else
В целом, оценка A
может очень хорошо изменить состояние, отсюда и обновление стоимости отдельных филиалов.
Для петель
Учитывая программу P
формы for x = [x1, ..., xk] do Q end
, назначьте расходы
CP(ψ)=cinit_for+∑i=1kcstep_for+CQ(ψi∘{x:=xi})
где - это состояние до обработки значения , т. е. после итерации с установленным значением , ..., .ψiQ
xi
x
x1
xi-1
Обратите внимание на дополнительные константы для обслуживания цикла; переменная цикла должна быть создана ( ) и присвоена ее значениям ( ). Это актуально сcinit_forcstep_for
- вычисление следующего
xi
может быть дорогостоящим и
- -
for
цикл с пустым телом (например, после упрощения в лучшем случае с определенной стоимостью) не имеет нулевой стоимости, если он выполняет итерации.
В то время как-Loops
Учитывая программу P
формы while A do Q end
, назначьте расходы
CP(ψ) =CA(ψ)+{0CQ(ψ/A)+CP(ψ/A;Q),A evaluates to false under ψ, else
При проверке алгоритма это повторение часто можно представить в виде суммы, аналогичной сумме для циклов for.
Пример: рассмотрим этот короткий алгоритм:
while x > 0 do 1
i += 1 2
x = x/2 3
end 4
Применяя правило, мы получаем
C1,4({i:=i0;x:=x0}) =c<+{0c+=+c/+C1,4({i:=i0+1;x:=⌊x0/2⌋}),x0≤0, else
с некоторыми постоянными затратами для отдельных утверждений. Мы неявно предполагаем, что они не зависят от состояния (значения и ); это может или не может быть правдой в «реальности»: подумайте о переполнении!c…i
x
Теперь мы должны решить эту повторяемость для . Отметим, что ни количество итераций, ни стоимость тела цикла не зависят от значения , поэтому мы можем его отбросить. Мы остались с этим повторением:C1,4i
C1,4(x)={c>c>+c+=+c/+C1,4(⌊x/2⌋),x≤0, else
Это решает с элементарными средствами для
C1,4(ψ)=⌈log2ψ(x)⌉⋅(c>+c+=+c/)+c> ,
повторное введение полного состояния символически; если , то .ψ={…,x:=5,…}ψ(x)=5
Процедурные вызовы
Учитывая программу P
вида M(x)
для некоторого параметра (ов), x
где M
есть процедура с (именованным) параметром p
, назначьте затраты
CP(ψ)=ccall+CM(ψglob∘{p:=x}) .
Еще раз обратите внимание на дополнительную константу (которая на самом деле может зависеть от !). Вызовы процедур дороги из-за того, как они реализованы на реальных машинах, и иногда даже доминируют во время выполнения (например, наивно оценивая повторяемость числа Фибоначчи).ccallψ
Я затушевываю некоторые семантические проблемы, которые у вас могут возникнуть с состоянием здесь. Вы захотите различать глобальное состояние и такие локальные для вызовов процедур. Давайте просто предположим, что мы передаем только глобальное состояние и M
получаем новое локальное состояние, инициализируемое установкой значения p
to x
. Кроме того, это x
может быть выражение, которое мы (обычно) предполагаем оценить перед его передачей.
Пример: рассмотрим процедуру
fac(n) do
if ( n <= 1 ) do 1
return 1 2
else 3
return n * fac(n-1) 4
end 5
end
Согласно правилу (ам), мы получаем:
Cfac({n:=n0})=C1,5({n:=n0})=c≤+{C2({n:=n0})C4({n:=n0}),n0≤1, else=c≤+{creturncreturn+c∗+ccall+Cfac({n:=n0−1}),n0≤1, else
Обратите внимание, что мы игнорируем глобальное состояние, поскольку fac
явно не имеет доступа к любому. Это конкретное повторение легко решить
Cfac(ψ)=ψ(n)⋅(c≤+creturn)+(ψ(n)−1)⋅(c∗+ccall)
Мы рассмотрели языковые возможности, с которыми вы столкнетесь, в типичном псевдокоде. Остерегайтесь скрытых затрат при анализе псевдокода высокого уровня; если сомневаешься, развернись. Запись может показаться громоздкой и, безусловно, является вопросом вкуса; перечисленные понятия нельзя игнорировать. Однако, имея некоторый опыт, вы сможете сразу увидеть, какие части штата имеют значение для какого показателя стоимости, например, «размер проблемы» или «количество вершин». Остальное можно отбросить - это значительно упрощает вещи!
Если вы сейчас думаете , что это слишком сложно, посоветуйте: она есть ! Получение точных затрат на алгоритмы в любой модели, которая настолько близка к реальным машинам, что позволяет делать прогнозы времени выполнения (даже относительные), является трудной задачей. И это даже не учитывая кеширование и другие неприятные эффекты на реальных машинах.
Следовательно, алгоритм анализа часто упрощается до такой степени, что его можно математически отследить. Например, если вам не нужны точные затраты, вы можете преувеличить или недооценить в любой точке (для верхних или нижних границ): уменьшить набор констант, избавиться от условных выражений, упростить суммы и т. Д.
Примечание об асимптотической стоимости
То, что вы обычно найдете в литературе и на веб-сайтах, это «анализ Big-Oh». Подходящим термином является асимптотический анализ, который означает, что вместо получения точных затрат, как мы делали в примерах, вы даете затраты только до постоянного фактора и в пределе (грубо говоря, «для больших »).n
Это (часто) справедливо, поскольку абстрактные операторы в действительности имеют некоторые (как правило, неизвестные) затраты, в зависимости от машины, операционной системы и других факторов, а в коротких периодах выполнения может преобладать операционная система, которая в первую очередь настраивает процесс, и еще много чего. Так или иначе, вы получаете некоторое возмущение.
Вот как асимптотический анализ относится к этому подходу.
Определите доминирующие операции (которые вызывают затраты), то есть операции, которые происходят чаще всего (с учетом постоянных факторов). В примере с Bubblesort одним из возможных вариантов является сравнение в строке 5.
В качестве альтернативы, ограничьте все константы для элементарных операций их максимумом (сверху) соответственно. их минимум (снизу) и выполнить обычный анализ.
- Выполните анализ, используя количество выполнений этой операции в качестве стоимости.
- При упрощении допускайте оценки. Позаботьтесь о том, чтобы оценки допускались только в том случае, если ваша цель - верхняя граница ( ) соответственно. снизу, если вы хотите нижние границы ( ).OΩ
Убедитесь, что вы понимаете значение символов Ландау . Помните, что такие границы существуют для всех трех случаев ; использование не подразумевает анализ наихудшего случая.O
дальнейшее чтение
Есть много других проблем и приемов в анализе алгоритмов. Вот некоторые рекомендуемые чтения.
Есть много вопросов, помеченных алгоритмом анализа вокруг, которые используют методы, подобные этому.