算法学习——拓扑排序

算法介绍

理论

在生活中,我们常常把一些较大的工程分解成几个子工程逐步完成,而子工程的完成常常有限定的先后顺序,如:先完成工程A,再完成工程B。我们将每一个工程抽象成一个结点,将工程完成的先后顺序抽象成一条边,就可以将工程之间的先后顺序转变为一张图。例如:假设完成工程C的先决条件是完成工程A和B,而完成A和B无先决条件,那么用图来表示就是:

显然这应该是一张有向图。
那么如何得到正确的完成工程的顺序呢?很明显仅仅DFS是不可能实现的,我们需要的是在每开始一项工程之前将它所有的前驱工作都完成,因此我们需要使用拓扑排序。(以上内容参考百度百科)

代码实现方式

要理解拓扑排序,首先需要知道图论的一个基本概念:
入度:有向图中某点作为图中边的终点的次数之和(某点的入边条数)
与之相对的是出度
出度:有向图中某点作为图中边的起点的次数之和(某点的出边条数)
拿上面那张图来说,A和B两点的出度均为1,入度为0,而C点的入度为2

在前面已经说过,我们将每一项工作的先后顺序抽象成了一条边,只有当完成了所有的前驱工作后才能进行该工作,用图来表示的话,就是只有当一个结点所有的入边都走过之后,才能访问该结点。
那么怎样表示一条边已经走过呢?我们发现,对于每个结点而言,其入边的访问顺序是任意的,只要入边都已经访问过,我们就能访问结点。因此,我们只需用一个数组记录每个结点的入度,初始时从入度为0的结点开始访问,每访问一个结点就将其出边删去(即出边指向的结点的入度减1),那么我们最终就能得到整个工程的完成顺序。而这就是拓扑排序的过程。

由于删去出边后入度变为0的点可能有多个,因此我们需要用队列来存储当前入度为0的点。

代码如下:

//in[v]表示每个点的入度,图用链式前向星存储
void TopSort(int u)
{
    q.push(u);
    while(!q.empty())
    {
        int temp = q.front();
        q.pop();
        for(int i=head[temp];i;i=e[i].nxt)
        {
            int v = e[i].to;
            in[v] --;
            if(!in[v]) q.push(v);
        }
    }
}

相关例题

例1 洛谷P1113杂务

这是一道简单的拓扑排序题,只需要在原来的模板上稍加改动即可。我们用一个权值数组w保存完成每项工作所需要的时间,再用一个Max数组动态更新到每个结点时时间的最大值即可(因为工作可以同时进行,因此需要取最大值来保证该结点的前驱工作已经全部完成)

代码如下:

#include <bits/stdc++.h>

using namespace std;

const int maxn = 1e5+10;
struct edge
{
    int to,nxt;
}e[maxn*100];
struct ti
{
    int now,t;
};
int w[maxn],head[maxn],in[maxn],cnt,ans,Max[maxn];
queue<ti> q;

void addEdge(int u,int v,int id)
{
    e[id].to = v;
    e[id].nxt = head[u];
    head[u] = id;
}

void read()
{
    int idx,len;
    scanf("%d %d",&idx,&len);
    w[idx] = len;
    while(1)
    {
        int temp;
        scanf("%d",&temp);
        if(!temp) break;
        in[idx] ++;
        addEdge(temp,idx,++cnt);
    }
}

void TopSort(int u)
{
    q.push((ti){u,w[u]});
    while(!q.empty())
    {
        ti temp = q.front();
        q.pop();
        for(int i=head[temp.now];i;i=e[i].nxt)
        {
            int v = e[i].to;
            in[v] --;
            Max[v] = max(Max[v],temp.t+w[v]);
            if(!in[v]) q.push((ti){v,Max[v]});
            ans = max(ans,temp.t+w[v]);
        }
    }
}

int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;++i)
        read();
    TopSort(1);
    printf("%d\n",ans);
    return 0;
}

例2 洛谷P1347排序

我们将两个元素之间的小于关系作为边,将两个元素作为点,建图。我们发现,如果这个数列的顺序是确定的,那么每个数在其中的位置也是唯一确定的,那如何找到这个顺序呢?
我们考虑每次取出这些数中的最大数,也就是入度为0的点,将这个点的所有出边删去,再去找剩下这些数中的最大数,即删去出边后入度为0的点,如此循环下去就可以找到这些数的顺序(前提是它的顺序已经确定),那么这就是一个拓扑排序。
有了这个想法之后再去判断其他的情况就不难了,如果我们在删边的过程中发现有两个出度为0的点,那么这个数列的顺序就未被确定;如果在拓扑排序的过程中发现有环,那么这个数列的大小关系就是矛盾的。
如何判断有环呢?
在这里插入图片描述
上图是一个包含环的有向图,可以发现,如果有向图内含有环的话,拓扑排序是无法访问到每个结点的,因为环内的每个结点的入度都无法变为0
因此我们只需要开一个标记数组和计数器,当拓扑排序遍历的结点数少于当前总结点数时,就是矛盾了。
当然这道题还有不少坑点,写的时候还是要细心些的
代码如下:

#include <bits/stdc++.h>

using namespace std;

const int maxn = 30,maxm = 5e5+10;
struct edge
{
    int to,nxt;
}e[maxm];
int cnt,head[maxn],in[maxn],temp[maxn],n,ans[maxn],now;
bool vis1[maxn],vis2[maxn];
queue<int> q;

void addEdge(int u,int v,int idx)
{
    e[idx].to = v;
    e[idx].nxt = head[u];
    head[u] = idx;
    in[v] ++;
}

///0表示无法确定,1表示可以确定,2表示矛盾
int TopSort()
{
    bool first = false,second = false;
    int sum = 0,cnt1 = 0;
    for(int i=1;i<=n;++i)
    {
        if(!in[i]&&vis1[i])
        {
            if(first&&!second) second = true;
            if(!first) first = true;
            q.push(i);
        }
        temp[i] = in[i];
    }
    while(!q.empty())
    {
        first = false;
        int t = q.front();
        q.pop();
        if(!vis2[t]) vis2[t] = true,sum ++;
        ans[++cnt1] = t;
        for(int i=head[t];i;i=e[i].nxt)
        {
            int v = e[i].to;
            temp[v] --;
            if(!temp[v])
            {
                q.push(v);
                if(first) second = true;
                else first = true;
            }
        }
    }
    if(sum<now) return 2;
    else if(second) return 0;
    else if(sum==n) return 1;
    return 0;
}

int main()
{
    int m,over;
    bool isContinue = false,isOK = false;
    scanf("%d %d",&n,&m);
    for(int i=1;i<=m;++i)
    {
        memset(vis2,0,sizeof(vis2));
        int a,b,flag;
        char ch = getchar();
        while(ch<'A'||ch>'Z') ch = getchar();
        a = ch-'A'+1;
        if(!vis1[a]) vis1[a] = true,now ++;
        ch = getchar();
        while(ch<'A'||ch>'Z') ch = getchar();
        b = ch-'A'+1;
        if(!vis1[b]) vis1[b] = true,now ++;
        if(isContinue||isOK) continue;
        addEdge(a,b,++cnt);

        flag = TopSort();
        if(flag==1) over = i,isOK = true;
        else if(flag==2) over = i,isContinue = true;
    }
    if(isOK)
    {
        printf("Sorted sequence determined after %d relations: ",over);
        for(int i=1;i<=n;++i)
            printf("%c",ans[i]-1+'A');
        printf(".\n");
    }
    else if(isContinue) printf("Inconsistency found after %d relations.\n",over);
    else printf("Sorted sequence cannot be determined.\n");
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值