在动态有向图中寻找最小循环路径

6
我最近发现了Spotify今年早些时候的黑客挑战中的一个有趣问题this (Edit: Problem A),涉及确定火车货车交汇处的转换以将火车路由回到其起点。 火车必须面对离开时的同一方向,且火车不能在轨道上倒车。
据我所知,可以将该问题建模为无向图,我们必须从某个顶点找到最短循环,或检测是否不存在这样的循环。 但是,有趣的部分在于对于一个顶点v,与v相邻的顶点取决于到达v的路径,因此从某种意义上讲,图可以被认为是有向的,尽管这个方向是依赖于路径的。
我的第一个想法是将每个节点建模为3个单独的顶点A、B和C,其中A<->B并且A<->C,然后使用广度优先搜索来构建搜索树,直到我们找到原始顶点,但是上述警告使得这变得复杂,即给定顶点的相邻点取决于我们访问的前一个顶点。 这意味着在我们的BFS树中,节点可以有多个父节点。
显然,简单的BFS搜索无法解决这个问题。我知道存在用于检测图中循环的算法。一种方法可能是检测所有的循环,然后对于每个循环,检测路径是否有效(即不反向)。
还有其他人对解决这个问题的方法有什么见解吗?
更新:我按照@Karussell在评论中提出的方法进行了尝试。 这是我在github上的解决方案。 关键是使用基于边的图来模拟情况,而不是传统的基于顶点的图。比赛中提供的输入文件已经方便地以边的形式指定,因此可以轻松地使用该文件构建基于边的图。
该程序使用两个重要的类:Road和Solver。一个Road有两个整数字段j1和j2。j1表示源交叉口,j2表示目标交叉口。每条路都是单向的,意味着只能从j1到j2行驶。每个Road还包括相邻Road的LinkedList和父Road。Road类还包括静态方法,用于在输入文件中使用的字符串和表示每个交叉口的A、B和C点之间的整数索引之间进行转换。
对于输入文件中的每个条目,我们向HashMap添加两个Road,分别代表两个交叉口之间的方向。现在,我们有了所有连接交叉口之间的道路列表。我们只需要通过A、B和C开关将道路连接在一起。如果一条Road以Junction.A结束,我们查找以Junction.B和Junction.C开头的Road,并将这些Road作为相邻项添加。buildGraph()函数返回目标交叉口(j2)为"1A" == index 0的Road。
在这一点上,我们的图已经构建完成。为了找到最短路径,我简单地使用BFS遍历图形。我们将根节点保持未标记状态,并从队列中开始排列根节点的邻接点。如果我们找到一个目标交叉口为“1A”(索引0)的道路,则我们已经找到了通过起点的最短循环。一旦我们使用每个道路的父属性重建路径,将开关适当地设置为问题所需就是一个微不足道的事情。
感谢Karussell提出此方法。如果您想将您的评论以答案形式放置,并附有简短的解释,我会接受它。也感谢@Origin。我必须承认我没有完全理解您的答案的逻辑,但这当然并不意味着它是错误的。如果有人使用您的解决方案解决了这个问题,我会非常感兴趣。

我认为你可以将这种情况建模成基于边缘的有向图,就像转向限制/成本一样 http://algo2.iti.kit.edu/documents/routeplanning/volker_sa.pdf ... 或者通过基于节点的图表进行改进 http://algo2.iti.kit.edu/1862.php - Karussell
谢谢@Karussell,我实现了你建议的解决方案(请参见编辑)。如果您能将其作为答案并稍加阐述,我将非常高兴接受它。 - tronbabylove
请查看我的回答。如果您想在Java中实现这个真实世界的应用,我会很高兴得到您对GraphHopper的帮助 :)! - Karussell
@Karussell GraphHopper 看起来非常有趣,如果我有空的话,我可能会 fork 它。 - tronbabylove
2个回答

1
一种可能的方法:首先构建某种图形来模拟所有连接(图G)。然后构建另一个图形,在其中找到循环(图H)。对于G中的每个节点A,我们将在图H中添加一个节点。每个A节点还有2个出边(指向图G中的B和C节点)。在H中,这些边将转到在G中遇到的下一个A节点。例如,与ID为3的交换机的A节点相对应的H中的A节点将向节点9和节点6发出出边。每条边的权重是通过该路线传递的交换机数量(包括起始交换机)。
这将产生一个图形,我们可以在其中增加前向最短路径树。如果我们再次到达起点,则循环将完成。
关键是,只有在A->方向上穿过交换机时,交换机才是决策点。没有必要模拟反向方向,因为这只会使搜索变得复杂。
编辑:更多澄清
问题在于确定从A到A(再次)的最短路径。这里最短的定义是通过的交换机数量。这将用于基于Dijkstra的搜索算法。我们基本上要在图H上执行Dijkstra算法,其中边缘的成本等于该边缘中的交换机数量。
在H图中,我们将为每个交换机设置一个节点。每个节点将有2个出边,对应于可以采取的2条路径(B和C方向)。 H中的边将对应于原始图中2个A节点之间的整个路线。对于问题描述中的示例,我们得到以下内容:
与交换机1对应的节点: - 1个指向节点2的出链路,权重为2,对应于离开交换机1时采取C方向。如果从A1-> C1-> C3-> A3-> A2,则权重为2,因为我们经过交换机1和交换机3。 - 1个指向节点3的出链路,权重为2,对应于采取B方向。
与交换机2对应的节点:
  • 1个指向节点6的出链路,权重为2,对应于选择B方向
  • 由于C方向是死胡同,因此没有第二条链路

一个对应于交换机3的节点:

  • 1个指向节点6的出链路,权重为2,对应于选择C方向
  • 1个指向节点9的出链路,权重为3,对应于选择B方向并经过交换机3、7和8

对于每个交换机都是如此。这样就得到了一个具有10个节点的图形,每个节点最多有2条有向边。

现在我们可以开始构建Dijkstra树。我们从节点1开始,有两个可能的方向,即B和C。我们将它们放在优先队列中。队列中包含[node 2,weight 2]和[node 3, weight 2],因为我们可以通过经过2个交换机到达交换机2的A入口和交换机3的A入口。然后,我们通过从队列中选择最低权重的条目来继续搜索:

  • [节点2,权重2]:只有B方向可走,因此将[节点6,权重4]放入队列
  • [节点3,权重2]:有两个方向可走,因此将[节点6,权重4]和[节点9,权重5]加入队列。
  • [节点6,权重4]:有两个方向可走,将[节点4,权重5]和[节点8,权重8]加入队列]
  • [节点9,权重5]:只有C方向可走,加入[节点10,权重6]
  • [节点4,权重5]:为C方向加入[节点5,权重7],为B方向加入[节点1,权重9]
  • [节点10,权重6]:为C方向加入[节点1,权重8],为B方向加入[节点1,权重10]
  • [节点5,权重7]:加入[节点1,权重11]和[节点8,权重10]
  • [节点8,权重8]:加入[节点7,权重9]
  • [节点1,权重8]:我们找到了回去的路,所以可以停止了

(可能会有错误,我只是手动翻译)

然后算法在循环的最终长度为8时停止。确定所跟踪的路径只需要在您安置节点并解包路径时维护节点的父指针。

我们可以使用Dijkstra,因为H中的每个节点对应于以正确方向遍历原始节点(在G中)。然后可以按照Dijkstra方式解决H中的每个节点,因此算法的复杂度仅限于Dijkstra的复杂度(可以处理开关数量的100k上限)。


有趣。我最终采取了不同的方法(请参见编辑),我必须承认我并不完全理解你的方法,但我很想看到一个实现。谢谢你的回复。 - tronbabylove
@Origin 你介意给个例子吗?我也很感兴趣 :) ! - Karussell
@Karussell和tronbabylove - 我为问题描述中所示的问题添加了完整的解决方案。由于它是一个具有100k节点和200k链接的Dijkstra算法,因此它应该能够解决所有问题实例。我有一个快速实现,但它看起来不是很易懂,所以我写了上面的解决方案。 - Origin
@Origin 哦,我现在明白了。非常聪明的解决方案,我没有想到过。谢谢分享。 - tronbabylove

1

根据我的评论建议:我认为您可以通过基于边缘的图形或通过改进来解决这个问题,后者更多或少是一种“增强”的基于节点的图形。

细节:

  • 您的情况类似于道路网络中的转向限制。如果您为每条(有向!)街道创建一个节点,并根据允许的转向连接该节点,则可以对其进行建模。
  • 因此,不仅要存储当前位置的位置,还要存储方向和可能的进一步“情况”。这样即使是180度转弯的相同位置也与您当前的状态不同。

与其将您的“状态”(有向!)建模到图形中,您还可以为每个交叉口分配可能的结果-现在算法需要更聪明,并且需要根据您先前的状态(包括方向)决定每个交叉口要做什么。我认为,这是“增强”基于节点的图形的主要思想,应该更少占用内存(在您的情况下不太重要)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接