§ 73. Поиск в глубину
В этом параграфе рассматривается один из методов обхода всех вершин и ребер графа. Этот метод, именуемый поиском в глубину (сокращенно ПГ), послужил основой для разработки ряда быстрых алгоритмов на графах. Один из таких алгоритмов — алгоритм выделения двусвязных компонент графа — приводится в § 74. Дадим вначале некоторые определения. Ориентирований граф D — (V, A) назовем ориентированным деревом корнем r € V,если каждая его вершина достижима из и основание графа D — граф Db — является деревом. В тех случаях, когда недоразумение исключено, ориентированное дерево с корнем r будем называть просто деревом.
Пусть G — неориентированный связный граф, В провесе поиска в глубину вершинам графа G присваиваютcя номера (ПГ-номера), а его ребра помечаются. В начале ребра не помечены, вершины не имеют ПГ-номеров. Начинаем с произвольной вершины v0. Присваиваем ей ПГ-номер ПГ(v0)=1 и выбираем произвольное ребро v0w Ребро v0w помечается как «прямое», а вершина w включает (из v0) ПГ-номер ПГ(w)=2. После этого переводим в вершину w. Пусть в результате выполнения нескольких шагов этого процесса пришли в вершину х, и Пусть k — последний присвоенный ПГ-номер. Далее действуем в зависимости от ситуации следующим образом.
1. Имеется непомеченное ребро ху. Если у вершины уже есть ПГ-номер, то ребро ху помечаем как «обратное» продолжаем поиск непомеченного ребра, инцидентного вершине х. Если же вершина у ПГ-номера не имеет, то полагаем ПГ(y) = k + 1, ребро ху помечаем как «прямое» : переходим в вершину у. Вершина у считается получившей свой ПГ-номер из вершины х. На следующем шаге будем просматривать ребра, инцидентные вершине у.
2. Все ребра, инцидентные вершине х, помечены. В этом случае возвращаемся в вершину, из которой х получила свой ПГ-номер.
Процесс закончится, когда все ребра будут помечены : произойдет возвращение в вершину v0.
Описанный процесс можно реализовать так, чтобы ремя работы соответствующего алгоритма составляло (/EG/ + /G/).
Такой алгоритм (алгоритм A1) мы сейчас рассмотрим. Пусть граф G задан списками смежности, т. е. Nv — список вершин, инцидентных вершине v, и v0 — исходная вершина, с которой начинается поиск. В процессе работы алгоритма каждая вершина графа ровно один раз включается в список Q и исключается из него. Вершина включается в этот список сразу после получения ПГ-номера, исключается, как только произойдет возвращение из этой вершины. Включение и исключение вершин производятся всегда с конца списка, т. е. Q — стек. Результат работы алгоритма — четыре списка ПГ, F, Т и В: ПГ(v)— ПГ-номер вершины v; F(v)—имя вершины, из которой вершина v получила свой ПГ-номер; Т и В— соответственно списки ориентированных «прямых» и «обратных» ребер графа G. Эти ребра получают ориентацию в процессе работы алгоритма a1. Именно, если ребро ху помечается из вершины х как «прямое», то в Т заносится дуга (х, у), а если — как «обратное», то эта дуга заносится в В.
АлгоритмA1 поиска в глубину в неориентированном связном графе.
1 ПГ(v):=1, Q(1):= v0, F(vo):=0, T:=¢, B:=¢, k:=1,р:=1 [k — последний присвоенный ПГ-номер, р — указатель конца стека Q, т. е. Q(p)— имя последней вершины стека Q].
v := Q(p) [v — исследуемая вершина].
Просматривая список Nv, найти такую вершину w,что ребро vw не помечено, и перейти к п. 4. Если таких вершин нет, то перейти к п. 5.
Если вершина w имеет ПГ-номер, то пометить ребро vw как «обратное» и занести дугу (v, w) в список В. Перейти к п. 3 и продолжить просмотр списка Nv. Иначе k:=k + 1, ПГ(w):=k, F(w):=v, ребро vw пометить как «прямое» и дугу (v, w) занести в список T, р:=р+ 1, Q(p):=w [вершина w получила ПГ-номер и занесена встек Q]. Перейти к п. 2.
р := р — 1 [вершина v вычеркнута из Q]. Если р = 0, то конец. Иначе перейти к п. 2.
Корректность алгоритма очевидна. Оценим его трудоемкость. Каждое включение и исключение вершины из стека Q выполняется за время O(1). Поэтому для всей работы, связанной с изменением стека Q, достаточно времени O (|G|). Каждое выполнение п. 4 требует O (1) времени, и для каждой вершины v € VG этот пункт выполняется deg v раз. Поэтому затраты на его выполнение в
целом составят O (∑deg v) = О (|EG|). Суммарное время выполнения п. 3 также составит O(|EG|), если позаботиться о том, чтобы каждая вершина списка Nv просматривалась только один раз при всех выполнениях данного пункта. Этого легко добиться, если, например, ввести такую новую функцию (массив) t, что t(v)—номер первой непросмотренной вершины в списке Nv. Тогда следующий просмотр п. 3 следует начинать с вершины z = Nv(t(v)).
Итак, трудоемкость алгоритма A1 составляет O(|EG|+|G|). Ясно, что это время является наилучшим возможным по порядку, так как каждая вершина и каждое ребро графа G должны быть просмотрены хотя бы один раз.
Легко видеть, что требуемый для реализации алгоритма A1 объем памяти также составляет O(|EG|+|G|).
На рис. 73.1 слева изображены граф G и списки смежности, которыми он задан. Справа представлены результаты применения к этому графу поиска в глубину из
вершины v0 = 1. «Прямые» ребра проведены сплошными линиями, а «обратные» пунктирными. Каждой вершине приписан ее ПГ-номер.
Из способа построения множеств Т и В непосредственно вытекают следующие утверждения.
Утверждение 73.1. Дуги множества Т образуют ориентированное остовное дерево с корнем в вершине v0. Это остовное дерево часто называют остовным глубинным деревом (ОГД). Обозначать
его будем также через Т.
Утверждение 73.2. Если ориентированное ребро (х, у) принадлежит В, то ПГ(х)>ПГ(у), т. е. «обратные» ребра всегда ведут к ранее пройденным вершинам.
Поиск в глубину используется в качестве составной тасти во многих алгоритмах. Отметим одну задачу, решение которой можно получить с помощью алгоритма A1 фазу, почти без дополнительных вычислительных затрат. Это — задача выделения связных компонент графа. Чтобы решить ее за время O(|EG|+|G|), достаточно один раз просмотреть список вершин графа, выполняя следующие действия. Если просматриваемая вершина vl имеет ПГ-номер, то перейти к следующей. Иначе — положить v0 = vl ПГ(v0) = k + 1, где k — последний присвоенный ПГ-номер, и применить поиск в глубину.После его окончания (т. е. выделения компоненты, содержащей vl) продолжить просмотр списка, перейти к вершине vl+1. Различать вершины разных компонент можно, например, по их ПГ-номерам, если для каждой компоненты запомнить последний ПГ-номер.