拓扑算法
一、用途
拓扑排序应用非常广泛,解决的问题的模型也非常一致。凡是需要通过局部顺序来推导全局顺序的,一般都能用拓扑排序来解决。除此之外,拓扑排序还能检测图中环的存在。
例如:先穿内衣再穿外套,先穿袜子再穿鞋子,先穿内裤再穿裤子。根据局部顺序推导全局顺序为:内衣->外套->袜子->鞋子->内裤->裤子(结果不止一种,因为先穿袜子还是内衣没有顺序)
二、原理
把依赖关系抽象成一个有向图。每个物品对应图中的一个顶点,物品之间的依赖关系就是顶点之间的边。
三、算法
1、Kahn算法(使用邻接表)
从 Kahn 代码中可以看出来,每个顶点被访问了一次,每个边也都被访问了一次,所以,Kahn 算法的时间复杂度就是 O(V+E)(V 表示顶点个数,E 表示边的个数)
示图如下:
关键是inDegree这个变量,统计入度,即该顶点依赖于几个顶点,几个顶点先于它。当它为0的时候就说明该顶点依赖的都已经输出,该点也可以输出了。
如图中的0->1,0先于1,1就是1个入度。
3->4和2->4就说明4有两个依赖,入度为2;
/*
0->1->2->4;0先于1先于2先于4
1->3->4;1先于3先于4
1->5;1先于5
*/
class Graph
{
private:
int _v;//顶点的个数
vector<vector<int>*> _adj;
public:
Graph(int v)
{
this->_v = v;
for (int i = 0; i < v; ++i)
{
_adj.push_back(new vector<int>);
}
}
void addEdge(int s, int t)//s先于t,s->t
{
_adj[s]->push_back(t);
}
void topoSortByKaha()
{
int* inDegree = new int[_v] {0};//统计入度,即该顶点依赖于几个顶点,几个顶点先于它。
for (int i = 0; i < _v; ++i)//遍历图,记录所有顶点的入度
{
for (int j = 0; j < (*_adj[i]).size(); ++j)
{
int w = (*_adj[i])[j];
++inDegree[w];
}
}
deque<int> result;
for (int i = 0; i < _v; ++i)
{
if (inDegree[i] == 0)//找到入度为0的顶点,也就是最先输出的顶点
result.push_back(i);
}
while (!result.empty())
{
int i = result.at(0);
result.pop_front();//输出result的第一个点,result中都是入度为0的点
cout << "->" << i << endl;
for (int j = 0; j<_adj[i]->size(); ++j)
{
int k = (*_adj[i])[j];//找到依赖于刚刚输出的顶点的剩余顶点
inDegree[k]--;//入度减去1;
if (inDegree[k] == 0)
{
result.push_back(k);//如果恰好有入读为0的就准备输出
}
}
}
}
};
2、DFS算法(使用逆邻接表)
类似于深度搜索算法,先递归输出依赖的顶点再输出自己。
DFS 算法的时间复杂度我们之前分析过。每个顶点被访问两次,每条边都被访问一次,所以时间复杂度也是 O(V+E)。
/*
0->1->2->4;0先于1先于2先于4
1->3->4;1先于3先于4
1->5;1先于5
*/
class Graph
{
private:
int _v;//顶点的个数
vector<vector<int>*> _adj;
public:
Graph(int v)
{
this->_v = v;
for (int i = 0; i < v; ++i)
{
_adj.push_back(new vector<int>);
}
}
void addEdge(int s, int t)//s先于t,s->t
{
_adj[s]->push_back(t);
}
void topoSortByKaha()
{
int* inDegree = new int[_v] {0};//统计入度,即该顶点依赖于几个顶点,几个顶点先于它。
for (int i = 0; i < _v; ++i)//遍历图,记录所有顶点的入度
{
for (int j = 0; j < (*_adj[i]).size(); ++j)
{
int w = (*_adj[i])[j];
++inDegree[w];
}
}
deque<int> result;
for (int i = 0; i < _v; ++i)
{
if (inDegree[i] == 0)//找到入度为0的顶点,也就是最先输出的顶点
result.push_back(i);
}
while (!result.empty())
{
int i = result.at(0);
result.pop_front();//输出result的第一个点,result中都是入度为0的点
cout << "->" << i << endl;
for (int j = 0; j<_adj[i]->size(); ++j)
{
int k = (*_adj[i])[j];//找到依赖于刚刚输出的顶点的剩余顶点
inDegree[k]--;//入度减去1;
if (inDegree[k] == 0)
{
result.push_back(k);//如果恰好有入读为0的就准备输出
}
}
}
}
void topoSortByDFS()
{
//先构建逆邻接表,边s->t表示,s依赖于t,t先于s
int* inDegree = new int[_v] {0};
vector<vector<int>*> _inverseAdj;
for (int i = 0; i < _v; ++i)
{
_inverseAdj.push_back(new vector<int>);
}
for (int i = 0; i < _v; ++i)//通过领接表,产生逆邻接表,如上图
{
for (int j = 0; j<(*_adj[i]).size(); ++j)
{
int w = (*_adj[i])[j];
_inverseAdj[w]->push_back(i);
}
}
bool* visited = new bool[_v] {0};//关键表示顶点是否被访问过
for (int i = 0; i<_v; ++i)
{
if (visited[i] == false)//未被访问的顶点进行访问
{
visited[i] = true;
dfs(i, _inverseAdj, visited);
}
}
}
void dfs(int vertex, vector<vector<int>*>& _inverseAdj, bool visited[])
{
for (int i = 0; i<_inverseAdj[vertex]->size(); ++i)
{
int w = (*_inverseAdj[vertex])[i];
if (visited[w] == true)//表示当前顶点依赖的点已经输出了
//如1->0,因为0之前已经输出,所以这里直接continue
{
continue;
}
visited[w] = true;
dfs(w, _inverseAdj, visited);//递归访问当前顶点依赖的顶点,访问它的依赖关系。
}
cout << "->" << vertex << endl;
}
};