目录
一、拓扑排序
1.1、算法的基本步骤
1.2、算法实现
1.4、习题思考
1.5、DFS生成逆拓扑序
一、拓扑排序
AOV网:在 有向图中, 顶点表示活动(或任务), 有向边表示活动(或任务)间的先后关系,称这样的有向图为AOV网(Activity On Vertex Network)。AOV网络中不能出现有向回路,即有向环。拓扑序列:就是把AOV网中的所有顶点排成一个线性序列,若AOV网中存在有向边 Vi → Vj ,则在该序列中, Vi 必位于 Vj 之前。在拓扑序列中,先进行的任务一定在后进行的任务的前面。按照拓扑序列完成各子任务,就可以顺利完成整个任务。拓扑序列未必唯一。如果不能排成一个拓扑序列,则说明AOV网络中存在有向环。
1.1、算法的基本步骤
①从图中选择一个入度为0的顶点并输出。
②从图中删除该顶点及该顶点引出的所有边。
③执行①②,直至所有顶点已输出,或剩余顶点入度均不为0(说明存在环,无法继续拓扑排序)
时间复杂度O(n+e):计算入度O(n+e),删除顶点O(n+e)。
1.2、算法实现
实际上我们并不需要真正的删除该顶点,我们只需要记录每个顶点的入度,在“删除”某个顶点之后,它所邻接到的所有顶点的入度都减1即可。并且我们用栈保存入度为0的顶点,以供删除。课本上用数组实现栈,这里直接采用STL:stack。
#include<bits/stdc++.h>
using namespace std;
#define n 10
struct Edge{
int VerName;
int cost;
Edge * next;
};
struct Vertex{
int VerName;
Edge * edge=nullptr;
};
vector<int> cnt(n);
Vertex Head[n];
vector<int> path;
void TopoOrder(){//进行拓扑排序,并判断是否存在环路
path.clear();
cnt.assign(n,0);
for(auto & i : Head){//计算入度
for(Edge * edge=i.edge;edge!=nullptr;edge=edge->next){
cnt[edge->VerName]+=1;//入度++
}
}
stack<int> sta;
for(int i=0;i<n;++i) if(!cnt[i]) sta.push(i);//初始化入度为0的家伙
int num=0;
while(!sta.empty()){//拓扑排序
int cur=sta.top();sta.pop();
path.emplace_back(cur);//路径存入
for(Edge * edge=Head[cur].edge;edge!=nullptr;edge=edge->next){//删除点
cnt[edge->VerName]-=1;//入度++
if(cnt[edge->VerName]==0) sta.push(edge->VerName);
}
}
if(path.size()!=n) printf("有向图中存在环路");
else for(auto i:path) printf("%d ",i);
return;
}
int main(void){
return 0;
}
由于一个顶点的入度被删减为0时,不可能再有边指向该顶点,因此该顶点不会被再次访问,则其cnt[i]空闲了。即当cnt[i]==0时,cnt[i]就不会被访问了,i也会入栈,因此我们可以利用这个空闲直接在cnt上实现栈。
优化用cnt数组空闲空间实现栈(静态链表):
int top=-1;//模拟栈顶
for(int i=0;i<n;++i){
if(cnt[i]==0){
cnt[i]=top;//指向以前的栈顶
top=i;//栈顶变为i
}
}
while(top!=-1){
int cur=top;
path.emplace_back(cur);
top=cnt[top];//静态链表,相当于弹栈
for(Edge * edge=Head[cur].edge;edge!=nullptr;edge=edge->next){//删除点
cnt[edge->VerName]-=1;//入度++
if(cnt[edge->VerName]==0) {
cnt[edge->VerName]=top;
top=edge->VerName;
}
}
}
1.4、习题思考
(1)给定一个图和顶点序列,编写算法判断该序列是否是图的拓扑序列。
只需要先计算一次入度,然后扫描所给定的顶点序列,对于每一个顶点:
①先判断该顶点入度是否为0,如果不为0,则说明不是拓扑序,如果为0,则进行②
②将该顶点的边删除(即为其邻接顶点减少入度),之后继续扫描。
实际上我们假定给定的顶点序列是拓扑序列,则按照该顶点序列选择顶点删除边,选择的顶点一定是入度为0的。
(2)802. 找到最终的安全状态
如果本题正着想,则从一个点开始DFS遍历,如果它能遍历到终端结点,则路径上的所有点都能遍历到终端结点,如果它的所有邻接结点都是安全结点,则它也将是安全结点,如果它有一个邻接结点不是安全结点则必然它也不是安全结点(换句话说如果存在环路,则遍历到一个结点不是安全结点且已经被遍历过,则这条路并不通往终端节点,则它不是安全结点)。然后继续寻找下一个不是安全结点的结点且未被遍历过的结点开始遍历。<不是安全结点的结点且被遍历过的结点一定不可能是安全结点了,因为如果它是,那么它的所有邻接结点都是安全结点。后根DFS也将判断它为安全结点。>
如果说你能走到终端结点,你才算是安全结点。那么反过来走,只能被终端节点才能走到的结点才是安全结点。考察反向图,终端节点是入度为0的顶点,进行拓扑排序,排序过程中如果入度为0则被认为是安全结点(证明:开始时终端结点为安全结点,入度为0,删除其所有出边,剩下的结点入度为0的则说明它们只能被终端结点走到,它们是安全结点,然后把它们当作终端结点继续执行同样的操作,以此类推)。
class Solution { public: vector<int> eventualSafeNodes(vector<vector<int>>& graph) { int n=graph.size(); vector<vector<int>> gra(n,vector<int>{}); vector<int> cnt(n); stack<int> sta; vector<int> ans; for(int i=0;i<n;++i){ cnt.at(i)=graph.at(i).size(); if(!cnt.at(i)){ sta.push(i); } for(int j=0;j<cnt.at(i);++j){ gra.at(graph.at(i).at(j)).emplace_back(i); } } while(!sta.empty()){ ans.push_back(sta.top());sta.pop(); for(auto i:gra[ans.back()]){ cnt.at(i)--; if(cnt.at(i)==0) sta.push(i); } } sort(ans.begin(),ans.end()); return ans; } }; //什么STL大王,在这全程at(),看得脑袋晕,一般还是别用了(。。),用起来自己看不明白呀,除非你想测试越界
1.5、DFS生成逆拓扑序
为什么DFS能生成逆拓扑序?
我们来思考一下拓扑序列的原始定义。在一个拓扑序列中,如果vi在vj之前,则必然在有向图中有vi→vj。那么如果我们使用DFS“后根遍历”,则对于任意的vi→vj,一定有vi在vj之后输出,换句话说,对于有向图中的每一条边vi→vj均满足,vi在vj之后,如果将这一输出序列反转,则等价于对于有向图中的每一条边vi→vj均满足,vi在vj之前。相当于对于任何一个顶点vi,其后继邻接顶点都会在其之前被输出,否则它不会被输出。反过来就是,只有当vi被输出之后,其邻接顶点才会被输出,即满足拓扑排序。因此DFS“后根输出”为拓扑排序的逆序。
当然你得先保证这是一个AOV网络,不然DFS就变成了纯遍历了。
int vis[n]; void DFS_TopoOrder(int root){ vis[root]=1; for(Edge * edge=Head[root].edge;edge!=nullptr;edge=edge->next){ if(vis[root]==0) DFS_TopoOrder(edge->VerName); } printf("%d ",root); return; } for(int i=0;i<n;++i) if(cnt[i]==0) DFS_TopoOrder(i);//从入度为0的开始遍历