Лучший алгоритм для обнаружения циклов в ориентированном графе

Какой алгоритм наиболее эффективен для обнаружения всех циклов в ориентированном графе?

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


person Peauters    schedule 04.11.2008    source источник
comment
Вы говорите, что хотите обнаружить все циклы, но ваш вариант использования предполагает, что этого будет достаточно, чтобы определить, существуют ли какие-либо циклы.   -  person Steve Jessop    schedule 04.11.2008
comment
Было бы лучше обнаруживать все циклы, чтобы их можно было исправить за один раз, а не проверять, исправлять, проверять, исправлять и т. Д.   -  person Peauters    schedule 04.11.2008
comment
Вам следует прочитать статью Дональда Б. Джонсона «Поиск всех элементарных схем ориентированного графа». Он найдет только элементарные схемы, но этого должно быть достаточно для вашего случая. И вот моя реализация этого алгоритма на Java, готовая к использованию: github.com/1123/johnson   -  person user152468    schedule 14.02.2015
comment
Запустите DFS с дополнительной модификацией алгоритма: отметьте каждый посещенный узел. если вы посещаете узел, который уже посещен, значит, у вас есть цикл. когда вы отступаете от пути, снимите отметку с посещаемых узлов.   -  person Hesham Yassin    schedule 07.05.2016
comment
@HeshamYassin, если вы посещаете узел, который уже посещали, это не обязательно означает, что есть цикл. Прочтите мой комментарий cs.stackexchange.com/questions/9676/.   -  person Maksim Dmitriev    schedule 15.01.2017
comment
Ваше первое предложение противоречит вашему последнему предложению; пожалуйста исправьте. Если вы действительно хотите обнаружить все циклы (первое предложение), ваш наихудший размер вывода и время выполнения будут экспоненциальными по отношению к вашему размеру ввода. Если вы действительно хотите просто обнаружить случай ошибки any цикла (последнее предложение), вы можете сделать это во времени, которое линейно по размеру ввода. Я бы рекомендовал последнее.   -  person Don Hatch    schedule 05.02.2019
comment
@Pneauters нет, не обязательно было бы лучше обнаруживать все циклы. Рассмотрим случай, когда их экспоненциальное количество.   -  person Don Hatch    schedule 05.02.2019
comment
Вы можете использовать мою простую и эффективную реализацию: stackoverflow.com/a/60196714/1763149   -  person Marcin Raczyński    schedule 13.02.2020


Ответы (14)


Алгоритм сильно связанных компонентов Тарьяна имеет O(|E| + |V|) временную сложность.

Для других алгоритмов см. Сильно связанные компоненты в Википедии.

person aku    schedule 04.11.2008
comment
Спасибо, Аку, это должно сработать, а также будет означать, что я могу показать подграфы, вызывающие проблему. - person Peauters; 04.11.2008
comment
Каким образом нахождение сильно связанных компонентов говорит вам о циклах, существующих в графе? - person Peter; 29.03.2010
comment
Может быть, кто-то может подтвердить, но алгоритм Tarjan не поддерживает циклы узлов, указывающих непосредственно на себя, например A- ›A. - person Cédric Guillemette; 14.09.2010
comment
@Cedrik Верно, не напрямую. Это не недостаток алгоритма Тарьяна, а то, как он используется для ответа на этот вопрос. Тарджан не находит напрямую циклы, он находит сильно связанные компоненты. Конечно, любой SCC размером больше 1 подразумевает цикл. Нециклические компоненты сами по себе имеют одноэлементный SCC. Проблема в том, что петля сама перейдет в SCC. Так что вам нужна отдельная проверка на наличие петель, что довольно тривиально. - person mgiuca; 09.02.2011
comment
Как заметил Питер, сильно связанные компоненты не эквивалентны циклам, но могут помочь в эффективном поиске циклов в графе. Для поиска циклов см. Статью Дональда Б. Джонсона «Поиск всех элементарных циклов ориентированного графа» и мою его реализацию на Java: github.com/1123/johnson - person user152468; 14.02.2015
comment
(все сильно связные компоненты в графе)! = (все циклы в графе) - person optimusfrenk; 16.02.2015
comment
Пожалуйста, избегайте ответов только по ссылкам. - person Adam Arold; 28.03.2015
comment
Из Википедии: Если каждая сильно связная компонента стягивается до одной вершины, результирующий граф является ориентированным ациклическим графом, сгущением G. Ориентированный граф является ацикличным тогда и только тогда, когда он не имеет сильно связных подграфов с более чем одной вершиной, поскольку направленный цикл сильно связен и каждая нетривиальная компонента сильной связности содержит хотя бы один направленный цикл. - person João Almeida; 03.02.2017
comment
@ aku: Трехцветная DFS также имеет такую ​​же среду выполнения O(|E| + |V|). Использование белого (никогда не посещенного), серого (текущий узел посещается, но все достижимые узлы еще не посещены) и черного (все достижимые узлы посещаются вместе с текущим) цветового кодирования, если серый узел находит другой серый узел, тогда мы ' ве цикл. [Примерно то, что мы в книге алгоритмов Кормена]. Интересно, имеет ли «алгоритм Тарьяна» какое-либо преимущество перед такой DFS !! - person KGhatak; 23.06.2017
comment
Это правильный ответ, но алгоритм Тарьяна излишни, если вам нужно только обнаруживать циклы, а не перечислять их. См. Ответ Курта Пика для более простого случая обнаружения, но не перечисления циклов. - person Luke Hutchison; 29.03.2020

Учитывая, что это расписание заданий, я подозреваю, что в какой-то момент вы собираетесь отсортировать их в соответствии с предлагаемым порядком выполнения.

В этом случае реализация топологической сортировки в любом случае может обнаруживать циклы. UNIX tsort определенно делает. Я думаю, что, вероятно, поэтому более эффективно обнаруживать циклы одновременно с сортировкой, а не на отдельном этапе.

Таким образом, вопрос может заключаться в том, «как мне наиболее эффективно выполнять сортировку», а не «как мне наиболее эффективно обнаруживать петли». На что ответ, вероятно, будет «использовать библиотеку», но в противном случае следующая статья в Википедии:

http://en.wikipedia.org/wiki/Topological_sorting

содержит псевдокод для одного алгоритма и краткое описание другого от Тарьяна. Оба имеют O(|V| + |E|) временную сложность.

person Steve Jessop    schedule 04.11.2008
comment
Топологическая сортировка может обнаруживать циклы, поскольку она основана на алгоритме поиска в глубину, но для фактического обнаружения циклов требуется дополнительный учет. См. Правильный ответ Курта Пика. - person Luke Hutchison; 29.03.2020

Согласно лемме 22.11 из Кормен и др., Введение в алгоритмы (CLRS):

Ориентированный граф G является ацикличным тогда и только тогда, когда поиск G в глубину не дает обратных ребер.

Об этом упоминалось в нескольких ответах; здесь я также приведу пример кода, основанный на главе 22 CLRS. Пример графика проиллюстрирован ниже.

введите описание изображения здесь

Псевдокод CLRS для поиска в глубину гласит:

введите описание изображения здесь

В примере, показанном на рис. 22.4 CLRS, граф состоит из двух деревьев DFS: одно состоит из узлов u, v, x и y, а другой из узлов w и z. Каждое дерево содержит один задний край: один от x до v, а другой от z до z (само- петля).

Ключевая реализация состоит в том, что задний край встречается, когда в функции DFS-VISIT при итерации по соседям v из u встречается узел с цветом GRAY.

Следующий код Python представляет собой адаптацию псевдокода CLRS с добавленным предложением if, которое обнаруживает циклы:

import collections


class Graph(object):
    def __init__(self, edges):
        self.edges = edges
        self.adj = Graph._build_adjacency_list(edges)

    @staticmethod
    def _build_adjacency_list(edges):
        adj = collections.defaultdict(list)
        for edge in edges:
            adj[edge[0]].append(edge[1])
        return adj


def dfs(G):
    discovered = set()
    finished = set()

    for u in G.adj:
        if u not in discovered and u not in finished:
            discovered, finished = dfs_visit(G, u, discovered, finished)


def dfs_visit(G, u, discovered, finished):
    discovered.add(u)

    for v in G.adj[u]:
        # Detect cycles
        if v in discovered:
            print(f"Cycle detected: found a back edge from {u} to {v}.")

        # Recurse into DFS tree
        if v not in finished:
            dfs_visit(G, v, discovered, finished)

    discovered.remove(u)
    finished.add(u)

    return discovered, finished


if __name__ == "__main__":
    G = Graph([
        ('u', 'v'),
        ('u', 'x'),
        ('v', 'y'),
        ('w', 'y'),
        ('w', 'z'),
        ('x', 'v'),
        ('y', 'x'),
        ('z', 'z')])

    dfs(G)

Обратите внимание, что в этом примере time в псевдокоде CLRS не захватывается, потому что нас интересует только обнаружение циклов. Существует также некоторый шаблонный код для построения представления списка смежности графа из списка ребер.

Когда этот сценарий выполняется, он выводит следующий результат:

Cycle detected: found a back edge from x to v.
Cycle detected: found a back edge from z to z.

Это в точности задние края в примере на рис. 22.4 CLRS.

person Kurt Peek    schedule 01.01.2019
comment
Я получаю RecursionError: maximum recursion depth exceeded while calling a Python object за этот код. - person zino; 01.12.2020
comment
@zino Похоже, после обнаружения цикла должно быть break. Я попытался добавить его, но очередь редактирования заполнена. - person A_P; 09.12.2020

Самый простой способ сделать это - выполнить обход графика в глубину (ДПФ).

Если граф имеет n вершину, это алгоритм O(n) временной сложности. Поскольку вам, возможно, придется выполнять ДПФ, начиная с каждой вершины, общая сложность становится O(n^2).

Вы должны поддерживать стек, содержащий все вершины в текущем обходе глубины, с его первым элементом, являющимся корневым узлом. Если вы встретите элемент, который уже находится в стеке во время DFT, то у вас есть цикл.

person Community    schedule 21.04.2009
comment
Это было бы верно для обычного графа, но неверно для ориентированного графа. Например, рассмотрим ромбовидную диаграмму зависимости с четырьмя узлами: A с ребрами, указывающими на B и C, каждый из которых имеет ребро, указывающее на D. Ваш обход этой диаграммы с помощью DFT из A неверно пришел бы к выводу, что цикл на самом деле был циклом - Хотя есть цикл, это не цикл, потому что по нему нельзя пройти, следуя стрелкам. - person Peter; 29.03.2010
comment
@peter, не могли бы вы объяснить, как DFT из A неправильно делает вывод о существовании цикла? - person Deepak; 26.09.2012
comment
@Deepak - На самом деле, я неправильно прочитал ответ физического мастера: где он писал в стеке, я думал, что уже найдено. Действительно, было бы достаточно (для обнаружения направленного цикла) проверить наличие дубликатов в стеке во время выполнения ДПФ. По одному голосу за каждого из вас. - person Peter; 27.09.2012
comment
Почему вы говорите, что временная сложность равна O(n), в то время как вы предлагаете проверить стек, чтобы увидеть, содержит ли он уже посещенный узел? Сканирование стека добавляет время O(n) среде выполнения, потому что ей приходится сканировать стек на каждом новом узле. Вы можете достичь O(n), если отметите посещенные узлы - person James Wierzba; 03.08.2016
comment
Как сказал Питер, это неполно для ориентированных графов. См. Правильный ответ Курта Пика. - person Luke Hutchison; 29.03.2020

На мой взгляд, наиболее понятным алгоритмом обнаружения цикла в ориентированном графе является алгоритм раскраски графа.

По сути, алгоритм раскраски графа обходит граф способом DFS (поиск в глубину, что означает, что он полностью исследует путь, прежде чем исследовать другой путь). Когда он находит задний край, он помечает граф как содержащий петлю.

Подробное объяснение алгоритма раскраски графа можно найти в этой статье: http://www.geeksforgeeks.org/detect-cycle-direct-graph-using-colors/

Кроме того, я предоставляю реализацию раскраски графов в JavaScript https://github.com/dexcodeinc/graph_algorithm.js/blob/master/graph_algorithm.js

person Armin Primadi    schedule 26.05.2016
comment
100% согласен. Просто потратил слишком много времени на решение этой проблемы. Раскрашивание - единственный алгоритм, который имеет смысл, и поэтому единственный, который я могу успешно реализовать. Я обязательно сохраню это в памяти или хотя бы на то, что он существует. - person Pavel Komarov; 17.04.2021

Начните с DFS: цикл существует тогда и только тогда, когда задний край обнаружен во время DFS. Это доказано в результате теории белого пути.

person Ajay Garg    schedule 30.12.2010
comment
Да, я думаю то же самое, но этого недостаточно, я публикую свой путь cs.stackexchange.com/questions/7216/find-the-simple-cycles-in-a-directed-graph - person Jonathan Prieto-Cubides; 07.12.2012
comment
Истинный. Аджай Гарг говорит только о том, как найти цикл, что является частичным ответом на этот вопрос. В вашей ссылке говорится о поиске всех циклов в соответствии с заданным вопросом, но опять же похоже, что он использует тот же подход, что и Аджай Гарг, но также делает все возможные деревья dfs. - person Manohar Reddy Poreddy; 11.09.2016
comment
Это неполно для ориентированных графов. См. Правильный ответ Курта Пика. - person Luke Hutchison; 29.03.2020
comment
Он не отвечает на вопрос, вопрос требует решения, чтобы найти все циклы - person sia; 25.07.2020

Если вы не можете добавить свойство «посещенные» к узлам, используйте набор (или карту) и просто добавьте все посещенные узлы в набор, если они еще не включены в набор. Используйте уникальный ключ или адрес объектов в качестве «ключа».

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

Другое решение - попытаться найти следующую зависимость для выполнения. Для этого у вас должен быть какой-то стек, в котором вы можете помнить, где вы сейчас находитесь и что вам нужно делать дальше. Перед выполнением проверьте, есть ли зависимость в этом стеке. Если это так, вы нашли цикл.

Хотя это может показаться сложностью O (N * M), вы должны помнить, что стек имеет очень ограниченную глубину (поэтому N мало) и что M становится меньше с каждой зависимостью, которую вы можете отметить как «выполнено» плюс вы можете остановить поиск, когда найдете лист (так что вам никогда не придется проверять каждый узел -> M тоже будет маленьким).

В MetaMake я создал график в виде списка списков, а затем удалил все узлы по мере их выполнения, что естественным образом сократило объем поиска. На самом деле мне никогда не приходилось запускать независимую проверку, все это происходило автоматически во время обычного выполнения.

Если вам нужен режим «только тест», просто добавьте флаг «пробного прогона», который отключает выполнение фактических заданий.

person Aaron Digulla    schedule 04.11.2008

Не существует алгоритма, который мог бы найти все циклы в ориентированном графе за полиномиальное время. Предположим, что ориентированный граф имеет n узлов, и каждая пара узлов имеет связи друг с другом, что означает, что у вас есть полный граф. Таким образом, любое непустое подмножество этих n узлов указывает на цикл, и таких подмножеств 2 ^ n-1. Таким образом, не существует алгоритма полиномиального времени. Итак, предположим, что у вас есть эффективный (не глупый) алгоритм, который может сказать вам количество направленных циклов в графе, вы можете сначала найти сильные связанные компоненты, а затем применить свой алгоритм к этим связанным компонентам. Поскольку циклы существуют только внутри компонентов, а не между ними.

person Yuwen    schedule 13.04.2013
comment
Верно, если количество узлов принято за размер ввода. Вы также можете описать сложность выполнения в терминах числа ребер или даже циклов или комбинации этих мер. Алгоритм Поиск всех элементарных схем ориентированного графа Дональда Б. Джонсона имеет полиномиальное время работы, равное O ((n + e) ​​(c + 1)), где n - количество узлов, e - количество ребер и c. количество элементарных схем графа. А вот моя реализация этого алгоритма на Java: github.com/1123/johnson. - person user152468; 14.02.2015

Я реализовал эту проблему в sml (императивное программирование). Вот схема. Найдите все узлы с нулевой или исходящей степенью. Такие узлы не могут быть частью цикла (поэтому удалите их). Затем удалите все входящие или исходящие ребра из таких узлов. Рекурсивно примените этот процесс к получившемуся графу. Если в конце у вас не останется ни одного узла или ребра, граф не имеет циклов, иначе они есть.

person Rpant    schedule 24.01.2013

https://mathoverflow.net/questions/16393/finding-a-cycle-of-fixed-length Мне больше всего нравится это решение, особенно для длины 4 :)

Также мастер физ говорит, что вам нужно сделать O (V ^ 2). Я считаю, что нам нужен только O (V) / O (V + E). Если граф подключен, то DFS посетит все узлы. Если в графе есть связанные подграфы, то каждый раз, когда мы запускаем DFS на вершине этого подграфа, мы найдем связанные вершины, и нам не нужно будет их рассматривать при следующем запуске DFS. Следовательно, возможность запуска для каждой вершины неверна.

person amitgoswami    schedule 16.02.2013

Я делаю это путем топологической сортировки, подсчитывая количество посещенных вершин. Если это число меньше общего числа вершин в DAG, у вас есть цикл.

person Steve    schedule 04.11.2008
comment
Это не имеет смысла. Если в графе есть циклы, топологическая сортировка отсутствует, что означает, что любой правильный алгоритм топологической сортировки будет прерван. - person sleske; 30.09.2009
comment
из википедии: Многие алгоритмы топологической сортировки также обнаруживают циклы, поскольку они препятствуют существованию топологического порядка. - person Oleg Mikheev; 20.01.2013
comment
@OlegMikheev Да, но Стив говорит: «Если это число меньше, чем общее количество вершин в DAG, у вас есть цикл, который не имеет смысла. - person nbro; 29.07.2015
comment
@nbro Готов поспорить, они означают вариант алгоритма топологической сортировки, который прерывается, когда топологическая сортировка не существует (а затем они не посещают все вершины). - person maaartinus; 11.09.2017
comment
Если вы выполните топологическую сортировку на графе с циклом, вы получите порядок с наименьшим количеством плохих ребер (порядковый номер ›порядковый номер соседа). Но после сортировки легко обнаружить эти плохие края, что приведет к обнаружению графа с циклом. - person Plagon; 04.02.2018

Если DFS находит ребро, которое указывает на уже посещенную вершину, у вас есть цикл.

person mafonya    schedule 12.05.2013
comment
Сбой на 1,2,3: 1,2; 1,3; 2,3; - person noisy cat; 04.02.2014
comment
@kittyPL, ты можешь объяснить, почему это дело провалилось? Либо 1) Это ориентированный граф, и поэтому цикл не был сформирован, либо 2) DFS будет идти 1 - ›2 (с использованием 1,2), 2 -› 3 (с использованием 2,3), 3 - ›1 (с использованием 1 , 3) - person Jake Greene; 05.02.2014
comment
@JakeGreene Посмотрите здесь: i.imgur.com/tEkM5xy.png Достаточно просто, чтобы понять. Допустим, вы начинаете с 0. Затем вы переходите к узлу 1, оттуда больше нет путей, повторное восхождение возвращается. Теперь вы посещаете узел 2, у которого есть ребро к вершине 1, которая уже была посещена. По вашему мнению, тогда у вас был бы цикл, а на самом деле его нет. - person noisy cat; 10.02.2014
comment
@kittyPL Этот график не содержит цикла. Из Википедии: Направленный цикл в ориентированном графе - это последовательность вершин, начинающаяся и заканчивающаяся в одной и той же вершине, такая, что для каждых двух последовательных вершин цикла существует ребро, направленное от более ранней вершины к более поздней. иметь возможность следовать по пути от V, который ведет обратно к V для направленного цикла. решение мафони работает на данную проблему - person Jake Greene; 10.02.2014
comment
@JakeGreene Конечно, нет. Используя свой алгоритм и начиная с 1, вы все равно обнаружите цикл ... Этот алгоритм просто плох ... Обычно достаточно идти назад всякий раз, когда вы встречаетесь с посещенной вершиной. - person noisy cat; 10.02.2014
comment
@kittyPL DFS обнаруживает циклы с данного начального узла. Но при выполнении DFS вы должны раскрасить посещенные узлы, чтобы отличить поперечный край от заднего края. При первом посещении вершины она становится серой, затем вы превращаете ее в черный, когда все ее ребра были посещены. Если при выполнении DFS вы попали в серую вершину, то эта вершина является предком (т.е. у вас есть цикл). Если вершина черная, то это просто поперечное ребро. - person Kyrra; 25.12.2014
comment
@kittyPLm - неплохой алгоритм, если вы отслеживаете узлы, которые находятся в рекурсивном стеке вызовов. - person Pavel; 18.09.2015

Как вы сказали, у вас есть набор заданий, их нужно выполнять в определенном порядке. Topological sort дал вам необходимый порядок для планирования заданий (или для проблем с зависимостями, если это direct acyclic graph). Запустите dfs и ведите список, и начните добавлять узел в начало списка, и если вы встретили узел, который уже посещался. Тогда вы нашли цикл в данном графике.

person Bhagwati Malav    schedule 12.04.2017

Если граф удовлетворяет этому свойству

|e| > |v| - 1

то граф содержит хотя бы один цикл.

person dharmendra singh    schedule 28.10.2010
comment
Это может быть верно для неориентированных графов, но определенно не для ориентированных графов. - person Hans-Peter Störr; 26.05.2011
comment
Контрпримером может быть A- ›B, B-› C, A- ›C. - person user152468; 14.02.2015
comment
Не у всех вершин есть ребра. - person Debanjan Dhar; 04.11.2016