2. В задаче о кратчайшем пути задается взвешенный
ориентированный граф G = (V, Е) с весовой
функцией w: Е → R, отображающей ребра на их
веса, значения которых выражаются
действительными числами. Вес пути
p = (v0,
v1,..., vN) равен суммарному весу входящих в него
N
ребер:
w( p ) = ∑w(vi −1 , vi )
i =1
Вес кратчайшего пути из вершины u в вершину v
определяется соотношением
{
}
min w( p ) : u p v если имеется путь от u к v,
→
δ (u , v) =
∞
в противном случае.
3. Тогда по определению кратчайший путь из вершины u в
вершину v – это любой путь, вес которого
удовлетворяет соотношению w(р) = δ(u, v).
Рассмотрим алгоритмы решения задачи о кратчайшем
пути из одной вершины, в которой для заданного
графа G = (V, Е) требуется найти кратчайший путь,
который начинается в определенной исходной
вершине s ∈ V (для краткости будем именовать ее
истоком) и заканчивается в каждой из вершин v ∈ V.
В описанных алгоритмах используется метод
релаксации, или ослабления. Для каждой вершины v ∈
V поддерживается атрибут d[v], представляющий
собой верхнюю границу веса, которым обладает
кратчайший путь из истока s в вершину v. Назовем
атрибут d[v] оценкой кратчайшего пути.
Инициализация оценок кратчайших путей и
предшественников производится в приведенной ниже
процедуре, время работы которой равно Θ(V):
4. Initialize_Single_Source(G, s)
1. for (для) каждой вершины v ∈ V[G]
2.
do d[v] ← ∞
3.
π[v] ← NIL
4. d[s] ← 0
После инициализации для всех v ∈ V π[v] = NIL,
d[s] = 0 и для всех v ∈ V – {s} d[v] = ∞.
Процесс ослабления ребра (u, v) заключается в
проверке, нельзя ли улучшить найденный до
сих пор кратчайший путь к вершине v, проведя
его через вершину u, а также в обновлении
атрибутов d[v] и π[v] при наличии такой
возможности улучшения. Ослабление может
уменьшить оценку кратчайшего пути d[v] и
обновить поле π[v] вершины v. Приведенный
ниже код выполняет ослабление ребра (u, v):
5. Relax(u, v, w)
1. if d[v] > d[u] + w(u, v)
2. then d[v] ← d[u) + w(u, v)
3.
π[v] ← u
В каждом из описанных алгоритмов сначала
вызывается процедура Initialize_Single_Source,
а затем производится ослабление ребер.
Алгоритм Дейкстры
Алгоритм
Дейкстры
решает
задачу
о
кратчайшем пути из одной вершины во
взвешенном ориентированном графе G = (V, Е)
в
том
случае,
когда
веса
ребер
неотрицательны.
Поэтому
будем
предполагать, что для всех ребер (u, v) ∈ Е
выполняется неравенство w(u, v) ≥ 0.
6. В алгоритме Дейкстры поддерживается множество вершин
S, для которых уже вычислены окончательные веса
кратчайших путей к ним из истока s. В этом алгоритме
поочередно выбирается вершина u ∈ V – S, которой на
данном этапе соответствует минимальная оценка
кратчайшего пути. После добавления этой вершины u в
множество S производится ослабление всех исходящих
из нее ребер. В приведенной ниже реализации
используется неубывающая очередь с приоритетами Q,
состоящая из вершин, в роли ключей для которых
выступают значения d.
Dijkstra(G, w, s)
1. Initialize_Single_Source(G, s)
2. S ← ∅
3. Q ← V[G]
4. while Q ≠ 0
5.
do u ← Extract_Min(Q)
6.
S ← S ∪ {u}
7.
for (для) каждой вершины v ∈ Adj[u]
8.
do Relax(u, v, w)
7. Работа рассматриваемого алгоритма. В строке 1 производится
обычная инициализация величин d и π, а в строке 2
инициализируется пустое множество вершин S. В этом
алгоритме поддерживается инвариант, согласно которому в
начале каждой итерации цикла while в строках 4 – 8 выполняется
равенство Q = V – S. В строке 3 неубывающая очередь с
приоритетами Q инициализируется таким образом, чтобы она
содержала все вершины множества V; поскольку в этот момент S
= 0, после выполнения строки 3 сформулированный выше
инвариант выполняется. При каждой итерации цикла while в
строках 4 – 8 вершина u извлекается из множества Q = V – S и
добавляется в множество S, в результате чего инвариант
продолжает соблюдаться. Во время первой итерации этого цикла
u = s. Т.о., вершина u имеет минимальную оценку кратчайшего
пути среди всех вершин множества V – S. Затем в строках 7 – 8
ослабляются все ребра (u, v), исходящие из вершины u. Если
текущий кратчайший путь к вершине v может быть улучшен в
результате прохождения через вершину u, выполняется
ослабление и соответствующее обновление оценки величины
d[v] и предшественника π[v]. После выполнения строки 3
вершины никогда не добавляются в множество Q и каждая
вершина извлекается из этого множества и добавляется в
множество S ровно по одному разу, поэтому количество
итераций цикла while в строках 4 – 8 равно |V|.
8. Теорема 1 (Корректность алгоритма Дейкстры).
По завершении обработки алгоритмом
Дейкстры взвешенного ориентированного
графа
G = (V, Е) с неотрицательной
весовой функцией w и истоком s для всех
вершин u ∈ V выполняется равенство d[u] =
δ(s, u).
Следствие 1. Если выполнить алгоритм
Дейкстры для взвешенного ориентированного
графа G = (V, E) с неотрицательной весовой
функцией w и истоком s, то по завершении
работы алгоритма подграф предшествования
Gπ является деревом кратчайших путей с
корнем в вершине s.
9. Очередь с приоритетами – это структура данных,
предназначенная для обслуживания множества S, с
каждым элементом которого связано определенное
значение, называющееся ключом (key). В
неубывающей очереди с приоритетами
поддерживаются следующие операции.
• Операция Insert(S, x) вставляет элемент x в множество
S. Эту операцию можно записать как S ← S ∪ {x}.
• Операция Minimum(S) возвращает элемент множества
S с наименьшим ключом.
• Операция Extract_Min(S) возвращает элемент с
наименьшим ключом, удаляя его при этом из
множества S.
• Операция Decrease_Key(S, x, k) уменьшает значение
ключа, соответствующего элементу x, путем его
замены ключом со значением k. Предполагается, что
величина k не больше текущего ключа элемента x.
10. Насколько быстро работает алгоритм Дейкстры? В нем
поддерживается неубывающая очередь с
приоритетами Q и тремя операциями, характерными
для очередей с приоритетами: Insert (явно
вызывается в строке 3), Extract_Min (строка 5) и
Decrease_Key (неявно присутствует в процедуре
Relax, которая вызывается в строке 8). Процедура
Insert, как и процедура Extract_Min, вызывается по
одному разу для каждой вершины. Поскольку каждая
вершина v ∈ V добавляется в множество S ровно по
одному разу, каждое ребро в списке смежных
вершин Adj[v] обрабатывается в цикле for, заданном
в строках 7 – 8, ровно по одному разу на протяжении
работы алгоритма. Так как полное количество ребер
во всех списках смежных вершин равно |Е| всего
выполняется |Е| итераций этого цикла for, а
следовательно, не более |Е| операций
Decrease_Key.
11. Время выполнения алгоритма Дейкстры зависит от
реализации неубывающей очереди с
приоритетами. Сначала рассмотрим случай,
когда неубывающая очередь с приоритетами
поддерживается за счет того, что все вершины
пронумерованы от 1 до |V|. Атрибут d[v] просто
помещается в элемент массива с индексом v.
Каждая операция Insert и Decrease_Key
занимает время O(1), а каждая операция
Extract_Min – время O(|V|) (поскольку в ней
производится поиск по всему массиву); в
результате полное время работы алгоритма
равно O(|V|2 + |Е|) = O(|V|2).
12. Алгоритм Флойда-Уоршалла
В
алгоритме
Флойда-Уоршалла
рассматриваются
"промежуточные"
вершины
кратчайшего
пути.
Промежуточной вершиной простого пути
p = (v1,v2,
…, vN) называется произвольная вершина, отличная от v1
и vN, т.е. это любая вершина из множества {v2, v3,..., vN – 1}.
Алгоритм Флойда-Уоршалла основан на следующем
наблюдении. Предположим, что граф G состоит из
вершин V = {1, 2, ..., n}. Рассмотрим подмножество
вершин {1, 2, ..., k} для некоторого k. Для произвольной
пары вершин i, j ∈ V рассмотрим все пути из вершины i в
вершину j, все промежуточные вершины которых
выбраны из множества {1, 2, ..., k}. Пусть среди этих
путей p – путь с минимальным весом (этот путь простой).
В
алгоритме
Флойда-Уоршалла
используется
взаимосвязь между путем р и кратчайшими путями из
вершины i в вершину j, все промежуточные вершины
которых принадлежат множеству {1, 2, ..., k – 1}. Эта
взаимосвязь зависит от того, является ли вершина k
промежуточной на пути р.
13. • Если k – не промежуточная вершина пути р, то все
промежуточные вершины этого пути принадлежат
множеству {l, 2, ..., k – 1}. Таким образом, кратчайший
путь из вершины i в вершину j со всеми
промежуточными вершинами из множества {l, 2, ..., k –
1} одновременно является кратчайшим путем из
вершины i в вершину j со всеми промежуточными
вершинами из множества {1, 2, ..., k}.
• Если k – промежуточная вершина пути р, то этот путь
можно разбить следующим образом: . Путь p1 –
кратчайший путь из вершины i в вершину k, все
промежуточные вершины которого принадлежат
множеству {1, 2, ..., k}. Поскольку k не является
промежуточной вершиной пути p1 понятно, что p1 –
кратчайший путь из вершины i в вершину k, все
промежуточные вершины которого принадлежит
множеству {1, 2, ..., k – 1}. Аналогично, p2 –
кратчайший путь из вершины k в вершину j, все
промежуточные вершины которого принадлежат
множеству {1, 2, ..., k – 1}.
14. Определим на основе сделанных выше наблюдений
рекурсивную формулировку оценок кратчайших
путей. Пусть – вес кратчайшего пути из вершины i в
вершину j, для которого все промежуточные
вершины принадлежат множеству {1, 2, ..., k}. Если
k = 0, то путь из вершины i в вершину j, в котором
отсутствуют промежуточные вершины с номером,
большим нуля, не содержит промежуточных
вершин вообще. Такой путь содержит не более
одного ребра, поэтому = wij. Рекурсивное
определение, которое соответствует приведенному
выше описанию, дается соотношением
(
d ijk )
если k = 0,
wij
=
(
(
k
min(d ijk −1) , d ikk −1) + d kj−1 ) если k ≥ 1.
15. Поскольку все промежуточные вершины
произвольного пути принадлежат множеству {1,
(
(
2, ..., n}, матрица Dijk ) = d ijk )
дает
(
конечный ответ:
d ijn ) =δ(i, j )
для всех пар вершин i, j ∈ V.
Исходя из рекуррентного соотношения, можно
составить приведенную ниже процедуру,
предназначенную для вычисления величин
(
)
(
d ijk )
в порядке возрастания k. В качестве
входных данных выступает матрица W=(wij)
0
размерами n × n, где
если i = j ,
wij = вес ориентированного ребра (i, j ) если i ≠ j и (i, j ) ∈ E ,
∞
если i ≠ j и (i, j ) ∉ E.
16. Процедура возвращает матрицу D(n),
содержащую веса кратчайших путей.
Floyd_Warshall(W)
1. n ← rows[W]
2. D(0) ← W
3. for k ← 1 to n
4. do for i ← 1 to n
5.
do for j ← 1 to n
(
(
(
k
d ijk ) ← min(d ijk −1) , d ikk −1) + d kj−1 )
6. do
7. 7. return D(n)
17. Время работы алгоритма Флойда-Уоршалла определяется
трижды вложенными друг в друга циклами for,
определенными в строках 3 – 6. Поскольку для каждого
выполнения строки 6 требуется время O(1), алгоритм
завершает работу в течение времени Θ(n3). Код этого
алгоритма Он не содержит сложных структур данных,
поэтому константа, скрытая в Θ-обозначениях, мала.
Таким образом, алгоритм Флойда-Уоршалла имеет
практическую ценность даже для входных графов
среднего размера.
Существует множество различных методов, позволяющих
строить кратчайшие пути в алгоритме Флойда-Уоршалла.
Один из них – вычисление матрицы D, содержащей веса
кратчайших путей, с последующим конструированием на
ее основе матрицы предшествования П = (πij). Этот
метод можно реализовать таким образом, чтобы время
его выполнения было равно O(n3). Если задана матрица
предшествования П, то вывести вершины на указанном
кратчайшем пути можно с помощью процедуры
Print_All_Pairs_Shortest_Path.
18. Print_All_Pairs_Shortest_Path(II, i, j)
1. if i = j
2. then print i
3. else if πij = NIL
4. then print "He существует пути из" i "в" j
5. else Print_All_Pairs_Shortest_Path(II, i, πij)
6. print j
Матрицу предшествования П можно так же
вычислить "на лету", как в алгоритме ФлойдаУоршалла вычисляются матрицы D(k). Точнее
говоря, вычисляется последовательность
матриц П(0), П(1), ..., П(n), где П = П(n), а элемент
определяется как предшественник вершины j
на кратчайшем пути из вершины i, все
промежуточные вершины которого
принадлежат множеству {1, 2, ..., k}.
19. Можно дать рекурсивное определение величины
(k
π)
. Если k = 0, то кратчайший
ij
путь из вершины i в вершину j не содержит
промежуточных вершин. Таким образом,
(
π ij0)
NIL если i = j или wij = ∞,
=
если i ≠ j и wij < ∞.
i
Если при k ≥ 1 получаем путь i → k → j, где k ≠ j, то
выбранный нами предшественник вершины j
совпадает с выбранным предшественником этой
же вершины на кратчайшем пути из вершины k,
все промежуточные вершины которого
принадлежат множеству {1, 2, ..., k – 1}.
20. В противном случае выбирается тот же
предшественник вершины j, который выбран
на кратчайшем пути из вершины i, у которого
все промежуточные вершины принадлежат
множеству {1, 2,..., k – 1}. Выражаясь
формально, при k ≥ 1
(
πijk )
(
1
(k 1
(k 1
ijk − ) если d ijk − ) ≤d ik − ) +d kj − ) ,
π( 1
=
1
(
1
(
1
(
1
π( k − ) если d ijk − ) >d ikk − ) +d kjk − ) .
kj
21. 3. Алгоритм транзитивного
замыкания
Может возникнуть необходимость установить,
существуют ли в заданном ориентированном
графе G = (V, Е), множество вершин которого
V = {1, 2,..., n}, пути из вершины i в вершину j для
всех возможных пар вершин i, j ∈ V.
Транзитивное замыкание графа G определяется
как граф
G* = (V, E*), где E* = {(i, j) : в графе G
имеется путь из вершины i в вершину j}.
Один из способов найти транзитивное замыкание
графа в течение времени Θ(n3) – присвоить
каждому ребру из множества Е вес 1 и
выполнить алгоритм Флойда-Уоршалла. Если
путь из вершины i в вершину j существует, то мы
получим dij < n; в противном случае dij = ∞.
22. Другой метод включает в себя подстановку
логических операций ∨ (логическое ИЛИ) и ∧
(логическое И) вместо использующихся в
алгоритме Флойда-Уоршалла арифметических
операций min и +. Определим значение при i, j, k
= 1, 2, ..., n равным 1, если в графе G существует
путь из вершины i в вершину j, все
промежуточные вершины которого принадлежат
множеству {1, 2, ..., k}; в противном случае эта
величина равна 0. Конструируя транзитивное
замыкание G* = (V, E*), будем помещать ребро (i,
j) в множество Е* тогда и только тогда, когда
(k
(k
tij ) = 1. Рекурсивное определение величины t ij )
имеет вид
23. (
tij0)
0 если i ≠ j и (i, j ) ∉E ,
=
1
если i = j или (i, j ) ∈E ,
а при k ≥ 1 выполняется соотношение
(
(
(
(k
tijk ) = tijk −1) ∨ (tikk −1) ∧ t kj −1) ).
Как и в алгоритме Флойда-Уоршалла, матрицы вычисляются в
порядке возрастания k:
Transitive_Closure(G)
1. n ← |V[G]|
2. for i ← 1 to n
3. do for j ← 1 to n
4.
do if i = j или (i, j) ∈ E[G]
5.
then ← 1
6.
else ← 0
7. for k ← 1 to n
8. do for i ← 1 to n
9.
do for j ← 1 to n
10.
do
11. return T(n)
24. Время работы процедуры Transitive_Closure, как и
время работы алгоритма Флойда-Уоршалла, равно
Θ(n3).
4. Минимальное остовное дерево
Для соединения множества из n контактов мы можем
использовать некоторую компоновку из n – 1 проводов,
каждый из которых соединяет два контакта.
Мы можем смоделировать эту задачу при помощи
связного неориентированного графа G = (V, Е), где V –
множество контактов, Е – множество возможных
соединений между парами контактов, и для каждого
ребра (u, v) ∈ Е задан вес w(u, v), определяющий
стоимость
(количество
необходимого
провода)
соединения u и v. Мы хотим найти ациклическое
подмножество Т ⊆ Е, которое соединяет все вершины
w(T ) = ∑w(u , v ) минимален.
и чей общий вес
( u , v )∈
T
25. Для решения задачи поиска минимального остовного
дерева можно воспользоваться алгоритмами Крускала
и Прима.
Алгоритм Крускала
MST_Kruskal(G, w)
1. A ← ∅
2. for (для) каждой вершины v ∈ V[G]
3. do Make_Set(v)
4. Сортируем ребра из Е в неубывающем порядке их
весов w
5. for (для) каждого (u, v) ∈ Е (в порядке возрастания
веса)
6. do if Find_Set(u) ≠ Find_Set(v)
7.
then A ← A ∪ {(u,v)}
8. Union(u, v)
9. return A
26. Алгоритм Крускала работает следующим образом. В
строках 1 – 3 выполняется инициализация
множества A пустым множеством и создается |V|
деревьев, каждое из которых содержит по одной
вершине. Ребра в E в строке 4 сортируются
согласно их весу в неубывающем порядке. Цикл for
в строках 5 – 8 проверяет для каждого ребра (u, v),
принадлежат ли его концы одному и тому же
дереву. Если это так, то данное ребро не может
быть добавлено к лесу без того, чтобы создать при
этом цикл, поэтому в таком случае ребро
отбрасывается. В противном случае, когда концы
ребра принадлежат разным деревьям, в строке 7
ребро (u, v) добавляется в множество A, и вершины
двух деревьев объединяются в строке 8.
Время работы оптимальной реализации алгоритма
можно оценить как O(|Е|lg|V|).
27. Алгоритм Прима
Алгоритм Прима очень похож на алгоритм Дейкстры для
поиска кратчайшего пути в графе. Ключевым моментом в
эффективной реализации алгоритма Прима является
выбор нового ребра для добавления в дерево. В
качестве входных данных алгоритму передаются
связный граф G и корень r минимального остовного
дерева. В процессе работы алгоритма все вершины,
которые не входят в дерево, располагаются в очереди с
приоритетами Q, основанной на значении поля key,
причем меньшее значение этого поля означает более
высокий приоритет в очереди. Для каждой вершины v
значение поля key[v] представляет собой минимальный
вес среди всех ребер, соединяющих v с вершиной в
дереве. Если ни одного такого ребра нет, считаем, что
key[v] = ∞. Поле π[v] указывает родителя v в дереве. В
процессе работы алгоритма множество A из процедуры
неявно поддерживается как A = {(v, π[v]): v ∈ V – {r} – Q}.
28. Когда алгоритм завершает работу, очередь с приоритетами
Q пуста и минимальным остовным деревом для G
является дерево
A = {(v, π[v]): v ∈ V – {r}}.
MST_Prim(G, w, r)
1. for (для) каждой вершины u ∈ V[G]
2. do key[u] ← ∞
3.
π[u] ← NIL
4. kеу[r] ← 0
5. Q ← V[G]
6. while Q ≠ ∅
7.
do u ← Extract_Min(Q)
8.
for (для) каждой вершины v ∈ Adj[u]
9.
do if v ∈ Q и w(u, v) < key[v]
10.
then π[v] ← u
11.
key[v] ← w(u,v)
29. Работа алгоритма Прима. В строках 1 – 5 ключи всех
вершин устанавливаются равными ∞ (за исключением
корня r, ключ которого равен 0, так что он оказывается
первой обрабатываемой вершиной), указателям на
родителей для всех узлов присваиваются значения NIL
и все вершины вносятся в очередь с приоритетами Q.
Алгоритм поддерживает следующий инвариант цикла,
состоящий из трех частей.
Перед каждой итерацией цикла while в строках 6 – 11
1. A = {(v, π[v]): v ∈ V – {r} – Q};
2. вершины, уже помещенные в минимальное остовное
дерево, принадлежат множеству V – Q;
3. для всех вершин v ∈ Q справедливо следующее: если
π[v] ≠ NIL, то key[v] < ∞ и key[v] – вес ребра с
минимальным весом, соединяющего v с некоторой
вершиной, уже находящейся в минимальном остовном
дереве.
30. Цикл for в строках 8 – 11 обновляет поля
key и π для каждой вершины v, смежной с
u и не находящейся в дереве. Это
обновление сохраняет третью часть
инварианта.
Производительность алгоритма Прима
зависит от выбранной реализации
очереди с приоритетами Q. При
оптимальной реализации общее время
работы алгоритма Прима составляет O(|Е|
+ |V|lg|V|).
31. 5. Топологическая сортировка
Топологическую сортировку графа можно
рассматривать как такое упорядочение его
вершин вдоль горизонтальной линии, что все
ребра направлены слева направо.
Простой алгоритм топологической сортировки
ориентированного ациклического графа имеет
следующий вид:
Topological_Sort(G)
1. Вызов процедуры поиска в глубину DFS(G) для
вычисления времени завершения f[v] для каждой
вершины v
2. По завершении работы над вершиной внести ее
в начало связанного списка
3. return Связанный список вершин
32. Выполнить топологическую сортировку можно за время
Θ(|V| + |Е|), поскольку поиск в глубину выполняется
именно за это время, а вставка каждой из |V| вершин
в начало связанного списка занимает время O(1).
Лемма 1. Ориентированный граф G является
ациклическим тогда и только тогда, когда поиск в
глубину в G не находит в нем обратных ребер.
Предположим, что над данным ориентированным
ациклическим графом G = (V, Е) выполняется
процедура DFS, которая вычисляет время
завершения его вершин. Достаточно показать, что
если для произвольной пары разных вершин u, v ∈ V
в графе G имеется ребро от u к v, то f[v] < f[u].
Теорема 1. Процедура Topological_Sort(G) выполняет
топологическую сортировку ориентированного
ациклического графа G.