- •Предисловие
- •Введение
- •Алгоритмы и их сложность
- •Примеры задач и алгоритмов
- •Задачи на графах: «Коммивояжер», «Кратчайшие пути», «Остовные деревья»
- •Приближенные алгоритмы: «Составление расписаний»
- •«Сортировка слиянием»
- •«Быстрая сортировка»
- •«RAM»: машины с произвольным доступом
- •Формально об алгоритмах. Несложно о сложности
- •Сложность в худшем случае
- •Сложность в среднем
- •Полиномиальные алгоритмы
- •Полиномиальность и эффективность
- •Аппроксимация с гарантированной точностью
- •Алгоритмы с оценками точности
- •Жадные алгоритмы для «Покрытия множеств»
- •Приближенные алгоритмы для «Вершинного покрытия»
- •Жадный алгоритм для «Рюкзака»
- •Алгоритм Кристофидеса
- •Аппроксимация с заданной точностью
- •«Рюкзак»: динамическое программирование
- •Полностью полиномиальная приближенная схема для «Рюкзака»
- •Вероятностный анализ детерминированных алгоритмов
- •Сложность и полиномиальность в среднем
- •Задача упаковки
- •Выполнимость КНФ
- •Точность алгоритма для почти всех входов
- •«Рюкзак»: полиномиальность в среднем
- •Вероятностные алгоритмы и их анализ
- •Вероятностная проверка тождеств
- •Максимальное по включению независимое множество в графе
- •Протокол византийского соглашения
- •Вероятностное округление
- •Максимальный разрез в графе
- •Методы дерандомизации
- •Метод условных вероятностей
- •Метод малых вероятностных пространств
- •Полиномиальная проверка простоты
- •Основы теории сложности вычислений
- •Сложность вычислений
- •Машины Тьюринга и вычислимость
- •Сводимость по Куку
- •Недетерминированные алгоритмы
- •Сводимость по Карпу
- •Вероятностные вычисления
- •Вероятностно проверяемые доказательства
- •Схемы и схемная сложность
- •Коммуникационная сложность
- •Диаграмма классов сложности
- •Приложения
- •Введение в Python
- •Глоссарий
- •Предметный указатель
- •Список алгоритмов
Глава 1
Алгоритмы и их сложность
1.1Примеры задач и алгоритмов
1.1.1Теоретико-числовые задачи: «НОД», «факториал», «возведение в степень», «дискретный логарифм»
Мы начнем рассмотрение парадигмы сложности с простой задачи о натуральных числах.
Задача 1. Даны три натуральных числа x, n, и m. Требуется вычислить y = xn mod m.
Легко придумать следующий тривиальный алгоритм (см. алгоритм 1), который в цикле от 1 до n выполняет следующее действие: y y x mod m.
Очевидный анализ показывает, что сложность этого алгоритма есть величина O(n). Пока, на неформальном уровне, под сложностью мы будем понимать число элементарных операций.
15
16 |
Глава 1. АЛГОРИТМЫ И ИХ СЛОЖНОСТЬ |
Алгоритм 1 Тривиальное вычисление y = xn mod m
def mod_exp(x, n, m): print x, n, m
y = 1
for i in range(n): y = y*x % m print y
return y
13 16 47
13
28
35
32
40
3
39
37
11
2
26
9
23
17
33
6
y = 13^16 (mod 47) = 6
Рассмотрим двоичное разложение n = ∑ki=0 ai2i, ai 2 f0; 1g. Заметим, что
xn = x∑i=0 ai2i |
k |
∏ x2i : |
= ∏ xai2i = |
||
k |
|
|
i=0 fi:ai>0g
т. е. достаточно провести l (log 2n + 1) итераций по двоичному разложению n, чтобы на каждой i-й итерации, в зависимости от i-го бита в разложении, проводить домножение результата на сомножитель x2i , который легко получить из сомножителя предыдущей итерации (ключевое соотношение):
x2i (x2(i 1) )2 :
1.1. ПРИМЕРЫ ЗАДАЧ И АЛГОРИТМОВ |
17 |
Значит, можно существенно улучшить алгоритм 1, использовав эти соображения (см. алгоритм 2). Анализ же алгоритма 2 показывает, что его сложность есть O(log n), т. е. разница в сложности первоначального прямолинейного алгоритма и его модификации, основанной на быстром возведении в степень, экспоненциальна!
Алгоритм 2 Разумное вычисление y = xn mod m
def mod_exp(x, n, m): |
13 16 47 |
||
print x, n, m |
28 8 |
1 |
|
y = 1 |
|
||
|
32 4 |
1 |
|
X = x |
|
||
|
37 2 |
1 |
|
N = n |
|
||
while N > 0: |
6 1 1 |
|
|
if N % 2 == 1: |
36 0 |
6 |
|
X |
y = y * X % m |
y = 13^16 (mod 47) = 6 |
|
= X * X % m |
|
|
|
N |
= N / 2 |
|
|
print X, N, y |
|
|
|
return y |
|
|
|
|
|
|
|
Заметим, что процедура расчета модульной экспоненты весьма распространена в различных сетевых протоколах, например, при установке защищенного интернет-соединения (всякий раз, когда в браузере адрес начинается с https://, при установке соединения работают алгоритмы криптографии, и вычисляется модульная экспонента). Если взять длину показателя экспоненты 128 бит, то получается простое сравнение количества умножений в обоих алгоритмах:
18 |
|
|
Глава 1. АЛГОРИТМЫ И ИХ СЛОЖНОСТЬ |
|
|
|
|
|
|
|
Длина показателя (бит) |
Умножений в алг. 1 |
Умножений в алг. 2 |
|
|
56 |
256 7:2 1016 |
100 |
|
|
128 |
2128 3:4 1038 |
130 |
|
Комментарии излишни. |
|
|
|
|
Упражнение 1.1.1. Более внимательно взглянув на алгоритм быстрого возведения в степень, видно, что достаточно 2(log2 n + 1) умножений. На самом деле, как видно из изложенного, для чисел вида n = 2k достаточно k = log 2n + 1 умножений. Можно ли получить такую же асимптотическую оценку log 2n для чисел вида n = 2k + 2k 1 + 2k 2 + : : : + 2k t? Для произвольных чисел?
Теперь рассмотрим примитивный алгоритм точного вычисления факториала по модулю m (см. алгоритм 3).
Нетрудно видеть, что его сложность есть O(n). Можно ли ее существенно улучшить, как в случае с возведением степень? Оказывается, на сегодняшний день это является известной открытой проблемой
(см. следствия из «Problem 4» в [Sma00]).
Рассмотрим еще одну очень важную задачу, связанную с возведением в степень по модулю.
Задача 2. «Дискретный логарифм» (Discrete logarithm) Даны натуральные a, b, и p — нечетное простое число. Найти минимальный x, такой, что
ax b (mod p):
По сути, эта обратная задача к задаче возведения в степень по модулю. Прямолинейный алгоритм ее решения переборного типа, конечно, существует. Достаточно для каждого x в интервале [1; p] вычислить
1.1. ПРИМЕРЫ ЗАДАЧ И АЛГОРИТМОВ |
19 |
Алгоритм 3 Вычисление факториала y = n! mod m
def factorial(n, m): print n, m
y = 1
for i in range(1, n+1): y = y * i % m print y
return y
13 131
1
2
6
24
120
65
62
103
10
100
52
100
121
y =13! mod 131 = 121
ax mod p и проверить требуемое равенство. Возведение в степень делается эффективно, как мы уже видели. Проблема в том, что эту эффективную процедуру придется проделать огромное число раз (например, при p порядка 2100).
Можем ли мы, как и в задаче о возведении в степень, ожидать существования существенно более эффективного алгоритма решения? Оказывается, это весьма маловероятно, вычисление дискретного логарифма является очень сложной задачей, и самые быстрые из известных алгоритмов требуют сверхполиномиального (по длине двоичной записи p) времени. На этом важном свойстве (свойстве односторонней эффективной вычислимости модульной экспоненты) основано множество протоколов современной криптографии.
Еще одним примером взаимно обратных преобразований подобного рода являются операции произведения натуральных чисел и разложение на множители. Пусть p и q — два простых числа. Тогда не
20 |
Глава 1. АЛГОРИТМЫ И ИХ СЛОЖНОСТЬ |
составляет особого труда вычислить их произведение n = pq (применив, например, школьный алгоритм умножения «столбиком»). Однако если мы знаем лишь n и требуется найти p и q, то это уже труднорешаемая задача, для которой неизвестны эффективные алгоритмы решения. На трудности задачи факторизации (разложения на множители) основана одна из самых известных криптосистем RSA.
Рассмотрим одну из классических задач.
Задача 3. «Наибольший общий делитель»
Для заданных натуральных a и b найти максимальное q, для которого a ... q и b ... q (a и b кратны q).
Проанализируем алгоритм Евклида (алгоритм 4) для решения этой задачи (для определенности ниже будем считать, что a b.). Базовое соотношение, лежащее в основе алгоритма Евклида, таково:
НОД(a; b) = НОД(a; r); b = at + r; 0 r < a:
Алгоритм 4 Алгоритм Евклида
def gcd(a, b): print a, b if a == 0:
return b
return gcd(b % a, a)
Calculating gcd(123456, 6122256): 123456 6122256 72912 123456 50544 72912 22368 50544 5808 22368 4944 5808 864 4944 624 864 240 624 144 240 96 144 48 96 0 48
gcd(123456, 6122256) = 48
1.1. ПРИМЕРЫ ЗАДАЧ И АЛГОРИТМОВ |
21 |
Лемма 1. Время работы алгоритма 4 «НОД» составляет O(log a + log b) арифметических операций над натуральными числами.
Доказательство. Имеем
b a + (b mod a) 2(b mod a) 2r:
Отсюда получаем, что r 2b . Таким образом, ar ab/2, т. е. произведение ab уменьшается на каждой итерации вдвое, и после log(ab) итераций станет меньше 1, т. е. равно нулю. А это означает, что a = 0 (т. е. НОД уже найден). 

Алгоритм 5 Неэффективный алгоритм для «НОД»
def gcd_non_effective(a, b): print a, b
if a == 0: return b
return gcd( max(a, b) - min(a, b), min(a, b) )
Длина записи исходных данных в этой задаче есть O(log a + log b) и по порядку совпадает с числом операций алгоритма Евклида, т. е. алгоритм является эффективным. Отметим, что алгоритм 5 «Неэффективный НОД» (модификация алгоритма 4 «НОД», где деление с остатком заменено на вычитание меньшего числа из большего) не является столь же эффективным — ведь число операций вычитания пропорционально ab , а не сумме их логарифмов, и на входных данных, где b много больше a, алгоритм будет показывать экспоненциальную сложность.
Таким образом, мы увидели, что, несмотря на внешнюю похожесть, сложность подобных задач может существенно отличаться. Для обнаружения и понимания такой разницы и нужен анализ сложности, являющийся предметом нашего курса.
