2つの任意の頂点間のすべての接続を見つけるグラフアルゴリズム


117

以下で説明するタスクを実行するための最適な時間効率の良いアルゴリズムを決定しようとしています。

一連のレコードがあります。このレコードのセットには、このセットのレコードのペアが相互に接続する方法を示す接続データがあります。これは基本的に無向グラフを表し、レコードは頂点であり、接続データはエッジです。

セット内のすべてのレコードには接続情報があります(つまり、孤立したレコードはありません。セット内の各レコードは、セット内の他の1つ以上のレコードに接続しています)。

セットから任意の2つのレコードを選択し、選択したレコード間のすべての単純なパスを表示できるようにしたいと思います。「単純なパス」とは、パスに繰り返しレコードがないパス(つまり、有限パスのみ)を意味します。

注:選択した2つのレコードは常に異なります(つまり、開始頂点と終了頂点が同じになることはなく、サイクルはありません)。

例えば:

    次の記録がある場合:
        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
ループのない接続の有限リストが必要か、すべての可能なループを含む接続の無限ストリームが必要かどうかを言ってください。Cf. Blorgbeardの答え。
Charles Stewart

誰でもこれを手伝ってくれる??? stackoverflow.com/questions/32516706/...
tejas3006

回答:


116

これは、グラフの縦型検索で実行できるようです。深さ優先検索では、2つのノード間のすべての非循環パスが検出されます。このアルゴリズムは非常に高速で、大きなグラフに合わせてスケーリングする必要があります(グラフのデータ構造はスパースであるため、必要なだけのメモリを使用します)。

上記で指定したグラフには、方向性(B、E)のエッジが1つしかないことに気付きました。これはタイプミスですか、それとも本当に有向グラフですか?このソリューションは関係なく機能します。申し訳ありませんが、Cでそれを行うことができませんでした。その領域は少し苦手です。この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);
    }
}

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
これは幅優先トラバーサルではないことに注意してください。広さで最初の最初など、その後の距離1、2、とそれら、ルートに距離0を持つすべてのノードを訪問
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#に反復バージョンを追加しました。
バッタ2014年

23

米国国立標準技術研究所(NIST)のアルゴリズムとデータ構造のオンライン辞書は、この問題を「すべての単純なパス」としてリストし、深さ優先検索を推奨しています。CLRSは関連するアルゴリズムを提供します。

ペトリネットを使用した巧妙なテクニックはここにあります


2
より良い解決策を教えていただけますか?DFSの実行に永遠にかかる:stackoverflow.com/q/8342101/632951
Pacerier

2つのノード間のすべての単純なパスのセットが小さく、簡単に見つけることができる場合でも、DFSが非常に非効率的なグラフを作成するのは簡単です。たとえば、開始ノードAに2つの隣接ノードがある無向グラフを考えてみます。ゴールノードB(A以外の隣接ノードはありません)と、n + 1ノードの完全に接続されたクリークの一部であるノードCです。AからBへの単純なパスが1つしかないことは明らかですが、ナイーブなDFSはO(n!)の時間を無駄にして、無駄にクリークを探索します。同様の例(1つのソリューション、DFSには指数関数的な時間がかかります)もDAGの中にあります。
Ilmari Karonen、2016

NISTは言う:「パス、深さ優先検索列挙される可能性があります。」
2017

13

これが私が思いついた疑似コードです。これは特定の疑似コードの方言ではありませんが、理解するのに十分簡単なはずです。

だれでもこれを分けたいと思う。

  • [p]は、現在のパスを表す頂点のリストです。

  • [x]は、基準を満たすパスのリストです

  • [s]はソース頂点です

  • [d]は宛先頂点です

  • [c]は現在の頂点です(PathFindルーチンへの引数)。

隣接する頂点を検索する効率的な方法があると仮定します(6行目)。

     1つのPathList [p]
     2 ListOfPathLists [x]
     3頂点[s]、[d]

     4 PathFind(頂点[c])
     5リスト[p]の末尾に[c]を追加します
     6 [c]に隣接する各頂点[v]
     7 [v]が[d]と等しい場合
     8リスト[p]を[x]に保存
     9その他[v]がリストにない場合[p]
    10 PathFind([v])
    11次の
    12 [p]から尾を削除
    13戻る

ステップ11とステップ12に光を当てていただけますか
bozoユーザー

11行目は、6行目から始まるForループに続く終了ブロックを示しています。12行目は、呼び出し元に戻る前にパスリストの最後の要素を削除することを意味しています。
Robert Groves

PathFindの最初の呼び出しは何ですか-ソース頂点[s]を渡しますか?
bozoユーザー

この例では「はい」ですが、この疑似コードと1対1で対応する実際のコードを記述したくない場合があることに注意してください。うまく設計されたコードではなく、思考プロセスを説明するためのものです。
Robert Groves 2013

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

このコードは2つの並列スタックを維持します。1つは現在のパスの以前のノードを含み、もう1つはノードスタックの各ノードの現在のネイバーインデックスを含みます(これにより、ノードをポップオフしたときにノードのネイバーの反復を再開できます。スタック)。(ノード、インデックス)ペアの単一スタックを同様に使用することもできましたが、2スタック方式の方が読みやすく、おそらく他の言語のユーザーが実装しやすいと考えました。

このコードvisitedは、現在のノードとスタック上のノードを常に含む別のセットも使用して、ノードが既に現在のパスの一部であるかどうかを効率的に確認できるようにしています。言語に、効率的なスタックのようなプッシュ/ポップ操作効率的なメンバーシップクエリの両方を提供する「順序付けられたセット」のデータ構造がある場合、それをノードスタックに使用して、個別のvisitedセットを取り除くことができます。

または、ノードにカスタムの可変クラス/構造を使用している場合は、各ノードにブールフラグを格納して、現在の検索パスの一部としてアクセスされたかどうかを示すことができます。もちろん、この方法では、何らかの理由で同じグラフに対して2つの検索を並行して実行することはできません。

上記の関数がどのように機能するかを示すテストコードを次に示します。

# 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 -> BBのネイバーリストから削除することにより)エッジを削除するCと、3番目のパス(A -> C -> B -> D)を除いて同じ出力が生成されますが、これは不可能です。


Ps。このような単純な検索アルゴリズム(およびこのスレッドで提供されるその他のアルゴリズム)のパフォーマンスが非常に低いグラフを作成するのは簡単です。

たとえば、開始ノードAに2つの隣接ノードがある無向グラフ上でAからBへのすべてのパスを検索するタスクを考えてみます。ターゲットノードB(A以外の隣接ノードはありません)とクリークの一部であるノードC 次のように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の間の唯一のパスが直接パスであることは簡単にわかりますが、ノードAから開始された単純なDFSは、O(n!)時間を無駄にします。これらのパスのいずれもBにつながる可能性はありません。

一つも構築することができるのDAG開始ノードAの接続ターゲットノードBと他の2つのノードCに有することにより、例えば類似の特性を有するが1とC 2のノードに接続どちらもが、D 1およびD 2 Eに接続どちらも、1とE 2など。以下のために、Nのノードの層がこのように配置され、BのAからのすべてのパスのためのナイーブ検索はO(2無駄にしてしまうNあきらめる前に、すべての可能な行き止まりを調べる)時間。

もちろん、(C以外)クリーク内のノードのいずれかからターゲットノードBにエッジを追加する、またはDAGの最後の層からなる指数関数的に大きいBのAからの可能な経路の数、及びAを作成します純粋なローカル検索アルゴリズムは、そのようなエッジを見つけるかどうかを事前に実際に判断することはできません。したがって、ある意味で、このような単純な検索の出力感度が低いのは、グラフのグローバル構造を認識していないためです。

これらの「指数時間の行き止まり」の一部を回避するために使用できるさまざまな前処理方法(リーフノードの反復的な削除、単一ノードの頂点セパレーターの検索など)がありますが、一般的なことはわかりませんすべてのケースでそれらを排除できる前処理のトリック。一般的な解決策は、ターゲットノードがまだ到達可能かどうかを(サブサーチを使用して)検索のすべてのステップでチェックし、到達できない場合は早期にバックトラックすることです。 、グラフのサイズに比例します)このような病理学的行き止まりを含まない多くのグラフ。


1
それが私が探しているものです、ありがとう:)
arslan

DFSの非再帰的なソリューションをありがとうございます。ただ、結果を印刷する最後の行に構文エラーがあり注意し、する必要がありfor path in find_simple_paths(graph, "A", "D"): print(" -> ".join(path))printかっこがありませんでした。
DavidOlivánUbieto 18

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

これは遅いかもしれませんが、スタックを使用して2つのノード間のすべてのパスをトラバースする、CaseyからJavaのDFSアルゴリズムの同じC#バージョンを次に示します。可読性はいつものように再帰的に優れています。

    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 2015

まだ理解できませんneighbours.Reverse()。何ですか?ですよList<T>.Reverse ね?

この非再帰バージョンを確認しましたが、正しくないようです。再帰バージョンは問題ありません。非再帰に変更すると、小さな間違いが発生する可能性があります
arslan

@alim:同意しました。このコードは単に壊れています。(バックトラック時に訪問済みセットからノードを正しく削除せず、スタック処理もめちゃくちゃになっているように見えます。修正できるかどうかを確認しようとしましたが、基本的には完全な書き換えが必要になります。)正しく機能する非再帰的ソリューションを使用して回答を追加しまし(Pythonでは、他の言語への移植は比較的簡単です)。
Ilmari Karonen、2016

@llmari Karonen、ニース、私はチェックするつもりです、素晴らしい仕事。
arslan

1

私はこれに似た問題を最近解決しましたが、最短のものだけに関心があったすべてのソリューションの代わりに。

ステータスキューを使用する「幅優先」反復検索を使用しました。それぞれ、グラフ上の現在のポイントとそこに到達するためにたどったパスを含むレコードを保持していました。

開始ノードと空のパスを持つキュー内の単一のレコードから開始します。

コードを反復するたびに、リストの先頭から項目が削除され、それがソリューションであるかどうかが確認されます(到達したノードは、目的のノードである場合は、完了です)。それ以外の場合は、新しい現在のノードに接続しているノードと、前のノードのパスに基づいて修正されたパスを持つキューアイテム。新しいジャンプが最後に追加されます。

これで、類似したものを使用できますが、解決策を見つけたら、停止する代わりに、その解決策を「見つかったリスト」に追加して続行します。

訪問したノードのリストを追跡する必要があります。そうしないと、自分でバックトラックしないでください。そうしないと、無限ループが発生します。

もう少し疑似コードが必要な場合はコメントや何かを投稿してください。詳しく説明します。


6
私はあなたが最短経路にのみ興味があるなら、ダイクストラのアルゴリズムが「解決策」だと思います:)。
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 2010

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は、パスが存在するかどうかを確認するだけでなく、2つのノード間のすべての単純なパスを検索する必要があります。
Ilmari Karonen 2016

1

find_paths [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)

最初に、いくつかのことに注意しましょう:

  • 上記の疑似コードは言語のマッシュアップですが、Pythonに最もよく似ています(私がコーディングしているだけなので)。厳密なコピー貼り付けは機能しません。
  • [] 初期化されていないリストです。選択したプログラミング言語に相当するものに置き換えてください
  • paths_found参照渡しされます。再帰関数が何も返さないことは明らかです。これを適切に処理してください。
  • ここでgraphは、何らかの形のhashed構造を想定しています。グラフを実装する方法はたくさんあります。どちらの方法graph[vertex]でも、有向で隣接する頂点のリストを取得しますグラフを-それに応じて調整します。
  • これは、「バックル」(セルフループ)、サイクル、およびマルチエッジを削除するように前処理されていることを前提としています。

0

ここに私の頭の上の考えがあります:

  1. 1つの接続を見つけます。(パスの長さが問題ではないため、深さ優先検索はおそらくこれに適したアルゴリズムです。)
  2. 最後のセグメントを無効にします。
  3. 以前に無効にした接続の前に、最後のノードから別の接続を見つけてください。
  4. 接続がなくなるまで2に進みます。

これは一般的には機能しません。頂点間の2つ以上のパスが同じ最後のエッジを持つ可能性は十分にあります。あなたの方法はそのようなパスの1つだけを見つけるでしょう。
Ilmari Karonen 2016

0

私の知る限りは、ライアン・フォックス(で与えソリューション言うことができるように58343、クリスチャン(58444)、そして自分自身(58461を)それが得ると良いと同程度です。あなたがするように私は、その幅優先トラバースが、この場合に役立ちます信じていません。すべてのパスを取得できません。例えば、エッジと(A,B)(A,C)(B,C)(B,D)(C,D)あなたのパスを取得しますABDACD、ではなくABCD


mweerden、私が送信した幅優先トラバーサルは、サイクルを回避しながらすべてのパスを見つけます。指定したグラフの場合、実装は3つのパスすべてを正しく検出します。
ケーシーワトソン

私はコードを完全には読んでおらず、幅優先のトラバーサルを使用していると想定していました(そう言ったため)。しかし、あなたのコメントの後の詳細な調査で、私は実際にはそうではないことに気付きました。これは実際には、ライアン、クリスチャン、ロバートのような、記憶のない深さ優先のトラバーサルです。
mweerden 2008

0

ループを含む無限パスを含むすべてのパスを列挙する方法を見つけました。

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

アトミックパスとサイクルを見つける

Definition

ここでは、ポイントAからポイントBに至るすべての可能なパスを見つけます。サイクルが含まれているため、すべてを通過して列挙することはできません。代わりに、ループしないアトミックパスと可能な限り最小のサイクルを見つける必要があります(サイクルが繰り返されないようにする必要があります)。

私が最初に取ったアトミックパスの定義は、同じノードを2回通過しないパスです。しかし、それがすべての可能性を引き受けているわけではないことがわかりました。いくつかの反射の後、ノードは重要ではないが、エッジは重要であることがわかりました。したがって、アトミックパスは、同じエッジを2回通過しないパスです。

この定義は便利で、サイクルにも適用されます。ポイントAのアトミックサイクルは、ポイントAからポイントAまでのアトミックパスです。

実装

Atomic Paths A -> B

ポイントAから始まるすべてのパスを取得するために、ポイントAから再帰的にグラフをトラバースします。子を通過しながら、リンクの子->親を作成して、すべてのエッジを確認しますすでに交差しています。その子に移動する前に、そのリンクリストをたどり、指定されたエッジがまだウォークスルーされていないことを確認する必要があります。

目的地に到着したら、見つけた経路を保存できます。

Freeing the list

リンクリストを解放するときに問題が発生します。基本的には逆の順序でチェーンされたツリーです。解決策は、そのリストをダブルリンクし、すべてのアトミックパスが見つかったら、ツリーを開始点から解放することです。

しかし、賢い解決策は、(ガベージコレクションに触発された)参照カウントを使用することです。親にリンクを追加するたびに、参照カウントにリンクを追加します。次に、パスの最後に到達すると、参照カウントが1に等しい間、後退して自由になります。それより高い場合は、1つを削除して停止します。

Atomic Cycle A

Aのアトミックサイクルを探すことは、AからAへのアトミックパスを探すことと同じです。ただし、いくつかの最適化を行うことができます。最初に、目的地点に到着したとき、エッジコストの合計が負の場合にのみパスを保存します。吸収サイクルのみを通過する必要があります。

以前に見たように、アトミックパスを探すときにグラフ全体がトラバースされています。代わりに、Aを含む強く接続されたコンポーネントに検索領域を制限できます。これらのコンポーネントを見つけるには、Tarjanのアルゴリズムを使用してグラフを単純にトラバースする必要があります。

アトミックパスとサイクルの組み合わせ

この時点で、AからBへのすべてのアトミックパスと各ノードのすべてのアトミックサイクルが得られました。最短のパスを取得するためにすべてを整理する必要があります。これからは、原子パス内の原子サイクルの最適な組み合わせを見つける方法を研究します。


これは質問されたとおりの質問には答えないようです。
Ilmari Karonen 2016

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();  
                }
            }
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.