拓扑排序

。_饼干妹妹 提交于 2019-11-26 21:03:34

一、实际生活中的问题

  在日常生活中,一项大的工程可以看作是由若干个子工程组成的集合,这些子工程之间必定存在一些先后关系,即某些子工程必须在其它一些子工程完成之后才能开始。

  我们可以用有向图来表示这些子工程之间的先后关系:子工程为顶点,子工程之间的先后关系为有向边,这种有向图称为“顶点活动网络”,又称“AOV 网”。一个AOV 网应该是一个有向无环图(Directed Acyclic Graph,DAG),否则必定会有一些活动互相牵制,造成环中的活动都无法进行。

二、WHAT IS 拓扑排序

  在AOV 网中,所有活动可排列成一个线性序列,使得每个活动的所有前驱活动都排在该活动的前面。

  对一个DAG G = (V, E) 进行拓扑排序,是将G 中所有顶点排成一个线性序列,使得图中任意一对顶点u 和v,若边u —> v ∈ E,则u 在线性序列中出现在v 之前。

  由某个集合上的一个偏序得到该集合上的一个全序,这个操作称为拓扑排序。所得的线性序列,称为拓扑序。

三、算法流程

  1. 在有向图中选一个没有前驱的顶点并且输出;

  2. 从图中删除该顶点和所有它指出的有向边;

  3. 重复上述两步,直至所有顶点输出,或者当前图中不存在无前驱的顶点为止,后者代表我们的有向图是有环的。因此,也可以通过拓扑排序来判断一个图是否有环。

四、例题与代码实现

模板题1  比赛 有n个比赛队进行比赛,给出m场比赛的结果,每场都形如P1赢P2(排名时P1在P2之前)。你需要确定排名。由于排名可能不唯一,你需要给出字典序最小的排名方案。输入第一行,两个整数,n和m。接下来m行,每行两个整数P1, P2。输出共一行,n个整数,表示字典序最小的排名。

SOL:若是P1赢P2,就连一条从P1到P2的边,使用小根堆(优先队列)进行拓扑排序即可。

#include <cstdio> 
#include <queue>
using namespace std;
int n,m,deg[100010];
struct DAGedge
{
    int next,to;
}edge[200010];
int head[100010],edge_num;
void add(int from,int to)//链式前向星存边 
{
    edge[++edge_num].next=head[from];
    edge[edge_num].to=to;
    head[from]=edge_num;
}
priority_queue<int,vector<int>,greater<int> > q;//由于按照字典序输出,应使用优先队列实现拓扑排序 
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++) 
    {
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y);
        deg[y]++;
    }//读入并存边 
    for(int i=1;i<=n;i++) if(deg[i]==0) q.push(i);//将入度为0的点加入队列 
    while(!q.empty())
    {
        int t=q.top();
        q.pop();
        printf("%d ",t);//取出字典序最小的点并输出 
        for(int i=head[t];i;i=edge[i].next)//枚举与其相连的所有边 
        {
            deg[edge[i].to]--;//将这些边的入度-1,相当于删去了这些边 
            if(deg[edge[i].to]==0) q.push(edge[i].to);//如果该点的入度变为0则加入队列 
        }
    }
    return 0;
}
I LOVE LIJINGYAN...

模板题2  【NOIp提高组 2003】神经网络

#include <cstdio>
#include <stack>
#pragma optimize (2)
#define debug() printf("我喜欢李靖妍")
using namespace std;
stack <int> sta;
int kase[1005], head[1005], tot, n, p, indeg[1005];
bool ok = false;
struct edge
{
    int u, v, next, w;
} e[100005];
void addedge(int from, int to, int value)
{
    e[++tot].u = from;
    e[tot].v = to;
    e[tot].w = value;
    e[tot].next = head[from];
    head[from] = tot;
}
void toposort()
{
    while (!sta.empty())
    {
        int u = sta.top();
        sta.pop();
        if (kase[u] <= 0)
        {
            for (int i = head[u]; i; i = e[i].next)
            {
                indeg[e[i].v]--;
                if (!indeg[e[i].v])
                    sta.push(e[i].v); 
            }
        }
        else
        {
            for (int i = head[u]; i; i = e[i].next)
            {
                indeg[e[i].v]--;
                kase[e[i].v] += kase[u] * e[i].w;
                if (!indeg[e[i].v])
                    sta.push(e[i].v);
            }
        }
    }
}
int main()
{
    scanf("%d%d", &n, &p);
    for (int i = 1; i <= n; i++)
    {
        int que;
        scanf("%d%d", &kase[i], &que);
        if (kase[i] != 0)
            sta.push(i);
        else
            kase[i] -= que;
    }
    for (int i = 1; i <= p; i++)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        addedge(a, b, c);
        indeg[b]++;
    }
    toposort();
    for (int i = 1; i <= n; i++)
    {
        if (!head[i] && kase[i] > 0)
        {
            printf("%d %d\n", i, kase[i]);
            ok = true;
        }
    }
    if (!ok) printf("NULL");
    return 0;
}
View My Love

此题建议使用栈来实现。

实战题1 某公司有n员工,这n个员工共有m个要求,每个要求表示第x个人的工资比第y个人的高。每个人的工资都是整数,且至少为888。问老板发出的总工资最少是多少。

输入
第一行,两个整数,n和m。
接下来m行,每行两个整数x, y,表示第x个人的工资比第y个人的高。
输出
共一行,一个整数,表示最少的总工资。
样例
输入

2 1
1 2

输出

1777

数据范围
n≤10000, m≤20000

SOL:由于最低工资有限制,我们肯定从小往大推。对于ax > ay 的限制,建边y -> x,进行拓扑排序,那么每次取出入度为0 的点,用它的工资+1 去更新后继节点。

#include <cstdio>
#include <iostream>
#include <cmath>
#include <algorithm>
#include <cstring>
#include<string>
#include<queue>
using namespace std;
int n,m,ru[10010],money[10010];
struct hh
{
  int next,to;
}edge[20010];
int head[10010],edge_num;
void add(int from,int to)
{
  edge[++edge_num].next=head[from];
  edge[edge_num].to=to;
  head[from]=edge_num;
}
queue<int> q;
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++) 
    {
      int x,y;
      scanf("%d%d",&x,&y);
      add(y,x);
      ru[x]++;
    }
    for(int i=1;i<=n;i++) if(ru[i]==0) {q.push(i);money[i]=888;}
    int ans=0;
    while(!q.empty())
    {
      int t=q.front();
      ans+=money[t];
      q.pop();
      for(int i=head[t];i;i=edge[i].next)
      {
        ru[edge[i].to]--;
        if(ru[edge[i].to]==0) {money[edge[i].to]=money[t]+1;q.push(edge[i].to);}
      }
    }
    printf("%d",ans);
    return 0;
}
LiJingyan is the prettiest!

实战题2  【NOIp普及组 2013】车站分级

然而我作死用线段树维护拓扑排序

#include <cstdio>
#define nm 1501 
using namespace std;

int h[nm << 3], n, m, s[nm], ind[nm << 3], num[nm];
int dis[nm << 3], depth[nm << 3], now, topo, q[nm << 5];
int head, talow, count, ans;

struct edge
{
    int to;
    int next;
}e[nm * 699];

inline char getchars()
{
    static char buf[100000], * p1 = buf, * p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++;
}

inline int re()
{
    int x = 0, fl = 1;
    char ch = getchars();
    for (; ch < 48 || ch>57; ch = getchars())
        if (ch == '-')
            fl = -1;
    for (; 48 <= ch && ch <= 57; ch = getchars())
        x = (x << 3) + (x << 1) + (ch ^ 48);
    return x * fl;
}

inline void plus(int a, int b)
{
    ind[b]++;
    e[++now] = (edge){ b, h[a] };
    h[a] = now;
}

inline int ls(int x)
{
    return x << 1;
}

inline int rs(int y)
{
    return y << 1 | 1;
}

inline void make(int ljy, int l, int r)
{
    if (ljy > topo)
        topo = ljy;
    if (l == r)
    {
        num[l] = ljy;
        dis[ljy] = 1;
        return;
    }
    int mid = (l + r) >> 1;
    make(ls(ljy), l, mid);
    plus(ls(ljy), ljy);
    plus(rs(ljy), ljy);
    make(rs(ljy), mid + 1, r);
}

inline void ud(int ljy, int l, int r, int x, int y, int tmp)
{
    if (x <= l && r <= y)
    {
        plus(ljy, tmp);
        return;
    }
    int mid = (l + r) >> 1;
    if (mid >= x)
        ud(ls(ljy), l, mid, x, y, tmp);
    if (y > mid)
        ud(rs(ljy), mid + 1, r, x, y, tmp);
}

inline void toposort(int a)
{
    for (int i = 1; i <= topo; i++)
        if (!ind[i])
        {
            q[talow++] = i;
            depth[i] = dis[i];
        }
    while (head < talow && a)
    {
        int u = q[head++];
        for (int i = h[u]; i; i = e[i].next)
        {
            int v = e[i].to;
            if (depth[u] + dis[v] > depth[v])
                depth[v] = depth[u] + dis[v];
            ind[v]--;
            if (!ind[v])
                q[talow++] = v;
        }
    }
}

int main()
{
    n = re();
    m = re();
    make(1, 1, n);
    for (int i = 1; i <= m; i++)
    {
        count = re();
        topo++;
        for (int j = 1; j <= count; j++)
            s[j] = re();
        for (int j = 1; j < count; j++)
        {
            plus(topo, num[s[j]]);
            if (s[j] + 1 <= s[j + 1] - 1)
                ud(1, 1, n, s[j] + 1, s[j + 1] - 1, topo);
        }
        plus(topo, num[s[count]]);
    }
    toposort(1);
    for (int i = 1; i <= n; i++)
        if (depth[num[i]] > ans)
            ans = depth[num[i]];
    printf("%d", ans);
    return 0;
}
My_love_2_ljy=true;

实战题3 给定一个DAG,含有n个点和m条有向边。你需要计算极长有向路径的数目。如果一条路径u1→u2→⋯→uk满足:对于1≤i<k,边ui→ui+1∈E; 原图中,u1u1没有前驱节点、uk没有后继节点,那么这条路径就是极长有向路径。单点不算极长有向路径

格式输入

第一行,两个整数n, m。接下来mm行,每行两个整数u, v,表示一条u到v的有向边。

输出

共一行,一个整数,表示极长有向路径的数目,保证在int范围内。

样例输入

10 16
1 2
1 4
1 10
2 3
2 5
4 3
4 5
4 8
6 5
7 6
7 9
8 5
9 8
10 6
10 7
10 9

输出

9

数据范围

n≤105, m≤2×105

需要使用加法原理emmm

#include <cstdio>
#include <iostream>
#include <cmath>
#include <algorithm>
#include <cstring>
#include<string>
#include<queue>
using namespace std;
int n,m,ru[100010],num[100010];
struct hh
{
  int next,to;
}edge[200010];
int head[100010],edge_num;
void add(int from,int to)
{
  edge[++edge_num].next=head[from];
  edge[edge_num].to=to;
  head[from]=edge_num;
}
queue<int> q;
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++) 
    {
      int x,y;
      scanf("%d%d",&x,&y);
      add(x,y);
      ru[y]++;
    }
    for(int i=1;i<=n;i++) if(ru[i]==0 && head[i]) {q.push(i);num[i]=1;}
    int ans=0;
    while(!q.empty())
    {
      int t=q.front();
      if(!head[t]) ans+=num[t];
      q.pop();
      for(int i=head[t];i;i=edge[i].next)
      {
        ru[edge[i].to]--;
        num[edge[i].to]+=num[t];
        if(ru[edge[i].to]==0) {q.push(edge[i].to);}
      }
    }
    printf("%d",ans);
    return 0;
}
while("我喜欢李靖妍");

实战题4  

你需要生成一个n的排列,有m个要求。第i个要求长为ki,有ki个数ai,1,ai,2,…,ai,ki, 表示在最终的排列里,这ki个数要按照这个顺序组成一个子序列(不一定要连续)。

因为很可能不是所有的要求都能满足,你要最大化M,使得前M个要求都能被满足。在这个前提下,排列的字典序最小。

格式输入

第一行,两个整数n和m。接下来m行,每行描述一个要求:第一个数是ki,接下来ki个整数ai,j,含义如题目所述。

输出  共一行,n个整数,表示满足要求的字典序。

样例输入

4 3
3 1 2 3
2 4 2
3 3 4 1

输出

1 4 2 3

数据范围

n≤105, ∑ki≤2×105

思路:最大化M 可以通过二分答案来做。把一个要求拆成ki- 1 个要求(有向边),拓扑排序即可。

 

#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;
int n,m;
struct edge
{
    int from,to,nxt;
} e[200010];
int cnt;
struct point
{
    int to,deg,fir,num;
} p[100010];
bool operator < (point a,point b)
{
    return a.num>b.num;
}
priority_queue <point> pq;
void add(int from,int to,int num)
{
    e[num].nxt=p[from].fir,p[from].fir=num,p[to].deg++;
    return;
}
int asd[100010];
int ans1[100010],ans2[100010];
void init(int tim)
{
    for(int i=1;i<=asd[tim];i++)
    {
        e[i].nxt=0;
    }
    for(int i=1;i<=n;i++)
    {
        p[i].deg=0,p[i].fir=0;
    }
    for(int i=1;i<=asd[tim];i++)
    {
        add(e[i].from,e[i].to,i);
    }
    return;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) p[i].num=i;
    for(int i=1;i<=m;i++)
    {
        int ki;
        scanf("%d",&ki);
        asd[i]=asd[i-1]+ki-1;
        int tmp;
        scanf("%d",&tmp);
        for(int j=2;j<=ki;j++)
        {
            int tmp2;
            scanf("%d",&tmp2);
            cnt++;
            e[cnt].from=tmp,e[cnt].to=tmp2;
            tmp=tmp2;
        }
    }
    int l=0,r=m;
    while(l<r)
    {
        int cnt2=0;
        int mid=(l+r)/2;
        init(mid);
        for(int i=1;i<=n;i++)
        {
            if(!p[i].deg)
            {
                pq.push(p[i]);
            }
        }
        while(!pq.empty())
        {
            point tmp=pq.top();
            pq.pop();
            cnt2++;
            ans2[cnt2]=tmp.num;
            for(int q=tmp.fir;q;q=e[q].nxt)
            {
                p[e[q].to].deg--;
                if(!p[e[q].to].deg)
                {
                    pq.push(p[e[q].to]);
                }
            }
        }
        if(cnt2==n)
        {
            for(int i=1;i<=n;i++)
            {
                ans1[i]=ans2[i];
            }
            l=mid+1;
        }
        else
        {
            r=mid;
        }
    }
    for(int i=1;i<=n;i++)
    {
        printf("%d ",ans1[i]);
    }
    return 0;
}
Lijingyan.cute==true;

 实战题5 

对一个有n个点、m条边的DAG拓扑排序。
定义一个拓扑序{pi}的位置数列{qi}为:  ,换言之,qj表示数j在拓扑序中的位置。你需要给出位置数列字典序最小的方案。

格式输入   第一行,两个整数,n和m。接下来m行,每行两个整数u, v,表示有一条u到v的有向边。

输出    共一行,nn个整数,表示位置数列字典序最小的方案。注意,你需要输出的是拓扑序,而不是位置数列。

样例输入1

3 1
3 1

输出1

3 1 2

输入2

6 4
6 3
3 1
5 4
4 2

输出2

6 3 1 5 4 2

数据范围
对于100%的数据,n≤105, m≤2×105

#include<bits/stdc++.h>
using namespace std;
const int maxn=100010;
const int maxm=200010;
int n,m,u,v;
int head[maxn],nxt[maxm],to[maxm],in[maxn];
int tot;
stack<int> st;
void add(int x,int y){
    to[++tot]=y;
    nxt[tot]=head[x];
    head[x]=tot;
}
void topo(){
    priority_queue<int> q;
    for (int i=1;i<=n;i++){
        if (in[i]==0) q.push(i);
    }
    while (!q.empty()){
        int x=q.top();
        q.pop();
        st.push(x);
        for (int i=head[x];i;i=nxt[i]){
            in[to[i]]--;
            if (in[to[i]]==0) q.push(to[i]);
        }
    }
}
int main(){
    scanf("%d%d",&n,&m);
    for (int i=1;i<=m;i++){
        scanf("%d%d",&u,&v);
        add(v,u);
        in[u]++;
    }
    topo();
    while (!st.empty()){
        printf("%d ",st.top());
        st.pop();
    }
}
LiJingYan %%%

实战题6 

给定一个DAG,有n个点、m条有向边,且每条有向边均从编号小的节点指向编号大的节点。图G还满足,对于1≤i<n,i号点一定能够到达n号点。有可能会有重边。如果一条路径u1→u2→⋯→uk满足:对于1≤i<k,边ui→ui+1∈E; 原图中,u1没有前驱节点、uk没有后继节点,那么这条路径就是极长有向路径。一条边的繁忙程度,定义为经过这条边的极长有向路径的数目,你需要找出最繁忙的边的繁忙程度。

格式输入    第一行,两个整数n, 。接下来m行,每行两个整数u, v,表示一条u到v的有向边。

输出  共一行,一个整数,表示最繁忙的边的繁忙程度,保证答案在int范围内

样例输入

7 7
1 3
3 4
3 5
4 6
2 3
5 6
6 7

输出

4

数据范围

100%的数据满足n≤105, m≤2×105

#include<cstdio>
#include<iostream>
#include<queue>
using namespace std;
int n,cnt=0,ans=0,head[500005],rd[500005],dp[500005];
struct Edge{
    int v,w,nxt;
}e[500005],e2[500005];
void addEdge(int u,int v,int w){
    e[++cnt].v=v;
    e[cnt].w=w;
    e[cnt].nxt=head[u];
    head[u]=cnt;
}
void topoSort(){
    queue<int> q;
    for(int i=1;i<=n;i++){
        if(rd[i]==0){
            q.push(i);
            dp[i]=1;
        }
    }
    while(!q.empty()){
        int nowValue=q.front();q.pop();
        for(int i=head[nowValue];i;i=e[i].nxt){
            rd[e[i].v]--;
            dp[e[i].v]+=dp[nowValue];
            //cout<<"dp["<<e[i].v<<"]+=dp["<<nowValue<<"]"<<endl;
            ans=max(ans,dp[e[i].v]);
            if(rd[e[i].v]==0){
                q.push(e[i].v);
            }
        }
    }
} 
int cnt2=0,ans2=0,head2[500005],rd2[500005],dp2[500005];
void addEdge2(int u,int v,int w){
    e2[++cnt2].v=v;
    e2[cnt2].w=w;
    e2[cnt2].nxt=head2[u];
    head2[u]=cnt2;
}
void topoSort2(){
    queue<int> q;
    for(int i=1;i<=n;i++){
        if(rd2[i]==0){
            q.push(i);
            dp2[i]=1;
        }
    }
    while(!q.empty()){
        int nowValue=q.front();q.pop();
        for(int i=head2[nowValue];i;i=e2[i].nxt){
            rd2[e2[i].v]--;
            dp2[e2[i].v]+=dp2[nowValue];
            //cout<<"dp2["<<e2[i].v<<"]+=dp2["<<nowValue<<"]"<<endl;
            ans2=max(ans2,dp2[e2[i].v]);
            if(rd2[e2[i].v]==0){
                q.push(e2[i].v);
            }
        }
    }
}
int trueAns=0;
void work(){
    queue<int> q;
    for(int i=1;i<=n;i++){
        if(rd[i]==0){
            q.push(i);
        }
    }
    while(!q.empty()){
        int nowValue=q.front();q.pop();
        for(int i=head[nowValue];i;i=e[i].nxt){
            rd[e[i].v]--;
            trueAns=max(trueAns,dp[nowValue]*dp2[e[i].v]);
            if(rd[e[i].v]==0){
                q.push(e[i].v);
            }
        }
    }
}
int main(){
   
    int m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        int ta,tb;
        scanf("%d%d",&ta,&tb);
        addEdge(ta,tb,1);
        addEdge2(tb,ta,1);
        rd[tb]++;
        rd2[ta]++;
    }
    topoSort();
    topoSort2();
    work();
    printf("%d\n",trueAns);
    //printf("%d\n",ans2);
    return 0;
}
LOVE U IS SO HARD...

[全剧终]

以上是史上最烂拓扑排序教程,如果想看巨佬的生花妙笔,请点击链接  会宁狐狸    陌阡    RandomName

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!