我有一个我为给定图形创建的邻接列表,其中包含节点和加权边。我试图找出在图中找到最长路径的最佳方法。我有一个拓扑排序方法,我听说这可能很有用,但我不确定如何实现它以找到最长的路径。那么有没有办法使用拓扑排序来实现这一点,还是有更有效的方法?
以下是我对adj列表的示例(括号中的值是箭头(cost)to get to -> node
后到达节点的成本:
Node 0 (4)->1(9)->2
Node 1 (10)->3
Node 2 (8)->3
Node 3
Node 4 (3)->8(3)->7
Node 5 (2)->8(4)->7(2)->0
Node 6 (2)->7(1)->0
Node 7 (5)->9(6)->1(4)->2
Node 8 (6)->9(5)->1
Node 9 (7)->3
Node 10 (12)->4(11)->5(1)->6
答案 0 :(得分:2)
布莱恩已经回答了你上面的问题,但我认为我可以更深入地解决。
首先,正如他所指出的,如果没有周期,这个问题很容易解决。如果有循环,则会遇到路径无限长的情况。在这种情况下,您可以将最长路径定义为没有重复节点的任何路径。不幸的是,这个问题可以证明是NP-Hard。因此,我们将专注于您实际需要解决的问题(因为您提到了拓扑排序) - 有向无环图(DAG)中的最长路径。我们还假设我们有两个节点s
和t
,它们是我们的起始节点和结束节点。除非您可以对图表做出某些假设,否则问题会更加丑陋。如果您理解下面的文字,并且图表中的这些假设是正确的,那么您可以删除s
和t
限制(否则,您必须在每对顶点上运行它)你的图表!慢......)
算法的第一步是拓扑排序顶点。直觉上这是有道理的。假设您从左到右订购它们(即最左边的节点没有传入边缘)。从s
到t
的最长路径通常从左侧开始,在右侧结束。这条道路也不可能向左方向前进。这为您提供了生成最长路径的顺序排序 - 从左侧开始向右移动。
下一步是按顺序从左到右依次为每个节点定义最长路径。对于没有传入边的任何节点,该节点的最长路径到为0(根据定义,这是真的)。对于具有传入边缘的任何节点,递归地定义最长路径到该节点是所有传入边缘的最大值+到达“传入”邻居的最长路径(请注意,此数字可能为负数) ,例如,如果所有传入的边都是负的!)。直觉上这是有道理的,但证明也是微不足道的:
假设我们的算法声称某个节点v
的最长路径为d
,但实际最长路径为d' > d
。选择“最小”这样的节点v
(我们使用拓扑排序定义的排序。换句话说,我们选择我们的算法失败的“最左边”节点。这很重要,这样我们就可以假设我们的算法已经正确地确定了v
的“左”的任何节点的最长路径。将假设的最长路径的长度定义为d' = d_1 + e
,其中d_1
是假设路径的长度,直到具有边v_prev
到e
的节点v
(注意草率命名。边缘e
也有权重e
)。我们可以这样定义它,因为v
的任何路径都必须经过其中一个边缘为v
的邻居(因为你无法通过v
到达d_1
它的一些优势)。那么v_prev
必须是v
的最长路径(否则,矛盾。有一条较长的路径与我们选择d_1 + e
作为“最少”这样的节点相矛盾!)我们的算法会根据需要选择包含v
的路径。
要生成实际路径,您可以确定使用了哪条边。假设您已经重建了路径,直到路径长度为d
的某个顶点d' = d - e
。然后遍历所有传入的顶点,找到路径长度e
最长的顶点,其中v
是进入v
的边的权重。您还可以在执行算法时跟踪父节点的节点。也就是说,当您找到public List<Nodes> FindLongestPath(Graph graph, Node start, Node end)
{
var longestPathLengths = Dictionary<Node, int>;
var orderedNodes = graph.Nodes.TopologicallySort();
// Remove any nodes that are topologically less than start.
// They cannot be in a path from start to end by definition
while (orderedNodes.Pop() != start);
// Push it back onto the top of the stack
orderedNodes.Push(start);
// Do algorithm until we process the end node
while (1)
{
var node = orderedNodes.Pop();
if (node.IncomingEdges.Count() == 0)
{
longestPathLengths.Add(node, 0);
}
else
{
var longestPathLength = Int.Min;
foreach (var incomingEdge in node.IncomingEdges)
{
var currPathLength = longestPaths[incomingEdge.Parent] +
incomingEdge.Weight);
if (currPathlength > longestPathLength)
{
longestPath = currPathLength;
}
}
longestPathLengths.Add(node, longestPath);
}
if (node == end)
{
break;
}
}
// Reconstruct path. Go backwards until we hit start
var node = end;
var longestPath = new List<Node>();
while (node != start)
{
foreach (var incomingEdge in node.IncomingEdges)
{
if (longestPathLengths[incomingEdge.Parent] ==
longestPathLengths[node] - incomingEdge.Weight)
{
longestPath.Prepend(incomingEdge.Parent);
node = incomingEdge.Parent;
break;
}
}
}
return longestPath;
}
的最长路径时,请将其父级设置为选择的相邻节点。您可以使用简单的矛盾来说明为什么这两种方法都会产生最长的路径。
最后是一些伪代码(对不起,它基本上是在C#中。在没有自定义类的情况下用C代码编写代码很麻烦,而且我有一段时间没有编写C语言。)
Graph
请注意,此实现并不是特别有效,但希望它很清楚!当您通过代码/实现进行思考时,您可以通过许多小方法进行优化。通常,如果您在内存中存储更多内容,它将运行得更快。构建IncomingEdges
的方式也很重要。例如,您的节点似乎没有{{1}}属性。但如果没有这个,找到每个节点的传入边缘是一种痛苦(并且不具备性能!)。在我看来,图算法在概念上与字符串和数组上的算法不同,因为实现非常重要!如果您阅读图算法上的wiki条目,您会发现它们通常根据不同的实现(具有不同的数据结构)提供三到四个不同的运行时。如果你关心速度,请记住这一点
答案 1 :(得分:1)
假设你的图形没有周期,否则最长的路径变成一个模糊的概念,你可以确实进行拓扑排序。现在,您可以使用此拓扑排序,并且每个节点通过查看其所有前任来计算与源节点的最长距离,并将连接到它们的边的权重添加到它们的距离。然后选择为您提供此节点最长距离的前一个。拓扑排序可以保证所有前任都已正确确定距离。
如果除了最长路径的长度之外,还需要路径本身。然后从具有最长长度的节点开始,查看其所有前任,找到导致此长度的节点。然后重复此过程,直到找到图形的源节点。