找到第k个最短的路径?

时间:2011-08-26 17:56:19

标签: algorithm graph shortest-path

在图表中找到两点之间的最短路径是一个经典的算法问题,有很多好的答案(Dijkstra's algorithmBellman-Ford等等。)我的问题是,是否有一个有效的算法,给定定向加权图,一对节点s和t以及值k,找到s和t之间的第k个最短路径。如果有多条相同长度的路径都与第k个最短路径相关,那么算法可以返回任何路径。

我怀疑这个算法可能在多项式时间内完成,但我知道longest path problem可能会导致NP难以减少。

有没有人知道这样的算法,或者是否表明它是NP难的?

4 个答案:

答案 0 :(得分:10)

您正在寻找Yen的算法来查找K最短路径。 k最短路径将成为该集合中的最后一条路径。

这是Yen算法的implementation

这是描述它的original paper

答案 1 :(得分:9)

最佳(且基本上是最优的)算法归因于Eppstein

答案 2 :(得分:2)

从可用的第K条最短路径算法中,Yen的算法最容易理解。主要是因为Yen的算法在计算第K个最短路径之前首先需要计算所有K-1个最短路径,因此可以将其分解为子问题。

此外,由于每个子问题都使用标准的最短路径算法(例如Dijkstra’s algorithm),因此从第一最短路径问题到 Kth是更自然的扩展最短路径问题。

这是在具有相等权重边缘的示例图中查找第三条最短路径的方法。

第一条最短路径(K = 1)

如果我们要寻找起点和终点之间的第一条最短路径(此处为DF之间),则可以运行Dijkstra的算法。第一次迭代时,Yen算法的整个代码为:

shortest_1 = Dijkstra(graph, D, F)

给出一个起始图,它给出了第一条最短路径(K = 1)。

First shortest path

第二个最短路径(K = 2)

找到第二条最短路径的直觉是走第一条最短路径,但“ Dijkstra”算法沿另一条稍微不那么理想的路线“强制”。颜氏算法沿另一条路线“强迫”迪杰斯特拉算法的方法是通过删除属于第一条最短路径的一条边。

但是我们要去除哪一条边以获得下一条最短路径?我们需要尝试一个接一个地移除每个边缘,然后看看哪个边缘移除为我们提供了下一条最短路径。

Second shortest path

首先,我们尝试删除边缘D-Eshortest_1中的第一个边缘),然后通过运行Dijkstra(graph_1, D, F)完成最短路径。我们将节点DD的最短路径(这只是节点D本身)与从节点D到{{1 }}。这为我们提供了另一条路径F

然后,我们尝试删除边缘D->FE-F中的第二条边缘),然后通过运行shortest_1完成最短路径。我们将节点Dijkstra(graph_2, E, F)D的最短路径与节点EE的新最短路径结合起来。这为我们提供了另一种替代路径F

第二次迭代的过程如下:

D->F

最后,选择新路径中的最短路径作为第二条最短路径。

第三条最短路径(K = 3)

就像通过从第一条最短路径中删除边来找到第二条最短路径一样,通过从第二条最短路径中删除边来找到第三条最短路径。

这次,新的细微差别是,当我们删除边缘// Try without edge 1 graph_1 = remove_edge(graph, D-E) candidate_1 = shortest_1(D, D) + Dijkstra(graph_1, D, F) // Try without edge 2 graph_2 = remove_edge(graph, E-F) candidate_2 = shortest_1(D, E) + Dijkstra(graph_2, E, F) shortest_2 = pick_shortest(candidate_1, candidate_2) D-A中的第一个边缘)时,我们也希望删除边缘shortest_2。如果不这样做,仅除去边缘D-E,则在D-A上运行Dijkstra时,我们将再次找到第一条最短路径(graph_3),而不是第三条最短路径!

对于D-E-Fgraph_4,我们不需要删除其他任何边缘,因为使用这些边缘不会给我们以前看到的最短路径。

Third shortest path

因此,总体过程看起来类似于找到第二条最短路径,但有一点细微的差别,我们除了第二条最短路径外,还希望删除一些在第一条最短路径中看到的边。决定是基于graph_5shortest_1是否共享一条通向被删除边缘的子路径。

shortest_2

请注意,这一次我们如何选择最短路径时,我们会考虑迭代2中未使用的候选对象(即// Try without edge 1 edges_3 = [D-A] if shortest_1 and shortest_2 share subpath up to node D: // True because both shortest_1 and shortest_2 have D in shortest path // D-E is edge in shortest_1 that is connected from D, and so it is added edges_3.add(D-E) graph_3 = remove_edges(graph, edges_3) candidate_3 = shortest_e(D, D) + Dijkstra(graph_3, D, F) // returns infinity // Try without edge 2 edges_4 = [A-E] if shortest_1 and shortest_2 share subpath up to node A: // False because there are no edges in shortest_1 that go through A // So no edges added graph_4 = remove_edges(graph, edges_4) candidate_4 = shortest_2(D, A) + Dijkstra(graph_4, A, F) // returns infinity // Try without edge 3 edges_5 = [E-F] if shortest_1 and shortest_2 share subpath up to node E: // False because shortest_1 goes through D-E while shortest_2 goes through D-A-E // So no edges added graph_5 = remove_edges(graph, edges_5) candidate_5 = shortest_2(D, E) + Dijkstra(graph_5, E, F) shortest_3 = pick_shortest(candidate_2, candidate_3, candidate_4, candidate_5) ),实际上最终会选择从candidate_2中找到的最短路径。同样,在下一次迭代(K = 4)中,我们将发现在迭代K = 3中实际上找到了第四个最短路径。如您所见,该算法正在预先完成工作,因此,在查找第K个最短路径的同时,它还在探索第K个最短路径之外的一些路径。因此,它不是第K条最短路径问题的最佳算法。 Eppstein算法可以做得更好,但是更复杂。

可以通过使用多个嵌套循环来概括上述方法。 Wikipedia page on Yen’s algorithm已经为更通用的实现提供了出色的伪代码,因此我将避免在此处编写它。请注意,维基百科代码使用列表graph_2来保存每个最短路径,并使用列表A来保存每个候选路径,并且候选最短路径会在循环迭代中持续存在。

备注

由于使用Dijkstra的算法,Yen的算法不能具有包含循环的Kth最短路径。当使用未加权的边缘时(如上面的示例),这不是很明显,但是如果添加了权重,则可能会发生这种情况。因此,Eppstein的算法考虑了循环,因此效果也更好。 This other answer包含link,可以很好地说明Eppstein的算法。

答案 3 :(得分:-1)

即使问题有一个有效的接受答案,这个答案通过提供样本工作代码来解决实施问题。在这里找到第k个最短路径的有效代码:

  

//时间复杂度:O(V k (V * logV + E))

using namespace std;

typedef long long int ll;
typedef short int i16;
typedef unsigned long long int u64;
typedef unsigned int u32;
typedef unsigned short int u16;
typedef unsigned char u8;

const int N = 128;

struct edge
{
    int to,w;
    edge(){}
    edge(int a, int b)
    {
        to=a;
        w=b;
    }
};

struct el
{
    int vertex,cost;
    el(){}
    el(int a, int b)
    {
        vertex=a;
        cost=b;
    }
    bool operator<(const el &a) const
    {
        return cost>a.cost;
    }
};

priority_queue <el> pq;

vector <edge> v[N];

vector <int> dist[N];

int n,m,q;

void input()
{
    int i,a,b,c;
    for(i=0;i<N;i++)
        v[i].clear();
    for(i=1;i<=m;i++)
    {
        scanf("%d %d %d", &a, &b, &c);
        a++;
        b++;
        v[a].push_back(edge(b,c));
        v[b].push_back(edge(a,c));
    }
}

void Dijkstra(int starting_node, int ending_node)
{
    int i,current_distance;
    el curr;
    for(i=0;i<N;i++)
        dist[i].clear();
    while(!pq.empty())
        pq.pop();
    pq.push(el(starting_node,0));
    while(!pq.empty())
    {
        curr=pq.top();
        pq.pop();
        if(dist[curr.vertex].size()==0)
            dist[curr.vertex].push_back(curr.cost);
        else if(dist[curr.vertex].back()!=curr.cost)
            dist[curr.vertex].push_back(curr.cost);
        if(dist[curr.vertex].size()>2)
            continue;
        for(i=0;i<v[curr.vertex].size();i++)
        {
            if(dist[v[curr.vertex][i].to].size()==2)
                continue;
            current_distance=v[curr.vertex][i].w+curr.cost;
            pq.push(el(v[curr.vertex][i].to,current_distance));
        }
    }
    if(dist[ending_node].size()<2)
        printf("?\n");
    else
        printf("%d\n", dist[ending_node][1]);
}

void solve()
{
    int i,a,b;
    scanf("%d", &q);
    for(i=1;i<=q;i++)
    {
        scanf("%d %d", &a, &b);
        a++;
        b++;
        Dijkstra(a,b);
    }
}

int main()
{
    int i;
    for(i=1;scanf("%d %d", &n, &m)!=EOF;i++)
    {
        input();
        printf("Set #%d\n", i);
        solve();
    }
    return 0;
}