Кращий алгоритм виявлення циклів у спрямованому графіку


396

Який найефективніший алгоритм виявлення всіх циклів у межах спрямованого графіка?

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


13
Ви кажете, що хочете виявити всі цикли, але ваш випадок використання підказує, що було б достатньо, щоб виявити, чи є цикли.
Стів Джессоп

29
Було б краще виявити всі цикли, щоб їх можна було виправити за один раз, а не перевіряти, виправляти, перевіряти, виправляти тощо
Peauters

2
Ви повинні прочитати документ «Пошук усіх елементарних схем спрямованого графіка» Дональда Б. Джонсона. Він знайде лише елементарні схеми, але цього має бути достатньо для вашого випадку. А ось моя реалізація цього алгоритму на Java, готова до використання: github.com/1123/johnson
user152468

Запустіть DFS з додатковою модифікацією алгоритму: позначте кожен відвідуваний вами вузол. якщо ви відвідуєте вже відвіданий вузол, то у вас є цикл. коли ви відступаєте від шляху, зніміть позначку відвіданих вузлів.
Гешам Яссін

2
@HeshamYassin, якщо ви відвідуєте вузол, який ви вже відвідали, це не обов'язково означає, що існує цикл. Будь ласка , прочитайте мій коментар cs.stackexchange.com/questions/9676 / ... .
Максим Дмитрієв

Відповіді:


193

Алгоритм сильно пов'язаних компонентів Таряна має O(|E| + |V|)часову складність.

Інші алгоритми див. У Сильно пов'язаних компонентах у Вікіпедії.


69
Як пошук сильно пов'язаних компонентів говорить вам про цикли, які існують у графіку?
Пітер

4
Можливо, хтось може це підтвердити, але алгоритм Тарджана не підтримує цикли вузлів, що вказують безпосередньо на себе, як A-> A.
Седрик Гільємет

24
@Cedrik Право, не безпосередньо. Це не вада алгоритму Таряна, а спосіб його використання для цього питання. Тарджан не знаходить безпосередньо циклів , він знаходить сильно пов'язані компоненти. Звичайно, будь-який SCC з розміром більше 1 передбачає цикл. Нециклічні компоненти мають однотонний SCC. Проблема полягає в тому, що самостійна петля також перейде в SCC сама. Тож вам потрібна окрема перевірка на самонавіювання, що досить тривіально.
mgiuca

13
(всі сильно пов'язані компоненти в графіку)! = (всі цикли в графіку)
optimusfrenk

4
@ aku: триколірний DFS також має однаковий час виконання O(|E| + |V|). Використовуючи біле (ніколи не відвідуване), сіре (поточний вузол відвідується, але всі доступні вузли ще не відвідуються) і чорне (всі доступні вузли відвідуються разом із поточним) кольорове кодування, якщо сірий вузол знаходить інший сірий вузол, то ми ' у циклі. [Досить багато, що ми маємо в книзі алгоритмів Кормена]. Цікаво, чи має алгоритм «Таряна» якусь перевагу над такою DFS !!
KGhatak

73

Враховуючи, що це графік завдань, я підозрюю, що в якийсь момент ви збираєтесь сортувати їх за запропонованим порядком виконання.

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

Тож може статися питання "як я найбільш ефективно розбираю", а не "як я найбільш ефективно виявляю петлі". На що відповідь, ймовірно, "використовуйте бібліотеку", але не дотримуйтесь наступної статті у Вікіпедії:

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

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


Топологічний сорт може виявляти цикли, оскільки він спирається на алгоритм пошуку на глибині, але вам потрібна додаткова бухгалтерія, щоб фактично виявити цикли. Дивіться правильну відповідь Курта Піка.
Люк Хатчісон

33

Найпростіший спосіб зробити це - перше проходження глибини (DFT) графіка .

Якщо графік має nвершини, це O(n)алгоритм складності часу. Оскільки вам, можливо, доведеться робити DFT, починаючи з кожної вершини, загальна складність стає O(n^2).

Ви повинні підтримувати стек, що містить усі вершини в поточному першому проходженні глибини , причому його першим елементом є кореневий вузол. Якщо ви натрапили на елемент, який вже є у стеці під час DFT, у вас є цикл.


21
Це справедливо для "звичайного" графіка, але неправдиво для спрямованого графа. Наприклад, розглянемо "діаграму залежності алмазів" з чотирма вузлами: A з ребрами, які вказують на B і C, кожен з яких має ребро, що вказує на D. Ваш обхід DFT цієї діаграми від A неправильно зробив висновок, що "цикл" був насправді цикл - хоча цикл є, це не цикл, оскільки його не можна пройти, слідуючи стрілками.
Пітер

9
@peter, чи можете ви пояснити, як DFT від A неправильно зробить висновок про наявність циклу?
Діпак

10
@Deepak - Насправді я неправильно прочитав відповідь від "майстра фізики": де він написав "у стеці" я подумав, що "вже знайдений". Дійсно, було б достатньо (для виявлення спрямованого циклу), щоб перевірити наявність дупів "у стеці" під час виконання DFT. Один внесок для кожного з вас.
Пітер

2
Чому ви кажете, що часова складність полягає в тому O(n), що ви пропонуєте перевірити стек, щоб побачити, чи він уже містить відвіданий вузол? Сканування стека додає час O(n)виконання, оскільки він повинен сканувати стек на кожному новому вузлі. Ви можете досягти цього, O(n)якщо відзначити відвідувані вузли
James Wierzba

Як сказав Петро, ​​це не є повним для спрямованих графіків. Дивіться правильну відповідь Курта Піка.
Люк Хатчісон

32

Відповідно до леми 22.11 Cormen та ін., Вступ до алгоритмів (CLRS):

Спрямований графік G є ациклічним тоді і лише тоді, коли пошук на глибині першого G не дає зворотних ребер.

Про це згадувалося в кількох відповідях; тут я також наведу приклад коду, який базується на главі 22 CLRS. Приклад графіку проілюстровано нижче.

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

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

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

У прикладі на CLRS, рисунок 22.4, графік складається з двох дерев 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.

Це саме задні ребра в прикладі на CLRS Малюнок 22.4.


29

Почніть з DFS: цикл існує тоді і лише тоді, коли під час DFS виявлено зворотний край . Це доведено як результат теорію білого контуру.


3
Так, я думаю, що те саме, але цього недостатньо, я розміщую свій шлях cs.stackexchange.com/questions/7216/find-the-simple-cycles-in-a-directed-graph
jonaprieto

Правда. Аджай Гарг розповідає лише про те, як знайти «цикл», що є частиною відповіді на це питання. Ваш посилання говорить про пошук усіх циклів відповідно до заданого питання, але, схоже, він використовує той же підхід, що і Ajay Garg, але також робить усі можливі дерева-dfs.
Манохар Редді Поредді

Це є неповним для спрямованих графіків. Дивіться правильну відповідь Курта Піка.
Люк Хатчісон

26

На мою думку, найбільш зрозумілим алгоритмом виявлення циклу у спрямованому графіку є графік-колорит-алгоритм.

В основному алгоритм забарвлення графіка поводить графік в DFS-манері («Поглиблений перший пошук», що означає, що він вивчає шлях повністю перед вивченням іншого шляху). Коли він знаходить зворотний край, він позначає графік як містить цикл.

Для глибшого пояснення алгоритму забарвлення графіків читайте цю статтю: http://www.geeksforgeeks.org/detect-cycle-direct-graph-using-colors/

Також я забезпечую реалізацію розфарбовування графіків у JavaScript https://github.com/dexcodeinc/graph_algorithm.js/blob/master/graph_algorithm.js


8

Якщо ви не можете додати властивість "відвідане" до вузлів, використовуйте набір (або карту) і просто додайте всі нагадані вузли до набору, якщо вони вже відсутні. Використовуйте унікальний ключ або адресу об'єктів як "ключ".

Це також дає вам інформацію про "кореневий" вузол циклічної залежності, який стане у нагоді, коли користувачеві доведеться вирішити проблему.

Ще одне рішення - спробувати знайти наступну залежність, яку слід виконати. Для цього у вас повинен бути якийсь стек, де ви можете запам'ятати, де ви зараз, і що вам потрібно зробити далі. Перевірте, чи залежність вже є у цьому стеку, перш ніж її виконати. Якщо це так, ви знайшли цикл.

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

У MetaMake я створив графік як список списків, а потім видалив кожен вузол, коли я їх виконував, що природно скорочувало обсяг пошуку. Мені ніколи насправді не доводилося запускати незалежну перевірку, все відбувалося автоматично під час звичайного виконання.

Якщо вам потрібен режим "лише для тестування", просто додайте прапор "сухого виконання", який відключає виконання фактичних завдань.


7

Не існує алгоритму, який би міг знайти всі цикли у спрямованому графіку в поліноміальний час. Припустимо, спрямований графік має n вузлів, і кожна пара вузлів має з'єднання один з одним, а значить, у вас є повний графік. Отже, будь-яке непусте підмножина цих n вузлів вказує на цикл, і існує 2 ^ n-1 кількість таких підмножин. Отже, не існує алгоритму багаточленного часу. Отже, припустимо, у вас є ефективний (нерозумний) алгоритм, який може повідомити вам кількість спрямованих циклів у графіку, ви можете спочатку знайти сильно пов'язані компоненти, а потім застосувати свій алгоритм до цих підключених компонентів. Оскільки цикли існують лише всередині компонентів, а не між ними.


1
Правда, якщо кількість вузлів приймається за розмір вводу. Ви також можете описати складність виконання з точки зору кількості ребер або навіть циклів, або комбінацію цих заходів. Алгоритм "Знаходження всіх елементарних схем спрямованого графа" Дональда Б. Джонсона має поліноміальний час виконання, заданий O ((n + e) ​​(c + 1)), де n - кількість вузлів, e кількість ребер і c кількість елементарних схем графіка. І ось моя реалізація цього алгоритму на Java: github.com/1123/johnson .
користувач152468

4

Я реалізував цю проблему в sml (імперативне програмування). Ось контур. Знайдіть усі вузли, які мають 0 або інгредієнт 0. Такі вузли не можуть бути частиною циклу (тому видаліть їх). Далі видаліть усі вхідні або вихідні краї з таких вузлів. Рекурсивно застосуйте цей процес до отриманого графіка. Якщо в кінці у вас не залишилося жодного вузла чи ребра, графік не має циклів, інакше він має.


2

Як я це роблю, це робити топологічний Сортування, рахуючи кількість відвіданих вершин. Якщо це число менше загальної кількості вершин у DAG, у вас є цикл.


4
Це не має сенсу. Якщо графік має цикли, топологічне сортування не існує, а це означає, що будь-який правильний алгоритм топологічного сортування відмінить.
sleske

4
з Вікіпедії: Багато алгоритмів топологічного сортування також визначають цикли, оскільки це є перешкодами для існування топологічного порядку.
Олег Міхеєв

1
@OlegMikheev Так, але Стів каже: "Якщо це число менше загальної кількості вершин у DAG, у вас є цикл", що не має сенсу.
nbro

@nbro Я б обміняв, що вони мають на увазі варіант алгоритму топологічного сортування, який припиняється, коли не існує топологічного сортування (і тоді вони відвідують не всі вершини).
maaartinus

Якщо ви будете проводити топологічне сортування на графіку з циклом, ви отримаєте замовлення з найменшою кількістю неправильних ребер (номер замовлення> номер замовлення сусіда). Але після того, як вам доведеться сортувати, це легко виявити ті погані краї, що призводить до виявлення графіка з циклом
UGP

2

/mathpro/16393/finding-a-cycle-of-fixed-length Мені подобається це рішення найкраще спеціально на 4 довжини :)

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


1

Якщо DFS знайде край, який вказує на вже відвідану вершину, у вас є цикл.


1
Невдачі на 1,2,3: 1,2; 1,3; 2,3;
галасливий кіт

4
@JakeGreene Подивіться тут: i.imgur.com/tEkM5xy.png Досить просто, щоб зрозуміти. Скажімо, ви починаєте з 0. Потім ви переходите до вузла 1, звідти вже немає шляхів, відхилення повертається назад. Тепер ви відвідуєте вузол 2, який має вершину до вершини 1, яку вже відвідали. На вашу думку, у вас тоді був би цикл - а у вас цього немає насправді
галасливий кіт

3
@kittyPL Цей графік не містить циклу. З Вікіпедії: "Направлений цикл у спрямованому графіку - це послідовність вершин, що починаються і закінчуються в одній вершині, так що для кожної двох послідовних вершин циклу існує ребро, спрямоване від попередньої вершини до пізнішої" Ви повинні бути в змозі пройти шлях від V, який веде назад до V протягом спрямованого циклу. Рішення mafonya працює для даної проблеми
Джейк Грін

2
@JakeGreene Звичайно, це не так. Використовуючи свій алгоритм і починаючи з 1, ви все одно виявите цикл ... Цей алгоритм просто поганий ... Зазвичай достатньо було б ходити назад, коли ви стикаєтесь з відвідуваною вершиною.
галасливий кіт

6
@kittyPL DFS працює для виявлення циклів із заданого початкового вузла. Але, роблячи DFS, слід забарвити відвідані вузли, щоб відрізнити поперечний край від заднього краю. Перший раз, коли ви відвідуєте вершину, вона стає сірою, потім ви стаєте чорною, коли всі її краї були відвідані. Якщо при виконанні DFS ви потрапляєте в сіру вершину, то ця вершина є предком (тобто: у вас є цикл). Якщо вершина чорна, то це просто поперечний край.
Кирра

0

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


-11

Якщо графік задовольняє цій властивості

|e| > |v| - 1

то графік містить щонайменше на циклі.


10
Це може бути вірно для непрямих графіків, але, звичайно, не для спрямованих графіків.
Ганс-Пітер Стрер

6
Протилежним прикладом може бути A-> B, B-> C, A-> C.
користувач152468

Не всі вершини мають ребра.
Debanjan Dhar
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.