Graph-Algorithmus zum Finden aller Verbindungen zwischen zwei beliebigen Scheitelpunkten


117

Ich versuche, den besten zeiteffizienten Algorithmus zu ermitteln, um die unten beschriebene Aufgabe zu erfüllen.

Ich habe eine Reihe von Aufzeichnungen. Für diesen Datensatz habe ich Verbindungsdaten, die angeben, wie Paare von Datensätzen aus diesem Satz miteinander verbunden sind. Dies stellt im Wesentlichen einen ungerichteten Graphen dar, wobei die Datensätze die Eckpunkte und die Verbindungsdaten die Kanten sind.

Alle Datensätze im Satz enthalten Verbindungsinformationen (dh es sind keine verwaisten Datensätze vorhanden; jeder Datensatz im Satz stellt eine Verbindung zu einem oder mehreren anderen Datensätzen im Satz her).

Ich möchte zwei beliebige Datensätze aus dem Satz auswählen und alle einfachen Pfade zwischen den ausgewählten Datensätzen anzeigen können. Mit "einfachen Pfaden" meine ich die Pfade, die keine wiederholten Datensätze im Pfad haben (dh nur endliche Pfade).

Hinweis: Die beiden ausgewählten Datensätze sind immer unterschiedlich (dh Start- und Endscheitelpunkt sind niemals gleich; keine Zyklen).

Beispielsweise:

    Wenn ich folgende Unterlagen habe:
        A, B, C, D, E.

    und das Folgende repräsentiert die Verbindungen: 
        (A, B), (A, C), (B, A), (B, D), (B, E), (B, F), (C, A), (C, E),
        (C, F), (D, B), (E, C), (E, F), (F, B), (F, C), (F, E)

        [wobei (A, B) bedeutet, dass Datensatz A mit Datensatz B verbunden ist]

Wenn ich B als Startdatensatz und E als Enddatensatz wählen würde, würde ich alle einfachen Pfade durch die Datensatzverbindungen finden wollen, die Datensatz B mit Datensatz E verbinden würden.

   Alle Pfade, die B mit E verbinden:
      B-> E.
      B-> F-> E.
      B-> F-> C-> E.
      B-> A-> C-> E.
      B-> A-> C-> F-> E.

Dies ist ein Beispiel, in der Praxis kann ich Sätze haben, die Hunderttausende von Datensätzen enthalten.


Die Verbindungen werden Zyklen genannt , und diese Antwort enthält viele Informationen für Sie.
Elhoim

3
Bitte geben Sie an, ob Sie eine endliche Liste von schleifenfreien Verbindungen oder einen unendlichen Strom von Verbindungen mit allen möglichen Schleifen wünschen. Vgl. Blorgbeards Antwort.
Charles Stewart

kann mir jemand dabei helfen ??? stackoverflow.com/questions/32516706/…
tejas3006

Antworten:


116

Es scheint, dass dies mit einer Tiefensuche des Graphen erreicht werden kann. Bei der Tiefensuche werden alle nicht zyklischen Pfade zwischen zwei Knoten gefunden. Dieser Algorithmus sollte sehr schnell sein und auf große Diagramme skaliert werden können (Die Diagrammdatenstruktur ist spärlich, sodass nur so viel Speicher benötigt wird, wie benötigt wird).

Mir ist aufgefallen, dass das oben angegebene Diagramm nur eine Kante hat, die gerichtet ist (B, E). War das ein Tippfehler oder ist es wirklich ein gerichteter Graph? Diese Lösung funktioniert unabhängig davon. Entschuldigung, ich konnte es in C nicht machen, ich bin ein bisschen schwach in diesem Bereich. Ich gehe jedoch davon aus, dass Sie diesen Java-Code ohne allzu große Probleme übersetzen können.

Graph.java:

import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;

public class Graph {
    private Map<String, LinkedHashSet<String>> map = new HashMap();

    public void addEdge(String node1, String node2) {
        LinkedHashSet<String> adjacent = map.get(node1);
        if(adjacent==null) {
            adjacent = new LinkedHashSet();
            map.put(node1, adjacent);
        }
        adjacent.add(node2);
    }

    public void addTwoWayVertex(String node1, String node2) {
        addEdge(node1, node2);
        addEdge(node2, node1);
    }

    public boolean isConnected(String node1, String node2) {
        Set adjacent = map.get(node1);
        if(adjacent==null) {
            return false;
        }
        return adjacent.contains(node2);
    }

    public LinkedList<String> adjacentNodes(String last) {
        LinkedHashSet<String> adjacent = map.get(last);
        if(adjacent==null) {
            return new LinkedList();
        }
        return new LinkedList<String>(adjacent);
    }
}

Search.java:

import java.util.LinkedList;

public class Search {

    private static final String START = "B";
    private static final String END = "E";

    public static void main(String[] args) {
        // this graph is directional
        Graph graph = new Graph();
        graph.addEdge("A", "B");
        graph.addEdge("A", "C");
        graph.addEdge("B", "A");
        graph.addEdge("B", "D");
        graph.addEdge("B", "E"); // this is the only one-way connection
        graph.addEdge("B", "F");
        graph.addEdge("C", "A");
        graph.addEdge("C", "E");
        graph.addEdge("C", "F");
        graph.addEdge("D", "B");
        graph.addEdge("E", "C");
        graph.addEdge("E", "F");
        graph.addEdge("F", "B");
        graph.addEdge("F", "C");
        graph.addEdge("F", "E");
        LinkedList<String> visited = new LinkedList();
        visited.add(START);
        new Search().depthFirst(graph, visited);
    }

    private void depthFirst(Graph graph, LinkedList<String> visited) {
        LinkedList<String> nodes = graph.adjacentNodes(visited.getLast());
        // examine adjacent nodes
        for (String node : nodes) {
            if (visited.contains(node)) {
                continue;
            }
            if (node.equals(END)) {
                visited.add(node);
                printPath(visited);
                visited.removeLast();
                break;
            }
        }
        for (String node : nodes) {
            if (visited.contains(node) || node.equals(END)) {
                continue;
            }
            visited.addLast(node);
            depthFirst(graph, visited);
            visited.removeLast();
        }
    }

    private void printPath(LinkedList<String> visited) {
        for (String node : visited) {
            System.out.print(node);
            System.out.print(" ");
        }
        System.out.println();
    }
}

Programmausgabe:

B E 
B A C E 
B A C F E 
B F E 
B F C E 

5
Bitte beachten Sie, dass dies keine erste Durchquerung ist. Mit der Breite zuerst besuchen Sie zuerst alle Knoten mit Abstand 0 zur Wurzel, dann diejenigen mit Abstand 1, dann 2 usw.
mweerden

14
Richtig, das ist eine DFS. Ein BFS müsste eine Warteschlange verwenden und Level- (N + 1) -Knoten in die Warteschlange stellen, um nach allen Level-N-Knoten verarbeitet zu werden. Für die Zwecke des OP funktioniert jedoch entweder BFS oder DFS, da keine bevorzugte Sortierreihenfolge für Pfade angegeben ist.
Matt J

1
Casey, ich habe seit Ewigkeiten nach einer Lösung für dieses Problem gesucht. Ich habe vor kurzem diese DFS in C ++ implementiert und es funktioniert ein Vergnügen.
AndyUK

6
Der Nachteil der Rekursion ist, wenn Sie ein tiefes Diagramm (A-> B-> C -> ...-> N) haben, könnten Sie StackOverflowError in Java haben.
Rrr

1
Ich habe unten in C # eine iterative Version hinzugefügt.
Batta

23

Das Online-Wörterbuch für Algorithmen und Datenstrukturen des Nationalen Instituts für Standards und Technologie (NIST) listet dieses Problem als " alle einfachen Pfade" auf und empfiehlt eine Tiefensuche . CLRS liefert die relevanten Algorithmen.

Eine clevere Technik mit Petri-Netzen finden Sie hier


2
Könnten Sie mir mit einer besseren Lösung helfen? Die Ausführung einer DFS dauert ewig : stackoverflow.com/q/8342101/632951
Pacerier

Beachten Sie, dass es einfach ist, Diagramme zu erstellen, für die DFS sehr ineffizient ist, obwohl die Menge aller einfachen Pfade zwischen den beiden Knoten klein und leicht zu finden ist. Stellen Sie sich zum Beispiel einen ungerichteten Graphen vor, in dem der Startknoten A zwei Nachbarn hat: den Zielknoten B (der keine anderen Nachbarn als A hat) und einen Knoten C, der Teil einer vollständig verbundenen Clique von n + 1 Knoten ist. Obwohl es eindeutig nur einen einfachen Weg von A nach B gibt, wird eine naive DFS O ( n !) Zeit verschwenden, um die Clique nutzlos zu erkunden. Ähnliche Beispiele (eine Lösung, DFS benötigt exponentielle Zeit) finden sich auch bei DAGs.
Ilmari Karonen

Das NIST sagt: "Die Pfade können mit einer Tiefensuche aufgelistet werden."
chomp

13

Hier ist der Pseudocode, den ich mir ausgedacht habe. Dies ist kein bestimmter Pseudocode-Dialekt, sollte aber einfach genug sein, um zu folgen.

Jeder möchte das auseinander nehmen.

  • [p] ist eine Liste von Eckpunkten, die den aktuellen Pfad darstellen.

  • [x] ist eine Liste von Pfaden, bei denen die Kriterien erfüllt sind

  • [s] ist der Quellscheitelpunkt

  • [d] ist der Zielscheitelpunkt

  • [c] ist der aktuelle Scheitelpunkt (Argument für die PathFind-Routine)

Angenommen, es gibt eine effiziente Möglichkeit, die benachbarten Scheitelpunkte nachzuschlagen (Zeile 6).

     1 PathList [p]
     2 ListOfPathLists [x]
     3 Vertex [s], [d]

     4 PathFind (Vertex [c])
     5 Fügen Sie [c] zum Ende der Liste hinzu [p]
     6 Für jeden Scheitelpunkt [v] neben [c]
     7 Wenn [v] gleich [d] ist, dann
     8 Speichern Sie die Liste [p] in [x]
     9 Sonst, wenn [v] nicht in Liste [p] enthalten ist
    10 PathFind ([v])
    11 Weiter für
    12 Entfernen Sie den Schwanz von [p]
    13 Zurück

Können Sie bitte etwas Licht auf Schritt 11 und Schritt 12 werfen
Bozo-Benutzer

Zeile 11 bezeichnet nur den Endblock, der zur For-Schleife gehört, die in Zeile 6 beginnt. Zeile 12 bedeutet, das letzte Element der Pfadliste zu entfernen, bevor zum Aufrufer zurückgekehrt wird.
Robert Groves

Was ist der erste Aufruf von PathFind - übergeben Sie die Quellscheitelpunkte?
Bozo Benutzer

In diesem Beispiel ja, aber denken Sie daran, dass Sie möglicherweise keinen echten Code schreiben möchten, der diesem Pseudocode eins zu eins zuordnet. Es soll eher einen Denkprozess veranschaulichen als gut gestalteten Code.
Robert Groves

8

Da die in dieser Antwort angegebene nicht rekursive DFS-Implementierung fehlerhaft zu sein scheint, möchte ich eine bereitstellen, die tatsächlich funktioniert.

Ich habe dies in Python geschrieben, weil ich es durch Implementierungsdetails ziemlich lesbar und übersichtlich finde (und weil es das praktische yieldSchlüsselwort zum Implementieren von Generatoren enthält ), aber es sollte ziemlich einfach sein, es in andere Sprachen zu portieren.

# a generator function to find all simple paths between two nodes in a
# graph, represented as a dictionary that maps nodes to their neighbors
def find_simple_paths(graph, start, end):
    visited = set()
    visited.add(start)

    nodestack = list()
    indexstack = list()
    current = start
    i = 0

    while True:
        # get a list of the neighbors of the current node
        neighbors = graph[current]

        # find the next unvisited neighbor of this node, if any
        while i < len(neighbors) and neighbors[i] in visited: i += 1

        if i >= len(neighbors):
            # we've reached the last neighbor of this node, backtrack
            visited.remove(current)
            if len(nodestack) < 1: break  # can't backtrack, stop!
            current = nodestack.pop()
            i = indexstack.pop()
        elif neighbors[i] == end:
            # yay, we found the target node! let the caller process the path
            yield nodestack + [current, end]
            i += 1
        else:
            # push current node and index onto stacks, switch to neighbor
            nodestack.append(current)
            indexstack.append(i+1)
            visited.add(neighbors[i])
            current = neighbors[i]
            i = 0

Dieser Code verwaltet zwei parallele Stapel: einen mit den früheren Knoten im aktuellen Pfad und einen mit dem aktuellen Nachbarindex für jeden Knoten im Knotenstapel (sodass wir die Iteration durch die Nachbarn eines Knotens fortsetzen können, wenn wir ihn wieder entfernen der Stapel). Ich hätte genauso gut einen einzelnen Stapel von (Knoten-, Index-) Paaren verwenden können, aber ich dachte, die Zwei-Stapel-Methode wäre besser lesbar und für Benutzer anderer Sprachen möglicherweise einfacher zu implementieren.

Dieser Code verwendet auch einen separaten visitedSatz, der immer den aktuellen Knoten und alle Knoten auf dem Stapel enthält, damit ich effizient prüfen kann, ob ein Knoten bereits Teil des aktuellen Pfads ist. Wenn Ihre Sprache zufällig über eine Datenstruktur "geordneter Satz" verfügt, die sowohl effiziente stapelähnliche Push / Pop-Operationen als auch effiziente Mitgliedschaftsabfragen bietet, können Sie diese für den Knotenstapel verwenden und den separaten visitedSatz entfernen.

Wenn Sie alternativ eine benutzerdefinierte veränderbare Klasse / Struktur für Ihre Knoten verwenden, können Sie einfach ein boolesches Flag in jedem Knoten speichern, um anzugeben, ob es als Teil des aktuellen Suchpfads besucht wurde. Natürlich können Sie mit dieser Methode nicht zwei Suchvorgänge im selben Diagramm gleichzeitig ausführen, falls Sie dies aus irgendeinem Grund wünschen.

Hier ist ein Testcode, der zeigt, wie die oben angegebene Funktion funktioniert:

# test graph:
#     ,---B---.
#     A   |   D
#     `---C---'
graph = {
    "A": ("B", "C"),
    "B": ("A", "C", "D"),
    "C": ("A", "B", "D"),
    "D": ("B", "C"),
}

# find paths from A to D
for path in find_simple_paths(graph, "A", "D"): print " -> ".join(path)

Wenn Sie diesen Code in dem angegebenen Beispieldiagramm ausführen, wird die folgende Ausgabe ausgegeben:

A -> B -> C -> D.
A -> B -> D.
A -> C -> B -> D.
A -> C -> D.

Beachten Sie, dass dieses Beispieldiagramm zwar ungerichtet ist (dh alle Kanten in beide Richtungen verlaufen), der Algorithmus jedoch auch für beliebig gerichtete Diagramme funktioniert. Wenn Sie beispielsweise die C -> BKante entfernen (indem Sie sie Baus der Nachbarliste von entfernen C), erhalten Sie dieselbe Ausgabe, mit Ausnahme des dritten Pfads ( A -> C -> B -> D), der nicht mehr möglich ist.


Ps. Es ist einfach, Diagramme zu erstellen, für die einfache Suchalgorithmen wie dieser (und die anderen in diesem Thread) sehr schlecht funktionieren.

Betrachten Sie beispielsweise die Aufgabe, alle Pfade von A nach B in einem ungerichteten Diagramm zu finden, in dem der Startknoten A zwei Nachbarn hat: den Zielknoten B (der keine anderen Nachbarn als A hat) und einen Knoten C, der Teil einer Clique ist von n + 1 Knoten, wie folgt:

graph = {
    "A": ("B", "C"),
    "B": ("A"),
    "C": ("A", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O"),
    "D": ("C", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O"),
    "E": ("C", "D", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O"),
    "F": ("C", "D", "E", "G", "H", "I", "J", "K", "L", "M", "N", "O"),
    "G": ("C", "D", "E", "F", "H", "I", "J", "K", "L", "M", "N", "O"),
    "H": ("C", "D", "E", "F", "G", "I", "J", "K", "L", "M", "N", "O"),
    "I": ("C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "O"),
    "J": ("C", "D", "E", "F", "G", "H", "I", "K", "L", "M", "N", "O"),
    "K": ("C", "D", "E", "F", "G", "H", "I", "J", "L", "M", "N", "O"),
    "L": ("C", "D", "E", "F", "G", "H", "I", "J", "K", "M", "N", "O"),
    "M": ("C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "N", "O"),
    "N": ("C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "O"),
    "O": ("C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N"),
}

Es ist leicht zu erkennen, dass der einzige Pfad zwischen A und B der direkte ist, aber eine naive DFS, die von Knoten A aus gestartet wird, verschwendet O ( n !) Zeit, um Pfade innerhalb der Clique nutzlos zu erkunden, obwohl dies (für einen Menschen) offensichtlich ist Keiner dieser Pfade kann möglicherweise zu B führen.

Man kann auch DAGs mit ähnlichen Eigenschaften konstruieren , z. B. indem der Startknoten A den Zielknoten B und zwei andere Knoten C 1 und C 2 verbindet , die beide mit den Knoten D 1 und D 2 verbunden sind , die beide mit E verbunden sind 1 und E 2 und so weiter. Für n Schichten von Knoten, die so angeordnet sind, verschwendet eine naive Suche nach allen Pfaden von A nach B O (2 n ) Zeit, um alle möglichen Sackgassen zu untersuchen, bevor sie aufgegeben wird.

Das Hinzufügen einer Kante zum Zielknoten B von einem der Knoten in der Clique (außer C) oder von der letzten Schicht der DAG würde natürlich eine exponentiell große Anzahl möglicher Pfade von A nach B und a erzeugen Ein rein lokaler Suchalgorithmus kann nicht wirklich im Voraus sagen, ob er eine solche Kante findet oder nicht. In gewissem Sinne ist die schlechte Ausgabesensitivität solcher naiven Suchvorgänge darauf zurückzuführen, dass sie sich der globalen Struktur des Graphen nicht bewusst sind.

Zwar gibt es verschiedene Vorverarbeitungsmethoden (z. B. iteratives Entfernen von Blattknoten, Suchen nach Scheitelpunkttrennzeichen für einzelne Knoten usw.), mit denen einige dieser "Sackgassen in Exponentialzeit" vermieden werden können, aber ich kenne keine allgemeinen Vorverarbeitungstrick, der sie in allen Fällen beseitigen könnte . Eine allgemeine Lösung wäre, bei jedem Schritt der Suche zu überprüfen, ob der Zielknoten noch erreichbar ist (mithilfe einer Untersuche), und frühzeitig zurückzuverfolgen, wenn dies nicht der Fall ist - aber leider würde dies die Suche erheblich verlangsamen (im schlimmsten Fall) (proportional zur Größe des Diagramms) für viele Diagramme, die keine solchen pathologischen Sackgassen enthalten.


1
Das ist, was ich suche, danke :)
Arslan

Vielen Dank für Ihre nicht rekursive DFS-Lösung. Nur beachten Sie die letzte Zeile das Drucken des einen Syntaxfehler hat, sollte sein for path in find_simple_paths(graph, "A", "D"): print(" -> ".join(path)), das printfehlte die Klammer.
David Oliván Ubieto

1
@ DavidOlivánUbieto: Es ist Python 2-Code, deshalb gibt es keine Klammern. :)
Ilmari Karonen

5

Hier ist eine logisch besser aussehende rekursive Version im Vergleich zum zweiten Stock.

public class Search {

private static final String START = "B";
private static final String END = "E";

public static void main(String[] args) {
    // this graph is directional
    Graph graph = new Graph();
    graph.addEdge("A", "B");
    graph.addEdge("A", "C");
    graph.addEdge("B", "A");
    graph.addEdge("B", "D");
    graph.addEdge("B", "E"); // this is the only one-way connection
    graph.addEdge("B", "F");
    graph.addEdge("C", "A");
    graph.addEdge("C", "E");
    graph.addEdge("C", "F");
    graph.addEdge("D", "B");
    graph.addEdge("E", "C");
    graph.addEdge("E", "F");
    graph.addEdge("F", "B");
    graph.addEdge("F", "C");
    graph.addEdge("F", "E");
    List<ArrayList<String>> paths = new ArrayList<ArrayList<String>>();
    String currentNode = START;
    List<String> visited = new ArrayList<String>();
    visited.add(START);
    new Search().findAllPaths(graph, seen, paths, currentNode);
    for(ArrayList<String> path : paths){
        for (String node : path) {
            System.out.print(node);
            System.out.print(" ");
        }
        System.out.println();
    }   
}

private void findAllPaths(Graph graph, List<String> visited, List<ArrayList<String>> paths, String currentNode) {        
    if (currentNode.equals(END)) { 
        paths.add(new ArrayList(Arrays.asList(visited.toArray())));
        return;
    }
    else {
        LinkedList<String> nodes = graph.adjacentNodes(currentNode);    
        for (String node : nodes) {
            if (visited.contains(node)) {
                continue;
            } 
            List<String> temp = new ArrayList<String>();
            temp.addAll(visited);
            temp.add(node);          
            findAllPaths(graph, temp, paths, node);
        }
    }
}
}

Programmausgabe

B A C E 

B A C F E 

B E

B F C E

B F E 

4

Lösung in C-Code. Es basiert auf DFS, das minimalen Speicher benötigt.

#include <stdio.h>
#include <stdbool.h>

#define maxN    20  

struct  nodeLink
{

    char node1;
    char node2;

};

struct  stack
{   
    int sp;
    char    node[maxN];
};   

void    initStk(stk)
struct  stack   *stk;
{
    int i;
    for (i = 0; i < maxN; i++)
        stk->node[i] = ' ';
    stk->sp = -1;   
}

void    pushIn(stk, node)
struct  stack   *stk;
char    node;
{

    stk->sp++;
    stk->node[stk->sp] = node;

}    

void    popOutAll(stk)
struct  stack   *stk;
{

    char    node;
    int i, stkN = stk->sp;

    for (i = 0; i <= stkN; i++)
    {
        node = stk->node[i];
        if (i == 0)
            printf("src node : %c", node);
        else if (i == stkN)
            printf(" => %c : dst node.\n", node);
        else
            printf(" => %c ", node);
    }

}


/* Test whether the node already exists in the stack    */
bool    InStack(stk, InterN)
struct  stack   *stk;
char    InterN;
{

    int i, stkN = stk->sp;  /* 0-based  */
    bool    rtn = false;    

    for (i = 0; i <= stkN; i++)
    {
        if (stk->node[i] == InterN)
        {
            rtn = true;
            break;
        }
    }

    return     rtn;

}

char    otherNode(targetNode, lnkNode)
char    targetNode;
struct  nodeLink    *lnkNode;
{

    return  (lnkNode->node1 == targetNode) ? lnkNode->node2 : lnkNode->node1;

}

int entries = 8;
struct  nodeLink    topo[maxN]    =       
    {
        {'b', 'a'}, 
        {'b', 'e'}, 
        {'b', 'd'}, 
        {'f', 'b'}, 
        {'a', 'c'},
        {'c', 'f'}, 
        {'c', 'e'},
        {'f', 'e'},               
    };

char    srcNode = 'b', dstN = 'e';      

int reachTime;  

void    InterNode(interN, stk)
char    interN;
struct  stack   *stk;
{

    char    otherInterN;
    int i, numInterN = 0;
    static  int entryTime   =   0;

    entryTime++;

    for (i = 0; i < entries; i++)
    {

        if (topo[i].node1 != interN  && topo[i].node2 != interN) 
        {
            continue;   
        }

        otherInterN = otherNode(interN, &topo[i]);

        numInterN++;

        if (otherInterN == stk->node[stk->sp - 1])
        {
            continue;   
        }

        /*  Loop avoidance: abandon the route   */
        if (InStack(stk, otherInterN) == true)
        {
            continue;   
        }

        pushIn(stk, otherInterN);

        if (otherInterN == dstN)
        {
            popOutAll(stk);
            reachTime++;
            stk->sp --;   /*    back trace one node  */
            continue;
        }
        else
            InterNode(otherInterN, stk);

    }

        stk->sp --;

}


int    main()

{

    struct  stack   stk;

    initStk(&stk);
    pushIn(&stk, srcNode);  

    reachTime = 0;
    InterNode(srcNode, &stk);

    printf("\nNumber of all possible and unique routes = %d\n", reachTime);

}

2

Dies mag spät sein, aber hier ist dieselbe C # -Version des DFS-Algorithmus in Java von Casey, um alle Pfade zwischen zwei Knoten mithilfe eines Stapels zu durchlaufen. Die Lesbarkeit ist mit rekursiv wie immer besser.

    void DepthFirstIterative(T start, T endNode)
    {
        var visited = new LinkedList<T>();
        var stack = new Stack<T>();

        stack.Push(start);

        while (stack.Count != 0)
        {
            var current = stack.Pop();

            if (visited.Contains(current))
                continue;

            visited.AddLast(current);

            var neighbours = AdjacentNodes(current);

            foreach (var neighbour in neighbours)
            {
                if (visited.Contains(neighbour))
                    continue;

                if (neighbour.Equals(endNode))
                {
                    visited.AddLast(neighbour);
                    printPath(visited));
                    visited.RemoveLast();
                    break;
                }
            }

            bool isPushed = false;
            foreach (var neighbour in neighbours.Reverse())
            {
                if (neighbour.Equals(endNode) || visited.Contains(neighbour) || stack.Contains(neighbour))
                {
                    continue;
                }

                isPushed = true;
                stack.Push(neighbour);
            }

            if (!isPushed)
                visited.RemoveLast();
        }
    }
Dies ist ein Beispieldiagramm zum Testen:

    // Beispieldiagramm. Zahlen sind Kanten-IDs
    // 1 3       
    // A --- B --- C ----
    // | | 2 |
    // | 4 ----- D |
    // ------------------

1
Hervorragend - darüber, wie Sie die Rekursion durch eine stapelbasierte Iteration ersetzt haben.
Siddhartha Ghosh

Ich verstehe es immer noch nicht, was ist das neighbours.Reverse()? Ist es List<T>.Reverse ?

Ich habe diese nicht rekursive Version überprüft, aber sie scheint nicht korrekt zu sein. rekursive Version ist in Ordnung. Vielleicht ist ein kleiner Fehler passiert, wenn er auf nicht rekursiv geändert wird
Arslan

@alim: Einverstanden, dieser Code ist einfach kaputt. (Es entfernt Knoten beim Zurückverfolgen nicht korrekt aus dem besuchten Satz, und die Stapelbehandlung scheint ebenfalls durcheinander zu sein. Ich habe versucht zu prüfen, ob sie behoben werden kann, aber das würde im Grunde ein vollständiges Umschreiben erfordern.) Ich habe es gerade getan fügte eine Antwort mit einer korrekten, funktionierenden nicht rekursiven Lösung hinzu (in Python, aber es sollte relativ einfach sein, in andere Sprachen zu portieren).
Ilmari Karonen

@llmari Karonen, Schön, ich werde nachsehen, Großartige Arbeit.
Arslan

1

Ich habe kürzlich ein ähnliches Problem gelöst, anstatt an allen Lösungen, die mich nur am kürzesten interessierten.

Ich habe eine iterative Suche mit der Breite zuerst verwendet, bei der eine Statuswarteschlange verwendet wurde, von der jede einen Datensatz enthielt, der einen aktuellen Punkt in der Grafik und den Weg dorthin enthielt.

Sie beginnen mit einem einzelnen Datensatz in der Warteschlange, der den Startknoten und einen leeren Pfad enthält.

Bei jeder Iteration durch den Code wird das Element vom Kopf der Liste gestrichen und überprüft, ob es sich um eine Lösung handelt (der Knoten ist der gewünschte, wenn dies der Fall ist, wir sind fertig). Andernfalls wird ein neuer erstellt Warteschlangenelement mit den Knoten, die mit dem aktuellen Knoten verbunden sind, und geänderten Pfaden, die auf dem Pfad des vorherigen Knotens basieren, wobei der neue Sprung am Ende angehängt wird.

Jetzt könnten Sie etwas Ähnliches verwenden, aber wenn Sie eine Lösung finden, anstatt anzuhalten, fügen Sie diese Lösung Ihrer "gefundenen Liste" hinzu und fahren Sie fort.

Sie müssen die Liste der besuchten Knoten verfolgen, damit Sie sich nie selbst zurückverfolgen, da Sie sonst eine Endlosschleife haben.

Wenn du ein bisschen mehr Pseudocode willst, poste einen Kommentar oder so, und ich werde es näher erläutern.


6
Ich glaube, wenn Sie nur am kürzesten Weg interessiert sind, dann ist Dijkstras Algorithmus "die Lösung" :).
Vicatcu

1

Ich denke, Sie sollten Ihr eigentliches Problem dahinter beschreiben. Ich sage das, weil Sie nach etwas zeiteffizientem fragen, aber die Antwort auf das Problem scheint exponentiell zu wachsen!

Daher würde ich keinen besseren Algorithmus erwarten als etwas Exponentiales.

Ich würde das ganze Diagramm zurückverfolgen und durchgehen. Speichern Sie alle besuchten Knoten auf dem Weg, um Zyklen zu vermeiden. Wenn Sie zurückkehren, heben Sie die Markierung des Knotens auf.

Rekursion verwenden:

static bool[] visited;//all false
Stack<int> currentway; initialize empty

function findnodes(int nextnode)
{
if (nextnode==destnode)
{
  print currentway 
  return;
}
visited[nextnode]=true;
Push nextnode to the end of currentway.
for each node n accesible from nextnode:
  findnodes(n);
visited[nextnode]=false; 
pop from currenteay
}

Oder ist das falsch?

edit: Oh, und ich habe vergessen: Sie sollten die rekursiven Aufrufe eliminieren, indem Sie diesen Knotenstapel verwenden


Mein eigentliches Problem ist genau wie ich es beschrieben habe, nur mit viel größeren Sets. Ich bin damit einverstanden, dass dies mit der Größe des Sets exponentiell zu wachsen scheint.
Robert Groves

1

Das Grundprinzip ist, dass Sie sich keine Gedanken über Diagramme machen müssen. Dies ist ein Standardproblem, das als Problem der dynamischen Konnektivität bezeichnet wird. Es gibt folgende Arten von Methoden, mit denen Sie erreichen können, dass Knoten verbunden sind oder nicht:

  1. Schnell finden
  2. Schnelle Vereinigung
  3. Verbesserter Algorithmus (Kombination von beiden)

Hier ist der C-Code, den ich mit minimaler Zeitkomplexität ausprobiert habe. O (log * n) Das bedeutet, dass für die 65536-Kantenliste 4 Suchvorgänge und für 2 ^ 65536 5 Suchvorgänge erforderlich sind. Ich teile meine Implementierung mit dem Algorithmus: Algorithm Course der Princeton University

TIPP: Sie finden die Java-Lösung unter dem oben angegebenen Link mit den entsprechenden Erklärungen.

/* Checking Connection Between Two Edges */

#include<stdio.h>
#include<stdlib.h>
#define MAX 100

/*
  Data structure used

vertex[] - used to Store The vertices
size - No. of vertices
sz[] - size of child's
*/

/*Function Declaration */
void initalize(int *vertex, int *sz, int size);
int root(int *vertex, int i);
void add(int *vertex, int *sz, int p, int q);
int connected(int *vertex, int p, int q);

int main() //Main Function
{ 
char filename[50], ch, ch1[MAX];
int temp = 0, *vertex, first = 0, node1, node2, size = 0, *sz;
FILE *fp;


printf("Enter the filename - "); //Accept File Name
scanf("%s", filename);
fp = fopen(filename, "r");
if (fp == NULL)
{
    printf("File does not exist");
    exit(1);
}
while (1)
{
    if (first == 0) //getting no. of vertices
    {
        ch = getc(fp);
        if (temp == 0)
        {
            fseek(fp, -1, 1);
            fscanf(fp, "%s", &ch1);
            fseek(fp, 1, 1);
            temp = 1;
        }
        if (isdigit(ch))
        {
            size = atoi(ch1);
            vertex = (int*) malloc(size * sizeof(int));     //dynamically allocate size  
            sz = (int*) malloc(size * sizeof(int));
            initalize(vertex, sz, size);        //initialization of vertex[] and sz[]
        }
        if (ch == '\n')
        {
            first = 1;
            temp = 0;
        }
    }
    else
    {
        ch = fgetc(fp);
        if (isdigit(ch))
            temp = temp * 10 + (ch - 48);   //calculating value from ch
        else
        {
            /* Validating the file  */

            if (ch != ',' && ch != '\n' && ch != EOF)
            {
                printf("\n\nUnkwown Character Detected.. Exiting..!");

                exit(1);
            }
            if (ch == ',')
                node1 = temp;
            else
            {
                node2 = temp;
                printf("\n\n%d\t%d", node1, node2);
                if (node1 > node2)
                {
                    temp = node1;
                    node1 = node2;
                    node2 = temp;
                }

                /* Adding the input nodes */

                if (!connected(vertex, node1, node2))
                    add(vertex, sz, node1, node2);
            }
            temp = 0;
        }

        if (ch == EOF)
        {
            fclose(fp);
            break;
        }
    }
}

do
{
    printf("\n\n==== check if connected ===");
    printf("\nEnter First Vertex:");
    scanf("%d", &node1);
    printf("\nEnter Second Vertex:");
    scanf("%d", &node2);

    /* Validating The Input */

    if( node1 > size || node2 > size )
    {
        printf("\n\n Invalid Node Value..");
        break;
    }

    /* Checking the connectivity of nodes */

    if (connected(vertex, node1, node2))
        printf("Vertex %d and %d are Connected..!", node1, node2);
    else
        printf("Vertex %d and %d are Not Connected..!", node1, node2);


    printf("\n 0/1:  ");

    scanf("%d", &temp);

} while (temp != 0);

free((void*) vertex);
free((void*) sz);


return 0;
}

void initalize(int *vertex, int *sz, int size) //Initialization of graph
{
int i;
for (i = 0; i < size; i++)
{
    vertex[i] = i;
    sz[i] = 0;
}
}
int root(int *vertex, int i)    //obtaining the root
{
while (i != vertex[i])
{
    vertex[i] = vertex[vertex[i]];
    i = vertex[i];
}
return i;
}

/* Time Complexity for Add --> logn */
void add(int *vertex, int *sz, int p, int q) //Adding of node
{
int i, j;
i = root(vertex, p);
j = root(vertex, q);

/* Adding small subtree in large subtree  */

if (sz[i] < sz[j])
{
    vertex[i] = j;
    sz[j] += sz[i];
}
else
{
    vertex[j] = i;
    sz[i] += sz[j];
}

}

/* Time Complexity for Search -->lg* n */

int connected(int *vertex, int p, int q) //Checking of  connectivity of nodes
{
/* Checking if root is same  */

if (root(vertex, p) == root(vertex, q))
    return 1;

return 0;
}

Dies scheint das Problem nicht wie gewünscht zu lösen. Das OP möchte alle einfachen Pfade zwischen den beiden Knoten finden und nicht nur prüfen, ob ein Pfad vorhanden ist.
Ilmari Karonen

1

find_paths [s, t, d, k]

Diese Frage ist alt und bereits beantwortet. Keiner zeigt jedoch vielleicht einen flexibleren Algorithmus, um dasselbe zu erreichen. Also werfe ich meinen Hut in den Ring.

Ich persönlich finde einen Algorithmus der Form find_paths[s, t, d, k]nützlich, wobei:

  • s ist der Startknoten
  • t ist der Zielknoten
  • d ist die maximale Suchtiefe
  • k ist die Anzahl der zu findenden Pfade

Verwenden Sie die Unendlichkeitsform Ihrer Programmiersprache für dund kgeben Sie alle Pfade§.

Offensichtlich § wenn Sie einen gerichteten Graphen verwenden und Sie alle ungerichtete Pfade zwischen sund tSie werden diese in beide Richtungen laufen müssen:

find_paths[s, t, d, k] <join> find_paths[t, s, d, k]

Hilfsfunktion

Ich persönlich mag Rekursion, obwohl es manchmal schwierig sein kann, lassen Sie uns trotzdem zuerst unsere Hilfsfunktion definieren:

def find_paths_recursion(graph, current, goal, current_depth, max_depth, num_paths, current_path, paths_found)
  current_path.append(current)

  if current_depth > max_depth:
    return

  if current == goal:
    if len(paths_found) <= number_of_paths_to_find:
      paths_found.append(copy(current_path))

    current_path.pop()
    return

  else:
    for successor in graph[current]:
    self.find_paths_recursion(graph, successor, goal, current_depth + 1, max_depth, num_paths, current_path, paths_found)

  current_path.pop()

Hauptfunktion

Damit ist die Kernfunktion trivial:

def find_paths[s, t, d, k]:
  paths_found = [] # PASSING THIS BY REFERENCE  
  find_paths_recursion(s, t, 0, d, k, [], paths_found)

Lassen Sie uns zunächst einige Dinge beachten:

  • Der obige Pseudocode ist ein Mash-up von Sprachen - aber am ähnlichsten wie Python (da ich nur darin codiert habe). Ein striktes Kopieren und Einfügen funktioniert nicht.
  • [] Ist eine nicht initialisierte Liste, ersetzen Sie diese durch die Entsprechung für die Programmiersprache Ihrer Wahl
  • paths_foundwird als Referenz übergeben . Es ist klar, dass die Rekursionsfunktion nichts zurückgibt. Behandeln Sie dies angemessen.
  • hier graphwird irgendeine Form von hashedStruktur angenommen. Es gibt eine Vielzahl von Möglichkeiten, ein Diagramm zu implementieren. In beiden graph[vertex]Fällen erhalten Sie eine Liste benachbarter Scheitelpunkte in einem gerichteten Diagramm - passen Sie sie entsprechend an.
  • Dies setzt voraus, dass Sie vorverarbeitet haben, um "Schnallen" (Selbstschleifen), Zyklen und Mehrfachkanten zu entfernen

0

Hier ist ein Gedanke aus meinem Kopf:

  1. Finde eine Verbindung. (Die Tiefensuche ist wahrscheinlich ein guter Algorithmus dafür, da die Pfadlänge keine Rolle spielt.)
  2. Deaktivieren Sie das letzte Segment.
  3. Versuchen Sie, eine andere Verbindung vom letzten Knoten vor der zuvor deaktivierten Verbindung zu finden.
  4. Gehe zu 2, bis keine Verbindungen mehr bestehen.

Dies funktioniert im Allgemeinen nicht: Es ist durchaus möglich, dass zwei oder mehr Pfade zwischen den Scheitelpunkten dieselbe letzte Kante haben. Ihre Methode würde nur einen solchen Pfad finden.
Ilmari Karonen

0

Soweit ich das beurteilen kann, sind die von Ryan Fox ( 58343 , Christian ( 58444 ) und Ihnen ( 58461 ) ) gegebenen Lösungen so gut wie es nur geht. Ich glaube nicht, dass die Durchquerung der Breite in diesem Fall hilft, wie Sie wollen erhalten nicht alle Pfade , z. B. mit Kanten (A,B), (A,C), (B,C), (B,D)und (C,D)Sie bekommen Pfade ABDund ACD, aber nicht ABCD.


mweerden, Die von mir eingereichte erste Durchquerung findet ALLE Pfade und vermeidet dabei Zyklen. Für das von Ihnen angegebene Diagramm findet die Implementierung alle drei Pfade korrekt.
Casey Watson

Ich habe Ihren Code nicht vollständig gelesen und angenommen, dass Sie eine Durchquerung der Breite zuerst verwendet haben (weil Sie dies gesagt haben). Bei näherer Betrachtung nach Ihrem Kommentar habe ich jedoch festgestellt, dass dies tatsächlich nicht der Fall ist. Es ist tatsächlich eine gedächtnislose Tiefenüberquerung wie die von Ryan, Christian und Robert.
Mweerden

0

Ich habe einen Weg gefunden, alle Pfade aufzulisten, einschließlich der unendlichen, die Schleifen enthalten.

http://blog.vjeux.com/2009/project/project-shortest-path.html

Atompfade und -zyklen finden

Definition

Wir möchten alle möglichen Pfade finden, die von Punkt A nach Punkt B führen. Da es sich um Zyklen handelt, können Sie nicht alle durchlaufen und aufzählen. Stattdessen müssen Sie einen atomaren Pfad finden, der keine Schleife aufweist, und die kleinstmöglichen Zyklen (Sie möchten nicht, dass sich Ihr Zyklus wiederholt).

Die erste Definition, die ich für einen Atompfad vorgenommen habe, ist ein Pfad, der nicht zweimal denselben Knoten durchläuft. Ich fand jedoch heraus, dass nicht alle Möglichkeiten genutzt wurden. Nach einigem Nachdenken stellte ich fest, dass Knoten nicht wichtig sind, Kanten jedoch! Ein Atompfad ist also ein Pfad, der nicht zweimal durch dieselbe Kante verläuft.

Diese Definition ist praktisch und funktioniert auch für Zyklen: Ein Atomzyklus von Punkt A ist ein Atompfad, der von Punkt A zu Punkt A führt.

Implementierung

Atomic Paths A -> B

Um den gesamten Pfad von Punkt A aus zu erhalten, werden wir den Graphen rekursiv von Punkt A aus durchlaufen. Während wir durch ein Kind gehen, werden wir ein Link-Kind -> Elternteil erstellen, um alle Kanten zu kennen, die wir haben habe schon gekreuzt. Bevor wir zu diesem Kind gehen, müssen wir diese verknüpfte Liste durchlaufen und sicherstellen, dass die angegebene Kante noch nicht durchlaufen wurde.

Wenn wir am Zielpunkt ankommen, können wir den gefundenen Pfad speichern.

Freeing the list

Ein Problem tritt auf, wenn Sie die verknüpfte Liste freigeben möchten. Es ist im Grunde ein Baum, der in umgekehrter Reihenfolge angekettet ist. Eine Lösung wäre, diese Liste doppelt zu verknüpfen und den Baum vom Startpunkt zu befreien, wenn alle Atompfade gefunden wurden.

Eine clevere Lösung ist jedoch die Verwendung einer Referenzzählung (inspiriert von der Garbage Collection). Jedes Mal, wenn Sie einem übergeordneten Element einen Link hinzufügen, fügen Sie dem Referenzzähler einen hinzu. Wenn Sie dann am Ende eines Pfades ankommen, gehen Sie rückwärts und frei, während der Referenzzähler gleich 1 ist. Wenn er höher ist, entfernen Sie einfach einen und halten an.

Atomic Cycle A

Das Suchen nach dem Atomzyklus von A ist dasselbe wie das Suchen nach dem Atompfad von A nach A. Es gibt jedoch mehrere Optimierungen, die wir vornehmen können. Erstens, wenn wir am Zielpunkt ankommen, wollen wir den Pfad nur speichern, wenn die Summe der Kantenkosten negativ ist: Wir wollen nur Absorptionszyklen durchlaufen.

Wie Sie zuvor gesehen haben, wird der gesamte Graph bei der Suche nach einem Atompfad durchlaufen. Stattdessen können wir den Suchbereich auf die stark verbundene Komponente beschränken, die A enthält. Um diese Komponenten zu finden, müssen Sie den Graphen mit dem Tarjan-Algorithmus einfach durchlaufen.

Atompfade und Zyklen kombinieren

Zu diesem Zeitpunkt haben wir alle Atompfade von A nach B und alle Atomzyklen jedes Knotens, die uns überlassen bleiben, um alles zu organisieren, um den kürzesten Pfad zu erhalten. Von nun an werden wir untersuchen, wie man die beste Kombination von Atomzyklen auf einem Atompfad findet.


Dies scheint die gestellte Frage nicht zu beantworten.
Ilmari Karonen

0

Wie von einigen anderen Postern geschickt beschrieben, besteht das Problem auf den Punkt gebracht darin, einen Tiefensuchalgorithmus zu verwenden, um den Graphen rekursiv nach allen Kombinationen von Pfaden zwischen den kommunizierenden Endknoten zu durchsuchen.

Der Algorithmus selbst beginnt mit dem Startknoten, den Sie ihm geben, untersucht alle ausgehenden Links und erweitert den ersten untergeordneten Knoten des angezeigten Suchbaums, wobei er immer tiefer sucht, bis ein Zielknoten gefunden wird oder bis er auf einen Knoten trifft das hat keine Kinder.

Die Suche wird dann zurückverfolgt und kehrt zum letzten Knoten zurück, den die Erkundung noch nicht abgeschlossen hat.

Ich gebloggt über dieses Thema sehr vor kurzem ein Beispiel C ++ Implementierung in dem Prozess der Veröffentlichung.


0

Neben der Antwort von Casey Watson finden Sie hier eine weitere Java-Implementierung. Initialisierung des besuchten Knotens mit dem Startknoten.

private void getPaths(Graph graph, LinkedList<String> visitedNodes) {
                LinkedList<String> adjacent = graph.getAdjacent(visitedNodes.getLast());
                for(String node : adjacent){
                    if(visitedNodes.contains(node)){
                        continue;
                    }
                    if(node.equals(END)){
                        visitedNodes.add(node);
                        printPath(visitedNodes);
                        visitedNodes.removeLast();
                    }
                    visitedNodes.add(node);
                    getPaths(graph, visitedNodes);
                    visitedNodes.removeLast();  
                }
            }
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.