Wie überprüfe ich, ob ein gerichteter Graph azyklisch ist? Und wie heißt der Algorithmus? Ich würde mich über eine Referenz freuen.
Wie überprüfe ich, ob ein gerichteter Graph azyklisch ist? Und wie heißt der Algorithmus? Ich würde mich über eine Referenz freuen.
Antworten:
Ich würde versuchen, das Diagramm topologisch zu sortieren , und wenn Sie nicht können, dann hat es Zyklen.
Eine einfache Tiefensuche ist nicht gut genug, um einen Zyklus zu finden. Es ist möglich, einen Knoten in einer DFS mehrmals zu besuchen, ohne dass ein Zyklus vorhanden ist. Je nachdem, wo Sie beginnen, besuchen Sie möglicherweise auch nicht das gesamte Diagramm.
Sie können in einer verbundenen Komponente eines Diagramms wie folgt nach Zyklen suchen. Suchen Sie einen Knoten, der nur ausgehende Kanten hat. Wenn es keinen solchen Knoten gibt, gibt es einen Zyklus. Starten Sie an diesem Knoten eine DFS. Überprüfen Sie beim Durchlaufen jeder Kante, ob die Kante auf einen Knoten zurückweist, der sich bereits auf Ihrem Stapel befindet. Dies zeigt die Existenz eines Zyklus an. Wenn Sie keine solche Kante finden, gibt es in dieser verbundenen Komponente keine Zyklen.
Wie Rutger Prins betont, müssen Sie die Suche für jede verbundene Komponente wiederholen, wenn Ihr Diagramm nicht verbunden ist.
Als Referenz ist Tarjans stark verbundener Komponentenalgorithmus eng verwandt. Es hilft Ihnen auch dabei, die Zyklen zu finden und nicht nur zu melden, ob sie vorhanden sind.
In Lemma 22.11 über das Buch Introduction to Algorithms
(2. Auflage) heißt es:
Ein gerichteter Graph G ist genau dann azyklisch, wenn eine Tiefensuche von G keine Hinterkanten ergibt
Lösung 1 : Kahn-Algorithmus zur Überprüfung des Zyklus . Hauptidee: Pflegen Sie eine Warteschlange, in der Knoten mit einem Grad von Null in die Warteschlange aufgenommen werden. Ziehen Sie dann den Knoten nacheinander ab, bis die Warteschlange leer ist. Überprüfen Sie, ob die In-Kanten eines Knotens vorhanden sind.
Lösung 2 : Tarjan-Algorithmus zur Überprüfung der stark verbundenen Komponente.
Lösung 3 : DFS . Verwenden Sie ein Integer-Array, um den aktuellen Status des Knotens zu kennzeichnen: dh 0 - bedeutet, dass dieser Knoten zuvor noch nicht besucht wurde. -1 - bedeutet, dass dieser Knoten besucht wurde und seine untergeordneten Knoten besucht werden. 1 - bedeutet, dass dieser Knoten besucht wurde und fertig ist. Wenn der Status eines Knotens während der DFS -1 ist, bedeutet dies, dass ein Zyklus vorhanden sein muss.
Die von ShuggyCoUk angegebene Lösung ist unvollständig, da möglicherweise nicht alle Knoten überprüft werden.
def isDAG(nodes V):
while there is an unvisited node v in V:
bool cycleFound = dfs(v)
if cyclefound:
return false
return true
Dies hat die Zeitkomplexität O (n + m) oder O (n ^ 2)
m = O(n^2)
da das komplette Diagramm genau m=n^2
Kanten hat. Das ist es also O(n+m) = O(n + n^2) = O(n^2)
.
Ich weiß, dass dies ein altes Thema ist, aber für zukünftige Suchende ist hier eine C # -Implementierung, die ich erstellt habe (kein Anspruch darauf, dass es am effizientesten ist!). Hierbei wird eine einfache Ganzzahl verwendet, um jeden Knoten zu identifizieren. Sie können das nach Belieben dekorieren, vorausgesetzt, Ihr Knotenobjekt hascht und ist gleich.
Bei sehr tiefen Graphen kann dies einen hohen Overhead verursachen, da an jedem Knoten in der Tiefe ein Hashset erstellt wird (diese werden über die Breite zerstört).
Sie geben den Knoten ein, von dem aus Sie suchen möchten, und geben den Pfad zu diesem Knoten an.
Wenn Sie nach Zyklen unterhalb eines bestimmten Knotens suchen, übergeben Sie diesen Knoten einfach zusammen mit einem leeren Hashset
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;
}
Hier ist ein schneller Code, um festzustellen, ob ein Graph Zyklen hat:
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)
Die Idee ist wie folgt: Ein normaler dfs-Algorithmus mit einem Array zur Verfolgung der besuchten Knoten und ein zusätzliches Array, das als Markierung für die Knoten dient, die zum aktuellen Knoten geführt haben, sodass wir jedes Mal ein dfs für einen Knoten ausführen Wir setzen das entsprechende Element im Marker-Array auf true, sodass wir bei jedem Auftreten eines bereits besuchten Knotens prüfen, ob das entsprechende Element im Marker-Array true ist. Wenn es true ist, ist es einer der Knoten, die sich selbst zulassen (daher a Zyklus), und der Trick ist, wenn ein dfs eines Knotens zurückkehrt, setzen wir seinen entsprechenden Marker zurück auf false, damit wir uns nicht täuschen lassen, wenn wir ihn erneut von einer anderen Route aus besuchen.
Hier ist meine Ruby-Implementierung des Peel-Off-Leaf-Node-Algorithmus .
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
Hatte gerade diese Frage in einem Google-Interview.
Sie können versuchen, topologisch zu sortieren. Dies ist O (V + E), wobei V die Anzahl der Eckpunkte und E die Anzahl der Kanten ist. Ein gerichteter Graph ist genau dann azyklisch, wenn dies möglich ist.
Die entfernen rekursiv Blattknoten, bis keine mehr übrig sind. Wenn mehr als ein Knoten übrig ist, haben Sie einen Zyklus. Wenn ich mich nicht irre, ist dies O (V ^ 2 + VE).
Ein effizienter DFS-ähnlicher Algorithmus, Worst-Case O (V + E), ist jedoch:
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);
}
Sie können die Umkehrung des Suchzyklus aus meiner Antwort hier https://stackoverflow.com/a/60196714/1763149 verwenden
def is_acyclic(graph):
return not has_cycle(graph)