目录
1.两种二元关系(仅学习算法思想的读者可以跳过)
(1)偏序
非严格偏序(自反偏序):
设R是集合X上的二元关系,若R是自反的、反对称的、传递的,则称R是集合X上的非严格偏序
- 自反性:对∀a∈X,有aRa
- 反对称性:对∀a,b∈X,若有aRb、bRa,则有a=b
- 传递性:对∀a,b,c∈X,若有aRb、bRc,则有aRc
严格偏序(反自反偏序):
设R是集合X上的二元关系,若R是反自反的、反对称的、传递的,则称R是集合X上的严格偏序
- 反自反性:对∀a∈X,均不存在aRa
(2)全序
非严格全序:
设R是集合X上的二元关系,若R是自反的、反对称的、传递的、完全的,则称R是集合X上的全序
- 完全性:对∀a,b∈X,均有aRb(或bRa)
严格全序:
设R是集合X上的二元关系,若R是反自反的、反对称的、传递的、完全的,则称R是集合X上的全序
(3)偏序与全序的关系
由上述定义可知,非严格全序就是非严格偏序的一个特例,严格全序就是严格偏序的一个特例。通俗地讲,偏序就是指集合中仅有部分元素可以比较,而对应的全序就是指集合中的所有元素均可比较
2.拓扑排序
(1)广义定义
由某个集合上的一个偏序得到该集合上的一个全序,这个过程称为拓扑排序
(2)图论中的定义
根据广义定义,考虑到无向图顶点的前驱(或后继)关系不是偏序(不满足反对称性),故只能对有向图进行拓扑排序,且仅对有向无环图DAG进行拓扑排序,才能得到完整的拓扑序列
定义:
对有向图进行拓扑排序,使得对于图中任意一条弧<u,v>,在拓扑序列中都有顶点u是顶点v的前驱(或顶点v是顶点u的后继)
DAG与其拓扑序列的解释:
对于DAG来说,顶点之间的前驱关系(或后继关系)就是一种严格偏序关系,因为:
- 反自反性:所有顶点不存在自回路,即所有顶点的前驱(或后继)都不是自身
- 反对称性:无环使得不会同时有,a是b的前驱(或后继)、b是a的前驱(或后继)
- 传递性:若a是b的前驱(或后继)、b是c的前驱(或后继),则a是c的前驱(或后继)
DAG的拓扑序列就是一种严格全序关系,因为它不仅满足上面三个性质,还满足:
- 完全性:对序列中任意两个顶点a和b,都有a是b的前驱(或后继)
(3)拓扑排序的应用
对一个有向图进行拓扑排序,根据是否能够得到一个完整的拓扑序列,来判断这个有向图中是否存在环。在日常应用中,通常都是描述一个项目中的所有事件是否能够按顺序完成,而这些事件之间存在一定的先后关系(通常用AOV(Active On Vertex)网描述)
3.Kahn算法实现拓扑排序
算法思想:BFS
Kahn算法按照与拓扑序列相同的顺序依次选择有向图中的顶点,具体如下:
依次找到一个入度为0的顶点,将它输出,并删除所有以它为弧尾的弧。重复该过程直至有向图的边集为空(对应无环),或有向图中所有顶点的入度均不为0(对应有环)
算法实现:
首先需要根据给定的顶点数和边集合,构造邻接表、并求出所有顶点的入度。
- T(n)=O(|E|)
- S(n)=O(|V|+|E|)(邻接表)
然后将所有入度为0的顶点入队(或压栈),每次弹出队首(或栈顶),并将所有以它为直接前驱的顶点的入度都-1,若-1后发现这些顶点的入度为0则需要将它们也入队(或压栈)。重复此过程直至输出所有顶点,或输出的顶点数小于|V|。
- T(n)=O(|V|+|E|)(每个顶点均访问1次,每条弧均访问1次)
- S(n)=O(1)
总时空开销为:
- T(n)=O(|V|+|E|)
- S(n)=O(|V|+|E|)
(1)数据结构
#include<iostream>
#include<vector>
#include<queue>
#include<stack>
class TopologicalSort{
private:
std::vector<std::vector<int>> adj; // 邻接表
std::vector<int> indegree; // 记录每个顶点的入度
public:
TopologicalSort(int n,const std::vector<vector<int>> &arc);
bool sort(std::vector<int> &res);
};
(2)初始化
TopologicalSort::TopologicalSort(int n,const std::vector<vector<int>> &arc){
adj.resize(n);
indegree.resize(n);
for(auto a:arc){
adj[a[0]].push_back(a[1]);
indegree[a[1]]++;
}
}
(3)排序
bool TopologicalSort::sort(std::vector<int> &res){// res用来返回拓扑序列
res.clear();
// std::stack<int> stk;
std::queue<int> que;
for(int i=0;i<indegree.size();i++){
if(indegree[i]==0){
// stk.push(i);
que.push(i);
}
}
// while(!stk.empty()){
while(!que.empty()){
// int vertex=stk.top();
// stk.pop();
int vertex=que.front();
que.pop();
res.push_back(vertex);
for(auto next:adj[vertex]){
if(--indegree[next]==0){
// stk.push(next);
que.push(next);
}
}
}
return res.size()==adj.size();
}
4.DFS实现拓扑排序
算法思想:DFS后序+栈
任选一个顶点开始进行DFS后序。
假设某一时刻搜索到顶点u,我们先不访问而是将其入栈。之后在某个时刻回溯到顶点u时,我们再访问它,而此时所有以u为前驱的顶点均已访问完(即均在栈中,且u为栈顶)。此时按照后进先出的规则,可以保证顶点u在其所有后继结点的前面输出,满足拓扑排序的定义
算法实现:
有了算法思想,我们只需要队有向图进行有限次DFS后序就可以得到拓扑序列(或检查到图中存在环),另外借助一个一维数组来记录顶点的状态:未搜索、已搜索但未访问、已访问。显然,若在某一时刻搜索到已搜索但未访问的顶点,就说明图中存在环。
T(n)=O(|V|+|E|)(每个顶点访问一次,每条边访问一次)
S(n)=O(|V|+|E|)(邻接表+递归工作栈+栈)
(1)数据结构
#include <iostream>
#include <vector>
#include <stack>
class TopologicalSort{
private:
std::vector<std::vector<int>> adj; // 邻接表
std::vector<int> visit; // 记录每个顶点的状态(0:未搜索、1:已搜索但未访问、2:已访问
void dfs_R(int src, bool &hasCircle, std::vector<int> &res);
public:
TopologicalSort(int n,const std::vector<vector<int>> &arc);
bool sort(std::vector<int> &res);
}
(2)初始化
TopologicalSort::TopologicalSort(int n,const std::vector<vector<int>> &arc){
adj.resize(n);
visit.resize(n);
for(auto a:arc){
adj[a[0]].push_back(a[1]);
}
}
(3)排序
bool TopologicalSort::sort(std::vector<int> &res){// res用来返回拓扑序列
res.clear();
bool hasCircle=false;
for (int i=0; i<visit.size() && !hasCircle; i++) {
if (!visit[i]) {
dfs_R(i, hasCircle, res);
}
}
reverse(res.begin(),res.end());
return !hasCircle;
}
void TopologicalSort::dfs_R(int vertex, bool &hasCircle, std::vector<int> &res){
visit[vertex]=1; // 状态置为已搜索
for(auto v:adj[src]){
if(visit[v]==0){
dfs_R(v,hasCircle,res);
if(hasCircle){
return;
}
}else if(visit[v]==1){ // 已搜索但未访问
hasCircle=true;
return;
}
}
visit[vertex]=2; // 状态置为已访问,然后压栈
res.push_back(vertex);
}