Алгоритмы

Амортизированный анализ: Худший случай - ложь

Цели урока

  • Понять когда worst-case анализ даёт неточную картину
  • Освоить три метода амортизированного анализа
  • Применить агрегатный метод к ArrayList
  • Знать амортизированные сложности популярных структур

Предварительные знания

  • Big O нотация
  • Space complexity
  • Big O: Язык эффективности
  • Пространственная сложность

Python list, Java ArrayList, C++ vector используются в каждом проекте мира. PyTorch строит на них тензоры. Redis хранит так списки. Nginx держит так очереди соединений. И все три структуры имеют O(n) на вставку в worst-case. Этот парадокс разрешается одним понятием - амортизация.

  • **PyTorch/TensorFlow** - динамические массивы для параметров модели, resize при расширении батча
  • **Redis lists** - O(1) амортизированный append, RPUSH/LPUSH под нагрузкой
  • **Splay trees** - в сетевых маршрутизаторах Cisco для LRU-кеша маршрутов
  • **Union-Find** - Kruskal MST в алгоритмах построения spanning tree в сетях датацентров

Тарьян и рождение амортизации

В 1985 году Роберт Тарьян опубликовал статью «Amortized Computational Complexity» в SIAM Journal on Algebraic and Discrete Methods. Тарьян к тому моменту уже изобрёл Splay Trees (1985, совместно с Sleator) и улучшил Union-Find до почти-O(1). Именно эти структуры потребовали нового инструмента анализа - worst-case был бесполезен, average-case не давал гарантий. Амортизированный анализ стал третьим путём.

Когда worst-case врёт

**Python list, Java ArrayList, C++ vector** - три самые популярные структуры в индустрии. Все три имеют O(n) на вставку в худшем случае. Но PyTorch хранит параметры в таких списках. TensorFlow строит на них вычислительные графы. Ни один production-инженер не считает это проблемой.

Если вставка O(n), то n вставок = O(n²)? Но на практике - **линейно!** Это не трюк. Это математика, которую worst-case прячет.

Операция #РазмерЧто происходитСтоимость
11->2Resize + copy 12
22->4Resize + copy 23
3-44Просто вставка2
54->8Resize + copy 45
6-88Просто вставка3
98->16Resize + copy 89

**Ключевое наблюдение:** дорогие операции (resize) случаются редко. Большинство вставок - дешёвые O(1). Worst-case смотрит на одну операцию. Амортизация смотрит на серию.

После 16 вставок в пустой ArrayList сколько раз произошёл resize (если начальный размер 1, рост x2)?

Идея амортизации

**Амортизированный анализ** - усреднение стоимости операций по серии. Даже если одна операция дорогая, в среднем каждая дешёвая. Adam optimizer в PyTorch обновляет миллиарды параметров. Каждый шаг - O(1). Но периодически происходит gradient clipping - дороже. Суммарная стоимость тренинга считается именно амортизированно.

**Амортизированная сложность != средний случай!** Это гарантированная верхняя граница для последовательности операций. Без вероятностей. Без предположений о входных данных.

**Три метода** амортизированного анализа:

  1. **Aggregate (агрегатный)** - считаем общую стоимость n операций, делим на n
  2. **Accounting (бухгалтерский)** - каждая операция платит фиксированную цену
  3. **Potential (потенциалов)** - вводим функцию потенциала структуры данных

Амортизированная сложность O(1) означает:

Агрегатный метод

**Aggregate method** - самый простой: считаем общую стоимость n операций, делим на n. Именно так считают latency p50/p99 в production - суммируют все запросы, делят. Один timeout не делает систему медленной, если сотни тысяч запросов дешёвые.

**Геометрическая прогрессия:** 1 + 2 + 4 + ... + n = 2n - 1. Это ключ к O(1) амортизации!

Если ArrayList растёт в 2 раза, после 1024 вставок сколько суммарно элементов было скопировано?

Бухгалтерский метод

**Accounting method** - назначаем каждой операции фиксированную «цену», которая может быть больше реальной стоимости. Избыток - «кредит» на будущее. Так работают сервисы с prepaid-моделью: Stripe берёт с каждой транзакции немного больше операционной стоимости, копит на покрытие редких chargeback-ов.

**Инвариант:** накопленный кредит >= 0 в любой момент. Если это выполняется, амортизированная оценка корректна.

В accounting method, если операция «дешевле» чем её амортизированная цена:

Метод потенциалов

**Potential method** - вводим функцию Phi (потенциал) структуры данных. Амортизированная стоимость = реальная + delta_Phi. Потенциал - как «накопленная энергия»: система заряжается на дешёвых операциях и разряжается на дорогих.

**Потенциал - это формализация кредита** из accounting method. Phi >= 0 гарантирует, что суммарная амортизированная стоимость >= реальной.

Потенциал структуры данных должен быть:

Примеры из реального мира

Амортизированный анализ - везде, где структура платит за будущее сейчас:

СтруктураОперацияWorst-caseАмортизированная
ArrayListappendO(n)O(1)
Hash TableinsertO(n)O(1)
Splay Treefind/insertO(n)O(log n)
Union-Findunion/findO(log n)O(alpha(n)) ~= O(1)
Fibonacci Heapdecrease-keyO(n)O(1)
Deque (two stacks)pop_frontO(n)O(1)

**Union-Find** с path compression и union by rank имеет амортизированную сложность O(alpha(n)), где alpha - обратная функция Аккермана. Для всех практических n, alpha(n) <= 4.

**Осторожно:** амортизированный анализ не гарантирует, что КАЖДАЯ операция быстрая. Для real-time систем (игры, торговые платформы) отдельные O(n) операции могут быть неприемлемы - один resize во время обработки ордера стоит деньги.

Когда амортизированный анализ НЕ подходит?

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

  • Амортизация = средняя стоимость по n операциям, не средний случай
  • Aggregate: считаем сумму, делим на n
  • Accounting: платим фиксированную цену, копим кредит
  • Potential: Phi структуры, амортизация = реальная + delta_Phi
  • ArrayList append: O(1) амортизированно, несмотря на O(n) resize
  • Real-time системы требуют worst-case гарантий - амортизация не подходит

Где это пригодится

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

  • Union-Find — O(alpha(n)) амортизация
  • Хеш-таблицы — O(1) амортизированный insert
  • Оптимизация — Амортизированный анализ как инструмент оценки алгоритмов

Вопросы для размышления

  • Ты разрабатываешь очередь задач для торговой платформы: каждый ордер должен быть обработан не дольше чем за 1 миллисекунду. Коллега предлагает использовать ArrayList с амортизированным O(1) append. Ты возражаешь. Что именно не так с этим решением - и какую структуру данных и почему предложишь вместо него?

Связанные уроки

  • alg-01-big-o — Big O - язык, на котором читается амортизация
  • ds-06-hash-intro — Hash table resize - классика амортизации O(1)
  • alg-02-space — Space-time tradeoff: resize платит памятью за скорость
  • alg-21-dp — DP и амортизация - оба считают итоговую стоимость, не локальную
  • par-01 — Параллельные структуры меняют амортизацию: resize под локом дорог
  • alg-04-basic-sorts — Timsort использует амортизированные run-слияния
  • opt-01 — Оптимизация алгоритмов строится на понимании амортизированной стоимости
  • ds-14-heaps-intro
Амортизированный анализ: Худший случай - ложь

0

1

Войти