Làm cách nào để kiểm tra xem một đồ thị có hướng có phải là hình tròn không?


82

Làm cách nào để kiểm tra xem một đồ thị có hướng có phải là hình tròn không? Và thuật toán được gọi như thế nào? Tôi sẽ đánh giá cao một tài liệu tham khảo.


Một trường hợp khác ủng hộ một số cách để "sửa chữa" các câu trả lời sai trên SO.
Sparr

2
Vì vậy, umm, tôi chủ yếu quan tâm đến thời gian cần thiết để tìm thấy nó. Vì vậy, tôi chỉ cần thuật toán trừu tượng.
nes1983 24/02/09

bạn phải đi qua tất cả các cạnh và kiểm tra tất cả các đỉnh để cận dưới là O (| V | + | E |). DFS và BFS đều cùng phức tạp nhưng DFS là dễ dàng hơn để mã nếu bạn có đệ quy như quản lý ngăn xếp cho bạn ...
ShuggyCoUk

DFS không phức tạp như nhau. Xét đồ thị có các nút {1 .. N} và các cạnh ở dạng {(a, b) | a <b}. Biểu đồ đó là dạng xoay vòng, nhưng DFS sẽ là O (n!)
FryGuy

1
DFS không bao giờ là O (n!). Nó truy cập mỗi nút một lần và mỗi cạnh nhiều nhất hai lần. Vậy O (| V | + | E |) hoặc O (n).
Jay Conrod

Câu trả lời:


95

Tôi sẽ cố gắng sắp xếp biểu đồ theo cấu trúc liên kết , và nếu bạn không thể, thì nó có chu kỳ.


2
Làm thế nào mà điều này không có phiếu bầu ?? Nó tuyến tính trên các nút + cạnh, cao hơn nhiều so với các nghiệm O (n ^ 2)!
Loren Pechtel

5
Trong nhiều trường hợp, DFS (xem câu trả lời của J.Conrod) có thể dễ dàng hơn, đặc biệt nếu DFS vẫn cần được thực hiện. Nhưng tất nhiên điều này phụ thuộc vào ngữ cảnh.
sleske 30/09/09

1
Trật tự topo sẽ được trong một vòng lặp vô hạn nhưng nó sẽ không nói chúng tôi biết nơi chu kỳ xảy ra ...
Baradwaj Aryasomayajula

35

Thực hiện tìm kiếm theo chiều sâu đơn giản là không đủ tốt để tìm ra một chu trình. Có thể truy cập một nút nhiều lần trong DFS mà không có chu kỳ tồn tại. Tùy thuộc vào nơi bạn bắt đầu, bạn cũng có thể không truy cập toàn bộ biểu đồ.

Bạn có thể kiểm tra các chu kỳ trong một thành phần được kết nối của biểu đồ như sau. Tìm một nút chỉ có các cạnh đi ra. Nếu không có nút này, thì có một chu trình. Bắt đầu một DFS tại nút đó. Khi đi qua mỗi cạnh, hãy kiểm tra xem cạnh đó có quay lại một nút đã có trên ngăn xếp của bạn hay không. Điều này cho thấy sự tồn tại của một chu kỳ. Nếu bạn không tìm thấy cạnh nào như vậy, không có chu trình nào trong thành phần được kết nối đó.

Như Rutger Prins đã chỉ ra, nếu đồ thị của bạn không được kết nối, bạn cần lặp lại tìm kiếm trên từng thành phần được kết nối.

Như một tài liệu tham khảo, thuật toán thành phần được kết nối mạnh mẽ của Tarjan có liên quan chặt chẽ. Nó cũng sẽ giúp bạn tìm thấy các chu kỳ, không chỉ báo cáo liệu chúng có tồn tại hay không.


2
BTW: Một cạnh "trỏ trở lại một nút đã có trên ngăn xếp của bạn" thường được gọi là "cạnh sau" trong tài liệu, vì những lý do rõ ràng. Và có, điều này có thể đơn giản hơn việc sắp xếp biểu đồ theo cấu trúc liên kết, đặc biệt nếu bạn vẫn cần thực hiện DFS.
sleske

Để biểu đồ có dạng xoay vòng, bạn nói rằng mỗi thành phần được kết nối phải chứa một nút chỉ có các cạnh đi ra. Bạn có thể đề xuất một thuật toán để tìm các thành phần được kết nối (trái ngược với các thành phần được kết nối "mạnh") của biểu đồ có hướng, để thuật toán chính của bạn sử dụng không?
kostmo

@kostmo, nếu biểu đồ có nhiều hơn một thành phần được kết nối, thì bạn sẽ không truy cập tất cả các nút trong DFS đầu tiên của mình. Theo dõi các nút bạn đã truy cập và lặp lại thuật toán với các nút chưa truy cập cho đến khi bạn tiếp cận tất cả. Về cơ bản đây là cách hoạt động của thuật toán các thành phần được kết nối.
Jay Conrod 22/09/10

6
Mặc dù mục đích của câu trả lời này là đúng, nhưng câu trả lời sẽ gây nhầm lẫn nếu sử dụng triển khai DFS dựa trên ngăn xếp: ngăn xếp được sử dụng để triển khai DFS sẽ không chứa các phần tử chính xác để kiểm tra. Cần phải thêm một ngăn xếp bổ sung vào thuật toán được sử dụng để theo dõi tập hợp các nút tổ tiên.
Theodore Murdock

Tôi có nhiều câu hỏi về câu trả lời của bạn. Tôi đã đăng chúng ở đây: stackoverflow.com/questions/37582599/…
Ari

14

Bổ đề 22.11 trong Sách Introduction to Algorithms(Tái bản lần thứ hai) nói rằng:

Đồ thị có hướng G là xoay chiều nếu và chỉ khi tìm kiếm theo chiều sâu về G không mang lại cạnh sau


1
Về cơ bản đây chỉ là phiên bản viết tắt của câu trả lời của Jay Conrod :-).
sleske 30/09/09

Một trong những vấn đề từ cùng một cuốn sách dường như gợi ý rằng có một | V | thuật toán thời gian. Nó được trả lời ở đây: stackoverflow.com/questions/526331/…
Justin

9

Giải pháp1Thuật toán Kahn để kiểm tra chu kỳ . Ý tưởng chính: Duy trì một hàng đợi trong đó nút có độ bằng 0 sẽ được thêm vào hàng đợi. Sau đó bóc từng nút một cho đến khi hàng đợi trống. Kiểm tra xem có tồn tại trong các cạnh của nút nào không.

Giải pháp 2 : Thuật toán Tarjan để kiểm tra Thành phần được kết nối mạnh mẽ.

Giải pháp 3 : DFS . Sử dụng mảng số nguyên để gắn thẻ trạng thái hiện tại của nút: tức là 0 - nghĩa là nút này chưa được truy cập trước đây. -1 - nghĩa là nút này đã được truy cập và các nút con của nó đang được truy cập. 1 - có nghĩa là nút này đã được truy cập và nó đã hoàn tất. Vì vậy, nếu trạng thái của một nút là -1 trong khi thực hiện DFS, điều đó có nghĩa là phải có một chu trình tồn tại.


1

Giải pháp do ShuggyCoUk đưa ra không hoàn chỉnh vì nó có thể không kiểm tra tất cả các nút.


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

Điều này có thời gian giãn cách O (n + m) hoặc O (n ^ 2)


tôi thực sự không chính xác - Tôi đã xóa nó mặc dù vậy của bạn bây giờ có vẻ một chút ra khỏi bối cảnh
ShuggyCoUk

3
O (n + m) <= O (n + n) = O (2n), O (2n)! = O (n ^ 2)
Artru

@Artru O (n ^ 2) khi sử dụng ma trận kề, O (n + m) khi sử dụng danh sách kề để biểu diễn đồ thị.
0x450

Ừm ... m = O(n^2)vì đồ thị hoàn chỉnh có m=n^2các cạnh chính xác . Vậy đó O(n+m) = O(n + n^2) = O(n^2).
Alex Reinking vào

1

Tôi biết đây là một chủ đề cũ nhưng đối với những người tìm kiếm trong tương lai, đây là một triển khai C # mà tôi đã tạo (không có khẳng định rằng nó hiệu quả nhất!). Điều này được thiết kế để sử dụng một số nguyên đơn giản để xác định mỗi nút. Bạn có thể trang trí theo cách bạn thích miễn là đối tượng nút của bạn được băm và cân bằng đúng cách.

Đối với đồ thị rất sâu, điều này có thể có chi phí cao, vì nó tạo ra một bộ băm ở mỗi nút ở độ sâu (chúng bị phá hủy theo chiều rộng).

Bạn nhập nút mà bạn muốn tìm kiếm từ đó và đường dẫn đến nút đó.

  • Đối với một biểu đồ có một nút gốc, bạn gửi nút đó và một bộ băm trống
  • Đối với một biểu đồ có nhiều nút gốc, bạn bao bọc điều này trong một phần trước trên các nút đó và chuyển một bộ băm trống mới cho mỗi lần lặp
  • Khi kiểm tra các chu kỳ bên dưới bất kỳ nút nhất định nào, chỉ cần chuyển nút đó cùng với một bộ băm trống

    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

Không được có bất kỳ cạnh sau nào trong khi thực hiện DFS. Giữ theo dõi các nút đã được truy cập trong khi thực hiện DFS, nếu bạn gặp một cạnh giữa nút hiện tại và nút hiện tại, thì biểu đồ có chu kỳ.


1

đây là một mã nhanh để tìm xem một biểu đồ có chu kỳ hay không:

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)

Ý tưởng là như thế này: một thuật toán dfs bình thường với một mảng để theo dõi các nút đã truy cập và một mảng bổ sung đóng vai trò là điểm đánh dấu cho các nút dẫn đến nút hiện tại, để khi nào chúng ta thực thi một dfs cho một nút chúng tôi đặt mục tương ứng của nó trong mảng đánh dấu là true, để khi nào một nút đã được truy cập gặp phải, chúng tôi kiểm tra xem mục tương ứng của nó trong mảng đánh dấu có đúng không, nếu nó đúng thì một trong các nút cho phép chính nó (do đó a chu kỳ), và mẹo là bất cứ khi nào một dfs của một nút trả về, chúng tôi đặt điểm đánh dấu tương ứng của nó trở lại thành false, để nếu chúng tôi truy cập lại nó từ một tuyến đường khác, chúng tôi sẽ không bị lừa.


0

Đây là phần triển khai ruby ​​của tôi về thuật toán nút lá bóc .

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

0

Chỉ có câu hỏi này trong một cuộc phỏng vấn của Google.

Sắp xếp theo cấu trúc liên kết

Bạn có thể cố gắng sắp xếp theo cấu trúc liên kết, đó là O (V + E) trong đó V là số đỉnh và E là số cạnh. Một đồ thị có hướng là xoay chiều nếu và chỉ khi điều này có thể được thực hiện.

Loại bỏ lá đệ quy

Loại bỏ đệ quy các nút lá cho đến khi không còn nút nào và nếu còn lại nhiều nút đơn thì bạn sẽ có một chu kỳ. Trừ khi tôi nhầm, đây là O (V ^ 2 + VE).

Kiểu DFS ~ O (n + m)

Tuy nhiên, một thuật toán DFS-esque hiệu quả, trường hợp xấu nhất là O (V + E), là:

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);
}

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.