Algoritmo grafico per trovare tutte le connessioni tra due vertici arbitrari


117

Sto cercando di determinare il miglior algoritmo efficiente in termini di tempo per eseguire l'attività descritta di seguito.

Ho una serie di record. Per questo set di record dispongo di dati di connessione che indicano come le coppie di record di questo set si connettono tra loro. Questo fondamentalmente rappresenta un grafo non orientato, con i record che sono i vertici e i dati di connessione i bordi.

Tutti i record nel set hanno informazioni sulla connessione (cioè non sono presenti record orfani; ogni record nel set si connette a uno o più altri record nel set).

Voglio scegliere due record qualsiasi dal set ed essere in grado di mostrare tutti i percorsi semplici tra i record scelti. Per "percorsi semplici" intendo i percorsi che non hanno record ripetuti nel percorso (cioè solo percorsi finiti).

Nota: i due record scelti saranno sempre diversi (cioè il vertice iniziale e quello finale non saranno mai uguali; nessun ciclo).

Per esempio:

    Se ho i seguenti record:
        A, B, C, D, E

    e quanto segue rappresenta le connessioni: 
        (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)

        [dove (A, B) significa che il record A si collega al record B]

Se scegliessi B come record iniziale e E come record finale, vorrei trovare tutti i percorsi semplici attraverso le connessioni record che collegherebbero il record B al record E.

   Tutti i percorsi che collegano B a E:
      B-> E
      B-> F-> E
      B-> F-> C-> E
      B-> A-> C> E
      B-> A-> C> F-> E

Questo è un esempio, in pratica potrei avere set contenenti centinaia di migliaia di record.


Le connessioni sono chiamate cicli e questa risposta contiene molte informazioni per te.
elhoim

3
Si prega di dire se si desidera un elenco finito di connessioni senza loop o un flusso infinito di connessioni con tutti i loop possibili. Cf. La risposta di Blorgbeard.
Charles Stewart

qualcuno può aiutare con questo ??? stackoverflow.com/questions/32516706/...
tejas3006

Risposte:


116

Sembra che ciò possa essere ottenuto con una ricerca approfondita del grafico. La ricerca in profondità troverà tutti i percorsi non ciclici tra due nodi. Questo algoritmo dovrebbe essere molto veloce e scalare a grafici di grandi dimensioni (la struttura dei dati del grafico è scarsa, quindi utilizza solo la memoria necessaria).

Ho notato che il grafico che hai specificato sopra ha un solo bordo direzionale (B, E). È stato un errore di battitura o è davvero un grafico diretto? Questa soluzione funziona indipendentemente. Scusa se non sono riuscito a farlo in C, sono un po 'debole in quella zona. Mi aspetto che sarai in grado di tradurre questo codice Java senza troppi problemi.

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

Uscita del programma:

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

5
Tieni presente che questo non è un attraversamento in ampiezza. Con la larghezza si visitano prima tutti i nodi con distanza 0 dalla radice, poi quelli con distanza 1, poi 2, ecc.
mweerden

14
Esatto, questo è un DFS. Un BFS dovrebbe utilizzare una coda, accodando i nodi di livello (N + 1) da elaborare dopo tutti i nodi di livello N. Tuttavia, per gli scopi dell'OP, BFS o DFS funzioneranno, poiché non viene specificato alcun ordine di ordinamento preferito dei percorsi.
Matt J

1
Casey, sono anni che cerco una soluzione a questo problema. Recentemente ho implementato questo DFS in C ++ e funziona a meraviglia.
AndyUK

6
Lo svantaggio della ricorsione è che se avrai un grafo profondo (A-> B-> C -> ...-> N) potresti avere StackOverflowError in java.
Rrr

1
Ho aggiunto una versione iterativa in C # di seguito.
batta

23

Il Dizionario online degli algoritmi e delle strutture dati del National Institute of Standards and Technology (NIST) elenca questo problema come " tutti i percorsi semplici" e consiglia una ricerca approfondita . CLRS fornisce gli algoritmi rilevanti.

Una tecnica intelligente che utilizza le reti di Petri si trova qui


2
Potresti aiutarmi con una soluzione migliore? un DFS prende per sempre a correre: stackoverflow.com/q/8342101/632951
Pacerier

Si noti che è facile trovare grafici per i quali DFS è molto inefficiente, anche se l'insieme di tutti i percorsi semplici tra i due nodi è piccolo e facile da trovare. Ad esempio, si consideri un grafo non orientato in cui il nodo iniziale A ha due vicini: il nodo obiettivo B (che non ha vicini diversi da A) e un nodo C che fa parte di una cricca completamente connessa di n + 1 nodi. Anche se c'è chiaramente un solo semplice percorso da A a B, un DFS ingenuo sprecherà O ( n !) Tempo inutilmente esplorando la cricca. Esempi simili (una soluzione, DFS richiede tempo esponenziale) possono essere trovati anche tra i DAG.
Ilmari Karonen

Il NIST dice: "I percorsi possono essere enumerati con una ricerca approfondita".
mastica

13

Ecco lo pseudocodice che mi è venuto in mente. Questo non è un dialetto di pseudocodice particolare, ma dovrebbe essere abbastanza semplice da seguire.

Qualcuno vuole scegliere questo a parte.

  • [p] è un elenco di vertici che rappresentano il percorso corrente.

  • [x] è un elenco di percorsi che soddisfano i criteri

  • [s] è il vertice di origine

  • [d] è il vertice di destinazione

  • [c] è il vertice corrente (argomento della routine PathFind)

Supponiamo che esista un modo efficiente per cercare i vertici adiacenti (riga 6).

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

     4 PathFind (Vertice [c])
     5 Aggiungi [c] alla fine dell'elenco [p]
     6 Per ogni vertice [v] adiacente a [c]
     7 Se [v] è uguale a [d] allora
     8 Salva elenco [p] in [x]
     9 Altrimenti se [v] non è nell'elenco [p]
    10 PathFind ([v])
    11 Avanti per
    12 Rimuovi la coda da [p]
    13 Ritorno

Per favore, puoi fare un po 'di luce sui passaggi 11 e 12
utente bozo

La riga 11 indica semplicemente il blocco finale che va con il ciclo For che inizia sulla riga 6. La riga 12 significa rimuovere l'ultimo elemento dell'elenco dei percorsi prima di tornare al chiamante.
Robert Groves

Qual è la chiamata iniziale a PathFind - passi nel vertice [s] di origine?
utente bozo

In questo esempio sì, ma tieni presente che potresti non voler scrivere codice reale che associ uno a uno con questo pseudocodice. Ha lo scopo più di illustrare un processo di pensiero piuttosto che un codice ben progettato.
Robert Groves

8

Poiché l'implementazione DFS non ricorsiva esistente fornita in questa risposta sembra essere interrotta, consentitemi di fornirne una che funzioni effettivamente.

L'ho scritto in Python, perché lo trovo abbastanza leggibile e ordinato dai dettagli di implementazione (e perché ha la comoda yieldparola chiave per l'implementazione dei generatori ), ma dovrebbe essere abbastanza facile portarlo in altri linguaggi.

# 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

Questo codice mantiene due stack paralleli: uno contenente i nodi precedenti nel percorso corrente e uno contenente l'indice del vicino corrente per ogni nodo nello stack del nodo (in modo che possiamo riprendere l'iterazione attraverso i nodi vicini di un nodo quando lo ripristiniamo la pila). Avrei potuto usare ugualmente bene un singolo stack di coppie (nodo, indice), ma ho pensato che il metodo a due stack sarebbe stato più leggibile e forse più facile da implementare per gli utenti di altre lingue.

Questo codice utilizza anche un visitedset separato , che contiene sempre il nodo corrente e tutti i nodi sullo stack, per consentirmi di controllare in modo efficiente se un nodo fa già parte del percorso corrente. Se la lingua capita di avere un "insieme ordinato" struttura dati che fornisce sia efficiente stack come push / pop operazioni e query di appartenenza efficienti, è possibile utilizzare che per lo stack nodo e sbarazzarsi della separata visitedinsieme.

In alternativa, se stai utilizzando una classe / struttura modificabile personalizzata per i tuoi nodi, puoi semplicemente memorizzare un flag booleano in ogni nodo per indicare se è stato visitato come parte del percorso di ricerca corrente. Ovviamente, questo metodo non ti consentirà di eseguire due ricerche sullo stesso grafico in parallelo, se per qualche motivo desideri farlo.

Ecco un po 'di codice di prova che dimostra come funziona la funzione sopra indicata:

# 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)

L'esecuzione di questo codice sul grafico di esempio fornito produce il seguente output:

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

Si noti che, sebbene questo grafico di esempio non sia orientato (cioè tutti i suoi bordi vanno in entrambe le direzioni), l'algoritmo funziona anche per grafici diretti arbitrari. Ad esempio, la rimozione del C -> Bbordo (rimuovendo Bdall'elenco dei vicini di C) restituisce lo stesso output eccetto il terzo percorso ( A -> C -> B -> D), che non è più possibile.


Ps. È facile costruire grafici per i quali semplici algoritmi di ricerca come questo (e gli altri dati in questo thread) funzionano molto male.

Ad esempio, si consideri il compito di trovare tutti i percorsi da A a B su un grafo non orientato in cui il nodo iniziale A ha due vicini: il nodo obiettivo B (che non ha altri vicini oltre A) e un nodo C che fa parte di una cricca di n +1 nodi, in questo modo:

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"),
}

È facile vedere che l'unico percorso tra A e B è quello diretto, ma un ingenuo DFS avviato dal nodo A sprecherà O ( n !) Tempo esplorando inutilmente percorsi all'interno della cricca, anche se è ovvio (per un umano) che nessuno di questi percorsi può portare a B.

Si possono anche costruire DAG con proprietà simili, ad esempio facendo in modo che il nodo di partenza A colleghi il nodo di destinazione B e altri due nodi C 1 e C 2 , entrambi collegati ai nodi D 1 e D 2 , entrambi collegati a E 1 ed E 2 e così via. Per n strati di nodi disposti in questo modo, una ricerca ingenua di tutti i percorsi da A a B finirà per far perdere O (2 n ) tempo ad esaminare tutti i possibili vicoli ciechi prima di arrendersi.

Naturalmente, l'aggiunta di un bordo al nodo di destinazione B da uno dei nodi della cricca (oltre che C), o dall'ultima strato del DAG, potrebbe creare un esponenziale gran numero di possibili percorsi da A a B, e l'algoritmo di ricerca puramente locale non può davvero dire in anticipo se troverà un tale vantaggio o meno. Quindi, in un certo senso, la scarsa sensibilità all'output di tali ricerche ingenue è dovuta alla loro mancanza di consapevolezza della struttura globale del grafico.

Sebbene esistano vari metodi di preelaborazione (come l'eliminazione iterativa dei nodi foglia, la ricerca di separatori di vertici a nodo singolo, ecc.) Che potrebbero essere utilizzati per evitare alcuni di questi "vicoli ciechi del tempo esponenziale", non conosco nessun generale trucco di preelaborazione che potrebbe eliminarli in tutti i casi. Una soluzione generale sarebbe controllare in ogni fase della ricerca se il nodo di destinazione è ancora raggiungibile (utilizzando una sotto-ricerca) e tornare indietro in anticipo se non lo è, ma ahimè, ciò rallenterebbe notevolmente la ricerca (nel peggiore dei casi , proporzionalmente alla dimensione del grafico) per molti grafici che non contengono tali vicoli ciechi patologici.


1
Questo è quello che sto cercando, grazie :)
arslan

Grazie per la tua soluzione non ricorsiva DFS. Basta notare che l'ultima riga che stampa il risultato ha un errore di sintassi, dovrebbe essere for path in find_simple_paths(graph, "A", "D"): print(" -> ".join(path)), printmancava la parentesi.
David Oliván Ubieto

1
@ DavidOlivánUbieto: È il codice Python 2, ecco perché non ci sono parentesi. :)
Ilmari Karonen

5

Ecco una versione ricorsiva dall'aspetto logicamente migliore rispetto al secondo piano.

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

Output del programma

B A C E 

B A C F E 

B E

B F C E

B F E 

4

Soluzione in codice C. Si basa su DFS che utilizza una memoria minima.

#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

Potrebbe essere in ritardo, ma ecco la stessa versione C # dell'algoritmo DFS in Java da Casey per attraversare tutti i percorsi tra due nodi utilizzando uno stack. La leggibilità è migliore con la ricorsività come sempre.

    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();
        }
    }
Questo è un grafico di esempio da testare:

    // Grafico di esempio. I numeri sono identificativi di bordo
    // 1 3       
    // A --- B --- C ----
    // | | 2 |
    // | 4 ----- D |
    // ------------------

1
eccellente - su come hai sostituito la ricorsione con l'iterazione basata sullo stack.
Siddhartha Ghosh

Ancora non lo capisco, cos'è neighbours.Reverse()? È vero List<T>.Reverse ?

Ho controllato questa versione non ricorsiva, ma non sembra corretta. la versione ricorsiva va bene. forse quando è cambiato in non ricorsivo, si è verificato un piccolo errore
arslan

@alim: d'accordo, questo codice è semplicemente rotto. (Non rimuove correttamente i nodi dal set visitato durante il backtracking e anche la gestione dello stack sembra essere incasinata. Ho provato a vedere se potesse essere risolto, ma ciò richiederebbe fondamentalmente una riscrittura completa.) Ho appena aggiunta una risposta con una soluzione non ricorsiva corretta e funzionante (in Python, ma dovrebbe essere relativamente facile portarla in altri linguaggi).
Ilmari Karonen

@llmari Karonen, Nice, vado a controllare, ottimo lavoro.
arslan

1

Di recente ho risolto un problema simile a questo, invece di tutte le soluzioni mi interessava solo la più breve.

Ho utilizzato una ricerca iterativa "ampia prima" che utilizzava una coda di stato "ciascuna delle quali conteneva un record contenente un punto corrente sul grafico e il percorso intrapreso per arrivarci."

si inizia con un singolo record nella coda, che ha il nodo iniziale e un percorso vuoto.

Ogni iterazione del codice toglie l'elemento dalla testa della lista e controlla se si tratta di una soluzione (il nodo a cui si arriva è quello che si desidera, se lo è, abbiamo finito), altrimenti costruisce un nuovo elemento della coda con i nodi che si connettono al nodo corrente e percorsi modificati basati sul percorso del nodo precedente, con il nuovo salto allegato alla fine.

Ora, potresti usare qualcosa di simile, ma quando trovi una soluzione, invece di fermarti, aggiungi quella soluzione al tuo "elenco trovato" e continua.

Devi tenere traccia di un elenco di nodi visitati, in modo da non tornare mai indietro su te stesso altrimenti hai un ciclo infinito.

se vuoi un po 'più di pseudocodice pubblica un commento o qualcosa del genere, e io elaborerò.


6
Credo che se sei interessato solo al percorso più breve, l'algoritmo di Dijkstra è "la soluzione" :).
vicatcu

1

Penso che dovresti descrivere il tuo vero problema dietro questo. Dico questo perché chiedi qualcosa di efficiente in termini di tempo, eppure la risposta impostata al problema sembra crescere in modo esponenziale!

Quindi non mi aspetterei un algoritmo migliore di qualcosa di esponenziale.

Farei il backtracking e passare attraverso l'intero grafico. Per evitare cicli, salva tutti i nodi visitati lungo il percorso. Quando torni indietro, deseleziona il nodo.

Utilizzando la ricorsione:

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
}

O è sbagliato?

modifica: Oh, e ho dimenticato: dovresti eliminare le chiamate ricorsive utilizzando lo stack del nodo


Il mio vero problema è esattamente come ho descritto, solo con set molto più grandi. Sono d'accordo che questo sembra crescere in modo esponenziale con le dimensioni del set.
Robert Groves

1

Il principio di base è che non devi preoccuparti dei grafici, questo è un problema standard noto come problema di connettività dinamica. Esistono i seguenti tipi di metodi da cui è possibile ottenere che i nodi siano collegati o meno:

  1. Ricerca rapida
  2. Unione rapida
  3. Algoritmo migliorato (combinazione di entrambi)

Ecco il codice C che ho provato con una complessità temporale minima O (log * n) Ciò significa che per 65536 lista di bordi, richiede 4 ricerche e per 2 ^ 65536, richiede 5 ricerche. Condivido la mia implementazione dall'algoritmo: Algorithm Course dell'Università di Princeton

SUGGERIMENTO: è possibile trovare la soluzione Java dal collegamento condiviso sopra con le spiegazioni appropriate.

/* 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;
}

Questo non sembra risolvere il problema come richiesto. L'OP vuole trovare tutti i percorsi semplici tra i due nodi, non solo per verificare se esiste un percorso.
Ilmari Karonen

1

find_paths [s, t, d, k]

Questa domanda è vecchia e ha già risposto. Tuttavia, nessuno mostra forse un algoritmo più flessibile per realizzare la stessa cosa. Quindi lancio il mio cappello sul ring.

Personalmente trovo utile un algoritmo del form find_paths[s, t, d, k], dove:

  • s è il nodo di partenza
  • t è il nodo di destinazione
  • d è la profondità massima per la ricerca
  • k è il numero di percorsi da trovare

Usare la forma dell'infinito del tuo linguaggio di programmazione per de kti darà tutti i percorsi§.

§ ovviamente se stai usando un grafo diretto e vuoi tutti i percorsi non orientati tra se tdovrai eseguirlo in entrambi i modi:

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

Funzione di aiuto

Personalmente mi piace la ricorsione, anche se a volte può essere difficile, comunque prima definiamo la nostra funzione di aiuto:

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()

Funzione principale

Detto questo, la funzione principale è banale:

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

Innanzitutto, notiamo alcune cose:

  • lo pseudo-codice sopra è un mash-up di linguaggi, ma somiglia molto a python (dato che ci stavo solo scrivendo). Un rigoroso copia-incolla non funzionerà.
  • [] è un elenco non inizializzato, sostituiscilo con l'equivalente per il tuo linguaggio di programmazione preferito
  • paths_foundviene passato per riferimento . È chiaro che la funzione di ricorsione non restituisce nulla. Gestisci questo in modo appropriato.
  • qui graphsta assumendo una qualche forma di hashedstruttura. Esistono molti modi per implementare un grafico. In entrambi i casi, graph[vertex]si ottiene una lista di vertici adiacenti in un diretto grafico - regolare di conseguenza.
  • questo presuppone che tu abbia pre-elaborato per rimuovere "fibbie" (self-loop), cicli e multi-bordi

0

Ecco un pensiero fuori dalla mia testa:

  1. Trova una connessione. (La ricerca in profondità è probabilmente un buon algoritmo per questo, poiché la lunghezza del percorso non ha importanza.)
  2. Disabilita l'ultimo segmento.
  3. Prova a trovare un'altra connessione dall'ultimo nodo prima della connessione precedentemente disabilitata.
  4. Vai a 2 fino a quando non ci sono più connessioni.

Questo non funzionerà in generale: è del tutto possibile che due o più percorsi tra i vertici abbiano lo stesso ultimo bordo. Il tuo metodo troverà solo uno di questi percorsi.
Ilmari Karonen

0

Per quanto ne so, le soluzioni fornite da Ryan Fox ( 58343 , Christian ( 58444 ) e te stesso ( 58461 ) sono più o meno le migliori. Non credo che l'attraversamento in ampiezza aiuti in questo caso, come farai non ottenere tutti i percorsi. Ad esempio, con bordi (A,B), (A,C), (B,C), (B,D)e (C,D)si otterrà percorsi ABDe ACD, ma non ABCD.


mweerden, La traversata in ampiezza che ho presentato troverà TUTTI i percorsi evitando qualsiasi ciclo. Per il grafico specificato, l'implementazione trova correttamente tutti e tre i percorsi.
Casey Watson

Non ho letto completamente il tuo codice e ho pensato che tu usassi un attraversamento in ampiezza (perché l'hai detto tu). Tuttavia, a un esame più attento dopo il tuo commento, ho notato che in realtà non lo è. In realtà è una traversata profonda senza memoria come quelle di Ryan, Christian e Robert.
mweerden

0

Ho trovato un modo per enumerare tutti i percorsi compresi quelli infiniti contenenti i loop.

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

Trovare percorsi e cicli atomici

Definition

Quello che vogliamo fare è trovare tutti i percorsi possibili che vanno dal punto A al punto B. Poiché sono coinvolti dei cicli, non puoi semplicemente percorrerli ed enumerarli tutti. Invece, dovrai trovare un percorso atomico che non si ripete e i cicli più piccoli possibili (non vuoi che il tuo ciclo si ripeta).

La prima definizione che ho preso di un percorso atomico è un percorso che non attraversa due volte lo stesso nodo. Tuttavia, ho scoperto che non stava prendendo tutte le possibilità. Dopo un po 'di riflessione, ho capito che i nodi non sono importanti, tuttavia lo sono i bordi! Quindi un percorso atomico è un percorso che non attraversa due volte lo stesso bordo.

Questa definizione è utile, funziona anche per i cicli: un ciclo atomico del punto A è un percorso atomico che va dal punto A e termina al punto A.

Implementazione

Atomic Paths A -> B

Per ottenere tutto il percorso a partire dal punto A, attraverseremo il grafico ricorsivamente dal punto A. Mentre attraversiamo un bambino, creeremo un collegamento figlio -> genitore per conoscere tutti i bordi che abbiamo hanno già attraversato. Prima di andare da quel bambino, dobbiamo attraversare quell'elenco collegato e assicurarci che il bordo specificato non sia già stato attraversato.

Quando arriviamo al punto di destinazione, possiamo memorizzare il percorso che abbiamo trovato.

Freeing the list

Si verifica un problema quando si desidera liberare l'elenco collegato. È fondamentalmente un albero incatenato nell'ordine inverso. Una soluzione sarebbe quella di collegare due volte quella lista e quando tutti i percorsi atomici sono stati trovati, liberare l'albero dal punto di partenza.

Ma una soluzione intelligente è usare un conteggio dei riferimenti (ispirato da Garbage Collection). Ogni volta che aggiungi un collegamento a un genitore, ne aggiungi uno al suo conteggio dei riferimenti. Quindi, quando arrivi alla fine di un percorso, vai indietro e libero mentre il conteggio dei riferimenti è uguale a 1. Se è più alto, ne rimuovi uno e ti fermi.

Atomic Cycle A

Cercare il ciclo atomico di A è lo stesso che cercare il percorso atomico da A ad A. Tuttavia ci sono diverse ottimizzazioni che possiamo fare. Innanzitutto, quando arriviamo al punto di destinazione, vogliamo salvare il percorso solo se la somma del costo degli spigoli è negativa: vogliamo solo passare attraverso cicli assorbenti.

Come hai visto in precedenza, l'intero grafico viene attraversato quando si cerca un percorso atomico. Invece, possiamo limitare l'area di ricerca alla componente fortemente connessa contenente A. La ricerca di queste componenti richiede una semplice traversata del grafo con l'algoritmo di Tarjan.

Combinazione di percorsi e cicli atomici

A questo punto, abbiamo tutti i percorsi atomici che vanno da A a B e tutti i cicli atomici di ogni nodo, lasciati a noi organizzare il tutto per ottenere il percorso più breve. D'ora in poi studieremo come trovare la migliore combinazione di cicli atomici in un percorso atomico.


Questo non sembra rispondere alla domanda posta.
Ilmari Karonen

0

Come abilmente descritto da alcuni degli altri poster, il problema in poche parole è quello di utilizzare un algoritmo di ricerca in profondità per cercare ricorsivamente nel grafico tutte le combinazioni di percorsi tra i nodi finali comunicanti.

L'algoritmo stesso inizia con il nodo di partenza che gli dai, esamina tutti i suoi collegamenti in uscita e progredisce espandendo il primo nodo figlio dell'albero di ricerca che appare, cercando progressivamente sempre più in profondità fino a trovare un nodo di destinazione o fino a quando non incontra un nodo che non ha figli.

La ricerca quindi torna indietro, tornando al nodo più recente che non ha ancora terminato di esplorare.

Ho scritto sul blog proprio di questo argomento di recente, pubblicando un esempio di implementazione C ++ nel processo.


0

Aggiungendo alla risposta di Casey Watson, ecco un'altra implementazione di Java. Inizializzazione del nodo visitato con il nodo iniziale.

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();  
                }
            }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.