对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列—— [ 百度百科 ]
拓扑排序表示了顶点按照边的方向出现的先后顺序。如果有环,则该图没有拓扑排序。如下图,其拓扑排序就是为A—–>B—–>C.
拓扑排序有两种实现思路,一种是依靠简单粗暴的DFS(深度优先搜索),另一种是Kahn算法(用到了BFS)。
算法导论中就提到了利用DFS来进行拓扑排序的思路,如下:
TOPOLOGICAL-SORT(G)
1 call DFS(G) to compute finishing times v.f for each vertex v
2 as each vertex is finished, insert it onto the front of a linked list
3 return the linked list of vertexs
简单来说就是当深入优先搜索一个节点,若从该节点出去的所有邻居节点都搜索完毕后,该节点也即将搜索完毕,即将该节点加入到拓扑链表的最前面。实现代码如下:
const int MAXN = 100000;
const int MAXM = 500000;
struct node
{
vector<int> child;
};
node point_attr[MAXN + 1];
void addEdge(int u,int v)
{
point_attr[u].child.push_back(v);
}
std::stack<int> st;
void DFS(int nodeNum)
{
bnode[nodeNum]=true;//标记为已经访问过
for(auto num: point_attr[nodeNum].child)
{
if(!bnode[num])
DFS(num);
}
st.push(nodeNum);//加入到栈中
}
//是否被访问过
bool bnode[MAXN + 1];
int main()
{
int n,m;
cin>>n>>m;
int u=0,v=0;
for(int i=0;i<m;i++)
{
cin >> u >> v;
addEdge(u,v);
}
memset(bnode , 0 , sizeof(bnode));
for(int i=1;i<=n;i++)
{
if(!bnode[i])
DFS(i);
}
while(!st.empty()){
cout<<st.top()<<" ";
st.pop();
}
return 0;
}
其中用栈来保存DFS过程中已经遍历完成的节点,利用栈的后进先出的特点,最后的输出正好就是需要的拓扑排序。然而我实现上述实现的方法还是存在缺陷的,因为首先要对一个图进行无环的判断,然后才能进行拓扑排序..上述方法最好再加上无环的判定环节,这样就是极好的了.
算法思想:每次找到找到图中入度数为0的节点,此时将节点插入到拓扑链表的尾部,并将该节点指向的所有节点的入度数减1;不断循环,直到所有的节点都被插入链表或者没有入度数为0的节点。
这个思路相对DFS来说可能更符合人们的思路,每次加入到队列尾部的节点都是已经没有节点指入的。实现代码如下:
int deg[MAXN + 1] = {0}; //记录每个点的入度数
struct node
{
vector<int> child;
};
node point_attr[MAXN + 1];
void addEdge(int u,int v)
{
point_attr[u].child.push_back(v);
++deg[v];
}
queue<int> rgb;
int main()
{
int n,m;
cin>>n>>m;
int u=0,v=0;
for(int i=0;i<m;i++)
{
cin >> u >> v;
addEdge(u,v);
}
//统计入度为0的结点,并加入队列;
for (int j = 1; j <= n; j++) {
if (!deg[j]) rgb.push(j);
}
int count = 0;
while(!rgb.empty())
{
int node_num = rgb.front();
rgb.pop();
count++;
for(auto num: point_attr[node_num].child)
{
--deg[num];
if(deg[num] ==0)
rgb.push(num);
}
}
return 0;
}
最终rgb中存放节点的顺序就是拓扑排序。代码中不断将入度数为0的节点放到队列中其实就是一种广度优先搜索。
两种方法思路不同,但本质相同。我之前看到一篇博客里面讲到,DFS是从出度的角度来考虑,Kahn算法是从入度的角度出发。
当一个节点的入度数为0时,显然已经没有节点指向该节点,理应是剩余节点中拓扑排序中的第一位;当一个节点的出度数为0,它指向的节点方向的所有节点都已经被处理(加入到拓扑队列中),那么该节点就该被加入到队列的最前面。
两种方法的时间复杂度都是O(V+E).
之前在hihocoder上看到一个题目,是典型的拓扑排序的应用,看一下这个题目,可以让我们更加熟悉拓扑排序。题目链接:hihocoder-拓扑排序
题目大意就是当图中的某个节点感染了病毒之后,会沿着边的方向不断传递,若一个图(如下)一开始节点1上有病毒.
那么最后完全感染完毕之后的病毒分布图如下:
也就是说一个节点有多个入度的话,该节点要被感染多次。此题目直观上可以用DFS的方法来实现,即对每一个初始感染的节点进行DFS,但是这样会进行大量的重复计算,就如同递推求解斐波那契数一样,会存在着大量的重复项计算。实际上在求拓扑排序的过程中就能完成病毒数目的计算,具体思路参见hihocoder上的讲解,我的实现代码如下:(欢迎大家讨论)
const int MAXN = 100000;
const int MAXM = 500000;
const int MOD =142857;
vector<int> deg(MAXN+1,0);//rudu shu
struct node
{
vector<int> child;
int value;
};
node point_attr[MAXN + 1];
void addEdge(int u,int v)
{
point_attr[u].child.push_back(v);
++deg[v];
}
queue<int> rgb;
int main()
{
int n,m,k;
cin>>n>>m>>k;
int t;
while(k--)
{
cin>>t;
point_attr[t].value =1;
}
int u=0,v=0;
for(int i=0;i<m;i++)
{
cin >> u >> v;
addEdge(u,v);
}
for (int j = 1; j <= n; j++)
{
if (!deg[j]) rgb.push(j);
}
int sum=0;
while(!rgb.empty())
{
int node_num = rgb.front();
rgb.pop();
sum = sum+point_attr[node_num].value;
if(sum >MOD) sum-=MOD;
for(auto num: point_attr[node_num].child)
{
point_attr[num].value = point_attr[num].value +point_attr[node_num].value;
if(point_attr[num].value > MOD)
point_attr[num].value -=MOD;
--deg[num];
if(deg[num] ==0)
rgb.push(num);
}
}
cout<<sum<<endl;
//int i;
// cin>>i;
return 0;
}
原文地址:http://blog.csdn.net/zhaoyunfullmetal/article/details/46563511