教程——使用拓扑排序寻找有向无环图中"最短路径"的算法

2,858 阅读6分钟

在这篇文章中,我们解释了使用拓扑排序寻找有向无环图中最短路径的算法。

内容表

  1. 问题陈述
  2. 方法
  3. 拓扑排序(DFS)的伪代码
  4. 使用拓扑排序寻找最短路径的伪代码
  5. 时间复杂度
  6. 拓扑排序法与其他最短距离查找算法的比较
  7. 拓扑排序的应用
  8. 问题

我们现在将深入研究。

问题陈述

我们有一个加权的有向无环图和一个源顶点,我们需要计算从源顶点到图中其他每个顶点的最短路径(假定与图中每条边相关的权重代表两个顶点之间的路径长度

graph

方法

  • 由于我们给定的图是一个有向无环图,我们可以考虑在图上应用拓扑排序,以便计算出从源顶点的最短路径。

  • 什么是拓扑排序?

  1. 拓扑排序只对有向无环图(DAG)有效。
  2. 有向无环图顶点的拓扑排序是对图中的顶点v1,v2,--Vn进行排序,即 "如果有一条从vi到vj顶点的边,那么在图的拓扑排序中,vi应该总是排在vj之前。"
  3. 因此,基本上在图的拓扑排序中,具有较少依赖性的顶点被印在具有相对较大依赖性的顶点之前。
  4. 我们可以同时使用DFS/BFS来实现拓扑排序。

我们如何使用拓扑排序法来寻找最短路径?

  1. 由于源顶点和特定顶点之间的最短路径应涉及最小的中间边,因此首先找到拓扑排序来计算最短路径是有意义的,因为拓扑排序将顶点按照它们的indegree递增顺序排列。
  2. 我们的想法是根据图形的拓扑排序遍历图形的顶点,对于拓扑排序中的每一个当前顶点,我们遍历当前顶点的邻接列表,实现以下操作。
for(auto i : adj[u])
{
  if dist[i]>dist[u]+weight(u,i)
     dist[i] = dist[u]+weight(u,i);
}
  • 这里 "u "是拓扑排序中的当前顶点

  • adj[u]代表顶点 "u "的邻接列表。

  • "i "是 "u "的邻接列表中的当前顶点。

  • dist[]是一个向量,其索引号代表图中的一个顶点,对应的数值存储在一个索引中,代表索引号所代表的顶点与源顶点之间的最短路径的长度。除了源顶点的索引号,每个索引的dist[i]的初始值都是INT_MAX。

  • 因此,基本上我们要检查我们是否能从源顶点通过顶点 "u "到达顶点 "i",如果我们得到更短的路径,我们就更新dist[i]的值。

拓扑排序(DFS)的伪代码

拓扑排序可以通过图的深度优先遍历来简单实现。我们将使用一个堆栈来存储通过图的深度优先遍历所遍历的顶点,这样,具有最大阶数的顶点就在堆栈的底部,而具有最小阶数的顶点就在堆栈的顶部。

void topological(vector<int> adj[], int v)
{
  vector<bool> visited(v,false);
  stack<int> stk;
  for(int i=0;i<v;i++)
  {
    if(visited[i]==false)
      {
        DFSrec(adj,i,visited,stk);
      }
   }
}

void DFSrec(vector<int> adj, int u, vector<bool> &visited, stack<int>& stk)
{
   visited[u] = true;
   for( auto i : adj[u])
   {
     if(visited[i] == false)
       {
         DFSrec(adj,i,visited,stk)
       }
    }
    stk.push(u);
 }

最后,在实现这个基于图的深度优先遍历的拓扑排序功能后,我们将得到一个堆栈,其顶部元素代表具有最小阶数的顶点,堆栈的底部元素代表具有最大阶数的顶点。

使用拓扑分类法寻找最短路径的伪代码

  • 我们将遍历包含拓扑排序的堆栈,该堆栈是我们使用深度优先遍历法形成的图形。
  • 我们将形成一个向量dist[],就像前面解释的那样,我们将对每个索引进行惯性化,除了代表源顶点的索引为INT_MAX。
  • 在每个迭代中,我们将遍历图中顶点的邻接列表,该列表由堆栈的当前顶部元素代表,在当前迭代结束时,我们将跳出堆栈的当前顶部。
void shortestpath(vector<int> adj, stack<int>& stk, int source)
{
  int v = adj.size();
  vector<int> dist(v,INT_MAX);
  dist[source] = 0;
  
  while(stk.empty() != true)
  {
    int u = stk.top();
    stk.pop();
    for(auto i: adj[u])
    {
      if(dist[i] > dist[u]+weight(u,i))
        dist[i] = dist[u] + weight(u,i);
    }
}

最后,dist[]向量将存储索引号所代表的顶点与源顶点的距离,如果任何顶点的dist[i]为无穷大,这意味着源顶点和索引号 "i "所代表的顶点之间不可能有路径。

时间复杂度

  • 使用DFS进行拓扑排序的时间复杂度为O(v+e),其中 "v "是顶点的数量,"e "是图中边的数量。

    • 原因是什么?使用DFS的拓扑排序是一个正常的
      DFS程序,只是对将顶点推入堆栈做了很小的修改,这需要O(1)的时间,因此基本上我们可以说时间复杂性与正常的DFS函数相同。
  • 最短路径函数的时间复杂度是O(v+e)
    ,其中v是图中veritces的数量,e是图中edge的数量。

  • 原因是什么?使用while循环遍历堆栈,for循环遍历顶点的邻接列表,我们只是简单地遍历图形,因此将花费O(v+e)。

  • 总的时间复杂度是O(v+e+v+e),基本上等于O(v+e)。

拓扑排序法与其他最短距离查找算法的比较

  • Bellman Ford算法可用于一般的加权图,Belman Ford算法的时间复杂度为O(v*e)。
  • Dijkstra算法的时间复杂度为O(e*logv),比Bellman Ford算法好,但Dijkstra算法的问题是,它不能用于有负加权边的图。
  • 对于非加权图,我们可以使用BFS算法,时间复杂度为O(v+e)。
  • 对于有向无环图,我们可以使用时间复杂度为O(v+e)的Topological Sorting算法。

拓扑分类法的应用

拓扑排序法可用于工作调度

在这里,图中的每个顶点都可以被视为工作,如果有一条从顶点v1到v2的边,那么这意味着工作2与工作1有依赖关系,因此工作1应该在工作2之前完成,因此我们需要进行拓扑排序,以获得每个工作完成的最合适的顺序。

问题

为什么拓扑排序只适用于有向无环图?

答案已经在本文的前一节中回答了。如果你无法回答,请再看一遍。

通过OpenGenus的这篇文章,你一定对使用拓扑排序寻找最短路径有了深刻的认识。好好享受吧。