두 임의의 정점 사이의 모든 연결을 찾기위한 그래프 알고리즘


117

아래에 설명 된 작업을 수행하기 위해 최적의 시간 효율적인 알고리즘을 결정하려고합니다.

나는 일련의 기록을 가지고있다. 이 레코드 세트의 경우이 세트의 레코드 쌍이 서로 연결되는 방법을 나타내는 연결 데이터가 있습니다. 이것은 기본적으로 레코드가 정점이고 연결 데이터가 에지 인 무 방향 그래프를 나타냅니다.

세트의 모든 레코드에는 연결 정보가 있습니다 (즉, 고아 레코드가 존재하지 않으며 세트의 각 레코드는 세트의 하나 이상의 다른 레코드에 연결됩니다).

세트에서 두 개의 레코드를 선택하고 선택한 레코드 사이의 모든 간단한 경로를 표시 할 수 있기를 원합니다. "단순 경로"란 경로에 반복 된 레코드가없는 경로를 의미합니다 (예 : 유한 경로 만).

참고 : 선택한 두 레코드는 항상 다릅니다 (예 : 시작 및 끝 꼭지점이 동일하지 않고 주기도 없음).

예를 들면 :

    다음 기록이있는 경우 :
        에이 비 씨 디이

    다음은 연결을 나타냅니다. 
        (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
루프없는 연결의 유한 목록을 원하는지, 가능한 모든 루프가있는 무한 연결 스트림을 원하는지 말씀해주세요. Cf. Blorgbeard의 대답.
Charles Stewart

누구든지 이것으로 도울 수 있습니까 ??? stackoverflow.com/questions/32516706/...
tejas3006

답변:


116

이것은 그래프의 깊이 우선 검색으로 달성 할 수있는 것으로 보입니다. 깊이 우선 검색은 두 노드 사이의 모든 비순환 경로를 찾습니다. 이 알고리즘은 매우 빠르고 큰 그래프로 확장되어야합니다 (그래프 데이터 구조는 희박하므로 필요한만큼의 메모리 만 사용합니다).

위에서 지정한 그래프에는 방향성 (B, E) 인 모서리가 하나만 있습니다. 이것은 오타였습니까 아니면 실제로 방향성 그래프입니까? 이 솔루션은 상관없이 작동합니다. C로 못해서 미안해, 그 부분이 좀 약하다. 나는 당신이 너무 많은 문제없이이 자바 코드를 번역 할 수있을 것이라고 기대한다.

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

프로그램 출력 :

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

5
이것은 폭 우선 순회가 아닙니다. 너비를 사용하여 먼저 루트까지 거리가 0 인 모든 노드를 방문한 다음 거리가 1, 그다음에 2 인 노드를 방문합니다.
mweerden

14
맞습니다. 이것은 DFS입니다. BFS는 모든 레벨 N 노드 이후에 처리 될 레벨 (N + 1) 노드를 대기열에 추가하는 대기열을 사용해야합니다 . 그러나 OP의 목적을 위해 선호하는 경로 정렬 순서가 지정되지 않았기 때문에 BFS 또는 DFS가 작동합니다.
Matt J

1
케이시, 저는이 문제에 대한 해결책을 오랫동안 찾고있었습니다. 나는 최근 에이 DFS를 C ++로 구현했으며 치료 효과가 있습니다.
AndyUK

6
재귀의 단점은 깊은 그래프 (A-> B-> C-> ...-> N)가 있으면 java에서 StackOverflowError가 발생할 수 있다는 것입니다.
Rrr

1
아래 C #에서 반복 버전을 추가했습니다.
batta

23

NIST (National Institute of Standards and Technology) 온라인 알고리즘 및 데이터 구조 사전은이 문제를 " 모든 단순 경로" 로 나열 하고 깊이 우선 검색을 권장합니다 . CLRS는 관련 알고리즘을 제공합니다.

Petri Nets를 사용하는 영리한 기술이 여기에 있습니다.


2
더 나은 솔루션으로 저를 도울 수 있습니까? DFS 소요 영원히 실행 : stackoverflow.com/q/8342101/632951
Pacerier

두 노드 사이의 모든 단순 경로 집합이 작고 찾기 쉽지만 DFS가 매우 비효율적 인 그래프를 쉽게 찾을 수 있습니다. 예를 들어, 시작 노드 A에 두 개의 인접 항목이있는 무 방향 그래프를 고려하십시오. 목표 노드 B (A 이외의 인접 항목 없음)와 n + 1 노드 의 완전히 연결된 클릭의 일부인 노드 C입니다 . A에서 B까지의 단순한 경로가 분명히 하나 뿐이지 만 순진한 DFS는 파벌을 탐색하는 데 쓸데없이 O ( n !) 시간을 낭비 합니다. 유사한 예 (하나의 솔루션, DFS는 기하 급수적 시간이 소요됨)는 DAG에서도 찾을 수 있습니다.
Ilmari Karonen 2016

NIST는 "경로 깊이 우선 검색 으로 열거 될 수 있습니다 ."라고 말합니다 .
chomp

13

여기 내가 생각해 낸 의사 코드가 있습니다. 이것은 특정 의사 코드 방언은 아니지만 따라갈 수있을만큼 간단해야합니다.

누구나 이것을 분리하고 싶어합니다.

  • [p]는 현재 경로를 나타내는 정점 목록입니다.

  • [x]는 기준을 충족하는 경로 목록입니다.

  • [s]는 소스 정점입니다.

  • [d]는 대상 정점입니다.

  • [c]는 현재 정점 (PathFind 루틴에 대한 인수)입니다.

인접한 정점을 찾는 효율적인 방법이 있다고 가정합니다 (6 행).

     1 경로 목록 [p]
     2 ListOfPathLists [x]
     3 꼭지점 [s], [d]

     4 PathFind (정점 [c])
     5 목록의 끝 [p]에 [c] 추가
     6 [c]에 인접한 각 정점 [v]에 대해
     7 [v]가 [d]와 같으면
     8 [x]에 목록 [p] 저장
     9 Else [v]가 목록에없는 경우 [p]
    10 경로 찾기 ([v])
    11 다음
    12 [p]에서 꼬리 제거
    13 반환

11 단계와 12 단계에 대해 설명해 주시겠습니까
bozo user

11 행은 6 행에서 시작하는 For 루프와 함께가는 끝 블록을 나타냅니다. 12 행은 호출자에게 돌아 가기 전에 경로 목록의 마지막 요소를 제거하는 것을 의미합니다.
Robert Groves

PathFind에 대한 초기 호출은 무엇입니까? 소스 정점을 전달합니까?
광대 사용자

이 예에서는 예,하지만이 의사 코드로 일대일로 매핑되는 실제 코드를 작성하고 싶지 않을 수도 있습니다. 잘 설계된 코드보다는 사고 과정을 설명하기위한 것입니다.
Robert Groves

8

이 답변에 제공된 기존 비 재귀 DFS 구현 손상된 것 같으므로 실제로 작동하는 것을 제공하겠습니다.

파이썬으로 작성했습니다. 왜냐하면 구현 세부 사항에 의해 꽤 읽기 쉽고 복잡하지 않기 때문입니다 (그리고 생성기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항상 현재 노드와 스택의 모든 노드를 포함 하는 별도의 집합을 사용하여 노드가 이미 현재 경로의 일부인지 여부를 효율적으로 확인할 수 있도록합니다. 언어에 효율적인 스택과 같은 푸시 / 팝 작업 효율적인 멤버십 쿼리를 모두 제공하는 "정렬 된 집합"데이터 구조가 있는 경우이를 노드 스택에 사용하고 별도의 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)를 제외하고 동일한 출력이 생성 됩니다.


추신. 이와 같은 간단한 검색 알고리즘 (및이 스레드에 제공된 다른 알고리즘)이 매우 저조한 성능을 발휘하는 그래프를 구성하는 것은 쉽습니다.

예를 들어, 시작 노드 A에 두 개의 이웃이있는 방향이 지정되지 않은 그래프에서 A에서 B까지의 모든 경로를 찾는 작업을 생각해보십시오. 대상 노드 B (A 이외의 다른 이웃이 없음)와 파벌의 일부인 노드 CN 과 같은 하나의 노드 :

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 사이의 유일한 경로가 직접적인 경로라는 것을 쉽게 알 수 있지만, 노드 A에서 시작된 순진한 DFS는 (인간에게) 명백한 경우에도 무리 내에서 경로를 탐색하는 데 O ( n !) 시간을 낭비하게됩니다 . 이러한 경로 중 어느 것도 B로 이어질 수 없습니다.

DAG 를 구성 할 수도 있습니다.예를 들어 시작 노드 A가 대상 노드 B와 두 개의 다른 노드 C 1 및 C 2 를 연결하도록하여 유사한 속성을 가진 를 . 둘 다 노드 D 1 및 D 2 에 연결되며 둘 다 E에 연결됩니다. 1 및 E 2 등. 들면 N 이런 배열 노드 층, O 낭비 끝날 B A에서 모든 경로에 대한 검색 나이브 (2 N 포기하기 전에 모든 가능한 데드 단부 검사) 시간.

물론, (C 이외) 또는 DAG의 마지막 층으로부터 도당의 노드 중 하나에서 타겟 노드 B로 에지를 추가하는 기하 급수적으로 많은 B A에서 가능한 경로의 수, 및를 만들 순전히 로컬 검색 알고리즘은 그러한 에지를 찾을 수 있는지 여부를 미리 알 수 없습니다. 따라서 어떤 의미에서 이러한 순진한 검색의 출력 감도 가 좋지 않은 것은 그래프의 글로벌 구조에 대한 인식 부족 때문입니다.

이러한 "지수 시간 막 다른 골목"중 일부를 피하는 데 사용할 수있는 다양한 전처리 방법 (예 : 리프 노드를 반복적으로 제거, 단일 노드 정점 구분 기호 검색 등)이 있지만 일반적인 방법은 알 수 없습니다. 모든 경우에 그들을 제거 할 수있는 전처리 트릭 . 일반적인 해결책은 검색의 모든 단계에서 대상 노드에 도달 할 수 있는지 (하위 검색을 사용하여) 확인하고 그렇지 않은 경우 조기에 역 추적하는 것입니다.하지만 안타깝게도 검색 속도가 크게 느려집니다 (최악의 경우 , 그래프 크기에 비례 함) 이러한 병리학 적 막 다른 골목을 포함 하지 않는 많은 그래프의 경우 .


1
그것이 제가 찾고있는 것입니다. 감사합니다 :)
arslan

DFS 비 재귀 솔루션에 감사드립니다. 결과를 인쇄하는 마지막 줄에 구문 오류가 있습니다 for path in find_simple_paths(graph, "A", "D"): print(" -> ".join(path)). 이어야합니다 print.에 괄호가 없습니다.
David Oliván Ubieto

1
@ DavidOlivánUbieto : Python 2 코드이므로 괄호가 없습니다. :)
Ilmari Karonen

5

다음은 2 층에 비해 논리적으로 더 나은 재귀 버전입니다.

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

C 코드의 솔루션. 최소 메모리를 사용하는 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

이것은 늦을 수 있지만 여기에는 스택을 사용하여 두 노드 사이의 모든 경로를 탐색하는 Casey의 Java에서 동일한 C # 버전의 DFS 알고리즘이 있습니다. 항상 그렇듯이 재귀를 사용하면 가독성이 더 좋습니다.

    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();
        }
    }
다음은 테스트 할 샘플 그래프입니다.

    // 샘플 그래프. 숫자는 가장자리 ID입니다.
    // 1 3       
    // A --- B --- C ----
    // | | 2 |
    // | 4 ----- D |
    // ------------------

1
우수-재귀를 스택 기반 반복으로 대체 한 방법에 대해.
Siddhartha Ghosh

나는 아직도 그것을 이해하지 못한다 neighbours.Reverse(). 그렇 List<T>.Reverse 습니까?

이 비 재귀 버전을 확인했지만 올바르지 않은 것 같습니다. 재귀 버전은 괜찮습니다. 비 재귀로 변경하면 아마도, 작은 실수는 발생한다
아르 슬란

@alim : 동의합니다.이 코드는 단순히 손상되었습니다. (역 추적 할 때 방문한 세트에서 노드를 올바르게 제거하지 못하며 스택 처리도 엉망인 것처럼 보입니다. 고칠 수 있는지 확인하려고했지만 기본적으로 완전히 다시 작성해야합니다.) 방금 전 정확하고 작동하는 비 재귀 솔루션 으로 답변 을 추가 했습니다 (Python에서는 있지만 다른 언어로 이식하기가 상대적으로 쉽습니다).
일 마리 카로 넨

@llmari Karonen, 니스, 확인하겠습니다, 훌륭합니다.
arslan

1

나는 최근에 이것과 비슷한 문제를 해결했습니다. 모든 솔루션 대신 가장 짧은 것에 만 관심이있었습니다.

나는 그래프의 현재 지점과 거기에 도달하기 위해 취한 경로를 포함하는 레코드를 각각 보유한 상태 대기열을 사용하는 '폭 우선'반복 검색을 사용했습니다.

시작 노드와 빈 경로가있는 대기열의 단일 레코드로 시작합니다.

코드를 반복 할 때마다 목록에서 항목을 가져 와서 솔루션인지 확인합니다 (도착한 노드는 원하는 노드이고 완료되면 완료됩니다). 현재 노드에 연결되는 노드가있는 대기열 항목과 마지막에 새 점프가 연결된 이전 노드의 경로를 기반으로하는 수정 된 경로.

이제 비슷한 것을 사용할 수 있지만 솔루션을 찾으면 중지하는 대신 해당 솔루션을 '찾은 목록'에 추가하고 계속하십시오.

방문한 노드 목록을 추적하여 자신을 역 추적하지 않도록해야합니다. 그렇지 않으면 무한 루프가 발생합니다.

좀 더 의사 코드를 원하면 댓글이나 뭔가를 게시하면 자세히 설명하겠습니다.


6
최단 경로에만 관심이 있다면 Dijkstra의 알고리즘이 "해결책"이라고 생각합니다. :).
vicatcu 2010 년

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
}

아니면 그게 잘못입니까?

편집 : 아, 그리고 잊어 버렸습니다 : 해당 노드 스택을 활용하여 재귀 호출을 제거해야합니다.


내 진짜 문제는 훨씬 더 큰 세트에서만 내가 설명한 것과 똑같습니다. 나는 이것이 세트의 크기에 따라 기하 급수적으로 증가하는 것 같다는 데 동의합니다.
Robert Groves

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

이것은 요청한 문제를 해결하지 못하는 것 같습니다. OP는 단순히 경로가 있는지 확인하는 것이 아니라 두 노드 사이의 모든 단순 경로를 찾으려고합니다.
Ilmari Karonen

1

찾기 _ 경로 [s, t, d, k]

이 질문은 오래되었고 이미 답변되었습니다. 그러나 동일한 작업을 수행하는 데 더 유연한 알고리즘은 없습니다. 그래서 나는 내 모자를 반지에 던질 것이다.

개인적으로 다음과 같은 형식의 알고리즘이 find_paths[s, t, d, k]유용합니다.

  • s는 시작 노드입니다.
  • t는 대상 노드입니다.
  • d는 검색 할 최대 깊이입니다.
  • k는 찾을 경로의 수입니다.

무한대의 프로그래밍 언어의 양식을 사용 d하고 k당신에게 모든 paths§을 줄 것이다.

§ 분명히 당신은 유향 그래프를 사용하고 모든 원하는 경우 방향성 사이의 경로를 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)

먼저, 몇 가지 사항에 주목하십시오.

  • 위의 의사 코드는 언어의 매시업이지만 가장 강하게 파이썬과 닮았습니다 (단순히 코딩했기 때문에). 엄격한 복사-붙여 넣기는 작동하지 않습니다.
  • [] 초기화되지 않은 목록입니다.이 목록을 선택한 프로그래밍 언어에 해당하는 것으로 바꿉니다.
  • paths_found참조 로 전달됩니다 . 재귀 함수가 아무것도 반환하지 않는 것이 분명합니다. 적절하게 처리하십시오.
  • 여기 graph에 어떤 형태의 hashed구조를 가정하고 있습니다. 그래프를 구현하는 방법에는 여러 가지가 있습니다. 어느 쪽이든, 방향성 그래프 graph[vertex]에서 인접한 정점 목록을 가져옵니다 . 그에 따라 조정하십시오.
  • 이것은 "버클"(자체 루프), 사이클 및 다중 에지를 제거하기 위해 사전 처리했다고 가정합니다.

0

내 머릿속에서 떠오르는 생각이 있습니다.

  1. 하나의 연결을 찾으십시오. (경로 길이는 중요하지 않기 때문에 깊이 우선 검색은 아마도이를위한 좋은 알고리즘 일 것입니다.)
  2. 마지막 세그먼트를 비활성화합니다.
  3. 이전에 비활성화 된 연결 전에 마지막 노드에서 다른 연결을 찾으십시오.
  4. 더 이상 연결이 없을 때까지 2로 이동합니다.

이것은 일반적으로 작동하지 않습니다. 정점 사이의 두 개 이상의 경로가 동일한 마지막 가장자리를 가질 수 있습니다. 귀하의 방법은 그러한 경로 중 하나만 찾습니다.
Ilmari Karonen

0

Ryan Fox ( 58343 , Christian ( 58444 ) 및 사용자 자신 ( 58461 )가 제공 한 솔루션 이 얻을 수있는만큼 훌륭하다고 말할 수 있습니다.이 경우 너비 우선 탐색이 도움이된다고 생각하지 않습니다. 하지 가장자리, 예를 들어. 모든 경로를 얻을 수 (A,B), (A,C), (B,C), (B,D)그리고 (C,D)당신은 경로를 얻을 것이다 ABD하고 ACD있지만 ABCD.


mweerden, 내가 제출 한 폭 우선 순회는 모든주기를 피하면서 모든 경로를 찾을 것입니다. 지정한 그래프의 경우 구현에서 세 경로를 모두 올바르게 찾습니다.
Casey Watson

나는 당신의 코드를 완전히 읽지 않았고 (당신이 그렇게 말했기 때문에) 너비 우선 순회를 사용했다고 가정했습니다. 그러나 귀하의 의견 후 자세히 살펴보면 실제로 그렇지 않다는 것을 알았습니다. 실제로 Ryan, Christian 및 Robert와 같은 메모리없는 깊이 우선 순회입니다.
mweerden

0

루프를 포함하는 무한 경로를 포함하여 모든 경로를 열거하는 방법을 찾았습니다.

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

원자 경로 및 순환 찾기

Definition

우리가 원하는 것은 A 지점에서 B 지점으로가는 가능한 모든 경로를 찾는 것입니다. 관련된주기가 있기 때문에 그것들을 모두 열거 할 수는 없습니다. 대신 반복되지 않는 원자 경로와 가능한 가장 작은주기를 찾아야합니다 (주기 자체가 반복되는 것을 원하지 않음).

원자 경로에 대한 첫 번째 정의는 동일한 노드를 두 번 통과하지 않는 경로입니다. 그러나 나는 그것이 모든 가능성을 가지고 있지 않다는 것을 알았습니다. 약간의 반성 끝에 노드는 중요하지 않지만 가장자리는 중요하다는 것을 알았습니다! 따라서 원자 경로는 동일한 가장자리를 두 번 통과하지 않는 경로입니다.

이 정의는 편리하며주기에도 적용됩니다. A 지점의 원자주기는 A 지점에서 A 지점으로 끝나는 원자 경로입니다.

이행

Atomic Paths A -> B

A 지점에서 시작하는 모든 경로를 얻기 위해 A 지점에서 반복적으로 그래프를 횡단합니다. 자식을 통과하는 동안 우리는 모든 모서리를 알기 위해 링크 자식-> 부모를 만들 것입니다. 이미 교차했습니다. 그 자식에게 가기 전에, 우리는 그 연결 목록을 순회하고 지정된 가장자리가 이미지나 가지 않았는지 확인해야합니다.

목적지에 도착하면 찾은 경로를 저장할 수 있습니다.

Freeing the list

연결 목록을 해제하려는 경우 문제가 발생합니다. 기본적으로 역순으로 연결된 트리입니다. 해결책은 해당 목록을 이중 연결하고 모든 원자 경로가 발견되면 시작점에서 트리를 해제하는 것입니다.

그러나 현명한 해결책은 참조 계수를 사용하는 것입니다 (Garbage Collection에서 영감을 얻음). 상위 링크를 추가 할 때마다 참조 횟수에 링크를 추가합니다. 그런 다음 경로의 끝에 도착하면 참조 횟수가 1이되는 동안 뒤로 이동하여 자유 로워집니다. 더 높으면 하나를 제거하고 중지합니다.

Atomic Cycle A

A의 원자주기를 찾는 것은 A에서 A 로의 원자 경로를 찾는 것과 같습니다. 그러나 우리가 할 수있는 몇 가지 최적화가 있습니다. 첫째, 목적지 지점에 도착했을 때 가장자리 비용의 합이 음수 인 경우에만 경로를 저장하고 싶습니다. 흡수 주기만 거치고 싶습니다.

이전에 보았 듯이 원자 경로를 찾을 때 전체 그래프가 순회되고 있습니다. 대신 A를 포함하는 강력하게 연결된 구성 요소로 검색 영역을 제한 할 수 있습니다. 이러한 구성 요소를 찾으려면 Tarjan의 알고리즘을 사용하여 그래프를 간단히 탐색해야합니다.

원자 경로와 순환 결합

이 시점에서 우리는 A에서 B로가는 모든 원자 경로와 각 노드의 모든 원자주기를 가지고 있으며, 최단 경로를 얻기 위해 모든 것을 구성하도록 남겨 둡니다. 이제부터 우리는 원자 경로에서 원자주기의 최상의 조합을 찾는 방법을 연구 할 것입니다.


이것은 질문에 대한 대답이 아닌 것 같습니다.
Ilmari Karonen

0

다른 포스터 중 일부에서 잘 설명했듯이 한마디로 문제는 깊이 우선 검색 알고리즘을 사용하여 통신하는 종단 노드 간의 모든 경로 조합에 대해 그래프를 반복적으로 검색하는 것입니다.

알고리즘 자체는 사용자가 제공 한 시작 노드로 시작하여 모든 나가는 링크를 검사하고 나타나는 검색 트리의 첫 번째 하위 노드를 확장하고 대상 노드를 찾을 때까지 또는 노드를 만날 때까지 점진적으로 더 깊고 깊게 검색하여 진행합니다. 자녀가 없습니다.

그런 다음 검색이 역 추적되어 아직 탐색을 완료하지 않은 가장 최근 노드로 돌아갑니다.

나는 이 주제에 대해 아주 최근에 블로그올렸고 그 과정에서 예제 C ++ 구현을 게시했습니다.


0

Casey Watson의 답변에 추가하면 여기에 또 다른 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();  
                }
            }
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.