Як перевірити, чи спрямований графік ациклічний?


82

Як перевірити, чи спрямований графік ациклічний? А як називається алгоритм? Буду вдячний за посилання.


Інший випадок на користь певного способу "виправити" неправильні відповіді на SO.
Sparr

2
Отже, м-м-м, мене здебільшого цікавить час, необхідний для його пошуку. Отже, мені просто потрібен абстрактний алгоритм.
nes1983

ви повинні пройти всі ребра і перевірити всі вершини, щоб нижня межа дорівнювала O (| V | + | E |). DFS і BFS мають однакову складність, але DFS легше
кодувати,

DFS - це не однакова складність. Розглянемо графік з вузлами {1 .. N} та ребрами у вигляді {(a, b) | a <b}. Цей графік ациклічний, і все ж DFS буде O (n!)
FryGuy

1
DFS ніколи не має значення O (n!). Він відвідує кожен вузол один раз і кожне ребро максимум двічі. Отже O (| V | + | E |) або O (n).
Джей Конрод,

Відповіді:


95

Я спробував би сортувати графік топологічно , а якщо ви не можете, то він має цикли.


2
Як це не мало голосів ?? Він лінійний за вузлами + ребрами, набагато перевершує рішення O (n ^ 2)!
Loren Pechtel,

5
У багатьох випадках DFS (див. Відповідь J.Conrod) може бути простішим, особливо якщо DFS все одно потрібно виконати. Але, звичайно, це залежить від контексту.
sleske

1
Топологічне впорядкування відбуватиметься в нескінченному циклі, але це не сказало б нам, де відбувається цикл ...
Барадвадж Арьясомаяджула

35

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

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

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

Як посилання, сильно зв’язаний компонентний алгоритм Тар’яна тісно пов’язаний. Це також допоможе вам знайти цикли, а не просто повідомити, чи існують вони.


2
До речі: край, який "вказує на вузол, що вже є у вашому стеку", в літературі із зрозумілих причин часто називається "заднім краєм". І так, це може бути простіше, ніж сортувати графік топологічно, особливо якщо вам все одно потрібно зробити DFS.
sleske

Щоб графік був ациклічним, ви говорите, що кожен підключений компонент повинен містити вузол лише з вихідними краями. Чи можете ви порекомендувати алгоритм пошуку підключених компонентів (на відміну від «сильно» зв’язаних компонентів) спрямованого графіка для використання вашим основним алгоритмом?
kostmo

@kostmo, якщо графік має більше одного підключеного компонента, то ти не будеш відвідувати всі вузли у своєму першому DFS. Відстежуйте відвідані вами вузли та повторюйте алгоритм із невідвіданими вузлами, поки не дійдете до всіх. В основному так працює алгоритм підключених компонентів.
Jay Conrod

6
Хоча намір цієї відповіді правильний, відповідь бентежить, якщо використовується реалізація DFS на основі стеку: стек, що використовується для реалізації DFS, не буде містити правильних елементів для тестування. Необхідно додати додатковий стек до алгоритму, який використовується для відстеження набору вузлів предків.
Теодор Мердок,

У мене є кілька запитань щодо вашої відповіді. Я розмістив їх тут: stackoverflow.com/questions/37582599/…
Арі

14

Лема 22.11 про книгу Introduction to Algorithms(друге видання) стверджує, що:

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


1
Це в основному лише скорочена версія відповіді Джея Конрода :-).
sleske

Однією з проблем тієї самої книги, мабуть, є припущення, що існує | V | часовий алгоритм. Тут дається
Джастін,

9

Рішення1 : Алгоритм Кана для перевірки циклу . Основна ідея: вести чергу, де вузол з нульовим ступенем буде доданий до черги. Потім відклеюйте вузол по одному, поки черга не порожня. Перевірте, чи існують внутрішні краї вузла.

Рішення2 : Тар'я алгоритм для перевірки Strong приєднаного компонента.

Рішення3 : DFS . Використовуйте цілочисельний масив для позначення поточного стану вузла: тобто 0 - означає, що цей вузол ще не відвідувався. -1 - означає, що цей вузол відвідали, а його дочірні вузли відвідують. 1 - означає, що цей вузол відвідано, і це зроблено. Отже, якщо при виконанні DFS статус вузла дорівнює -1, це означає, що повинен існувати цикл.


2

Рішення, надане ShuggyCoUk, є неповним, оскільки воно може перевірити не всі вузли.


def isDAG(nodes V):
    while there is an unvisited node v in V:
        bool cycleFound = dfs(v)
        if cyclefound:
            return false
    return true

Це має часову складність O (n + m) або O (n ^ 2)


мій дійсно неправильний - я видалив його, хоча ваш тепер здається трохи не контекстом
ShuggyCoUk

3
O (n + m) <= O (n + n) = O (2n), O (2n)! = O (n ^ 2)
Артру

@Artru O (n ^ 2) при використанні матриці суміжності, O (n + m) при використанні списку суміжностей для представлення графіка.
0x450

Гм ... m = O(n^2)оскільки повний графік має точно m=n^2ребра. Отож O(n+m) = O(n + n^2) = O(n^2).
Алекс Рейнкінг,

1

Я знаю, що це стара тема, але для майбутніх пошукачів тут є реалізація C #, яку я створив (не заявляю, що це найефективніше!). Це призначено для використання простого цілого числа для ідентифікації кожного вузла. Ви можете прикрасити, як завгодно, за умови, що об'єкт вузла хеш і дорівнює належним чином.

Для дуже глибоких графіків це може мати великі накладні витрати, оскільки це створює хешсет на кожному вузлі в глибину (вони руйнуються вшир).

Ви вводите вузол, з якого ви хочете здійснити пошук, і шлях до цього вузла.

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

    private bool FindCycle(int node, HashSet<int> path)
    {
    
        if (path.Contains(node))
            return true;
    
        var extendedPath = new HashSet<int>(path) {node};
    
        foreach (var child in GetChildren(node))
        {
            if (FindCycle(child, extendedPath))
                return true;
        }
    
        return false;
    }
    

1

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


1

ось швидкий код, щоб визначити, чи є графік циклами:

func isCyclic(G : Dictionary<Int,Array<Int>>,root : Int , var visited : Array<Bool>,var breadCrumb : Array<Bool>)-> Bool
{

    if(breadCrumb[root] == true)
    {
        return true;
    }

    if(visited[root] == true)
    {
        return false;
    }

    visited[root] = true;

    breadCrumb[root] = true;

    if(G[root] != nil)
    {
        for child : Int in G[root]!
        {
            if(isCyclic(G,root : child,visited : visited,breadCrumb : breadCrumb))
            {
                return true;
            }
        }
    }

    breadCrumb[root] = false;
    return false;
}


let G = [0:[1,2,3],1:[4,5,6],2:[3,7,6],3:[5,7,8],5:[2]];

var visited = [false,false,false,false,false,false,false,false,false];
var breadCrumb = [false,false,false,false,false,false,false,false,false];




var isthereCycles = isCyclic(G,root : 0, visited : visited, breadCrumb : breadCrumb)

Ідея така: звичайний алгоритм dfs з масивом для відстеження відвіданих вузлів і додатковий масив, який служить маркером для вузлів, що призвели до поточного вузла, так що коли ми коли-небудь ми виконуємо dfs для вузла ми встановлюємо відповідний елемент у масиві маркера як true, так що, коли коли-небудь вже відвіданий вузол зустрічається, ми перевіряємо, чи відповідає його відповідний елемент в масиві маркера true, якщо його true, то це один з вузлів, який дозволяє собі (отже циклу), і фокус полягає в тому, що коли dfs вузла повертається, ми встановлюємо його відповідний маркер назад на false, так що якщо ми знову відвідали його з іншого маршруту, нас не обдурять.


1

Щойно це запитання було в інтерв’ю Google.

Топологічне сортування

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

Рекурсивне видалення листя

Рекурсивно видаляйте листові вузли, поки їх не залишиться, і якщо залишилося більше одного вузла, у вас буде цикл. Якщо я не помиляюсь, це O (V ^ 2 + VE).

DFS-стиль ~ O (n + m)

Однак ефективним алгоритмом DFS-esque, найгірший випадок O (V + E), є:

function isAcyclic (root) {
    const previous = new Set();

    function DFS (node) {
        previous.add(node);

        let isAcyclic = true;
        for (let child of children) {
            if (previous.has(node) || DFS(child)) {
                isAcyclic = false;
                break;
            }
        }

        previous.delete(node);

        return isAcyclic;
    }

    return DFS(root);
}

0

Ось моя рубінова реалізація алгоритму відшарування листового вузла .

def detect_cycles(initial_graph, number_of_iterations=-1)
    # If we keep peeling off leaf nodes, one of two things will happen
    # A) We will eventually peel off all nodes: The graph is acyclic.
    # B) We will get to a point where there is no leaf, yet the graph is not empty: The graph is cyclic.
    graph = initial_graph
    iteration = 0
    loop do
        iteration += 1
        if number_of_iterations > 0 && iteration > number_of_iterations
            raise "prevented infinite loop"
        end

        if graph.nodes.empty?
            #puts "the graph is without cycles"
            return false
        end

        leaf_nodes = graph.nodes.select { |node| node.leaving_edges.empty? }

        if leaf_nodes.empty?
            #puts "the graph contain cycles"
            return true
        end

        nodes2 = graph.nodes.reject { |node| leaf_nodes.member?(node) }
        edges2 = graph.edges.reject { |edge| leaf_nodes.member?(edge.destination) }
        graph = Graph.new(nodes2, edges2)
    end
    raise "should not happen"
end

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