207/210:课程表
有多门课程需要选修,给定课程总数和选修课程的依赖关系集合([a,b],即若需要选修课程a则需先选修课程b),求能否找到一个学完所有课程的学习顺序。
以课程为点,依赖关系为单向边建图,题目转化为求图中是否存在环。
验证图中是否有环的最简单方式是拓扑排序。
给定一个包含 n n n 个节点的有向图 G G G,我们给出它的节点编号的一种排列,如果满足:
对于图 G G G 中的任意一条有向边 ( u , v ) (u,v) (u,v), u u u 在排列中都出现在 v v v 的前面。
那么称该排列是图 G G G 的「拓扑排序」。
根据上述的定义,我们可以下结论:
如果图 G G G 中存在环,那么图 G G G 不存在拓扑排序( u u u在 v v v前也在 v v v后)
因此,题目转化为建图后求图是否存在拓扑排序。
建图我一般使用邻接表的方式建图,但这次也学到了如何使用C++动态容器建图。(之后算法阐述中使用C++动态容器法)
//传统邻接矩阵
const int maxn = 100005;
struct edge{
int next, to;
}w[maxn];
int head[maxn], cnt = 0;
void Addedge(int x,int y){
w[++cnt].to = y; w[cnt].next = head[x]; head[x] = cnt;
}
//C++动态容器法
vector<vector<int>> edges;
void Addedge(int x,int y){edges[x].emplace_back(y);}
求拓扑排序存在两种思路,DFS与BFS。
DFS法中,一个点有3种状态——【未搜索】【搜索中】和【已搜索】。
由于拓扑排序的特性,一个深度优先搜索的过程就是我们寻找可行拓扑排列的过程;因此我们算法的流程如下:
- 从任意一个【未搜索】的点开始出发,将其状态置为【搜索中】;
- 在向下搜索的过程中,每次遇到【未搜索】的子节点就对其进行迭代DFS,待其搜索完毕生成子序列后回溯。
- 如果在向下搜索的过程中发现【搜索中】的节点,说明图中有环,此时直接不存在拓扑排序。
- 如果搜索的过程中发现了【已搜索】的节点,则忽略,对结果无影响;
- 搜索完毕一个点后将其置为【已搜索】。
具体看代码。
//DFS Topology
//vis:0未搜索,1搜索中,2已搜索
void DFS(int x){
vis[x] = 1;
for(int y: edges[x]){
if(vis[y] == 0){
DFS(y);
if(!valid) return;
}
if(vis[y] == 1){
valid = false;
return;
}
}
vis[x] = 2;
}
for(int i = 0; i < numCourses && valid; i++){
if(!vis[i]) DFS(i);
}
与之相对的是BFS算法。BFS算法不模拟拓扑寻找顺序,而是统计每个点的入度。若一个图存在拓扑排序,则一定有一个开始点,这个点没有任何依赖——即入度为零。BFS每次找出这个点之后,将其从图中删去,移除它的所有出边,使得其后继节点“少了一门先修课程”。如果存在拓扑排序,则在移除点之后紧邻的点应当变为入度为0的“可修课”。不断重复这个流程,直到:
- 所有点都已经变为出度为0的点,说明图中无环,存在拓扑序,且拓扑序正是每一次移除点的顺序。
- 某些点出度不为0,说明图中有环,环彼此依赖导致出度无法归零。
//在Addedge的过程中要统计入度。
queue<int> q;
vector<int> ans;//记录拓扑序
for(int i=0;i<numCourses;i++) if(!indegree[i]) q.push(i);//找初始点
while(!q.empty()){
int x = q.front(); q.pop();
ans.push_back(x);
for(int y: edges[x]){
indegree[y]--;
if(indegree[y] == 0) q.push(y);
}
}
if(ans.size()!=numCourses) return {};//存在环
else return ans;