拓扑排序

目录

一、概念

二、算法 

三、代码

四、题目

1.Genealogical tree

2.Sorting It All Out

3.Labeling Balls

4.Following Orders


一、概念

一个无环的有向图称为有向无环图

有向无环图是描述一个工程、计划、生产、系统等流程的有效工具。一个大工程可分为若干个子工程(活动),活动之间通常有一定的约束,例如先做什么活动,什么活动完成后才可以开始下一个活动。

用顶点表示活动,用弧表示活动之间的优先关系的有向图,称为顶点表示活动的网,简称AOV网

在AOV网中,若从顶点i到顶点j之间存在一条有向路径,称顶点i是顶点j的前驱,或者称顶点j是顶点i的后继。若<i,j>是图中的,则称顶点i是顶点j的直接前驱,顶点j是顶点i的直接后继

AOV网中是不允许有环的,否则会出现自己是自己的前驱,陷入死循环。可以对有向图的顶点进行拓扑排序,如果AOV网中所有的顶点都在拓扑序列中,则AOV网中必定无环。

拓扑排序是指将AOV网中的顶点排成一个线性序列,该序列必须满足:若从顶点i到顶点j有一条路径,则该序列中顶点i一定在顶点j之前。

如何进行拓扑排序呢?拓扑排序的基本思想:

①选择一个无前驱的顶点并输出;

②从图中删除该顶点和该顶点的所有发出边;

③重复①和②,直到不存在无前驱的顶点;

④如果输出的顶点数小于AOV网中的顶点数,则说明网中有环,否则输出的序列即为拓扑排序。

举例,学生应该按照怎样的顺序来学习下面这些课程

课程编号课程名称先修课程
C_{0}程序设计基础
C_{1}数据结构C_{0}C_{2}
C_{2}离散数学C_{0}
C_{3}高级程序设计C_{0}C_{5}
C_{4}数值分析C_{2}C_{3}C_{5}
C_{5}高等数学

如果用顶点表示课程,弧表示先修关系,若课程i是课程j的先修课程,在用弧<i,j>表示,课程之间的关系如图所示

C_{0}C_{5}都无前驱,先输出C_{0},删除C_{0}C_{0}的所有发出边,如图(a)所示;

此时C_{2}C_{5}都无前驱,输出C_{5},删除C_{5}C_{5}的所有发出边,如图(b)所示;

此时C_{2}C_{3}都无前驱,输出C_{2},删除C_{2}C_{2}的所有发出边,如图(c)所示;

此时C_{1}C_{3}都无前驱,输出C_{3},删除C_{3}C_{3}的所有发出边,如图(d)所示;

此时C_{1}C_{4}都无前驱,输出并删除即可。

很明显,拓扑排序不是唯一的, C_{0}C_{5}可以先输出C_{0}也可以先输出C_{5}(但程序代码中输出是唯一的)

第二章用代码来解决拓扑排序问题。 

二、算法 

上面讲到的删除顶点和边操作,没必要真的删除顶点和边。可以将没有前驱的顶点(入度为0)暂存到栈中,输出时出栈即表示删除。边的删除只需要将其邻接点的入度减1即可。如图所示,删除C_{0}的所有发出边(C_{1}C_{2}C_{3}顶点的入度减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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值