2. При работе с графами часто приходится
выполнять некоторое действие по одному разу
с каждой из вершин графа. Например,
некоторую порцию информации следует
передать каждому из компьютеров в сети. При
этом мы не хотим посещать какой-либо
компьютер дважды. Аналогичная ситуация
возникает, если мы хотим собрать
информацию, а не распространить ее.
Подобный обход можно совершать двумя
различными способами. При обходе в глубину
проход по выбранному пути осуществляется
настолько глубоко, насколько это возможно, а
при обходе в ширину (по уровням) мы
равномерно двигаемся вдоль всех возможных
направлений.
3. Поиск в ширину
Пусть задан граф G = (V, Е) и выделена исходная
вершина s. Алгоритм поиска в ширину
систематически обходит все ребра G для
«открытия» всех вершин, достижимых из s,
вычисляя при этом расстояние (минимальное
количество ребер) от s до каждой достижимой из
s вершины. Кроме того, в процессе обхода
строится «дерево поиска в ширину» с корнем s,
содержащее все достижимые вершины. Для
каждой достижимой из s вершины v путь в
дереве поиска в ширину соответствует
кратчайшему (т.е. содержащему наименьшее
количество s ребер) пути от s до v в G. Алгоритм
работает как для ориентированных, так и для
неориентированных графов.
4. Поиск в ширину имеет такое название потому, что в
процессе обхода мы идем вширь, т.е. перед тем как
приступить к поиску вершин на расстоянии k +1,
выполняется обход всех вершин на расстоянии k.
Для отслеживания работы алгоритма поиск в ширину
раскрашивает вершины графа в белый, серый и
черный цвета.
Изначально все вершины белые, и позже они могут стать
серыми, а затем черными. Когда вершина открывается
в процессе поиска, она окрашивается. Таким образом,
серые и черные вершины – это вершины, которые уже
были открыты, но алгоритм поиска в ширину поразному работает с ними, чтобы обеспечить
объявленный порядок обхода. Если (u, v) ∈ Е и
вершина u черного цвета, то вершина v либо серая,
либо черная, т.е. все вершины, смежные с черной, уже
открыты. Серые вершины могут иметь белых соседей,
представляя собой границу между открытыми и
неоткрытыми вершинами.
5. Поиск в ширину строит дерево поиска в ширину,
которое изначально состоит из одного корня,
которым является исходная вершина s. Если в
процессе сканирования списка смежности уже
открытой вершины u открывается белая
вершина v, то вершина v и ребро (u, v)
добавляются в дерево. Мы говорим, что u
является предшественником, или родителем, v в
дереве поиска вширь. Поскольку вершина может
быть открыта не более одного раза, она имеет
не более одного родителя. Взаимоотношения
предков и потомков определяются в дереве
поиска в ширину как обычно – если и находится
на пути от корня s к вершине u, то u является
предком v, а u –потомком u.
6. Процедура поиска в ширину BFS
предполагает, что входной граф G = (V, Е)
представлен при помощи списков
смежности. Кроме того, поддерживаются
дополнительные структуры данных в
каждой вершине графа. Цвет каждой
вершины u ∈ V хранится в переменной
color[u], а предшественник – в переменной
π[u]. Если предшественника у u нет
(например, если u = s или u не открыта),
то π[u] = NIL. Расстояние от s до вершины
u, вычисляемое алгоритмом, хранится в
поле d[u]. Алгоритм использует очередь Q
для работы с множеством серых вершин:
7. BFS(G, s)
1. for (для) каждой вершины u ∈ V[G] – s
2. do color[u] ← WHITE (значение белого цвета)
3.
d[u] ← ∞
4.
π[u] ← NIL
5. color[s] ← GRAY (значение серого цвета)
6. d[s] ← 0
7. π[s] ← NIL
8. Q ← ∅
9. Enqueue(Q, s)
10. while Q ≠ ∅
11.
do u ← Dequeue(Q)
12.
for (для) каждой v ∈ Adj[u]
13.
do if color[v] = WHITE
14.
then color[v] ← GRAY
15.
d[v] ← d[v] + 1
16.
π[v] ← u
17.
Enqueue(Q, v)
18.
color[v] ← BLACK (значение черного цвета)
8. Процедура BFS работает следующим образом. В
строках 1 – 4 все вершины, за исключением
исходной вершины s, окрашиваются в белый
цвет, для каждой вершины u полю d[u]
присваивается значение ∞, а в качестве
родителя для каждой вершины устанавливается
NIL (пустое значение). В строке 5 исходная
вершина s окрашивается в серый цвет,
поскольку она рассматривается как открытая в
начале процедуры. В строке 6 ее полю d[s]
присваивается значение 0, а в строке 7 ее
родителем становится NIL. В строках 8 – 9
создается пустая очередь Q, в которую
помещается один элемент s.
Цикл while в строках 10 – 18 выполняется до тех
пор, пока остаются серые вершины (т.е.
открытые, но списки смежности которых еще не
просмотрены).
9. Инвариант данного цикла выглядит следующим образом:
При выполнении проверки в строке 10 очередь Q состоит
множества серых вершин.
Перед первой итерацией единственной серой вершиной и
единственной вершиной в очереди Q, является
исходная вершина s. В строке 11 определяется серая
вершина u в голове очереди Q, которая затем
удаляется из очереди. Цикл for в строках 12 – 17
просматривает все вершины v в списке смежности u.
Если вершина v белая, значит, она еще не открыта, и
алгоритм открывает ее, выполняя строки 14 – 17.
Вершине назначается серый цвет, дистанция d[v]
устанавливается равной d[u] + 1, а в качестве ее
родителя указывается вершина u. После этого
вершина помещается в хвост очереди Q. После того
как все вершины из списка смежности u просмотрены,
вершине u присваивается черный цвет. Инвариант
цикла сохраняется, так как все вершины, которые
окрашиваются в серый цвет (строка 14), вносятся в
очередь (строка 17), а вершина, которая удаляется из
очереди (строка 11), окрашивается в черный цвет
(строка 18).
10. Выполним анализ времени работы алгоритма для
входного графа
G = (V,E).
Сумма длин всех списков смежности равна O(|Е|),
общее время, необходимое для сканирования
списков, равно O(|Е|). Накладные расходы на
инициализацию равны O(|V|), так что общее
время работы процедуры BFS составляет O(|V| +
|Е|). Таким образом, время поиска в ширину
линейно зависит от размера представления
графа G с использованием списков смежности.
Очередь – это динамическое множество, элементы
из которого удаляются согласно стратегии
«первым вошел – первым вышел» (first-in, firstout – FIFO).
11. Очередь имеет голову (head) и хвост (tail).
Очередь Q пуста, если выполняется
условие head[Q] = tail[Q].
Изначально выполняется соотношение
head[Q] = tail[Q] = 1.
Если head[Q] = tail[Q] + l, то очередь
заполнена, и попытка добавить в нее
элемент приводит к ее переполнению.
Рассмотрим процедуры добавления
элемента в очередь Enqueue и удаления
элемента из очереди Dequeue (в них
проверка ошибок опустошения и
переполнения не производится).
12. Enqueue(Q, x)
1. Q[tail[Q]] ← x
2. if tail[Q] = length[Q]
3. then tail[Q] ← 1
4. else tail[Q] ← tail[Q] + 1
Dequeue(Q)
1. x ← Q[head[Q]]
2. if head[Q] = length[Q]
3. then head[Q] ← 1
4. else head[Q] ← head[Q] + 1
5. return x
13. Поиск в глубину
Стратегия поиска в глубину, как следует из ее
названия, состоит в том, чтобы идти «вглубь»
графа, насколько это возможно.
Когда вершина v открывается в процессе
сканирования списка смежности уже открытой
вершины u, процедура поиска записывает это
событие, устанавливая поле предшественника
v π[v] равным u. В отличие от поиска в ширину,
где подграф предшествования образует
дерево, при поиске в глубину подграф
предшествования может состоять из
нескольких деревьев, так как поиск может
выполняться из нескольких исходных вершин.
14. Подграф предшествования поиска в глубину
определяем как граф Gπ = (V, Еπ), где Еπ = {( π[v],
v) : v ∈ V и π[v] ≠ NIL} . Подграф
предшествования поиска в глубину образует лес
поиска в глубину, который состоит из нескольких
деревьев поиска в глубину. Ребра в Еπ
называются ребрами дерева.
Каждая вершина изначально белая, затем при
открытии в процессе поиска она окрашивается в
серый цвет, и по завершении, когда ее список
смежности полностью сканирован, она
становится черной. Такая методика гарантирует,
что каждая вершина в конечном счете находится
только в одном дереве поиска в глубину, так что
деревья не пересекаются.
15. Помимо построения леса поиск в глубину также
проставляет в вершинах метки времени. Каждая
вершина имеет две такие метки – первую d[v], в
которой указывается, когда вершина v открывается (и
окрашивается в серый цвет), и вторая – f[v], которая
фиксирует момент, когда поиск завершает
сканирование списка смежности вершины v и она
становится черной.
Процедура DFS записывает в поле d[u] момент, когда
вершина u открывается, а в поле f[u] – момент
завершения работы с вершиной u. Эти метки времени
представляют собой целые числа в диапазоне от 1 до
2|V|, поскольку для каждой из |V| вершин имеется
только одно событие открытия и одно – завершения.
Для каждой вершины u d[u] < f[u].
До момента времени d[u] вершина имеет цвет WHITE,
между d[u] и f[u] – цвет GRAY, а после f[u] – цвет
BLACK.
16. Псевдокод алгоритма поиска в глубину. Входной
граф G может быть как ориентированным, так и
неориентированным. Переменная time –
глобальная и используется нами для меток
времени.
DFS(G)
1. for (для) каждой вершины u ∈ V[G)
2.
do color[u] ← WHITE
3.
π[u] ← NIL
4. time ← 0
5. for (для) каждой вершины u ∈ V[G)
6.
do if color[u] = WHITE
7.
then DFS_Visit(u)
17. DFS_Visit(u)
1. соlоr[u] ← GRAY //Открыта белая вершина u
2. time ← time + 1
3. d[u] ← time
4. for (для) каждой вершины v ∈ Adj[u]
//Исследование ребра (u, v)
5.
do if color[v] = WHITE
6.
then π[v] ← u
7.
DFS_Visit(v)
8. color[u] ← BLACK //Завершение
9. f[u] ← time ← time + 1
Процедура DFS работает следующим образом. В
строках 1 – 3 все вершины окрашиваются в
белый цвет, а их поля π инициализируются
значением NIL. В строке 4 выполняется сброс
глобального счетчика времени.
18. В строках 5 – 7 поочередно проверяются все вершины из V,
и когда обнаруживается белая вершина, она
обрабатывается при помощи процедуры DFS_Visit.
Каждый раз при вызове процедуры DFS_Visit(u) в строке
7, вершина u становится корнем нового дерева леса
поиска в глубину. При возврате из процедуры DFS
каждой вершине u сопоставляются два момента времени
– время открытия d[u] и время завершения f[u].
При каждом вызове DFS_Visit(u) вершина u изначально
имеет белый цвет. В строке 1 она окрашивается в серый
цвет, в строке 2 увеличивается глобальная переменная
time, а в строке 3 выполняется запись нового значения
переменной time в поле времени открытия d[u]. В строках
4 – 7 исследуются все вершины, смежные с u, и
выполняется рекурсивное посещение белых вершин.
При рассмотрении в строке 4 вершины v ∈ Adj[u], мы
говорим, что ребро (u, v) исследуется поиском в глубину.
И, наконец, после того как будут исследованы все ребра,
покидающие u, в строках 8 – 9 вершина u окрашивается
в черный цвет, а в поле f[u] записывается время
завершения работы с ней.
19. Определим время работы процедуры DFS.
Циклы в строках 1 – 3 и
5–7
процедуры DFS выполняются за время Θ(|
V|), исключая время, необходимое для
вызова процедуры DFS_Visit. Процедура
DFS_Visit вызывается ровно по одному
разу для каждой вершины v ∈ V, так как
она вызывается только для белых вершин,
и первое, что она делает, – это
окрашивает переданную в качестве
параметра вершину в серый цвет. В
процессе выполнения DFS_Visit(v) цикл в
строках 4 – 7 выполняется |Adj[v]| раз.
Поскольку
∑| Adj[v] | = Θ(| E |)
v∈V
20. общая стоимость выполнения строк 4 – 7
процедуры DFS_Visit равна Θ(|Е|). Время работы
процедуры DFS, таким образом, равно Θ(|V| + |
Е|).
Процедура DFS_Visit является рекурсивной.
Стек – это динамическое множество, элементы
которого обрабатываются согласно стратегии
«последним вошел – первым вышел» (last-in,
first-out – LIFO). Чаще всего стек реализуется с
помощью массива. Над стеком определены две
основные операции – вставки и удаления
элемента. Операция вставки применительно к
стекам часто называется Push (запись в стек), а
операция удаления – Pop (снятие со стека).
21. Стек, способный вместить не более n элементов,
можно реализовать с помощью массива S[1..n].
Этот массив обладает атрибутом top[S],
представляющим собой индекс последнего
помещенного в стек элемента. Стек состоит из
элементов S [1.. top[S]], где S[1] – элемент на
дне стека, а
S[top[S]] – элемент на его
вершине.
Если top[S] = 0, то стек не содержит ни одного
элемента и является пустым. Протестировать
стек на наличие в нем элементов можно с
помощью операции-запроса Stack_Empty.
Stack_Empty(E)
1. if top[S] = 0
2. then return TRUE