Алгоритм графіку для пошуку всіх зв’язків між двома довільними вершинами


117

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

У мене є набір записів. Для цього набору записів у мене є дані про з'єднання, які вказують, як пари записів із цього набору з'єднуються між собою. Це в основному являє собою непрямий графік, при цьому записи є вершинами, а дані з'єднання - ребрами.

Усі записи в наборі мають інформацію про з'єднання (тобто немає записів сиріт; кожна запис у наборі підключається до однієї або декількох інших записів у наборі).

Я хочу вибрати будь-які два записи з набору і мати змогу показати всі прості шляхи між обраними записами. Під "простими шляхами" я маю на увазі шляхи, які не мають повторених записів на шляху (тобто лише кінцеві контури).

Примітка. Два обраних запису завжди будуть різними (тобто початкова і кінцева вершини ніколи не будуть однаковими; відсутні цикли).

Наприклад:

    Якщо у мене є такі записи:
        A, B, C, D, E

    і нижче представлені з'єднання: 
        (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)

        [де (A, B) означає запис A підключається до запису B]

Якби я вибрав B як свій початковий запис, а E - як закінчуючий запис, я хотів би знайти всі прості шляхи через з'єднання записів, які з'єднували б запис B із записом E.

   Усі шляхи, що з'єднують B до E:
      B-> E
      B-> F-> E
      B-> F-> C-> E
      B-> A-> C-> E
      B-> A-> C-> F-> E

Це приклад, на практиці у мене можуть бути набори, що містять сотні тисяч записів.


З'єднання називаються циклами , і ця відповідь має багато інформації для вас.
elhoim

3
Скажіть, будь ласка, чи хочете ви обмеженого списку безконтурних з'єднань або нескінченного потоку з'єднань з усіма можливими петлями. Ср. Відповідь Блоргберда.
Чарльз Стюарт

хтось може допомогти з цим ??? stackoverflow.com/questions/32516706 / ...
tejas3006

Відповіді:


116

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

Я помітив, що графік, який ви вказали вище, має лише один край, спрямований (B, E). Це була друкарська помилка чи це справді спрямований графік? Це рішення працює незалежно. Вибачте, що мені не вдалося це зробити в С, я трохи слабкий у цій галузі. Я сподіваюся, що ви зможете перекласти цей код Java без особливих проблем.

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

Пошук.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();
    }
}

Вихід програми:

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

5
Зверніть увагу, що це не обхід першої широти. З широтою спочатку ви відвідуєте всі вузли з відстанью 0 до кореня, потім ті, які мають відстань 1, потім 2 і т. Д.
mweerden,

14
Правильно, це ДФС. BFS потребує використання черги, залучаючи вузли рівня (N + 1) для обробки після всіх вузлів рівня N. Однак для цілей ОП працюватиме або BFS, або DFS, оскільки не вказаний упорядкований порядок сортування шляхів.
Метт Дж.

1
Кейсі, я шукав рішення цієї проблеми століттями. Я нещодавно реалізував цю програму DFS в C ++, і вона працює за бажанням.
AndyUK

6
Недоліком рекурсії є те, якщо у вас буде глибокий графік (A-> B-> C -> ...-> N), у вас може бути StackOverflowError у Java.
Rrr

1
Я додав ітераційну версію в C # нижче.
batta

23

Інтернет-словник алгоритмів та структур даних Національний інститут стандартів і технологій (NIST) перераховує цю проблему як " всі прості шляхи" і рекомендує здійснювати глибинний пошук . CLRS постачає відповідні алгоритми.

Розумна техніка з використанням мереж Петрі знаходиться тут


2
Чи можете ви мені допомогти з кращим рішенням? ДФС приймає назавжди для запуску: stackoverflow.com/q/8342101/632951
Pacerier

Зауважте, що легко створити графіки, для яких DFS дуже неефективний, навіть якщо набір усіх простих шляхів між двома вузлами невеликий і їх легко знайти. Наприклад, розглянемо непрямий графік, де у початкового вузла A є два сусіди: вузол цілі B (у якого немає інших сусідів, крім A), і вузол C, який є частиною повністю пов'язаної кліки з n + 1 вузлів. Навіть незважаючи на те, що існує просто один простий шлях від А до В, наївний ДФС витратить O ( n !) Час, марно досліджуючи кліку. Подібні приклади (одне рішення, DFS займає експоненціальний час) також можна знайти серед DAG.
Ільмарі Каронен

У НІСТі сказано: "Шляхи можуть бути перераховані за допомогою першого глибинного пошуку".
чемп

13

Ось псевдокод, який я придумав. Це не який-небудь конкретний діалект псевдокоду, але він повинен бути досить простим для його дотримання.

Усі хочуть забрати це.

  • [p] - це список вершин, що представляють поточний шлях.

  • [x] - це список шляхів, де відповідають критеріям

  • [s] - вершина вершини

  • [d] - вершина призначення

  • [c] - поточна вершина (аргумент до процедури PathFind)

Припустимо, існує ефективний спосіб пошуку сусідніх вершин (рядок 6).

     1 PathList [p]
     2 ListOfPathLists [x]
     3 Вершина [s], [d]

     4 PathFind (Vertex [c])
     5 Додайте [c] до кінця списку [p]
     6 Для кожної вершини [v], що примикає до [c]
     7 Якщо [v] дорівнює [d], то
     8 Зберегти список [p] у [x]
     9 Else Якщо [v] немає в списку [p]
    10 PathFind ([v])
    11 Наступний для
    12 Видаліть хвіст із [p]
    13 Повернення

Ви можете пролити трохи світла на крок 11 та крок 12
користувач bozo

Рядок 11 просто позначає кінцевий блок, що йде з циклу For, який починається в рядку 6. Рядок 12 означає видалити останній елемент списку шляхів перед поверненням до абонента.
Роберт Гроуз

Який початковий виклик до PathFind - чи передаєте ви у вихідну вершину [s]?
користувач bozo

У цьому прикладі так, але майте на увазі, що ви, можливо, не хочете писати реальний код, який відображає один на один із цим псевдокодом. Це мається на увазі швидше для ілюстрації продуманого процесу, а не добре розробленого коду.
Роберт Гроувс

8

Оскільки існуюча нерекурсивна реалізація DFS, наведена у цій відповіді, здається, порушена, дозвольте надати таку, яка насправді працює.

Я написав це в Python, тому що я вважаю це досить читабельним і незграбним деталями реалізації (і тому, що це зручно yield досить легко ключове слово для впровадження генераторів ), але він повинен бути досить легким для порту на інші мови.

# 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

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

Цей код також використовує окремий visitedнабір, який завжди містить поточний вузол та будь-які вузли на стеку, щоб я міг ефективно перевірити, чи вже вузол є частиною поточного шляху. Якщо ваша мова має структуру даних "впорядкованого набору", яка забезпечує ефективні операції push-pop, схожі на стек та ефективні запити членства, ви можете використовувати це для стека вузлів і позбутися окремоїvisited набору.

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

Ось декілька тестових кодів, що демонструють, як працює наведена вище функція:

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

Запуск цього коду на наведеному прикладі графіка дає такий результат:

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

Зауважимо, що, хоча цей приклад графа непрямий (тобто всі його краї йдуть обома напрямками), алгоритм також працює для довільно спрямованих графіків. Наприклад, видалення C -> Bкраю (видалення Bзі списку сусідів C) дає такий же вихід, за винятком третього шляху ( A -> C -> B -> D), який вже неможливий.


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

Наприклад, розглянемо завдання знайти всі шляхи від А до В на непрямому графіку, де у початкового вузла А є два сусіди: цільовий вузол В (у якого немає інших сусідів, крім А) і вузол С, який є частиною кліки з n +1 вузлів, як це:

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

Неважко помітити, що єдиний шлях між A і B - прямий, але наївна DFS, починається з вузла A, марно витратить O ( n !) Час, марно досліджуючи шляхи всередині кліки, хоча очевидно (для людини), що жоден із цих шляхів не може призвести до Б.

Можна також сконструювати DAG з подібними властивостями, наприклад, маючи стартовий вузол A, що з'єднує цільовий вузол B, та два інші вузли C 1 і C 2 , обидва з яких підключаються до вузлів D 1 і D 2 , обидва з яких з'єднуються з E 1 і Е 2 тощо. Для n шарів вузлів, розташованих так, наївний пошук усіх шляхів від А до В закінчиться витрачати час (2 n ), вивчаючи всі можливі тупики перед відмовою.

Звичайно, додаючи край до цільового вузла B від одного з вузлів в кліці (крім З), або з останнього шару DAG, буде створювати експоненціально велике число можливих шляхів від A до B, а суто локальний алгоритм пошуку насправді не може заздалегідь сказати, знайде він такий край чи ні. Таким чином, у певному сенсі погана чутливість виводу таких наївних пошуків пояснюється їх недостатньою обізнаністю про глобальну структуру графіка.

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


1
Це те, що я шукаю, дякую :)
arslan

Дякуємо за ваше нерекурсивне рішення DFS. Просто зауважте, що в останньому рядку друку результату є синтаксична помилка, повинно бути for path in find_simple_paths(graph, "A", "D"): print(" -> ".join(path)), printвідсутні дужки.
Девід Оліван Убіето

1
@ DavidOlivánUbieto: Це код Python 2, тому немає дужок. :)
Ілмарі Каронен

5

Ось логічно краще виглядає рекурсивна версія порівняно з другим поверхом.

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

Вихід програми

B A C E 

B A C F E 

B E

B F C E

B F E 

4

Рішення в коді С. Він заснований на DFS, який використовує мінімальну пам'ять.

#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

Це може бути пізно, але ось одна і та сама версія C # алгоритму DFS в Java від Casey для проходження всіх шляхів між двома вузлами, використовуючи стек. Читання легше з рекурсивними, як завжди.

    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();
        }
    }
Це зразковий графік для тестування:

    // Зразок графіка. Цифри - це крайові ідентифікатори
    // 1 3       
    // A --- B --- C ----
    // | | 2 |
    // | 4 ----- D |
    // ------------------

1
відмінно - про те, як ви замінили рекурсію на ітерацію на основі стека.
Siddhartha Ghosh

Я досі не розумію, що це neighbours.Reverse()? Це List<T>.Reverse ?

Я перевірив цю нерекурсивну версію, але вона здається невірною. рекурсивна версія - це добре. можливо, коли буде змінено на нерекурсивне, трапиться невелика помилка
арслан

@alim: Погоджено, цей код просто порушений. (Під час зворотного відстеження неправильно видаляються вузли з відвідуваного набору, і обробка стека, здається, також заплутана. Я намагався зрозуміти, чи можна це виправити, але це, як правило, вимагає повного переписування.) Я просто додав відповідь правильним, працюючим нерекурсивним рішенням (в Python, але він повинен бути відносно легким для порту на інші мови).
Ільмарі Каронен

@llmari Karonen, Ніцца, я збираюся перевірити, чудова робота.
арслан

1

Я вирішив подібну проблему до цього нещодавно, замість усіх рішень мене зацікавив лише найкоротший.

Я використовував ітераційний пошук "вперше вшир", який використовував чергу статусу ", кожна з яких містила запис, що містить поточну точку на графіку та шлях, пройдений туди.

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

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

Тепер ви можете використовувати щось подібне, але коли ви знайдете рішення, замість того, щоб зупинятись, додайте це рішення у свій «знайдений список» та продовжуйте.

Вам потрібно відстежувати список відвідуваних вузлів, щоб ви ніколи не відступали від себе, інакше у вас є нескінченна петля.

якщо ви хочете трохи більше псевдокоду, опублікуйте коментар чи щось таке, і я докладно.


6
Я вважаю, що якщо вас цікавить лише найкоротший шлях, то алгоритм Діккстри - це "рішення" :).
vicatcu

1

Я думаю, ви повинні описати свою справжню проблему. Я говорю це тому, що ви запитуєте про щось ефективне в часі, але відповідь на цю проблему, схоже, зростає експоненціально!

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

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

Використання рекурсії:

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
}

Або це неправильно?

редагувати: О, і я забув: Ви повинні усунути рекурсивні дзвінки, використовуючи цей стек вузлів


Моя реальна проблема - це точно так, як я описав, лише зі значно більшими наборами. Я згоден, це, здається, зростає експоненціально з розміром набору.
Роберт Гроувз

1

Основний принцип - вам не потрібно турбуватися про графіки. Це стандартна проблема, відома як проблема динамічного підключення. Існують такі типи методів, за допомогою яких можна досягти, щоб вузли були підключені чи ні:

  1. Швидкий пошук
  2. Швидкий союз
  3. Вдосконалений алгоритм (поєднання обох)

Ось код C, який я намагався з мінімальною складністю часу O (log * n) Це означає, що для списку ребер 65536 потрібен 4 пошук, а для 2 ^ 65536, він потребує 5 пошуку. Я ділюсь своєю реалізацією за алгоритмом: Курс алгоритму з університету Принстона

ПОРАДА: Ви можете знайти рішення Java за посиланням, яке надано вище, за допомогою відповідних пояснень.

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

Це, здається, не вирішує проблему, як її задали. ОП хоче знайти всі прості шляхи між двома вузлами, а не просто перевірити, чи існує шлях.
Ільмарі Каронен

1

find_paths [s, t, d, k]

На це питання вже давно відповіли. Однак жоден не показує, можливо, гнучкішого алгоритму досягнення того ж самого. Тож я кину капелюшок на ринг.

Я особисто вважаю find_paths[s, t, d, k]корисним алгоритм форми , де:

  • s - початковий вузол
  • t - цільовий вузол
  • d - максимальна глибина пошуку
  • k - кількість шляхів, які потрібно знайти

Використання вашої мови програмування для нескінченності dі kдасть вам усі шляхи§.

§ Очевидно, що якщо ви використовуєте спрямований графік, і ви хочете, щоб усі ненаправлені шляхи між ними, sі tвам доведеться запустити це обома способами:

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

Функція помічника

Мені особисто подобається рекурсія, хоча це може бути важким, але все-таки спочатку давайте зможемо визначити нашу функцію:

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

Основна функція

Якщо це не виходить, основна функція є тривіальною:

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

Спочатку давайте помітимо декілька речей:

  • вищевказаний псевдокод - це розбір мов, але найбільш сильно нагадує python (оскільки я щойно кодував його). Сувора копія-паста не працюватиме.
  • [] є неініціалізованим списком, замініть його на еквівалентну для вашої мови програмування на вибір
  • paths_foundпередається шляхом посилання . Зрозуміло, що функція рекурсії нічого не повертає. Поводьтеся з цим належним чином.
  • тут graphприпускаємо певну форму hashedструктури. Існує безліч способів реалізації графіка. У будь-якому випадку, graph[vertex]отримує вам список суміжних вершин в направленому графіку - відповідно налаштуйте.
  • це передбачає, що ви попередньо обробили для видалення "пряжок" (циклів), циклів і декількох ребер

0

Ось думка в голові:

  1. Знайдіть одне з'єднання. (Швидкий пошук по глибині - це, мабуть, хороший алгоритм для цього, оскільки довжина шляху не має значення.)
  2. Вимкнути останній сегмент.
  3. Спробуйте знайти інше з'єднання з останнього вузла перед раніше відключеним з'єднанням.
  4. Перейдіть на 2, поки більше не буде з'єднань.

Загалом це не спрацює: цілком можливо, що два та більше шляхів між вершинами мають однаковий останній край. Ваш метод знайшов би лише один із таких шляхів.
Ільмарі Каронен

0

Наскільки я можу сказати, рішення, які дають Райан Фокс ( 58343 , Крістіан ( 58444 ) та ти сам ( 58461 )), настільки ж хороші, як і отримують. не одержати всі шляхи. Так , наприклад, з краями (A,B), (A,C), (B,C), (B,D)і (C,D)ви отримаєте шлях ABDі ACD, але не ABCD.


mweerden, Перехід, який я представив на широті, знайде ВСІ шляхи, уникаючи будь-яких циклів. Для вказаного вами графіка реалізація правильно знаходить усі три шляхи.
Кейсі Уотсон

Я не повністю прочитав ваш код і припустив, що ви використовуєте обхід першої широти (тому що ви так сказали). Однак при більш детальному огляді після Вашого коментаря я помітив, що насправді це не так. Це насправді безпам'ятний перший глибинний обхід, як у Райана, Крістіана та Роберта.
mweerden

0

Я знайшов спосіб перерахувати всі шляхи, включаючи нескінченні, що містять петлі.

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

Пошук атомних шляхів та циклів

Definition

Що ми хочемо зробити - це знайти всі можливі шляхи, що йдуть від точки А до точки В. Оскільки в них є цикли, ви не можете просто пройти і перерахувати їх усі. Натомість вам доведеться знайти атомний шлях, який не циркулює та найменші можливі цикли (ви не хочете, щоб ваш цикл повторювався).

Перше визначення, яке я взяв за атомний шлях, - це шлях, який не проходить через один і той же вузол двічі. Однак я з’ясував, що це не використання всіх можливостей. Після деякого роздуму я зрозумів, що вузли не важливі, однак краї є! Отже, атомний шлях - це шлях, який не проходить через один і той же край двічі.

Це визначення зручно, воно також працює для циклів: атомний цикл точки A - атомний шлях, який йде від точки A і закінчується до точки A.

Впровадження

Atomic Paths A -> B

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

Коли ми прибудемо до пункту призначення, ми можемо зберігати знайдений нами шлях.

Freeing the list

Проблема виникає, коли ви хочете звільнити пов'язаний список. Це в основному дерево, приковане в зворотному порядку. Рішенням було б подвійне посилання цього списку і, коли всі атомні шляхи знайдені, звільнити дерево від початкової точки.

Але розумним рішенням є використання довідкового підрахунку (натхненного збиранням сміття). Щоразу, коли ви додаєте посилання на батьків, ви додаєте його до його еталонного рахунку. Потім, коли ви приїдете в кінці шляху, ви переходите назад і вільно, тоді як кількість відліку дорівнює 1. Якщо вона вище, ви просто виймаєте її і зупиняєтесь.

Atomic Cycle A

Шукати атомний цикл А - це те саме, що шукати атомний шлях від А до А. Однак ми можемо зробити кілька оптимізацій. По-перше, коли ми прибуваємо до точки призначення, ми хочемо зберегти шлях лише в тому випадку, якщо сума вартості ребер від’ємна: ми хочемо пройти лише поглинаючі цикли.

Як ви вже бачили раніше, весь графік проїжджається, шукаючи атомний шлях. Натомість ми можемо обмежити область пошуку сильно пов’язаним компонентом, що містить A. Для пошуку цих компонентів потрібне просте пересування графіка з алгоритмом Таряна.

Поєднання атомних шляхів і циклів

На даний момент у нас є всі атомні шляхи, що йдуть від А до В, і всі атомні цикли кожного вузла, залишилося нам організувати все, щоб отримати найкоротший шлях. Відтепер ми будемо вивчати, як знайти найкраще поєднання атомних циклів в атомному шляху.


Схоже, це не дає відповіді на запитання.
Ільмарі Каронен

0

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

Сам алгоритм починається з пускового вузла, який ви йому надаєте, вивчає всі його вихідні посилання та прогресує, розширюючи перший дочірній вузол дерева пошуку, що з’являється, шукаючи прогресивно глибше і глибше, поки не буде знайдений цільовий вузол або поки він не зустріне вузол що не має дітей.

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

Я дуже часто писав про цю тему зовсім недавно, публікуючи приклад реалізації C ++ у процесі.


0

Додавши до відповіді Кейсі Уотсон, ось ще одна реалізація Java. Ініціалізація відвіданого вузла із початковим вузлом.

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();  
                }
            }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.