有向无环图
若一个有向图中不存在环,则称这个有向图为有向无环图,简称DAG图。
例如:
这就是一个DAG图
DAG图的应用很广泛,但是最常用的地方是用来表示活动网络,例如,后台常用的任务调度工具dolphinscheduler,核心就是用DAG图来描述工作流。
拓扑排序
在图论中,由一个DAG图的顶点组成的序列,当且仅当满足下列条件时,称该序列为该图的一个拓扑排序:
- 每个顶点出现且只出现一次。
- 若顶点A在序列中排在顶点B的前面,则图中不存在从B到A的路径。
实现拓扑排序的算法有很多,最常用的思路如下:
- 从DAG图中选择一个没有前驱的顶点并输出。
- 从图中删除该节点以及从它出发的有向边。
- 重复操作1和2,直至当前的DAG图为空或图中不存在没有前驱的结点。
根据这个思路,我们可以知道,若图中不存在没有前驱的顶点,且DAG图不为空,则说明图中一定含有环路。所以拓扑排序也可以用来判断图中是否有环
依据拓扑排序的思路,我们给一个例子:
对该图进行拓扑排序。
第一轮
- 选择无前驱的结点,有1。
- 删除它以及从它出发的有向边。
第一轮操作后的图为:
第二轮
- 选择无前驱的结点,有2、3,任选一个,假设选2。
- 删除它以及从它出发的有向边。
第二轮操作后的图为:
第三轮
- 选择无前驱的结点,3、4,任选一个,假设选3。
- 删除它以及从它出发的有向边。
第三轮操作后的图为:
第四轮
- 选择无前驱的结点,有4、5,假设选4。
- 删除它以及从它出发的有向边,没有边可删。
第四轮操作后的图为:
第五轮
- 选择无前驱的结点,有5
- 删除它以及从它出发的有向边,没有边可删。
第五轮操作后的图为:
此时图为空,拓扑排序结束。
实现
用队列实现(基于BFS)
// C++
#include <queue>
#include <vector>
/**
* @param E DAG图的边集,用来判断两点之间是否存在边。
* @param indegree 各顶点的入度
*
* @return 拓扑序列
*/
std::vector<int> topoSort(std::vector<std::vector<bool>> E, std::vector<int> indegree)
{
std::queue<int> q;
std::vector<int> res;
// 将入度为0的顶点加入队列
for (int i = 0; i < indegree.size(); i++)
{
if (indegree[i] == 0)
q.push(i);
}
while (!q.empty())
{
// 输出
int tmp = q.front();
q.pop();
res.push_back(tmp);
// 删除从该结点出发的有向边
for (int i = 0; i < E[tmp].size(); i++)
{
if(E[tmp][i]){
indegree[i]--;
if(indegree[i] == 0) q.push(i);
}
}
}
// 需要判断res.size()==indegree.size()才能确定结果是正确的输出序列,否则表示图中存在环路。
return res;
}
用栈实现(基于用队列实现演变)
#include <vector>
#include <stack>
/**
* @param E DAG图的边集,用来判断两点之间是否存在边。
* @param indegree 各顶点的入度
*
* @return 拓扑序列
*/
std::vector<int> topoSort(std::vector<std::vector<bool>> E, std::vector<int> indegree)
{
std::stack<int> s;
std::vector<int> res;
// 将入度为0的顶点加入队列
for (int i = 0; i < indegree.size(); i++)
{
if (indegree[i] == 0)
s.push(i);
}
while (!s.empty())
{
// 输出
int tmp = s.top();
s.pop();
res.push_back(tmp);
// 删除从该结点出发的有向边
for (int i = 0; i < E[tmp].size(); i++)
{
if(E[tmp][i]){
indegree[i]--;
if(indegree[i] == 0) s.push(i);
}
}
}
return res;
}
全篇结束,感谢阅读!