Задание обязательно для желающих получить оценку «отлично».
Компонентой связности неориентированного графа будем называть любой
максимальный связный подграф этого графа. Это определение можно переформулировать
так: компонента связности — это такой подграф, что для любой вершины u
из этого подграфа все вершины, в которые в исходном графе существует путь из
u
, принадлежат этому же подграфу. На рисунке изображен граф,
имеющий две компоненты связности.
Будем называть некоторую вершину неориентированного графа точкой сочленения,
если при удалении ее и всех инцидентных ей ребер в графе увеличивается количество
компонент связности. Эквивалентным определением является следующее: вершина u
является точкой сочленения тогда и только тогда, когда в графе существуют две вершины
v
и w
, отличные от u
и принадлежащие одной
компоненте связности, такие, что любой путь из v
в w
проходит
через u
.
Будем называть граф двусвязным, если он не содержит точек сочленения.
Всякий максимальный двусвязный подграф графа будем называть двусвязной компонентой.
Другими словами, двусвязная компонента графа — это любой его подграф, в котором
удаление произвольной вершины и инцидентных ей ребер не влечет потерю связности
этого подграфа, и к этому подграфу нельзя добавить ни одной вершины, сохранив это свойство.
На рисунке в графе выделены точки сочленения (вершины 2
и 4
) и
указаны двусвязные компоненты ({1, 2, 4}
, {4, 6, 7}
,
{2, 3}
, {5, 8}
).
Заметим, что любые две различные двусвязные компоненты либо не имеют общих вершин, либо имеют одну общую вершину, которая является точкой сочленения.
Сформулируем без доказательства следующую теорему.
Теорема. Пусть D
— остовное дерево
(каркас) неориентированного связного графа
G
, построенный методом поиска в глубину от
вершины r
. Вершина v
является точкой сочленения графа G
тогда и только тогда, когда:
v = r
и r
имеет
по крайней мере двух сыновей в дереве D
,v
не равно r
и у вершины v
существует в дереве D
такой сын w
, что
ни w
, ни какой-либо из его потомков в дереве D
не связаны ребром ни с одним предком вершины v
в D
.
Изменим процедуру обхода графа в глубину, добавив в нее вычисление двух массивов:
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
которой равен четырем.
Зная значения L
и number
, найти точки сочленения не составляет труда.
Согласно приведенной выше теореме, вершина, не являющаяся корнем остовного дерева, будет
являться точкой сочленения тогда и только тогда, когда для какого-либо из ее потомков в
остовном дереве значение L
будет больше либо равно значению number
самой вершины. Это означает, что из этого потомка невозможно найти обратное ребро, соединенное
с предком рассматриваемой вершины, и по теореме вершина будет являться точкой сочленения.
Случай с корнем рассматривается отдельно: корень является точкой сочленения тогда и только
тогда, когда у него существует больше одного сына в остовном дереве.
Сформулируем алгоритм поиска точек сочленения более строго. Реализуем рекурсивную функцию
void go(int curr, int prev)
, где curr
— текущая вершина,
а prev
— вершина, из которой мы попали в текущую. При первом вызове
curr = r
, prev = –1
. В теле функции
будут выполняться следующие шаги:
number[curr]
номер вершины curr
в порядке обхода в глубину.L[curr]
значение number[curr]
.curr
.
Для каждой такой вершины i
выполним следующие действия:
i
еще не посещена, вызовем рекурсивно
функцию go
с параметрами i, curr
. Если
после этого значение L[i]
стало меньше, чем
L[curr]
, присвоим L[curr] = L[i]
.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
выполняется условие
теоремы.
Реализовать алгоритм поиска двусвязных компонент и точек сочленения в связном неориентированном графе, заданном списком ребер либо матрицей смежности (на выбор студента).