
Lektsii_TRPO / Graph_Poisk_G_SH_lec
.doc2.3. Методы поиска в графе
При работе с графами часто приходится выполнять некоторое действие по одному разу с каждой из вершин графа. Например, некоторую порцию информации следует передать каждому из компьютеров в сети. При этом мы не хотим посещать какой-либо компьютер дважды. Аналогичная ситуация возникает, если мы хотим собрать информацию, а не распространить ее.
Существует много алгоритмов на графах, в основе которых лежит систематический перебор вершин графа, такой что каждая вершина просматривается (посещается) в точности один раз. Поэтому важной задачей является нахождение хороших методов поиска в графе. Под обходом графов (поиском на графах) мы будем понимать процесс систематического просмотра всех вершин графа с целью отыскания вершин, удовлетворяющих некоторому условию.
Два наиболее распространенных алгоритма обхода графов называются:
-
обход графа в глубину (поиск в глубину (англ. Depth First Search)) и
-
обход графа в ширину (поиск в ширину (англ. Breadth First Search)).
Поиск в глубину. Поиск в глубину стремится проникнуть подальше от исходной вершины. Идея этого метода следующая - на каждом шаге метода:
-
Из текущей вершины движемся в первую вершину, смежную с текущей, в которой мы еще не были, если таковая есть.
-
Если таковой нет, то возвращаемся в вершину, из которой мы попали в текущую.
-
Если же таковой нет и мы оказались в исходной вершине (возвращаться некуда), то это означает, что перебор вершин графа закончен.
При выполнении обхода графа по этим правилам мы стремимся проникнуть "вглубь" графа так далеко, как это возможно, затем отступаем на шаг назад и снова стремимся пройти вперед и так далее.
Вот как выглядит рекурсивный алгоритм обхода в глубину[MacConnel]:
DFS(G,v) { Depth-first search }
G граф
v текущая вершина
Visit(v) {действия в вершине }
Mark(v) {помечаем вершину как посещенную}
For каждого ребра vw графа G Do {поиск не просмотренных вершин}
If вершина w непомечена Then
DFS(G,w)
End If
End For
End DFS.
Этот
рекурсивный алгоритм использует
системный стек для отслеживания текущей
вершины графа, что позволяет правильно
осуществить возвращение, наткнувшись
на тупик.
Порядок обхода вершин при поиске в глубину показан на примере на рис.6.11. Поиск в глубину лежит в основе многих алгоритмов на графах, порой в несколько модифицированном виде.
Рассмотрим несколько вариантов реализации процедур поиска в глубину.
1. Граф задан матрицей смежности А. Алгоритм поиска рекурсивный. Для фиксации признака, просмотрена вершина графа или нет, вводится множество
Т :Set Of 1..n;
Procedure DFS_R ( v :Integer );
Var j :Integer;
Begin
Write(v:3); {вывод вершины }
T:=T + [v]; {помечаем вершину как посещенную}
For j := 1 To n Do {поиск не просмотренных вершин}
If ( A[ v, j ] <> 0 ) And Not ( j In T)
Then DFS_R( j );
End;
{ Головная программа }
Begin …. Ввод матрицы А; T := [ ]; DFS_R (1); …. End.
2. Граф задан матрицей смежности А. Алгоритм поиска нерекурсивный. Для хранения пройденных вершин используется структура данных «Стек».
Program DFS_Nrec; { Поиск в глубину - нерекурсивный вариант }
Uses Stack;
Const n=6;
Var A :Array[1..n,1..n] Of Integer; { Граф-матрица смежности }
T :Set Of 1..n; { множество посещенных вершин }
Procedure DFS_NR ( s: Integer ); s – начальная вершина
Var v, j :Integer; f :Boolean; sp :u;
Begin
Write(s:3); {вывод начальной вершины }
T := [s]; {помечаем ее как посещенную}
sp := Nil; Push ( sp, s ); {и помещаем в стек пройденных вершин}
While Not Empty (sp) Do Begin {основной цикл поиска в глубину}
v := Top( sp ); {взяли очередную вершину }
f := False;
For j :=1 To n Do {поиск не просмотренного потомка}
If ( A[ v, j ] <> 0 ) And Not( j In T )
Then Begin f := True; Break; End;
{Найдена новая вершина или все связанные с данной просмотрены}
If f Then Begin { если найден не просмотренный потомок}
Push ( sp, j ); { потомка - в стек }
Write(j:3); {вывод вершины j }
T := T + [ j ]; {помечаем ее как посещенную}
End
Else v := Pop( sp ); {возврат к предку с удалением из стека}
End;
End;
{ Головная программа }
Begin ……. Ввод матрицы А; DFS_NR (1); …….. End.
3. Перебор цепей поиском в глубину. Другим вариантом исчерпывающего исследования графа является перебор всевозможных цепей, начинающихся в заданной вершине s. Он применяется для поиска оптимальной в каком-то смысле цепи или цепи, удовлетворяющей перечню требований.
Хотя алгоритм изменяется минимально, сложность поиска, как правило, выше, чем в предыдущем варианте, и значительно, ибо одни и те же вершины участвуют в разных цепях, поэтому посещение вершины при движении "вперед" может быть многократным. С этим связано и изменение в алгоритме. Делая шаг "назад" от некоторой вершины x, мы заменяем признак «вершина x посещена» признаком «вершина x не посещена» и вершина x вновь доступна. Уникальность цепей обеспечивается тем, что после возврата к вершине y движение "вперед" произойдет не к той вершине, которая прежде следовала за y, а к иной (если она найдется). Поэтому две цепи поиска всегда различаются хотя бы одной вершиной.
На рис. . приведен пример выделения в заданном графе всех цепей, начинающихся с заданной вершины. Переменная b, принимающая значение True на шаге "вперед", нужна для распознания выделенной цепи. Текущая цепь представлена содержанием стека, вывод которого и производится. Граф задан матрицей смежности A.
Program Cepi;
Const
n=9;
A: Array[1..n,1..n] Of Byte=
((0,1,0,1,0,0,0,1,0),
(1,0,1,0,0,1,0,0,0),
(0,1,0,1,1,0,1,0,0),
(1,0,1,0,0,0,1,0,0),
(0,0,1,0,0,0,0,1,1),
(0,1,0,0,0,0,0,0,0),
(0,0,1,1,0,0,0,0,0),
(1,0,0,0,1,0,0,0,0),
(0,0,0,0,1,0,0,0,0));
Var T :Set Of 1..n; { множество посещенных вершин }
St :Array[ 1..n ] Of Byte; { стек для формирования цепей }
k :Integer; {указатель стека}
b :Boolean;
i :Integer;
Procedure Print; {Процедура вывода цепи}
Var i:Integer;
Begin
For i:=1 To k Do Write(St[i]:3);
Writeln;
End;
Procedure DFS_С( v :Integer );
Var j :Integer;
Begin
T:=T+[v];
Inc(k); St[k]:=v; {Текущую вершину заносим в стек}
b:=True; {Шаг «вперед», ибо v - подходящая вершина }
For j := 1 To n Do {Цикл поиска смежной вершины }
If ( A[v,j] <> 0 ) And Not(j In T) Then Begin
DFS_С (j); {Перемещаемся к потомку j}
If b {Если предыдущий шаг был «вперед»- выводим цепь}
Then Begin Print; b:=false; End;
Dec(k);
End;
T:=T-[v]; {Вершина v удаляется из множества посещенных }
End;
{ Головная программа }
Begin …. T:=[ ]; k:=0; DFS_С (5); ….. End.
Результаты работы программы приведены ниже. Выделены цепи начинающиеся из вершины 5.
5 3 2 1 4 7
5 3 2 1 8
5 3 2 6
5 3 4 1 2 6
5 3 4 1 8
5 3 4 7
5 3 7 4 1 2 6
5 3 7 4 1 8
5 8 1 2 3 4 7
5 8 1 2 3 7 4
5 8 1 2 6
5 8 1 4 3 2 6
5 8 1 4 3 7
5 8 1 4 7 3 2 6
5 9
Поиск в ширину. (BFS, Breadth-first search) Перейдем теперь к другому алгоритму обхода графа, известному под названием обход в ширину (поиск в ширину). Прежде чем описать его, отметим, что при обходе в глубину чем позднее будет посещена вершина, тем раньше она будет использована. Это прямое следствие того факта, что просмотренные, но еще не использованные вершины накапливаются в стеке. Обход графа в ширину основывается на замене стека очередью. После такой модификации чем раньше посещается вершина (помещается в очередь), тем раньше она используется (удаляется из очереди). Использование вершины происходит с помощью просмотра сразу всех еще не просмотренных вершин, смежных этой вершины. Таким образом, "поиск ведется как бы во всех возможных направлениях одновременно" [1, с.131]
Таким образом, идея метода заключается в том, чтобы рассмотреть все вершины связанные с текущей. Их еще называют вершинами одного «поколения» или ярусом. Граф исследуется, начиная от начальной вершины, ярус за ярусом пока не будут пройдены все его вершины. Например, на рис.6.12 для вершины 1 ярус образуют вершины 3, 4, 6. Подобную систему исследования графа называют поиском в ширину. Принцип выбора следующей текущей вершины таков: выбирается та, которая была раньше рассмотрена. Для реализации данного принципа необходима структура данных «Очередь». Вот как выглядит алгоритм обхода по уровням [MacConnel]:
BFS(G,v)
G граф
v текущий узел
Visit(v) {действия в вершине v}
Mark(v) {помечаем вершину v как посещенную}
Enqueue(v) {включаем вершину v в очередь}
While очередь непуста Do {поиск не просмотренных вершин}
Dequeue(x) {достаем вершину x из очереди}
For каждого ребра xw в графе G Do
If вершина w непомечена Then
Visit(w)
Mark(w)
Enqueue(w)
End If
End For
End While
End BFS
Этот
алгоритм заносит в очередь корень дерева
обхода по уровням, но затем немедленно
удаляет его из очереди. При просмотре
соседних с корнем вершин он заносит их
в очередь.
После посещения всех соседних с корнем
вершин происходит возвращение к очереди
и обращение к первой вершине оттуда.
Обратите внимание на то, что поскольку
вершины добавляются к концу очереди,
ни одна из вершин, находящихся на
расстоянии двух ребер от корня, не будет
рассмотрена повторно, пока не будут
обработаны и удалены из очереди все
вершины на расстоянии одного ребра от
корня.
Реализация алгоритма может выглядеть так:
Var Q :Array[1..n] Of Integer; {Очередь}
r, w :Integer; {r,w указатели чтения и записи соответственно}
Procedure BFS ( v: Integer); { Поиск в ширину.}
Var j :Integer;
Begin
r:=0; w:=1; Q[w]:=v; { начальную вершину -> в очередь }
T:=[ v ]; {помечаем ее как посещенную}
While r < w Do Begin { пока очередь не пуста }
Inc( r ); v := Q[ r ]; {берем текущую вершину из очереди }
Write( v:3 ); {вывод текущей вершины }
For j := 1 To n Do {перебор всех потомков вершины v}
If (A[ v, j ] <> 0 ) And Not ( j In T ) { всех не просмотренных}
Then Begin { потомков вершины v }
Inc( w ); Q[ w ] := j; { записываем в очередь }
T := T + [ j ];
End;
End;
End;
{ Головная программа }
Begin ……… BFS( 1 ); ……….. End;
Замечания.
-
Как система поиск в ширину конкурирует с поиском в глубину.
-
Одни задачи удобнее (или эффективнее) решать поиском в ширину, другие – поиском в глубину, а некоторые приемлют и ту, и другую систему.
-
Поиск в ширину часто используют для нахождения кратчайших путей между вершинами.
Что можно сказать про эффективность алгоритмов поиска в глубину и ширину? Будем предполагать, что основная часть всей работы приходится на посещение вершин. Тогда расходы на проверку того, посещали ли мы уже данную вершину, и на переход по ребру можно не учитывать. Поэтому порядок сложности алгоритма определяется числом посещений вершин. Как мы уже говорили, каждая вершина посещается в точности один раз, поэтому на графе с n вершинами происходит n посещений. Таким образом, сложность алгоритмов равна O(n).