Назад

Методы программирования

Задание 8. Поиск двусвязных компонент и точек сочленения

Общая информация

Задание обязательно для желающих получить оценку «отлично».

Теория

Компонентой связности неориентированного графа будем называть любой максимальный связный подграф этого графа. Это определение можно переформулировать так: компонента связности — это такой подграф, что для любой вершины u из этого подграфа все вершины, в которые в исходном графе существует путь из u, принадлежат этому же подграфу. На рисунке изображен граф, имеющий две компоненты связности.

Пример графа с 2 компонентами связности

Будем называть некоторую вершину неориентированного графа точкой сочленения, если при удалении ее и всех инцидентных ей ребер в графе увеличивается количество компонент связности. Эквивалентным определением является следующее: вершина u является точкой сочленения тогда и только тогда, когда в графе существуют две вершины v и w, отличные от u и принадлежащие одной компоненте связности, такие, что любой путь из v в w проходит через u.

Будем называть граф двусвязным, если он не содержит точек сочленения. Всякий максимальный двусвязный подграф графа будем называть двусвязной компонентой. Другими словами, двусвязная компонента графа — это любой его подграф, в котором удаление произвольной вершины и инцидентных ей ребер не влечет потерю связности этого подграфа, и к этому подграфу нельзя добавить ни одной вершины, сохранив это свойство. На рисунке в графе выделены точки сочленения (вершины 2 и 4) и указаны двусвязные компоненты ({1, 2, 4}, {4, 6, 7}, {2, 3}, {5, 8}).

Двусвязные компоненты и точки сочленения

Заметим, что любые две различные двусвязные компоненты либо не имеют общих вершин, либо имеют одну общую вершину, которая является точкой сочленения.

Сформулируем без доказательства следующую теорему.

Теорема. Пусть Dостовное дерево (каркас) неориентированного связного графа G, построенный методом поиска в глубину от вершины r. Вершина v является точкой сочленения графа G тогда и только тогда, когда:

Изменим процедуру обхода графа в глубину, добавив в нее вычисление двух массивов: int number[N] и int L[N], где N — количество вершин графа. Для каждой вершины v в number[v] будем хранить номер этой вершины в порядке обхода графа в глубину, а в L[v] запишем число number[w], где w — вершина, которая связана обратным ребром с v или каким-либо из потомков v в остовном дереве и имеет минимальный возможный номер number, либо number[v], если такой вершины w найти не удалось. Напомним, что обратными ребрами мы называем ребра, не вошедшие в каркас графа, построенный обходом в глубину.

Разберем подробнее смысл значений L[v]. На рисунке построено остовное дерево графа с корнем в вершине r = 1 и выписаны значения массивов number[N] и L[N]. Для корня остовного дерева L[r] = used[r] = 1. Для вершины 2 значение L[2] равно 1, т.к. из вершины 4 (потомка вершины 2 в остовном дереве) можно вернуться по обратному ребру в вершину 1, для которой number[1] = 1. Аналогично, значения L[6] и L[7] равны 4, потому что из вершины 7 (потомка вершины 6) можно вернуться по обратному ребру в вершину, номер number которой равен четырем.

Вычисление значений number и L

Зная значения L и number, найти точки сочленения не составляет труда. Согласно приведенной выше теореме, вершина, не являющаяся корнем остовного дерева, будет являться точкой сочленения тогда и только тогда, когда для какого-либо из ее потомков в остовном дереве значение L будет больше либо равно значению number самой вершины. Это означает, что из этого потомка невозможно найти обратное ребро, соединенное с предком рассматриваемой вершины, и по теореме вершина будет являться точкой сочленения. Случай с корнем рассматривается отдельно: корень является точкой сочленения тогда и только тогда, когда у него существует больше одного сына в остовном дереве.

Сформулируем алгоритм поиска точек сочленения более строго. Реализуем рекурсивную функцию void go(int curr, int prev), где curr — текущая вершина, а prev — вершина, из которой мы попали в текущую. При первом вызове curr = r, prev = –1. В теле функции будут выполняться следующие шаги:

  1. Запишем в number[curr] номер вершины curr в порядке обхода в глубину.
  2. Запишем в L[curr] значение number[curr].
  3. Переберем в цикле все вершины, в которые есть ребро из curr. Для каждой такой вершины i выполним следующие действия:
    1. Если вершина i еще не посещена, вызовем рекурсивно функцию go с параметрами i, curr. Если после этого значение L[i] стало меньше, чем L[curr], присвоим L[curr] = L[i].
    2. Если вершина i уже была посещена и ее номер number[i] < number[curr], и при этом i не равно prev (т.е. ребро (i, prev) обратное и возвращается в вершину с меньшим номером), то если L[curr] > number[i], присвоим L[curr] = number[i].

Данный алгоритм заполнит массивы L[N] и number[N] требуемым образом. Проверять, является ли вершина точкой сочленения, можно на шаге 3a. Также, если реализовать стек для хранения ребер, можно реализовать вывод самих двусвязных компонент: ребро нужно добавлять в стек на шагах 3a (перед рекурсивным вызовом) и 3b и выталкивать из стека в поток вывода все ребра вплоть до текущего (curr, i), если на шаге 3а выяснилось, что для текущей вершины curr выполняется условие теоремы.

Формулировка задания

Реализовать алгоритм поиска двусвязных компонент и точек сочленения в связном неориентированном графе, заданном списком ребер либо матрицей смежности (на выбор студента).