На этом шаге мы рассмотрим поиск в глубину.
Многие алгоритмы на графах основаны на систематическом переборе всех вершин графа, при котором каждая вершина просматривается в точности один раз. Здесь мы рассмотрим два стандартных и широко используемых метода такого перебора: поиск в глубину и поиск в ширину. Оба метода изучаются применительно к обыкновенным графам, поскольку на произвольные графы они могут быть распространены очевидным образом. Поиск в глубину применяется для отыскания компонент сильной связности в орграфе.
Идея этого метода состоит в следующем. Поиск в обыкновенном графе G начинается с некоторой начальной вершины v (с этого момента v считается просмотренной). Пусть m - последняя просмотренная вершина (этой вершиной может быть и v). Тогда возможны два случая.
Что произойдет, когда все просмотренные вершины будут использованы? Если в графе G не осталось непросмотренных вершин, то поиск заканчивается. Если же осталась непросмотренная вершина y, то поиск продолжается из этой вершины.
Поиск в глубину просматривает вершины графа G в определенном порядке. Для того чтобы зафиксировать этот порядок, будет использоваться массив num[v]. При этом естественно считать, что num[v] = 1, если v начальная вершина, и num[w] = num[m] + 1, если w просматривается сразу после того, как просмотрена вершина m.
Пусть в обыкновенном графе G проведен поиск в глубину. Обозначим через Т множество всех древесных ребер. Все остальные ребра графа будем называть обратными ребрами. Множество всех обратных ребер будем обозначать через В.
Результат применения поиска в глубину к связному графу G (рисунок 1а) показан на рисунке 1б. Здесь сплошные линии изображают древесные ребра, а пунктирные линии - обратные ребра. Заметим, что нумерация вершин соответствует порядку, в которым они просматриваются поиском в глубину. Можно обратить внимание на то, что множество всех древесных ребер с выделенной начальной вершиной v1 образует корневое дерево с корнем v1. Это корневое дерево часто называют глубинным деревом, или, короче, d-деревом графа G. Следует также отметить, что каждое обратное ребро соединяет в d-дереве предка и потомка.
Рис.1. Обход графа
Пусть G - несвязный граф, G1, ..., Gk - множество всех его компонент связности. Обозначим через Тi множество древесных ребер, выделенных поиском в глубину в компоненте Gi, а через vi - корневую вершину из Gi, т.е. первую просмотренную вершину подграфа Gi. Таким образом, множество всех древесных ребер несвязного графа образует остовный лес. Фиксируя в каждом поддереве этого леса корневую вершину, мы получим глубинный лес или, короче, d-лес графа G.
Представим теперь формальное описание указанного алгоритма. В алгоритме используются описанные ранее массивы num и father. В процессе работы алгоритма массив num используется для распознавания непросмотренных вершин, а именно, равенство num[v] = 0 означает, что вершина v еще не просмотрена.
Вначале изложим версию алгоритма поиска в глубину, основанную на рекурсивной процедуре DFS(v) (название процедуры является аббревиатурой от англ. depth first search), осуществляющей поиск в глубину из вершины v.
Алгоритм 1.
1. procedure DFS(v); 2. begin 3. num[v]:= i; i:= i + 1; 4. for u ∈ list[v] do 5. if num [u] = 0 then 6. begin 7. T:= T ∪ {uv}; father[u]:= v; DFS(u) 8. end 9. else 10. if num[u] < num[v] and u ≠ father[v] 11. then 12 . B:= B ∪ {uv} 13. end; 14. begin 15. i:= 1; T:= 0; В:= 0; 16. for v ∈ V do num[v]:= 0; 17. for v ∈ V do 16. if num[v] = 0 then 17. begin 18. father[v]:= 0; DFS(v); 19. end 20. end.
Алгоритм 1 применим к произвольному обыкновенному графу G. Если G - связный граф, то цикл достаточно заменить вызовом процедуры DFS(vq) применительно к начальной вершине vq. Заметим, что в терминах алгоритма 1 вершина v просмотрена с началом работы процедуры DFS(v); в тот момент, когда процедура DFS(v) закончила работу, вершина v является использованной.
Пусть G - связный граф, v0 - некоторая его вершина. Предположим, что в графе G проведен поиск в глубину из вершины vq. Дерево (V, Т) с выделенной вершиной vq является корневым деревом. Как отмечалось выше, это корневое дерево часто называют d-деревом или глубинным деревом графа G. Для любой вершины u≠v0 вершина father[u] является отцом и в d-дереве.
Рассмотрим нерекурсивную версию процедуры DFS(v). Рекурсия устраняется при помощи стека S, элементами которого являются вершины графа. Вершина v является просмотренной, если num[v]≠0. Вершина v становится использованной с момента, когда v - top(S) (v находится в вершине стека) и все вершины, смежные с v, уже просмотрены (в этом случае v удаляется из стека). Вычисления, связанные с множеством обратных ребер B, здесь опущены; разобравшись с рекурсивной версией процедуры DFS(v), без труда можно восстановить их.
1. procedure DFS(v); 2. begin 3. num[u]:= i; i:= i+1; 4. S:= nil; S <= v; 5. while S ≠ nil do 6. begin 7. v:= top(S); 8. if exists u, u ∈ list[v] and num[u] = 0 9. then 10. begin 11. num[u]:= i; i:= i + 1; T:= T ∪ {uv}; 12. father[u]:= v; S<= v 13. end 14. else v <= S 15. end 16. end;
Отметим следующее важное свойство поиска в глубину: если xy - обратное ребро, то вершины x, y сравнимы в d-дереве, т.е. одна из этих вершин является предком другой.
В самом деле, пусть xy - обратное ребро графа G, причем num[x] < num[y]. Предположим, что вершины x и y несравнимы в d-дереве. Из описания алгоритма 1 следует, что в промежуток времени между началом работы процедуры DFS(x) и ее завершением, будут просмотрены только потомки этой вершины. Поскольку y ∈ list[v] и в момент завершения процедуры DFS(x) вершина y еще не просмотрена, получаем противоречие с описанием алгоритма 1.
На следующем шаге мы рассмотрим алгоритм отыскания блоков и точек сочленения.