Вы находитесь на странице: 1из 13

Математические основы алгоритмов, осень 2022 г.

Лекция 3. Метод динамического программирования: задача


о порядке умножения матриц. Нахождение наибольшей
общей подпоследовательности: «народный» алгоритм,
алгоритм Хиршберга. Поиск в ориентированном графе:
поиск в ширину, поиск в глубину, топологическая
сортировка, нахождение компонентов сильной связности.
Поиск в графе с весами: алгоритм Беллмана–Форда∗
Александр Охотин

12 декабря 2022 г.

Содержание
1 Задача о порядке умножения матриц 1

2 Нахождение наибольшей общей подпоследовательности 2

3 Поиск в ориентированном графе 6

4 Поиск в графе с весами: алгоритм Беллмана–Форда 11

1 Задача о порядке умножения матриц


Пусть нужно перемножить 𝑛 матриц 𝑀1 × . . . × 𝑀𝑛 , где каждая матрица 𝑀𝑖 имеет размер
𝑚𝑖−1 × 𝑚𝑖 , для набора чисел 𝑚0 , 𝑚1 , . . . , 𝑚𝑛 . В силу ассоциативности, скобки можно рас-
ставить как угодно. От их расстановки зависит общее число операций, и, чтобы умножить
матрицы быстрее, ставится задача заранее определить наилучший порядок их умножения.

Пример 1. Для произведения трёх матриц размера 100 × 2, 2 × 100 и 100 × 3, если до-
гадаться сперва перемножить две последних матрицы, то получится матрица 2 × 3,
которую легко будет перемножить с матрицей 100 × 2 — всего потребуется выполнить
2 · 100 · 3 + 100 · 2 · 3 = 1200 умножений и примерно столько же сложений. Если же начать
с умножения двух первых матриц, получится неудобоумножаемая промежуточная мат-
рица размера 100×100, и общее число умножений составит 100·2·100+100·100·3 = 50000.


Краткое содержание лекций, прочитанных студентам 1-го курса факульте-
та МКН СПбГУ в осеннем семестре 2022–2023 учебного года. Страница курса:
http://users.math-cs.spbu.ru/~okhotin/teaching/algorithms1_2022/.

1
Конечно, можно просто перебрать все возможные порядки — их ровно число Каталана
𝐶𝑛 — для каждого определить общее число операций, и найти среди этих чисел наимень-
шее. Но не исключено, что чем за такое браться, быстрее будет перемножить матрицы как
придётся!
Задача решается методом динамического программирования. Сперва ставится цель: по-
строить верхнедиагональную числовую матрицу 𝑇 размера 𝑛 × 𝑛, где 𝑇𝑖,𝑗 — наименьшее
число действий, необходимых для вычисления 𝑀𝑖+1 × . . . × 𝑀𝑗 .
Внешний цикл по длине куска ℓ = 𝑗 − 𝑖, второй — по 𝑖, во внутреннем перебираются все
разбиения 𝑀𝑖+1 × . . . × 𝑀𝑗 = (𝑀𝑖+1 × . . . × 𝑀𝑘 ) × (𝑀𝑘+1 × . . . × 𝑀𝑗 ) и для каждого такого раз-
биения вычисляется время вычисления произведения 𝑀𝑖+1 × . . . × 𝑀𝑗 при таком разбиении,
которое составит 𝑇𝑖,𝑘 + 𝑇𝑘,𝑗 + 𝑚𝑖 𝑚𝑘 𝑚𝑗 . Из всех таких значений берётся минимальное.
𝑗−1 (︀ )︀
𝑇𝑖,𝑗 = min 𝑇𝑖,𝑘 + 𝑇𝑘,𝑗 + 𝑚𝑖 𝑚𝑘 𝑚𝑗
𝑘=𝑖+1

Алгоритм 1 Нахождение наилучшего порядка умножения матриц и их умножение в соот-


ветствии с этим порядком
На входе: матрицы 𝑀1 , . . . , 𝑀𝑛 , размер каждой матрицы 𝑀𝑖 — 𝑚𝑖−1 × 𝑚𝑖 .
Нахождение числа действий
1: for 𝑖 = 1 to 𝑛 do
2: 𝑇𝑖−1,𝑖 = 0 /* произведение одной матрицы — это она сама, считать нечего */
3: for ℓ = 2 to 𝑛 do
4: for 𝑖 = 0 to 𝑛 − ℓ do
5: пусть 𝑗 = 𝑖 + ℓ /* ищется наилучший порядок для 𝑀𝑖+1 × . . . × 𝑀𝑗 */
6: 𝑇𝑖,𝑗 = ∞ /* пока не найдено никаких способов перемножить */
7: for 𝑘 = 𝑖 + 1 to 𝑗 − 1 do
8: 𝑡 = 𝑇𝑖,𝑘 + 𝑇𝑘,𝑗 + 𝑚𝑖 𝑚𝑘 𝑚𝑗 /* способ перемножить за столько шагов */
9: if 𝑡 < 𝑇𝑖,𝑗 then /* если это быстрее известного */
10: 𝑇𝑖,𝑗 = 𝑡 /* . . . то оценка числа действий улучшается */
Произведение 𝑀𝑖+1 × . . . × 𝑀𝑗 вычисляется следующей процедурой.
Процедура умножить(𝑖, 𝑗)
1: if 𝑖 + 1 = 𝑗 then
2: return 𝑀𝑗
3: for 𝑘 = 𝑖 + 1 to 𝑗 − 1 do
4: if 𝑇𝑖,𝑗 = 𝑇𝑖,𝑘 + 𝑇𝑘,𝑗 + 𝑚𝑖 𝑚𝑘 𝑚𝑗 then /* для какого-то 𝑘 равенство выполнится */
5: return умножить(𝑖, 𝑘) × умножить(𝑘, 𝑗)

Потом, используя таблицу, сами матрицы перемножаются вызовом процедуры


умножить(0, 𝑛).
Время работы: 𝑂(𝑛3 ) для построения таблицы, далее — 2𝑛 − 1 вызовов процедуры
умножить(𝑖, 𝑗), в каждом — 𝑂(𝑛) итераций цикла. И ещё само время умножения матриц.
В процедуре умножить(𝑖, 𝑗) можно обойтись без цикла по 𝑘, если записывать значение
𝑘𝑖,𝑗 при построении таблицы.

2 Нахождение наибольшей общей подпоследовательности


Ставится задача сравнить две строки на похожесть. Пример: сравнение двух редакций од-
ного и того же текста. Другой пример: ДНК двух организмов. Реализовано в программе
diff. Как это работает?

2
Ww дубль-вэ
Ww дубль-вэ (курсив)
Ωω омега

Рис. 1: (слева) Различия между начертаниями букв 𝑤 (дубль-вэ) и 𝜔 (омега); (справа)


Каждый раз, когда строку обозначают буквой 𝜔, умирает котёнок.

2.1 Строки
Сперва задаётся конечное множество символов Σ, называемое алфавитом. В абстрактных
примерах элементы Σ обозначаются строчными латинскими буквами из начала алфавита
(𝑎, 𝑏, . . . ).

Определение 1. Строкой над алфавитом Σ называется всякая конечная последователь-


ность 𝑤 = 𝑎1 . . . 𝑎ℓ , где ℓ ⩾ 0, и 𝑎1 , . . . , 𝑎ℓ ∈ Σ — символы.

Строки обычно обозначаются латинскими буквами 𝑤 (дубль-вэ1 ), 𝑢, 𝑣, 𝑥, 𝑦 и 𝑧. Напри-


мер, 𝑤 = 𝑎𝑏𝑏 — это 3-символьная строка над алфавитом, содержащим символы 𝑎 и 𝑏.
Число символов в строке называется её длиной, |𝑤| = ℓ. Существует единственная строка
длины 0, называемая пустой строкой и обозначаемая через 𝜀. Множество всех строк над
алфавитом Σ обозначается через Σ* .
Строка, полученная удалением 0 и более символов из строки, при сохранении относи-
тельного порядка оставшихся символов, называется её подпоследовательностью: то есть,
если 1 ⩽ 𝑖1 < . . . < 𝑖𝑘 ⩽ 𝑚, то строка 𝑎𝑖1 . . . 𝑎𝑖𝑘 — подпоследовательность строки 𝑎1 . . . 𝑎𝑚 .

2.2 «Народный» алгоритм


Пусть 𝑎1 . . . 𝑎𝑚 и 𝑏1 . . . 𝑏𝑛 — две строки. Требуется найти самую длинную общую подпосле-
довательность (longest common subsequence) этих двух строк. Обычно бывает нужна даже
не сама подпоследовательность — а последовательность номеров соответствующих позиций
в первой и второй строках. Эта последовательность называется выравниванием (alignment)
двух строк.
«Наивный» алгоритм: перебрать все варианты совмещения подпоследовательностей. Но
это будет экспоненциальное время относительно 𝑚 и 𝑛.
Задача решается динамическим программированием за время 𝑂(𝑚𝑛); это «народный»
алгоритм, его автора трудно установить — и это классический пример применения метода
динамического программирования. Для всякого префикса (начального куска) 𝑢𝑖 = 𝑎1 . . . 𝑎𝑖
первой строки и для всякого префикса 𝑣𝑗 = 𝑏1 . . . 𝑏𝑗 второй строки алгоритм находит длину
наибольшей общей подпоследовательности 𝑢𝑖 и 𝑣𝑗 и запоминает её в переменной 𝑇𝑖,𝑗 . Ячей-
ки прямоугольной таблицы 𝑇𝑖,𝑗 заполняются последовательно, начиная от более коротких
префиксов, и заканчивая префиксами 𝑢𝑚 и 𝑣𝑛 — то есть, данными строками целиком.
Значение каждой ячейки 𝑇𝑖,𝑗 вычисляется так. Если 𝑖 = 0 или 𝑗 = 0, то есть если один из
двух префиксов пуст, то 𝑇𝑖,𝑗 = 0. Для всяких 𝑖, 𝑗 ⩾ 1 рассматриваются следующие два слу-
чая. Если последние символы двух префиксов совпадают (𝑎𝑖 = 𝑏𝑗 ), это значит, что в одной
из наибольших общих подпоследовательностей они соответствуют друг другу: действитель-
но, если 𝑎𝑖 соответствует другому символу 𝑏𝑗 ′ , где 𝑗 ′ < 𝑗, то тогда символам 𝑏𝑗 ′ +1 , . . . , 𝑏𝑗 не
1
На худой конец, «дабл-ю». Но, ради Бога, ни в коем случае не «омега»! Омега (𝜔, Ω) — это совсем
другая буква, и строки ею никогда не обозначаются! (см. рис. 1)

3
b1 bj–1 bj b1 bj–1 bj
a1 a1

ai–1 +1 ai–1
ai ai
max

Рис. 2: Построение элемента 𝑇𝑖,𝑗 : (слева) случай 𝑎𝑖 = 𝑏𝑗 ; (справа) случай 𝑎𝑖 ̸= 𝑏𝑗 .

соответствует ничего, и потому можно переключить 𝑎𝑖 на 𝑏𝑗 , получив другую общую подпо-


следовательность 𝑢𝑖 и 𝑣𝑗 той же длины; если же ни 𝑎𝑖 , ни 𝑏𝑗 ничему не соответствуют, то их
можно поставить друг другу в соответствие и получить более длинную общую подпоследо-
вательность. Поэтому можно считать, что 𝑎𝑖 и 𝑏𝑗 соответствуют друг другу, а перед ними
идёт наибольшая общая подпоследовательность 𝑢𝑖−1 и 𝑣𝑗−1 длины 𝑇𝑖−1,𝑗−1 . Eсли же по-
следние символы различны (𝑎𝑖 ̸= 𝑏𝑗 ), то в наибольшей общей подпоследовательности или 𝑎𝑖
не соответствует никакому символу (и тогда остаётся подпоследовательность длины 𝑇𝑖−1,𝑗 ),
или 𝑏𝑗 не соответствует никакому символу (остаётся 𝑇𝑖,𝑗−1 ). На основе этих соображений
строится следующая формула.
{︃
𝑇𝑖−1,𝑗−1 + 1 если 𝑎𝑖 = 𝑏𝑗
𝑇𝑖,𝑗 =
max(𝑇𝑖−1,𝑗 , 𝑇𝑖,𝑗−1 ) если 𝑎𝑖 ̸= 𝑏𝑗

Алгоритм 2 Нахождение длины наибольшей общей подпоследовательности


На входе: строки 𝑢 = 𝑎1 . . . 𝑎𝑚 и 𝑣 = 𝑏1 . . . 𝑏𝑛 . Строится матрица 𝑇 размера (𝑚 + 1) × (𝑛 + 1),
где 𝑇𝑖,𝑗 — длина наибольшей общей подпоследовательности 𝑢𝑖 = 𝑎1 . . . 𝑎𝑖 и 𝑣𝑗 = 𝑏1 . . . 𝑏𝑗 .

1: for 𝑗 = 0 to 𝑛 do
2: 𝑇0,𝑗 = 0
3: for 𝑖 = 1 to 𝑚 do
4: 𝑇𝑖,0 = 0
5: for 𝑗 = 1 to 𝑛 do
6: if 𝑎𝑖 = 𝑏𝑗 then
7: 𝑇𝑖,𝑗 = 𝑇𝑖−1,𝑗−1 + 1
8: else
9: 𝑇𝑖,𝑗 = max(𝑇𝑖−1,𝑗 , 𝑇𝑖,𝑗−1 )
10: return 𝑇𝑚,𝑛

Сама наибольшая общая подпоследовательность и соответствующее ей совмещение ис-


ходных строк строятся на основе чисел в таблице, от конца к началу. Глядя на 𝑇𝑖,𝑗 и на
символы 𝑎𝑖 и 𝑏𝑗 , можно определить, по какому из двух случаев вычислено значение 𝑇𝑖,𝑗 .
Недостаток алгоритма в том, что таблица занимает память 𝑂(𝑚𝑛), и это много. Если
нужно только вычислить длину наибольшей общей подпоследовательности, то хранить всю
таблицу не обязательно, можно ограничиться двумя столбцами (или двумя строчками). Но
обычно бывает нужна сама подпоследовательность, а скорее даже совмещение двух строк,
дающее эту подпоследовательность. Можно ли использовать меньше памяти и при этом всё
это вычислить? Оказывается, что можно.

4
Рис. 3: Дэниэл Хиршберг (род. 1949).

b1 bj–1 bj bj+1 bn
a1
u' T u',v

u''
~
T u'',v
am

Рис. 4: Нахождение оптимального разбиения 𝑣 = 𝑣 ′ 𝑣 ′′ в алгоритме Хиршберга.

2.3 Алгоритм Хиршберга


Алгоритм Хиршберга [1975]: построение наибольшей общей подпоследовательности за вре-
мя 𝑂(𝑚𝑛), используя память 𝑂(min(𝑚, 𝑛)).
Этот алгоритм интересен красивым сочетанием двух изучавшихся методов построения
алгоритмов: используется метод «разделяй и властвуй», внутри которого наилучшее раз-
биение находится подобно динамическому программированию.
Пусть 𝑢 = 𝑢′ 𝑢′′ — некоторое разбиение 𝑢. Тогда оптимальное совмещение 𝑢 и 𝑣 совмещает
𝑢 с каким-то начальным куском 𝑣 — пусть это 𝑣 ′ — и 𝑢′′ — с остатком, 𝑣 ′′ . Нужно найти это

разбиение 𝑣 = 𝑣 ′ 𝑣 ′′ , чтобы потом отдельно запустить совмещение двух соответствующих


пар кусков.
Как это сделать? Алгоритм делит 𝑢 на две подстроки 𝑢′ , 𝑢′′ примерно равной длины.

Сперва динамическим программированием находится последняя строчка таблицы 𝑇 𝑢 ,𝑣 , со-
ответствующей алгоритму LCS для 𝑢′ и 𝑣. Её 𝑗-й элемент содержит длину наибольшей
общей подпоследовательности 𝑢′ и 𝑣𝑗 = 𝑏1 . . . 𝑏𝑗 .
′′
Для строк 𝑢′′ и 𝑣 рассматривается симметричная таблица 𝑇̃︀𝑢 ,𝑣 , в которой элемент с ко-
ординатами (𝑖, 𝑗) содержит длину наибольшей общей подпоследовательности суффикса 𝑢′′ ,
содержащего символы с 𝑖-го до последнего, и суффикса 𝑏𝑗 . . . 𝑏𝑛 , то есть последних 𝑛 − 𝑗 + 1
символов строки 𝑣. Для этой таблицы вычисляется её первая строчка (𝑖 = 1), соответ-
ствующая всей строке 𝑢′′ — и 𝑗-й элемент этой строчки даёт длину наибольшей общей
подпоследовательности 𝑢′′ и 𝑣̃︀𝑗 = 𝑏𝑗 . . . 𝑏𝑛 .
Для каждого разбиения 𝑣 = 𝑣 ′ 𝑣 ′′ , пусть 𝑗 = |𝑣 ′ |. Тогда длина наибольшей общей подпо-

следовательности при таком разбиении — это сумма 𝑗-го элемента последней строки 𝑇 𝑢 ,𝑣
′′
и (𝑗 + 1)-го элемента первой строки 𝑇̃︀𝑢 ,𝑣 . Остаётся посмотреть, для какого 𝑗 достигает-

5
ся максимум — это и будет искомое разбиение 𝑣 = 𝑣 ′ 𝑣 ′′ . Для этого вычисления алгоритм
использовал 𝑂(|𝑣|) ячеек памяти, которые теперь можно освободить.
Далее алгоритм дважды рекурсивно вызывает сам себя, чтобы вычислить наилучшее
совмещение 𝑢′ и 𝑣 ′ , и 𝑢′′ и 𝑣 ′′ . Полученные совмещения последовательно приписываются
друг к другу.

Лемма 1. Алгоритм Хиршберга работает за время 𝑂(𝑚𝑛).

Доказательство. Принимая за единицу времени время, затрачиваемое на вычисление зна-


чения одного элемента 𝑇𝑖,𝑗 в «народном» алгоритме, утверждается, что в общей сложности
будет выполнено не более чем 2𝑚𝑛 шагов.
Пусть 𝑓 (𝑚, 𝑛) — время работы в наихудшем случае. Тогда индукцией по 𝑚 и 𝑛 дока-
зывается неравенство 𝑓 (𝑚, 𝑛) ⩽ 2𝑚𝑛. При запуске на строках 𝑢 и 𝑣, где |𝑢| = 𝑚 и |𝑣| = 𝑛,
вычисление таблицы 𝑇𝑢′ ,𝑣 займёт 12 𝑚𝑛 шагов, и за столько же шагов будет вычисляться
′′
таблица 𝑇̃︀𝑢 ,𝑣 . После этого проводятся два рекурсивных вызова, один из которых занимает
𝑓(𝑚2 , 𝑘) шагов, а другой — 𝑓 ( 2 , 𝑛 − 𝑘) шагов, для некоторого 𝑘. Время работы рекурсивных
𝑚

вызовов оценивается по предположению индукции, откуда получается оценка того же вида


для 𝑓 (𝑚, 𝑛).

𝑓 (𝑚, 𝑛) = 2 · 12 𝑚𝑛 + max 𝑓 ( 𝑚 𝑚
(︀ )︀
𝑘 2 , 𝑘) + 𝑓 ( 2 , 𝑛 − 𝑘) ⩽ 𝑚𝑛 + max(𝑚𝑘 + 𝑚(𝑛 − 𝑘)) = 2𝑚𝑛
𝑘

3 Поиск в ориентированном графе


3.1 Поиск в ширину
Поиск в ширину (breadth-first search): в ориентированном графе 𝐺 = (𝑉, 𝐸) обойти все
вершины, достижимые из данной вершины 𝑠 ∈ 𝑉 , каждую по одному разу. Это програм-
ма, которую то и дело приходится писать на практике: граф может быть задан неявно,
вершины могут быть сложными объектами, рёбра — какими-то действиями над этими объ-
ектами, и нужно получить все объекты, выразимые из данного через имеющиеся действия.
Или же граф может быть непосредственно задан в памяти: каждая вершина — это массив
указателей на другие такие же вершины, соответствующих исходящим дугам.
Алгоритм поиска в ширину работает так. В каждый момент времени каждая вершина
может быть помечена или не помечена. Если вершина помечена, это значит, что алгоритм
уже нашёл путь из 𝑠 в неё. Кроме пометок на вершинах, алгоритм хранит очередь, в ко-
торой находятся все те помеченные вершины, для которых ещё не обработаны исходящие
дуги. Таким образом, в каждый момент времени каждая вершина может быть или (1)
непомеченной, или (2) помеченной, но необработанной, или (3) помеченной и обработан-
ной.2 Изначально помечена только вершина 𝑠 и очередь состоит из 𝑠. На каждом шаге из
очереди извлекается следующая вершина 𝑢, рассматриваются все исходящие из неё дуги
(𝑢, 𝑣), и если 𝑣 оказывается непомеченной, она помечается и помещается в конец очереди.
Время работы: каждая вершина попадает в очередь не более одного раза, каждая вы-
ходящая из неё дуга рассматривается однажды. Отсюда: 𝑂(|𝐸|).

2
Их иногда называют «белыми», «серыми» и «чёрными», соответственно.

6
Алгоритм 3 Поиск в ширину
Структуры данных: очередь вершин для обработки, множество всех встреченных вершин
(можно хранить вместе с вершинами как пометки, а если граф задан неявно, то в отдельной
структуре данных).
1: пометить 𝑠
2: поместить 𝑠 в конец очереди
3: while очередь непуста do
4: извлечь из очереди первый элемент, 𝑢
5: for all 𝑣: (𝑢, 𝑣) ∈ 𝐸 do
6: if 𝑣 не помечена then
7: пометить 𝑣
8: поместить 𝑣 в конец очереди
9: теперь 𝑢 обработана

Утверждение 1. В каждый момент времени очередь состоит из некоторых вершин,


находящихся на расстоянии ℓ от 𝑠, вслед за которыми идут некоторые вершины, нахо-
дящиеся на расстоянии ℓ + 1 от 𝑠. При этом все вершины на расстоянии, меньшем, чем
ℓ, уже обработаны, равно как и все вершины на расстоянии ℓ, не вошедшие в очередь. Из
вершин на расстоянии ℓ + 1 в очереди есть ровно все потомки обработанных вершин.

Набросок доказательства. Базис: в очереди только 𝑠, она находится на расстоянии 0, и это


все вершины на расстоянии 0.
Переход: пусть извлекается 𝑢, она на расстоянии ℓ. Все новые вершины 𝑣, добавленные в
очередь по дуге (𝑢, 𝑣), будут на расстоянии ℓ + 1. Действительно, будь для 𝑣 более короткий
путь, она, по предположению индукции, уже была бы помечена ранее.

Утверждение 2. (I) алгоритм помечает вершину 𝑣 тогда и только тогда, когда есть
путь из 𝑠 в 𝑣; (II) если алгоритм находит 𝑣 по дуге (𝑢, 𝑣), то один из кратчайших путей
из 𝑠 в 𝑣 идёт через 𝑢; (III) все пройденные дуги (𝑢, 𝑣) образуют дерево.

Доказательство. (I)
(обходит ⇒ есть путь) индукция по длине вычисления. Базис: помечена в этой строке
— тогда это 𝑠. Переход: когда 𝑣 помечается, есть ранее помеченная вершина 𝑢 и дуга (𝑢, 𝑣).
Путь из 𝑠 в 𝑢 есть по предположению индукции, он продолжается дугой до искомого.
(есть путь ⇒ обходит) индукция по длине пути. Базис: 0, помечается в первой строке.
Переход: пусть 𝑢 — предпоследняя вершина на пути из 𝑠 в 𝑣. Тогда путь в неё короче, и
по предположению индукции 𝑢 когда-то помечается. Одновременно 𝑢 заносится в очередь,
откуда она рано или поздно будет извлечена, и тогда при рассмотрении дуги (𝑢, 𝑣) будет
обнаружена вершина 𝑣.
(II) Из утверждения 1.
(III) Всякий раз, когда алгоритм переходит по дуге, он переходит в ранее не рассмат-
ривавшуюся вершину. Поэтому все рассмотренные дуги образуют ориентированный граф,
в котором у одной вершины степень захода 0, а у остальных — 1. В таком графе не может
быть циклов, в том числе неориентированных.

В ходе своей работы алгоритм проходит по некоторому остовному дереву подграфа,


достижимого из 𝑠. Для каждой пройденной дуги можно добавить какое-нибудь содержа-

7
тельное действие — например, записывать куда-нибудь это остовное дерево или вычислять
кратчайшее расстояние от 𝑠 до очередной вершины.
Если нужно обойти не только вершины, достижимые из данной, а вообще все вершины,
то надо перезапускать 𝐵𝐹 𝑆(𝑠) на непомеченных вершинах, пока таковые будут оставаться.

3.2 Поиск в глубину


Поиск в глубину (depth-first search, DFS): пройти до конца самый длинный путь, затем
вернуться на шаг назад и продолжить следующий путь — и так далее, пока все вершины,
достижимые из данной, не будут найдены.
В каждый момент времени каждая вершина — непомеченная («белая»); помеченная
и обрабатываемая («серая») или помеченная и обработанная («чёрная»). Изначально все
непомеченные. По мере работы алгоритма каждая вершина будет сперва обнаруживаться
и помечаться (начало обработки), а потом в какой-то момент её обработка будет завершать-
ся. Сперва произвольно выбирается одна непомеченная вершина: она помечается, и все её
непомеченные соседи один за другим рекурсивно обходятся. Когда обходы всех соседей
заканчиваются и последний рекурсивный вызов возвращается, обработка текущей верши-
ны завершается, процедура возвращается. По завершении обхода выбирается следующая
непомеченная вершина, и повторяется то же самое.

Алгоритм 4 Поиск в глубину


Процедура 𝐷𝐹 𝑆(𝑢)
1: пометить 𝑢 /* «серая» */
2: for all 𝑣: (𝑢, 𝑣) ∈ 𝐸 do
3: if 𝑣 не помечена then /* «белая» */
4: 𝐷𝐹 𝑆(𝑣)
5: обработка 𝑢 закончена /* «чёрная» */

Обрабатываемые, «серые» вершины — это те, которые находятся в стеке возврата.

Алгоритм 5 Поиск в глубину


Основная процедура
1: 𝑡 = 0
2: for all 𝑠 ∈ 𝑉 do
3: if 𝑠 не помечена then
4: 𝐷𝐹 𝑆(𝑠)
Процедура 𝐷𝐹 𝑆(𝑢)
1: пометить 𝑢
2: 𝑡 = 𝑡 + 1
3: начало(𝑢) = 𝑡
4: for all 𝑣: (𝑢, 𝑣) ∈ 𝐸 do
5: if 𝑣 не помечена then
6: 𝐷𝐹 𝑆(𝑣)
7: 𝑡 = 𝑡 + 1
8: конец(𝑢) = 𝑡

Если записать все дуги, по которым делаются рекурсивные вызовы, то получится лес.
Кроме того, алгоритм часто используется, чтобы поставить каждой вершине в соответ-
ствие два числа, соответствующие времени её обхода, измеряемому в шагах работы алго-

8
ритма. В момент перекраски в серый вершине назначается время обнаружения, а в момент
перекраски в чёрный — время окончания.
Время работы: 𝑂(|𝑉 | + |𝐸|); написать просто |𝐸| нельзя, потому что может быть много
изолированных вершин.

3.3 Топологическая сортировка


Дан ориентированный граф 𝐺 = (𝑉, 𝐸). Задача: расположить его вершины в таком поряд-
ке, чтобы для каждой дуги (𝑢, 𝑣) ∈ 𝐸 вершина 𝑢 была перечислена раньше, чем 𝑣. Это
возможно тогда и только тогда, когда граф ациклический.
Пример: порядок чтения книг. Не прочитав книгу по математическому анализу, не имеет
смысла читать книгу о дифференциальных уравнениях — это дуга в графе. Топологическая
сортировка даёт допустимый порядок прочтения.
Решение: запустить поиск в глубину, потом отсортировать вершины по времени окон-
чания. Законченные позже всего должны быть в начале.
Почему это работает? Это следует из простых свойств поиска в глубину.

Лемма 2. Граф ациклический тогда и только тогда, когда при поиске в глубину никогда
не рассматривается дуга, ведущая в вершину, находящуюся в стеке возврата (дуга из
«серой» вершины в «серую»).

Доказательство. Поскольку последовательность вершин в стеке возврата образует путь в


графе, такая дуга замкнёт этот путь в цикл.
И обратно: если есть цикл, то какая-то из его вершин, 𝑣, будет рассмотрена первой.
Тогда рекурсивные вызовы, делаемые процедурой 𝐷𝐹 𝑆(𝑣), обойдут весь цикл и рассмотрят
замыкающее цикл ребро.

Лемма 3. Если в ациклическом графе есть дуга (𝑢, 𝑣), то время завершения 𝑣 меньше,
чем время завершения 𝑢.

Доказательство. Эта дуга однажды будет рассматриваться. В этот момент 𝑣 нет в стеке
возврата. Если 𝑣 помечена, то у неё уже есть время завершения, а у 𝑢 ещё только будет
(позднее). Если же 𝑣 ранее не рассматривалась, то в неё спустится рекурсия, и по воз-
вращении из рекурсии 𝑣 будет уже обработана — а 𝑢 только предстоит получить время
завершения.

3.4 Нахождение компонентов сильной связности


Ставится задача найти в данном ориентированном графе 𝐺 = (𝑉, 𝐸) его компоненты силь-
ной связности.
Поиск в глубину как таковой компоненты сильной связности не выдаёт. Например, для
первой выбранной вершины он просто выдаст все вершины, из неё достижимые — то есть,
все компоненты связности, лежащие в графе компонентов связности ниже исходного ком-
понента.
Задача решается алгоритмом Косараджу–Шарира; этот алгоритм был опубликован Ша-
риром [1981], у Косараджу же был неопубликованный черновик.
Алгоритм работает так. Сперва запускается поиск в глубину для 𝐺. Затем запускается
поиск в глубину для обращённого графа 𝐺𝑅 , в котором направления всех дуг изменены на

9
Рис. 5: Рао Косараджу (род. 1943), Миха Шарир (род. 1950).

обратные3 . При поиске в глубину для 𝐺𝑅 , во внешнем цикле вершины рассматриваются в


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

Лемма 4. Пусть в графе 𝐺 есть сильно связные компоненты 𝐶 и 𝐷, и есть дуга (𝑢, 𝑣) ∈ 𝐸
из 𝐶 в 𝐷, как показано на рис. 6(левом). Тогда при поиске в глубину в графе 𝐺 самое позднее
время завершения вершины в 𝐶 превосходит таковое в 𝐷.

max конец(𝑥) > max конец(𝑦)


𝑥∈𝐶 𝑦∈𝐷

Доказательство. Рассматриваются два случая, в зависимости от того, который из компо-


нентов, 𝐶 или 𝐷, был обнаружен при поиске первым.
• Пусть самое раннее время обнаружения в 𝐶 меньше, чем в 𝐷. Пусть первой была
обнаружена вершина 𝑥 ∈ 𝐶. Тогда в этот момент есть путь из 𝑥 в любую вершину из
𝐶∪𝐷, состоящий из непомеченных вершин («белый путь»). Поэтому при рассмотрении
𝑥 все эти вершины обрабатываются в рекурсивных вызовах, получая меньшее время
окончания, чем у 𝑥.

• Если же самое раннее время обнаружения в 𝐶 больше, чем в 𝐷, пусть 𝑦 ∈ 𝐷 —


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

Стало быть, все вершины из 𝐷 будут закончены раньше, чем вершина из 𝐶, законченная
последней. Поэтому когда поиск в глубину будет запущен в графе 𝐺𝑅 , и первой будет
рассмотрена позже всех законченная вершина из 𝐶, из неё не удастся придти в 𝐷, поскольку
в графе 𝐺𝑅 есть только дуга (𝑣, 𝑢), а будь в нём какие-то дуги из 𝐶 в 𝐷, это был бы один
компонент связности. Позднее, когда будут рассмотрены вершины из 𝐷, переходить из них
в 𝐶 будет уже поздно, поскольку все вершины из 𝐶 к тому времени будут уже рассмотрены.
3
Стоит упомянуть, что у графов 𝐺 и 𝐺𝑅 одни и те же компоненты сильной связности.

10
C D C D
G: GR:
u v u v

все закончатся будут запущены


прежде раньше
последней из C вершин из D

Рис. 6: Сильно связные компоненты, соединённые дугой: (слева) в графе 𝐺; (справа) в


графе 𝐺𝑅 .

Теорема 1. Вершины каждого дерева, найденного алгоритмом Косадаржу–Шарира при


втором поиске в глубину — это и есть сильно связные компоненты исходного графа.

Доказательство. Индукция по количеству найденных компонентов связности. Надо дока-


зать, что если первые 𝑘 выданных компонентов связности действительно будут таковыми,
то и следующий тоже будет компонентом связности. На этом этапе поиск в глубину в графе
𝐺𝑅 находит вершину 𝑦, позже всех закончившуюся среди ещё не рассмотренных вершин.
Эта вершина, в частности, закончилась позже всех в своём сильно связном компоненте 𝐷.
Утверждается, что при поиске в 𝐺𝑅 из 𝑦 не удастся пройти ни в какой другой ССК. Дей-
ствительно, если можно пройти в 𝐶 по дуге (𝑣, 𝑢), то в графе 𝐺 была дуга (𝑢, 𝑣), и потому,
согласно лемме 4, самое позднее время завершения в 𝐶 — позднее чем в 𝑢. Поэтому все
вершины из 𝐶 к этому моменту уже рассмотрены, туда больше не перейти.

Время работы алгоритма — 𝑂(|𝑉 | + |𝐸|).

4 Поиск в графе с весами: алгоритм Беллмана–Форда


Пусть в ориентированном графе 𝐺 = (𝑉, 𝐸). для каждой дуги (𝑢, 𝑣) ∈ 𝐸 задан её вес —
число 𝑤𝑢,𝑣 ∈ R. Если дуги нет, можно положить 𝑤𝑢,𝑣 = ∞.
Нужно найти пути наименьшего веса из данной вершины 𝑠 ∈ 𝑉 во все вершины графа.
Стоит заметить, что в графе допускаются дуги отрицательного веса, и такие дуги могут
сократить уже проделанный путь. Если же в графе есть цикл отрицательного веса, то
тогда, проходя его много раз, можно сделать вес пути сколь угодно малым — и потому
путь наименьшего веса окажется неопределённым.
Поэтому алгоритм ищет пути наименьшего веса из 𝑠 во все вершины, коль скоро из
𝑠 нельзя придти ни в какой цикл отрицательного веса. Если же это условие неверно, то
алгоритм должен об этом сообщить.
Алгоритм Беллмана–Форда решает эту задачу, вычисляя для каждой вершины 𝑣 ∈ 𝑉
следующие значения:

• 𝑑𝑣 : наименьший вес пути из 𝑠 в 𝑣;

• 𝜋𝑣 : предыдущая вершина на пути наименьшего веса из 𝑠 в 𝑣.

Значения 𝜋𝑣 образуют остовное дерево.


Изначально полагается 𝑑𝑣 = ∞, 𝜋𝑣 = NULL для всех вершин, и 𝑑𝑠 = 0. Это первое
приближение: найдена только начальная вершина, пути в другие вершины неизвестны.

11
Рис. 7: Ричард Беллман (1920–1984), Лестер Форд мл. (1927–2017).

Алгоритм постепенно находит пути меньшего веса в другие вершины, запоминая веса
лучших из найденных путей в этих переменных. Значения уменьшаются с помощью эле-
ментарной операции улучшения пути, используя некоторую дугу (𝑢, 𝑣) ∈ 𝐸.

1: if 𝑑𝑢 + 𝑤𝑢,𝑣 < 𝑑𝑣 then


2: 𝑑𝑣 = 𝑑𝑢 + 𝑤𝑢,𝑣
3: 𝜋𝑣 = 𝑢
Алгоритм 6 применяет эту операцию, пока можно что-то улучшить. Как будет показано
ниже, для этого достаточно просмотреть все дуги |𝑉 | − 1 раз.

Алгоритм 6 Алгоритм Беллмана–Форда

1: 𝑑𝑠 = 0
2: 𝑑𝑣 = ∞ для всех 𝑣 ̸= 𝑠
3: for 𝑖 = 1 to |𝑉 | − 1 do
4: for all (𝑢, 𝑣) ∈ 𝐸 do
5: if 𝑑𝑢 + 𝑤𝑢,𝑣 < 𝑑𝑣 then /* если путь из 𝑠 в 𝑣 можно улучшить этой дугой */
6: 𝑑𝑣 = 𝑑𝑢 + 𝑤𝑢,𝑣
7: 𝜋𝑣 = 𝑢
8: for all (𝑢, 𝑣) ∈ 𝐸 do
9: if 𝑑𝑢 + 𝑤𝑢,𝑣 < 𝑑𝑣 then
10: return есть достижимый цикл отрицательного веса
11: return достижимых циклов отрицательного веса нет

Лемма 5. После 𝑖-й итерации внешнего цикла алгоритм Беллмана–Форда находит все
пути наименьшего веса длины не более чем 𝑖.

Доказательство. Индукция по 𝑖.

Базис: 𝑖 = 0. Вес пути длины 0 равен 0.

Индукционный переход. Пусть верно для 𝑖-го прохода. Всякий путь наименьшего веса
длины 𝑖 + 1 в вершину 𝑣 на предыдущем шаге проходит через некоторую вершину 𝑢,
и потому содержит путь наименьшего веса длины 𝑖 в 𝑢. Тогда на (𝑖 + 1)-м проходе
искомый путь в вершину 𝑣 будет найден при рассмотрении вершины 𝑢.

12
Теорема 2. Алгоритм Беллмана–Форда за |𝑉 | · |𝐸| шагов или правильно вычисляет пути
наименьшего веса из вершины 𝑠 во все вершины, или сообщает о наличии достижимого
цикла отрицательного веса.

Доказательство. После |𝑉 |−1 итераций, согласно лемме 5, найдены все пути наименьшего
веса, состоящие не более чем из |𝑉 | − 1 дуг. Время работы — поскольку на каждой итерации
рассматриваются все дуги.
Если после |𝑉 | − 1 итераций первого цикла оказывается, что какие-то пути можно ещё
улучшить, это значит, что есть путь длины |𝑉 | в какую-то вершину 𝑣, вес которого меньше,
чем у всех путей длины не более чем |𝑉 | − 1, идущих в 𝑣. Какая-то вершина 𝑥 на этом
пути повторяется дважды. При удалении фрагмента пути между двумя вхождениями 𝑥
+получается путь длины не более чем |𝑉 | − 1, который, по предположению, имеет больший
вес, чем исходный путь длины |𝑉 |. Стало быть, вес удалённого цикла отрицательный, о чём
и сообщит алгоритм.
Пусть после |𝑉 | − 1 итераций никакие пути улучшить уже нельзя. Утверждается, что
всякий цикл 𝑣0 , 𝑣1 , . . . , 𝑣𝑘−1 , 𝑣0 , достижимый из 𝑠, будет иметь неотрицательный вес. Дей-
ствительно, для каждой дуги (𝑣𝑖 , 𝑣𝑖+1 ) этого цикла условие улучшения пути не выполняется.

𝑑𝑣𝑖 + 𝑤𝑣𝑖 ,𝑣𝑖+1 ⩾ 𝑑𝑣𝑖+1

(арифметика с номерами вершин — по модулю 𝑘) Эти неравенства складываются по всем


𝑖.
𝑘−1
∑︁ 𝑘−1
∑︁ 𝑘−1
∑︁
𝑑𝑣𝑖 + 𝑤𝑣𝑖 ,𝑣𝑖+1 ⩾ 𝑑𝑣𝑖
𝑖=0 𝑖=0 𝑖=0
∑︀𝑘−1
Отсюда следует, что вес цикла 𝑖=0 𝑤𝑣𝑖 ,𝑣𝑖+1 неотрицателен.

Список литературы
[1975] D. S. Hirschberg, “A linear space algorithm for computing maximal common
subsequences”, Communications of the ACM, 18:6 (1975), 341–343.

[1981] M. Sharir, “A strong connectivity algorithm and its applications to data flow analysis”,
Computers and Mathematics with Applications, 7:1 (1981), 67–72.

13

Вам также может понравиться