跳转至

拓朴排序

引入

在生活中,我们在完成一个工作可能需要按一定顺序完成若干个任务。例如在完成 \(B\) 前必须先完成 \(A\) ,那么我们就说 \(B\)\(A\) 存在偏序关系。我们用工作中任务之间的偏序关系建一个有向图,如下:

Image title

拓扑排序其实就是找到一个执行任务的顺序,使这个顺序满足上述的偏序条件。如上图的的一个拓扑序就是 \(A\rightarrow D\rightarrow B\rightarrow C\)

定义

对于一个有向无环图\(G=(V,E)\)来说,其拓扑排序是\(G\)中所有节点的一种线性次序。即将图的所有节点在一条水平线上排开,图的所有有向边都从左指向右。

上图的一种拓扑序:

Image title

但是对于一个有环图,是不能进行拓扑排序的,这很好理解。首先拓扑排序时,只有一个点没有前驱时,我们才能对其进行排序。但是对于一个环形的偏序关系,我们是找不到可以排序的点的。

Image title

拓扑排序

我用数组 \(deg[]\) 记录一个点的入度,用邻接表的形式存图。

我们从第入度为 \(0\) 的节点开始,删除该顶点,并删除该顶点所有的出边。重复该步骤直到删除所有顶点。

我们在删除边时不必要真的删除,我们只需将该边的终点的入度减 \(1\) 即可。因为我们在拓扑排序过程中,只关心一个点当前是否有前驱,当 \(deg[u]=0\) 时,代表一个点没有前驱,就可以将其排到当前拓扑序的下一个位置。

实现上述过程用栈或队列都可以。

void topo()
{
    queue<int> que;
    vector<int> ans;    //记录拓扑序
    // 将一开始入度为0的点加入到队列中
    for(int i = 1;i <= n;++i)
        if(!deg[i])
        {
            que.push(i);
            ans.push_back(i);
        }

    while(!que.empty())
    {
        int u = que.front();
        que.pop();
        for(auto& v:g[u])
        {
            // 终点的入度减1,相当于删掉这条边
            deg[v]--;

            // 删除之后入度为0,说明v已经没有前驱将其加入队列和拓扑序中
            if(!deg[v])
            {
                que.push(v);
                ans.push_back(v);
            }
        }
    }
}

利用拓扑排序找环

我们进行一次拓扑排序之后,我们可以发现,所有不在环上的点入度都变成了 \(0\) ,而在环上的点入度都不为 \(0\)

在处理一些有环图问题时(如:基环树),首先将环处理出来,可以很大程度上降低问题的难度。

=================

Exercise

题目 难度 知识
B3644 普及− 拓扑排序模板
P1347 普及+/提高 拓扑排序判断环
LC.2603 困难 拓扑排序变形