SPb HSE, 2 курс, осень 2022/23
Конспект лекций по алгоритмам
Собрано 22 апреля 2024 г. в 22:02
Содержание
1. Паросочетания 1
1.1. Определения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2. Сведения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.3. Дополняющие чередующиеся пути . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.4. Поиск паросочетания в двудольном графе . . . . . . . . . . . . . . . . . . . . . . . . 2
1.5. Реализация . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.6. Алгоритм Куна . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.7. Оптимизации Куна . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.8. Поиск минимального вершинного покрытия (на практике) . . . . . . . . . . . . . . 4
1.9. Обзор решений . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.10. Решения для произвольного графа . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.10.1. Обзор . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.10.2. Читерский подход . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.10.3. Связь паросочетаний и определителя . . . . . . . . . . . . . . . . . . . . . . 5
1.10.4. Матрица Татта . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2. Потоки (база) 6
2.1. Основные определения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.2. Обратные рёбра . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.3. Декомпозиция потока . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.4. Теорема и алгоритм Форда-Фалкерсона . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.5. Реализация, хранение графа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.6. Паросочетание, вершинное покрытие . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.6.1. Вершинное покрытие . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.7. Леммы, позволяющие работать с потоками . . . . . . . . . . . . . . . . . . . . . . . 11
2.8. Алгоритмы поиска потока . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.8.1. Эдмондс-Карп за 𝒪(𝑉 𝐸 2 ) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.8.2. Масштабирование за 𝒪(𝐸 2 log 𝑈 ) . . . . . . . . . . . . . . . . . . . . . . . . . 12
3. Паросочетания (продолжение) 12
3.1. Классификация рёбер двудольного графа . . . . . . . . . . . . . . . . . . . . . . . . 13
3.2. Stable matching (marriage problem) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
3.3. (*) Венгерский алгоритм . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3.3.1. (*) Реализация за 𝒪(𝑉 3 ) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
3.3.2. (*) Псевдокод . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
3.4. (*) Покраска графов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3.4.1. (*) Вершинные раскраски . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3.4.2. (*) Вершинные раскраски планарных графов . . . . . . . . . . . . . . . . . 17
3.4.3. (*) Рёберные раскраски . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3.4.4. (*) Рёберные раскраски двудольных графов . . . . . . . . . . . . . . . . . . 18
3.4.5. (*) Покраска не регулярного графа за 𝒪(𝐸 2 ) . . . . . . . . . . . . . . . . . . 18
4. Потоки (быстрые) 18
4.1. Алгоритм Диница . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
4.2. Алгоритм Хопкрофта-Карпа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
4.3. Теоремы Карзанова . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
4.4. Диниц с link-cut tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
4.5. Глобальный разрез . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
4.5.1. Алгоритм Штор-Вагнера . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
4.5.2. Алгоритм Каргера-Штейна . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
4.6. (*) Алгоритм Push-Relabel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
5. Mincost 23
5.1. Mincost k-flow в графе без отрицательных циклов . . . . . . . . . . . . . . . . . . . 24
5.2. Потенциалы и Дейкстра . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
5.3. Задачи на mincost поток, паросочетания . . . . . . . . . . . . . . . . . . . . . . . . . 25
5.4. Графы с отрицательными циклами . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
5.5. Mincost flow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
5.6. Полиномиальные решения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
5.7. (*) Cost Scaling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
6. Базовые алгоритмы на строках 28
6.1. Обозначения, определения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
6.2. Поиск подстроки в строке . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
6.2.1. C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
6.2.2. Префикс функция и алгоритм КМП . . . . . . . . . . . . . . . . . . . . . . . 29
6.2.3. LCP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
6.2.4. Z-функция . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
6.3. Полиномиальные хеши строк . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
6.3.1. Алгоритм Рабина-Карпа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
6.3.2. Наибольшая общая подстрока за 𝒪(𝑛 log 𝑛) . . . . . . . . . . . . . . . . . . . 33
6.3.3. Оценки вероятностей . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
6.3.4. Число различных подстрок (на практике) . . . . . . . . . . . . . . . . . . . . 34
6.3.5. Строка Туэ-Морса . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
6.4. Алгоритм Бойера-Мура . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
7. Суффиксный массив 38
7.1. Построение за 𝒪(𝑛 log2 𝑛) хешами . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
7.2. Применение суффиксного массива: поиск строки в тексте . . . . . . . . . . . . . . 38
7.3. Построение за 𝒪(𝑛2 ) и 𝒪(𝑛 log 𝑛) цифровой сортировкой . . . . . . . . . . . . . . . 38
7.4. LCP за 𝒪(𝑛): алгоритм Касаи . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
7.5. Быстрый поиск строки в тексте . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
7.6. (*) Построение за 𝒪(𝑛): алгоритм Каркайнена-Сандерса . . . . . . . . . . . . . . . 40
7. Бор 41
7.7. Собственно бор . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
7.8. Сортировка строк . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
8. Ахо-Корасик и Укконен 42
8.1. Алгоритм Ахо-Корасика . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
8.2. Суффиксное дерево, связь с массивом . . . . . . . . . . . . . . . . . . . . . . . . . . 44
8.3. Суффиксное дерево, решение задач . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
8.4. Алгоритм Укконена . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
8.5. LZSS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
8. Хеширование 46
8.6. Универсальное семейство хеш функций . . . . . . . . . . . . . . . . . . . . . . . . . 47
8.7. Оценки для хеш-таблицы с закрытой адресацией . . . . . . . . . . . . . . . . . . . . 47
8.8. Оценки других функций для хеш-таблиц . . . . . . . . . . . . . . . . . . . . . . . . 48
8.9. (-) Фильтр Блюма . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
8.10. (-) Совершенное хеширование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
8.10.1. (-) Одноуровневая схема . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
8.10.2. (-) Двухуровневая схема . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
8.10.3. (*) Графовый подход . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
8.11. (*) Хеширование кукушки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
9. Теория чисел 52
9.1. (-) Решето Эратосфена . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
9.2. (-) Решето и корень памяти (на практике) . . . . . . . . . . . . . . . . . . . . . . . 53
9.3. (-) Вычисление мультипликативных функций функций на [1, 𝑛] . . . . . . . . . . . 53
9.4. (*) Число простых на [1, 𝑛] за 𝑛2/3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
9.5. Определения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
9.6. Расширенный алгоритм Евклида . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
9.7. (-) Свойства расширенного алгоритма Евклида . . . . . . . . . . . . . . . . . . . . 55
9.8. Обратные в (Z/𝑚Z)* и Z/𝑝Z . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
9.9. (-) Возведение в степень за 𝒪(log 𝑛) . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
9.10. (-) Обратные в Z/𝑝Z для чисел от 1 до 𝑘 за 𝒪(𝑘) . . . . . . . . . . . . . . . . . . 56
9.11. (-) Первообразный корень . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
9.12. Криптография. RSA. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
9.13. Протокол Диффи-Хеллмана . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
9.14. (-) Дискретное логарифмирование . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
9.15. (-) Корень 𝑘-й степени по модулю . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
9.16. (-) КТО . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
9.16.1. (-) Использование КТО в длинной арифметике. . . . . . . . . . . . . . . . 60
10. Линейные системы уравнений 60
10.1. Гаусс для квадратных невырожденных матриц. . . . . . . . . . . . . . . . . . . . . 61
10.2. Гаусс в общем случае . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
10.3. Гаусс над F2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
10.4. Погрешность . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
10.5. Метод итераций . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
10.6. Вычисление обратной матрицы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
10.7. Гаусс для евклидова кольца . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
10.8. Разложение вектора в базисе . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
10.8.1. Ортогонализация Грама-Шмидта . . . . . . . . . . . . . . . . . . . . . . . . 65
10.9. Вероятностные задачи . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
10.10. (*) СЛАУ над Z и Z/𝑚Z . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
10.10.1. (*) СЛАУ над Z . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
10.10.2. (*) СЛАУ по модулю . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
10.10.3. (*) СЛАУ над Z/𝑝𝑘 Z . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
11. Быстрое преобразование Фурье 68
11.1. Прелюдия к FFT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
11.2. Собственно идея FFT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
11.3. Крутая реализация FFT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
11.4. Обратное преобразование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
11.5. Два в одном . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
11.6. Умножение чисел, оценка погрешности . . . . . . . . . . . . . . . . . . . . . . . . . 70
11.7. Применение. Циклические сдвиги. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
12. Длинная арифметика 70
12.1. Простейшие операции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
12.2. (-) Бинарная арифметика . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
12.3. Деление многочленов за 𝒪(𝑛 log2 𝑛) . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
12.4. (-) Деление чисел . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
12.5. (-) Деление чисел за 𝒪((𝑛/𝑘)2 ) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
13. Умножение матриц и 4 русских 75
13.1. Умножение матриц, простейшие оптимизации . . . . . . . . . . . . . . . . . . . . . 75
13.2. Четыре русских . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
13.3. Умножение матриц над F2 за 𝒪(𝑛3 /(𝑤 log 𝑛)) . . . . . . . . . . . . . . . . . . . . . 76
13.4. НОП за 𝒪(𝑛2 / log2 𝑛) (на практике) . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
13.5. (-) Схема по таблице истинности . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
13.6. (-) Оптимизация перебора для клик . . . . . . . . . . . . . . . . . . . . . . . . . . 77
13.7. (-) Транзитивное замыканиие . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
Алгоритмы, 2 курс, осень 2022/23 Паросочетания
Лекция #1: Паросочетания
20 сентября 2022
1.1. Определения
Def 1.1.1. Паросочетание (matching) – множество попарно не смежных рёбер 𝑀 .
Def 1.1.2. Вершинное покрытие (vertex cover) –
множество вершин 𝐶, что у любого ребра хотя бы один конец лежит в 𝐶.
Def 1.1.3. Независимое множество (indpendent set) –
множество попарно несмежных вершин 𝐼.
Def 1.1.4. Клика (clique) – множество попарно смежных вершин.
Def 1.1.5. Совершенное паросочетание – паросочетание, покрывающее все вершины графа.
В двудольном графе совершенным является паросочетание, покрывающее все вершины мень-
шей доли.
Def 1.1.6. Относительно любого паросочетания все вершины можно поделить на
∙ покрытые паросочетанием (принадлежащие паросочетанию),
∙ не покрытые паросочетанием (свободные).
Обозначения: Matching (M), Vertex Cover (VС или С), Independent Set (IS или I).
1.2. Сведения
Пусть дан граф 𝐺, заданный матрицей смежности 𝑔𝑖𝑗 . Инвертацией 𝐺 назовём граф 𝐺′ , задан-
ный матрицей смежности 𝑔𝑖𝑗′ = 1 − 𝑔𝑖𝑗 . тогда независимое множество в 𝐺 задаёт клику в 𝐺′ , а
клика в 𝐺 задаёт независимое множество в 𝐺′ .
Следствие 1.2.1. Задачи поиска max клики и max IS сводятся друг к другу.
Lm 1.2.2. Дополнение любого VC – IS. Дополнение любого IS – VC.
Следствие 1.2.3. Все три задачи поиска min VC, max IS, max clique сводятся друг к другу.
Утверждение 1.2.4. Задачи поиска min VC, max IS, max clique NP-трудны.
Утверждение было доказано в прошлом семестре в разделе про сложность.
1.3. Дополняющие чередующиеся пути
Def 1.3.1. Чередующийся путь – простой путь, в котором рёбра чередуются в смысле при-
надлежности паросочетанию.
Def 1.3.2. Дополняющий чередующийся путь (ДЧП) – чередующийся путь, первая и послед-
няя вершина которого не покрыты паросочетанием.
Lm 1.3.3. Паросочетание 𝑃 максимально ⇔ ∄ ДЧП (лемма о дополняющем пути).
Глава #1. 20 сентября 2022. 1/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Паросочетания
Доказательство. Пусть ∃ ДЧП ⇒ инвертируем все рёбра на нём, получим паросочетание раз-
мера |𝑃 | + 1. Докажем теперь, что если ∃ паросочетание 𝑀 : |𝑀 | > |𝑃 |, то ∃ ДЧП. Для этого
рассмотрим 𝑆 = 𝑀 ▽𝑃 . Степень каждой вершины в 𝑆 не более двух (одно ребро из 𝑀 , одно из
𝑃 ) ⇒ 𝑆 является объединением циклов и путей. ∑︀Каждому пути сопоставим число 𝑎𝑖 – разность
количеств рёбер из 𝑀 и 𝑃 . Тогда |𝑀 | = |𝑃 | + 𝑖 𝑎𝑖 ⇒ ∃𝑎𝑖 > 0 ⇒ один из путей – ДЧП. ■
Лемма доказана для произвольного графа, но с лёгкостью найти ДЧП мы сможем только для
двудольного графа.
1.4. Поиск паросочетания в двудольном графе
Lm 1.4.1. Пусть 𝐺 – двудольный граф, а 𝑃 паросочетание в нём. Построим орграф 𝐺′ (𝐺, 𝑃 ).
Вершины такие же, как в 𝐺. Рёбра: из первой доли во вторую пустим все рёбра 𝐺, а из второй
в первую долю только рёбра из 𝑃 . Тогда есть биекция между путями в 𝐺′ и чередующимися
путями в 𝐺.
Lm 1.4.2. Поиск ДЧП в 𝐺 ⇔ поиску пути в 𝐺′ из свободной вершины в свободную.
Следствие 1.4.3. Мы получили алгоритм поиска максимального паросочетания 𝑀 за 𝒪(|𝑀 |·𝐸):
0. 𝑃 ← ∅
1. Попробуем найти путь dfs-ом в 𝐺′ (𝐺, 𝑃 )
2. if не нашли ⇒ 𝑀 максимально
3. else goto 1
1.5. Реализация
Важной идеей является применение ДЧП к паросочетания на обратном ходу рекурсии.
1 def dfs ( v ) :
2 used [ v ] = 1 # массив пометок для вершин первой доли
3 for x in graph [ v ]: # рёбра из 1-й доли во вторую
4 if ( pair [ x ] == -1) or ( used [ pair [ x ]] == 0 and dfs ( pair [ x ]) ) :
5 pair [ x ] = v # массив пар для вершин второй доли
6 return True
7 return False
Граф 𝐺′ в явном виде мы нигде не строим. Вместо этого, когда идём из 1-й доли во вторую,
перебираем все рёбра (v → x in graph[v]), а когда из 2-й в первую, идём только по ребру
паросочетания (x → pair[x]).
1.6. Алгоритм Куна
Lm 1.6.1. В процессе поиска максимального паросочетания ∄ ДЧП из 𝑣 ⇒
ДЧП из 𝑣 уже никогда не появится.
Пусть появился ДЧП 𝑝 : 𝑣 ⇝ 𝑢 после применения ДЧП 𝑞 : 𝑎 ⇝ 𝑏. x b
Тогда пойдём по 𝑝 из 𝑣, найдём 𝑥 ∈ 𝑝 – первую вершину в 𝑞. Из 𝑥 v
пойдём по пути 𝑎 ↔ 𝑏 по ребру паросочетания, дойдём до конца,
получим пути 𝑣 ⇝ (𝑎 ∨ 𝑏), который существовал до применения 𝑞.
a
■
Глава #1. 20 сентября 2022. 2/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Паросочетания
Получили алгоритм Куна:
1 for v in range ( n ) :
2 used = [0] * n
3 dfs ( v )
1.7. Оптимизации Куна
Обозначим за 𝑘 размер максимального паросочетания. Сейчас время работы алгоритма Куна
𝒪(𝑉 𝐸) даже для 𝑘 = 𝒪(1). Будем обнулять пометки used[] только, если мы нашли ДЧП.
1 used = [0] * n
2 for v in range ( n ) :
3 if dfs ( v ) :
4 used = [0] * n
Алгоритм остался корректным, так как, между успешными запусками dfs граф 𝐺′ не меняется.
И теперь работает за 𝒪(𝑘𝐸).
Следующая оптимизация – «жадная инициализация». До запуска Куна переберём все рёбра и
те из них, что можем, добавим в паросочетание.
Lm 1.7.1. Жадная инициализация даст паросочетание размера ⩾ 𝑘2 .
Доказательство. Если мы взяли ребро, которое на самом деле не должно лежать в максималь-
ном паросочетании 𝑀 , мы заблокировали возможность взять ⩽ 2 рёбер из 𝑀 . ■
При использовании жадной инициализации у нас появляется необходимость поддерживать мас-
сив covered[v], хранящий для вершины первой доли, покрыта ли она паросочетанием.
Попробуем теперь сделать следующее:
1 used = [0] * n
2 for v in range ( n ) :
3 if not covered [ v ]:
4 dfs ( v )
Код работает за 𝒪(𝑉 + 𝐸). Если паросочетание не максимально, найдёт хотя бы один ДЧП.
А может найти больше чем один... в этом и заключается последняя оптимизация «вообще не
обнулять пометки»: пока данный код находит хотя бы один путь, запускаем его.
Замечание 1.7.2. На практике докажем, что, если мы используем последнюю оптимизацию,
«жадная инициализация» является полностью бесполезной.
Напоминание: мы умеем обнулять пометки за 𝒪(1). Для этого помеченной считаем вершины v:
«used[v] == cc», тогда операция «cc++» сделает все вершины не помеченными.
Напоминание: если использовать random_shuffle рёбер, работает быстрее, в том смысле, что
max𝑡𝑒𝑠𝑡 𝐸[𝑇 𝑖𝑚𝑒(𝑡𝑒𝑠𝑡)] уменьшилось (макстест перестаёт быть макстестом).
Замечание 1.7.3. Для последней версии алгоритма авторам конспекта неизвестен тест, на ко-
тором достигается время работы Ω(𝑉 𝐸). Если не использовать random_shuffle рёбер, есть
конструкция для Ω( log
𝑉𝐸
𝑉
).
Глава #1. 20 сентября 2022. 3/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Паросочетания
1.8. Поиск минимального вершинного покрытия (на практике)
Lm 1.8.1. ∀𝑀, 𝑉 𝐶 верно, что |𝑉 𝐶| ⩾ |𝑀 |
Доказательство. Для каждого ребра 𝑒 ∈ 𝑀 , нужно взять в 𝑉 𝐶 хотя бы один из концов 𝑒. ■
Пусть у нас уже построено максимальное паросочетание 𝑀 . Запустим dfs на 𝐺′ из всех свобод-
ных вершин первой доли. Обозначим первую долю 𝐴, вторую 𝐵. Посещённые dfs-ом вершины
соответственно 𝐴+ и 𝐵 + , а непосещённые 𝐴− и 𝐵 − .
Теорема 1.8.2. 𝑋 = 𝐴− ∪ 𝐵 + – минимальное вершинное покрытие.
Доказательство. Если бы из 𝑎 ∈ 𝐴+ было бы ребро в 𝑏 ∈ 𝐵 − , мы бы по нему прошли, и 𝑏
лежала бы в 𝐵 + ⇒ в 𝐴+ ∪ 𝐵 − нет рёбер ⇒ 𝑋 – вершинное покрытие.
Оценим размер 𝑋: все вершины из 𝐴− ∪ 𝐵 + – концы рёбер паросочетания 𝑀 , т.к. dfs не нашёл
дополняющего пути. Более того это концы обязательно разных рёбер паросочетания, т.к. если
один конец ребра паросочетания лежит в 𝐵 + , то dfs пойдёт по нему, и второй конец окажется
в 𝐴+ . Итого |𝑋| ⩽ |𝑀 |. Из этого и 1.8.1 следует |𝑋| = |𝑀 | и |𝑋| = min. ■
Следствие 1.8.3. 𝐴+ ∪ 𝐵 − – максимальное независимое множество (1.2.2).
Замечание 1.8.4. Умеем строить min 𝑉 𝐶 и max 𝐼𝑆 за 𝒪(𝑉 +𝐸) при наличии max паросочетания.
Следствие 1.8.5. max 𝑀 = min 𝑉 𝐶 (теорема Кёнига).
1.9. Обзор решений
Мы изучили алгоритм Куна со всеми оптимизациями.
Асимптотически время его работы 𝒪(𝑉 𝐸), на практике же он жутко шустрый.
На графах 𝑉, 𝐸 ⩽ 105 решение укладывается в секунду.
∃ также алгоритм Хопкрофта-Карпа за 𝒪(𝐸𝑉 1/2 ). Его мы изучим в контексте потоков.
В регулярном двудольном графе можно найти совершенное паросочетание за время 𝒪(𝑉 log 𝑉 ).
Статья Михаила Капралова и ко.
1.10. Решения для произвольного графа
1.10.1. Обзор
Наиболее стандартным считается решение через сжатие «соцветий» (видим нечётный цикл –
сожмём его, найдём паросочетание в новом цикле, разожмём цикл, перестроим паросочетание).
Этот подход используют реализации Габова за 𝒪(𝑉 3 ) и Тарьяна за 𝒪(𝑉 𝐸𝛼). Подробно эту тему
мы будем изучать на 3-м курсе.
Оптимальный по времени – алгоритм Вазирани, 𝒪(𝐸𝑉 1/2 ).
Кроме этого есть два подхода, которые мы обсудим подробнее.
Глава #1. 20 сентября 2022. 4/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Паросочетания
1.10.2. Читерский подход
Давайте на недвудольном графе запустим dfs из Куна для поиска дополняющей цепи...
При этом помечать, как посещённые, будем вершины обеих долей в одном массиве used.
С некоторой вероятностью алгоритм успешно найдёт дополняющий путь.
4
1 2 3 6
Если мы запускаем dfs из вершины 1, то у неё есть шанс найти дополняющий путь 1 → 2 →
3 → 5 → 4 → 6. Но если из вершины 3 dfs сперва попытается идти в 4, то он пометит 4 и 5, как
посещённые, больше в них заходить не будет, путь не найдёт.
Т.е. на данном примере в зависимости от порядка соседей вершины 3 dfs или найдёт, или не
найдёт путь. Если выбрать случайный порядок, то найдёт с вероятностью 1/2.
Это лучше, чем «при некотором порядке рёбер вообще не иметь возможности найти путь»,
поэтому первой строкой dfs добавляем random_shuffle(c[v].begin(), c[v].end()).
На большинстве графов алгоритм сможет найти максимальное паросочетание. ∃ графы, на
которых вероятность нахождения дополняющей цепи экспоненциально от числа вершин мала.
1.10.3. Связь паросочетаний и определителя
Пусть дан двудольный граф. Если доли имеют размеры 𝑛 и 𝑚, матрицу смежности разумно
задавать размера 𝑛 × 𝑚, а не (𝑛 + 𝑚) × (𝑛 + 𝑚), как для обычного.
Пусть 𝑛 = 𝑚. Совершенному паросочетанию соответствует перестановка 𝜋 : 𝑖 →∑︀𝜋∏︀
𝑖 и ячейки
матрица смежности 𝐴 : 𝐴𝑖 𝜋𝑖 = 1 ⇒ количество совершенных паросочетаний 𝑁 = 𝑎𝑖 𝜋𝑖 .
𝜋 𝑖
Следствие 1.10.1. Чётность числа совершенных паросочетаний = det 𝐴 mod 2
Доказательство. det 𝐴 — такая же сумма, только у слагаемых другой знак, но −1 = 1 mod 2 ■
Замечание 1.10.2. Подсчёт 𝑁 — трудная задача, её не умеют решать за 2𝑜(𝑛) .
Поскольку ответ — целое число, формально это не NP-hard, а P#-hard задача.
Глава #1. 20 сентября 2022. 5/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Паросочетания
1.10.4. Матрица Татта
Пусть дан двудольный граф 𝐺(𝐿, 𝑅, 𝐸), |𝐿| = |𝑅| = 𝑛. Определим для него матрицу Эдмондса
𝐷 размера 𝑛 × 𝑛: 𝑑𝑖,𝑗 = 𝑥𝑖,𝑗 , если есть ребро из 𝑖 ∈ 𝐿 в 𝑗 ∈ 𝑅. В противном случае 𝑑𝑖,𝑗 = 0.
Здесь 𝑥𝑖,𝑗 – это 𝑛2 различных переменных.
det 𝐷 – это многочлен от 𝑛2 переменных. Легко видеть, что он тождественно равен нулю тогда
и только тогда, когда в графе нет совершенного паросочетания (слагаемые в определителе не
взаимоуничтожаются, а любое такое слагаемое соответствует совершенному паросочетанию).
Пусть теперь граф произвольный. Определим для него матрицу Татта 𝑇 .
Для каждого ребра (𝑖, 𝑗) элементы 𝑡𝑖𝑗 = 𝑥𝑖𝑗 , 𝑡𝑗𝑖 = −𝑥𝑖𝑗 .
Остальные элементы равны нулю. Здесь 𝑥𝑖𝑗 – переменные.
Получается, для каждого ребра неорграфа мы ввели ровно одну переменную.
det 𝑇 – многочлен от 𝑛(𝑛 − 1)/2 переменных.
Теорема 1.10.3. Татта: det 𝑇 ̸≡ 0 ⇔ ∃ совершенное паросочетание.
Доказательство будет в курсе дискретной математики.
Чтобы проверить det 𝑇 ≡ 0, используем лемму Шварца-Зиппеля: в каждую переменную 𝑥𝑖𝑗
подставим случайное значение, посчитаем определитель матрицы над полем F𝑝 , где 𝑝 = 109 + 7,
получим вероятность ошибки не более deg(det 𝑇 )/𝑝 = 𝑛/𝑝.
Время работы 𝒪(𝑛3 ) (нахождение определителя матрицы по модулю 𝑝), алгоритм умеет лишь
проверять наличие совершенного паросочетания.
Алгоритм можно модифицировать сперва для определения размера максимального паросоче-
тания, а затем для его нахождения. ⎡ ⎤
0 𝑥12 0
Пример: 𝑛 = 3, 𝐸 = {(1, 2), (2, 3)}, 𝑇 = ⎣−𝑥12 0 𝑥23 ⎦, подставляем 𝑥12 = 9, 𝑥23 = 7, считаем
⎡ ⎤ 0 −𝑥23 0
0 9 0
det ⎣−9 0 7⎦ = 0 ⇒ с большой вероятностью нет совершенного паросочетания.
0 −7 0
Глава #2. 20 сентября 2022. 6/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Потоки (база)
Лекция #2: Потоки (база)
27 сентября
2.1. Основные определения
Дан орграф 𝐺, у каждого ребра 𝑒 есть пропускная способность 𝑐𝑒 ∈ R.
Def 2.1.1. Поток в орграфе из 𝑠 в 𝑡 – сопоставленные рёбрам числа 𝑓𝑒 ∈ R :
∑︁ ∑︁
(∀ ребра 𝑒 0 ⩽ 𝑓𝑒 ⩽ 𝑐𝑒 ) ∧ (∀ вершины 𝑣 ̸= 𝑠, 𝑡 𝑓𝑒 = 𝑓𝑒 )
𝑒∈𝑖𝑛(𝑣) 𝑒∈𝑜𝑢𝑡(𝑣)
Вершина 𝑠 называется истоком, вершина 𝑡 стоком.
Говорят, что по ребру 𝑒 течёт 𝑓𝑒 единиц потока.
Определение говорит «поток течёт из истока в сток и ни в какой вершине не задерживается».
∑︀ ∑︀
Def 2.1.2. Величина потока |𝑓 | = 𝑓𝑒 − 𝑓𝑒 (сколько вытекает из истока).
𝑒∈𝑜𝑢𝑡(𝑠) 𝑒∈𝑖𝑛(𝑠)
Утверждение 2.1.3. В сток втекает ровно столько, сколько вытекает из истока.
Замечание 2.1.4. |𝑓 | может быть отрицательной: пустим по ребру 𝑡 → 𝑠 единицу потока.
Def 2.1.5. Циркуляцией называется поток величины 0.
∙ Примеры потока
Рассмотрим пока граф с единичными пропускными способностями.
1. ∀ цикл – циркуляция. 0/1
2. ∀ путь из 𝑠 в 𝑡 – поток величины 1. 1/2 1/2 2/2
3. ∀ 𝑘 не пересекающихся по рёбрам путей 0 1 2 3
из 𝑠 в 𝑡 – поток величины 𝑘.
4. На картинке поток величины 2, подписи 𝑓𝑒 /𝑐𝑒 . 1/1
Def 2.1.6. Остаточная сеть потока 𝑓 – 𝐺𝑓 , граф с пропускными способностями 𝑐𝑒 − 𝑓𝑒 .
Def 2.1.7. Дополняющий путь – путь из 𝑠 в 𝑡 в остаточной сети 𝐺𝑓 .
Lm 2.1.8. Если по всем рёбрам дополняющего пути 𝑝 увеличить величину потока
на 𝑥 = min(𝑐𝑒 − 𝑓𝑒 ), получится корректный поток величины |𝑓 | + 𝑥.
𝑒∈𝑝
2.2. Обратные рёбра
Def 2.2.1. Для каждого ребра сети 𝐺 с пропускной способностью 𝑐𝑒 создадим
обратное ребро 𝑒′ пропускной способностью 0. При этом по определению 𝑓𝑒′ = −𝑓𝑒 .
Добавим в граф обратные рёбра, упростим определения
∑︀ потока и величины потока:
Теперь ∀ потока 𝑓 должно выполняться ∀𝑣 ̸= 𝑠, 𝑡 𝑓𝑒 = 0, величина потока |𝑓 | = 𝑓𝑒 .
∑︀
𝑒∈𝑜𝑢𝑡(𝑣) 𝑒∈𝑜𝑢𝑡(𝑠)
Здесь 𝑜𝑢𝑡(𝑣) – множество прямых и обратных рёбер, выходящих из 𝑣.
Def 2.2.2. Ребро называется насыщенным, если 𝑓𝑒 = 𝑐𝑒 , иначе оно ненасыщено.
Утверждение 2.2.3. Если по прямому ребру течёт поток, обратное ненасыщено.
Глава #2. 27 сентября. 7/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Потоки (база)
0/1
После добавления обратных рёбер в в 𝐺, они появи-
1/1 1/1 1/1
лись и в 𝐺𝑓 . Поэтому для такого потока из 0 в 3 вели- 0 1 2 3
чины 1 в 𝐺𝑓 есть дополняющий путь 0 → 2 → 1 → 3.
0/1
1/1
Увеличим поток по пути 0 → 2 → 1 → 3, получим
1/1 0/1 1/1
новый поток. Заметьте, добавляя +1 потока к ребру 0 1 2 3
2 → 1, мы уменьшаем поток по ребру 1 → 2.
1/1
Замечание 2.2.4. Сейчас мы научимся разбивать поток величины 2 на 2 непересекающихся
по рёбрам пути. Если бы мы действовали жадно (найдём какой-нибудь первый путь, удалим
его рёбра из графа, на оставшихся рёбрах найдём второй путь), нас постигла бы не удача.
Потоку же благодаря обратным рёбрам получается даже при неверном первом пути найти
дополняющий путь и получить поток размера два.
2.3. Декомпозиция потока
Def 2.3.1. Элементарный поток – путь из 𝑠 в 𝑡, по которому течёт 𝑥 единиц потока.
Def 2.3.2. Декомпозиция потока 𝑓 – представление 𝑓 в виде суммы элементарных потоков
(путей) и циркуляции.
Lm 2.3.3. |𝑓 | > 0 ⇒ ∃ путь из 𝑠 в 𝑡 по рёбрам 𝑒 : 𝑓𝑒 > 0.
∙ Алгоритм декомпозиции за 𝒪(𝐸 2 )
Пока |𝑓 | > 0 найдём путь 𝑝 из 𝑠 в 𝑓 по рёбрам 𝑒 : 𝑓𝑒 > 0,
по всем рёбрам пути 𝑝 уменьшим поток на 𝑥 = min𝑒∈𝑝 𝑓𝑒 .
Lm 2.3.4. Время работы 𝒪(𝐸 2 )
Доказательство. По рёбрам 𝑒 : 𝑓𝑒 > 0 поток только убывает. После отщепления одного пути,
как минимум у одного из рёбер 𝑓𝑒 обнулится ⇒ не более 𝐸 поисков пути. ■
2.4. Теорема и алгоритм Форда-Фалкерсона
Def 2.4.1. Для любых множеств 𝑆, 𝑇 ⊆ 𝑉 определим
∑︁ ∑︁
𝐹 (𝑆, 𝑇 ) = 𝑓𝑎→𝑏 , 𝐶(𝑆, 𝑇 ) = 𝑐𝑎→𝑏
𝑎∈𝑆,𝑏∈𝑇 𝑎∈𝑆,𝑏∈𝑇
Сумма включает обратные
{︃ рёбра ⇒ на графе из одного ребра 𝑒 : 𝑡 → 𝑠, 𝑓𝑒 = 1 𝐹 ({𝑠}, {𝑡}) = −1.
𝐹 ({𝑣}, 𝑉 ) = 0 𝑣 ̸= 𝑠, 𝑡
Lm 2.4.2. ∀𝑣 ∈ 𝑉
𝐹 ({𝑣}, 𝑉 ) = |𝑓 | 𝑣 = 𝑠
Lm 2.4.3. ∀𝑆 𝐹 (𝑆, 𝑆) = 0
Доказательство. В вместе с каждым ребром в сумму войдёт и обратное ему. ■
Lm 2.4.4. ∀𝑆, 𝑇 𝐹 (𝑆, 𝑇 ) ⩽ 𝐶(𝑆, 𝑇 )
Доказательство. Сложили неравенства 𝑓𝑒 ⩽ 𝑐𝑒 по всем рёбрам 𝑒 : 𝑆 → 𝑇 . ■
Def 2.4.5. Разрез – дизьюнктное разбиение вершин (𝑆, 𝑇 ) : 𝑉 = 𝑆 ⊔ 𝑇, 𝑠 ∈ 𝑆, 𝑡 ∈ 𝑇 .
Def 2.4.6. Величина разреза (𝑆, 𝑇 ) = 𝐶(𝑆, 𝑇 ).
Глава #2. 27 сентября. 8/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Потоки (база)
Lm 2.4.7. ∀ разреза (𝑆, 𝑇 ) |𝑓 | = 𝐹 (𝑆, 𝑇 )
Доказательство. Интуитивно: поток вытекает из 𝑠, нигде не задерживается ⇒ он весь протечёт
через разрез. Строго: 𝐹 (𝑆, 𝑇 ) = 𝐹 (𝑆, 𝑇 ) + 𝐹 (𝑆, 𝑆) = 𝐹 (𝑆, 𝑉 ) = 𝐹 ({𝑠}, 𝑉 ) + 0 + · · · + 0 = |𝑓 |. ■
Lm 2.4.8. ∀ разреза (𝑆, 𝑇 ) и потока 𝑓 |𝑓 | ⩽ 𝐶(𝑆, 𝑇 )
Доказательство. |𝑓 | = 𝐹 (𝑆, 𝑇 ) ⩽ 𝐶(𝑆, 𝑇 ) (пользуемся леммами 2.4.4 и 2.4.7). ■
Теорема 2.4.9. Форда-Фалекрсона
(1) |𝑓 | = max ⇔ ∄ дополняющий путь
(2) max |𝑓 | = min 𝐶(𝑆, 𝑇 ) (максимальный поток равен минимальному разрезу)
Доказательство. ∃ дополняющий путь ⇒ можно увеличить по нему 𝑓 ⇒ |𝑓 | = ̸ max.
Пусть нет дополняющего пути ⇒ dfs из 𝑠 по ненасыщенным рёбрам не посетит 𝑡. Множество
посещённых вершин обозначим 𝑆, обозначим 𝑇 = 𝑉 ∖ 𝑆. Из 𝑆 в 𝑇 ведут только 𝑒 : 𝑓𝑒 = 𝑐𝑒 .
Значит, |𝑓 | = 𝐹 (𝑆, 𝑇 ) = 𝐶(𝑆, 𝑇 ). Из леммы 2.4.8 следует, что |𝑓 | = max, 𝐶(𝑆, 𝑇 ) = min. ■
∙ Поиск минимального разреза
Из доказательства теоремы 2.4.9 мы заодно получили алгоритм за 𝒪(𝐸) поиска min разреза по
max потоку.
∙ Алгоритм Форда-Фалкерсона
Из теоремы следует простейший алгоритм поиска максимального потока: пока есть
дополняющий путь 𝑝, найдём его, толкнём по нему 𝑥 = min𝑒∈𝑝 (𝑐𝑒 −𝑓𝑒 ) единиц потока.
Утверждение 2.4.10. Если все 𝑐𝑒 ∈ Z, алгоритм конечен.
Время работы алгоритма мы умеем оценивать сверху только как 𝒪(|𝑓 | · 𝐸).
При 𝑐𝑒 ⩽ 𝑝𝑜𝑙𝑦𝑛𝑜𝑚(|𝑉 |, |𝐸|) получаем |𝑓 | ⩽ 𝑝𝑜𝑙𝑦𝑛𝑜𝑚(|𝑉 |, |𝐸|) ⇒ Ф.Ф. работает за полином.
При экспоненциально больших 𝑐𝑒 на практике мы построим тест: время работы Ω(2𝑉 /2 ).
2.5. Реализация, хранение графа
Первый способ хранения графа более естественный:
1 struct Edge {
2 int a , b , f , c , rev ; // a → b
3 };
4 vector < Edge > c [ n ]; // c[c[v][i].b][c[v][i].rev] – обратное ребро
5 for ( Edge e : c [ v ]) // перебор рёбер, смежных с 𝑣
6 ;
Второй часто работает быстрее, и позволяет проще обращаться к обратному ребру.
Поэтому про него поговорим подробнее.
1 struct Edge {
2 int a , b , f , c ; // собственно ребро
3 int next ; // интрузивный список, список на массиве
4 };
5 vector < Edge > edges ;
6 vector < int > head (n , -1) ; // для каждой вершин храним начало списка
7 for ( int i = head [ v ]; i != -1; i = edges [ i ]. next )
8 Edge e = edges [ i ]; // перебор рёбер, смежных с 𝑣
Глава #2. 27 сентября. 9/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Потоки (база)
Добавить орребро можно так:
1 void add (a , b , c ) :
2 edges . push_back ({ a , b , 0 , c }) ; // прямое
3 edges . push_back ({ b , a , 0 , 0}) ; // обратное
Заметим, что взаимообратные рёбра добавляются парами ⇒
∀𝑖 к edges[i] обратным является edges[iˆ1].
Теперь реализуем алгоритм Ф.Ф. Также, как и Кун, dfs, ищущий путь, сразу на обратном ходу
рекурсии будет изменять поток по пути.
1 bool dfs ( int v ) :
2 u [ v ] = 1;
3 for ( int i = head [ v ]; i != -1; i = edges [ i ]. next ) :
4 Edge & e = edges [ i ];
5 if ( e . f < e . c && ! u [ e . b ] && ( e . b == t || dfs ( e . b ) ) ) :
6 e . f ++ , edges [ i ^ 1]. f - -; // не забудьте пересчитать обратное ребро
7 return 1;
8 return 0;
По сути мы лишь нашли путь из 𝑠 в 𝑡 в остаточной сети 𝐺𝑓 .
Если мы хотим толкать не единицу потока, а min𝑒 (𝑐𝑒 − 𝑓𝑒 ), нужно, чтобы dfs на прямом ходу
рекурсии насчитывал минимум и возвращал из рекурсии полученное значение.
2.6. Паросочетание, вершинное покрытие
a4 b4 a4 b4 a4 b4
a3 b3 a3 b3 a3 b3
s t s t s t
a2 b2 a2 b2 a2 b2
a1 b1 a1 b1 a1 b1
Картинки: собственно сеть −→ какой-то поток в сети −→ дополняющий путь.
Чтобы с помощью потоков искать паросочетание, добавляем исток и сток, ориентируем рёбра
графа из первой доли во вторую. Пропускные способности «из истока» и «в сток» – единицы
(ограничение на суммарные поток через вершину). Между долями можно +∞, можно 1.
Алгоритм Форда-Фалекрсона работает за 𝒪(|𝑓 |𝐸) = 𝒪(|𝑀 |𝐸) ⩽ 𝒪(𝑉 𝐸).
Корректность. Следует из двух биекций: (1) между потоками в построенной нами сети и
паросочетаниями, (2) между дополняющими путями в нашей цепи и Ч.Д.П. в исходном графе.
∙ Что нового мы научились делать?
Алгоритм Диница поиска потока (см. дальше) сработает на такой сети быстрее: за 𝒪(𝐸𝑉 1/2 ).
Def 2.6.1. Мультисочетание – обобщение паросочетания, подмножество рёбер графа такое,
что для каждой вершины 𝑣 верно ограничение на максимальную степень 𝑤𝑣 .
Заменим пропускные способности «из истока», «в сток» на 𝑤𝑣 . Максимальный поток даст нам
максимальное мультисочетание. А алгоритм Диница найдёт его за 𝒪(𝐸 3/2 ).
Глава #2. 27 сентября. 10/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Потоки (база)
2.6.1. Вершинное покрытие
Существует ещё третья биекция: min разрез ←→ min вершинное покрытие.
a4 b4 a4 b4
a4 b4
a3 b3 a3 b3
a3 b3
s b2 t s b2 t
s t
a2 b2 a2 a2
a1 b1 a1 b1
a1 b1
Картинки: max поток −→ построенный по нему min разрез −→ min вершинное покрытие.
Рёбрам между долями сделаем 𝑐𝑒 = +∞ ⇒ min разрезе их, конечно, не будет.
Мы уже умеем строить min cover, используя dfs от Куна: 𝐶 = 𝐵 + ∪ 𝐴− .
Осталось заметить, что в построенном разрезе 𝑆 ⊔ 𝑇 по построению 𝑆 = {𝑠} ∪ 𝐴+ ∪ 𝐵 + .
Новое: научились искать взвешенное минимальное вершинное покрытие в двудольном графе.
Для этого поменяем пропускные способности рёбрам «из истока» и «в сток» на веса вершин.
2.7. Леммы, позволяющие работать с потоками
Lm 2.7.1. Условие 0 ⩽ 𝑓𝑒 ⩽ 𝑐𝑒 можно заменить на (𝑓𝑒 ⩽ 𝑐𝑒 ) ∧ (−𝑓𝑒 ⩽ 0).
То есть, и прямые, и обратные рёбра обладают пропускными способностями, ограничениями
сверху на поток, по ним текущий. А про ограничения снизу можно не думать.
Lm 2.7.2. 𝑓1 и 𝑓2 – потоки в 𝐺 ⇒ 𝑓2 − 𝑓1 – поток в 𝐺𝑓1 .
Доказательство. ∀ ребра 𝑒 имеем 𝑓2𝑒 ⩽ 𝑐𝑒 ⇒ 𝑓2𝑒 − 𝑓1𝑒 ⩽ 𝑐𝑒 − 𝑓1𝑒 .
Это∑︀верно и для прямых, и для обратных.
∑︀ Теперь∑︀
проверим сумму в вершине:
∀𝑣 𝑒∈𝑜𝑢𝑡(𝑣) (𝑓2𝑒 − 𝑓1𝑒 ) = ∀𝑣 ̸= 𝑠, 𝑡 𝑒∈𝑜𝑢𝑡(𝑣) 𝑓2𝑒 − ∀𝑣 𝑒∈𝑜𝑢𝑡(𝑣) 𝑓1𝑒 = 0 − 0 = 0. ■
Следствие 2.7.3. ∀𝑓1 , 𝑓2 : |𝑓1 |=|𝑓2 | ⇒ 𝑓2 можно получить из 𝑓1 добавлением циркуляции из 𝐺𝑓1 .
Lm 2.7.4. |𝑓2 − 𝑓1 | = |𝑓2 | − |𝑓1 |
Доказательство. Также, как в 2.7.2, распишем сумму для вершины 𝑠. ■
Lm 2.7.5. Если 𝑓2 – поток в 𝐺𝑓1 , 𝑓1 + 𝑓2 – поток в 𝐺
Lm 2.7.6. |𝑓1 + 𝑓2 | = |𝑓1 | + 𝑓2 |
2.8. Алгоритмы поиска потока
2.8.1. Эдмондс-Карп за 𝒪(𝑉 𝐸 2 )
Алгоритм прост: путь ищем bfs-ом, проталкиваем по пути min𝑒 (𝑐𝑒 − 𝑓𝑒 ). Конец.
Lm 2.8.1. После увеличения потока по пути, найденному bfs-ом,
для любой вершины 𝑣 расстояние от истока не уменьшится: ∀𝑣 𝑑[𝑠, 𝑣] = 𝑑[𝑣] ↗.
Глава #2. 27 сентября. 11/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Потоки (база)
Доказательство. От противного. Пусть после увеличения потока 𝑓 по пути 𝑝 расстояния
уменьшились. Расстояния в 𝐺𝑓 обозначим 𝑑0 , а в 𝐺𝑓 +𝑝 обозначим 𝑑1 .
Возьмём вершину 𝑣 : 𝑑1 [𝑣] < 𝑑0 [𝑣], а из таких 𝑑1 [𝑣] = min. Рассмотрим кратчайший путь 𝑞 из 𝑠
в 𝑣: 𝑠 ⇝ · · · ⇝ 𝑥 → 𝑣, 𝑑1 [𝑣] = 𝑑1 [𝑥] + 1. Поскольку 𝑣 – минимальная по 𝑑1 [𝑣] вершина из тех,
для кого расстояние уменьшилось, имеем 𝑑0 [𝑥] ⩽ 𝑑1 [𝑥].
Пусть ребро 𝑒 : 𝑥 → 𝑣. 𝑒 ∈ 𝑞, 𝑞 ∈ 𝐺𝑓 +𝑝 . Рассмотрим два случая: 𝑒 ∈ 𝐺, 𝑒 ̸∈ 𝐺.
1. 𝑒 ∈ 𝐺, кратчайшие путь 𝑝 ⩽ какой-то путь [*] ⇒ 𝑑0 [𝑣] ⩽ 𝑑0 [𝑥] + 1 ⩽ 𝑑1 [𝑥] + 1 = 𝑑1 [𝑣]. ?!?
2. 𝑒 ̸∈ 𝐺 ⇒ 𝑝 прошёл по обратному к 𝑒 ⇒ 𝑑0 [𝑣] = 𝑑0 [𝑥] − 1 ⩽ 𝑑1 [𝑥] − 1 = 𝑑1 [𝑣] − 2. ?!? ■
[*] – здесь и только здесь мы пользовались тем, что 𝑝 – кратчайший.
∙ Время работы Эдмондса-Карпа
Толкаем по пути мы всё ещё min𝑒 (𝑐𝑒 − 𝑓𝑒 ) ⇒ после каждого bfs-а хотя бы одно ребро насытится.
Чтобы ещё раз пройти по насыщенному ребру 𝑒, нужно сперва уменьшить по нему потока ⇒
пройти по обратному к 𝑒. Рассмотрим 𝑒 : 𝑎 → 𝑏, кратчайший путь прошёл через 𝑒 ⇒
𝑑[𝑏] = 𝑑[𝑎] + 1. Когда кратчайший путь пройдёт через обратное к 𝑒 имеем
2.8.1
𝑑′ [𝑎] = 𝑑′ [𝑏] + 1 ⩾ 𝑑[𝑏] + 1 = 𝑑[𝑎] + 2
Расстояние до 𝑎 между двумя насыщениями 𝑒 увеличится хотя бы на 2 ⇒ каждое ребро 𝑒
насытится не более 𝑉2 раз ⇒ суммарное число насыщений ⩽ 𝑉2𝐸 ⇒ Э.К. работает за 𝒪(𝑉 𝐸 2 )
Следствие 2.8.2. В графах с R пропускными способностями ∃ max поток.
Доказательство. В оценке времени работы Э.К. мы не пользовались целочисленностью. По
завершении Э.К. нет дополняющих путей ⇒ по 2.4.9 поток максимален. ■
2.8.2. Масштабирование за 𝒪(𝐸 2 log 𝑈 )
Будем пытаться сперва найти толстые пути:
перебирать 𝑘 ↓, искать пути в остаточной сети, по которым можно толкнуть хотя бы 2𝑘 .
Для этого dfs-у разрешим ходить только по рёбрам: 𝑓𝑒 + 2𝑘 ⩽ 𝑐𝑒 .
1 for k = logU .. 0:
2 u <-- 0
3 while dfs (s , 2 𝑘 ) :
4 u <-- 0
5 flow += 2 𝑘
Ф.Ф. искал любой путь в 𝐺𝑓 , мы ищем в 𝐺𝑓 пути толщиной 2𝑘 . Время работы 𝒪(𝐸 2 log 𝑈 ).
Алгоритм можно соптимизировать, толкая по пути не 2𝑘 , а min𝑒 (𝑐𝑒 −𝑓𝑒 ) ⩾ 2𝑘 .
Асимптотика в худшем случае не улучшится. На практике алгоритм ведёт себя как ≈ 𝐸 2 .
∙ Доказательство времени работы
Поток после фазы, на которой мы искали 2𝑘 -пути, обозначим 𝐹𝑘 . В остаточной сети 𝐺𝐹𝑘 нет
пути толщины 2𝑘 ⇒ есть разрез, для всех рёбер которого верно 𝑐𝑒 − 𝑓𝑒 < 2𝑘 .
Рассмотрим декомпозицию 𝐹𝑘−1 −𝐹𝑘 на пути. Все пути имеют толщину 2𝑘−1 , все проходят через
разрез ⇒ все по разным рёбрам разреза ⇒ их не больше, чем рёбер в разрезе ⩽ 𝐸.
Доказали, что путей при переходе от 𝐹𝑘 к 𝐹𝑘−1 не более 𝐸.
Глава #3. 27 сентября. 12/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Паросочетания (продолжение)
Лекция #3: Паросочетания (продолжение)
13 сентября
3.1. Классификация рёбер двудольного графа
Дан двудольный граф 𝐺 = ⟨𝑉, 𝐸⟩. Задача – определить ∀𝑒 ∈ 𝐸, ∃ ли максимальное
паросочетание 𝑀1 : 𝑒 ∈ 𝑀1 , а также ∃ ли максимальное паросочетание 𝑀2 : 𝑒 ̸∈ 𝑀2 .
Иначе говоря, мы хотим разбить рёбра на три класса:
1. Должно лежать в максимальном паросочетании. MUST.
2. Не лежит ни в каком максимальном паросочетании. NO.
3. Все остальные. MAY.
Решение: для начала найдём любое максимальное паросочетание 𝑀 .
Если мы найдём класс MAY, то MUST = 𝑀 ∖ MAY, NO = 𝑀 ∖ MAY.
Lm 3.1.1. 𝑒 ∈ MAY ⇔ ∃𝑃 – чередующийся относительно 𝑀 путь чётной длины или чётный
цикл такой, что 𝑒 ∈ 𝑃 .
Доказательство. 𝑒 ∈ MAY ⇒ ∃ другое max паросочетание 𝑀 ′ : 𝑒 ∈ 𝑀 ▽𝑀 ′ .
Симметрическая разность, как мы уже знаем, состоит из чередующихся путей и циклов.
Посмотрим с другой стороны: если относительно 𝑀 есть 𝑃 – чередующийся путь чётной длины
или чередующийся цикл, то 𝑃 ⊆ MAY, так как и 𝑀 , и 𝑀 ▽𝑃 являются максимальными, а каждое
ребро 𝑃 лежит ровно в одном из двух. ■
Осталось научиться находить циклы и пути алгоритмически. Для этого рассмотрим тот же
граф 𝐺′ , на котором работает dfs из Куна. С чётными путями всё просто: все они начинаются в
свободных вершинах, dfs из Куна, запущенный от всех свободных вершин обеих долей пройдёт
ровно по всем рёбрам, которые можно покрыть чётными путями, и пометит их. А про циклы:
Lm 3.1.2. Ребро 𝑒 лежит на чётном цикле ⇔
концы 𝑒 лежат в одной компоненте сильной связности графа 𝐺′ .
Следствие 3.1.3. Получили алгоритм построения MAY по данному 𝑀 за 𝒪(𝑉 + 𝐸).
Следствие 3.1.4. Из MAY и 𝑀 за 𝒪(𝐸) умеем получить MUST и NO.
3.2. Stable matching (marriage problem)
Сформулируем задачу на языке мальчиков/девочек. Есть 𝑛 мальчиков, у каждого из них есть
список девочек 𝑏𝑠[𝑎], которые ему нравятся в порядке от наиболее приоритетных к менее. Есть
𝑚 девочек, у каждой есть список мальчиков 𝑎𝑠[𝑏], которые ей нравятся в таком же порядке.
Мальчики и девочки хотят образовать пары.
Никто не готов образовывать пару с тем, кто вообще отсутствует в его списке.
И для мальчиков, и для девочек наименее приоритетный вариант – остаться вообще без пары.
Будем обозначать 𝑝𝑎 – пара мальчика 𝑎 или −1, 𝑞𝑏 – пара девочки 𝑞 или −1.
Def 3.2.1. Паросочетание называется не стабильным, если ∃ мальчик 𝑎 и девочка 𝑏:
мальчику 𝑎 нравится 𝑏 больше чем 𝑝𝑎 и девочке 𝑏 нравится 𝑎 больше чем 𝑞𝑏 .
Def 3.2.2. Иначе паросочетание называется стабильным
Глава #3. 13 сентября. 13/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Паросочетания (продолжение)
∙ Алгоритм поиска: «мальчики предлагают, девочки отказываются»
Изначально проинициализируем 𝑝𝑎 = 𝑏𝑠[𝑎].best, далее, пока есть
два мальчика 𝑖, 𝑗 : 𝑏 = 𝑝𝑖 = 𝑝𝑗 ̸= −1, Девочка 𝑏 откажет тому из них, кто ей меньше нравится.
Пусть она отказала мальчику 𝑖, тогда делаем 𝑏𝑠[𝑖].remove_best(), 𝑝𝑖 = 𝑏𝑠[𝑖].best.
Теорема 3.2.3. Алгоритм всегда завершится и найдёт стабильное паросочетание
Доказательство. Длины списков 𝑏𝑠[𝑖] убывают ⇒ завершится. Пусть в конце мальчик 𝑎 и
девочка 𝑏 дают не стабильность (не являются парой, но хотят образовать). Это значит, что 𝑎
перед тем, как образовать пару с 𝑝𝑎 , предлагал себя 𝑏, а она ему отказала.
Но зачем?! Ведь он ей нравился больше. Противоречие. ■
Аккуратная реализация даёт время 𝒪(𝑉 + 𝐸) = 𝒪( 𝑖 |𝑏𝑠[𝑖]| + 𝑗 |𝑎𝑠[𝑗]|).
∑︀ ∑︀
1 def ask ( boyIndex ) : # мальчик предлагает себя
2 girlIndex = priorityList [ boyIndex ]. pop_best () # кому?
3 if ( q [ girlIndex ] == -1) : q [ girlIndex ] = boyIndex ;
4 else :
5 # нужно ещё сделать, чтобы priority работала за 𝒪(1)
6 if priority ( girlIndex , q [ girlIndex ]) < prioirity ( girlIndex , boyIndex ) :
7 swap ( p [ girlIndex ] , boyIndex )
8 # в boyIndex сейчас худший из двух вариантов, ему откажем, он предложит следующей
9 ask ( boyIndex ) ;
10 for b in boys : ask ( b ) # собственно алгоритм
Утверждение 3.2.4. На практике докажем, что предложенный алгоритм оптимален для маль-
чиков и предложим версию, оптимальную для девочек.
Теорема 3.2.5. Если граф полный (каждый список содержит все вершины) и размеры долей
равны, мы найдём совершенное паросочетание.
Доказательство. Пусть какому-то мальчику отказали все 𝑛 девочек. После отказа у девочки
всегда остаётся лучший кандидат ⇒ сейчас у всех девочек одновременно есть кандидаты = 𝑛
разных мальчиков ⇒ всего ⩾ 𝑛+1 мальчик ?!? ■
Пример использования.
Студенты хотят поступить в ВУЗы. У каждого ВУЗа ограниченное число мест и могут быть
разные приоритеты, кого хотят брать. Строим двудольный граф. Каждый студент указывает
список ВУЗов в порядке приоритетов, куда хочет поступить. Каждое место-в-вузе получает
список студентов в порядке приоритета данного ВУЗа. Ищем стабильное паросочетание, полу-
чаем решение, оптимальное для студентов.
3.3. (*) Венгерский алгоритм
Дан взвешенный двудольный граф, заданный матрицей весов 𝑎 : 𝑛 × 𝑛, где 𝑎𝑖𝑗 – вес ребра из 𝑖-й
вершины первой доли в 𝑗-ю вершину второй доли. Задача – найти совершенное паросочетание
минимального веса.
Формально: найти 𝜋 ∈ 𝑆𝑛 :
∑︀
𝑖 𝑎𝑖𝜋𝑖 → min.
Иногда задачу называют задачей о назначениях, тогда 𝑎𝑖𝑗 – стоимость выполнения 𝑖-м работ-
ником 𝑗-й работы, нужно каждому работнику сопоставить одну работу.
Глава #3. 13 сентября. 14/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Паросочетания (продолжение)
Lm 3.3.1. Если 𝑎𝑖𝑗 ⩾ 0 и ∃ совершенное паросочетание на нулях, оно оптимально.
Lm 3.3.2. Рассмотрим матрицу 𝑎′𝑖𝑗 = 𝑎𝑖𝑗 + 𝑟𝑜𝑤𝑖 + 𝑐𝑜𝑙𝑗 , где 𝑟𝑜𝑤𝑖 , 𝑐𝑜𝑙𝑗 ∈ R.
Оптимальные паросочетания для 𝑎′ и для 𝑎 совпадают.
Доказательство. Достаточно заметить, что в (𝑓 = 𝑖 𝑎𝑖𝜋𝑖 ) войдёт ровно по одному элементу
∑︀
каждой строки, каждого столбца ⇒ если все элементы строки (столбца) увеличить на констан-
ту 𝐶, в не зависимости от 𝜋 величина 𝑓 увеличится на 𝐶 ⇒ оптимум перейдёт в оптимум. ■
Венгерский алгоритм, как Кун, перебирает вершины первой доли и от 𝐵+ 𝐵−
каждой пытается строить ДЧП, но использует при этом только нулевые −𝑥
𝐴+
рёбра. Если нет нулевого ребра, то 𝑥 = min на 𝐴+ × 𝐵 − > 0. Давайте
все столбцы из 𝐵 − уменьшим на 𝑥, а все строки из 𝐴− увеличим на 𝑥. 𝐴− +𝑥 0
В итоге в подматрице 𝐴+ × 𝐵 − на месте минимального элемента появится 0, в матрице 𝐴− ×
𝐵 + элементы увеличатся, остальные не изменятся. При этом все элементы матрицы остались
неотрицательными. Рёбра из 𝐴− × 𝐵 + могли перестать быть нулевыми, но они не лежат ни в
текущем паросочетании, ни в дереве дополняющих цепей: 𝑀 ⊆ (𝐴− × 𝐵 − ) ∪ (𝐴+ × 𝐵 + ), рёбра
дополняющих цепей идут из 𝐴+ .
3.3.1. (*) Реализация за 𝒪(𝑉 3 )
Венгерский алгоритм = 𝑉 поисков ДЧП.
Поиск ДЧП = инициализировать 𝐴+ = 𝐵 + = ∅ и не более 𝑉 раз найти минимум 𝑥 = 𝑎𝑖𝑗 в
𝐴+ × 𝐵 − . Если 𝑥 > 0, то пересчитать матрицу весов. Посетить столбец 𝑗 и строку 𝑝𝑎𝑖𝑟𝑗 .
Чтобы быстро увеличивать столбец/строку на константу, поддерживаем 𝑟𝑜𝑤𝑖 , 𝑐𝑜𝑙𝑗 .
Реальное значение элемента матрицы: 𝑎′𝑖𝑗 = 𝑎𝑖𝑗 + 𝑟𝑜𝑤𝑖 + 𝑐𝑜𝑙𝑗 . Увеличение строки на 𝑥 : 𝑟𝑜𝑤𝑗 += 𝑥.
Чтобы найти минимум 𝑥, а также строку 𝑖, столбец 𝑗, на которых минимум достигается, вос-
пользуемся идеей из алгоритма Прима: 𝑤𝑗 = min+ ⟨𝑎′𝑖𝑗 , 𝑖⟩. Тогда ⟨⟨𝑥, 𝑖⟩, 𝑗⟩ = min− ⟨𝑤𝑗 , 𝑗⟩.
𝑖∈𝐴 𝑗∈𝐵
Научились находить (𝑥, 𝑖, 𝑗) за 𝒪(𝑛), осталось при изменении 𝑟𝑜𝑤𝑖 , 𝑐𝑜𝑙𝑗 пересчитать 𝑤𝑗 : 𝑗 ∈ 𝐵 − .
𝑐𝑜𝑙𝑗 += 𝑦 ⇒ 𝑤𝑘 += 𝑦. А 𝑟𝑜𝑤𝑖 будет меняться только у 𝑖 ∈ 𝐴− ⇒ на min𝑖∈𝐴+ не повлияет.
Замечание 3.3.3. Можно выбирать min в множестве 𝑤𝑗 : 𝑗 ∈ 𝐵 − не за линию, а используя кучи.
3.3.2. (*) Псевдокод
Обозначим, как обычно, первую долю 𝐴, вторую 𝐵, посещённые вершины – 𝐴+ , 𝐵 + .
Также, как в Куне, если 𝑥 ∈ 𝐵, то pair[𝑥] ∈ 𝐴 – её пара в первой доли.
Строки – вершины первой доли (𝐴). В нашем коде строки – 𝑖, 𝑣, 𝑢.
Столбцы – вершины второй доли (𝐵). В нашем коде столбцы – 𝑗.
pair[𝑏 ∈ 𝐵] – её пара в 𝐴, pair2[𝑎 ∈ 𝐴] – её пара в 𝐵.
1. row ← 0, col ← 0
2. for v ∈ A
3. 𝐴+ = {𝑣}, 𝐵 + = ∅ // (остальное в 𝐴− , 𝐵 − ).
4. for j ∈ 𝐵 − : w[j] = (a[v][j] + row[v] + col[j], v) // (минимум и номер строки)
5. while (True) // (пока не нашли путь из v в свободную вершину 𝐵)
6. ((x,i),j) = min {(w[j],j) : 𝑗 ∈ 𝐵 − } // (минимум и позиция минимума в 𝐴+ × 𝐵 − )
7. // (i – номер строки, j – номер столбца ⇒ a[i,j] + row[i] + col[j] == x)
8. for i ∈ 𝐴− : row[i] += x;
Глава #3. 13 сентября. 15/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Паросочетания (продолжение)
9. for j ∈ 𝐵 − : col[j] -= x, w[j].value -= x;
10. // (в итоге мы уменьшили 𝐴+ × 𝐵 − , увеличили 𝐴− × 𝐵 + , пересчитали w[j]).
11. j перемещаем из 𝐵 − в 𝐵 + ; запоминаем prev[j] = i;
12. if (u=pair[j]) == -1
13. break // дополняющий путь: j,prev[j],pair2[prev[j]],prev[pair2[prev[j]]],...
14. u перемещаем из 𝐴− в 𝐴+ ;
15. пересчитываем все w[j] = min(w[j], pair(a[u][j] + row[u] + col[j], u));
16. применим дополняющий путь 𝑣 ⇝ 𝑗, пересчитаем pair[], pair2[]
Текущая реализация даёт время 𝒪(𝑉 3 ).
Внутри цикла while строки 6, 8, 9, 15 работают за 𝒪(𝑉 ) каждая.
Из них 8 и 9 улучшить до 𝒪(1), храня специальные величины addToAMinus, addToBMinus.
Строки 6 и 15 можно улучшить до ⟨𝒪(log 𝑉 ), 𝑑𝑒𝑔[𝑥] · 𝒪(1)⟩, применив кучу Фибоначчи.
Итого получится 𝒪(𝑉 (𝐸 + 𝑉 log 𝑉 )).
Глава #3. 13 сентября. 16/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Паросочетания (продолжение)
3.4. (*) Покраска графов
3.4.1. (*) Вершинные раскраски
Задача: покрасить вершины графа так, чтобы любые смежные вершины имели разные цвета.
В два цвета (двудольный граф) красит обычный dfs за 𝒪(𝑉 + 𝐸).
В три цвета красить NP-трудно. В прошлом семестре мы научились это делать за 𝒪(1.44𝑛 ).
Во сколько цветов можно покрасить вершины за полиномиальное время?
∙ Жадность
Удалим вершину 𝑣 из графа → покрасим рекурсивно 𝐺 ∖ {𝑣} → докрасим 𝑣.
У вершины 𝑣 всего 𝑑𝑒𝑔𝑣 уже покрашенных соседей ⇒
в один из 𝑑𝑒𝑔𝑣 + 1 цветов мы её точно сможем покрасить.
Следствие 3.4.1. Вершины можно покрасить в 𝐷+1 цвет за 𝒪(𝑉 + 𝐸), где 𝐷 = max𝑣 𝑑𝑒𝑔𝑣 .
На дискретной математике будет доказана более сильная теорема:
Теорема 3.4.2. Брукс: все графы кроме нечётных циклов и клик можно покрасить в 𝐷 цветов.
Кроме теоремы есть алгоритм покраски в 𝐷 цветов за 𝒪(𝑉 + 𝐸).
На практике, если удалять вершину 𝑣 : 𝑑𝑒𝑔𝑣 = min и докрашивать её в минимально возможный
цвет, жадность будет давать приличные результаты. За счёт потребности выбирать вершину
именно минимальной степени нам потребуется куча, время возрастёт до 𝒪((𝐸 + 𝑉 ) log 𝑉 ).
Замечание 3.4.3. Иногда про покраску вершин удобно думать, как про разбиение множества
вершин на независимые множества.
3.4.2. (*) Вершинные раскраски планарных графов
Очередная Теорема Эйлера гласит, что для планарного графа 𝐸 ⩽ 3 · 𝑉 − 6 ⇒ есть вершина
степени ⩽ 5 ⇒ жадность выше всегда красит в ⩽ 6 цветов.
Можно тем же способом покрасить и в 5. Пусть мы докрашиваем вершину 𝑣, а у неё 5 разно-
цветных соседей. Воспользуемся планарностью, упорядочим их по часовой стрелке и заметим,
что не могут одновременно существовать два непересекающихся пути 1 ⇝ 3, 2 ⇝ 4. Поэтому
попробуем сперва поменять цвета 𝑐[1] ↔ 𝑐[3], для этого dfs-ом из 1 перебирая только вершины
цветов 𝑐[1], 𝑐[3] попробуем найти путь в 3. Если не нашли, перекрасим. Если нашли, то сделаем
тоже самое для {2, 4}, там пути точно не будет. После успешного перекрашивания у соседей
всего 4 разных цвета, есть 5-й, чтобы докрасить себя.
Мы не хотим укладывать граф, как нам угадать, порядок вершин? Просто переберём все 5·4
2
пар соседей {𝑢, 𝑣}. Для какой-то из них не будет пути и получится перекрасить.
3.4.3. (*) Рёберные раскраски
Задача: покрасить рёбра графа так, чтобы любые смежные рёбра имели разные цвета.
Попробуем для начала применить ту же жадность: удаляем ребро 𝑒 из графа, рекурсивно
красим рёбра в 𝐺 ∖ {𝑒}, докрасим 𝑒. У ребра 𝑒 может быть 2(𝐷−1) смежных, где 𝐷 = max𝑣 𝑑𝑒𝑔𝑣 .
Значит, чтобы ребро 𝑒 всегда получалось докрасить, в худшем, нашей жадности нужен 2𝐷−1
цвет, С другой стороны, поскольку рёбра, инцидентные одной вершине, должны иметь попарно
разные цвета, Есть гораздо более сильный результат, который также подробнее будет изучен в
курсе дискретной математики.
Глава #3. 13 сентября. 17/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Паросочетания (продолжение)
Теорема 3.4.4. Визинг: рёбра любого графа можно покрасить в 𝐷+1 цвет.
Доказательство теоремы представляет собой алгоритм покраски в 𝐷+1 цвет за 𝒪(𝑉 𝐸).
При этом задача «определить, можно ли покрасить в 𝐷 цветов» NP-трудна.
3.4.4. (*) Рёберные раскраски двудольных графов
С двудольными графами всё проще. Сейчас научимся красить их в 𝐷 цветов.
Покраска рёбер – разбиения множества рёбер на паросочетания. На последней практике мы
доказали, что в двудольном регулярном графе существует совершенное паросочетание.
Следствие 3.4.5. 𝑑-регулярный граф можно покрасить в 𝑑 цветов.
Доказательство. Отщепим совершенное паросочетание, покрасим его в первый цвет. Остав-
шийся граф является (𝑑−1)-регулярным, по индукции его можно покрасить в 𝑑−1 цвет. ■
Чтобы покрасить не регулярный граф, дополним его до 𝐷-регулярного.
∙ Дополнение до регулярного
1. Если в долях неравное число вершин, добавим новые вершины.
2. Пока граф не является 𝐷-регулярным (𝐷 – максимальная степень),
в обеих долях есть вершины степени меньше 𝐷, соединим эти вершины ребром.
3. В результате мы получим 𝐷-регулярный граф, возможно, с кратными рёбрами.
Кратные рёбра – это нормально, все изученные нами алгоритмы их не боятся.
Итого рёбра 𝑑-регулярного двудольного граф мы умеем красить за 𝒪(𝑑·Matching) = 𝒪(𝑑𝑉 𝐸) =
𝒪(𝐸 2 ), а рёбра произвольного двудольного за 𝒪(𝐷 · 𝑉 · 𝑉 𝐷) = 𝒪(𝑉 2 𝐷2 ).
Поскольку в полученном регулярном графе есть совершенное паросочетание, мы доказали:
Следствие 3.4.6. Для ∀ двудольного 𝐺 ∃ паросочетание, покрывающее все вершины 𝐺 (в обеих
долях) максимальной степени.
Раз такое паросочетание ∃, его можно попробовать найти, не дополняя граф до регулярного.
3.4.5. (*) Покраска не регулярного графа за 𝒪(𝐸 2 )
Обозначим 𝐴𝐷 – вершины степени 𝐷 первой доли, 𝐵𝐷 – вершины степени 𝐷 второй доли.
Уже знаем, что ∃ паросочетание 𝑀 , покрывающее 𝐴𝐷 ∪ 𝐵𝐷 .
1. Запустим Куна от вершин 𝐴𝐷 , получили паросочетание 𝑃 .
Обозначим 𝐵𝑃 покрытые паросочетанием 𝑃 вершины второй доли.
2. Если 𝐵𝐷 ̸⊆ 𝐵𝑃 , чтобы покрыть 𝑋 = 𝐵𝐷 ∖ 𝐵𝑃 рассмотрим 𝑀 ▽𝑃 . Каждой вершине
из 𝑋 в 𝑀 ▽𝑃 соответствует или ДЧП, или чётный путь из 𝑋 в 𝑌 = 𝐵𝑃 ∖ 𝐵𝐷 .
3. Алгоритм: для всех 𝑣 ∈ 𝑋 ищем путь или в свободную вершину первой доли, или в 𝑌 .
Чтобы оценить время работы, обозначим размер найденного паросочетания 𝑘𝑖 и заметим, что
нашли∑︀мы его за 𝒪(𝑘𝑖 𝐸). Все 𝑘𝑖 рёбер паросочетания∑︀
будут покрашены и удалены из графа, то
есть, 𝑖 𝑘𝑖 = 𝐸. Получаем время работы алгоритма 𝑖 𝒪(𝑘𝑖 𝐸) = 𝒪(𝐸 2 ).
Глава #4. 13 сентября. 18/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Потоки (быстрые)
Лекция #4: Потоки (быстрые)
10 октября
4.1. Алгоритм Диница
У нас уже есть алгоритм Эдмондса-Карпа, ищущий 𝒪(𝑉 𝐸) путей за 𝒪(𝐸) каждый.
Построим сеть кратчайших путей, расстояние 𝑠 ⇝ 𝑡 обозначим 𝑑.
Слоем 𝐴𝑖 будем называть вершины на расстоянии 𝑖 от 𝑠.
Э.К. сперва найдёт сколько-то путей длины 𝑑, затем расстояние 𝑠 ⇝ 𝑡 увеличится.
Lm 4.1.1. Пока ∃ путь длины 𝑑, он имеет вид 𝑠 = 𝑣0 ∈ 𝐴0 , 𝑣1 ∈ 𝐴1 , 𝑣2 ∈ 𝐴2 , . . . , 𝑡 = 𝑣𝑑 ∈ 𝐴𝑑
Доказательство. В первый момент это очевидно. Затем в 𝐺𝑓 будут появляться новые рёбра
– обратные к 𝑣𝑖 → 𝑣𝑖+1 . Все такие рёбра идут назад по слоям ⇒ новых рёбер идущих вперёд
по слоям не образуется ⇒ каждый раз при поиске пути единственный способ за 𝑑 шагов из 𝑠
попасть в 𝑡 – 𝑑 раз по одному из старых рёбер идти ровно в следующий слой. ■
Научимся искать все пути длины 𝑑 за 𝒪(𝐸 + 𝑑𝑘𝑑 ), где 𝑘𝑑 – количество путей.
Выделим множество 𝐸 ′ – рёбра 𝐴𝑖 → 𝐴𝑖+1 в 𝐺𝑓 . Будем запускать dfs по 𝐸 ′ .
Модифицируем dfs: если пройдя по рёбру 𝑒, dfs не нашёл путь до 𝑡, он∑︀удалит 𝑒 из 𝐸 ′ .
Каждый из 𝑘𝑑 dfs-ов сделал 𝑑 успешных шагов и 𝑥𝑖 неуспешных, но 𝑥𝑖 ⩽ 𝐸, так как после
каждого неуспешного шага мы удаляем ребро из 𝐸 ′ .
1 void dfs ( int v ) {
2 while ( head’[ v ] != -1) {
3 Edge & e = edges [ head’[ v ]];
4 if ( e . f < e . c && ( e . b == t || dfs ( e . b ) ) ) {
5 // нашли путь
6 }
7 head’[ v ] = e . next ;
8 }
9 }
Заметьте, что массив пометок вершин, обычный для любого dfs, тут можно не использовать.
∙ Алгоритм Диница
Состоит из фаз вида:
(1) запустить bfs, который построил слоистую сеть и нашёл 𝑑.
(2) пока в слоистой сети есть путь длины 𝑑,
найдём его dfs-ом и толкнём по нему min𝑒 (𝑐𝑒 − 𝑓𝑒 ) единиц потока.
Теорема 4.1.2. Время работы алгоритма Диница 𝒪(𝑉 2 𝐸).
Доказательство. Фаз всего не более 𝑉 штук, так как после каждой 𝑑 ↑
Фаза с расстоянием 𝑑 работает за 𝒪(𝐸 + 𝑑 · 𝑘𝑑 ).
∑︁ ∑︁
(𝐸 + 𝑑 · 𝑘𝑑 ) ⩽ 𝑉 𝐸 + 𝑉 𝑘𝑑 ⩽ 𝑉 𝐸 + 𝑉 (𝑉 𝐸)
Последнее известно из алгоритма Эдмондса-Карпа. ■
Глава #4. 10 октября. 19/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Потоки (быстрые)
∙ Алгоритм Диница с масштабированием потока
Масштабирование потока – не только конкретный алгоритм, но и общая идея:
1 for ( int k = log U ; k >= 0; k - -)
2 пускаем поток на графе с пропускными способностями (𝑐𝑒 − 𝑓𝑒 )/2𝑘
Давайте искать поток именно алгоритмом Диница.
Теорема 4.1.3. Время работы алгоритма Диница с масштабированием 𝒪(𝑉 𝐸 log 𝑈 ).
Доказательство. Каждая из фаз масштабирование – алгоритм Диница, который найдёт не
более 𝐸 путей и работает за время (делаем то же, что и в теореме 4.1.2):
∑︁ ∑︁
(𝐸 + 𝑑 · 𝑘𝑑 ) ⩽ 𝑉 𝐸 + 𝑉 𝑘𝑑 ⩽ 𝑉 𝐸 + 𝑉 𝐸 = 𝒪(𝑉 𝐸)
Значит, суммарно все log 𝑈 фаз отработают за 𝒪(𝑉 𝐸 log 𝑈 ). ■
4.2. Алгоритм Хопкрофта-Карпа
Lm 4.2.1. В единичной сети (𝑐𝑒 ≡ 1) фаза Диниц работает за 𝒪(𝐸)
Доказательство. Если dfs пройдёт по ребру 𝑒, он его в любом случае удалит – и если не найдёт
по нему путь, и если найдёт по нему путь: (𝑐𝑒 = 1) ⇒ 𝑒 насытится. ■
Мы уже умеем искать паросочетание за 𝒪(𝑉 𝐸) через потоки.
Давайте в том же графе запустим алгоритм Диница.
√
Теорема 4.2.2. Число фаз Диница на сети для поиска паросочетание не более 2 𝑉 .
√
Доказательство. Сделаем первые 𝑉 фаз, получили √ поток 𝑓 , посмотрим на 𝐺𝑓 .
В остаточной сети все пути имеют длину хотя бы 𝑉 . Вспомним биекцию
√ между доппутями в
𝐺𝑓 и ДЧП для паросочетания ⇒ все ДЧП тоже имеют длину хотя бы 𝑉 .
Пусть поток 𝑓 задаёт паросочетание 𝑃 , рассмотрим максимальное 𝑀 . √
𝑀 ▽𝑃√ содержит 𝑘 = |𝑀 | − |𝑃 | неперескающихся
√ ДЧП, каждый длины ⩾ 𝑉 ⇒
𝑘 ⩽ 𝑉 ⇒ Динице осталось найти ⩽ 𝑉 путей.
Осталось заметить, что за каждую фазу Диниц находит хотя бы один путь. ■
∙ Алгоритм Хопкрофта-Карпа
1 void dfs ( int v ) :
2 for ( x ∈ N ( v ) ) : // 𝑥 – сосед во второй доле
3 if ( used2 [ x ]++ == 0) // проверили, что в 𝑥 попадаем впервые
4 if ( pair2 [ x ] == -1 || ( dist [ pair2 [ x ]] == dist [ v ]+1 && dfs ( pair2 [ x ]) ) :
5 pair1 [ v ] = x , pair2 [ x ] = v
6 return 1
7 return 0
8 while ( bfs нашёл путь свободной в свободную) : // цикл по фазам
9 used2 <-- 0 // пометки для вершин второй доли
10 for ( v ∈ A ) : // вершины первой доли
11 if ( pair1 [ v ] == -1) : // вершина свободна
12 dfs ( v )
∀ вершины 𝑣 второй доли в 𝐺𝑓 из 𝑣 исходит не более одного ребра.
Для свободной вершины это ребро в сток 𝑡, для несвободной в её пару в первой доле.
Значит, заходить в 𝑣 dfs-ам одной фазы Диница имеет смысл только один раз.
hДавайте вместо «удаления рёбер» помечать вершины второй доли. Посмотрим на происходя-
щее, как на поиск ДЧП для паросочетания. Поймём, что сток с истоком нам особо не нужны...
Глава #4. 10 октября. 20/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Потоки (быстрые)
4.3. Теоремы Карзанова
Определим пропускную способность вершины:
∑︁ ∑︁
𝑐[𝑣] = min(𝑐𝑖𝑛 [𝑣], 𝑐𝑜𝑢𝑡 [𝑣]), где 𝑐𝑖𝑛 [𝑣] = 𝑐𝑒 , 𝑐𝑜𝑢𝑡 [𝑣] = 𝑐𝑒
𝑒∈𝑖𝑛[𝑣] 𝑒∈𝑜𝑢𝑡[𝑣]
√
Теорема 4.3.1. Число фаз алгоритма Диницы не больше 2 𝐶, где = 𝑣 𝑐[𝑣].
∑︀
Доказательство. Заметим, что ∀ потока 𝑓 и вершины
√ 𝑣 ̸= 𝑠, 𝑡 величины 𝑐𝑖𝑛 [𝑣], 𝑐𝑜𝑢𝑡 [𝑣], 𝑐[𝑣] равны
значениям в исходном графе. Запустим первые 𝐶 фаз, получим поток 𝑓0 , пусть |𝑓 * | = max,
рассмотрим
√ декомпозицию 𝑓 * − 𝑓0 . Она состоит из 𝑘 = |𝑓 * | − |𝑓0 | единичных путей длины хотя
бы 𝐶 (не считая 𝑠 и 𝑡). Обозначим 𝛼𝑣 – сколько путей проходят через 𝑣. Тогда:
√ ∑︀ ∑︀ √
𝑘 𝐶 ⩽ 𝑣̸=𝑠,𝑡 𝛼𝑣 ⩽ 𝑣̸=𝑠,𝑡 𝑐[𝑣] = 𝐶 ⇒ 𝑘 ⩽ 𝐶
√ √
Получили, что число фаз ⩽ 𝐶 + 𝑘 ⩽ 2 𝐶. ■
Следствие 4.3.2. Из теоремы следует время работы Хопкрофта-Карпа.
Утверждение 4.3.3. В единичных сетях 𝐶 ⩽ 𝐸 ⇒ алгоритм Диница работает за 𝒪(𝐸 3/2 ).
Теорема 4.3.4. Число фаз алгоритма Диницы не больше 2𝑈 1/3 𝑉 2/3 , где 𝑈 = max𝑒 𝑐𝑒 .
Доказательство. Запустим первые 𝑘 фаз (оптимальное 𝑘 выберем позже), на (𝑘+1)-й получим
слоистую сеть из ⩾ 𝑘+1 слоёв. Обозначим размеры слоёв 𝑎0 , 𝑎1 , 𝑎2 , . . . , 𝑎𝑘 .
Тогда величина min разреза не более min𝑖=1..𝑘 (𝑎𝑖−1 𝑎𝑖 𝑈 ).
Максимум такого минимума достигается при 𝑎1 = 𝑎2 = . . . 𝑎𝑘 = 𝑉𝑘 .
Получили разрез размера 𝑈 ( 𝑉𝑘 )2 ⇒ осталось не более чем столько фаз ⇒
всего фаз не более 𝑓 (𝑘) = 𝑘 + 𝑈 ( 𝑉𝑘 )2 . 𝑘 ↑, 𝑈 ( 𝑉𝑘 )2 ↓.
Асимптотический минимум 𝑓 достигается при 𝑘 = 𝑈 ( 𝑉𝑘 )2 ⇒ 𝑘 3 = 𝑈 𝑉 2 , число фаз ⩽ 2(𝑈 𝑉 2 )1/3 .
■
4.4. Диниц с link-cut tree
Улучшим время одной фазы алгоритмы Диница с 𝒪(𝑉 𝐸) до 𝒪(𝐸 log 𝑉 ).
Построим остовное дерево с корнем в 𝑡 по входящим не насыщенным рёбрам.
Теперь 𝐸 раз пускаем поток, по пути дерева 𝑠 ⇝ 𝑡 и перестраиваем дерево.
Для этого находим на пути 𝑠 ⇝ 𝑡 любое одно насытившееся ребро 𝑎 → 𝑏, разрезаем его, и для
вершины 𝑎 добавляем в дерево следующее ребро из 𝑜𝑢𝑡[𝑎]. Цикла появиться не может: рёбра
идут вперёд по слоям. Зато у 𝑎 могли просто кончиться рёбра, тогда 𝑎 объявляем тупиковой
веткой развития, и рекурсивно разрезаем ребро, входящее в 𝑎.
Заметим, что link-cut-tree со splay-tree умеет делать все описанные операции за 𝒪(log 𝑉 ):
• Поиск минимума и позиции минимума величины 𝑐𝑒 − 𝑓𝑒 на пути.
• Уменьшение величины 𝑐𝑒 − 𝑓𝑒 на пути.
• Разрезание ребра (cut), проведение нового ребра (link).
Один cut может рекурсивно удалить много рёбер, сильно перестроить дерево. Несмотря на это
каждое ребро удалится не более одного раза ⇒ суммарное время всех cut – 𝒪(𝐸 log 𝑉 ).
Глава #4. 10 октября. 21/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Потоки (быстрые)
4.5. Глобальный разрез
Задача: найти разбиение 𝑉 = 𝐴 ⊔ 𝐵 : 𝐴, 𝐵 ̸= ∅, 𝐶(𝐴, 𝐵) → min.
Простейшее решение: переберём 𝑠, 𝑡 и найдём разрез между ними. Конечно, можно взять 𝑠 = 1.
Время работы 𝒪(𝑉 · 𝐹 𝑙𝑜𝑤).
На практике покажем, что на единичных сетях эта идея работает уже за 𝒪(𝐸 2 ).
4.5.1. Алгоритм Штор-Вагнера
Выберем 𝑎1 = 1. Пусть 𝐴𝑖 = {𝑎1 , 𝑎2 , . . . , 𝑎𝑖 }. Определим 𝑎𝑖+1 = 𝑣 ∈ 𝑉 ∖ 𝐴𝑖 : 𝐶(𝐴𝑖 , {𝑣}) = max.
Утверждение 4.5.1. Минимальный разрез между 𝑎𝑛 и 𝑎𝑛−1 – 𝑆 = {𝑎𝑛 }, 𝑇 = 𝑉 ∖ 𝑆, где 𝑛 = |𝑉 |.
Доказательство. Можно прочесть на e-maxx. ■
Алгоритм: или 𝑎𝑛 и 𝑎𝑛−1 по разные стороны оптимального глобального разреза, и ответ равен
𝐶({𝑎𝑛 }, 𝑉 ∖ {𝑎𝑛 }), или 𝑎𝑛 и 𝑎𝑛−1 можно стянуть в одну вершину.
Время работы: 𝑉 фаз, каждая за 𝒪(𝐷𝑖𝑗𝑘𝑠𝑡𝑟𝑎) ⇒ 𝒪(𝑉 (𝐸 + 𝑉 log 𝑉 )).
4.5.2. Алгоритм Каргера-Штейна
Пусть 𝑐𝑒 ≡ 1. Минимальную степень обозначим 𝑘.
∃𝑣 : 𝑑𝑒𝑔𝑣 = 𝑘 ⇒ 𝐶({𝑣}, 𝑉 ∖ {𝑣}) = 𝑘 ⇒ в min разрезе не более 𝑘 рёбер. 𝐸 = 21 𝑑𝑒𝑔𝑣 ⩾ 12 𝑘𝑉 .
∑︀
Возьмём случайное ребро 𝑒, вероятность того, что оно попало в разрез 𝑃 𝑟[𝑒 ∈ 𝑐𝑢𝑡] ⩽ 𝐸𝑘 = 𝑉2 .
∙ Алгоритм Каргера.
Пока в графе > 2 вершин, выбираем случайное ребро, не являющееся петлёй, стягиваем его
концы в одну вершину. В конце 𝑉 ′ = {𝐴, 𝐵}, объявляем минимальным разрезом 𝑉 = 𝐴 ⊔ 𝐵.
Время работы: 𝑇 (𝑉 ) = 𝑉 + 𝑇 (𝑉 − 1) = Θ(𝑉 2 ). Здесь 𝑉 – время стягивания двух вершин.
Вероятность успеха: ни разу не ошиблись с 𝑃 𝑟 ⩾ 𝑉 −2
𝑉
· 𝑉 −3
𝑉 −1
· 𝑉 −4
𝑉 −2
· ... · 1
3
= 2·1
𝑉 ·(𝑉 −1)
⩾ 2
𝑉2
.
Чтобы алгоритм имел константную вероятность ошибки, достаточно запустить его 𝑉 2 ⇒
получили RP-алгоритм за 𝒪(𝑉 4 ).
∙ Алгоритм Каргера-Штейна.
В оценке 𝑉𝑉 −3 · 𝑉 −4 · . . . · 31 большинство первых сомножителей близки к 1. Последние
−1 𝑉 −2
сомножители – 42 , 13 напротив весьма малы ⇒ остановимся, когда в графе останется √𝑉2 вершин.
√ √
Вероятность ни разу не ошибиться при этом будет 𝑉 / 2(𝑉 / 2−1)
𝑉 (𝑉 −1)
≈ 12 .
√
Время, потраченное на (𝑉 − 𝑉 / 2) сжатий – Θ(𝑉 2 ).
√
После этого сделаем два рекурсивных вызова от получившегося графа с 𝑉 / 2 вершинами.
Алгоритм рандомизированный ⇒ ветки рекурсии могут дать разные разрезы ⇒ вернём мини-
мальный из полученных.
Время работы: 𝑇 (𝑉 ) = 𝑉 2 + 2𝑇 ( √𝑉2 ) = 𝑉 2 + 2( √𝑉2 )2 + 4𝑇 ( √𝑉 2 ) = · · · = 𝑉 2 log 𝑉 .
2
Вероятность ошибки. Ищем 𝑝(𝑉 ), вероятность успеха на графе из 𝑉 .
√
Обозначим 𝑝(𝑉 ) = 𝑞𝑘 , 𝑝(𝑉 / 2) = 𝑞𝑘−1 , . . . , ⇒
Глава #4. 10 октября. 22/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Потоки (быстрые)
𝑞𝑖 = 12 (1 − (1−𝑞𝑖−1 )2 ) = 𝑞𝑖−1 − 21 𝑞𝑖−1
2 2
⇒ 𝑞𝑖 −𝑞𝑖−1 = −𝑞𝑖−1 . Левая часть похожа на производную ⇒
−𝑞 ′ (𝑥)
решим диффур 𝑞 (𝑥) = −𝑞(𝑥) ⇔ 𝑞(𝑥)2 = 1 ⇔ 𝑞(𝑥) = 𝑥 + 𝐶 ⇒ 𝑞𝑘 = Θ( 𝑘1 ) ⇒ 𝑝(𝑉 ) = Θ( log1 𝑉 ).
′ 2 −1
Короткая и быстрая реализация получается через random_shuffle исходных рёбер.
Подробнее в разборе практики.
4.6. (*) Алгоритм Push-Relabel
Конспект по этой теме лежит в отдельном файле.
Глава #5. 10 октября. 23/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Mincost
Лекция #5: Mincost
11 октября 2022
5.1. Mincost k-flow в графе без отрицательных циклов
Сопоставим всем прямым рёбрам вес (стоимость) 𝑤𝑒 ∈ R.
∑︀
Def 5.1.1. Стоимость потока 𝑊 (𝑓 ) = 𝑒 𝑤𝑒 𝑓𝑒 . Сумма по прямым рёбрам.
Обратному к 𝑒 рёбру 𝑒 сопоставим 𝑤 𝑒 = −𝑤𝑒 .
Если толкнуть поток сперва по прямому, затем по обратному к нему ребру, стоимость не изме-
нится. Когда мы толкаем единицу потока по пути path, изменение потока и стоимости потока
теперь выглядят так:
1 for ( int e : path ) :
2 edges [ e ]. f ++
3 edges [ e ^ 1]. f - -
4 W += edges [ e ]. w ;
Задача mincost k-flow: найти поток 𝑓 : |𝑓 | = 𝑘, 𝑊 (𝑓 ) → min
При решении задачи мы будем говорить про веса путей, циклов, “отрицательные циклы”, крат-
чайшие пути... Везде вес пути/цикла – сумма весов рёбер (𝑤𝑒 ).
Решение #1. Пусть в графе нет отрицательных циклов, а также все 𝑐𝑒 ∈ Z.
Тогда по аналогии с алгоритмом Ф.Ф., который за 𝒪(𝑘 · dfs) искал поток размера 𝑘, мы можем
за 𝒪(𝑘·FordBellman) найти mincost поток размера 𝑘. Обозначим 𝑓𝑘 оптимальный поток размера
𝑘 ⇒ 𝑓0 ≡ 0, 𝑓𝑘+1 = 𝑓𝑘 + 𝑝𝑎𝑡ℎ, где 𝑝𝑎𝑡ℎ – кратчайший в 𝐺𝑓𝑘 .
Lm 5.1.2. ∀𝑘, |𝑓 | = 𝑘 (𝑊 (𝑓 ) = min) ⇔ (∄ отрицательного цикла в 𝐺𝑓 )
Доказательство. Если отрицательный цикл есть, увеличим по нему поток, |𝑓 | не изменится,
𝑊 (𝑓 ) уменьшится. Пусть ∃𝑓 * : |𝑓 * | = |𝑓 |, 𝑊 (𝑓 * ) < 𝑊 (𝑓 ), рассмотрим поток 𝑓 * − 𝑓 в 𝐺𝑓 .
Это циркуляция, мы можем декомпозировать её на циклы 𝑐1 , 𝑐2 , . . . , 𝑐𝑘 .
Поскольку 0 > 𝑊 (𝑓 * − 𝑓 ) = 𝑊 (𝑐1 ) + · · · + 𝑊 (𝑐𝑘 ), среди циклов 𝑐𝑖 есть отрицательный. ■
Теорема 5.1.3. Алгоритм поиска mincost потока размера 𝑘 корректен.
Доказательство. База: по условию нет отрицательных циклов ⇒ 𝑓0 корректен.
Переход: обозначим 𝑓𝑘+1*
mincost поток размера 𝑘+1, смотрим на декомпозицию Δ𝑓 = 𝑓𝑘+1
*
− 𝑓𝑘 .
|Δ𝑓 | = 1 ⇒ декомпозиция = путь 𝑝 + набор циклов. Все циклы по 5.1.2 неотрицательны ⇒
*
𝑊 (𝑓𝑘 + 𝑝) ⩽ 𝑊 (𝑓𝑘+1 ) ⇒, добавив, кратчайший путь мы получим решение не хуже 𝑓𝑘+1
*
. ■
Lm 5.1.4. Если толкнуть сразу 0 ⩽ 𝑥 ⩽ min𝑒∈𝑝 (𝑐𝑒 − 𝑓𝑒 ) потока по пути 𝑝,
то получим оптимальный поток размера |𝑓 | + 𝑥.
Доказательство. Обозначим 𝑓 * оптимальный поток размера |𝑓 | + 𝑥, посмотрим на декомпози-
цию 𝑓 * − 𝑓 , заметим, что все пути в ней имеют вес ⩾ 𝑊 (𝑝), а циклы вес ⩾ 0. ■
Теорема 5.1.5. Пусть 𝑤𝑒 ∈ Z ∩ [1, 𝑊 ] ⇒ умеем искать mincost k-flow за 𝒪(𝑊 𝑉 𝐸 · FordBellman)
Доказательство. Используем алгоритм, толкающий min𝑒∈𝑝 (𝑐𝑒 − 𝑓𝑒 ).
Также, как в Эдмондсе-Карпе, каждый раз какое-то ребро насытится.
Каждое ребро насытится не более 𝑚𝑎𝑥𝐷𝑖𝑠𝑡
2
⩽ 𝑊2𝑉 раз ⇒ всего 𝒪(𝑊 𝑉 𝐸) путей ■
Глава #5. 11 октября 2022. 24/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Mincost
5.2. Потенциалы и Дейкстра
Для ускорения хотим Форда-Беллмана заменить на Дейкстру.
Для корректности Дейкстры нужна неотрицательность весов.
В прошлом семестре мы уже сталкивались с такой задачей, когда изучали алгоритм Джонсона.
∙ Решение задачи mincost k-flow.
Запустим один раз Форда-Беллмана из 𝑠, получим массив расстояний 𝑑𝑣 , применим потенциалы
𝑑𝑣 к весам рёбер:
𝑒 : 𝑎 → 𝑏 ⇒ 𝑤𝑒 → 𝑤𝑒 + 𝑑𝑎 − 𝑑𝑏
′
Напомним, что из корректности 𝑑 имеем ∀𝑒 𝑑𝑎 + 𝑤𝑒 ⩾ 𝑑𝑏 ⇒ 𝑤𝑒 ⩾ 0.
′
Более того: для всех рёбер 𝑒 кратчайших путей из 𝑠 верно 𝑑𝑎 + 𝑤𝑒 = 𝑑𝑏 ⇒ 𝑤𝑒 = 0.
В 𝐺𝑓 найдём Дейкстрой из 𝑠 кратчайший путь 𝑝 и расстояния 𝑑′𝑣 .
′
Пустим по пути 𝑝 поток, получим новый поток 𝑓 = 𝑓 + 𝑝.
′
В сети 𝐺𝑓 могли появиться новые рёбра (обратные к 𝑝). Они могут быть отрицательными.
Пересчитаем веса:
𝑒 : 𝑎 → 𝑏 ⇒ 𝑤𝑒 → 𝑤𝑒 + 𝑑′𝑎 − 𝑑′𝑏
Поскольку 𝑑′ – расстояния, посчитанные в 𝐺𝑓 , все рёбра из 𝐺𝑓 останутся неотрицательными.
𝑝 – кратчайший путь, все рёбра 𝑝 станут нулевыми ⇒ рёбра обратные 𝑝 тоже будут нулевыми.
∙ Псевдокод
1 def applyPotentials ( d ) :
2 for e in Edges :
3 e.w = e.w + d[e.a] - d[e.b]
4 d <-- FordBellman ( s )
5 applyPotentials ( d )
6 for i = 1.. k :
7 d , path <-- Dijkstra ( s )
8 for e in path : e . f += 1 , e . rev . f -= 1
9 applyPotentials ( d )
5.3. Задачи на mincost поток, паросочетания
Чтобы найти паросочетания min веса, достаточно построить поточную сеть для поиска паро-
сочетания и расставить веса: 0 у рёбер, смежных с истоком/стоком 𝑤𝑖𝑗 у рёбер между долями.
Для поиска паросочетания max веса, ищем mincost поток на весах −𝑤𝑖𝑗 .
Корректность: биекция с сохранением веса между потоками и паросочетаниями.
Время работы с Дейкстрой: 𝑉 𝐸 + 𝑓 𝑙𝑜𝑤 · 𝑉 2 = 𝒪(𝑉 3 ).
5.4. Графы с отрицательными циклами
Задача: найти mincost циркуляцию.
Алгоритм Клейна: пока в 𝐺𝑓 есть отрицательный цикл, пустим по нему min𝑒 (𝑐𝑒 − 𝑓𝑒 ) потока.
Пусть ∀𝑒 𝑐𝑒 , 𝑤𝑒 ∈ Z ⇒ 𝑊 (𝑓 ) каждый раз уменьшается хотя бы на 1 ⇒ алгоритм конечен.
Задача: найти mincost 𝑘-flow в графе с отрицательными циклами.
Решение #1: добавить ребро 𝑒 : 𝑡 → 𝑠, 𝑐𝑒 = 𝑘, 𝑤𝑒 = −∞, найти mincost циркуляцию.
Глава #5. 11 октября 2022. 25/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Mincost
Решение #2: найти mincost циркуляцию, перейти от 𝑓0 за 𝑘 итераций к 𝑓𝑘 .
Решение #3: найти любой поток 𝑓 : |𝑓 | = 𝑘, в 𝐺𝑓 найти mincost циркуляцию, сложить с 𝑓 .
5.5. Mincost flow
Задача: найти 𝑓 : 𝑊 (𝑓 ) = min, размер 𝑓 не важен.
Если в графе есть отрицательные циклы ⇒ всё равно придётся искать mincost циркуляцию ⇒
Решение #1: добавить ребро 𝑒 : 𝑡 → 𝑠, 𝑐𝑒 = +∞, 𝑤𝑒 = −∞, найти mincost циркуляцию.
Иначе можно пытаться сделать лучше (проще).
Решение #2: пытаемся угадать 𝑘 линейным или бинарным поиском.
Обозначим 𝑓𝑘 – оптимальный поток размера 𝑘, 𝑝𝑘 кратчайший путь в 𝐺𝑓𝑘 .
Lm 5.5.1. 𝑊 (𝑝𝑘 )↗, как функция от 𝑘.
Доказательство. Аналогично доказательству леммы для Эдмондса-Карпа 2.8.1.
От противного. Был поток 𝑓 , мы увеличили его по кратчайшему пути 𝑝.
Расстояния в 𝐺𝑓 обозначим 𝑑0 , в 𝐺𝑓 +𝑝 – 𝑑1 .
Возьмём 𝑣 : 𝑑1 [𝑣] < 𝑑0 [𝑣], а из таких ближайшую к 𝑠 в дереве кратчайших путей.
Рассмотрим кратчайший путь 𝑞 в 𝐺𝑓 +𝑝 из 𝑠 в 𝑣: 𝑠 ⇝ · · · ⇝ 𝑥 → 𝑣.
𝑒 = (𝑣 → 𝑥), 𝑑1 [𝑣] = 𝑑1 [𝑥] + 𝑤𝑒 , 𝑑1 [𝑥] ⩾ 𝑑0 [𝑥] ⇒ 𝑑1 [𝑣] ⩾ 𝑑0 [𝑥] + 𝑤𝑒 ⇒ ребра (𝑥 → 𝑣) нет в 𝐺𝑓 ⇒
ребро (𝑣 → 𝑥) ∈ 𝑝 ⇒ 𝑑0 [𝑥] = 𝑑0 [𝑣] + 𝑤 𝑒 = 𝑑0 [𝑣] − 𝑤𝑒 ⇒
𝑑1 [𝑣] = 𝑑1 [𝑥] + 𝑤𝑒 ⩾ 𝑑0 [𝑥] + 𝑤𝑒 = (𝑑0 [𝑣] − 𝑤𝑒 ) + 𝑤𝑒 = 𝑑0 [𝑣]. Противоречие. ■
Следствие 5.5.2. (𝑊 (𝑓𝑘 ) = min) ⇔ (𝑊 (𝑝𝑘−1 ) ⩽ 0 ∧ 𝑊 (𝑝𝑘 ) ⩾ 0).
Осталось найти такое 𝑘 бинпоиском или линейным поиском. На текущий момент мы умеем
искать 𝑓𝑘 или за 𝒪(𝑘 · 𝑉 𝐸) с нуля, или за 𝒪(𝑉 𝐸) из 𝑓𝑘−1 ⇒ линейный поиск будет быстрее.
Глава #5. 11 октября 2022. 26/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Mincost
5.6. Полиномиальные решения
Mincost flow мы можем бинпоиском свести к mincost k-flow.
Mincost k-flow мы можем поиском любого потока размера 𝑘 свести к mincost циркуляции.
Осталось научиться за полином искать mincost циркуляцию.
∙ Решение #1: модифицируем алгоритм Клейна, будем толкать min𝑒 (𝑐𝑒 −𝑓𝑒 ) потока по циклу
min среднего веса. Заметим, что (∃ отрицательный цикл) ⇔ (min средний вес < 0).
Решение работает за 𝒪(𝑉 𝐸 log(𝑛𝐶)) поисков цикла. Цикл ищется алгоритмом Карпа за 𝒪(𝑉 𝐸).
Доказано будет на практике.
∙ Решение #2: Capacity Scaling.
Начнём с графа 𝑐′𝑒 ≡ 0, в нём mincost циркуляция тривиальна.
Будем понемногу наращивать 𝑐′𝑒 и поддерживать mincost циркуляцию. В итоге хотим 𝑐′𝑒 ≡ 𝑐𝑒 .
1 for k = logU ..0:
2 for e in Edges :
3 if c 𝑒 содержит бит 2𝑘 :
4 c ′𝑒 += 2𝑘 // 𝑒: ребро из 𝑎𝑒 в 𝑏𝑒 ‘
5 Найдём 𝑝 – кратчайший путь 𝑎𝑒 → 𝑏𝑒
6 if 𝑊 (𝑝) + 𝑤𝑒 ⩾ 0:
7 нет отрицательных циклов ⇒ циркуляция 𝑓 оптимальна
8 else :
9 пустим 2𝑘 потока по циклу 𝑝 + 𝑒 (изменим 𝑓 )
10 пересчитаем потенциалы, используя расстояния, найденные Дейкстрой
Время работы алгоритма 𝐸 log 𝑈 запусков Дейкстры = 𝐸(𝐸 + 𝑉 log 𝑉 ) log 𝑈 .
Lm 5.6.1. После 9-й строки циркуляция 𝑓 снова минимальна.
Доказательство. 𝑓 – минимальная циркуляция до 4-й строки, 𝑓 ′ – после.
Как обычно, рассмотрим 𝑓 ′ − 𝑓 . Это тоже циркуляция. Декомпозируем её на единичные циклы.
Любой цикл проходит через 𝑒 (иначе 𝑓 не оптимальна). Через 𝑒 проходит не более 2𝑘 циклов.
Каждый из этих циклов имеет вес не меньше веса 𝑝 + 𝑒 ⇒ 𝑊 (𝑓 ′ ) ⩾ 𝑊 (𝑓 + 2𝑘 (𝑝 + 𝑒)). ■
5.7. (*) Cost Scaling
Задача, которую решаем: mincost circulation.
∙ Решение
База: 𝑓 ≡ 0, если в 𝐺𝑓 нет отрицательных рёбер, то 𝑓 оптимален.
Общая идея: научимся делать шаг ∀𝑒 ∈ 𝐺𝑓 𝑤𝑒 ⩾ −2𝜀 → ∀𝑒 ∈ 𝐺𝑓 𝑤𝑒 ⩾ −𝜀.
Когда остановиться? Если исходные 𝑐𝑒 , 𝑤𝑒 ∈ Z, 𝑓𝑒 ∈ Z и 𝜀 < 𝑛1 , то вес любого цикла > −1 ⇒ из
целочисленности получаем оптимальность потока.
Как избавиться от отрицательных рёбер? Насытим их. Получим избытки-недостатки, пере-
направим избытки в недостатки, разрешая толкать поток только по отрицательным рёбрам.
Избыток есть, а исходящие отрицательных рёбер нет? Подгоним потенциалы и получим новое
отрицательное.
⇒ схема решения для целых 𝑐𝑒 , 𝑤𝑒 :
Глава #5. 11 октября 2022. 27/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Mincost
1 // push : поменять потоки рёбра и обратного, пересчитать избытки x[v]
2 for e do w 𝑒 *= n +1
3 for ( int EPS = max | w 𝑒 |; EPS >= 1; EPS /= 2)
4 for e do if w 𝑒 < 0 then push (e , c 𝑒 - f 𝑒 )
5 while ∃ v : x [ v ] > 0 do
6 for e : v → u do if push (e , min ( x [ v ] , c 𝑒 -f 𝑒 ) )
7 if x [ v ] > 0 then что-то сделаем с потенциалами, см. далее
∀𝑎, 𝑏 𝑤𝑒𝑝 = 𝑤𝑒 + 𝑝𝑎 − 𝑝𝑏 ⇒ найдём 𝑒 : 𝑣 → 𝑢, 𝑤𝑒𝑝 = min ⩾ 0 и уменьшим 𝑝𝑣 на 𝑤𝑒𝑝 + 𝜀.
Вес входящих в 𝑣 рёбер увеличился, вес исходящих ⩾ −𝜀, новый вес ребра 𝑒 равен −𝜀.
Если потенциалы здесь воспринимать, как «высоты» с шагов −𝜀, мы по сути каждую фазу
−2𝜀 → −𝜀 запускаем технику push-relabel. Все оценки для push-relabel будут верны. Для того,
чтобы показать, что как и «увеличений высот», так и «увеличений потенциалов» мало, заметим,
что для всех вершин у которых избыток всё ещё положителен ∃ путь по рёбрам 𝑐𝑒 −𝑓𝑒 > 0 в
вершину с отрицательным избытком, для которой на текущей фазе потенциал ещё не менялся.
Время работы на практике. Эксперимент показывает, что на не специальных тестах простей-
шая реализация (код выше) на графах размера 𝑛 = 100, 𝑚 = 1000 даёт уже ⩽ 3𝑚 шагов на
фазу. В коде выше мы делим на 𝛼 = 2, можно пробовать и другие значения.
В задачах assignment и mincost с контеста оптимально 𝛼 = 3.
Поиск mincost потока. Алгоритм выше ищет циркуляцию. Добавим ребро 𝑒 : 𝑡 → 𝑠 с нужными
𝑤𝑒 , 𝑐𝑒 в зависимости от задачи (mincost flow, mincost maxflow, mincost k-flow).
Поиск mincost паросочетания в двудольном графе. Можно добавить ребро 𝑒 : 𝑡 → 𝑠, 𝑐𝑒 = 𝑛,
𝑤𝑒 = −max𝑊 ·(𝑛+1)·𝑛 и в явном виде использовать код выше. Можно, пользуясь специфичной
структурой графа, получить более быстрый алгоритм (см. статью и код ниже).
Краткое описание фазы для паросочетания в двудольном графе. Кун. Для вершин второй до-
ли храним пары в первой. Пытаемся идти в минимальную. Если там уже занято, всё равно
идём, вытесняем вершину-старую-пару, для которой предстоит продолжить поиск, и меняем
потенциал вершины первой доли на «разницу второго и первого минимума» +𝜀.
[Lection: Cost scaling (part 1)]
[Lection: Cost scaling (part 2, end of proof)]
[Практически быстрые алгоритмы для задачи о назначениях]
[Код паросочетания через mincost-scaling]
[Код потока через mincost-scaling]
Глава #6. 11 октября 2022. 28/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Базовые алгоритмы на строках
Лекция #6: Базовые алгоритмы на строках
18 октября 2022
6.1. Обозначения, определения
𝑠, 𝑡 — строки, |𝑠| — длина строки, 𝑠 — перевёрнутая 𝑠,
s[l:r) и s[l:r] — подстроки,
s[0:i) — префикс, s[i:|s|-1] = s[i:] — суффикс.
Σ — алфавит, |Σ| — размер алфавита.
Говорят, что 𝑠 — подстрока 𝑡, если ∃ 𝑙, 𝑟 : 𝑠 = 𝑡[𝑙:𝑟).
6.2. Поиск подстроки в строке
Даны текст 𝑡 и строка 𝑠. Ещё иногда говорят «строка (string) 𝑡 и образец (pattern) 𝑠».
Вхождением 𝑠 в 𝑡 назовём позицию 𝑖 : s = t[i:i+|s|).
Возможны различные формулировки задачи поиска подстроки в строке:
(a) Проверить, есть ли хотя бы одно вхождение 𝑠 в 𝑡.
(b) Найти количество вхождений 𝑠 в 𝑡.
(c) Найти позицию любого вхождения 𝑠 в 𝑡, или вернуть −1, если таких нет.
(d) Вернуть множества всех вхождений 𝑠 в 𝑡.
6.2.1. C++
В языке C++ у строк типа string есть стандартный метод find. Работает за 𝒪(|𝑠| · |𝑡|), возвра-
щает целое число — номер позиции в исходной строке, начиная с которого начинается первое
вхождение подстроки или string::npos.
Функция из <cstring> strstr(t, s) ищет 𝑠 в 𝑡. Работает за линию в Unix, за квадрат в Windows.
В обоих случаях квадрат имеет очень маленькую константу (AVX-регистры).
Все вхождения можно перечислить таким циклом:
1 for ( size_t pos = t . find ( s ) ; pos != string :: npos ; pos = st . find (s , pos + 1) )
2 ; // pos –- позиция вхождения
или таким
1 for ( char * p = t ; ( p = strstr (p , s ) ) != 0; p ++)
2 ; // p –- указатель на позицию вхождения в t
6.2.2. Префикс функция и алгоритм КМП
Def 6.2.1. 𝜋0 (𝑠) — длина max собственного префикса 𝑠, совпадающего c суффиксом 𝑠.
(︀ )︀
Def 6.2.2. Префикс-функция строки s — массив 𝜋(𝑠) : 𝜋(𝑠)[𝑖] = 𝜋0 𝑠[0:𝑖) .
Когда из контекста понятно, о префикс-функции какой строки идёт речь, пишут просто 𝜋[𝑖].
∙ Алгоритм Кнута-Мориса-Пратта.
Пусть ‘#‘ — любой символ, который не встречается ни в 𝑡, ни в 𝑠. Создадим новую строку 𝑤 = 𝑠#𝑡
и найдем её префикс-функцию. Благодаря символу ‘#‘ ∀𝑖 𝜋(𝑤)[𝑖] ⩽ |𝑠|. Такие 𝑖, что 𝜋(𝑤)[𝑖] = |𝑠|,
задают позиции окончания вхождений 𝑠 в 𝑤 ⇒ (𝑗 = 𝑖 − 2|𝑠| − 1) — начало вхождения 𝑠 в 𝑡.
Глава #6. 18 октября 2022. 29/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Базовые алгоритмы на строках
∙ Вычисление префикс-функции за 𝒪(𝑛).
Все префиксы равные суффиксам для строки s[0:i) это 𝑋 = {𝑖, 𝜋[𝑖], 𝜋[𝜋[𝑖]], 𝜋[𝜋[𝜋[𝑖]]], . . . }
15
5 5 7
𝑠 a b a b a b a c a b a b a b a c a b a b a b a
𝑖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
𝜋[1:) 0 0 1 2 3 4 5 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
7 7 5
15
Пример (см. картинку выше): для всей строки 𝑠 имеем 𝑋 = {23, 15, 7, 5, 3, 1}.
Заметим, что или 𝜋[𝑖+1] = 0, или 𝜋[𝑖+1] получается, как 𝑥 + 1 для некоторого 𝑥 ∈ 𝑋.
Будем перебирать 𝑥 ∈ 𝑋 в порядке убывания, получается следующий код:
1 p [1] = 0 , n = | s |
2 for ( i = 2; i <= n ; i ++)
3 int k = p [ i - 1];
4 while ( k > 0 && s [ k ] != s [ i - 1])
5 k = p [ k ];
6 if ( s [ k ] == s [ i - 1])
7 k ++;
8 p[i] = k;
Заметим, что с учётом последней строки цикла первая не нужна, получаем:
1 p [1] = k = 0 , n = | s |
2 for ( i = 2; i <= n ; i ++)
3 while ( k > 0 && s [ k ] != s [ i - 1])
4 k = p [ k ];
5 if ( s [ k ] == s [ i - 1])
6 k ++;
7 p[i] = k;
Вычисление префикс функции работает за 𝒪(𝑛), так как 𝑘 увеличится ⩽ 𝑛 раз ⇒
суммарное число шагов цикла while не более 𝑛.
Собственно КМП работает за 𝒪(|𝑠| + |𝑡|) и требует 𝒪(|𝑠| + |𝑡|) дополнительной памяти.
Упражнение: придумайте, как уменьшить количество доппамяти до 𝒪(|𝑠|).
6.2.3. LCP
Def 6.2.3. lcp[i,j] (largest common prefix) для строки 𝑠 —
длина наибольшего общего префикса суффиксов s[i:] и s[j:].
{︃
1 + lcp[i+1,j+1] 𝑠[𝑖] = 𝑠[𝑗]
Вычислить массив lcp можно за 𝒪(𝑛2 ), так как lcp[i,j] =
0 иначе
Аналогично можно определить и вычислить массив lcp для двух разных строк.
Глава #6. 18 октября 2022. 30/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Базовые алгоритмы на строках
6.2.4. Z-функция
Def 6.2.4. 𝑍-функция — массив 𝑧 такой, что 𝑧[0] = 0, ∀𝑖>0 𝑧[𝑖] = lcp[0,i].
Для поиска подстроки снова введем 𝑤 = 𝑠#𝑡 и посчитаем 𝑍(𝑤).
Найдем все позиции 𝑖 : 𝑍(𝑤)[𝑖] = |𝑠|. Это позиции всех вхождений строки 𝑠 в строку 𝑡.
Осталось научиться вычислять 𝑍-функцию за линейное время.
1 z [0] = 0;
2 for ( i = 1; i < n ; i ++)
3 int k = 0;
4 while ( s [ i + k ] == s [ k ]) // s[n] == ’\x0’ ⇒ нет выхода за пределы!
5 k ++;
6 z[i] = k;
Приведенный алгоритм работает за 𝒪(𝑛2 ). На строке 𝑎𝑎𝑎. . .𝑎 оценка 𝑛2 достигается.
Ключом к ускорению является следующая лемма:
Lm 6.2.5. ∀𝑙 < 𝑖 < 𝑙 + 𝑧[𝑙] = 𝑟 имеем s[0:z[l]) = s[l:r) и s[i-l:z[l]) = s[i:r).
Следствие леммы: 𝑧[𝑖] ⩾ min(𝑟 − 𝑖, 𝑧[𝑖 − 𝑙]). Логично взять 𝑙 : 𝑟 = 𝑙 + 𝑧[𝑙] = max.
Немного модифицируем код, чтобы получить асимптотику 𝒪(𝑛).
1 z [0] = 0 , l = r = 0;
2 for ( i = 1; i < n ; i ++)
3 int k = max (0 , min ( r - i , z [ i - l ]) )
4 while ( s [ i + k ] == s [ k ])
5 k ++
6 z[i] = k
7 if ( i + z [ i ] > r ) l = i , r = i + z [ i ]
Теорема 6.2.6. Приведенный выше алгоритм работает за 𝒪(𝑛).
Доказательство. 𝑘++ ⇒ 𝑟++, а 𝑟 может увеличиваться ⩽ 𝑛 раз. ■
6.3. Полиномиальные хеши строк
Основная идея этой секции — научиться с предподсчётом за 𝒪(суммарной длины строк) веро-
ятностно сравнивать на равенство любые их подстроки за 𝒪(1).
Например, мы уже умеем считать частичные суммы за 𝒪(1) ⇒ можем за 𝒪(1) проверить, равны
ли суммы символов в подстроках. Если не равны ⇒ строки точно не равны...
Def 6.3.1. Хеш-функция объектов из мн-ва 𝐴 в диапазон [0, 𝑚) — любая функция 𝐴 → Z/𝑚Z.
Например, сумма символов строки, посчитанная по модулю 256 — пример хеш-фукнции из
множества строк в диапазон [0, 256). Задача — придумать более удачную хеш-функцию.
Зачем нужны хеши (хеш-функции)? Чтобы сравнивать объекты на равенство.
Хеши не совпали ⇒ объекты точно не совпали.
Хеши совпали ⇒ с некоторой вероятностью объекты всё равно различаются (коллизия хешей)
и у нас есть выбор —- или остатоновиться и получить RP алгоритм, или после равенства хешей
сравнить сами объекты и получить ZPP алгоритм.
Глава #6. 18 октября 2022. 31/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Базовые алгоритмы на строках
∙ Полиномиальная хеш-функция
Def 6.3.2. Пусть 𝑠 = 𝑠0 𝑠1 . . .𝑠𝑛−1 ⇒ ℎ𝑝,𝑚 (𝑠) = (𝑠0 𝑝𝑛−1 + 𝑠1 𝑝𝑛−2 + · · · + 𝑠𝑛−1 ) mod 𝑚
ℎ𝑝,𝑚 (𝑠) — полиномиальный хеш для строки 𝑠 посчитанный в точке 𝑝 по модулю 𝑚.
По сути мы взяли многочлен (полином) с коэффициентами «символы строки» ∑︀ и посчитали
его значение в точке 𝑝 по модулю 𝑚. Можно было бы определить ℎ𝑝,𝑚 (𝑠) = 𝑖 𝑠𝑖 𝑝 , но при
𝑖
реализации нам будет удобен порядок суммирования из 6.3.2.
1 int * h ;
2 void initialize ( int n , char * s ) {
3 h = new int [ n + 1]; // h[i] –- хеш префикса s[0:i)
4 h [0] = 0; // хеш пустой строки действительно 0...
5 for ( int i = 0; i < n ; i ++)
6 h [ i + 1] = (( int64_t ) h [ i ] * p + s [ i ]) % m ; // 0 < 𝑚 < 231
7 }
8 int getHash ( int l , int r , char * s ) { // [l,r)
9 // deg[r - l] = p𝑟−𝑙 , никогда не пишите здесь лишний if
10 T res = ( h [ r ] - ( int64_t ) h [ l ] * deg [ r - l ]) % m ;
11 return res < 0 ? res + m : res ; // остаток мог быть отрицательным
12 }
Чтобы вероятность коллизии была мала нужно:
𝑚 — простое большое число.
𝑝 — заранее фиксированное случайное число.
Стандартные ошибки:
(a) Символы строки должны быть >0, иначе hash(00) = hash(0).
(b) Переполнения! Например, при подсчёте 𝑝𝑘 .
(c) Остаток может быть отрицательными: (ℎ𝑟 − ℎ𝑙 𝑝𝑟−𝑙 ) mod 𝑚
(d) ±1 во всех формулах. В коде выше полуинтервал [𝑙, 𝑟), индексация с нуля.
Если вы хотите делать вычисления по более большому модулю ≈1018 , у вас два пути — или
считать два хеша по двум модулям 109 + 7, 109 + 9, или использовать тип __int128.
Ещё есть вариант — считать по модулю 232 или 264 ,
тогда можно везде писать тип uint_32/uint_64, и не делать лишних взять по модулю.
Константа станет меньше, а вот работать будет не всегда (ниже подробно разберёмся).
Def 6.3.3. Коллизия хешей — ситуация вида 𝑠 ̸= 𝑡, ℎ𝑝,𝑚 (𝑠) = ℎ𝑝,𝑚 (𝑡).
Займёмся точными оценками чуть позже, пока предположим, что ∀ простого 𝑚, если мы вы-
бираем 𝑝 равновероятно в [0, 𝑚), вероятность коллизии при одном сравнении равна 𝑚1 .
Из умения сравнивать строки на равенство за 𝒪(1) следует алгоритм поиска строки в тексте:
Глава #6. 18 октября 2022. 32/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Базовые алгоритмы на строках
6.3.1. Алгоритм Рабина-Карпа
Можно искать 𝑠 в 𝑡, предподсчитав полиномиальные хеши для 𝑡 и для каждого потенциального
вхождения [𝑖, 𝑖+|𝑠|) сравнить за 𝒪(1) хеш подстроки 𝑡[𝑖, 𝑖+|𝑠|) с хешом 𝑠.
Если хеши совпали, то возможно два развития событий: мы можем
или проверить за линию равенство строк, или, не проверяя, выдать вхождение 𝑖.
Для задачи поиска одного вхождения в первом случае мы получили RP, во втором ZPP.
На практике используют оба подхода.
Для задачи поиска всех вхождений проверять каждое вхождение за линию — слишком долго.
∙ Оптимизируем память.
Преимущество Рабина-Карпа над 𝜋-функцией и 𝑍-функцией — реализация с 𝒪(1) доппамяти.
Обозначим 𝑛 = |𝑠| и ℎ𝑡𝑖 = ℎ𝑝,𝑚 (𝑡[𝑖 : 𝑖 + 𝑛)). Посчитаем ℎ𝑝,𝑚 (𝑠), ℎ𝑡0 и 𝑝𝑛 .
Осталось, зная ℎ𝑡𝑖 , научиться считать ℎ𝑡𝑖+1 = ℎ𝑡𝑖 · 𝑝 − 𝑡𝑖 · 𝑝𝑛 .
6.3.2. Наибольшая общая подстрока за 𝒪(𝑛 log 𝑛)
Задача: даны строки 𝑠 и 𝑡, найти 𝑤 : 𝑤 — подстрока 𝑠, подстрока 𝑡, |𝑤| → max.
Заметим, что если 𝑤 — общая подстрока 𝑠 и 𝑡, то любой её префикс тоже ⇒
функция 𝑓 (𝑘) = «есть ли у 𝑠 и 𝑡 общая подстрока длины 𝑘» — монотонный предикат ⇒
максимальное 𝑘 : 𝑓 (𝑘) = 1 можно найти бинпоиском (𝒪(log min(|𝑠|, |𝑡|) итераций).
Осталось для фиксированного 𝑘 за 𝒪(|𝑠| + |𝑡|) проверить, есть ли у 𝑠 и 𝑡 общая подстрока
длины 𝑘. Сложим хеши всех подстрок 𝑠 длины 𝑘 в хеш-таблицу. Для каждой подстроки 𝑡
длины 𝑘 поищем её хеш в хеш-таблице. Если нашли совпадение, как и в Рабине-Карпе есть два
пути — проверить за линию или сразу поверить в совпадение. Оба метода работают.
6.3.3. Оценки вероятностей
∙ Многочлены
Пусть 𝑚 — простое число.
Lm 6.3.4. Число корней многочлена степени 𝑛 над Z/𝑚Z не более 𝑛.
Lm 6.3.5. Пусть 𝑡 ∈ Z/𝑚Z ⇒ 𝑃 𝑟[𝑃 (𝑡) = 0] = 1
𝑚
, где 𝑃 — случайный многочлен.
Lm 6.3.6. Матожидание числа корней случайного многочлена степени 𝑛 над Z/𝑚Z равно 1.
Первую лемму вы знаете из курса алгебры,
третья сразу следует из второй (𝑚 потенциальных корней, каждый с вероятностью 1/𝑚),
вторая получается из того, что 𝑃 (𝑥) = 𝑄(𝑥)(𝑥 − 𝑡) + 𝑟 : у случайного 𝑃 (𝑥) все 𝑟 равновероятны.
Нам важна простота модуля 𝑚, для непростого оценки неверны.
Например, у многочлена 𝑥64 при 𝑚 = 264 любое чётное число является корнем.
∙ Связь со строками
ℎ𝑝,𝑚 (𝑆) = 𝑆(𝑝) mod 𝑚, где 𝑆 — и строка, и многочлен с коэффициентами 𝑆𝑖 .
Тогда ℎ𝑝,𝑚 (𝑆) = ℎ𝑝,𝑚 (𝑇 ) ⇔ (𝑆 − 𝑇 )(𝑝) ≡ 0 mod 𝑚.
Запишем несколько следствий из лемм про многочлены.
Следствие 6.3.7. ∀ пары ⟨𝑝, 𝑚⟩ вероятность совпадения хешей случайных строк 𝑠 и 𝑡 — 1
𝑚
.
Доказательство. Разность многочленов 𝑠 и 𝑡 — случайный многочлен (𝑠 − 𝑡). Далее 6.3.5. ■
Глава #6. 18 октября 2022. 33/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Базовые алгоритмы на строках
Следствие 6.3.8. ∀𝑚, ∀𝑠, 𝑡 𝑃 𝑟𝑝 [ℎ𝑝,𝑚 (𝑠) = ℎ𝑝,𝑚 (𝑡)] ⩽ max(|𝑠|,|𝑡|)
𝑚
.
По-русски: даны фиксированные строки, выбираем случайное 𝑝, оцениваем Pr коллизии.
Доказательство. Подставим многочлен (𝑠 − 𝑡) в лемму 6.3.4. ■
Теперь пусть сравнений строк было много.
Теорема 6.3.9. Пусть дано множество случайных различных строк, и сделано 𝑘 сравнений
⟨𝑝, 𝑚⟩ хешей каких-то из этих строк ⇒ вероятность существования коллизии не более 𝑚
𝑘
.
Доказательство. 𝑃 𝑟[коллизии] ⩽ 𝐸[коллизий] = 𝑘 · 𝑃 𝑟[коллизии при 1 сравнении] = 𝑘/𝑚. ■
Теорема 6.3.10. Пусть дано множество произвольных различных строк длины ⩽ 𝑛,
выбрано случайное 𝑝 и сделано 𝑘 сравнений ⟨𝑝, 𝑚⟩ хешей каких-то из этих строк ⇒
вероятность существования коллизии ⩽ 𝑛𝑘
𝑚
.
Доказательство. Суммарное число корней у 𝑘 многочленов степени ⩽ 𝑛 не более 𝑛𝑘. ■
Замечание 6.3.11. На самом деле оценка 𝑛𝑘
𝑚
из 6.3.10 не достигается. В практических рассчётах
можно смело пользоваться оценкой 𝑚 𝑘
из 6.3.9.
6.3.4. Число различных подстрок (на практике)
Рассмотрим два решения, оценим их вероятности ошибок.
Решение #1: сложить хеши всех 12 𝑛(𝑛 − 1) подстрок в хеш-таблицу.
Решение #2: отдельно решаем для каждой длины, внутри сложим хеши всех ⩽ 𝑛 подстрок в
хеш-таблицу, просуммируем размеры хеш-таблиц.
В первом случае, у нас неявно происходит ≈ 𝑛4 /8 сравнений подстрок ⇒ вероятность наличия
4
коллизии ≈ 𝑛𝑚/8 ⇒ при 𝑛 = 1000 нам точно не хватит 32-битного модуля,
при 𝑛 = 10 000 для 𝑚 ≈ 1018 вероятность коллизии ≈ 800
1
.
Во втором случае ≈ 𝑖=1..𝑛 𝑖2 /2 ≈ 𝑛3 /6 сравнений подстрок ⇒ вероятность наличия коллизии
∑︀
3
≈ 𝑛𝑚/6 ⇒ при 𝑛 = 10 000 для 𝑚 ≈ 1018 вероятность коллизии ≈ 6·10
1
6.
Глава #6. 18 октября 2022. 34/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Базовые алгоритмы на строках
6.3.5. Строка Туэ-Морса
[Пост на codeforces в тему]
Сейчас мы построим коллизию (пару строк) для полиномиального хеша ⟨𝑝, 𝑚 = 2𝑘 ⟩. ∀𝑝, 𝑘.
Def 6.3.12. Определим строку Туэ — Морса. Сначала введём строки 𝑆𝑖 :
• 𝑆0 = 0
• 𝑆1 = 01
• 𝑆2 = 0110
• 𝑆𝑛 = 𝑆𝑛−1 𝑆𝑛−1 (𝑥 — отрицание 𝑥)
Каждая строка является префиксом всех последующих.
Обозначим 𝑡 = 𝑆∞ = lim𝑖→∞ 𝑆𝑖 . 𝑆∞ ∈ {0, 1}N . 𝑆∞ и есть строка Туэ — Морса.
∙ Построение (два способа)
1. Строим рекурсивно по определению.
2. s[i] = __builtin_popcount(i) mod 2 (количество единичных битов в двоичной записи 𝑖)
Второй способ доказывается по индукции 𝑆𝑘 → 𝑆𝑘+1 .
∙ Коллизия
Обозначим 𝐻(𝑠) = hash(𝑠). Для достаточно большого 𝑖 : 𝐻(𝑆𝑖 ) = 𝐻(𝑆𝑖 ) ⇔ 𝐻(𝑆𝑖 ) − 𝐻(𝑆𝑖 ) = 0.
Докажем по индукции, что 𝐻(𝑆𝑖 ) − 𝐻(𝑆𝑖 ) = (𝑝 − 1)(𝑝2 − 1) . . . (𝑝2 − 1):
𝑖−1
𝑆𝑛 = 𝑆𝑛−1 𝑆𝑛−1 , 𝑆𝑛 = 𝑆𝑛−1 𝑆𝑛−1 |𝑆𝑛 | = 2𝑛 . Пусть 𝑓 (𝑛) = 𝐻(𝑆𝑛 ) − 𝐻(𝑆𝑛 ) ⇒ 𝑓 (𝑛) = (𝐻(𝑆𝑛−1 ) −
𝑛−1 𝑛−1 𝑛−1
𝐻(𝑆𝑛−1 )) · 𝑝2 + (𝐻(𝑆𝑛−1 ) − 𝐻(𝑆𝑛−1 )) = 𝑓 (𝑛−1)(𝑝2 − 1) = (𝑝 − 1)(𝑝2 − 1) . . . (𝑝2 − 1).
Вспомним, что 𝑚 = 2𝑘 . Когда 𝑓 (𝑛) занулится?
Если 𝑝 чётно, то при 𝑖 ⩾ 𝑘 имеем 𝑝𝑖 = 0 ⇒ для всех строк длины >𝑘 тривиально строится.
Если 𝑝 нечётно, то заметим, что 𝑝2 −1 = (𝑝+1)(𝑝−1)(𝑝2 −1)(𝑝4 −1) . . . (𝑝2 −1) ⇒ содержит хотя
𝑖 𝑖−1
. .
бы 𝑖+1 чётных сомножителей ⇒ 𝑓 (𝑛) .. 21+2+3+···+𝑛
√ . Достаточно, чтобы 𝑓 (𝑛) .. 2𝑘 .
1 + 2 + 3 + · · · + 𝑛 ⩾ 𝑘 ⇔ 21 𝑛(𝑛+1) ⩾ 𝑘 ⇐ 𝑛 ⩾ 2𝑘.
√ √
Итого получили, что при 𝑛 ⩾ 2𝑘 √
всегда 𝐻(𝑆𝑛 ) = 𝐻(𝑆 𝑛 ) ⇒ есть коллизия для строк длины 2 2𝑘
.
Пример 𝑚 = 264 , 𝑘 = 64 длина 2 128 ⩽ 28 = 256.
Глава #6. 18 октября 2022. 35/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Базовые алгоритмы на строках
6.4. Алгоритм Бойера-Мура
Даны текст 𝑡 и шаблон 𝑠. Требуется найти хотя бы одно вхождение 𝑠 в 𝑡 или сказать, что их
|𝑡|
нет. БМ — алгоритм, решающий эту задачу за время 𝒪( |𝑠| ) в среднем и 𝒪(|𝑡| · |𝑠|) в худшем.
∙ Наивная версия алгоритма
1 for ( p = 0; p <= | t | - | s |; p ++)
2 for ( k = | s | - 1; k >= 0; k - -)
3 if ( t [ p + k ] != s [ k ])
4 break ;
5 if ( k < 0)
6 return 1;
То есть, мы прикладываем шаблон 𝑠 ко всем позициям 𝑡, сравниваем символы с конца.
∙ Оптимизации
Каждый раз мы сдвигаем шаблон на 1 вправо.
Сдвинем лучше сразу так, чтобы несовпавший символ текста t[p+k] совпал с каким-либо сим-
волом шаблона. Эта оптимизация называется «правилом плохого символа».
1 for ( i = 0; i < | s |; i ++)
2 pos [ s [ i ]]. push_back ( i ) ; // для каждого символа список позиций
3 for ( p = 0; p <= | t | - | s |; p += dp )
4 for ( k = | s | - 1; k >= 0; k - -)
5 if ( t [ p + k ] != s [ k ])
6 break ;
7 if ( k < 0)
8 return 1
9 auto & v = pos [ t [ p + k ]]; // нужно в 𝑣 найти последний элемент меньший 𝑘
10 for ( i = v . size () - 1; i >= 0 && v [ i ] >= k ; i - -)
11 ;
12 dp = ( k - ( i < 0 ? -1 : v [ i ]) ) ; // сдвигаем так, чтобы вместо s[k] оказался s[v[i]]
Вторая оптимизация «правило хорошего суффикса» — использовать информацию, что суффикс
𝑢 = s[k+1:] уже совпал с текстом ⇒ нужно сдвинуть 𝑠 до следующего вхождения 𝑢. Тут
нам поможет 𝑍-функция от 𝑠: |𝑢| = |𝑠|−𝑘−1, мы ищем shift[|u|] = min 𝑗 : 𝑧(𝑠)[𝑗] ⩾ |𝑢|, и
сдвигаем на 𝑗. На самом деле мы даже знаем, что следующий символ не совпадает, поэтому
ищем min 𝑗 : 𝑧(𝑠)[𝑗] = |𝑢|.
1 z <-- z_function ( reverse ( s ) )
2 for ( j = | s | - 1; j >= 0; j - -)
3 shift [ z [ j ]] = r ;
В итоге алгоритм Бойера-Мура сдвигает шаблон на max(𝑥, 𝑦), где 𝑥 = dp, 𝑦 = shift[|s|-k-1]).
Глава #6. 18 октября 2022. 36/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Базовые алгоритмы на строках
Время и память, требуемые на предподсчёт — Θ(|𝑠|). Предподсчёт зависит только от 𝑠.
Пример выполнения Бойера-Мура:
t = «abcabcabbababa», s = «baba»
a b c a b c a b b a a a b a b a
b a b a x= 3, y = 2
b a b a x= 3, y = 2
b a b a x= 1, y = 2
b a b a x= 1, y = 2
b a b a x= 1, y = 2
b a b a ok
Алгоритм можно продолжить модифицировать, например искать min 𝑗 :
после сдвига на 𝑗, совпададут c шаблоном все уже открытые символы в 𝑡.
Тогда в первом же шаге примера сдвиг будет на 4 вместо трёх.
⏟ ⏞. .𝑎 в строке 𝑡 = 𝑏𝑏.
Другой пример: 𝑠 = 𝑎𝑎. ⏟ ⏞. .𝑏. Тогда каждый раз мы сравниваем лишь один
𝑛 𝑚
символ, а сдвигаемся на 𝑛 позиций ⇒ время Θ(|𝑚|/|𝑛|).
Глава #6. 18 октября 2022. 37/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Суффиксный массив
Лекция #7: Суффиксный массив
7 ноября 2022
Def 7.0.1. Суффиксный массив 𝑠 – отсортированный массив суффиксов 𝑠.
Суффиксы сортируем в лексикографическом порядке. Каждый суффикс однозначно задается
позицией начала в 𝑠 ⇒ на выходе мы хотим получить перестановку чисел от 0 до 𝑛−1.
∙ Тривиальное решение: std::sort отработает за 𝒪(𝑛 log 𝑛) операций ‘<’ ⇒ за 𝒪(𝑛2 log 𝑛).
7.1. Построение за 𝒪(𝑛 log2 𝑛) хешами
Мы уже умеем сравнивать хешами строки на равенство, научимся сравнивать их на “>/<”.
Бинпоиском за 𝒪(log(𝑚𝑖𝑛(|𝑠|, |𝑡|))) проверок на равенство найдём 𝑥 = 𝑙𝑐𝑝(𝑠, 𝑡).
Теперь 𝑙𝑒𝑠𝑠(𝑠, 𝑡) = (𝑠[𝑥] < 𝑡[𝑥]). Кстати, в C/C++ после строки всегда идёт символ с кодом 0.
Получили оператор меньше, работающий за 𝒪(log 𝑛) и требующий 𝒪(𝑛) предподсчёта.
Итого: суффмассив за 𝒪(𝑛 + (𝑛 log 𝑛) · log 𝑛) = 𝒪(𝑛 log2 𝑛).
При написании сортировки нам нужно теперь минимизировать в первую очередь именно число
сравнений ⇒ с точки зрения C++::STL быстрее будет работать stable_sort (MergeSort внутри).
Замечание 7.1.1. Заодно научились за 𝒪(log 𝑛) сравнивать на больше/меньше любые подстроки.
7.2. Применение суффиксного массива: поиск строки в тексте
Задача: дана строка 𝑡, приходят строки-запросы 𝑠𝑖 : “является ли 𝑠𝑖 подстрокой 𝑡”.
Предподсчёт: построим суффиксный массив 𝑝 строки 𝑡.
В суффиксом массиве сначала лежат все суффиксы < 𝑠𝑖 , затем ⩾ 𝑠𝑖 ⇒ бинпоиском можно
найти min 𝑘 : 𝑡[𝑝𝑘 :] ⩾ 𝑠𝑖 . Осталось заметить, что (𝑠𝑖 – префикс 𝑡[𝑝𝑘 :]) ⇔ (𝑠𝑖 – подстрока 𝑡).
Внутри бинпоиска можно сравнивать строки за линию, получим время 𝒪(|𝑠𝑖 | log |𝑡|) на запрос.
Можно за 𝒪(log |𝑡|) с помощью хешей, для этого нужно один раз предподсчитать хеши для 𝑡, а
при ответе на запрос насчитать хеши 𝑠𝑖 . Получили время 𝒪(|𝑠𝑖 | + log |𝑡| · log |𝑠𝑖 |) на запрос.
В разд. 7.5 мы улучшим время обработки запроса до 𝒪(|𝑠𝑖 | + log |𝑡|).
7.3. Построение за 𝒪(𝑛2 ) и 𝒪(𝑛 log 𝑛) цифровой сортировкой
Заменим строку s на строку s#, где # – символ, лексикографически меньший всех в s.
Будем сортировать циклические сдвиги s#, порядок совпадёт с порядком суффиксом.
Длину s# обозначим 𝑛.
Решение за 𝒪(𝑛2 ): цифровая сортировка.
Сперва подсчётом по последнему символу, затем по предпоследнему и т.д.
Всего 𝑛 фаз сортировок подсчётом. В предположении |Σ| ⩽ 𝑛 получаем время 𝒪(𝑛2 ).
Суффмассив, как и раньше задаётся перестановкой начал... теперь циклических сдвигов.
Решение за 𝒪(𝑛 log 𝑛): цифровая сортировка с удвоением длины.
Пусть у нас уже отсортированы все подстроки длины 𝑘 циклической строки s#.
Научимся за 𝒪(𝑛) переходить к подстрокам длины 2𝑘.
Глава #7. 7 ноября 2022. 38/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Суффиксный массив
Давайте требовать не только отсортированности но и знания “равны ли соседние в отсортиро-
ванном порядке”. Тогда линейным проходом можно для каждого 𝑖 насчитать тип (цвет) цикли-
ческого сдвига 𝑐[𝑖] : (0 ⩽ 𝑐[𝑖] < 𝑛) ∧ (s[i:i+k) < s[j:j+k) ⇔ 𝑐[𝑖] ⩽ 𝑐[𝑗]).
Любая подстрока длины 2𝑘 состоит из двух половин длины 𝑘 ⇒
переход 𝑘 → 2𝑘 – цифровая сортировка пар ⟨𝑐[𝑖], 𝑐[𝑖+𝑘]⟩.
Прекратим удвоение 𝑘, когда 𝑘 ⩾ 𝑛. Порядки подстрок длины 𝑘 и 𝑛 совпадут.
Замечание 7.3.1. В обоих решениях в случае |Σ| > 𝑛 нужно первым шагом отсортировать и
перенумеровать символы строки. Это можно сделать за 𝒪(𝑛 log 𝑛) или за 𝒪(𝑛 + |Σ|) подсчётом.
Реализация решения за 𝒪(𝑛 log 𝑛).
p[i] – перестановка, задающая порядок подстрок длины s[i:i+k) циклической строки s#.
c[i] – тип подстроки s[i:i+k).
За базу возьмём 𝑘 = 1
1 bool sless ( int i , int j ) { return s [ i ] < s [ j ]; }
2 sort (p , p + n , sless ) ;
3 cc = 0; // текущий тип подстроки
4 for ( i = 0; i < n ; i ++) // тот самый линейный проход, насчитываем типы строк длины 1
5 cc += ( i && s [ p [ i ]] != s [ p [i -1]]) , c [ p [ i ]] = cc ;
Переход: (у нас уже отсортированы строки длины 𝑘) ⇒ (уже отсортированы строки длины 2𝑘
по второй половине) ⇒ (осталось сделать сортировку подсчётом по первой половине).
1 // pos – массив из 𝑛 нулей
2 for ( i = 0; i < n ; i ++)
3 pos [ c [ i ] + 1]++; // обойдёмся без лишнего массива cnt
4 for ( i = 1; i < n ; i ++)
5 pos [ i ] += pos [ i - 1];
6 for ( i = 0; i < n ; i ++) : // p[i] – позиция начала второй половины
7 int j = ( p [ i ] - k ) mod n ; // j – позиция начала первой половины
8 p2 [ pos [ c [ j ]]++] = j ; // поставили подстроку s[j,j+2k) на правильное место в p2
9 cc = 0; // текущий тип подстроки
10 for ( i = 0; i < n ; i ++) // линейным проходом насчитываем типы строк длины 2𝑘
11 cc += ( i && pair_of_c ( p2 [ i ]) != pair_of_c ( p2 [i -1]]) ) , c2 [ p2 [ i ]] = cc ;
12 c2 . swap ( c ) , p2 . swap ( p ) ; // не забудем перейти к новой паре (p,c)
Здесь pair_of_c(i) – пара ⟨c[i], c[(i + k) mod n]⟩ (мы сортировали как раз эти пары!).
Замечание 7.3.2. При написании суффмассива в контесте рекомендуется, прочтя конспект, на-
писать код самостоятельно, без подглядывания в конспект.
7.4. LCP за 𝒪(𝑛): алгоритм Касаи
Алгоритм Касаи считает LCP соседних суффиксов в суффиксном массиве. Обозначения:
∙ 𝑝[𝑖] – элемент суффмассива,
∙ 𝑝−1 [𝑖] – позиция суффикса s[i:] в суффмассиве,
∙ 𝑛𝑒𝑥𝑡𝑖 = 𝑝[𝑝−1 [𝑖] + 1], 𝑙𝑐𝑝𝑖 = LCP(𝑖, 𝑛𝑒𝑥𝑡𝑖 ). Наша задача – насчитать массив 𝑙𝑐𝑝𝑖 .
Утверждение 7.4.1. Если у 𝑖-го и 𝑗-го по порядку суффикса в суффмассиве совпадают первые
𝑘 символов, то на всём отрезке [𝑖, 𝑗] суффмассива совпадают первые 𝑘 символов.
Lm 7.4.2. Основная идея алгоритма Касаи: 𝑙𝑐𝑝𝑖 > 0 ⇒ 𝑙𝑐𝑝𝑖+1 ⩾ 𝑙𝑐𝑝𝑖 − 1.
Глава #7. 7 ноября 2022. 39/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Суффиксный массив
Доказательство. Отрежем у s[i:] и s[next𝑖 :] по первому символу.
Получили суффиксы s[i+1:] и какой-нибудь r.
(s[i:] ̸= s[next𝑖 :]) ∧ (первый символ у них совпадал) ⇒
7.4.1
(r в суффмассиве идёт после s[i+1:]) ∧ (у них совпадает первых 𝑙𝑐𝑝𝑖 −1 символов) ⇒
у s[i+1:] и s[next𝑖+1 ] совпадает хотя бы 𝑙𝑐𝑝𝑖 −1 символ ⇒ 𝑙𝑐𝑝𝑖+1 ⩾ 𝑙𝑐𝑝𝑖 − 1. ■
Собственно алгоритм заключается в переборе 𝑖 ↗ и подсчёте 𝑙𝑐𝑝𝑖 начиная с max(0, 𝑙𝑐𝑝𝑖+1 −1).
Задача: уметь выдавать за ⟨𝒪(𝑛), 𝒪(1)⟩ LCP любых двух суффиксов строки 𝑠.
Решение: используем Касаи для соседних, а для подсчёта LCP любых других считаем RMQ.
RMQ мы решили в прошлом семестре. Например, Фарах-Колтоном-Бендером за ⟨𝒪(𝑛), 𝒪(1)⟩.
7.5. Быстрый поиск строки в тексте
Представим себе простой бинпоиск за 𝒪(|𝑠| log(|𝑡𝑒𝑥𝑡|)). Будем стараться максимально переис-
пользовать информацию, полученную из уже сделанных сравнений.
Для краткости ∀𝑘 обозначим 𝑘-й суффикс (text[p𝑘 :]) как просто 𝑘.
Инвариант: бинпоиск в состоянии [𝑙, 𝑟] уже знает 𝑙𝑐𝑝(𝑠, 𝑙) и 𝑙𝑐𝑝(𝑠, 𝑟).
Сейчас мы хотим найти 𝑙𝑐𝑝(𝑠, 𝑚) и перейти к [𝑙, 𝑚] или [𝑚, 𝑟].
Заметим, 𝑙𝑐𝑝(𝑠, 𝑚) ≥ 𝑚𝑎𝑥{𝑚𝑖𝑛{𝑙𝑐𝑝(𝑠, 𝑙), 𝑙𝑐𝑝(𝑙, 𝑚)}, 𝑚𝑖𝑛{𝑙𝑐𝑝(𝑠, 𝑟), 𝑙𝑐𝑝(𝑟, 𝑚)}} = 𝑥.
Мы умеем искать 𝑙𝑐𝑝(𝑙, 𝑚) и 𝑙𝑐𝑝(𝑟, 𝑚) за 𝒪(1) ⇒ for ( 𝑙𝑐𝑝(𝑠, 𝑚) = 𝑥; можем; 𝑙𝑐𝑝(𝑠, 𝑚)++).
Кстати, 𝑙𝑐𝑝(𝑙, 𝑚) и 𝑙𝑐𝑝(𝑟, 𝑚) не обязательно считать Фарах-Колтоном-Бендером, так как,
аргументы 𝑙𝑐𝑝 – не произвольный отрезок, а вершина дерева отрезков (состояние бинпоиска).
Предподсчитаем 𝑙𝑐𝑝 для всех ⩽ 2|𝑡𝑒𝑥𝑡| вершин и по ходу бинпоиска будем спускаться по Д.О.
Теорема 7.5.1. Суммарное число увеличений на один 𝑙𝑐𝑝(𝑠, ?) не более |𝑥|
Доказательство. Сейчас бинпоиск в состоянии 𝑙𝑖 , 𝑚𝑖 , 𝑟𝑖 . Следующее состояние: 𝑙𝑖+1 , 𝑟𝑖+1 .
Предположим, 𝑙𝑐𝑝(𝑠, 𝑙𝑖 ) ⩾ 𝑙𝑐𝑝(𝑠, 𝑟𝑖 ). Будем следить за величиной 𝑧𝑖 = max{𝑙𝑐𝑝(𝑠, 𝑙𝑖 ), 𝑙𝑐𝑝(𝑠, 𝑟𝑖 )}.
Пусть 𝑙𝑐𝑝(𝑠, 𝑚𝑖 ) < 𝑧𝑖 ⇒ 𝑙𝑐𝑝(𝑠, 𝑚) = 𝑥 ∧ 𝑙𝑖+1 = 𝑙𝑖 ⇒ 𝑧𝑖+1 = 𝑧𝑖 . Иначе 𝑥 = 𝑧𝑖 ∧ 𝑧𝑖+1 = 𝑙𝑐𝑝(𝑠, 𝑚𝑖 ). ■
7.6. (*) Построение за 𝒪(𝑛): алгоритм Каркайнена-Сандерса
На вход получаем строку 𝑠 длины 𝑛, при этом 0 ⩽ 𝑠𝑖 ⩽ 32 𝑛.
Выход – суффиксный массив. Сортируем именно суффиксы, а не циклические сдвиги.
Допишем к строке 3 нулевых символа. Теперь сделаем новый алфавит: 𝑤𝑖 = (𝑠𝑖 , 𝑠𝑖+1 , 𝑠𝑖+2 ).
Отсортируем 𝑤𝑖 цифровой сортировкой за 𝒪(𝑛), перенумеруем их от 0 до 𝑛−1.
Запишем все суффиксы строки 𝑠 над новым алфавитом:
𝑡0 = 𝑤0 𝑤3 𝑤6 . . .
𝑡1 = 𝑤1 𝑤4 𝑤7 . . .
𝑡2 = 𝑤2 𝑤5 𝑤8 . . .
...
𝑡𝑛−1 = 𝑤𝑛−1
Про суффиксы 𝑡3𝑘+𝑖 , где 𝑖 ∈ {0, 1, 2}, будем говорить “суффикс 𝑖-типа”.
Запустимся рекурсивно от строки 𝑡0 𝑡1 . Длина 𝑡0 𝑡1 не более 2⌈ 𝑛3 ⌉.
Глава #7. 7 ноября 2022. 40/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Суффиксный массив
Теперь мы умеем сравнивать между собой все суффиксы 0-типа и 1-типа.
Суффикс 2-типа = один символ + суффикс 0-типа ⇒
их можно рассматривать как пары и отсортировать за 𝒪(𝑛) цифровой сортировкой.
Осталось сделать merge двух суффиксных массивов.
Операция merge работает за линию, если есть “operator <”, работающий за 𝒪(1).
Нужно научиться сравнивать суффиксы 2-типа с остальными за 𝒪(1).
∀𝑖, 𝑗 : 𝑡3𝑖+2 = 𝑠3𝑖+2 𝑡3𝑖+3 , 𝑡3𝑗 = 𝑠3𝑗 𝑡3𝑗+1 ⇒ чтобы сравнить суффиксы 2-типа и 0-типа,
достаточно уметь сравнивать суффиксы 0-типа и 1-типа. Умеем.
∀𝑖, 𝑗 : 𝑡3𝑖+2 = 𝑠3𝑖+2 𝑡3𝑖+3 , 𝑡3𝑗+1 = 𝑠3𝑗+1 𝑡3𝑗+2 ⇒ чтобы сравнить суффиксы 2-типа и 1-типа,
достаточно уметь сравнивать суффиксы 0-типа и 2-типа. Только что научились.
∙ Псевдокод.
Пусть у нас уже есть radixSort(a), возращающий перестановку.
1 def getIndex ( a ) : # новая нумерация, 𝒪(|a| + max𝑖 a[i])
2 p = radixSort ( a )
3 cc = 0
4 ind = [0] * n
5 for i in range ( n ) :
6 cc += ( i > 0 and a [ p [ i ]] != a [ p [i -1]])
7 ind [ p [ i ]] = cc
8 return ind
9
10 def sufArray ( s ) : # 0 ⩽ 𝑠𝑖 ⩽ 32 𝑛
11 n = len ( s )
12 if n < 3: return slowSlowSort ( s )
13 s += [0 , 0 , 0]
14 w = getIndex ( [( s [ i ] , s [ i +1] , s [ i +2]) for i in range ( n ) ] )
15 index01 = range (0 , n , 3) + range (1 , n , 3) # с шагом 3
16 p01 = sufArray ( [ w [ i ] for i in index01 ] )
17 pos = [0] * n
18 for i in range ( len ( p01 ) ) : pos [ index01 [ p01 [ i ]]] = i # позиция 01-суффикса в p01
19 index2 = range (2 , n , 3)
20 p2 = getIndex ( [( w [ i ] , pos [ i +1]) for i in index2 ] )
21 def less (i , j ) : # i mod 3 = 0/1, j mod 3 = 2
22 if i mod 3 == 1: return ( s [ i ] , pos [ i +1]) < ( s [ j ] , pos [ j +1])
23 else : return ( s [ i ] , s [ i +1] , pos [ i +2]) < ( s [ j ] , s [ j +1] , pos [ j +2])
24 return merge ( p01 ∘ index01 , p2 ∘ index2 , less )
25 # ∘ – композиция: index01[p01[i]], ...
Для 𝑛 ⩾ 3 рекурсивный вызов делается от строго меньшей строки:
3 → 1+1, 4 → 2+1, 5 → 2+2, . . . .
Неравенством 𝑠𝑖 ⩽ 32 𝑛 мы в явном виде в коде нигде не пользуемся.
Оно нужно, чтобы гарантировать, что radixSort работает за 𝒪(𝑛).
Есть и другие идеи построения суффиксного массива за линию.
Из более быстрых и современных стоит отметить [Nong, Zhang & Chan (2009)].
Реализации более быстрого SA-IS: [google-implementation], [SK-implementation].
Глава #7. 7 ноября 2022. 41/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Бор
Лекция #7: Бор
7 ноября 2022
7.7. Собственно бор
Бор – корневое дерево. Рёбра направлены от корня и подписаны
буквами. Некоторые вершины бора подписаны, как конечные.
Базовое применение бора – хранение словаря map<string, T>.
Пример из wiki бора, содержащего словарь
{A : 15, to : 7, tea : 3, ted : 4, ten : 12, i : 11, in : 5, inn : 9}.
Для строки s операции add(s), delete(s), getValue(s)
работают, как спуск вниз от корня.
Самый простой способ хранить бор: vector<Vertex> t;, где struct Vertex { int id[|Σ|]; };
Сейчас рёбра из вершины t хранятся в массиве t.id[]. Есть другие структуры данных:
Способ хранения Время спуска по строке Память на ребро
array 𝒪(|𝑠|) 𝒪(|Σ|)
list 𝒪(|𝑠| · |Σ|) 𝒪(1)
map (TreeMap) 𝒪(|𝑠| · log |Σ|) 𝒪(1)
HashMap 𝒪(|𝑠|) с большой const 𝒪(1)
SplayMap 𝒪(|𝑠| + log 𝑆 𝒪(1)
Иногда для краткости мы будем хранить бор массивом int next[N][|Σ|];. next[v][c] == 0
⇔ ребра нет.
7.8. Сортировка строк
Если мы храним рёбра в структуре, способной перебирать рёбра в лексикографическом порядке
(не хеш-таблица, не список), можно легко отсортировать массив строк:
(1) добавить их все в бор, (2) обойти бор слева направо.
Для SplayMap и 𝑛 и строк суммарной длины 𝑆, получаем время 𝒪(𝑆 + 𝑛 log 𝑆).
Для TreeMap получаем 𝒪(𝑆 log |Σ|).
Замечание 7.8.1. Если бы мы научились сортировать строки над произвольным алфавитом за
𝒪(|𝑆|), то для Σ = Z, получилась бы сортировка целых чисел за 𝒪(|𝑆|).
Часто размер алфавита считают 𝒪(1).
Например строчные латинские буквы – 26, или любимый для биологов |{𝐴, 𝐶, 𝐺, 𝑇 }| = 4.
Глава #8. 7 ноября 2022. 42/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Ахо-Корасик и Укконен
Лекция #8: Ахо-Корасик и Укконен
21 ноября 2022
8.1. Алгоритм Ахо-Корасика
Даны текст 𝑡 и словарь 𝑠1 , 𝑠2 , . . . , 𝑠𝑚 , нужно научиться искать словарные слова в тексте.
Простейший алгоритм, отлично работающий для коротких слов, – сложить словарные слова в
бор и от каждой позиции текста 𝑖 попытаться пройти вперёд, откладывая суффикс 𝑡𝑖 вниз по
бору, и отмечая все концы слов, которые мы проходим. Время работы – 𝒪(|𝑡| · max |𝑠𝑖 |).
Ту же асимптотику можно получить, сложив все хеши всех словарных слов в хеш-таблицу, и
проверив, есть ли в хеш-таблице какие-нибудь подстроки 𝑡 длины не более max |𝑠𝑖 |.
Давайте теперь соптимизируем первое решение также, как префикс-функция, позволяет про-
стейший алгоритм поиска подстроки в строке улучшить до линейного времени. Обобщение
префикс-функции на бор – суффиксные ссылки:
Def 8.1.1. ∀ вершины бора 𝑣:
𝑠𝑡𝑟[𝑣] – строка, написанная на пути от корня бора до 𝑣.
𝑠𝑢𝑓 [𝑣] – вершина бора, соответствующая самому длинному суффиксу 𝑠𝑡𝑟[𝑣] в боре.
∀ позиции текста 𝑖 насчитаем вершину бора 𝑣𝑖 : 𝑠𝑡𝑟[𝑣𝑖 ] – суффикс 𝑡[0:𝑖), |𝑠𝑡𝑟[𝑣𝑖 ]| → max.
Пересчёт 𝑣𝑖 :
1 v [0] = root , p = root
2 for ( i = 0; i < | t |; i ++)
3 while ( next [ p ][ t [ i ]] == 0) // нет ребра
4 p = suf [ p ]
5 v [ i +1] = p = next [ p ][ t [ i ]]
Чтобы цикл while всегда останавливался введём фиктивную вершину f и
сделаем suf[root] = f, ∀𝑐 next[f][c] = root.
Поиск словарных слов. Пометим все вершины бора, посещённые в процессе: used[v𝑖 ] = 1. В
конце алгоритма поднимем пометки вверх по суффиксным ссылкам: used[v] ⇒ used[suf[v]].
Для 𝑖-го словарного слова при добавлении мы запомнили вершину end[i], тогда наличии этого
слова в тексте лежит в used[end[i]]. Также можно насчитывать число вхождений.
Суффссылки. Чтобы всё это счастье работало осталось насчитать суффссылки.
Способ #1: полный автомат.
1 suf [ v ] = go [ suf [ parent [ v ]]][ parent_char [ v ]];
2 go [ v ][ c ] = ( next [ v ][ c ] ? next [ v ][ c ] : next [ suf [ v ]][ c ]) ;
Мы хотим, чтобы от parent[v] и suf[v] всё было уже посчитано ⇒ нужно или перебирать
вершины в порядке bfs от корня (1a), или считать эту динамику рекурсивно-лениво (1b).
Способ #2: пишем bfs от корня и пытаемся продолжить какой-нибудь суффикс отца.
1 q <-- root
2 while q --> v :
3 z = suf [ parent [ v ]]
4 while next [ z ][ parent_char [ v ]] == 0:
Глава #8. 21 ноября 2022. 43/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Ахо-Корасик и Укконен
5 z = suf [ z ]
6 suf [ v ] = next [ z ][ parent_char [ v ]]
7 for ( auto [c , vertex ] : next [ v ]) q <-- c
Этот способ экономнее по памяти, если next – не массив, а, например, map<int,int> (2).
Теорема 8.1.2. Время построения линейно от длины суммарной строк, но не от размера бора.
Доказательство. Линейность от размера бора ломается на примере «бамбук длины 𝑛 из букв
𝑎, из листа которого торчат рёбра по 𝑛 разным символам». Линейность от суммарной длины
строк следует из того, что если рассмотреть путь, соответствующий ∀ словарному слову 𝑠𝑖 ,
то при вычислении суффссылок от вершин именно этого пути, указатель 𝑧 в while всё время
поднимался, а затем опускался не более чем на 1 ⇒ сделал не более 2|𝑠𝑖 | шагов. ■
Сравнение способов.
Пусть размер алфавита равен 𝑘, число вершин бора 𝑉 , сумма длин строк в словаре 𝑆.
Заметим 𝑉 ⩽ 𝑆, но может быть сильно меньше, если у строк длинный общий префикс.
(1a) Ровно Θ(𝑘 · 𝑉 + 𝑆) времени, Θ(𝑘 · 𝑉 ) памяти.
(1b) В худшем случае 𝑘 · 𝑉 , но на практике за счёт ленивости быстрее.
(2) Θ(𝑆) времени, Θ(𝑉 ) памяти (линия и там, и там). Времени именно 𝑆, не 𝑉 .
8.2. Суффиксное дерево, связь с массивом
Def 8.2.1. Сжатый бор: разрешим на ребре писать не только букву, но и строку. При этом
из каждой вершины по каждой букве выходит всё ещё не более одного ребра.
Def 8.2.2. Суффиксное дерево – сжатый бор построенный из суффиксов строки.
Lm 8.2.3. Сжатое суффиксное дерево содержит не более 2𝑛 вершин.
Доказательство. Индукция: база один суффикс, 2 вершины, добавляем суффиксы по одному,
каждый порождает максимум +1 развилку и +1 лист. ■
∙ Построение суффдерева из суффмассива+LCP
Пусть мы уже построили дерево из первых 𝑖 суффиксов в порядке суффмассива. Храним путь
от корня до конца 𝑖-го. Чтобы добавить (𝑖 + 1)-й, поднимаемся до высоты 𝐿𝐶𝑃 (𝑖, 𝑖+1) и делаем
новую развилку, новый лист. Это несколько pop-ов и не более одного push-а. Итого 𝒪(𝑛).
∙ Построение суффмассива+LCP из суффдерева
Считаем, что дерево построено от строки 𝑠$ ⇒ (листья = суффиксы).
Обходим дерево слева направо. Если в вершине используется неупорядоченный map для хране-
ния рёбер, сперва отсортируем их. При обходе выписываем номера листьев-суффиксов.
𝐿𝐶𝑃 (𝑖, 𝑖 + 1) – максимально высокая вершина, из пройденных по пути из 𝑖 в 𝑖 + 1.
Время работы 𝒪(𝑛) или 𝒪(𝑛 log |Σ|).
Глава #8. 21 ноября 2022. 44/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Ахо-Корасик и Укконен
8.3. Суффиксное дерево, решение задач
∙ Число различных подстрок.
Это ровно суммарная длина всех рёбер. Так как любая подстрока есть префикс суффикса ⇒
откладывается от корня дерева вниз до «середины» ребра.
∙ Поиск подстрок в тексте.
Строим суффдерево от текста. ∀ строку 𝑠 можно за 𝒪(|𝑠|) искать в тексте спуском по дереву.
∙ Общая подстрока 𝑘 строк.
Построим дерево от 𝑠1 #1 𝑠2 #2 . . .𝑠𝑘 #𝑘 , найдём самую глубокую
∑︀ вершину, в поддереве которой со-
держатся суффиксы 𝑘 различных типов. Время работы 𝒪( |𝑠𝑖 |), оптимально по асимптотике.
Константу времени работы можно улучшать за счёт уменьшения памяти – строить суффдерево
не от конкатенации, а лишь от одной из строк.
8.4. Алгоритм Укконена
Обозначение: 𝑆𝑇 (𝑠) – суффиксное дерево строки 𝑠.
Алгоритм Укконена – онлайн алгоритм построения суффиксного дерево. Нам поступают по
одной буквы 𝑐𝑖 , мы хотим за амортизированное 𝒪(1) из 𝑆𝑇 (𝑠) получать 𝑆𝑇 (𝑠𝑐𝑖 ).
За квадрат это делать просто: храним позиции концов всех суффиксов, каждый из них про-
длеваем вниз на 𝑐𝑖 , если нужно, создаём при этом новые рёбра/вершины.
Ускорение #1: суффиксы, ставшие листьями, растут весьма однообразно – рассмотрим ребро
[𝑙, 𝑟), за которое подвешен лист, тогда всегда происходит r++. Давайте сразу присвоим [𝑙, ∞).
Теперь опишем жизненный цикл любого суффикса:
рождается в корне, ползёт вниз по дереву, разветвляется, становится саморастущим листом.
Нам интересно обработать только момент разветвления.
Lm 8.4.1. Суффикс длины 𝑘 не разветвился ⇒ все более короткие тоже не разветвились.
Доказательство. Суффикс длины 𝑘 не разветвился ⇒ он встречался в 𝑠 как подстрока.
Все более короткие являются его суффиксами ⇒ тоже встречаются в 𝑠 ⇒ не разветвятся. ■
Ускорение #2: давайте хранить только позицию самого длинного неразветвившегося суффикса.
Пока он спускается по дереву, ничего не нужно делать. Как только он разветвится, нужно
научиться быстро переходить к следующему по длине (отрезать первую букву).
Ускорение #3: отрезать первую букву = перейти по суффссылке, давайте от всех вершин под-
держивать суффссылки. Если мы были в вершине, когда не смогли пойти вниз, теперь всё
просто, перейдём по её суффссылке. Если же мы стояли посередине ребра и создали новую
вершину 𝑣, от неё следует посчитать суффссылку. Для этого возьмём суффссылку её отца 𝑝[𝑣]
и из 𝑠𝑢𝑓 [𝑝[𝑣]] спустимся вниз на строку, соединяющую 𝑝[𝑣] и 𝑣.
1 void build ( char * s ) :
2 int N = strlen ( s ) , VN = 2 * Ns ;
3 int vn = 2 , v = 1 , pos ; // идём по ребру из p[v] в v, сейчас стоим в pos
4 int suf [ VN ] , l [ VN ] , r [ VN ] , p [ VN ]; // «ребро p[v] → v» = s[l[v]:r[v])
5 map < char , int > t [ VN ]; // собственно рёбра нашего бора
6 for ( int i = 0; i < |Σ|; i ++) t [0][ i ] = 1; // 0 = фиктивная, 1 = корень
7 l [1] = -1 , r [1] = 0 , suf [1] = 0;
Глава #8. 21 ноября 2022. 45/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Ахо-Корасик и Укконен
8 for ( int n = 0; n < N ; n ++) :
9 char c = s [ n ];
10 auto new_leaf = [&]( int v ) {
11 p [ vn ] = v , l [ vn ] = n , r [ vn ] = ∞, t [ v ][ c ] = vn ++;
12 };
13 go :;
14 if ( r [ v ] <= pos ) { // дошли до вершины, конца ребра
15 if (! t [ v ]. count ( c ) ) { // по символу 𝑐 нет ребра вперёд, создаём
16 new_leaf ( v ) , v = suf [ v ] , pos = r [ v ];
17 goto go ;
18 }
19 v = t [ v ][ c ] , pos = l [ v ] + 1; // начинаем идти по новому ребру
20 } else if ( c == s [ pos ]) {
21 pos ++; // спускаемся по ребру
22 } else {
23 int x = vn ++; // создаём развилку
24 l [ x ] = l [ v ] , r [ x ] = pos , l [ v ] = pos ;
25 p[x] = p[v], p[v] = x;
26 t [ p [ x ]][ s [ l [ x ]]] = x , t [ x ][ s [ pos ]] = v ;
27 new_leaf ( x ) ;
28 v = suf [ p [ x ]] , pos = l [ x ]; // вычисляем позицию следующего суффикса
29 while ( pos < r [ x ])
30 v = t [ v ][ s [ pos ]] , pos += r [ v ] - l [ v ];
31 suf [ x ] = ( pos == r [ x ] ? v : vn ) ;
32 pos = r [ v ] - ( pos - r [ x ]) ;
33 goto go ;
34 }
Теорема 8.4.2. Суммарное время работы 𝑛 первых шагов равно 𝒪(𝑛).
Доказательство. Понаблюдаем за величиной 𝑧 «число вершин на пути от корня до нас».
Пока мы идём вниз, 𝑧 растёт, когда переходим по суффссылке, 𝑧 уменьшается максимум на 1
⇒ возьмём потенциал 𝜙 = −𝑧, суммарное число шагов вниз не больше 𝑛. ■
8.5. LZSS
Решим ещё одну задачу – сжатие текста алгоритмом LZSS.
В отличии от использования массива, дерево даёт чисто линейную асимптотику и простейшую
реализацию – насчитаем для каждой вершины 𝑙[𝑣] = самый левый суффикс в поддереве и при
попытки найти 𝑗 < 𝑖 : 𝐿𝐶𝑃 (𝑗, 𝑖) = max будем спускаться из корня, пока 𝑙[𝑣] < 𝑖.
Глава #8. 21 ноября 2022. 46/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Хеширование
Лекция #8: Хеширование
21 ноября 2022
8.6. Универсальное семейство хеш функций
[wiki] [итмо-конспект] [Carter,Wegman’1977]
Def 8.6.1. Хеш-функция. Сжимающее отображение ℎ : 𝑈 → 𝑀 , |𝑈 | > |𝑀 |, |𝑀 | = 𝑚.
Def 8.6.2. Универсальная система хеш-функций (1-я версия определения).
Множество хеш-функций ℋ – универсальная система, если
1
∀𝑥, 𝑦 𝑥 ̸= 𝑦 : 𝑃 𝑟 [ℎ(𝑥) = ℎ(𝑦)] ≤ 𝑚
ℎ∈ℋ
Def 8.6.3. Универсальная система хеш-функций (2-я версия определения ≡ 1-й).
∑︁
∀𝑥, 𝑦 𝑥 ̸= 𝑦 : [ℎ(𝑥) = ℎ(𝑦)] ≤ |ℋ|
𝑚
ℎ∈ℋ
Теорема 8.6.4. ℋ𝑝,𝑚 = {(𝑎, 𝑏) : 𝑥 → ((𝑎𝑥 + 𝑏) mod 𝑝) mod 𝑚}, 𝑎 ∈ [1, 𝑝), 𝑏 ∈ [0, 𝑝).
Утверждение: ℋ𝑝,𝑚 универсально для |𝑈 | = [0, 𝑝) и 𝑀 = [0, 𝑚) (𝑚 ⩽ 𝑝).
Доказательство. Зафиксируем пару 𝑥, 𝑦. Проверим для неё определение универсальности.
𝑓 (𝑥) = 𝑎𝑥 + 𝑏 mod 𝑝, 𝑓 (𝑦) = 𝑎𝑦 + 𝑏 mod 𝑝
𝑓 (𝑥) = 𝑓 (𝑦) ⇔ 𝑎(𝑥−𝑦) ≡ 0 mod 𝑝 ⇒ (𝑎 ̸= 0, 𝑥 ̸= 𝑦) по модулю 𝑝 коллизий нет и
[︀ ]︀
у нас есть биекция ⟨𝑎, 𝑏 : 𝑎 ̸= 0⟩ ↔ ⟨𝑓 (𝑥), 𝑓 (𝑦) : 𝑓 (𝑥) ̸= 𝑓 (𝑦)⟩.
Осталось понять, для скольки хеш-функций 𝑓 (𝑥) ≡ 𝑓 (𝑦) mod 𝑚?
биекция (*)
|ℋ|
[𝑓 (𝑥) ≡ 𝑓 (𝑦) mod 𝑚] ⩽ 𝑝(𝑝−1)
∑︀ ∑︀
[𝑓 (𝑥) ≡ 𝑓 (𝑦) mod 𝑚] = 𝑚
= 𝑚
(𝑎,𝑏),𝑎̸=0 𝑓 (𝑥)̸=𝑓 (𝑦)
(*) Зафиксируем 𝑓 (𝑥), будем перебирать 𝑓 (𝑦) = 𝑓 (𝑥)+1, 𝑓 (𝑥)+2, . . . В каждом блоке из 𝑚
вариантов, только последний даёт 𝑓 (𝑥) ≡ 𝑓 (𝑦) mod 𝑚 ⇒ таких 𝑓 (𝑦) ровно ⌊ 𝑝−1
𝑚
⌋ ⩽ 𝑝−1
𝑚
. ■
8.7. Оценки для хеш-таблицы с закрытой адресацией
[wiki] [wiki-probability] [2-choice-hashing]
Пусть у нас есть универсальное семейство хеш-функций ℋ.
Хеш-таблица с закрытой адресацией (на списках) – вероятностный алгоритм.
Вся вероятностная часть заключается в том, что мы выбираем случайную ℎ ∈ ℋ.
Операции Find(x), Del(x) работают за длину списка.
Оценим для них матожидание времени работы.
𝐸[𝑡𝑖𝑚𝑒(𝑓 𝑖𝑛𝑑(𝑧))] = 𝐸[длины списка] = 𝑃 𝑟[ℎ(𝑥) = ℎ(𝑧)] = 1 + (𝑛−1)· 𝑚1 = 𝒪(1 + 𝑛
где
∑︀
𝑚
),
𝑥∈𝑡𝑎𝑏𝑙𝑒
𝑛 – количество элементов в таблице, а 𝑚 – размер таблицы (и диапазон хеш-функции).
Замечание 8.7.1. Мы оценили именно матожидание времени работы. Матожидание средней
длины списка всегда 𝑚
𝑛
, даже если все элементы всегда класть в один список.
Глава #8. 21 ноября 2022. 47/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Хеширование
Теперь несколько утверждений без доказательства.
Утверждение 8.7.2. 𝐸[максимальной длины списка] = 𝒪(log 𝑛)
Утверждение 8.7.3. 2-choice hashing. Модифицируем хеш-таблицу: будем использовать две
хеш-функции ℎ1 , ℎ2 и при добавлении элемента будем выбирать из списков 𝑎[ℎ1 (𝑥)], 𝑎[ℎ2 (𝑥)]
список меньшей длины. Тогда 𝐸[максимальной длины списка] = Θ(log log 𝑛).
8.8. Оценки других функций для хеш-таблиц
Популярна функция 𝑥 → 𝑥 mod 𝑛, где 𝑛 размер таблицы (число списков, длина массива от-
крытой адресации). Поскольку все 𝑥-ы могут иметь одинаковый остаток по модулю 𝑛, ∀𝑛∃
контртест. Новая идея, давайте брать случайное 𝑛 ∈ P ∩ [𝑁 , 2𝑁 ), и хеш функцию из семейства
ℋ = 𝑠𝑖𝑧𝑒 ∈ P ∩ [𝑛, 2𝑛), 𝑥 → 𝑥 mod 𝑠𝑖𝑧𝑒.
Lm 8.8.1. ∀𝑥 ̸= 𝑦 𝑃 𝑟ℎ∈ℋ [ℎ(𝑥) = ℎ(𝑦)] ⩽ log𝑛 𝑚
𝑛/ ln 𝑛
.
Доказательство. Коллизция (𝑥 − 𝑦) ≡ 0 ⇒ оцениваем вероятность попасть в простой делитель
(𝑥 − 𝑦). У числа до 𝑚 не более log𝑛 𝑚 простых делителей ⩾𝑛, простых на [𝑛, 2𝑛) ≈ ln𝑛𝑛 . ■
Для хеш-таблицы на списках нам этого хватает. У хеш-таблицы с открытой адресацией есть
ещё проблема с тестом: [1, 2, 3, . . . , 𝑛4 ] + 𝑛4 случайных ключей, на нём работает за квадрат.
8.9. (-) Фильтр Блюма
[wiki] [итмо-конспект]
Прелюдия.
Мы хотим вероятностную структуру данных, которая умеет делать всего две операции –
добавлять 𝑥, и проверять, добавлен ли уже в структуру 𝑥.
Главная фича нашей структуры по сравнению с более простыми аналогами (массив, хеш-
таблица) — очень мало памяти, 𝒪(𝑛) бит.
Собственно структура.
Хотим хранить не более 𝑛 𝑥-ов. Для этого у нас есть 𝑚 бит и 𝑘 хеш-функций ℎ1 , . . . , ℎ𝑘 .
1 bitset <m > a ; // 𝑚 нулей
2 Add ( x ) : a [ h 1 ] = a [ h 2 ] = . . . = a [ h 𝑘 ] = 1;
3 Find ( x ) : return a [ h 1 ] & a [ h 2 ] & . . . & a [ h 𝑘 ];
Собственно алгоритм окончен, осталось разобраться, с какой вероятностью это работает.
Во-первых, если Find вернул 0, элемент точно ещё не был добавлен.
Оценим ошибку Find-а, который вернул 1. При оценке для простоты предположим, что все 𝑛𝑘
значений, которые вернули 𝑘 хеш-функций данных 𝑛 элементов, равномерно распределены.
(︀ −𝑘𝑛 )︀𝑘
𝑃 𝑟[в 𝑖-й ячейке 0] = (1−1/𝑚)𝑘𝑛 ≈ 𝑒𝑥𝑝( −𝑘𝑛
𝑚
) ⇒ 𝑃 𝑟[ложного срабатывания Find] = (1 − 𝑒𝑥𝑝 𝑚
)
Посчитаем, какое при фиксированных ⟨𝑛, 𝑚⟩ оптимально выбрать 𝑘?
Дифференцируем по 𝑘, решаем 𝑃 𝑟′ (𝑘) = 0, получаем 𝑘 = (ln 2)· 𝑚
𝑛
и 𝑃 𝑟[𝑒𝑟𝑟𝑜𝑟] = 2−𝑘 = 0.6185𝑚/𝑛 .
Например, если нам дали лишь 5𝑛 бит на всё про всё, мы достигли вероятности ошибки ≈9%.
∙ Применение и улучшения
[bio’2013]. В биоинформатике, сборка генома в небольшой памяти.
[xor-filter]. Если мы знаем ключи заранее, можно сделать оптимальнее по памяти.
Глава #8. 21 ноября 2022. 48/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Хеширование
Если у нас есть в оперативной (быстрой) памяти только фильтр Блюма от большого множе-
ства. Фильтр Блюма влезает, большая структура не влезает: всегда сперва спросили у фильтра
Блюма «а есть ли вообще во мне такое» и, только если есть, спросили сложную структуру.
Любое место, где поможет кеширование информации «есть ли во мне такое» в быстрой памяти.
Говорят, можно применять в block-list (по ip, по id канала, по нику) и т.д.
8.10. (-) Совершенное хеширование
[wiki] [итмо-конспект] [cs.cmu.edu] [practice:bbhash] [Fredman’1984]
Вспомним, как мы ищем младший бит 64-битного целого 𝑥.
1 uint64_t i2 ( uint64_t x ) { return x & (~ x + 1) ; }
Таким образом мы получили младший бит 𝑖 в форме 2𝑖 .
Осталось сделать последнее действие, 2𝑖 → 𝑖.
1 int table [67];
2 void init () { forn (i , 64) table [2𝑖 % 67] = i ; }
3 int get ( uint64_t i2 ) { return table [ i2 % 67]; }
Здесь все остатки по модулю 67 различны ⇒
мы получили детерминированный алгоритм поиска номера младшего бита за 𝒪(1).
𝑥 → 𝑥 mod 67 – пример совершенной хеш-функции для множества
фиксированных ключей {𝑥0 = 20 , 𝑥1 = 21 , . . . , 𝑥63 = 263 }.
Def 8.10.1. Совершенная хеш-функция – любая инъективная функция.
То есть, хеш-функция, у которой по определению не возникает коллизий.
Чтобы построить такую хеш-функцию, нам, конечно, нужно заранее знать набор ключей
𝑥1 , 𝑥2 . . . , 𝑥𝑘 (см. пример с младшим битом).
Задача построения совершенной хеш-функции.
Дан набор ключей 𝑥1 , 𝑥2 , . . . , 𝑥𝑛 , найти такую функцию
ℎ : 𝑋 = {𝑥1 , 𝑥2 , . . . , 𝑥𝑛 } → [0, 𝑚) ∩ Z, что все ℎ(𝑥𝑖 ) различны и функция вычислима за 𝒪(1).
𝑚 будем называть размером хеш-функции (𝑚 в итоге станет размером массива хеш-таблицы).
8.10.1. (-) Одноуровневая схема
Возьмём 𝑚 = 𝑛2 и любое универсальное семейство хеш-функций ℋ : 𝑋 → [0, 𝑚).
Например, ℋ = {ℎ𝑎,𝑏 (𝑥) = ((𝑎𝑥 + 𝑏) mod 𝑝) mod 𝑚}, 𝑚 ⩽ 𝑝, 𝑝 простое.
𝐸(количества коллизий) = 12 𝑛(𝑛−1) · 1
𝑚
< 1
2
⇒ с вероятностью хотя бы 1
2
коллизий нет ⇒
Алгоритм: берём случайную ℎ ∈ ℋ, пока не повезёт. 𝐸(времени работы) = 𝒪(𝑛), размер 𝑛2 .
8.10.2. (-) Двухуровневая схема
Возьмём 𝑚 = 𝑛 и любое универсальное семейство хеш-функций ℋ : 𝑋 → [0, 𝑚).
Выберем случайную ℎ ∈ ℋ и посмотрим на списки 𝐴𝑦 = {𝑥 | ℎ(𝑥) = 𝑦}.
Для каждого 𝐴𝑦 построим одноуровневую совершенную хеш-функцию 𝑓𝑦 .
Оценим суммарный диапазон всех внутренних уровней:
∑︁ ∑︁
𝐸( |𝐴𝑦 |2 ) = 𝑃 𝑟[ℎ(𝑥𝑖 ) = ℎ(𝑥𝑗 )] = 𝑛 · 1 + 𝑛(𝑛−1) · 𝑛1 = 2𝑛−1
𝑦 𝑖𝑗
Глава #8. 21 ноября 2022. 49/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Хеширование
Осталось внутренние уровни выписать в один массив.
Для этого берём префиксные суммы: pref_sum[y+1] = pref_sum[y] + |𝐴𝑦 |2 .
Итого, наша хеш-функция: int hash(x) { y = h(x); return pref_sum[y] + f[y](x); }
𝐸(времени работы) = 𝒪(𝑛), 𝐸(размера) = 2𝑛−1.
Глава #8. 21 ноября 2022. 50/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Хеширование
8.10.3. (*) Графовый подход
[[RandomGraphs|book]]
Начнём с забавного факта
Утверждение 8.10.2. Рассмотрим случайны неорграф из 3𝑛 вершин и 𝑛 рёбер.
𝑃 𝑟[отсутствия циклов] ⩾ 21 .
Строим совершенную хеш-функцию от 𝑥1 , . . . , 𝑥𝑛 .
Возьмём 𝑚 = 3𝑛 и любое универсальное семейство хеш-функций ℋ : 𝑋 → [0, 𝑚).
Выберем ℎ1 , ℎ2 ∈ 𝐻. Каждому 𝑥𝑖 сопоставим ребро ⟨ℎ1 (𝑥𝑖 ), ℎ2 (𝑥𝑖 )⟩.
Если граф не ацикличен, выберем другие хеш-функции, повторим. Пусть ацикличен.
Тогда запишем значения 𝑓 в вершинах и определим ℎ𝑎𝑠ℎ(𝑥𝑖 ) = (𝑓 [ℎ1 (𝑥𝑖 )] + 𝑓 [ℎ2 (𝑥𝑖 )]) mod 𝑛.
Как записать значения 𝑓 , чтобы было верно ℎ𝑎𝑠ℎ(𝑥𝑖 ) = 𝑖? Граф – лес. В каждом дереве в корне
пишем ноль (любое число), остальные числа расставит dfs по дереву.
8.11. (*) Хеширование кукушки
[wiki] [Pagh,Rodler’2001]
За сколько работает хеш-таблица на списках?
Add за 𝒪(1) в худшем, Find и Del за 𝒪(1) в среднем.
Сейчас мы придумаем, как сделать наоборот:
Add за 𝒪(1) в среднем, Find и Del за 𝒪(1) в худшем.
Заведём массив 𝑎 размера 𝑚 = 3𝑛 и две хеш-функции: ℎ1 , ℎ2 ∈ ℋ𝑚 .
∀ элемент 𝑥 всегда живёт в одной из двух ячеек — ℎ1 (𝑥) или ℎ2 (𝑥).
Find и Add, очевидно, обращаются не более чем к двум ячейкам — ℎ1 (𝑥) и ℎ2 (𝑥).
Add сперва ищет свободную среди ℎ1 (𝑥), ℎ2 (𝑥). Если заняты обе, начнём выталкивать элементы:
1 Add I f B o t h A r eO c c u p i e d ( x ) :
2 i = h1 ( x )
3 while ( a [ i ] != -1) :
4 y = a [ h1 ( x ) ] , a [ h1 ( x ) ] = x , x = y
5 i = h 1 [ y ] + h 2 [ y ] - i // вторая из двух ячеек, где может жить 𝑦
Почему и за сколько работает Add?
Представим себе граф с рёбрами между ℎ1 (𝑥) и ℎ2 (𝑥). Как мы уже знаем, он с большой веро-
ятностью ацикличен (значит Add хотя бы не циклится). Теперь осталось дождаться, пока голос
Белы Боллобаша (автор книги Random Graphs) не нашепчет нам недостающую мудрость —
глубина случайного дерева 𝒪(log 𝑛).
Глава #8. 21 ноября 2022. 51/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Теория чисел
Лекция #9: Теория чисел
пропушенная лекция
9.1. (-) Решето Эратосфена
Задача: найти все простые от 1 до 𝑛.
Решето Эратосфена предлагает вычёркивать числа, кратные уже найденным простым:
1 vector < bool > is_prime ( n + 1 , 1) ; // Хорошо по памяти даже при 𝑛 ≈ 109 !
2 is_prime [0] = is_prime [1] = 0;
3 for ( int i = 2; i <= n ; i ++)
4 if ( is_prime [ i ]) // Нашли новое простое!
5 for ( int j = i + i ; j <= n ; j += i )
6 is_prime [ j ] = 0; // cnt++, чтобы определить константу
Замечание 9.1.1. Сейчас для каждого числа мы находим лишь один бит. Код легко
модифицировать, чтобы для каждого числа находить наименьший простой делитель.
Данную версию кода можно соптимизировать в константу раз, пользуясь тем,
что у любого не простого числа есть делитель не более корня.
1 for ( int i = 2; i * i <= n ; i ++) // cnt++, чтобы определить константу
2 if ( is_prime [ i ])
3 for ( int j = i * i ; j <= n ; j += i )
4 is_prime [ j ] = 0; // cnt++, чтобы определить константу
Можно ещё соптимизить: мы ищем только нечётные простые ⇒
внешний цикл можно вести только по нечётным 𝑖, а во внутреннем прибавлять 2𝑖.
Теперь у нас три версии решета, отличающиеся не большими оптимизациями.
Эмпирический запуск при 𝑛 = 106 даёт значения cnt: 3.7752 𝑛, 2.1230 𝑛, 0.8116 𝑛 соответственно.
Теорема 9.1.2. Обе версии работают за Θ(𝑛 log log 𝑛).
Доказательство. При достаточно больших 𝑘 верно 0.5 𝑘 log 𝑘 ⩽ 𝑝𝑘 ⩽ 2 𝑘 log 𝑘 (без док-ва).
Так как ⌈ 𝑝𝑛𝑘 ⌉ = 𝒪(1) + 𝑘 log
𝑛
𝑘
, время работы 1-й версии равно
𝑝𝑘 ⩽𝑛 𝑛/ log 𝑛
∑︁ ∑︁
𝑛 𝑛
𝑛 + 𝒪(𝑛) + ⌈ 𝑝𝑘 ⌉ = Θ(𝑛 + 𝑘 log 𝑘
)=
𝑘=𝒪(1) 𝑘=𝒪(1)
∫︁𝑛
1
Θ(𝑛 + 𝑛 𝑥 log 𝑥
𝑑𝑥) = Θ(𝑛 + 𝑛 log log 𝑛 − 𝒪(1)) = Θ(𝑛 log log 𝑛)
𝒪(1)
■
∙ Более быстрое решение.
Чтобы найти все простые от 1 до 𝑛 за 𝒪(𝑛), достаточно модифицировать алгоритм так, чтобы
каждое составное 𝑥 помечать лишь один раз, например, наименьшим простым делителем 𝑥.
Пусть 𝑑[𝑥] – номер наименьшего простого делителя 𝑥 (𝑝𝑟𝑖𝑚𝑒𝑠[𝑑[𝑥]] – собственно делитель).
Пусть 𝑥 = 𝑝𝑟𝑖𝑚𝑒𝑠[𝑑[𝑥]] · 𝑦 ⇒ (𝑑[𝑦] ⩾ 𝑑[𝑥] ∨ 𝑦 = 1)
Алгоритм: перебирать 𝑦, а для него потенциальные 𝑑[𝑥] (простые не большие 𝑑[𝑦]).
Глава #9. пропушенная лекция. 52/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Теория чисел
1 vector < int > primes , d ( n + 1 , -1) ;
2 for ( int y = 2; y <= n ; y ++)
3 if ( d [ y ] == -1)
4 d [ y ] = primes . size () , primes . push_back (\ red { y }) ;
5 for ( int i = 0; i <= d [ y ] && y * primes [ i ] <= n ; i ++)
6 d [ y * primes [ i ]] = i ; // x=y*primes[i], i=d[x]
9.2. (-) Решето и корень памяти (на практике)
√
Пусть нам нужно найти все √ простые на промежутке√(𝑛− 𝑛..𝑛] = (𝑙..𝑟].
Это можно сделать за 𝒪( 𝑛 log log 𝑛) времени и 𝒪( 𝑛) √ памяти. √
1. У не простого числа до 𝑛 есть простой делитель ⩽ 𝑛 ⇒ посчитаем все простые до 𝑛.
2. Будем этими простыми «просеивать» нужный нам интервал... Для простого 𝑝, сперва нужно
пометить 𝑙 − (𝑙 mod 𝑝) + 𝑝, затем с шагом 𝑝 до 𝑟, как в обычном решете.
9.3. (-) Вычисление мультипликативных функций функций на [1, 𝑛]
Def 9.3.1. Функция мультипликативна ⇔ ∀𝑎, 𝑏 : (𝑎, 𝑏) = 1 𝑓 (𝑎𝑏) = 𝑓 (𝑎)𝑓 (𝑏).
Т.е. имея разложение числа на простые 𝑥 = 𝑖 𝑝𝛼𝑖 𝑖 , имеем 𝑓 (𝑥) = 𝑓 (𝑥/𝑝𝛼1 1 )𝑓 (𝑝𝛼1 1 )
∏︀
∙ Примеры:
Простой делитель 𝑛 p[x] = primes[d[x]]
Удобное обозначение y[x] = x / p[x]
Степень этого делителя deg[x] = (d[y[x]] == d[x] ? deg[y[x]] + 1 : 1)
Результат отщепления 𝑝𝛼1 1 rest[x] = (d[y[x]] == d[x] ? rest[y[x]] : y[x])
Собственно 𝑝𝛼1 1 term[x] = x / rest[x]
Число взаимнопростых 𝜙(𝑛) = 𝑛 𝑖 𝑝𝑖𝑝−1
∏︀
phi[x] = phi[rest[x]] * (term[x]/p[x])* (p[x] - 1)
𝑖
Число делителей
∏︀
𝜎0 (𝑛) = 𝑖 (𝛼𝑖 + 1) s0[x] = s0[rest[x]] * (deg[x] + 1)
𝛼 +1
𝑝𝑖 𝑖 −1
Чуть сложнее посчитать сумму делителей: 𝜎1 (𝑛) = 𝑖 (𝑝0𝑖 + 𝑝1𝑖 + · · · + 𝑝𝛼𝑖 𝑖 ) = 𝑖 .
∏︀ ∏︀
𝑝𝑖 −1
Итого: s1[x] = s1[rest[x]] * (term[x] * p[x] - 1)/ (p[x] - 1).
9.4. (*) Число простых на [1, 𝑛] за 𝑛2/3
Минимальный простой делитель 𝑥 обозначаем 𝑑[𝑥]. Массив 𝑑 на [1, 𝑚] мы умеем насчитывать
решетом Эратосфена за 𝒪(𝑚). 𝑑[1] := +∞. Заодно мы нашли за 𝒪(𝑚) все простые на [1, 𝑚].
Def 9.4.1. 𝜋(𝑛) – количество простых чисел от 1 до 𝑛
Def 9.4.2. 𝑓 (𝑛, 𝑘) – |{𝑥 ∈ [1, 𝑛] | 𝑑[𝑥] ⩾ 𝑝𝑘 }|, где 𝑝𝑘 – 𝑘-е простое.
√ √
Lm 9.4.3. 𝜋(𝑛) = 𝜋( 𝑛) + 𝑓 (𝑛, 𝑘( 𝑛)), где 𝑘(𝑥) – номер первого простого, большего 𝑥.
√ √ √
Теперь 𝜋( 𝑛) и 𝑘( 𝑛) найдём за линию решетом, а 𝑓 (𝑛, 𝑘( 𝑛)) рекурсивно по рекурренте:
𝑓 (𝑛, 𝑘) = 𝑓 (𝑛, 𝑘−1) − 𝑓 (⌊𝑛/𝑝𝑘−1 ⌋, 𝑘−1)
Поясним формулу: 𝑓 (𝑛, 𝑘−1) − 𝑓 (𝑛, 𝑘) – количество чисел вида 𝑝𝑘−1 ·𝑥, где 𝑑[𝑥] ⩾ 𝑝𝑘−1 .
(︀ )︀
Количество таких 𝑥 на [1, 𝑛] есть 𝑓 (⌊𝑛/𝑝𝑘−1 ⌋, 𝑘−1).
База: 𝑓 (𝑛, 0) = 𝑛, 𝑓 (0, 𝑘) = 0.
Глава #9. пропушенная лекция. 53/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Теория чисел
∙ Самое важное отсечение.
𝑓 (𝑛, 𝑘) есть количество пар ⟨𝑖, 𝑑[𝑖]⟩ : 𝑖 ⩽ 𝑛 ∧ 𝑑[𝑖] ⩾ 𝑘.
Зафиксируем некое 𝑚, предподсчитаем 𝑑[1..𝑚], теперь ∀𝑛 ⩽ 𝑚 𝑓 (𝑛, 𝑘) – запрос на плоскости.
Более того, мы можем вычислять 𝑓 процедурой вида:
1 int result = 0;
2 void calc ( int n , int k , int sign ) :
3 if ( k == 0)
4 result += sign * n ;
5 else if ( n <= m )
6 queries [ n ]. push_back ({ k , sign }) ;
7 else
8 calc (n , k - 1 , sign ) , calc ( n / p [ k - 1] , k - 1 , - sign ) ;
Тогда в итоге мы получим пачку из 𝑞 запросов на плоскости, уже отсортированных по 𝑛.
Обработаем их одним проходом сканирующей прямой с деревом Фенвика за 𝒪((𝑚 + 𝑞) log 𝑚).
∙ Оценка времени работы, выбор 𝑚.
Если мы считаем 𝜋(𝑛), пришли рекурсией в состояние (𝑥, 𝑘), то 𝑥 = ⌊𝑛/𝑦⌋.
Посчитаем число состояний рекурсии (𝑥, 𝑘), что в 𝑓 (𝑥, 𝑘) отсечение 𝑥 ⩽ 𝑚 ещё не сработало,
а в 𝑓 (𝑥/𝑝𝑘−1 , 𝑘 − 1) уже сработало.
√
1. Есть не более 𝑛 таких состояний с 𝑥 = 𝑛.
2. Если же 𝑥 ̸= 𝑛, 𝑥 = ⌊𝑛/𝑦⌋ ⇒ 𝑝𝑘−1 ⩽ 𝑦 ⩽ 𝑛/𝑚, т.к. простые мы перебираем по убыванию.
Осталось посчитать «число пар ⟨𝑥, 𝑘⟩» = «число пар ⟨𝑦, 𝑝𝑘−1 ⟩», их 𝒪((𝑛/𝑚)2 ).
Вспомнив, что простых до 𝑡 всего Θ(𝑡/ log 𝑡), можно дать более точную оценку: 𝒪((𝑛/𝑚)2 / log 𝑚
𝑛
).
Каждая пара даст 1 запрос, увеличит время scanline на log 𝑚.
В предположении 𝑚 = Θ(𝑛𝛼 ), log 𝑚 = Θ(log 𝑚
𝑛
) ⇒ общее время работы 𝒪(𝑚 log 𝑚 + (𝑛/𝑚)2 ).
Асимптотический минимум достигается, как обычно при 𝑚 log 𝑚 = (𝑛/𝑚)2 ⇒ 𝑚 log1/3 𝑚 = 𝑛2/3 .
Упражнение 9.4.4. Итоговое время работы – 𝑚 log 𝑚 = Θ((𝑛 log 𝑛)2/3 ).
∙ Другие оптимизации.
Предлагают предподсчёт для малых 𝑘: 𝑘 ⩽ 8 на практике, 𝑘 = Θ(log 𝑛/ log log 𝑛) в теории.
Для 𝑛 < 𝑝𝑘−1 можно отвечать не за log, а за 𝒪(1).
Глава #9. пропушенная лекция. 54/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Теория чисел
9.5. Определения
Def 9.5.1. Z/𝑝Z = F𝑝 поле остатков по модулю p.
Def 9.5.2. (Z/𝑚Z)* Группа по умножению {𝑎 : (𝑎, 𝑚) = 1 ∧ 1 ≤ 𝑎 < 𝑚}.
Def 9.5.3. Линейное диофантово уравнение (𝑎, 𝑏, 𝑐 даны, нужно найти 𝑥, 𝑦).
𝑎𝑥 + 𝑏𝑦 + 𝑐 = 0; 𝑥, 𝑦 ∈ Z
𝑎 −1
Деление: 𝑏
=𝑎·𝑏
∙ Алгоритм Евклида
Используется для подсчёта gcd (greatest common divisor):
𝑔𝑐𝑑(𝑎, 𝑏) = 𝑔𝑐𝑑(𝑎−𝑏, 𝑏), повторяя вычитание много раз, получаем 𝑔𝑐𝑑(𝑎, 𝑏) = 𝑔𝑐𝑑(𝑎 mod 𝑏, 𝑏)
Итого: int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }
Замечание: мы одинаково действуем и при 𝑎⩾𝑏, и при 𝑎<𝑏. Что сделает код в этом случае?
Время работы: 1 шаг, чтобы получить 𝑎 ⩾ 𝑏, далее заметим min(𝑏, 𝑎 mod 𝑏) ⩽ 𝑎
2
⇒ за каждые 2
шага 𝑎 будет уменьшаться минимум вдвоё.
9.6. Расширенный алгоритм Евклида
Задача: найти 𝑥 и 𝑦 : 𝑎𝑥 + 𝑏𝑦 = 𝑔𝑐𝑑(𝑎, 𝑏)
Повторим шаг обычного Евклида: найдём 𝑥1 и 𝑦1 для 𝑏 и 𝑎 mod 𝑏: 𝑏𝑥1 + (𝑎 mod 𝑏)𝑦1 = 𝑔𝑐𝑑(𝑎, 𝑏).
Заметим 𝑎 mod 𝑏 = 𝑎 − ⌊ 𝑎𝑏 ⌋𝑏 ⇒ 𝑥 = 𝑦1 , 𝑦 = 𝑥1 − ⌊ 𝑎𝑏 ⌋𝑦1 .
1 def euclid (a , b ) : # returns (x,y): ax + by = gcd(a, b)
2 if b == 0: return 1 , 0
3 x , y = euclid (b , a % b ) ;
4 return y , x - ( a // b ) * y # целочисленное деление
Нерекурсивная реализация: база, 𝑎 · 1 + 𝑏 · 0 = 𝑎
𝑎·0+𝑏·1=𝑏
Мы можем добавить переход из двух строк 𝑎 · 𝑥𝑖 + 𝑏 · 𝑦𝑖 = 𝑟𝑖
𝑎 · 𝑥𝑖+1 + 𝑏 · 𝑦𝑖+1 = 𝑟𝑖+1
в новую 𝑟𝑖+2 = 𝑟𝑖+1 mod 𝑟𝑖 = 𝑟𝑖+1 − 𝑘𝑟𝑖 (𝑘 – частное)
𝑥𝑖+2 = 𝑥𝑖+1 − 𝑘𝑥𝑖
𝑦𝑖+2 = 𝑦𝑖+1 − 𝑘𝑦𝑖
Алгоритм: while 𝑟𝑖+1 ̸= 0 do получить новую строку, i++.
В конце алгоритма ответ содержится в 𝑥𝑖 , 𝑦𝑖 , 𝑟𝑖 .
∙ Решение диофантового уравнения:
Если 𝑐 mod 𝑔𝑐𝑑(𝑎, 𝑏) ̸= 0, то решений нет.
Иначе найдём 𝑥, 𝑦 : 𝑎𝑥 + 𝑏𝑦 = 𝑔𝑐𝑑(𝑎, 𝑏) и домножим уравнение на 𝑐/𝑔𝑐𝑑(𝑎, 𝑏).
9.7. (-) Свойства расширенного алгоритма Евклида
Следующие утверждения обсуждались и доказывались на практике:
(a) ∀𝑖 в строке 𝑎𝑥𝑖 + 𝑏𝑦𝑖 = 𝑟𝑖 верно, что (𝑥𝑖 , 𝑦𝑖 ) = 1
(b) max |𝑥𝑖 | ⩽ |𝑏| и max |𝑦𝑖 | ⩽ |𝑎| ⇒
∀ типа T, если исходные данные помещаются в тип T, то и все промежуточные тоже.
Также на практике были решены следующие задачи:
Глава #9. пропушенная лекция. 55/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Теория чисел
(c) Найдите класс решений уравнения 𝑎𝑥 ≡ 𝑏(mod 𝑚)
(d) Найдите 𝑥, 𝑦 : 𝑎𝑥 + 𝑏𝑦 = 𝑐, |𝑥| + |𝑦| → min
9.8. Обратные в (Z/𝑚Z)* и Z/𝑝Z
Задача: 𝑎 и 𝑚 даны, хотим найти 𝑥 : 𝑎 · 𝑥 ≡ 1 mod 𝑚
Первый способ – решить диофантово уравнение 𝑎𝑥 + 𝑚𝑦 = 1 = 𝑔𝑐𝑑(𝑎, 𝑚)
Другой способ – воспользоваться малой теоремой Ферма или теоремой Эйлера:
𝑎𝑝−1 = 1 mod 𝑝 ⇒ 𝑥 = 𝑎𝑝−2 (для простого)
𝑎𝜙(𝑚) = 1 mod 𝑚 ⇒ 𝑥 = 𝑎𝜙(𝑚)−1 (для произвольного)
Замечание 9.8.1. Функцию Эйлера считать долго!
∏︁ ∏︁ 𝑝𝑖 − 1
Пусть 𝑛 = 𝑝𝛼𝑖 𝑖 ⇒ 𝜙(𝑛) = 𝑛 , для вычисления нужна факторизация 𝑛
𝑝𝑖
Замечание 9.8.2. Способ с расширенным Евклидом лучше: работает ∀𝑚, все промежуточные
значения по модулю ⩽ 𝑎, 𝑏 (для 𝑎, 𝑏 ⩽ 263 хватает int64_t, для Ферма нет).
9.9. (-) Возведение в степень за 𝒪(log 𝑛)
Сводим к 𝒪(log 𝑛) умножениям. Считаем, что одно умножение работает за 𝒪(1).
1 def pow (x , n ) :
2 if n == 0: return 1
3 return pow ( x **2 , n // 2) **2 * ( x if n % 2 == 1 else 1)
Замечание 9.9.1. При возведении в степень, если исходные данные в типе T, то при умножении
по модулю мы можем столкнуться с переполнением T...
Есть два решения: или умножение за 𝒪(1) превратить в 𝒪(log 𝑛) сложений тем же алгоритмом,
или использовать вещественные числа:
1 int64 mul ( int64 a , int64 b , int64 m ) : // 0 ⩽ 𝑎, 𝑏 < 𝑚
2 int64 k = ( long double ) a * b / m ; // посчитано с погрешностью!
3 int64 r = a * b - m * k ; // в знаковом типе формально это UB =(
4 while ( r < 0) r += m ;
5 while ( r >= m ) r -= m ;
6 return r ;
9.10. (-) Обратные в Z/𝑝Z для чисел от 1 до 𝑘 за 𝒪(𝑘)
Сейчас одно обращение работает за 𝒪(log 𝑝) ⇒ задачу мы умеем решать только за 𝒪(𝑘 log 𝑝).
Время улучшать! Используем динамику: зная, обратные к 1..𝑖−1 найдём 𝑖−1 .
Теорема 9.10.1. 𝑖−1 = −⌊ 𝑚𝑖 ⌋ · (𝑚 mod 𝑖)−1
0 ≡ 𝑚 = (𝑚 mod 𝑖) + 𝑖 · ⌊ 𝑚𝑖 ⌋. Домножим на (𝑚 mod 𝑖)−1 :
Доказательство. ■
0 ≡ 1 + (𝑚 mod 𝑖)−1 · 𝑖 · ⌊ 𝑚𝑖 ⌋ ⇒ 𝑖−1 ≡ −(𝑚 mod 𝑖)−1 ⌊ 𝑚𝑖 ⌋
Глава #9. пропушенная лекция. 56/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Теория чисел
9.11. (-) Первообразный корень
Пусть 𝑝 – простое. Работаем в (Z/𝑝Z)* (группа по умножению по модулю 𝑝).
На алгебре вы доказывали (и хорошо бы помнить, как именно!), что
∃𝑔 : {1, 2, . . . , 𝑝−1} = {𝑔 0 , 𝑔 1 , . . . , 𝑔 𝑝−2 }
Такое 𝑔 называется первообрАзным корнем.
∙ Задача проверки.
Дан 𝑔, проверить, является ли он первообразным корнем.
Если не является, то 𝑜𝑟𝑑 𝑔 < 𝑝−1, при этом 𝑜𝑟𝑑 𝑔 | 𝑝−1. То есть, достаточно перебрать все 𝑑
делители 𝑝−1, и для каждого возведением в степень проверить, что 𝑔 𝑑 ̸= 1.
Возведение в степень работает за 𝒪(log 𝑝), если 𝑝 помещается в машинное слово.
На длинных числах умножение и деление с остатком по модулю работают 𝒪(log 𝑝 log log 𝑝),
итого возведение в степень за 𝒪(log2 𝑝 log log 𝑝).
Делители перебирать нужно не все, а только вида 𝑝−1 𝛼
, где 𝛼 – простой делитель 𝑝−1.
Обозначим 𝑝𝑘 – 𝑘-е простое. Далее будем без доказательства пользоваться тем, что 𝑝𝑘 ⩾ 𝑘 log 𝑘.
Lm 9.11.1. Пусть 𝑓 (𝑥) – число различных простых делителей у 𝑥 ⇒ ∀𝑥 𝑓 (𝑥) = 𝒪( logloglog𝑥 𝑥 )
Доказательство. Худший случай: 𝑥 – произведение минимальных простых.
𝑥 = 𝑖=1..𝑘 𝑝𝑖 ⩾ 𝑖=1..𝑘 𝑖 ⇒ log 𝑥 ⩾ 𝑖=1..𝑘 log 𝑖 = Θ(𝑘 log 𝑘) ⇒ 𝑘 = 𝒪( logloglog𝑥 𝑥 ).
∏︀ ∏︀ ∑︀
■
Теорема 9.11.2. Мы научились проверять кандидат 𝑔 за 𝒪(FACT + log3 𝑝).
Факторизация нужна, как раз чтобы найти простые делители 𝑝−1.
∙ Задача поиска.
И сразу решение: ткнём в случайное число, с хорошей вероятностью оно подойдёт.
Из детерминированных решений популярным является перебор в порядке 1, 2, 3, . . .
Shoup’92 доказал, что в предположении обобщённой Гипотезы Римана, нужно 𝒪(𝑙𝑜𝑔 6 𝑝) шагов.
Обозначим как 𝐺(𝑝) множество всех первообразных корней для 𝑝.
Теорема 9.11.3. 𝑎 – случайное от 1 до 𝑝−1 ⇒ 𝑃 𝑟[𝑎 ∈ 𝐺(𝑝)] = Ω( log log
1
𝑝
) (т.е. хотя бы столько).
Доказательство. Пусть 𝑔 – ∀ первообразный, тогда 𝐺(𝑝) = {𝑔 𝑖 | (𝑖, 𝑝−1) = 1} ⇒ 𝐺(𝑝) 𝑝−1
= 𝜙(𝑝−1)
𝑝−1
.
Осталось научиться оценивать 𝑛 = 𝑞|𝑛 𝑞 ⩾
𝜙(𝑛) 𝑞−1 𝑖 log 𝑖−1
, где 𝑞 – простые делители 𝑛.
∏︀ ∏︀
𝑖 log 𝑖
Логарифмируем: log 𝑛 ⩾ log(𝑖 log 𝑖−1) − log(𝑖 log 𝑖) = Θ(− 𝑘𝑖=1 𝑖 log
𝜙(𝑛) 1
∑︀ ∑︀ ∑︀
𝑖
)) = Θ(− log log 𝑘)
Здесь 𝑘 – число простых делителей. Получаем 𝜙(𝑛)
𝑛
⩾ Θ( log1 𝑘 ) = Θ( log log
1
𝑛
). ■
Следствие 9.11.4. Получили ZPP-алгоритм поиска за 𝒪(FACT + log3 𝑝 log log 𝑝).
Замечание 9.11.5. Пусть есть алгоритм T, который работает корректно, если дать ему пра-
вильный первообразный корень. Пусть мы ещё умеем проверять корректность результата T.
Тогда проще всего вместо того, чтобы искать первообразный корень, ≈ log log 𝑝 раз запустить
алгоритм, подсовывая ему случайные числа.
Глава #9. пропушенная лекция. 57/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Теория чисел
9.12. Криптография. RSA.
Два типа шифрования:
Симметричная криптография. Один и тот же ключ позволяет и зашифровать, и расшиф-
ровать сообщение. Примеры шифрования: xor с ключом; циклический сдвиг алфавита.
Криптография с открытым ключем. Боб хочет послать сообщение Алисе и шифрует его
открытым ключем Алисы (𝑒), ключ (𝑒) знают все. Для расшифровки Алисе понадобит-
ся ее закрытый ключ (𝑑), который знает только она. Сами функции для шифровки и
расшифровки открыты, их знают все.
∙ RSA. (Rivest, Shamir, Adleman, 1977)
Выберем два больших простых числа 𝑝, 𝑞. Посчитаем 𝑛 = 𝑝𝑞, 𝜙(𝑛) = (𝑝 − 1)(𝑞 − 1).
Выберем случайное 1 ⩽ 𝑒 < 𝜙(𝑛), посчитаем 𝑑 : 𝑒𝑑 ≡ 1 mod 𝜙(𝑛).
Итого: генерим случайно 𝑝, 𝑞, 𝑒; вычисляем 𝑛, 𝜙(𝑛), 𝑑.
Тогда открытым ключем будет пара ⟨𝑒, 𝑛⟩, а закрытым – ⟨𝑑, 𝑛⟩.
Действия Боба для шифрования: 𝑚 → 𝜇 = 𝑚𝑒 mod 𝑛
Действия Алисы для дешифровки: 𝜇 → 𝑚 = 𝜇𝑑 mod 𝑛
Проверим корректность: (𝑚𝑒 )𝑑 = 𝑚𝑒𝑑 = 𝑚𝜙(𝑛)·𝑘+1 ≡ 1𝑘 · 𝑚1 = 𝑚.
Алгоритм надежен настолько, насколько сложна задача факторизации чисел.
Числа умеют факторизовать так (более полный список на wiki):
(a) 𝒪(𝑛1/2 ) – тривиальный перебор всех делителей до корня.
(b) √ – Эвристика Полларда, была в главе про вероятностные алгоритмы.
𝒪(𝑛1/4 · 𝑔𝑐𝑑)
(c) 𝐿𝑛 (1/2, 2 2) – алгоритм Диксона-Крайчика (есть на 3-м курсе)
(d) 𝐿𝑛 (1/2, 2) – метод эллиптических кривых (алгоритм Ленстры)
(e) 𝐿𝑛 (1/3, (32/9)3 ) – SNFS
Здесь 𝐿𝑛 (𝛼, 𝑐) = 𝒪(𝑒(𝑐+𝑜(1))(log 𝑛) ).
𝛼
При 0 < 𝛼 < 1 получаем 𝐿𝑛 между полиномом и экспонентой.
При 𝛼 = 1 получаем 𝐿𝑛 – ровно полином 𝑛𝑐+𝑜(1) .
Обычно в RSA используют ключ длины 𝑘 = 2048.
При шифровке/расшифровке используют 𝒪(𝑘) операций деления по модулю,
её мы скоро научимся реализовывать за 𝒪(𝑘 2 /𝑤2 ) и 𝒪(𝑘 log2 𝑘), оптимальное время – 𝒪(𝑘 log 𝑘).
Итого: простейшая реализация RSA даёт время 𝒪(𝑘 3 ), оптимальная – 𝒪(𝑘 2 log 𝑘) и 𝒪(𝑘 3 /𝑤2 ).
∙ Взлом RSA.
На практике разобраны два случая:
(a) Вариант взлома при 𝑒 = 3.
(b) Вариант взлома через Оракул, который ломает случайные сообщения с вероятностью 1%.
Глава #9. пропушенная лекция. 58/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Теория чисел
9.13. Протокол Диффи-Хеллмана
Есть Алиса и Боб. Им и вообще всем людям на Земле известны числа 𝑔 и 𝑝.
Алиса и Боб хотят создать неизвестный более никому секретный ключ.
Для этого они пользуются следующим алгоритмом (протоколом):
1. Алиса генерируют большое случайное число 𝑎, Боб 𝑏.
2. Алиса передаёт Бобу по открытому каналу (любой может его слушать) 𝑔 𝑎 mod 𝑝
3. Боб передаёт Алисе по открытому каналу 𝑔 𝑏 mod 𝑝
4. Алиса знает ⟨𝑎, 𝑔 𝑏 ⟩ ⇒ может вычислить 𝑔 𝑎𝑏 mod 𝑝, аналогично Боб. Ключ готов.
Предполагается, что злоумышленник не может вмешаться в процесс передачи данных, но может
все данные перехватить. Злоумышленник в итоге знает 𝑔, 𝑔 𝑎 , 𝑔 𝑏 , 𝑝, но не знает 𝑎 и 𝑏.
Оказывается, что задача «получить 𝑔 𝑎𝑏 по этим данным» не проще дискретного логарифмиро-
вания, а она не проще факторизации.
Глава #9. пропушенная лекция. 59/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Теория чисел
9.14. (-) Дискретное логарифмирование
Задача схожа по записи с обычным логарифмированием: 𝑎𝑥 = 𝑏 ⇒ 𝑥 = log𝑎 𝑏.
Собственно её нам и предстоит решить, только все вычисления по модулю 𝑚.
Заметим, что 𝑥 имеет смысл искать только в диапазоне [0, 𝜙(𝑚)) и 𝜙(𝑚) < 𝑚.
Общая идея решения:
√ корневая по 𝑥. Любую степень корнем поделим на две части...
Возьмём 𝑘 = ⌈ 𝑚⌉, построим множество пар 𝐵 = {⟨𝑎0 , 0⟩, ⟨𝑎𝑘 , 𝑘⟩, ⟨𝑎2𝑘 , 2𝑘⟩, . . . , ⟨𝑎(𝑘−1)𝑘 , (𝑘−1)𝑘⟩}.
Пусть 𝑥 существует, поделим его с остатком на 𝑘 : 𝑥 = 𝑖𝑘 + 𝑗 ⇒ 𝑎𝑥 = 𝑎𝑘𝑖 𝑎𝑗 ∧ ⟨𝑎𝑘𝑖 , 𝑘𝑖⟩ ∈ 𝐵.
Заметим, что 𝑎𝑘𝑖 𝑎𝑗 = 𝑏 ⇔ 𝑎𝑘𝑖 = 𝑏𝑎−𝑗 . Осталось перебрать 𝑗:
1 a1 = inverse ( a ) # один раз за 𝒪(log 𝑝)
2 for j in range ( k ) :
3 if b in B : # с точки зрения реализации 𝐵 – словарь
4 x = j + B [ b ] # словарь ;)
5 b = mul (b , a1 ) # по модулю! в итоге на j-й итерации у нас под рукой 𝑏𝑎−𝑗
√
Если B – хеш-таблица, то суммарное время работы 𝒪( 𝑝).
Замечание 9.14.1. Если для каждого 𝑏 в B[b] хранить весь список индексов, код легко моди-
фицировать так, чтобы он находил все решения.
9.15. (-) Корень 𝑘-й степени по модулю
Решаем уравнение вида 𝑥𝑘 ≡ 𝑎 mod 𝑝. Даны 𝑘, 𝑏, 𝑝 ∈ P.
Дискретно прологарифмируем по основанию первообразного кореня: 𝑎 = 𝑔 𝑏 , 𝑥 ищем в виде 𝑔 𝑦 .
Получаем 𝑔 𝑘𝑦 ≡ 𝑔 𝑏 mod 𝑝 ⇔ 𝑘𝑦 ≡ 𝑏 mod 𝑝−1 ⇔ 𝑦 ≡ 𝑘𝑏 mod 𝑝−1, 𝑥 = 𝑔 𝑦 .
√
Время работы – логарифмирование + деление, т.е. 𝑝 + log 𝑝.
9.16. (-) КТО
Китайская Теорема об Остатках (К.Т.О.) была у вас на алгебре в простейшем виде:
{︃
𝑥 ≡ 𝑎𝑖 (𝑚𝑖 )
⇒ ∃!𝑎 ∈ [0, 𝑀 ) : 𝑥 ≡ 𝑎(𝑀 ), где 𝑀 = 𝑚𝑖 .
∏︀
Теорема 9.16.1.
∀𝑖 ̸= 𝑗 (𝑚𝑖 , 𝑚𝑗 ) = 1
Также показывалось, что 𝑎 = 𝑎∏︀ 𝑖 𝑒𝑖 , где 𝑒𝑖 подбиралось так, что 𝑒𝑖 ≡ 1(𝑚𝑖 ), ∀𝑗 ̸= 𝑖 𝑒𝑖 ≡ 0(𝑚𝑗 ).
∑︀
Собственно, если обозначить 𝑡 = 𝑗̸=𝑖 𝑚𝑗 , то 𝑒𝑖 = (𝑡 · 𝑡−1 (𝑚𝑖 )) mod 𝑀 .
Сейчас мы пойдём чуть дальше и рассмотрим
∏︀ 𝛼𝑖𝑗 случай произвольных модулей 𝑚𝑖 .
𝛼
Первым делом ∀𝑖 факторизуем 𝑚𝑖 = 𝑝𝑖𝑗 . И заменим сравнения на ∀𝑗 𝑥 ≡ 𝑎𝑖 mod 𝑝𝑖𝑗𝑖𝑗 .
Для каждого простого 𝑝 оставим только главное сравнение: 𝑝𝛼 с максимальным 𝛼.
Нужно проверить, что любые сравнения по модулям вида 𝑝𝛽 главному не противоречат.
Итого за 𝒪(FACT) мы свели задачу к КТО, или нашли противоречие.
9.16.1. (-) Использование КТО в длинной арифметике.
Пусть нам нужно посчитать 𝑋 – значение арифметического выражения. Во время вычислений,
возможно, появляются очень длинные числа, но мы уверены, что 𝑋 ⩽ 𝐿 (𝐿 дано).
Возьмём несколько случайных 32-битных простых 𝑝𝑖 , чтобы их произведение было больше 𝐿.
Понадобится 𝑘 ≈ log 𝐿 чисел. Теперь 𝑘 раз найдём 𝑋 ∏︀mod 𝑝𝑖 , оперируя только с короткими
числами, а затем по ∏︀
формулам из КТО соберём 𝑋 mod 𝑝𝑖 .
Поскольку 𝑋 ⩽ 𝐿 < 𝑝𝑖 , мы получили верный 𝑋.
Глава #10. пропушенная лекция. 60/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Линейные системы уравнений
Лекция #10: Линейные системы уравнений
28 ноября 2022
СЛАУ – Система линейных алгебраических уравнений.
Решением СЛАУ мы сейчас как раз и займёмся. Заодно научимся считать определитель мат-
рицы, обращать матрицу, находить координаты вектора в базисе.
∙ Постановка задачи.
⎧
⎪
⎪ 𝑎00 𝑥0 + 𝑎01 𝑥1 + · · · + 𝑎0 𝑛−1 𝑥𝑛−1 = 𝑏0
⎪
⎨𝑎 𝑥 + 𝑎 𝑥 + · · · + 𝑎
01 0 11 1 0 𝑛−1 𝑥𝑛−1 = 𝑏1
Дана система уравнений
⎪
⎪
⎪...
𝑎𝑚−1 0 𝑥0 + 𝑎𝑚−1 1 𝑥1 + · · · + 𝑎𝑚−1 𝑛−1 𝑥𝑛−1 = 𝑏𝑚−1
⎩
Для краткости, будем записывать 𝐴𝑥 = 𝑏, где 𝐴 – матрица, 𝑏 – вектор-столбец.
Мы – программисты, поэтому нумерация везде с нуля.
Задача – найти какой-нибудь 𝑥, а лучше всё множество 𝑥-ов.
Все 𝑎𝑖𝑗 , 𝑏𝑖 – элементы поля (например, R, C, Z/𝑝Z), т.е. нам доступны операции +, -, *, \.
10.1. Гаусс для квадратных невырожденных матриц.
В данной части мы увидим классического Гаусса. Такого, как его обычно описывают.
Наша цель – превратить матрицу 𝐴 в треугольную или диагональную.
Наш инструмент – можно менять уравнения местами, вычитать уравнения друг из друга.
Для удобства реализации сразу начнём хранить 𝑏𝑖 в ячейке 𝑎𝑖𝑛
План: для каждого 𝑖 в ячейку 𝑎𝑖𝑖 поставить ненулевой элемент, и, пользуясь им и вычитанием
строк, занулить все другие ячейки в 𝑖-м столбце.
1 // этот код работает только для 𝑚 = 𝑛, det 𝐴 ̸= 0
2 // тем не менее, чтобы не путаться в размерностях, мы в разных местах пишем и 𝑚, и 𝑛
3 vector < vector <F > > a (m , vector <T >( n + 1) ) ; // 𝑏 хранится последним столбцом 𝑎
4 for ( int i = 0; i < n ; i ++) :
5 int j = i ;
6 while ( isEqual ( a [ j ][ i ] , 0) ) // isEqual для R обязана использовать 𝜀
7 j ++; // ненулевой элемент точно найдётся из невырожденности
8 swap ( a [ i ] , a [ j ]) ; // меняем строки местами, кстати, за 𝒪(1)
9 for ( int j = 0; j < n ; j ++) // перебираем все строки
10 if ( j != i ) // если хотим получить диагональную
11 if ( j > i ) // если хотим получить треугольную
12 if (! isEqual ( a [ j ][ i ] , 0) ) : // хотим в [i,j] поставить 0, вычтем 𝑖-ю строку
13 F coef = a [ j ][ i ] / a [ i ][ i ];
14 for ( int k = i ; k <= n ; k ++) // самая долгая часть
15 a [ j ][ k ] -= a [ i ][ k ] * coef ;
Строка 14 – единственная часть, работающая за 𝒪(𝑛3 ), поэтому для производительности важно,
что цикл начинается с 𝑖, а не с 0 (так можно, так как слева от 𝑖 точно нули).
Если результат Гаусса – диагональная матрица, то 𝑥𝑖 = 𝑏𝑖 /𝑎𝑖𝑖 .
Из треугольной матрицы 𝑥-ы нужно восстанавливать в порядке 𝑖 = 𝑛−1. . .0:
𝑥𝑖 = (𝑏𝑖 − 𝑗=𝑖+1..𝑛−1 𝑥𝑗 · 𝑎𝑖𝑗 )/𝑎𝑖𝑖 . Время восстановления 𝒪(𝑛2 ).
∑︀
Глава #10. 28 ноября 2022. 61/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Линейные системы уравнений
Замечание 10.1.1. Вычисление определителя. При swap строк det 𝐴 меняет знак, при вычитании
строк det 𝐴 не меняется ⇒ за то же время мы умеем вычислять det 𝐴.
Пример 10.1.2. Работа Гаусса.
6 23 21 23
⎡ ⎤ ⎡ ⎤ ⎡ ⎤ ⎡ ⎤ ⎡ ⎤
1 1 2 5 1 1 2 5 1 1 2 5 1 0 3 1 0 0
i=0 swap i=1 i=2
⎣ 3 3 8 5 ⎦−→⎣ 0 0 2 −10 −→ 0
⎦ ⎣ 3 −3 −5 −→ 0
⎦ ⎣ 3 −3 −5 ⎦−→ ⎣ 0 3 0 −20 ⎦
2 5 1 5 0 3 −3 −5 0 0 2 −10 0 0 2 −10 0 0 2 −10
Итого: 𝑥 = [21 23 , −6 23 , −5]
Оценим время работы в худшем случае (всегда заходим в if в 12-й строке):
Для превращения в треугольную
∑︀ 2 3
∑︀𝑖 𝑖 ≈ 𝑛 3/3
Для превращения в диагональную 𝑖 𝑛𝑖 ≈ 𝑛 /2
⇒ если важна скорость, приводите к △-ой. Когда на первом месте удобство, к диагональной.
10.2. Гаусс в общем случае
Если мы подойдём в вопросу чисто математически, придётся ввести трапецевидные и ступен-
чатые матрицы. Возможно, нам захочется менять столбцы (переменные) местами.
Мы хотим сразу удобный универсальный код. Поэтому задачу сформулируем так:
По одному добавляются пары ⟨𝑎𝑖 , 𝑏𝑖 ⟩ ∈ ⟨F𝑛 , F⟩, нам нужно поддерживать базис пространства
𝑙𝑖𝑛𝑒𝑎𝑟{𝑎1 , 𝑎2 , . . . , 𝑎𝑚 } и поддерживать множество решений системы 𝐴𝑥 = 𝑏.
1 vector < vector <F > > A ; // текущий базис и прикреплённые 𝑏-шки
2 vector < int > col ; // для каждого базисного вектора храним номер столбца, который он обнуляет
3 bool add ( vector <F > a ) { // a[0],...,a[n-1],b
4 for ( size_t i = 0; i < A . size () ; i ++)
5 if (! isEqual ( a [ col [ i ]] , 0) ) :
6 F coef = a[col[i]] / A[i][col[i]];
7 for ( size_t k = 0; k < a . size () ; k ++)
8 a [ k ] -= A [ i ][ k ] * coef ;
9
10 size_t i = 0;
11 while ( i < a . size () && isEqual ( a [ i ] , 0) )
12 i ++;
13 if ( i == a . size () ) return 1; // уравнение – линейная комбинация предыдущих
14 if ( i == a . size () - 1) return 0; // выразили из данных уравнений «0+..+0 = 1»
15 A . push_back ( a ) , col . push_back ( i ) ; // добавили в базис новый вектор
16 return 1; // система всё ещё разрешима
17 }
Время работы добавления 𝑚 векторов, если 𝑑𝑖𝑚 𝑙𝑖𝑛𝑒𝑎𝑟{𝑎1 , . . . , 𝑎𝑚 } = 𝑘, работает за 𝒪(𝑚𝑛𝑘).
Восстановим решение. Свободные переменные – ровно те, что не вошли в col.
1 vector <F > getX () : // 𝒪(𝑛 + 𝑘 2 ), что для маленьких 𝑘 гораздо быстрее обычного 𝒪(𝑛2 )
2 vector <F > x (n , 0) ; // пусть свободные переменные равны нулю
3 for ( int i = A . size () - 1; i >= 0; i - -) :
4 x [ col [ i ]] = A [ i ][ n ]; // A[i].size() == n + 1
5 for ( size_t j = i + 1; j < A . size () ; j ++)
6 x [ col [ i ]] -= A [ i ][ col [ j ]] * x [ col [ j ]];
7 return x ; // нашли какое-то одно решение
Теперь запомним начальное s = getX() и переберём те столбцы 𝑗, которыя являются свобод-
ными переменными. Для каждого 𝑗 начнём восстановление ответа с x[j] = 1, и после строки
Глава #10. 28 ноября 2022. 62/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Линейные системы уравнений
4 учтём слагаемое A[i][j]*x[j]. Результат для 𝑗-й переменной обозначим 𝑣𝑗 .
Теперь у нас есть всё пространство решений: 𝑠 + 𝑙𝑖𝑛𝑒𝑎𝑟{𝑣1 −𝑠, 𝑣2 −𝑠, . . . , 𝑣𝑛−𝑘 −𝑠}.
Время: 𝒪((𝑛 − 𝑘)(𝑛 + 𝑘 2 )). Где (𝑛−𝑘) – размерность пространства решений.
10.3. Гаусс над F2
Самая долгая часть Гаусса – вычесть из одной строки другую, умноженную на число.
В F2 вычетание – ⊕, а умножение &. Любимый нами bitset обе операции сделает за 𝒪(𝑛/𝑤).
Полученное ускорение применимо к обоим версиям Гаусса, описанным выше.
10.4. Погрешность
При вычислениях в Z/𝑝Z, естественно, отсутствует. При вычислениях в R она зашкаливает.
Рассмотрим матрицу Гильберта 𝐺: 𝑔𝑖𝑗 = 𝑖+𝑗1
; 𝑖, 𝑗 ∈ [1, 𝑛].
Попробуем решить руками уравнение 𝐺𝑥 = 0: det 𝐺 ̸= 0 ⇒ ∃ ! 𝑥 = {0, . . . , 0}.
Теперь применим Гаусса, реализованного в типе double при 𝜀 = 10−12 .
Уже при 𝑛 = 11 в процессе выбора ненулевого элемента мы не сможем отличить 0 от не нуля.
Окей! double (8 байт) → long double (10 байт), и 𝜀 = 10−15 . Та же проблема при 𝑛 = 17.
∙ Решения проблемы:
Обычно, чтобы хоть чуть-чуть уменьшить погрешность выбирают, не любой ненулевой элемент
в столбце 𝑖, а max по модулю элемент в подматрице [𝑖, 𝑛] × [𝑖, 𝑛] (эвристика max элемента).
В инкрементальном способе (добавлять строки по одной) эту оптимизацию не применить.
Во многих языках реализованы длинные вещественные числа, например, Java: BigDecimal.
Но всё равно возникает вопрос, какую точность выбрать? Содержательный ответ можно будет
извлечь из курса «вычислительные методы», а простой звучит так:
1. Запустите Гаусса два раза с 𝑘 и 2𝑘 значащими знаками.
2. Если ответы недопустимо сильно отличаются, 𝑘 слишком мало, его нужно увеличить.
«Детский» способ. Пусть известно ограничение по времени (например, ровно 1) секунда,
выберем max возможную точность, чтобы уложиться в ограничение.
На некотором классе матриц меньшую погрешность даёт метод простой итерации.
10.5. Метод итераций
Пусть нам нужно решить систему 𝑥 = 𝐴𝑥+𝑏. При этом ||𝐴|| < 1 (вы ведь помните про нормы?).
Решение: начнём с 𝑥0 = random, будем пересчитывать 𝑥𝑖+1 = 𝐴𝑥𝑖 + 𝑏.
Сделаем сколько-то шагов, последний выдадим, как ответ. Сложность 𝒪(𝑛2 𝑡), 𝑡 – число шагов.
На самом деле даже меньше: 𝒪(𝐸𝑡) шагов, где 𝐸 – число ненулевых ячеек матрицы.
Если бы система имела более простой вид 𝑥 = 𝐴𝑥, мы могли бы вычислять быстрее:
𝐴 → 𝐴2 → 𝐴4 → · · · → 𝐴2 → (𝐴2 )𝑥, итого сложность 𝒪(𝑛3 log 𝑡).
𝑘 𝑘
Попробуем такой же фокус провернуть с исходной системой 𝑥 = 𝐴𝑥 + 𝑏.
𝑥0 → 𝐴𝑥0 + 𝑏 → 𝐴2 𝑥0 + 𝐴𝑏 + 𝑏 → · · · → 𝐴𝑘 𝑥0 + 𝐴
⏟
𝑘−1
𝑏 + · ·⏞· + 𝐴𝑏 + 𝑏.
Обозначим 𝑆𝑖 ,𝑘=2𝑖
Заметим: 𝑆𝑘 = 𝐴 𝑘/2 𝑘/2
𝑆𝑘−1 + 𝑆𝑘−1 = (𝐴 + 𝐸)𝑆𝑘−1 .
Глава #10. 28 ноября 2022. 63/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Линейные системы уравнений
{︃
𝑆𝑖+1 = (𝑇𝑖 + 𝐸)𝑆𝑖
База: 𝑇0 = 𝐴, 𝑆0 = 𝑏. Переход:
𝑇𝑖+1 = 𝑇𝑖2
𝑘→∞
||𝐴|| = sup|𝑥|=1 |𝐴𝑥|, ||𝐴2 || ⩽ ||𝐴||2 −→ 0 ⇒ пренебрежём слагаемым 𝐴2 𝑥0 (или возьмём 𝑥0 = 0).
𝑘 𝑘 𝑘
Замечание 10.5.1. Есть два способа запустить 𝑡 итераций: линейная итерация 𝑥 → 𝐴𝑥, работает
за 𝒪(𝐸𝑡), и итерация с удвоением 𝐴 → 𝐴2 , работает за 𝒪(𝑛3 log 𝑡). Если в задаче имеется
быстрая сходимость и матрица 𝐴 разреженная, линейная итерация можеть работать лучше.
10.6. Вычисление обратной матрицы
Задача: дана 𝐴 над полем, найти 𝑋 : 𝐴𝑋 = 𝐸.
Из det 𝐴 · det 𝑋 = 1 следует, что 𝐴 невырождена ⇒ решение единственно.
Каждый столбец матрицы 𝑋 задаёт систему уравнений ⇒ нахождение 𝑋 за 𝒪(𝑛4 ) очевидно.
Чтобы получить время 𝒪(𝑛3 ), заметим, что у систем матрица 𝐴 общая, различны лишь столб-
цы 𝑏 ⇒ системы можно решать одновременно.
Также, как мы записывали 𝑏𝑖 в 𝑎𝑖𝑛 , если есть сразу 𝑏𝑖0 , 𝑏𝑖1 , . . . , 𝑏𝑖𝑘 , то ∀𝑗 запишем 𝑏𝑖𝑗 в 𝑎𝑖 𝑛+𝑗 .
Далее будем оперировать со строками длины 𝑛+𝑘. Время работы 𝒪((𝑛 + 𝑘)𝑛2 ) = 𝒪(𝑛3 ).
Короткое изложение: записали 𝐴𝐸 как матрицу из 2𝑛 столбцов, привели Гауссом 𝐴 к диаго-
нальному, а затем даже единичному виду ⇒ на месте 𝐸 у нас как раз 𝐴−1 .
Следствие 10.6.1. Над F2 обратную матрицу мы научились искать за 𝒪(𝑛3 /𝑤).
10.7. Гаусс для евклидова кольца
Напомним, евклидово кольцо – область целостности с делением с остатком
(есть +, -, * и / с остатком). Например, Z. Или R[𝑥] – многочлены, Z[𝑖] – Гауссовы числа.
Сейчас у нас получится чуть изменить обычного Гаусса, приводящего матрицу 𝐴 к треугольной.
А вот к диагональному виду, увы, привести не получится.
Основная операция в Гауссе – имея столбец 𝑖, строки 𝑖 и 𝑗 при 𝑎𝑖𝑖 ̸= 0 занулить 𝑎𝑗𝑖 .
𝑎
Раньше мы вычитали из строки 𝑎𝑗 строку 𝑎𝑖 , умноженную на 𝑎𝑗𝑖𝑖𝑖 . Теперь у нас нет деления...
Зато у нас есть деление с остатком ⇒ есть алгоритм Евклида.
Давайте на элементах 𝑎𝑗𝑖 и 𝑎𝑖𝑖 запустим Евклида. Только по ходу Евклида вычитать будем
строки целиком. Результат: ∀𝑘 < 𝑖 все 𝑎𝑖𝑘 , 𝑎𝑗𝑘 как были нулями, так и остались, а один из 𝑎𝑗𝑖
и 𝑎𝑖𝑖 занулился. Пример:
⎡ ⎤ ⎡ ⎤ ⎡ ⎤
2 8 2 0 1 2 8 2 0 1 2 8 2 0 1
⎣ 0 7 5 4 1 ⎦ 7−3·2 3−1·3
−→ ⎣ 0 1 −7 −2 −1 ⎦ −→ ⎣ 0 1 −7 −2 −1 ⎦
0 3 6 3 1 0 3 6 3 1 0 0 27 9 4
Как этим можно пользоваться?
1. Для подсчёта определителя квадратной матрицы.
2. Для решения системы 𝐴𝑥 = 𝑏, если нет свободных переменных (например, det 𝐴 ̸= 0)
Если есть свободные переменные, то во время восстановления ответа по треугольной матрице
в формуле 𝑥𝑖 = (𝑏𝑖 − 𝑗=𝑖+1..𝑛−1 𝑥𝑗 · 𝑎𝑖𝑗 )/𝑎𝑖𝑖 у нас может «не поделиться».
∑︀
В случае det 𝐴 ̸= 0 это значило бы «нет решений», а тут это значит «мы неправильно задали
значения свободным переменным».
Глава #10. 28 ноября 2022. 64/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Линейные системы уравнений
Замечание 10.7.1. Если стоит задача «проверки невырожденности матрицы над Z», то разумно,
чтобы избежать длинных чисел, вычисления проводить не над Z, а по большому простому
модулю. При подсчёте определителя матрицы над Z для борьбы с длинными числами можно
использовать приём из разд. 9.16.1.
10.8. Разложение вектора в базисе
Вернёмся в мир полей и безопасного деления.
Если нам дают базис пространства {𝑣1 , . . . , 𝑣𝑛 } и вектор 𝑝, просят разложить 𝑝 в базисе 𝑣, проще
всего решить систему уравнений 𝐴𝑥 = 𝑝, где 𝑣𝑖 – столбцы матрицы 𝐴. Время 𝒪(𝑛3 ).
Если нам дают сразу много векторов 𝑝1 , 𝑝2 , . . . 𝑝𝑘 , то мы дописываем их к 𝐴 : 𝐴 | 𝑝1 𝑝2 . . .𝑝𝑘 ,
[︀ ]︀
как делали в разд. 10.6, и получаем итоговое время 𝒪(𝑛2 (𝑛+𝑘) + 𝑛𝑘) = 𝒪(𝑛3 + 𝑛2 𝑘).
Теперь будем решать online задачу – нам нужно сделать некий предподсчёт от базиса, а векто-
ра 𝑝𝑖 будут выдавать по одному. Раскладывать каждый 𝑝𝑖 хочется за 𝒪(𝑛2 ).
Заметим, что в Гауссе разд. 10.2 мы как раз по сути разложили вектор... только не на исходные
вектора, а на текущие строки матрицы. Ок, давайте для каждой строки матрицы хранить
коэффициенты 𝑐𝑖𝑗 : 𝑎∑︀ 𝑗 𝑐𝑖𝑗 𝑣𝑗 , где∑︀
𝑎𝑖 – строки матрицы, а 𝑣𝑗 – исходные вектора.
∑︀ Тогда для
∑︀
𝑖 =
нового вектора 𝑎𝑘 = 𝑖=0..𝑘−1 𝛼𝑖 𝑎𝑖 = 𝑗 𝑣𝑗 ( 𝑖 𝛼𝑖 𝑐𝑖𝑗 ). Новые коэффициенты 𝑐𝑘𝑗 = 𝑖 𝛼𝑖 𝑐𝑖𝑗 .
∑︀
Итого мы за 𝒪(𝑛2 ) нашли коэффициенты строки, которую собираемся добавить в базис.
10.8.1. Ортогонализация Грама-Шмидта
Другой способ сделать базис удобным для пользования – привести его к ортонормированному
виду. Далее следует описание Ортогонализации Грама-Шмидта, знакомое вам с алгебры:
1 for i =0.. k -1
2 for j =0.. i -1
3 v [ i ] -= v [ j ] * scalarProduct ( v [ i ] , v [ j ])
4 v [ i ] /= sqrt ( scalarProduct ( v [ i ] , v [ i ]) )
Получить координаты вектора 𝑝 в новом милом базисе проще простого: 𝑥𝑖 = ⟨𝑝, 𝑣𝑖 ⟩.
Время разложения вектора в базисе – 𝒪(𝑛2 ) сложений и умножений.
Опять же, если мы хотим координаты∑︀в исходном базисе, то для каждого 𝑣𝑖 нам нужно будет
таскать вектор коэффициентов: 𝑣𝑖 = 𝑗 𝛼𝑗 𝑣𝑗 (выражение через исходный базис), и вычитая 𝑣𝑖 ,
*
вычитать и вектора коэффициентов. Время разложения 𝑝 в исходном базисе – тоже 𝒪(𝑛2 ).
10.9. Вероятностные задачи
Рассмотрим орграф, на рёбрах которого написаны вероятности.
Для каждой вершины верно, что сумма исходящих вероятностей равна 1.
Если бы мы хотели с некоторой вероятностью оставаться в вершине, добавили бы петлю.
Что нас может интересовать?
1. Начав в вершине 𝑣, в какой вершине с какой вероятностей мы находимся при 𝑡 = ∞?
2. Какова вероятность, что, начав в 𝑣 мы дойдём до вершины 𝐴 раньше, чем до вершины 𝐵
(𝐴 – спасти принцессу, 𝐵 – свалиться в болото)?
3. Какое матожидание числа шагов в пути из вершины 𝑣 в вершину 𝐴?
4. Какое условное матожидание числа шагов в пути из вершины 𝑣 в вершину 𝐴,
если попадание в 𝐵 – смерть?
Глава #10. 28 ноября 2022. 65/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Линейные системы уравнений
∙ Орграф ацикличен ⇒ динамика
Если исходный орграф ацикличен + в нём могут∑︀ быть петли, все задачи решаются динамикой.
Пример для матожидания без петли: 𝐸[𝑣] = 1 + 𝑝[𝑣 → 𝑥] · 𝐸[𝑥]
Пример для матожидания с петлёй вероятности 𝑞:
1
∑︀ ∑︀
𝐸[𝑣] = 1 + (1 − 𝑞)( 𝑝[𝑣 → 𝑥] · 𝐸[𝑥]) + 𝑞 · 𝐸[𝑣] ⇒ 𝐸[𝑣] = 1−𝑞 + 𝑝[𝑣 → 𝑥] · 𝐸[𝑥].
∙ Произвольный орграф ⇒ итерации или Гаусс
Рассмотрим вторую задачу. Тогда 𝑝[𝐴] = 1, 𝑝[𝐵] = 0, а на каждую другую вершину есть линей-
ное уравнение 𝑝[𝑣] = 𝑝[𝑣 → 𝑥] · 𝑝[𝑥]. Давайте решать!
∑︀
Гаусс работает за 𝒪(𝑉 3 ), обладает скверной погрешностью.
Метод итераций в данном случае будет работать так:
1. Изначально все кроме 𝑝[𝐴] нули.
2. Затем мы все 𝑝[𝑣] пересчитываем по формуле: 𝑝[𝑣] = 𝑥 𝑝[𝑣 → 𝑥] · 𝑝[𝑥].
∑︀
Можно пересчитывать возведением матрицы в степень:
𝑘 фаз за 𝒪(𝑉 3 log 𝑘) , а можно в лоб за 𝒪(𝐸 · 𝑘)
Замечание 10.9.1. Полезно ознакомиться с практиками, домашними заданиями, разборами.
Замечание 10.9.2. Пусть получившуюся систему уравнений задаёт матрица 𝐴.
Если бы было ||𝐴|| < 1, мы бы уже сейчас сказали про сходимость. Но ||𝐴|| ⩽ 1, поэтому анализ
сходимости метода итераций ждёт вас в курсе численных методов.
Глава #10. 28 ноября 2022. 66/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Линейные системы уравнений
10.10. (*) СЛАУ над Z и Z/𝑚Z
10.10.1. (*) СЛАУ над Z
Решение #1. Оценим ∏︀величину ответ: пусть |𝑎𝑛𝑠| < 𝑥. Решим ту же систему по 3простым мо-
дулям 𝑝1 , 𝑝2 , . . . 𝑝𝑘 : 𝑝𝑖 ⩾ 𝑥. Через КТО восстановим ответ. Время работы 𝒪(𝑛 𝑘). Минусы:
нужно оценить ответ. Модификация: если известно, что ответ ∃, можно находить 𝑘 итератив-
ным удвоением: 𝑘 = 1 → 2 → 4 → . . . .
Решение #2. Пытаемся всё сделать одним Гауссом. Запустить Гаусса
∑︀ проблем нет. Проблема —
выбрать свободные переменные так, чтобы всё поделилось: ∀𝑖 𝑏𝑖 − 𝑗>𝑖 𝑎𝑖𝑗 𝑥𝑗 ≡ 0 mod 𝑎𝑖𝑖 .
∑︀
𝑏𝑖 − 𝑎𝑖𝑗 𝑥𝑗
Как теперь 𝑥𝑖 = 𝑗>𝑖
𝑎𝑖𝑖
подставлять в следующие линейные уравнения и остаться в целых
числах? Можно вместо деления наоборот все уравнения умножить на 𝑎𝑖𝑖 . Такой процесс вызовет
большой, но предсказуемый рост коэффициентов: первое уравнение умножится на 𝑎22 𝑎33 . . .𝑎𝑛𝑛 .
10.10.2. (*) СЛАУ по модулю
Сперва представим себе полный ад: много линейных уравнений и каждое по своему модулю 𝑚𝑖 .
КТО даёт нам возможность для упрощений: факторизуем все 𝑚𝑖 , получим уравнения mod 𝑝𝑘 .
Сгруппируем уравнения по 𝑝. Если есть два уравнения ⟨𝑎1 , 𝑥⟩ = 𝑏1 mod 𝑝𝑖 и ⟨𝑎2 , 𝑥⟩ = 𝑏2 mod 𝑝𝑗 ,
при 𝑖 > 𝑗 второе можно домножить на 𝑝𝑖−𝑗 : ⟨𝑎2 𝑝𝑖−𝑗 , 𝑥⟩ = 𝑏2 𝑝𝑖−𝑗 mod 𝑝𝑖 ⇒ ∀𝑝 все степени 𝑖 равны.
Решаем для каждого 𝑝 свою систему. В конце КТО даёт ответ для исходной системы.
10.10.3. (*) СЛАУ над Z/𝑝𝑘 Z
Можно действовать похоже на второе решение для Z.
Разница в том, что все новые уравнения «чтобы поделились» будут уже mod 𝑝𝑘−1 ⇒
процесс нужно повторить ⩽ 𝑘 раз. Время работы 𝒪(𝑛3 𝑘).
Глава #10. 28 ноября 2022. 67/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Быстрое преобразование Фурье
Лекция #11: Быстрое преобразование Фурье
5 декабря 2022
Перед тем, как начать говорить «Фурье» то, «Фурье» сё, нужно сразу заметить:
Есть непрерывное преобразование Фурье. С ним вы должны столкнуться на теорвере.
Есть тригонометрический ряд Фурье. И есть общий ряд Фурье в гильбертовом пространстве,
который появляется в начале курса функционального анализа.
Мы же с вами будем заниматься исключительно дискретным преобразованием Фурье.
Коротко DFT (Discrete Fourier transform). FFT – по сути то же, первая буква означает «fast».
Задача: даны два многочлена 𝐴, 𝐵 суммарной длины ⩽ 𝑛, переменожить их за 𝒪(𝑛 log 𝑛).
Длина многочлена – 𝛾(𝐴) = (deg 𝐴) + 1. Вводим её, чтобы везде не писать «−1».
Если даны 𝑛 точек (𝑥𝑖 , 𝑦𝑖 ), все 𝑥𝑖 различны, ∃! интерполяционный многочлен длины 𝑛, постро-
енный по этим точкам (из алгебры). Ещё заметим: 𝛾(𝐴𝐵) = 𝛾(𝐴) + 𝛾(𝐵) − 1. Наш план:
1. Подобрать удачные 𝑘 и точки 𝑤0 , 𝑤1 , . . . , 𝑤𝑘−1 : 𝑘 ⩾ 𝛾(𝐴) + 𝛾(𝐵) − 1 = 𝑛.
2. Посчитать значения 𝐴 и 𝐵 в 𝑤𝑖 .
3. 𝐴𝐵(𝑥𝑖 ) = 𝐴(𝑥𝑖 )𝐵(𝑥𝑖 ). Эта часть самая простая, работает за 𝒪(𝑛).
4. Интерполировать 𝐴𝐵 длины 𝑘 по полученным парам ⟨𝑤𝑖 , 𝐴𝐵(𝑤𝑖 )⟩.
Вспомним комплексные числа:
𝑒𝑖𝛼 𝑒𝑖𝛽 = 𝑒𝑖(𝛼+𝛽) 𝑒𝑖𝜙 = (cos 𝜙, sin 𝜙),
√ (𝑎, 𝑏)√= (𝑎, −𝑏) ⇒ 𝑒𝑖𝜙 = 𝑒𝑖(−𝜙)
Извлечение корня 𝑘-й степени: 𝑘 𝑧 = 𝑒𝑖𝜙 = 𝑒𝑖𝜙/𝑘
𝑘
Если взять все корни из 1 степени 2𝑡 , возвести в квадрат,
получатся ровно все корни степени 2𝑡−1 . Корни из 1 степени 𝑘: 𝑒𝑖𝑗/𝑘 .
11.1. Прелюдия к FFT
Возьмём min 𝑁 = 2𝑡 ⩾ 𝑛 и 𝑤𝑗 = ∑︀
𝑒𝑖𝑗/𝑁 . Тут мы предполагаем, что 𝐴, 𝐵 ∈ C[𝑥] или 𝐴, 𝐵 ∈ R[𝑥].
Пусть есть многочлены 𝐴(𝑥) = 𝑎𝑖 𝑥 и 𝐵(𝑥) = 𝑏𝑖 𝑥 . Ищем 𝐶(𝑥) = 𝐴(𝑥)𝐵(𝑥).
𝑖
∑︀ 𝑖
Обозначим их значения в точках 𝑤0 , 𝑤1 , . . . , 𝑤𝑘−1 : 𝐴(𝑤𝑖 ) = 𝑓 𝑎𝑖 , 𝐵(𝑤𝑖 ) = 𝑓 𝑏𝑖 , 𝐶(𝑤𝑖 ) = 𝑓 𝑐𝑖 .
Схема быстрого умножения многочленов:
𝒪(𝑛 log 𝑛) 𝒪(𝑛) 𝒪(𝑛 log 𝑛)
𝑎𝑖 , 𝑏𝑖 −→ 𝑓 𝑎𝑖 , 𝑓 𝑏𝑖 −→ 𝑓 𝑐𝑖 = 𝑓 𝑎𝑖 𝑓 𝑏𝑖 −→ 𝑐𝑖
11.2. Собственно идея FFT
𝐴(𝑥) = 𝑎𝑖 𝑥𝑖 = (𝑎0 + 𝑥2 𝑎2 + 𝑥4 𝑎4 + . . . ) + 𝑥(𝑎1 𝑥 + 𝑎3 𝑥3 + 𝑎5 𝑥5 + . . . ) = 𝑃 (𝑥2 ) + 𝑥𝑄(𝑥2 )
∑︀
Т.е. обозначили все чётные коэффициенты 𝐴 многочленом 𝑃 , а нечётные соответственно 𝑄.
𝛾(𝐴) = 𝑛, все 𝑤𝑗2 = 𝑤𝑛/2+𝑗
2
⇒ многочлены 𝑃 и 𝑄 нужно считать не в 𝑛, а в 𝑛
2
точках.
1 def FFT ( a ) :
2 n = len ( a )
3 if n == 1: return a # посчитать значение A(x) = a[0] в точке 1
4 a ---> p , q # разбили коэффициенты на чётные и нечётные
5 p , q = FFT ( p ) , FFT ( q )
6 w = exp (2 pi * i / n ) # корень из единицы 𝑛-й степени
7 for i =0.. n -1: a [ i ] = p [ i %( n /2) ] + w i * q [ i %( n /2) ]
8 return a
Глава #11. 5 декабря 2022. 68/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Быстрое преобразование Фурье
Время работы 𝑇 (𝑛) = 2𝑇 (𝑛/2) + 𝒪(𝑛) = 𝒪(𝑛 log 𝑛).
11.3. Крутая реализация FFT
Чтобы преобразование работало быстро, нужно заранее предподсчитать все 𝑤𝑗 = 𝑒2𝜋𝑖𝑗/𝑁 .
Заметим, что 𝑝 и 𝑞 можно хранить прямо в массиве 𝑎.
Тогда получается, что на прямом ходу рекурсии мы просто переставляем местами элементы 𝑎,
и только на обратном делаем какие-то полезные действия.
Число 𝑎𝑖 перейдёт на позицию 𝑎𝑟𝑒𝑣(𝑖) , где 𝑟𝑒𝑣(𝑖) – перевёрнутая битовая запись 𝑖.
Кстати, 𝑟𝑒𝑣(𝑖) мы уже умеем считать динамикой для всех 𝑖.
При реализации на C++ можно использовать комплексные числа из STL: complex<double>.
1 const int K = 20 , N = 1 << K ;
2 complex < double > root [ N ];
3 int rev [ N ];
4 void init () :
5 for ( int j = 0; j < N ; j ++) :
6 root [ j ] = exp (2𝜋·i·j / N ) ; // cos(2𝜋𝑗/𝑁 ), sin(2𝜋𝑗/𝑁 )
7 rev [ j ] = ( rev [ j >> 1] >> 1) + (( j & 1) << ( K - 1) ) ;
Теперь, корни из единицы степени 𝑘 хранятся в root[j*N/k], 𝑗 ∈ [0, 𝑘). Две проблемы:
1. Доступ к памяти при этом не последовательный, проблемы с кешом.
2. Мы 2𝑁 раз вычисляли тригонометрические функции.
⇒ можно лучше, вычисления корней #2:
1 for ( int k = 1; k < N ; k *= 2) :
2 num tmp = exp (𝜋/ k ) ;
3 root [ k ] = {1 , 0}; // в root[k..2k) хранятся первые k корней степени 2k
4 for ( int i = 1; i < k ; i ++)
5 root [ k + i ] = ( i & 1) ? root [( k + i ) >> 1] * tmp : root [( k + i ) >> 1];
Теперь код собственно преобразования Фурье может выглядеть так (используем root #2):
Алгоритм 11.3.1. Эффективная реализация FFT
1 void FFT (a , fa ) : // a –> fa
2 for ( int i = 0; i < N ; i ++)
3 fa [ rev [ i ]] = a [ i ]; // можно иначе, но давайте считать массив «a» readonly
4 for ( int k = 1; k < N ; k *= 2) // уже посчитаны FFT от кусков длины k, база: k=1
5 for ( int i = 0; i < N ; i += 2 * k ) // [i..i+k) [i+k..i+2k) –-> [i..i+2k)
6 for ( int j = 0; j < k ; j ++) : // оптимально написанный стандартный цикл FFT
7 num tmp = root [ k + j ] * fa [ i + j + k ]; // вторая версия root[]
8 fa [ i + j + k ] = fa [ i + j ] - tmp ; // exp(2𝜋𝑖(𝑗+𝑛/2)/𝑛) = -exp(2𝜋𝑖𝑗/𝑛)
9 fa [ i + j ] = fa [ i + j ] + tmp ;
11.4. Обратное преобразование
Теперь имея при 𝑤 = 𝑒2𝜋𝑖/𝑛 :
𝑓 𝑎0 = 𝑎0 + 𝑎1 + 𝑎2 + 𝑎3 + . . .
𝑓 𝑎1 = 𝑎0 + 𝑎1 𝑤 + 𝑎2 𝑤2 + 𝑎3 𝑤3 + . . .
𝑓 𝑎2 = 𝑎0 + 𝑎1 𝑤2 + 𝑎2 𝑤4 + 𝑎3 𝑤3 + . . .
...
Нам нужно научиться восстанавливать коэффициенты 𝑎0 , 𝑎1 , 𝑎2 , . . . , имея только 𝑓 𝑎𝑖 .
Глава #11. 5 декабря 2022. 69/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Быстрое преобразование Фурье
𝑛−1 𝑛−1
Заметим, что ∀𝑗 ̸= 0 𝑤𝑗𝑘 = 0 (геометрическая прогрессия). А при 𝑗 = 0 получаем
∑︀ ∑︀ 𝑗𝑘
𝑤 = 𝑛.
𝑘=0 ∑︀ 𝑘 ∑︀ 2𝑘 𝑘=0
⇒ 𝑓 𝑎0 + 𝑓 𝑎1 + 𝑓 𝑎2 + · · · = 𝑎0 𝑛 + 𝑎1 𝑘 𝑤 + ∑︀ 𝑎2 𝑘 𝑤 + · · · = 𝑎0 𝑛∑︀
Аналогично 𝑓 𝑎0 + ∑︀ 𝑓 𝑎1 𝑤−1 + 𝑓 𝑎2 𝑤−2 + · · · = 𝑘 𝑎0 𝑤−𝑘 + 𝑎1 𝑛 + 𝑎2 𝑘 𝑤𝑘 + · · · = 𝑎1 𝑛
И в общем случае 𝑘 𝑓 𝑎𝑘 𝑤−𝑗𝑘 = 𝑎𝑗 𝑛.
Заметим, что это ровно значение многочлена с коэффициентами 𝑓 𝑎𝑘 в точке 𝑤−𝑗 .
Осталось заметить, что множества чисел {𝑤𝑗 | 𝑗 = 0..𝑛−1} и {𝑤−𝑗 | 𝑗 = 0..𝑛−1} совпадают ⇒
1 void FFT_inverse ( fa , a ) : // fa → a
2 FFT ( fa , a )
3 reverse ( a + 1 , a + N ) // 𝑤𝑗 ↔ 𝑤−𝑗
4 for ( int i = 0; i < N ; i ++) a [ i ] /= N ;
Другой способ. Возьмём код 11.3.1, заметим, что строки 7-8-9 обратимы:
1 tmp = ( fa [ i + j ] + fa [ i + j + k ]) / 2
2 fa [ i + j ] -= tmp , fa [ i + j + k ] = tmp / root [ k + j ];
⇒ запустим циклы 4-5-6 в обратном порядке, обращая каждый шаг прямого FFT.
11.5. Два в одном
Часто коэффициенты многочленов – вещественные числа.
Если у нас есть многочлены 𝐴(𝑥), 𝐵(𝑥) ∈ R[𝑥], возьмём числа 𝑐𝑗 = 𝑎𝑗 + 𝑖𝑏𝑗 и
посчитаем 𝑓 𝑐 = 𝐹 𝐹 𝑇 (𝑐). Тогда по 𝑓 𝑐 за 𝒪(𝑛) можно легко восстановить 𝑓 𝑎 и 𝑓 𝑏.
Для этого вспомним про сопряжения комплексных чисел:
𝑥 + 𝑖𝑦 = 𝑥 − 𝑖𝑦, 𝑎 · 𝑏 = 𝑎 · 𝑏, 𝑤𝑛−𝑗 = 𝑤−𝑗 = 𝑤𝑗 ⇒ 𝑓 𝑐𝑛−𝑗 = 𝐶(𝑤𝑛−𝑗 ) = 𝐶(𝑤𝑗 ) ⇒
𝑓 𝑐𝑗 + 𝑓 𝑐𝑛−𝑗 = 2 · 𝐴(𝑤𝑗 ) = 2 · 𝑓 𝑎𝑗 . Аналогично 𝑓 𝑐𝑗 − 𝑓 𝑐𝑛−𝑗 = 2𝑖 · 𝐵(𝑤𝑗 ) = 2𝑖 · 𝑓 𝑏𝑗 .
Теперь, например, для умножения двух R[𝑥] можно использовать не 3 вызова FFT, а 2.
11.6. Умножение чисел, оценка погрешности
Общая схема умножения чисел:
цифра – коэффициент многочлена (𝑥 = 10); умножим многочлены; сделаем переносы.
Число длины 𝑛 в системе счисления 10 можно за 𝒪(𝑛) перевести в систему счисления 10𝑘 . Тогда
многочлены будут длины 𝑛/𝑘, умножение многочленов работать за 𝑛𝑘 log 𝑛𝑘 (убывает от 𝑘).
Возникает вопрос, какое максимальное 𝑘 можно использовать?
Коэффициенты многочлена-произведения будут целыми числами до (10𝑘 )2 𝑛𝑘 .
Чтобы в типе double целое число хранилось с погрешностью меньше 0.5 (тогда его можно
правильно округлить к целому), оно должно быть не более 1015 .
Получаем при 𝑛 ⩽ 106 , что (10𝑘 )2 106 /𝑘 ⩽ 1015 ⇒ 𝑘 ⩽ 4.
Аналогично для типа long double имеем (10𝑘 )2 106 /𝑘 ⩽ 1018 ⇒ 𝑘 ⩽ 6.
Это оценка сверху, предполагающая, что само FFT погрешность не накапливает... на самом
деле эта оценка очень близка к точной.
11.7. Применение. Циклические сдвиги.
Часто вылезает не «умножение многочленов», а подсчёт «скалярных произведений массива 𝑎
и сдвигов массива 𝑏». Это ровно коэффициенты 𝐴(𝑥) · 𝐵(𝑥), где 𝐴(𝑥) = 𝑎𝑖 𝑥𝑖 , 𝐵(𝑥) = 𝑏𝑖 𝑥𝑛−𝑖 .
∑︀
Глава #12. 5 декабря 2022. 70/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Длинная арифметика
Лекция #12: Длинная арифметика
5 декабря 2022
12.1. Простейшие операции
Самое главное — научиться операциям над целыми беззнаковыми числами.
Целые со знаком — то же + дополнительно хранить знак.
Вещественные — то же + хранить экспоненту: 12.345 = 12345𝑒−3, мы храним 12345 и −3.
Удобно хранить число в «массиве цифр», младшие цифры в младших ячейках.
Во примерах ниже мы выбираем систему счисления BASE = 10𝑘 , 𝑘 → max : нет переполнений.
Пусть есть длинное число 𝑎. При оценки времени работы будем использовать обозначения:
|𝑎| = 𝑛 – битовая длина числа и 𝑛𝑘 – длина числа, записанного в системе 10𝑘 . Помните, max 𝑘 ≈ 9.
Если мы ленивы и уверены, что в процессе вычислений не появятся числа длиннее 𝑁 ,
наш выбор – int[N];, иначе обычно используют vector<int> и следят за длиной числа.
Примеры простейших операций:
1 const int N = 100 , BASE = 1 e9 , BASE_LEN = 9;
2 vhoid add ( int *a , int * b ) { // сложение за 𝒪(𝑛/𝑘)
3 for ( int i = 0; i + 1 < N ; i ++) // +1, чтобы точно не было range check error
4 if (( a [ i ] += b [ i ]) >= BASE )
5 a [ i ] -= BASE , a [ i + 1]++;
6 }
7 void subtract ( int *a , int * b ) { // вычитание за 𝒪(𝑛/𝑘), 𝑎 ⩾ 𝑏
8 for ( int i = 0; i + 1 < N ; i ++) // +1, чтобы точно не было range check error
9 if (( a [ i ] -= b [ i ]) < 0)
10 a [ i ] += BASE , a [ i + 1] - -;
11 }
12 int divide ( int *a , int k ) { // деление на короткое за 𝒪(𝑛/𝑘), делим со старших разрядов
13 long long carry = 0; // перенос с более старшего разряда, он же остаток
14 for ( int i = N - 1; i >= 0; i - -) :
15 carry = carry * BASE + a [ i ]; // максимальное значение carry < BASE2
16 a [ i ] = carry / k , carry %= k ;
17 return carry ; // как раз остаток
18 }
19 int mul_slow ( int *a , int *b , int * c ) { // умножение за (𝑛/𝑘)2
20 fill (c , c + N , 0) ;
21 for ( int i = 0; i < N ; i ++)
22 for ( int j = 0; i + j < N ; j ++)
23 c[i + j] += a[i] * b[j]; // здесь почти наверняка произойдёт переполнение
24 for ( int i = 0; i + 1 < N ; i ++) // сначала умножаем, затем делаем переносы
25 c [ i + 1] += c [ i ] / BASE , c [ i ] %= BASE ;
26 }
27 void out ( int * a ) { // вывод числа за 𝒪(𝑛/𝑘)
28 int i = N - 1;
29 while ( i && ! a [ i ]) i - -;
30 printf ( " % d " , a [i - -]) ;
31 while ( i >= 0) printf ( " %0* d " , BASE_LEN , a [i - -]) ; // воспользовались таки BASE_LEN!
32 }
Чтобы в строке 19 не было переполнения, нужно выбирать BASE так, что BASE2 · N помещалось
в тип данных. Например, хорошо сочетаются BASE = 108 , N = 103 , тип — uint64_t.
Глава #12. 5 декабря 2022. 71/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Длинная арифметика
12.2. (-) Бинарная арифметика
Пусть у нас реализованы простейшие процедуры: «+, -, mul2, div2, less, equal, isZero».
Давайте выразим через них «*, \, gcd». Обозначим |𝑎| = 𝑛, |𝑏| = 𝑚.
Умножение будет полностью изоморфно бинарному возведению в степень.
1 num mul ( num a , num b ) :
2 if ( isZero ( b ) ) return 0; // если храним число, как vector, то isZero за 𝒪(1)
3 num res = mul ( mul ( mul2 ( a ) , div2 ( b ) ) ;
4 if ( mod2 ( b ) == 1) add ( res , a ) ; // функция mod2 всегда за 𝒪(1)
5 return res ;
Глубина рекурсии равна 𝑚. В процессе появляются числа не более (𝑛+𝑚) бит длины ⇒
каждая операция выполняется за 𝒪( 𝑛+𝑚
𝑘
) ⇒ суммарное время работы 𝒪((𝑛 + 𝑚) 𝑚
𝑘
).
Если большее умножать на меньшее, то 𝒪(max(𝑛, 𝑚) min(𝑛, 𝑚)/𝑘).
Деление в чём-то похоже... деля 𝑎 на 𝑏, мы будем пытаться вычесть из 𝑎 числа 𝑏, 2𝑏, 4𝑏, . . . .
1 pair < num , num > div ( num a , num b ) : // найдём для удобства и частное, и остаток
2 num c = 1 , res = 0;
3 while ( b < a ) // (𝑛−𝑚) раз
4 mul2 ( b ) , mul2 ( c ) ;
5 while (! isZero ( c ) ) : // Этот цикл сделает ≈ 𝑛−𝑚 итераций
6 if ( a >= b ) // 𝒪( 𝑛𝑘 ), так как длины 𝑎 и 𝑏 убывают от 𝑛 до 1
7 sub (a , b ) , add ( res , c ) ; // 𝒪( 𝑛𝑘 )
8 div2 ( b ) , div2 ( c ) ; // 𝒪( 𝑛𝑘 )
9 return { res , a };
Шагов главного цикла 𝑛−𝑚. Все операции за 𝒪( 𝑛𝑘 ) ⇒ суммарное время 𝒪((𝑛 − 𝑚) 𝑛𝑘 ).
Наибольший общий делитель сделаем самым простым Евклидом «с вычитанием».
Добавим только одну оптимизацию: если числа чётные, надо сразу их делить на два...
1 num gcd ( num a , num b ) :
2 int pow2 = 0;
3 while ( mod2 ( a ) == 0 && mod2 ( b ) == 0)
4 div2 ( a ) , div2 ( b ) , pow2 ++;
5 while (! isZero ( a ) && ! isZero ( b ) ) :
6 while ( mod2 ( a ) == 0) div2 ( a ) ;
7 while ( mod2 ( b ) == 0) div2 ( b ) ;
8 if ( a < b ) swap (a , b ) ;
9 a = sub (a , b ) ; // одно из чисел станет чётным
10 if ( isZero ( a ) ) swap (a , b ) ;
11 while ( pow2 - -) mul2 ( a ) ;
12 return a ;
Шагов главного цикла не больше 𝑛+𝑚. Все операции выполняются за max(𝑛, 𝑚)/𝑘.
Отсюда суммарное время работы: 𝒪(max(𝑛, 𝑚)2 /𝑘).
12.3. Деление многочленов за 𝒪(𝑛 log2 𝑛)
Коэффициенты многочлена 𝐴(𝑥) : 𝐴[0] – младший, 𝐴[deg 𝐴] – старший. 𝛾(𝐴) = deg 𝐴 − 1.
Задача: даны 𝐴(𝑥), 𝐵(𝑥) ∈ R[𝑥], найти 𝑄(𝑥), 𝑅(𝑥) : deg 𝑅 < deg 𝐵 ∧ 𝐴(𝑥) = 𝐵(𝑥)𝑄(𝑥) + 𝑅(𝑥).
Сперва простейшее решение за 𝒪(deg 𝐴 · deg 𝐵), призванное побороть страх перед делением:
Глава #12. 5 декабря 2022. 72/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Длинная арифметика
1 pair < F * , F * > divide ( int n , F *a , int m , F * b ) : // deg 𝐴 = 𝑛, deg 𝐵 = 𝑚, F – поле
2 F q [n - m +1];
3 for ( int i = n - m ; i >= 0; i - -) : // коэффициенты в порядке убывания 𝑥𝑖
4 q [ i ] = a [ i + m ] / b [ m ]; // m – степень ⇒ b[m] ̸= 0
5 for ( int j = 0; j <= m ; j - -) // вычитать имеет смысл, только если q[i] ̸= 0
6 a [ i + j ] -= b [ j ] * q [ i ]; // можно соптимизить, перебирать только b[j] ̸= 0
7 return {q , a }; // в массиве a[] как раз остался остаток
Теперь перейдём к решению за 𝒪(𝑛 log2 𝑛).
Зная 𝑄, мы легко найдём 𝑅, как 𝐴(𝑥) − 𝐵(𝑥)𝑄(𝑥) за 𝒪(𝑛 log 𝑛). Сосредоточимся на поиске 𝑄.
Пусть deg 𝐴 = deg 𝐵 = 𝑛, тогда 𝑄(𝑥) = 𝑎𝑏𝑛𝑛 . То есть, 𝑄(𝑥) можно найти за 𝒪(1).
Из этого мы делаем вывод, что 𝑄 зависит не обязательно от всех коэффициентов 𝐴 и 𝐵.
Lm 12.3.1. deg 𝐴 = 𝑚, deg 𝐵 = 𝑛 ⇒ deg 𝑄 = 𝑚 − 𝑛, и 𝑄 зависит только
от 𝑚−𝑛+1 старших коэффициентов 𝐴 и 𝑚−𝑛+1 коэффициентов 𝐵.
Доказательство. Рассмотрим деление в столбик, шаг которого: 𝐴 -= 𝛼𝑥𝑖 𝐵. 𝛼 = 𝐴[𝑖+deg 𝐵]
𝐵[deg 𝐵]
.
Поскольку 𝑖 + deg 𝐵 ⩾ deg 𝐵 = 𝑛, младшие 𝑛 коэффициентов 𝐴 не будут использованы. ■
Теперь будем решать задачу:
Даны 𝐴, 𝐵 ∈ R[𝑥] : 𝛾(𝐴) = 𝛾(𝐵) = 𝑛, найти 𝐶 ∈ R[𝑥] : 𝛾(𝐶) = 𝑛,
что у 𝐴 и 𝐵𝐶 совпадает 𝑛 старших коэффициентов.
1 int * Div ( int n , int *A , int * B ) // n – степень двойки (для удобства)
2 C = Div ( n /2 , A + n /2 , B + n /2) // нашли старших n/2 коэффициентов ответа
3 A’ = Subtract (n , A , n + n /2 -1 , Multiply (C , B ) )
4 D = Div ( n /2 , A’, B + n /2) // сейчас A’ состоит из n/2 не нулей и n/2 нулей
5 return concatenate (D , C ) // склеили массивы коэффициентов; вернули массив длины ровно n
Здесь Subtract – хитрая функция. Она знает длины многочленов, которые ей передали, и
сдвигает вычитаемый многочлен так, чтобы старшие коэффициенты совместились.
Время работы: 𝑇 (𝑛) = 2𝑇 (𝑛/2)+𝒪(𝑛 log 𝑛) = 𝒪(𝑛 log2 𝑛). Здесь 𝒪(𝑛 log 𝑛) – время умножения.
12.4. (-) Деление чисел
Оптимально использовать метод Ньютона, внутри которого все умножения – FFT.
Тогда мы получим асимптотику 𝒪(𝑛 log 𝑛). Об этом можно будет узнать на третьем курсе.
Разделяй и властвуй для многочленов также можно применить для чисел. Только аккуратно:
мы вычислим не 𝑛2 старших цифр, а лишь 𝑛2 − 𝒪(1).
Подробно мы сегодня изучим только метод за 𝒪((𝑛/𝑘)2 ).
Простейшие методы (оценка времени деление числа битовой длины 2𝑛 на число длины 𝑛).
1. Бинпоиск по ответу: 𝒪(𝑛3 /𝑘 2 ) при простейшем умножении, 𝒪(𝑛2 log 𝑛) при Фурье внутри.
2. Бинарной арифметикой (+, -, mul2, div2): 𝒪(𝑛2 /𝑘) времени.
3. Деление в столбик: 𝒪(𝑛2 /𝑘), 𝒪(𝑛2 /𝑘 2 ) времени. На нём остановимся подробнее.
Глава #12. 5 декабря 2022. 73/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Длинная арифметика
12.5. (-) Деление чисел за 𝒪((𝑛/𝑘)2 )
Делить будем в столбик. У нас уже было деление многочленов за квадрат. Действуем также.
Нужно научиться быстро искать старшую цифру частного.
∙ «Бинпоиск»: 𝒪(𝑛2 /𝑘)
Обозначим систему счисления S, log S = 𝑘.
Ищем старшую цифру частного бинарным поиском за 𝒪(𝑛): 𝑘 итераций бинпоиска, на каждой
проверяем за 𝒪(𝑛/𝑘). Итоговое время деления 𝒪((𝑛/𝑘) × 𝑘 × (𝑛/𝑘)) = 𝒪(𝑛2 /𝑘).
∙ «Деление старших цифр»: 𝒪(𝑛2 /𝑘 2 )
Старшую цифру можно почти точно посчитать за 𝒪(1).
Если старшие цифры чисел 𝑎 и 𝑏 – 𝑎𝑛 и 𝑏𝑚 соответственно, то хочется взять ≈ 𝑏𝑎𝑚𝑛 .
Такая формула не работает. Пример: делим 99 на 19, получится 19 = 9, должно получиться 4.
Тогда возьмём не одну, а две старшие цифры 𝑧0 = 𝑏𝑎𝑚𝑛 𝑎𝑏𝑚−1
𝑛−1
. Другой вариант 𝑧1 = 𝑏𝑚𝑎(𝑏𝑛𝑚−1
𝑎𝑛−1
+1)
. Ниже
представлен код деления в столбик, в строке 3 вычисляется старшая цифра, как 𝑧1 .
1 Div ( an , a , bn , b ) // [0..an] [0..bn] a[i]·10𝑖
2 for ( j = an - bn ; j >= 0; j - -) // считаем частное со старших разрядов
3 c [ j ] = ( a [ an - j ]* S + a [ an -1 - j ]) /( b [ bn ]* S + b [ bn -1]+1) // b[bn] != 0
4 a -= b * c [ j ]* S 𝑖
5 while ( a >= b * S 𝑖 ) c [ j ]++ , a -= b * S 𝑖 ; // взяли цифру меньше, чем нужно
6 a [ an -j -1] += a [ an - j ]* S , a [ an - j ] = 0 // перенос
Обозначим за 𝑥 реальное значение старшей цифры.
Для обеих формул (𝑧0 , 𝑧1 ) можно показать, |𝑧𝑖 − 𝑥| ⩽ 2. Докажем для 𝑧1 = 𝑎𝑛 𝑎𝑛−1
𝑏𝑚 (𝑏𝑚−1 +1)
.
Lm 12.5.1. 𝑧1 ⩽𝑥
Доказательство. При вычислении 𝑧1 числитель 𝑥 округлили вниз, знаменатель 𝑥 вверх. ■
Lm 12.5.2. 𝑎𝑛 < S2
Доказательство. Обычно цифры в системе счисления S меньше S, но мы могли ещё S(S−1)
перенести из предыдущего разряда ⇒ max 𝑎𝑛 = S(S−1) + (S−1) < S2 . ■
Lm 12.5.3. 𝑥⩽𝑧1 + 2
Доказательство. 𝑥 ⩽ 𝑦 = 𝑎𝑛𝑏(𝑎 𝑛−1 +1)
𝑚 𝑏𝑚−1
. Оценим разность 𝑦 − 𝑧1 =
1 𝑎𝑛 𝑎𝑛−1 𝑧1
𝑏𝑚 𝑏𝑚−1
+ 𝑏𝑚 𝑏𝑚−1 ·𝑏𝑚 (𝑏𝑚−1 +1) ⩽ S + 𝑏𝑚 𝑏𝑚−1 ⩽ 1S + 𝑆𝑥 ⩽ 1 + 1 = 2
1
■
Глава #12. 5 декабря 2022. 74/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Умножение матриц и 4 русских
Лекция #13: Умножение матриц и 4 русских
декабрь 2022
[Охотин ’2021]. Конспект Александра Охотина по нашей теме.
[Арлазаров,Диниц,Кронрод,Фараджев ’1970]. Оригинальная статья четырёх русских
про быстрое умножение матриц... вернее про быструю композицию отображений.
[CF: оптимизации]. Про то, как ускорить умножение за куб в 50 раз. Можно скрещивать со
Штрассеном.
13.1. Умножение матриц, простейшие оптимизации
Мы умеем за 𝒪(𝑛3 ). Из практически эффективных ещё есть алгоритм Штрассена за 𝒪(𝑛log2 7 ),
похожий на Карацубу, а из теоретических 𝒪(𝑛2.37 ). Подробности в конспекте Охотина.
Мы не будем затрагивать решения за 𝒪(𝑛3−𝜀 ), а сосредоточимся на технических оптимизациях
метода за 𝒪(𝑛3 ) для матриц над F2 .
3
∙ Обычное битовое сжатие и 𝒪( 𝑛𝑤 )
Куб выглядит так.
1 for ( i = 0; i < n ; i ++) // (i,k,j): можем выбрать любой порядок циклов
2 for ( k = 0; k < n ; k ++)
3 for ( j = 0; j < n ; j ++)
4 c [ i ][ j ] ^= a [ i ][ k ] & b [ k ][ j ];
Если матрицы 𝑎, 𝑏, 𝑐 представлены, как bitset<n> a[n],b[n],c[n], то часть
1 for ( j = 0; j < n ; j ++) // 𝒪(𝑛)
2 c [ i ][ j ] ^= a [ i ][ k ] & b [ k ][ j ];
эквивалентна версии за 𝒪( 𝑤𝑛 ) (𝑤 – размер машинного слова):
1 if ( a [ i ][ k ])
𝑛
2 c [ i ] ^= b [ k ]; // 𝒪( 𝑤 )
То есть, 𝑐𝑖 (𝑖-я строка 𝑐) – сумма ровно тех строк 𝑏, которые помечены единицами в 𝑎𝑖 .
𝑛 3
∙ Предподсчёт и 𝒪( log 𝑛
)
Попробуем в решении за 𝑛3 порядок циклов for i, for j, for k :
𝑐𝑖𝑗 = ⊕𝑘 (𝑎𝑖𝑘 & 𝑏𝑘𝑗 ), разобьём эту сумму на части длины 𝑚.
Чтобы за 𝒪(1) посчитать сумму сразу 𝑚 слагаемых, достаточно
1. Так хранить строки 𝑎 и столбцы 𝑏, чтобы за 𝒪(1) получать целое число из нужных 𝑚 бит.
2. Взять AND двух 𝑚-битных чисел.
3. Посчитать число бит в 𝑚-битном числе. Это предподсчёт за 𝒪(2𝑚 ): bn[i] = bn[i>>1] + (i&1).
Возьмём 𝑚 = log 𝑛, получим 𝒪(𝑚) на предподсчёт и 𝒪(𝑛3 / log 𝑛) на умножение.
На практике можно сделать предподсчёт, например, для 𝑚 = 20 при 𝑤 = 32
3
⇒ по скорости алгоритм работает также, как 𝑛𝑤 .
Глава #13. декабрь 2022. 75/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Умножение матриц и 4 русских
13.2. Четыре русских
∙ Общие слова
Если задач какого-то вида мало, давайте предподсчитаем заранее ответы для всех возможных
задач такого вида, а затем будем пользоваться предподсчётом в нужный момент за 𝒪(1).
∙ Задача
Даны 𝐴, 𝐵, найти 𝐶 = 𝐴 × 𝐵. Умножаем матрицы над Z (на самом ∀ кольцо),
предполагая, что 𝐴 содержит только нули и единицы.
∙ Решение
Разобьём 𝐴 на части по 𝑘 столбцов: 𝐴 = 𝐴1 𝐴2 . . . 𝐴𝑛/𝑘 .
Разобьём 𝐵 на части по 𝑘 строк: 𝐵 = 𝐵1 𝐵2 . . . 𝐵𝑛/𝑘 .
Заметим, 𝐴 × 𝐵 = 𝑖=1..𝑛/𝑘 (𝐴𝑖 × 𝐵𝑖 ). Проверьте размерности: (𝑛 × 𝑘) × (𝑘 × 𝑛) = 𝑛 × 𝑛.
∑︀
Теперь сосредоточимся на умножении 𝐶1 = 𝐴1 × 𝐵1 . Число 𝑘 выберем в самом конце.
∀𝑖 строка 𝐴1 [𝑖] матрицы 𝐴1 задаёт 𝐶1 [𝑖] (𝑖-ю∑︀
строку произведения 𝐶1 ), которая является суммой
(линейной комбинацией) строк 𝐵1 : 𝐶1 [𝑖] = 𝑗 𝐴1 [𝑖, 𝑗] · 𝐵1 [𝑗].
Матрица 𝐴1 состоит из {0, 1} ⇒ ∃ всего 2𝑘 различных строк 𝐴1 [𝑖]. Предподсчитаем все 2𝑘 сумм:
sum[mask] = add(sum[mask ^ (1 << bit)], b[bit]), где
bit – любой единичный бит mask, а функция add за 𝒪(𝑛) складывает строки. После предпод-
счёта алгоритм умножения выглядит так: for i: C[i] = sum[A[i]] и работает за 𝒪(𝑛2 ).
Получили умножение 𝐴1 × 𝐵1 за 2𝑘 𝑛 + 𝑛2 ⇒ оптимально взять 𝑘 = log 𝑛.
Итого 𝑛𝑘 умножений по Θ(𝑛2 ) каждое ⇒ 𝒪(𝑛3 / log 𝑛) на вычисление 𝐴 × 𝐵.
Замечание 13.2.1. Метод из данной главы подходит не только для F2 (см. постановку задачи).
13.3. Умножение матриц над F2 за 𝒪(𝑛3 /(𝑤 log 𝑛))
Если в явном виде применить идеи четырёх русских и битового сжатия, то получится
𝑛2
ровно 𝒪( log 𝑛
) операций со строками, каждая операция за 𝒪( 𝑤𝑛 ) ⇒ 𝒪(𝑛3 /(𝑤 log 𝑛)).
Прикинем время для 𝑛 = 4 000 и 𝑤 = 64: получаем ≈83 · 106 операций ≈ 0.1 секунда.
13.4. НОП за 𝒪(𝑛2 / log2 𝑛) (на практике)
Задача: даны две строки над алфавитом {0, 1}, найти длину НОП.
{︃
𝑓 [𝑖−1, 𝑗−1] + 1 если 𝑠[𝑖] = 𝑡[𝑗]
Рассмотрим обычную динамику: 𝑓 [𝑖, 𝑗] =
max(𝑓 [𝑖−1, 𝑗], 𝑓 [𝑖, 𝑗−1]) иначе
Идея: зафиксируем 𝑘 = 41 log 𝑛 и будем за 𝒪(1) сразу насчитывать кусок матрицы 𝑘 × 𝑘.
Заметим ∀𝑖, 𝑗 0 ⩽ 𝑓 [𝑖+1, 𝑗] − 𝑓 [𝑖, 𝑗] ⩽ 1 ∧ 0 ⩽ 𝑓 [𝑖, 𝑗+1] − 𝑓 [𝑖, 𝑗] ⩽ 1. Также ∀𝑖 𝑓 [𝑖, 0] = 𝑓 [0, 𝑖] = 0.
Давайте хранить только битовые матрицы 𝑥[𝑖, 𝑗] = 𝑓 [𝑖, 𝑗+1] − 𝑓 [𝑖, 𝑗], 𝑦[𝑖, 𝑗] = 𝑓 [𝑖+1, 𝑗] − 𝑓 [𝑖, 𝑗].
Зафиксируем 𝑘 и любую клетку [𝑖, 𝑗] пусть мы знаем «угол квадрата»: 𝑦[𝑖..𝑖+𝑘, 𝑗] и 𝑥[𝑖, 𝑗..𝑗+𝑘].
Противоположный «угол квадрата» (𝑦[𝑖..𝑖+𝑘, 𝑗] и 𝑥[𝑖, 𝑗..𝑗+𝑘]) зависит только от 4𝑘 бит:
𝑦[𝑖..𝑖+𝑘, 𝑗], 𝑥[𝑖, 𝑗..𝑗+𝑘], 𝑠[𝑖..𝑖+𝑘], 𝑡[𝑗..𝑗+𝑘] ⇒ используем четырёх русских и за 𝒪* (24𝑘 ) всё пред-
подсчитываем. Важно, что 𝑦[𝑖..𝑖+𝑘, 𝑗], 𝑥[𝑖, 𝑗..𝑗+𝑘], 𝑦[𝑖..𝑖+𝑘, 𝑗], 𝑥[𝑖, 𝑗..𝑗+𝑘], 𝑠[𝑖..𝑖+𝑘], 𝑡[𝑗..𝑗+𝑘] –
Глава #13. декабрь 2022. 76/77 Автор конспекта: Сергей Копелиович
Алгоритмы, 2 курс, осень 2022/23 Умножение матриц и 4 русских
целые 𝑘-битные числа ⇒ операции с ними происходят за 𝒪(1).
Заметьте, для каждого квадрата 𝑘 × 𝑘 мы получали только значения на границы.
Другие нам и не нужны. Также мы ни в какой момент времени не пытались считать 𝑓 ,
нам хватает 𝑥 и 𝑦. Реальные значения 𝑓 возникают внутри предподсчёта.
Ещё нам в самом конце нужна собственно длина НОП – это сумма последней строки 𝑥.
13.5. (-) Схема по таблице истинности
Задача. Дана таблица истинности булевой функции от 𝑛 переменных, вектор длины 2𝑛 из нулей
и единиц. Построить булеву схему с такой таблицей истинности.
Построить минимальную схему NP-трудно.
КНФ и ДНФ дадут 𝒪(2𝑛 𝑛) гейтов.
Разделяй и властвуй: построить формулу 𝜙0 от 𝑛−1 переменной для 𝑥𝑛 = 0 и 𝜙1 для 𝑥𝑛 = 1,
ответ: 𝜙 = (𝜙0 ∧ 𝑥𝑛 = 0) ∨ (𝜙1 ∧ 𝑥𝑛 = 1). Получаем размер 𝑆(𝑛) = 2𝑆(𝑛−1) + 3 = Θ(2𝑛 ).
К последнему решению можно применить оптимизацию предподсчёта: 𝑛 ⩽ 𝑘 ⇒ все 𝑚 = 22
𝑘
𝑛
возможных функций посчитано. Для 𝑘 = (log 𝑛) − 1 имеем 𝑚 = 2 2 , построим схемы для всех 𝑚
возможных функций из 𝑘 переменных, получим отсечение: на глубине рекурсии 𝑛 − 𝑘 вместо
построения 𝜙 взять уже готовую.
Замечание 13.5.1. Первые два решения могут строить и формулу, и схему. В последнем решении
мы переиспользуем одну и ту же часть ⇒ схему мы так можем построить, а формулу нет.
Замечание 13.5.2. Для «схемы от случайной таблицы истинности» мы получили асимптотиче-
ски оптимальное решение.
13.6. (-) Оптимизация перебора для клик
Рассмотрим перебор для поиска максимальной клики
1 int go ( int A ) : // A = маска вершин, которые можно добавить к клике
2 if ( A == 0) return 0 // больше никого не добавить...
3 int i = anyBit ( A ) // например, младший бит мы точно умеем за 𝒪(1)
4 return max ( A & 2 𝑖 , 1 + go ( A & graph [ i ]) ) ; // 𝑔𝑟𝑎𝑝ℎ[𝑖] – соседи 𝑖
5 go (2 𝑛 -1) // изначально можно брать все вершины
Если |𝐴| ⩽ 7 = 𝑘, мы можем сказать «оставшийся граф мал, обратимся к предподсчёту». Нужно
заранее предподсчитать ответы для всех 2𝑘(𝑘−1)/2 = 221 возможных графов из 𝑘 = 7 вершин.
Получили оптимизацию по времени работы 𝑇 (𝑛) → 2𝑘(𝑘−1)/2 + 𝑇 (𝑛 − 𝑘).
13.7. (-) Транзитивное замыканиие
На практике мы также изучим, как транзитивное замыкание
1. Сводить к 𝒪(log 𝑛) умножениям матриц.
2. Считать за 𝒪(одного умножения матриц).
3
3. Считать инкрементально за 𝒪(𝑞 𝑤𝑛 + 𝑛𝑤 ).
Глава #13. декабрь 2022. 77/77 Автор конспекта: Сергей Копелиович