拓扑排序

发布时间 2023-12-18 22:13:02作者: 橙皮^-^

一、拓扑排序的定义

__拓扑排序是一个有向无环图的所有顶点的一种线性排序,使得对于顶点u到顶点v的每个有向边u \(\rightarrow\) w u在排序中都在v之前。当且仅当无环时(有向无环)才有可能进行拓扑排序。

二、DFS求拓扑排序

1、先看dfs前序和后序遍历、逆后序遍历的实现 伪代码

void dfs(Digraph G, int v)
{
	pre.push_back(v);//前序遍历
	marked[v] = true;
	for(int ne : G.adj(v)) {
	  if(!marked[ne])
	    dfs(G, ne)
	}
	Post.push_back(v);//后序排序
}

int main(int argc, char*argv[])
{
	dfs(G, 0);
	reverse(Post.begin(), Post.end());//逆后序排序--即拓扑排序
}

使用dfs求拓扑排序这里需要用到一个命题:一副有向无环图的拓扑排序顺序即为所有顶点的逆后序排序。

证明:对于任意一条边 v \(\rightarrow\) w,在调用dfs(v)时,存在以下三种情况
1、dfs(w)已经被调用且返回,w已在Post队列中
2、dfs(w)还没被调用, mark[w]还未被标记,那么根据 v \(\rightarrow\) w 会在dfs(v)中调用dfs(w),且dfs(w)会在dfs(v)返回前返回,w先于v加入Post中
3、dfs(w)被调用,mark[w]被标记且未返回。这种情况在有向无环图中不存在。反证法:当存在时,根据递归调用链意味着存在w \(\rightarrow\) v 路径,那么w和v顶点间存在环,跟有向无环条件矛盾,故不成立。

三、DFS判断有向图是否存在环

根据DFS拓扑排序的证明中,可以得知v \(\rightarrow\) w, 当调用dfs(v)时,若w顶点已经被标记,根据递归调用链可知存在路径 w \(\rightarrow\) v,那么图中存在环。

//寻找有向环
class DirectedCycle
{
public:
  DirectedCycle(Digraph G)
  {
    marked_.resize(G.V());
    edge_to_.resize(G.V());
    on_stack_.resize(G.V());
    for(int v = 0; v < G.V(); v++)
    {
      if(!marked_[v]) dfs(G, v);
    }
  }

  void dfs(Digraph G, int v)
  {
    marked_[v] = true;
    on_stack_[v] = true;
    for(int w : G.Adj(v))
    {
      if(!marked_[w])
      {
        edge_to_[w] = v;// 这样可以用通过叶子结点回溯到父结点, 然后通过栈形式先进后出反转
        dfs(G, w);
      }
      else if(on_stack_[w])
      {
        //找到一个环,当前顶点为w
        //根据edge_to_访问前后关系找到v->w中间的顶点
        for(int x = v; x != w; x = edge_to_[x])
        {
          cycle_.push(x);
        }
        cycle_.push(w);
        cycle_.push(v);
      }
      on_stack_[v] = false;
    }
  }

  bool HasCycle(){ return !cycle_.empty(); }

  std::stack<int> Cycle(){ return cycle_; }

private:
  std::vector<int> marked_;
  std::vector<int> edge_to_;
  std::vector<int> on_stack_;
  std::stack<int> cycle_;
};

四、BFS求拓扑排序(Kahn's algorithm)

在一个顶点的有向图中,一个顶点的「出度」指的是该顶点指出的边的总数,一个顶点的「入度」为指向该顶点的边的总数,目前个人觉得bfs算法相比dfs求拓扑排序更加直观。
算法的主要思想:每次找到入度为0的顶点,把该顶点加入排序中,并对指向点的入度-1
算法流程:
1、统计每个顶点的入度
2、找到入度为0的顶点并加入队列中
3、每次从队列中移除顶点,并把指向的顶点入度减少1,如果入度减少到0,则加入队列中,直到队列为空

vector<int> bfs(int n, vector<vector<int>> edges) {
	//根据边建图
	//1.统计每个顶点的入度
	vector<vector<int>> graph(n);
	vector<int> ind(n, 0);
	for(auto&edge : edges){
		graph[edge[0]].push_back(edge[1]);
		ind[edge[1]]++;
	}
	queue<int> q;
	vector<int> res;
	//2.找到一开始入度为0的顶点
	for(int i = 0; i < n; ++i)
	{
	  if(ind[i] == 0) {
	    q.push(i);
	  }
	}
	while(!q.empty()) {
	  int x = q.front();
	  q.pop();
	  res.push_back(x);
	  for(int ne : graph[x]){
	    if(--ind[ne] == 0) {
		  q.push(ne);
	    }
	  }
    }
	return res;
}

五 、根据BFS拓扑排序判断存在环

使用bfs对有向图进行拓扑排序,如果能够将所有节点都进行排序, 那么有向图中不存在环,否则则存在环。修改上面的判断条件即可

bool HasCycle(int n, vector<vector<int>> edges) {
	//根据边建图
	//1.统计每个顶点的入度
	vector<vector<int>> graph(n);
	vector<int> ind(n, 0);
	for(auto&edge : edges){
		graph[edge[0]].push_back(edge[1]);
		ind[edge[1]]++;
	}
	queue<int> q;
	int cnt = 0;//统计已排序顶点数量
	//2.找到一开始入度为0的顶点
	for(int i = 0; i < n; ++i)
	{
      if(ind[i] == 0) {
	    q.push(i);
	  }
    }
    while(!q.empty()) {
      int x = q.front();
      q.pop();
      cnt++;
      for(int ne : graph[x]){
        if(--ind[ne] == 0) {
          q.push(ne);
        }
      }
    }
    return cnt == n; //判断是否已对所有顶点排序即可
}

六、相关题目

2115. 从给定原材料中找到所有可以做出的菜
1462. 课程表 IV
1136. 并行课程

七 、总结

1、拓扑排序存在的前提是有向图中不存在环
2、求解拓扑排序可以通过求dfs的逆后序排序和通过bfs入度来进行查找