查找两个图节点之间的所有路径


72

我正在研究Dijkstras算法的实现,以检索路由网络上互连节点之间的最短路径。我正在努力。当我将起始节点传递到算法中时,它将返回所有节点的所有最短路径。

我的问题:如何检索从节点A到节点G的所有可能路径,甚至从节点A到节点A的所有可能路径。


1
好吧,如果您的图形具有循环,则该列表可能会非常长。
zmccord 2012年

您是否想要不重复顶点/边的路径?
利亚姆·孟塞尔

@ HexTree我不太清楚你的意思。每个顶点都是唯一的。我基本上是在寻找每条路径的权重以及通过每条路径触及的节点数
Paul Paul

为什么要查找所有路径?如果您的问题是在某些节点发生故障等时如何重新路由,则有一些算法(启发式)。但是您目前的情况非常笼统,很难理解。
Saeed Amiri'3

1
嗨,保罗,您解决了这个问题吗?这是一个可能对您有用的链接:geeksforgeeks.org/find-paths-given-source-destination
GoingMyWay

Answers:


58

找到所有可能的路径是一个难题,因为简单路径的数量是指数的。甚至找到第k条最短路径(或最长路径)也是NP-Hard

查找从s到的所有路径(或直到一定长度的所有路径)的一种可能解决方案tBFS,而不保留visited集合,或者对于加权版本-您可能要使用统一成本搜索

请注意,在每个具有周期[不是DAG ]的图中,也可能有s到之间的无限数量的路径t


谢谢阿米特,我将尝试研究BFS或统一费用搜索
Paul

1
@Paul:不客气。只要确保它们两个都不使用visited集合(如原始算法所建议的那样),否则您将仅获得部分路径。另外,您应该将路径限制为一定长度,以避免无限循环[如果图形具有循环...]。祝好运!
阿米特2012年

3
@VShreyas这是一个旧线程,答案专门说“所有路径达到一定长度”,并且可以使用BFS来完成而无需访问集合。如果要使用两个节点之间的所有简单路径,则可以使用具有“本地”访问集的DFS(在回溯时从访问集中删除节点)来实现。
艾米特

1
@GarethRees假设有一个用于两个节点之间k最短简单路径的多项式时间(非伪多项式)算法。由于最多有(3/2)n!这样的路径,因此您可以进行二进制搜索并查找是否存在一个简单的length路径n。由于log{(3/2)n!} 是中的多项式n,因此编码数量和所需的重复次数都是输入大小的多项式。由于该算法也为输入运行多项式时间,因此总运行时间为多项式,答案是节点之间是否存在哈密顿路径。因此,如果存在这种算法,则P = NP。
amit

1
@GarethRees附录:路径数,选择路径中的节点数(i = 1,....,n),对于每个i,选择(n,i)个节点,并对它们进行重新排序(i!),得到:(3/2)n!
amit

12

我已经实现了一个版本,该版本基本上可以找到从一个节点到另一个节点的所有可能路径,但是它不计算任何可能的“周期”(我使用的图形是循环的)。因此,基本上,没有一个节点会在同一路径中出现两次。如果图是非循环的,那么我想您可以说它似乎找到了两个节点之间的所有可能路径。它似乎工作得很好,并且在我的图形大小约为150的情况下,它几乎可以立即在我的计算机上运行,​​尽管我确信运行时间必须是指数级的,因此随着图变大。

这是一些Java代码,演示了我实现的内容。我确信也必须有更有效或更优雅的方法来做到这一点。

Stack connectionPath = new Stack();
List<Stack> connectionPaths = new ArrayList<>();
// Push to connectionsPath the object that would be passed as the parameter 'node' into the method below
void findAllPaths(Object node, Object targetNode) {
    for (Object nextNode : nextNodes(node)) {
       if (nextNode.equals(targetNode)) {
           Stack temp = new Stack();
           for (Object node1 : connectionPath)
               temp.add(node1);
           connectionPaths.add(temp);
       } else if (!connectionPath.contains(nextNode)) {
           connectionPath.push(nextNode);
           findAllPaths(nextNode, targetNode);
           connectionPath.pop();
        }
    }
}

有这个的非递归版本吗?
arslan '02

1
我没有一个,但是,但我认为从理论上讲,任何递归程序都可以转换为非递归程序,我认为通过使用类似栈对象的方法,重点是模拟递归程序实际上在做什么我相信使用程序的堆栈空间。您可以查找将递归程序转换为非递归程序的原理。
Omer Hassan

11

我要给你一个(有点小)版本(尽管我可以理解)的科学证明,证明您在可行的时间内无法做到这一点。

我要证明的是,枚举任意图中两个选定的不同节点(例如st)之间的所有简单路径的时间复杂度G不是多项式。注意,由于我们仅关心这些节点之间的路径数量,因此边缘成本并不重要。

当然,如果图形具有一些精心选择的属性,则这很容易。我正在考虑一般情况。


假设我们有一个多项式算法,列出了s和之间的所有简单路径t

如果G已连接,则列表为非空。如果Gnot和sandt位于不同的组件中,则列出它们之间的所有路径确实很容易,因为没有路径!如果它们在同一个组件中,我们可以假装整个图仅由该组件组成。因此,我们假设G确实已连接。

然后,列出的路径数必须是多项式,否则算法无法将它们全部归还给我。如果它枚举了所有这些,它必须给我最长的一个,所以它就在那里。有了路径列表,可以应用一个简单的过程来指向我这是最长的路径。

我们可以证明(尽管我无法想出一种有凝聚力的说法),这条最长的路径必须遍历的所有顶点G。因此,我们刚刚找到了具有多项式过程的哈密​​顿路径!但这是一个众所周知的NP难题。

然后我们可以得出结论,除非P = NP,否则我们认为存在的这种多项式算法极不可能存在。


如果我理解正确,则该证明仅适用于无向图,因为在有向图中,“此最长路径必须遍历所有顶点G”的主张不一定成立。是对的吗?
boycy 2014年

是的,是的,但是您可以使用算法以类似的方式回答是否存在有向汉密尔顿路径,并且该路径也是NP完全的。如果您的答案是n-1,则存在。如果不是,则不可能有这样的路径,否则它将比您知道的最长的路径更长。
2014年

1
只是要清楚。如果定向版本可以在聚合时间内解决,那么答案将为定向哈密顿路径。此外,如果我们有加权边,则可以证明通过多项式过程,我们可以回答旅行商问题。
2014年

4

是一种使用DFS修改查找和打印从s到t的所有路径的算法。动态编程也可以用于查找所有可能路径的计数。伪代码将如下所示:

AllPaths(G(V,E),s,t)
 C[1...n]    //array of integers for storing path count from 's' to i
 TopologicallySort(G(V,E))  //here suppose 's' is at i0 and 't' is at i1 index

  for i<-0 to n
      if i<i0
          C[i]<-0  //there is no path from vertex ordered on the left from 's' after the topological sort
      if i==i0
         C[i]<-1
      for j<-0 to Adj(i)
          C[i]<- C[i]+C[j]

 return C[i1]

2

find_paths [s,t,d,k]

这个问题现在有点老了……但是我会把我的帽子扔进去。

我个人发现一种find_paths[s, t, d, k]有用形式的算法,其中:

  • s是起始节点
  • t是目标节点
  • d是要搜索的最大深度
  • k是要查找的路径数

用无穷大的编程语言的形式d,并k会给你所有paths§。

§显然,如果您使用的是有向图,并且希望它们之间所有无方向的路径st则必须同时运行以下两种方法:

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]都可以在有图中获取相邻顶点的列表-进行相应调整。
  • 这假定您已进行了去除“扣环”(自环),循环和多边的预处理

2

如果您真正关心的是从最短路径到最长路径的顺序排序,那么使用改进的A *或Dijkstra算法会更好。稍作修改,该算法将以最短路径优先的顺序返回所需的尽可能多的路径。因此,如果您真正想要的是从最短到最长的所有可能路径排序,那么这就是要走的路。

如果您想要一个基于A *的实现,该实现能够返回从最短到最长的所有路径排序,那么下面的操作将实现这一点。它有几个优点。首先,它可以有效地从最短到最长排序。此外,它仅在需要时才计算每个其他路径,因此如果您由于不需要每个路径而提早停止,则可以节省一些处理时间。每次计算下一条路径时,它还会为后续路径重用数据,因此效率更高。最后,如果找到了所需的路径,则可以中止,从而节省一些计算时间。总体而言,如果您关心按路径长度排序,那么这应该是最有效的算法。

import java.util.*;

public class AstarSearch {
    private final Map<Integer, Set<Neighbor>> adjacency;
    private final int destination;

    private final NavigableSet<Step> pending = new TreeSet<>();

    public AstarSearch(Map<Integer, Set<Neighbor>> adjacency, int source, int destination) {
        this.adjacency = adjacency;
        this.destination = destination;

        this.pending.add(new Step(source, null, 0));
    }

    public List<Integer> nextShortestPath() {
        Step current = this.pending.pollFirst();
        while( current != null) {
            if( current.getId() == this.destination )
                return current.generatePath();
            for (Neighbor neighbor : this.adjacency.get(current.id)) {
                if(!current.seen(neighbor.getId())) {
                    final Step nextStep = new Step(neighbor.getId(), current, current.cost + neighbor.cost + predictCost(neighbor.id, this.destination));
                    this.pending.add(nextStep);
                }
            }
            current = this.pending.pollFirst();
        }
        return null;
    }

    protected int predictCost(int source, int destination) {
        return 0; //Behaves identical to Dijkstra's algorithm, override to make it A*
    }

    private static class Step implements Comparable<Step> {
        final int id;
        final Step parent;
        final int cost;

        public Step(int id, Step parent, int cost) {
            this.id = id;
            this.parent = parent;
            this.cost = cost;
        }

        public int getId() {
            return id;
        }

        public Step getParent() {
            return parent;
        }

        public int getCost() {
            return cost;
        }

        public boolean seen(int node) {
            if(this.id == node)
                return true;
            else if(parent == null)
                return false;
            else
                return this.parent.seen(node);
        }

        public List<Integer> generatePath() {
            final List<Integer> path;
            if(this.parent != null)
                path = this.parent.generatePath();
            else
                path = new ArrayList<>();
            path.add(this.id);
            return path;
        }

        @Override
        public int compareTo(Step step) {
            if(step == null)
                return 1;
            if( this.cost != step.cost)
                return Integer.compare(this.cost, step.cost);
            if( this.id != step.id )
                return Integer.compare(this.id, step.id);
            if( this.parent != null )
                this.parent.compareTo(step.parent);
            if(step.parent == null)
                return 0;
            return -1;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Step step = (Step) o;
            return id == step.id &&
                cost == step.cost &&
                Objects.equals(parent, step.parent);
        }

        @Override
        public int hashCode() {
            return Objects.hash(id, parent, cost);
        }
    }

   /*******************************************************
   *   Everything below here just sets up your adjacency  *
   *   It will just be helpful for you to be able to test *
   *   It isnt part of the actual A* search algorithm     *
   ********************************************************/

    private static class Neighbor {
        final int id;
        final int cost;

        public Neighbor(int id, int cost) {
            this.id = id;
            this.cost = cost;
        }

        public int getId() {
            return id;
        }

        public int getCost() {
            return cost;
        }
    }

    public static void main(String[] args) {
        final Map<Integer, Set<Neighbor>> adjacency = createAdjacency();
        final AstarSearch search = new AstarSearch(adjacency, 1, 4);
        System.out.println("printing all paths from shortest to longest...");
        List<Integer> path = search.nextShortestPath();
        while(path != null) {
            System.out.println(path);
            path = search.nextShortestPath();
        }
    }

    private static Map<Integer, Set<Neighbor>> createAdjacency() {
        final Map<Integer, Set<Neighbor>> adjacency = new HashMap<>();

        //This sets up the adjacencies. In this case all adjacencies have a cost of 1, but they dont need to.
        addAdjacency(adjacency, 1,2,1,5,1);         //{1 | 2,5}
        addAdjacency(adjacency, 2,1,1,3,1,4,1,5,1); //{2 | 1,3,4,5}
        addAdjacency(adjacency, 3,2,1,5,1);         //{3 | 2,5}
        addAdjacency(adjacency, 4,2,1);             //{4 | 2}
        addAdjacency(adjacency, 5,1,1,2,1,3,1);     //{5 | 1,2,3}

        return Collections.unmodifiableMap(adjacency);
    }

    private static void addAdjacency(Map<Integer, Set<Neighbor>> adjacency, int source, Integer... dests) {
        if( dests.length % 2 != 0)
            throw new IllegalArgumentException("dests must have an equal number of arguments, each pair is the id and cost for that traversal");

        final Set<Neighbor> destinations = new HashSet<>();
        for(int i = 0; i < dests.length; i+=2)
            destinations.add(new Neighbor(dests[i], dests[i+1]));
        adjacency.put(source, Collections.unmodifiableSet(destinations));
    }
}

上面代码的输出如下:

[1, 2, 4]
[1, 5, 2, 4]
[1, 5, 3, 2, 4]

请注意,每次致电 nextShortestPath()它会根据需要为您生成下一条最短路径。它仅计算所需的额外步骤,并且不会遍历任何旧路径两次。而且,如果您决定不需要所有路径并尽早结束执行,则可以节省大量的计算时间。您只计算最多所需的路径数,而无需再计算。

最后应该指出的是,尽管我认为A *和Dijkstra算法不会对您造成影响,但确实存在一些较小的限制。也就是说,它在权重为负的图形上无法正常工作。

这是JDoodle的链接,您可以在其中自行在浏览器中运行代码并查看其工作情况。您还可以在图表周围进行更改,以显示它也可以在其他图表上使用:http : //jdoodle.com/a/ukx


2

以下功能(经过修改的BFS,在两个节点之间具有递归路径查找功能)将完成非循环图的工作:

from collections import defaultdict

# modified BFS
def find_all_parents(G, s):
    Q = [s]
    parents = defaultdict(set)
    while len(Q) != 0:
        v = Q[0]
        Q.pop(0)
        for w in G.get(v, []):
            parents[w].add(v)
            Q.append(w) 
    return parents

# recursive path-finding function (assumes that there exists a path in G from a to b)   
def find_all_paths(parents, a, b): 
    return [a] if a == b else [y + b for x in list(parents[b]) for y in find_all_paths(parents, a, x)]

例如,下图(DAGG

G = {'A':['B','C'], 'B':['D'], 'C':['D', 'F'], 'D':['E', 'F'], 'E':['F']}

如果要查找和之间的所有路径(使用上面定义的函数作为),它将返回以下路径:'A''F'find_all_paths(find_all_parents(G, 'A'), 'A', 'F')

在此处输入图片说明


1

您通常不希望这样做,因为在平凡的图中它们的数量是指数的。如果您确实想要获得所有(简单)路径或所有(简单)循环,则只需找到一个(通过遍历图表),然后回溯到另一个即可。


它对于任何DAG而言都是简单,高效且可行的。你在误导@Paul。
迭戈


0

我想您想找到“简单”路径(如果没有一个节点出现在其中一次以上,则可能是简单的路径,也许第一个和最后一个除外。)

由于问题是NP难题,因此您可能需要进行深度优先搜索的变体。

基本上,从A生成所有可能的路径,然后检查它们是否以G结尾。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.