Амортизационный анализ

в 12:01, , рубрики: data structures, Алгоритмы, метки:

Привет!

Сегодня мы поговорим об амортизационном анализе. Сначала я расскажу, что это такое, и приведу игрушечный пример. А потом расскажу, как его применить для анализа системы непересекающихся множеств.

Амортизационный анализ

Амортизационный анализ применяют для алгоритмов, которые запускаются несколько раз. Время алгоритма может сильно колебаться, но мы оцениваем среднее или суммарное время по всем запускам. Чаще всего алгоритм реализует какую-нибудь операцию над структурой данных.

Само слово «амортизация» пришло из финансов. Оно означает небольшие периодические выплаты с целью погашения займа. В нешем случае оно означает, что быстрые операции своим временем компенсируют затраты на медленные.

Рассмотрим последовательность операций Амортизационный анализ. Каждая операция Амортизационный анализ по факту выполняется за время Амортизационный анализ. Это время назовем фактическим. Для каждой операции Амортизационный анализ мы посчитаем амортизационное время Амортизационный анализ. Мы вводим это время, чтобы было проще оценить Амортизационный анализ. Отсюда вытекают два ограничения: а) формальное — для любого момента Амортизационный анализ выполняется неравенство Амортизационный анализ, и б) неформальное — сумма амортизационных стоимостей должна легко считаться.

Чтобы иметь на глазах конкретный пример, рассмотрим структуру данных, внутри которой лежит стек. Структура данных поддерживает операцию op(m, x), которая выкидывает из стека последние Амортизационный анализ элементов, а затем вставляет элемент Амортизационный анализ.

def op(stack, m, x):
  for i in range(m):
    stack.pop()
  stack.push(x)

Мы будем считать, что аргументы всегда корректны, и от нас никогда не потребуют выкинуть больше, чем есть в стеке. Кроме того, будем считать, что op(m, x) выполняется за Амортизационный анализ шаг без Амортизационный анализ-нотации. Все наши оценки всегда можно домножить на константу, чтобы получить корректные.

Заметим, что время операция op(m, x) может длиться от одного до Амортизационный анализ шага. Грубая оценка даст время Амортизационный анализ на выполнение всех Амортизационный анализ операций.

Амортизационный анализ предлагает три метода оценки.

Метод банкира. Время — деньги

Пусть каждая единица времени — это монета. Каждой операции выдадим Амортизационный анализ монет и разрешим тратить Амортизационный анализ. Все свои монеты операции могут раскладывать по структуре данных. Амортизационная стоимость одной операции составит Амортизационный анализ.

Чтобы поддержать инвариант Амортизационный анализ, мы зададим ограничение: нельзя потратить монет больше, чем их есть на текущий момент: Амортизационный анализ. Поскольку Амортизационный анализ, то Амортизационный анализ.

Для нашего примера мы выдадим каждой операции одну монету (Амортизационный анализ). Положим ее на элемент, который только что добавили в стек. Таким образом, мы оплатили будущий POP этого элемента.

Отметим, что в любой момент времени на каждом элементе стека лежит по монете, т.е. все POP-ы предоплачены.

Чтобы выкинуть Амортизационный анализ элементов мы потратим все монеты, которые лежат на них (Амортизационный анализ). Тогда амортизационная стоимость каждой операции Амортизационный анализ. Осталось заметить, что Амортизационный анализ. Мы получили верхнюю линейную оценку на выполнение Амортизационный анализ операций.

Метод физика. Используй свой потенциал

Любая структура данных может принимать несколько возможных состояний. Пусть Амортизационный анализ — это ее начальное состояние до всех операций, а Амортизационный анализ — состояние после Амортизационный анализ-ой операции. Мы введем функцию потенциала Амортизационный анализ, которая принимает на вход текущее состояние структуры данных и возвращает некоторое неотрицательное число. Потенциал Амортизационный анализ начального состояния должен быть равен нулю. Для простоты мы обозначим Амортизационный анализ через Амортизационный анализ, а Амортизационный анализ через Амортизационный анализ. Амортизационная стоимость Амортизационный анализ.

Инвариант Амортизационный анализ следует из определения потенциала. Действительно, Амортизационный анализ; все Амортизационный анализ, кроме первого и последнего, сокращаются и Амортизационный анализ. Поскольку Амортизационный анализ, то Амортизационный анализ.

Общая идея, найти такую функцию потенциала, которая на дорогих операциях бы сильно падала, компенсируя фактическое время, а на дешевых немного росла. Для нашего примера подойдет число элементов в стеке. После Амортизационный анализ POP-ов и одного PUSHАмортизационный анализ. Тогда амортизационная стоимость Амортизационный анализ, и снова получаем линейную оценку.

Агрегационный метод. Нужно все взять и поделить

Этот метод является обобщением двух предыдущих, и заключается в том, чтобы как-то посчитать Амортизационный анализ и поделить на число запросов.

В нашем случае работает такое рассуждение. Каждая операция op(m, x) выполняет один PUSH и несколько POP-ов. Из стека нельзя выкинуть больше элементов, чем туда положили. Всего в стек положим Амортизационный анализ элементов, выкинем не больше Амортизационный анализ. Значит, суммарное время — Амортизационный анализ. Амортизационная стоимость одной операции Амортизационный анализ.

Система непересекающихся множеств

Нам дали Амортизационный анализ элементов. Элементы можно объединять в множества. Мы хотим сделать структуру данных, которая поддерживает три операции

  • make_set(key) — создает множество из одного элемента,
  • union(A, B) — объединяет множества A и B,
  • find(x) — возвращает множество, в котором лежит элемент Амортизационный анализ.

Структуру данных, которая поддерживает эти операции, называют системой непересекающихся множеств (СНМ) (англ. disjoint-set data structure, union-find data structure или merge-find set).

На хабре уже писали про СНМ. Подробное описание алгоритма и его применения можно прочитать здесь. В этом посте я кратко напомню сам алгоритм и сконцентрирую внимание на анализе.

Алгоритм

Каждое множество мы будем хранить в виде дерева. Узел дерева хранит ссылку на родителя. Если узел — корень, ссылка указывают на None. Кроме того, у каждого узла будет дополнительное поле rank. Мы его обсудим ниже.

class Node:
  def __init__(self, key):
    self.parent = None
    self.rank   = 0
    self.key    = key

Чтобы обращаться с множествами, мы выберем для каждого множества по представителю. В нашем случае, это будет корень дерева. Если для двух элементов представитель совпадает, то они лежат в одном множестве. Чтобы объединить два множества, нужно подвесить одно дерево к корню другого. Получаем незатейливый алгоритм:

def find(x):
  if x.parent == None:
    return x
  return find(x.parent)

def union(x, y):
  x = find(x)
  y = find(y)
  y.parent = x
  return x

Для ускорения алгоритма используют две эвристики.

Ранги. Каждому узлу назначим ранг, изначально равный нулю. Рангом дерева будет ранг его корня. При объединении множеств, будем подвешивать дерево меньшего ранга к дереву большего. Если ранги совпадают, то сначала увеличим ранг одного из деревьев на единицу.

def union(x, y):
  x = find(x)
  y = find(y)
  if x.rank < y.rank:
    x, y = y, x
  if x.rank == y.rank:
    x.rank += 1
  y.parent = x
  return x

Одной только ранговой эвристики хватит, чтобы ускорить работу СНМ до Амортизационный анализ на операцию в худшем случае. Но мы пойдем дальше.

Сжатие путей. Пусть мы запустили find от элемента Амортизационный анализ. Можно заметить, что все вершины на пути от Амортизационный анализ до корня можно подвесить сразу к корню.

Амортизационный анализ

def find(x):
  if x.parent == None:
    return x
  x.parent = find(x.parent)
  return x.parent

Удивительно, но вместе эти две эвристики сводят амортизационное время одной операции почти к константе.

Анализ

В 1973 году Хопкрофт и Ульман показали, что СНМ с двумя эвристиками обрабатывает Амортизационный анализ операций за Амортизационный анализ, где Амортизационный анализ — итеративный логарифм. Позже, в 1975, Тарьян показал, что СНМ работает за Амортизационный анализ, где Амортизационный анализ — обратная функция Аккермана.

Мой план сначала разобрать, что это за хитрые функции, потом доказать простую логарифмическую оценку в худшем случае, а в конце разобрать оценку Хопкрофта и Ульмана. Оценка Тарьяна тоже активно использует амортизационный анализ, но содержит больше технических деталей.

Хитрые функции

Итеративный логарифм — эта функция, обратная степенной башне. Давайте представим себе число вида Амортизационный анализ, где всего двоек будет Амортизационный анализ. Тогда Амортизационный анализ — это минимальное такое Амортизационный анализ, что степенная башня высоты Амортизационный анализ будет больше Амортизационный анализ. Формально

  • Амортизационный анализ для любого Амортизационный анализ;
  • Амортизационный анализ для Амортизационный анализ.

Упражнение читателю, показать, что итеративный логарифм от числа частиц в известной нам части вселенной не превосходит пяти.

Можно подумать, что степенная башня растет быстро, но математикам начала двадцатого века этого было мало, и они придумали функцию Аккермана. Определяется она так:

  • Амортизационный анализ для Амортизационный анализ;
  • Амортизационный анализ для Амортизационный анализ;
  • Амортизационный анализ для Амортизационный анализ.

Эта функция растет очень быстро. При Амортизационный анализ, она является степенной башней. При Амортизационный анализ она тоже является степенной башней, только теперь число двоек в башне — тоже степенная башня и т.д.

Хотите отомстить врагу, попросите его посчитать эту функцию, используя только арифметические операции, if-ы и for-ы (while-ы и рекурсию нельзя). Аккерман доказал, что он провалится.

Обратная функция Аккермана Амортизационный анализ определяется как минимальное Амортизационный анализ такое, что Амортизационный анализ. Несложно понять, что растет она очень медленно.

Оценка в худшем случае

Сначала заметим, что каждый union делается за две операции find и некоторую дополнительную константу времени. Значит, достаточно оценить только find. Оценка в худшем случае следует из двух простых наблюдений.

Наблюдение 1. Дерево ранга Амортизационный анализ содержит не менее Амортизационный анализ узлов.

Это утверждение доказывается по индукции. Для дерева ранга 0, очевидно. Чтобы получить дерево ранга Амортизационный анализ, нужно объединить два дерева ранга Амортизационный анализ, в каждом из которых хотя бы Амортизационный анализ узлов. Значит, в дереве ранга Амортизационный анализ окажется хотя бы Амортизационный анализ узлов.

Как следствие, в любой момент времени узлов ранга Амортизационный анализ будет не более Амортизационный анализ, и максимальный ранг узла Амортизационный анализ.

Замечание 2. Ранг родителя всегда больше ранга ребенка.

Следует из построения. Поскольку на пути от вершины к корню ранг всегда увеличивается, то его длина не превосходит Амортизационный анализ, и любой find работает за Амортизационный анализ.

Оценка Хопкрофта и Ульмана

Докажем, что Амортизационный анализ операций СНМ над Амортизационный анализ элементами можно провести за Амортизационный анализ.

Операция make_set выполняется за Амортизационный анализ. Операция union — за Амортизационный анализ плюс время двух операций find. Значит, достаточно показать, что Амортизационный анализ операций find будут выполняться за Амортизационный анализ.

Время выполнения операции find пропорционально длине пути, который требуется пройти от заданного узла до корня. Поэтому нужно оценивать суммарное число проходов по узлам.

Разобъем все ранги на Амортизационный анализ множеств: Амортизационный анализ. В Амортизационный анализ-ом множестве будут находиться все ранги от степенной башни Амортизационный анализ высоты Амортизационный анализ до башни высоты Амортизационный анализ. Номер множества, в котором лежит ранг узла, назовем уровнем узла.

Будем говорить, что узел — хороший, если он является корнем дерева, непосредственным ребенком корня, или он и его родитель имеют разные уровни. Оставшиеся узлы назовем плохими.

Всего уровней не больше Амортизационный анализ. Значит, путь от любого узла до корня содержит не более Амортизационный анализ хороших узлов. В сумме find пройдет не более Амортизационный анализ хороших узлов.

Оценим число проходов по плохим узлам. Пусть плохой узел Амортизационный анализ имеет ранг Амортизационный анализ, и Амортизационный анализ принадлежит множеству Амортизационный анализ. Заметим, что плохой узел не является корнем, и потому его ранг фиксирован. Если find проходит по узлу Амортизационный анализ, то Амортизационный анализ меняет своего родителя на нового с большим рангом. Не более, чем за Амортизационный анализ проходов по Амортизационный анализ, как по плохому узлу, Амортизационный анализ станет хорошим.

Всего узлов ранга Амортизационный анализ не более Амортизационный анализ. Значит узлов с рангами из множества Амортизационный анализ будет не более Амортизационный анализ. На каждый узел приходится не более Амортизационный анализ проходов, как по плохому узлу. Значит в сумме проходов будет не более Амортизационный анализ. Всего уровней Амортизационный анализ. Значит, проходов по плохим узлам будет не более Амортизационный анализ.

Итого, получили верхнюю оценку Амортизационный анализ на все операции в сумме.

Литература

  • Кормен, Лейзерсон, Ривест «Алгоритмы: построение и анализ»
  • Tarjan «Data Structures and Network Algorithms»
  • Rebecca Fiebrink «Amortized Analysis Explained»

Автор: SeptiM

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js