Pull to refresh

Алгоритм Kosaraju по полкам

Reading time6 min
Views20K

Эта статья продолжает обсуждение того, как понятнее изложить алгоритм Косарайю -- поиска компонент сильной связности в графе. В статье приводится изложение и обоснование корректности алгоритма.

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

Для того, чтобы понять алгоритм Косарайю необходимо владеть некоторыми понятиями теории графов

Основные понятия

сильно связанные вершины u, v
сильно связанные вершины u, v

Вершины u, v называются сильно связанными, если в графе G существует путь (необязательно прямой) u→v И v→u (Обозначим сильно связанные вершины u↔v), причём для каждой вершины u истинно uu

Компоненты сильной связности
Компоненты сильной связности




Компоненты сильной связности
-- максимальные сильно связанные подграфы

Инвертирование графа -- смена направлений всех рёбер в графе на противоположные (двунаправленное ребро остаётся самим собой)

Такт DFS из вершины v -- обход (в глубину) всех вершин графа, достижимых из v. Такт можно интерпретировать как рекурсивный вызов функции. Такт обработки вершины, у которой нет соседей, будет равняться 1.

Время выхода вершины -- число, соответствующее времени выхода рекурсии алгоритма DFS из вершины. Притом, изначально счётчик времени 0, увеличивается он лишь в двух случаях:

  1. Начало нового такта DFS

  2. Прохождение по ребру (при том, не важно, рекурсивный проход или нет)

Пример присвоения времени выхода:

Дан следующий граф:

Обойдём его алгоритмом DFS, помечая вершины, согласно правилам выше:


Непройденные вершины обозначим '?'
Будем выбирать стартовую вершину в порядке возрастания непомеченных вершин
Изначально, счётчик 0

Начинаем новый такт, выбрав вершину 0 (как вершину с минимальным индексом). Повышаем счётчик на 1 и присваиваем это число метке вершины 0.
Попутно будем строить деревья тактов DFS справа от исходного графа


У вершины 0 есть лишь один сосед -- вершина 2. Проходим по ребру 0-2, повышая счётчик и присваивая метку вершине 2

У вершины 2 нет соседей. Поэтому нам остаётся лишь рекурсивно вернуться в предыдущую вершину (чтобы понять, какая вершина была предыдущей, мы и строим дерево). Проходя по рекурсивному ребру 2-0, увеличиваем счётчик на 1 и присваиваем вершине 0 новую пометку

У вершины 0 больше нет непомеченных соседей -- такт заканчивается. Начиная новый такт, увеличиваем счётчик и присваиваем следующей (в порядке возрастания) вершине метку счётчика. Также, начинаем строить дерево нового такта


У вершины 1 есть 2 соседа: 0 и 3. Но вершина 0 уже имеет метку -- в неё идти нельзя. Тогда идём в вершину 3, увеличивая счётчик и присваивая метку


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


У вершины 1 больше нет соседей -- такт завершается. Также видим, что все вершины помечены. Алгоритм поиска в глубину завершён

Из примера прослеживается, что вершина начала такта (корень) имеет самую большую (в такте) метку -- это объясняется рекурсивностью алгоритма DFS -- алгоритм заходит в одну вершину и выходит из неё же. Этот факт понадобится при доказательстве.

Приведём несколько простых лемм:

Лемма 1: Если истинно uv И vw, то истинно u↔w

Доказательство:
Поскольку u↔v, то v достижима из u, также имеем v↔w, значит w достижима из v.
Следовательно w достижима из u, аналогично, u достижима из w. Получаем u↔w

Лемма 2: Компонента сильной связности -- это множество вершин, входящих в множество
циклов, имеющих хотя бы одну общую вершину

цикл А имеет общую вершину u с циклом В
цикл А имеет общую вершину u с циклом В

Доказательство:
Заметим, что все вершины одного цикла входят в одну компоненту связности, так как по циклу из любой его вершины можно достичь другую. Если два цикла имеют общую точку, то через неё можно перейти к вершинам другого цикла и достичь любую из них. Таким образом, все вершины, принадлежащие связанным
между собой циклам, входят в одну компоненту связности.
Рассмотрим любое из максимальных объединений циклов C. Докажем, что вершина, не
входящая в C, принадлежит другой компоненте сильной связности. Докажем от противного.
Пусть вершина v не входит в объединение циклов C, но сильно связана с одной из вершин этих
циклов u. Тогда по определению сильной связности есть путь от вершины v до u и путь от
вершины u до v. Склеивая (конкатенацией) эти пути, получаем цикл, связанный с объединением
циклов через вершину u, но не входящий в C, что противоречит предположению.
Заметим, что эта лемма дает идею о том, как на графе увидеть компоненты сильной связности.

Лемма 3: Инвертирование рёбер цикла не влияет на его цикличность

Доказательство:
Если в исходном графе есть путь из u в v, то в инвертированном графе будет путь из v в u.

Алгоритм Косарайю

Алгоритм Косарайю предназначен для поиска компонент сильной связности в ориентированном графе и состоит из трёх шагов:

  1. Выполнить поиск в глубину (DFS), пока не будут «помечены» все вершины. Вершина считается «помеченной», когда ей присвоено время выхода из рекурсии (см. основные понятия).

  2. Инвертировать исходный граф

  3. Выполнить DFS в порядке убывания пометок вершин.

Полученные деревья каждого такта DFS последнего шага являются компонентами сильной связности

Прежде чем доказывать алгоритм, предлагаю разобрать пример работы алгоритма

Пример работы алгоритма Косарайю пошагово

Дан следующий граф:








Шаг 1:
Непомеченные вершины имеют метку '?'
Справа от исходного графа находится лес DFS.
Стартовая вершина выбирается в порядке возрастания индексов.
Полный протокол работы не показал, так он имеется в разделе "основные понятия"




Шаг 2:
Инвертируем рёбра


Шаг 3:
Выбираем стартовую вершину в порядке убывания меток.
Так как теперь нам интересен лишь лес тактов DFS, то нет смысла считать время выхода из вершин, поэтому я не показал рекурсивных переходов.
Справа от инвертированного графа
показан лес тактов DFS




После завершения алгоритма имеем 3 компоненты сильной связности


Доказательство корректности алгоритма

Немного уточним, что требуется доказать:

Вершины u и v сильно связаны после выполнения алгоритма они принадлежат одному дереву такта DFS

Доказательство:

Если вершины u и v были сильно связаны в графе G, на третьем этапе будет найден путь из одной вершины в другую (по Лемме 3), так как на первом шаге был найден путь u→v, а на третьем -- путь v→u. Это означает, что по окончании алгоритма обе вершины лежат в одном дереве.

Вершины u и v лежат в одном и том же дереве поиска в глубину на третьем шаге алгоритма. Значит, они обе достижимы из корня r этого дерева.

Вершина r была рассмотрена на 3 шаге раньше всех, значит время выхода из неё на 1 шаге больше, чем время выхода из вершин u и v. Из этого мы получаем 2 случая:

  1. Обе эти вершины были достижимы из r в исходном графе. Это означает сильную связность вершин u и r и сильную связность вершин v и r (по Лемме 3). Склеивая пути мы получаем связность вершин u и v (по Лемме 1)

  2. Хотя бы одна вершина не достижима из r в исходном графе, например v. Значит и r была не достижима из v в исходном графе, так как время выхода из r — больше (если бы она была достижима, то время выхода из v было бы больше, чем из r, просмотрите ещё раз первый шаг примера). Значит между этими вершинами нет пути (ни в исходном, ни в инвертированном графах), но последнего быть не может, потому что по условию v достижима из r на 3 шаге (в инвертированном графе)

Значит, из случая 1 и не существования случая 2 получаем, что вершины u и v сильно связаны в обоих графах

Сложность алгоритма

Поиск в глубину в исходном графе выполняется за O(V+E)

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

Количество рёбер в инвертированном равно количеству рёбер в изначальном графе, поэтому поиск в глубину будет работать за O(V+E)

В итоге получаем, что при оценке снизу сложность алгоритма — O(V+E)
Оценка сверху (когда имеем полный граф) будет следующей (напомню, что количество рёбер в полном графе на n вершинах равно n(n-1)/2):

O(V+E)=O\bigg(V+\frac {V*(V-1)}{2} \bigg)=O\bigg(\frac{V^2+V}{2}\bigg) \equiv O(V^2)

То есть, в худшем случае имеем квадратичную сложность

Итог

Алгоритм Косарайю ищет компоненты сильной связности, причём делает он это за O(V+E) времени в лучшем случае, а в худшем -- за O(V^2).

Где применять алгоритм, полностью зависит от Вашего воображения составлять графы.

Например: проецирование транспортной сети на граф. Алгоритм поможет проверить новоиспечённую транспортную сеть на достижимость каждой вершины из каждой вершины (чтобы убедиться, что есть путь из окраины в центр и обратно).

Можно тестировать алгоритмом систему воздуховода в зданиях; течение тока в полупроводниковых приборах

Можно мыслить шире: проецируем кровеносную систему живого существа, которое Вам поручили создать в рамках проекта генной инженерии, где-нибудь в 2077 году). Алгоритм поможет узнать, доходит ли кровь от сердца до органов и обратно.

Tags:
Hubs:
Total votes 6: ↑6 and ↓0+6
Comments2

Articles