如何在一个环形图中找到两个节点之间的最长路径?

11

我已经解决了这里发布的大部分问题,除了最长路径问题。我读过有关最长路径的维基百科文章,如果图是无环的话,这似乎是一个简单的问题,但我的图不是无环的。

那么我该如何解决这个问题呢?采用蛮力法,检查所有可能的路径吗?我该如何开始做呢?

我知道对于一个具有约18000个节点的图形来说,这将需要很多时间。但是我仍然想开发它,因为这对于项目是必需品,我将在一个规模较小的图形上测试并展示给导师,其执行时间只需一两秒钟。

至少我完成了所有要求的任务,并且拥有一个可以运行的概念验证,但在循环图中没有更好的方式。但我不知道从哪里开始检查所有这些路径...


2
在循环图上,最长路径将是无限长度,不是吗?你只会一直绕来绕去... - qrdl
即使我标记了访问过的节点,那我为什么还会再次访问它们呢?这是我仍然无法理解的问题。在我的想象中,它应该像Dijkstra算法一样,只是“反着来”。 就像下面建议的那样,但我无法使其正常工作。算法结束了,但结果似乎并不正确。 - rfgamaral
1个回答

9
解决方案是暴力破解。您可以进行一些优化以加快速度,有些很简单,有些非常复杂。我怀疑您无法在台式计算机上使其足够快地工作,即使您能够这样做,我也不知道如何做到。然而,以下是暴力破解的原理。
注意:如果您想要一个准确的答案,Dijkstra和任何其他最短路径算法都不适用于此问题。
Start at a root node *root*
Let D[i] = longest path from node *root* to node i. D[*root*] = 0, and the others are also 0.

void getLongestPath(node, currSum)
{
    if node is visited
        return;
    mark node as visited;

    if D[node] < currSum
        D[node] = currSum;

    for each child i of node do
        getLongestPath(i, currSum + EdgeWeight(i, node));

    mark node as not visited;
}

让我们手动在这个图上运行它:1 - 2 (4), 1 - 3 (100), 2 - 3 (5), 3 - 5 (200), 3 - 4 (7), 4 - 5 (1000)

Let the root be 1. We call getLongestPath(1, 0);
2 is marked as visited and getLongestPath(2, 4); is called
D[2] = 0 < currSum = 4 so D[2] = 4.
3 is marked as visited and getLongestPath(3, 4 + 5); is called
D[3] = 0 < currSum = 9 so D[3] = 9.
4 is marked as visited and getLongestPath(4, 9 + 7); is called
D[4] = 0 < currSum = 16 so D[4] = 16.
5 is marked as visited and getLongestPath(5, 16 + 1000); is called
D[5] = 0 < currSum = 1016 so D[5] = 1016.
getLongestPath(3, 1016 + 200); is called, but node 3 is marked as visited, so nothing happens.
Node 5 has no more child nodes, so the function marks 5 as not visited and backtracks to 4. The backtracking will happen until node 1 is hit, which will end up setting D[3] = 100 and updating more nodes.

这是迭代方式的大致实现(未经测试,仅为基本思路):
Let st be a stack, the rest remains unchanged;
void getLongestPath(root)
{
    st.push(pair(root, 0));

    while st is not empty
    {
        topStack = st.top();
        if topStack.node is visited
            goto end;
        mark topStack.node as visited;

        if D[topStack.node] < topStack.sum
            D[topStack.node = topStack.sum;

        if topStack.node has a remaining child (*)
            st.push(pair(nextchild of topStack.node, topStack.sum + edge cost of topStack.node - nextchild)) 

        end:
        mark topStack.node as not visited
        st.pop();
    }
}

(*) - 这是一个小问题 - 您需要为每个节点保留下一个子节点的指针,因为它可能在 while 循环的不同迭代中甚至重置自身(当您将 topStack.node 节点从堆栈中弹出时,指针会重置自身,因此请确保重置它)。这在链表上最容易实现,但是您应该使用 int[] 列表或 vector 列表,以便能够存储指针并具有随机访问,因为您将需要它。例如,您可以保持 next[i] = node i 在其邻接列表中的下一个子节点,并相应地更新它。您可能会遇到一些边缘情况,并且可能需要不同的 end: 情况:一种正常情况,一种是当您访问已经访问过的节点时发生的情况,在这种情况下,指针不需要被重置。也许将 visited 条件移动到您决定将某些内容推送到堆栈之前,以避免这种情况。

递归只是意味着在内存中维护了一个隐式的堆栈,每次函数调用时都会将诸如函数参数和局部变量之类的东西推入其中。您可以自己维护该堆栈,从而避免递归,但我认为这只会使事情更加复杂。递归不是瓶颈所在。您应该专注于启发式优化(例如,如果 D[node] >= currSum,则可以返回)。这类似于旅行商问题,因此您可能想要搜索一下并查看其他人提出了什么解决方案。 - IVlad
此外,考虑使用近似算法。你必须返回最佳答案吗?或者接近最佳也可以吗?考虑研究贪心近似算法和遗传算法。如果你让遗传算法运行足够长的时间,它们也可以得到最佳解决方案。 - IVlad
此时此刻,我真的不关心优化,我真的没有时间去烦这些事情。我给讲师发了电子邮件,他理解这需要一段时间,并且这是在展示项目时要讨论的事情之一。我谈到了递归,不是因为性能,而是因为我更喜欢这种方式,仅此而已。否则,我将不得不通过函数参数传递相当多的变量,而我倾向于避免这样做,因为它会使事情变得复杂(至少对我来说是这样)。雨有点停了,让我看看现在是否可以去上课了 :X - rfgamaral
我刚刚实现了你发布的算法,但是:1)我无法将根节点标记为已访问,否则它会在第一次调用getLongestPath时退出;2)这计算了从X到所有其他节点的最长路径,我只想计算从X到Y,如何在到达那里时停止它?3)另外,我该如何保存路径,需要遍历的节点以获取最长路径?4)我真的更喜欢迭代算法,你能发布一个吗?我可能需要使用队列或堆栈,不是吗?但我无法理解如何迭代地实现递归回溯。 - rfgamaral
1
  1. 哎呀,抱歉:)。你是对的,不要最初标记根节点。
  2. 你无法准确地停止它,因为你不知道何时完成了最终更新。只需从节点X开始并打印D[Y]。Dijkstra算法也是如此,它计算从起始节点到至少一些其他节点的距离。
  3. 保持T[x]=最后一个更新x距离的节点。例如,向函数添加一个“father”参数。然后,当D[node]<currSum触发时,设置T[node]=father
  4. 是的,你应该实现自己的堆栈。这会变得非常混乱,我认为你真的应该使用递归。
- IVlad
显示剩余4条评论

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