目录
一、概念
一个无环的有向图称为有向无环图。
有向无环图是描述一个工程、计划、生产、系统等流程的有效工具。一个大工程可分为若干个子工程(活动),活动之间通常有一定的约束,例如先做什么活动,什么活动完成后才可以开始下一个活动。
用顶点表示活动,用弧表示活动之间的优先关系的有向图,称为顶点表示活动的网,简称AOV网。
在AOV网中,若从顶点i到顶点j之间存在一条有向路径,称顶点i是顶点j的前驱,或者称顶点j是顶点i的后继。若<i,j>是图中的弧,则称顶点i是顶点j的直接前驱,顶点j是顶点i的直接后继。
AOV网中是不允许有环的,否则会出现自己是自己的前驱,陷入死循环。可以对有向图的顶点进行拓扑排序,如果AOV网中所有的顶点都在拓扑序列中,则AOV网中必定无环。
拓扑排序是指将AOV网中的顶点排成一个线性序列,该序列必须满足:若从顶点i到顶点j有一条路径,则该序列中顶点i一定在顶点j之前。
如何进行拓扑排序呢?拓扑排序的基本思想:
①选择一个无前驱的顶点并输出;
②从图中删除该顶点和该顶点的所有发出边;
③重复①和②,直到不存在无前驱的顶点;
④如果输出的顶点数小于AOV网中的顶点数,则说明网中有环,否则输出的序列即为拓扑排序。
举例,学生应该按照怎样的顺序来学习下面这些课程
课程编号 | 课程名称 | 先修课程 |
程序设计基础 | 无 | |
数据结构 | , | |
离散数学 | ||
高级程序设计 | , | |
数值分析 | ,, | |
高等数学 | 无 |
如果用顶点表示课程,弧表示先修关系,若课程i是课程j的先修课程,在用弧<i,j>表示,课程之间的关系如图所示
和都无前驱,先输出,删除和的所有发出边,如图(a)所示;
此时和都无前驱,输出,删除和的所有发出边,如图(b)所示;
此时和都无前驱,输出,删除和的所有发出边,如图(c)所示;
此时和都无前驱,输出,删除和的所有发出边,如图(d)所示;
此时和都无前驱,输出并删除即可。
很明显,拓扑排序不是唯一的, 和可以先输出也可以先输出(但程序代码中输出是唯一的)
第二章用代码来解决拓扑排序问题。
二、算法
上面讲到的删除顶点和边操作,没必要真的删除顶点和边。可以将没有前驱的顶点(入度为0)暂存到栈中,输出时出栈即表示删除。边的删除只需要将其邻接点的入度减1即可。如图所示,删除的所有发出边(顶点的入度减1)
算法步骤:
①求出各顶点的入度,存入数组indegree[]中,并将入度为0的顶点入栈S。
②如果栈不空,则重复执行以下操作:
- 栈顶元素i出栈,并保存到拓扑序列数组topo[]中;
- 顶点i的所有邻接点入度减1,如果减1后入度为0,立即入栈S。
③如果输出的顶点数小于AOV网中的顶点数,则说明网中有环,否则输出拓扑序列。
本章使用邻接表来存储图(邻接矩阵、链式前向星都可以)
邻接表访问邻接点容易,计算入度难,因此为了计算顶点的入度,在创建邻接表的同时,再创键一个逆邻接表,根据逆邻接表轻松计算各顶点的入度。
①求出各顶点的入度(遍历逆邻接表即可),存入数组indegree[]中,并将入度为0的顶点入栈S。
②栈顶元素5出栈,并保存到拓扑序列数组topp[]中
顶点5的所有邻接点(C4,C3)入度减1,如果减1后入度为0,立即入栈S
③栈顶元素0出栈,并保存到拓扑序列数组topo[]中
顶点0的所有邻接点(C3,C2,C1)入度减1, 如果减1后入度为0,立即入栈S
④栈顶元素2出栈,并保存到拓扑序列数组topo[]中
顶点2的所有邻接点(C1,C4)入度减1, 如果减1后入度为0,立即入栈S
⑤栈顶元素1出栈,并保存到拓扑序列数组topo[]中
顶点1没有邻接点什么也不做
⑥栈顶元素3出栈,并保存到拓扑序列数组topo[]中
顶点3的所有邻接点(C4)入度减1, 如果减1后入度为0,立即入栈S
⑦栈顶元素4出栈,并保存到拓扑序列数组topo[]中
顶点4没有邻接点什么也不做
⑧栈空,算法停止。输出顶点个数等于AOV网中的顶点个数,输出拓扑排序序列
三、代码
#define INF 0x3f3f3f3f
#define ll long long
#include<iostream>
#include <cstring>
#include <stdio.h>
#include <vector>
#include <stack>
using namespace std;
const int N=1005;
typedef struct AdjNode{//定义邻接点类型
int v;//邻接点下标
struct AdjNode *next;
}AdjNode;
typedef struct VexNode{//定义顶点类型
string data;//顶点的数据类型,根据需要定义
AdjNode *first;//指向第一个邻接点
}VexNode;
vector<int> topo;
VexNode G[N];
VexNode G_reverse[N];
int n,m,u,v;
string a,b;
int indegree[N];
bool TopologicalSort(){
stack<int> s;
int index;
for(int i=0;i<n;i++){
for(AdjNode *node=G_reverse[i].first;node;node=node->next){
indegree[i]++;
}
if(indegree[i]==0)
s.push(i);
}
while(!s.empty()){
index=s.top();
s.pop();
topo.push_back(index);
for(AdjNode *node=G[index].first;node;node=node->next){
indegree[node->v]--;
if(indegree[node->v]==0)
s.push(node->v);
}
}
if(topo.size()==n)
return true;
else
return false;
}
int locatevex(VexNode Graph[N], string data){
for(int i=0;i<n;i++){
if(G[i].data==data)
return i;
}
return -1;
}
void insertedge(){
cin>>a>>b;
AdjNode *s1,*s2;
s1=new AdjNode;
s2=new AdjNode;
u=locatevex(G,a);
v=locatevex(G_reverse,b);
s1->v=v;
s1->next=G[u].first;
G[u].first=s1;
s2->v=u;
s2->next=G_reverse[v].first;
G_reverse[v].first=s2;
}
void printGraph(){
cout<<"输出邻接表"<<endl;
for(int i=0;i<n;i++){
cout<<G[i].data<<"-> ";
for(AdjNode *node=G[i].first;node;node=node->next){
cout<<G[node->v].data<<" ";
}
cout<<endl;
}
cout<<"输出逆邻接表"<<endl;
for(int i=0;i<n;i++){
cout<<G_reverse[i].data<<"-> ";
for(AdjNode *node=G_reverse[i].first;node;node=node->next){
cout<<G_reverse[node->v].data<<" ";
}
cout<<endl;
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++){
cin>>G[i].data;
G_reverse[i].data=G[i].data;
G[i].first=NULL;
G_reverse[i].first=NULL;
}
for(int i=0;i<m;i++){
insertedge();
}
printGraph();
if(TopologicalSort()){
for(int i=0;i<topo.size();i++){
cout<<G[topo[i]].data<<" ";
}
cout<<endl;
}
else
cout<<"该有向图有环"<<endl;
return 0;
}
算法分析:
时间复杂度:求有向图中各顶点的入度需遍历邻接表,算法的时间复杂度为O(e)。度数为0的顶点入栈的时间复杂度为O(n),若有向图无环,每个顶点出栈后其邻接点入度减1,时间复杂度为O(e),。总的时间复杂度为O(n+e)。
空间复杂度:算法所需要的辅助空间包含入度数组indegree[]、拓扑排序数组topo[]、栈S,则算法的空间复杂度是O(n)。
四、题目
1.Genealogical tree
这道题直到拓扑排序后就是一道水题
#define INF 0x3f3f3f3f
#define ll long long
#include<iostream>
#include <cstring>
#include <stdio.h>
#include <vector>
#include <stack>
using namespace std;
const int N=105;
vector<int> G[N];
vector<int> G_reverse[N];
int nums[N];
vector<int> topo;
int n,a;
bool TopologicalSort(){
stack<int> s;
for(int i=1;i<=n;i++){
for(int j=0;j<G_reverse[i].size();j++){
nums[i]++;
}
if(nums[i]==0)
s.push(i);
}
while(!s.empty()){
int u=s.top();
s.pop();
topo.push_back(u);
for(int i=0;i<G[u].size();i++){
int v=G[u][i];
nums[v]--;
if(nums[v]==0)
s.push(v);
}
}
if(topo.size()==n)
return true;
else
return false;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
while(scanf("%d",&a)&&a){
G[i].push_back(a);
G_reverse[a].push_back(i);
}
}
if(TopologicalSort()){
for(int i=0;i<topo.size();i++){
cout<<topo[i]<<" ";
}
cout<<endl;
}
return 0;
}
2.Sorting It All Out
结果有3种情况:①在某个关系后确定了排序序列
注意:这题不是简单地将拓扑排序的序列作为结果,拓扑排序可以处理多个入度为0的节点,这些节点排序顺序依赖于存储结构,但这样的结果属于第2种情况,无法确定排序序列,还需要更多的关系来保证每次同时只能有一个入度为0的节点,否则无法确定这几个入度为0的节点间的大小关系
② 无法确定排序序列(当所给的关系全部输入完后,还不确定排序序列,且没有环,则无法确定排序序列)
③在某个关系后发现不一致(根据目前所给的关系,发现有环)
有环:拓扑排序后还有入度为1的情况说明有环;排序后排序序列的个数小于n。对于这题第二个判断条件可能因为关系还没有提供完造成误判,所以我使用第一个判断条件
另外还需注意,即使在输入第x个条件时已经确定属于前两个中的某个情况,还需要继续输入等全部输入完,再输出结果 ,否则输入数据会影响第二组测试用例。
#define INF 0x3f3f3f3f
#define ll long long
#include<iostream>
#include <cstring>
#include <stdio.h>
#include <vector>
#include <queue>
#include <stack>
#include <algorithm>
using namespace std;
const int N=30;
int G[N][N],indegree[N],step,n,m;
string str;
char u,v,w;
bool flag1,flag2;
stack<int> s;
vector<int> topo;
void toposort(int indegree[N],int cnt){
if(flag1 || flag2)//已经判定有序或不一致则不进行排序了
return;
int in[n],flag=0;
for(int i=0;i<n;i++){
in[i]=indegree[i];
if(indegree[i]==0){
s.push(i);
}
}
//如果栈中有两个及以上入度为0的节点,则它们的大小关系不确定,还需输入边,继续向下判断是否有环
if(s.size()>1) flag=1;//这步不能省,否则s.pop后没有邻接点入度为0,栈中只有一个节点就可能误认为有序了
topo.clear();
while(!s.empty()){
int a=s.top();
s.pop();
topo.push_back(a);
for(int b=0;b<n;b++){
if(G[a][b]){
if(--in[b]==0)
s.push(b);
}
}
if(s.size()>1) flag=1;//关系不确定,继续向下判断是否有环
}
if(topo.size()==n && flag!=1){//排序的大小为n且没有不确定关系的节点说明找到了排序
flag1=true;
step=cnt;
}
else{
for(int i=0;i<n;i++){//拓扑排序了还有1说明有环
if(in[i]==1){
step=cnt;
flag2=true;
break;
}
}
}
}
void init(){
memset(G,0,sizeof(G));
memset(indegree,0,sizeof(indegree));
flag1=false;flag2=false;
step=0;
}
int main(){
while(scanf("%d%d%*c",&n,&m) && n!=0 &&m!=0){
init();
for(int i=1;i<=m;i++){
scanf("%c%c%c%*c",&u,&w,&v);
if(!G[u-'A'][v-'A']){//防重边
G[u-'A'][v-'A']=1;
indegree[v-'A']++;
}
toposort(indegree,i);
}
if(flag1){
printf("Sorted sequence determined after %d relations: ",step);
for(int i=0;i<n;i++){
printf("%c",topo[i]+'A');
}
printf(".\n");
}
else if(flag2){
printf("Inconsistency found after %d relations.\n",step);
}
else{
printf("Sorted sequence cannot be determined.\n");
}
}
return 0;
}
3.Labeling Balls
这道题注意是按标签输出小球的重量,而且标签小的球重量尽可能小
使用逆拓扑排序先确定“最重”的标签(出度为0,没有比他更重的了),并使用优先队列(最大堆)来保存出度为0的标签,确保每次取出最大标签赋予“极重”重量,确保“最重”的球被分配最大的标签,而”较轻“的球则被分配较小的标签,从而保证标签越小的球重量越小。
#define INF 0x3f3f3f3f
#define ll long long
#include<iostream>
#include <cstring>
#include <stdio.h>
#include <vector>
#include <queue>
using namespace std;
const int N=205;
int G[N][N];
int out[N];
int cas,n,m,a,b;
int topo[N];
priority_queue <int > q;
void TopologicalSort(){
int w=n;
for(int i=1;i<=n;i++){
if(out[i]==0)
q.push(i);//将出度为0的放入优先队列中
}
while(!q.empty()){
int u=q.top();
q.pop();
topo[u]=w--;
for(int i=1;i<=n;i++){
if(G[i][u]){
if(--out[i]==0)
q.push(i);
}
}
}
if(w>0){
printf("-1\n");
}
else{
for(int i=1;i<n;i++)
printf("%d ",topo[i]);
printf("%d\n",topo[n]);
}
}
int main(){
scanf("%d",&cas);
for(int i=0;i<cas;i++){
memset(out,0,sizeof(out));
memset(G,0,sizeof(G));
memset(topo,0,sizeof(topo));
scanf("%d%d",&n,&m);
for(int i=0;i<m;i++){
scanf("%d%d",&a,&b);
if(G[a][b]==0){//防止重边,out重复加了
G[a][b]=1;
out[a]++;
}
}
TopologicalSort();
}
return 0;
}
这道题我现在没想明白,按照拓扑排序先确定“最轻”的标签(入度为0,没有比他更轻的了),并使用优先队列(最小堆)来保存入度为0的标签,确保每次取出最小标签赋予“极轻”重量,为什么不可以,这和逆拓扑排序不一样的地方我认为只是孤立点的处理,一个看作“最轻”,一个看作“最重”
4.Following Orders
这道题就是将拓扑排序和回溯法联合起来,求出拓扑排序的所有解,注意输出按字典顺序输出
#define INF 0x3f3f3f3f
#define ll long long
#include<iostream>
#include <cstring>
#include <stdio.h>
#include <vector>
#include <queue>
#include <map>
#include <algorithm>
using namespace std;
const int N=30;
char vex[N];
map<char,int> m;
int n,u,v;
int G[N][N],in[N];
vector<int> topo;
void toposort(){
if(topo.size()==n){//完成一个拓扑排序
for(int i=0;i<topo.size();i++)
printf("%c",vex[topo[i]]);
printf("\n");
return;
}
for(int i=0;i<n;i++){
if(in[i]==0){
in[i]--;
topo.push_back(i);
for(int j=0;j<n;j++){
if(G[i][j]){
in[j]--;
}
}
toposort();
//回溯
topo.pop_back();
in[i]++;
for(int j=0;j<n;j++){
if(G[i][j]){
in[j]++;
}
}
}
}
}
void init(){
memset(G,0,sizeof(G));
memset(in,0,sizeof(in));
topo.clear();
n=0;
}
int main(){
string s;
int k=0;
while(getline(cin, s)) {
if(k>0)
printf("\n");
k++;
init();
//将字符串变成字符数组
for(int i=0;i<s.length();i++){
if(s[i]!=' '){
vex[n]=s[i];
n++;
}
}
sort(vex,vex+n);//排序,为了按字典顺序打印
for(int i=0;i<n;i++){
m[vex[i]]=i;//字母映射下标,如此可通过字母寻找下标
}
getline(cin, s);
for(int i=0,j=0;i<s.length();i++){
if(s[i]!=' '){
if(j%2==0)u=m[s[i]];
else{
v=m[s[i]];
G[u][v]=1;
in[v]++;
}
j++;
}
}
toposort();
}
return 0;
}