对一个DAG$G=(V,E)$($V$为点集,$E$为边集)进行拓扑排序,是将$G$中所有顶点排成一个线性序列,使得图中任意一边$(u,v)∈E$,$u$在线性序列中出现在$v$之前……
一 概念
对一个DAG $G=(V,E)$($V$为点集,$E$为边集)进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一边$(u,v)∈E$,$u$在线性序列中出现在$v$之前。通常,这样的线性序列称为满足拓扑次(Topological  Order)的序列,简称拓扑序列。
简单来说,就是找一种排序方案,使得每条边的起点总排在终点之前。
注意:一个DAG的拓扑序列不一定是唯一的。

如图,合法的拓扑序列有$4,3,6,1,2,5$或$4,3,6,5,1,2$。
二 实现
1 怎么求拓扑序
要使得起点排在终点之前,那么我们首先要找到起点,即入度为$0$的节点。
然后我们删去这些点,即在图中删除以它们为起点的边。
因为DAG的特性,那么这样操作以后,新得到的图也一定是一个DAG。
于是我们可以继续找起点,删边,那么我们得到起点的顺序就是一个拓扑序。
因为DAG的性质,所以一个点不会被重复访问。
2 算法
求出一有向图的一个拓扑序。
边表
1 2 3 4 5 6 7 8 9 10 11 12 13
   | const int CP=1e3+3; const int CE=CP*CP;
  class fs{     public:         int to,nxt; }E[CE]; int hd[CP],ecnt=0; void add(int x,int y){     E[++ecnt].to=y;     E[ecnt].nxt=hd[x];     hd[x]=ecnt; }
   | 
 
排序算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
   | const int CP=1e3+3; const int CE=CP*CP;
  int in[CP];  int ans[CP]; 
  queue<int>Q; 
  void tsort() {     int pos=0;          for(int i=1;i<=n;i++)         for(int k=hd[i]; k; k=E[k].nxt)             in[E[k].to]++;            for(int i=1;i<=n;i++)         if(!in[i])             Q.push(i);           while(!Q.empty())     {         int cur=Q.front();         Q.pop();                  ans[++pos]=cur;                   for(int k=hd[cur]; k; k=E[k].nxt)         {             int to=E[k].to;             in[to]--;              if(!in[to])                  Q.push(to);         }     } }
   | 
 
三 建图
拓扑排序的题目中,难点一般在建图,因为建图之后求拓扑序是一件极其容易的事情。
讨论建图之前,我们先讨论一些基础的东西。
1 字典序
字符在某一标准(一般是ASCII)中出现的先后顺序叫做字典序。
类似于数字比大小,某一字符串$A$相对于字符串$B$,$A$的高位上的字符在标准中比$B$出现得早,则称$A$的字典序(相对于$B$)小。反之,则称$A$的字典序(相对于$B$)大。
若以ASCII为标准,则串$abc$比串$acb$字典序小。
设定拓扑序列中第一个元素为最高位,最后一个元素为最低位。
那么字典序小的拓扑序则是高位元素的编号尽量小,字典序大的拓扑序则是高位元素的编号尽量大。
对于第一章节中的图,拓扑序列$4,3,6,1,2,5$的字典序小,拓扑序列$4,3,6,5,1,2$的字典序大。
2 怎么求一个字典序大或小的拓扑序列
前文中给出的算法是以入队顺序确定拓扑序列。那么怎么求一个指定字典序大小的拓扑序列呢?
只需要把上文算法中的普通队列改成优先队列即可。因为在队列中的元素总是起点,在拓扑序列中的相对位置可以调换。
具体实现(重载运算符)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
   | const int CP=1e3+3; const int CE=CP*CP;
  class node{   public:     int cde;     void init(int c){cde=c;}               bool operator < (const node a)const     {         return cde<a.cde;     }          
 
 
 
 
 
  };
  priority_queue<node>Q;
   | 
 
然后照常跑拓扑排序即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
   | int in[CP];
  void tsort() {     int pos=0;          for(int i=1;i<=n;i++)         for(int k=hd[i]; k; k=E[k].nxt)             in[E[k].to]++;            for(int i=1;i<=n;i++)         if(!in[i])         {             node temp;             temp.init(i);             Q.push(temp);         }          while(!Q.empty())     {         node cur=Q.top();         Q.pop();                  ans[++pos]=cur.cde;                   for(int k=hd[cur]; k; k=E[k].nxt)         {             int to=E[k].to;             in[to]--;              if(!in[to])              {                 node temp;                 temp.init(to);                 Q.push(temp);             }         }     } }
   | 
 
3 例题1-建图求拓扑序
「Luogu P1983」车站分级
一条单向的铁路线上,依次有编号为$ 1, 2, …, n $的$ n $个火车站。每个火车站都有一个级别,最低为 $1$ 级。现有若干趟车次在这条线路上行驶,每一趟都满足如下要求:如果这趟车次停靠了火车站 $x$,则始发站、终点站之间所有级别大于等于火车站 $x$ 的都必须停靠。(注意:起始站和终点站自然也算作事先已知需要停靠的站点)
现有 $m$ 趟车次的运行情况(全部满足要求),试推算这 $n$ 个火车站至少分为几个不同的级别。
 
对于任意一趟列车,经过的站点总比没有经过的级别高,我们把级别高的站点向级别低的站点依次连边。
这样处理完$m$趟列车后,我们就会得到一个DAG。
接下来我们只需要对这个DAG进行拓扑排序。但还有一个问题,怎么求出最小级别数?
很明显,若定义排序起点的深度为$1$,那么这个最小级别数就是图中节点的最大深度。我们只需要在广搜的时候递推记录一下节点深度就可以了。
递推式:
$$\begin{aligned}f_i&=1  | i \in S \newline f_i&=\max f_i,f_j+1  |  (j,i)\in E \end{aligned}$$
$S$为起点集,$E$为边集。
具体实现(仅核心算法):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
   | const int CP=1e3+3;
  int f[CP];
  for(int i=1;i<=n;i++)     if(!in[i])     {         f[i]=1;         Q.push(i);     }
  while(!Q.empty()) {     int cur=Q.front();     Q.pop();              for(int k=hd[cur]; k; k=E[k].nxt)     {         int to=E[k].to;                  f[to]=max(f[to], f[cur]+1);                   in[to]--;          if(!in[to])              Q.push(to);     } }
   | 
 
4 反图
4-1 什么是反图?
对一个有向图$G={V,E}$,若存在一个图$G_r={V,E_r}$,其中两个图的点集相同,边集相反(定义为每一条边的方向均相反),那么称$G_r$为$G$的反图。
建立反图其实是一件非常简单的事情,只需要在节点连边的时候连反向边即可。
4-2为什么要建反图?
先考虑能否用字典序解决:
字典序小使得居高位的数字尽可能小;
字典序大使得居高位的数字尽可能大。
于是我们发现,最小的字典序并不能保证最小数字尽可能靠前。
继续分析:
最小数字尽可能靠前,那么其它数字(都比它大)要尽可能靠后。
看不出什么来,那我们反过来看:最小数字尽可能靠后,那么其它数字要尽可能前。
这恰好是原图中的最大字典序(无论高位怎样排布,最大字典序会使低位尽可能小,也就是最小数字尽可能靠后)。
于是我们求一个最大字典序的拓扑序列,反过来读,就是答案吗?
不是。
就像sort一样,一段排好序的数列,反过来读(相对于排序条件)一定是无序的。
那么怎么解决这个问题?
考虑反向建图。
在原图的反图中跑最大字典序的拓扑序列。这个序列看起来是不合法的,但是反过来看就exciting了。
我们不难发现,反图中的每个拓扑序列,都是正图中的一个合法拓扑序列反转而形成的。继续考虑,求出反图中最大字典序的拓扑序列$K$,会使得最小数字在$K$中出现的尽量晚。然后我们把$K$反转形成$K_r$,那么最小数字在$K_r$中就会出现的尽可能早。然后$K_r$依然是正图的合法拓扑序列,完美解决。
4-3 算法实现
代码依然是上面的代码改了改。
边表
1 2 3 4 5 6 7 8 9 10 11 12 13
   | const int CP=1e3+3; const int CE=CP*CP;
  class fs{     public:         int to,nxt; }E[CE]; int hd[CP],ecnt=0; void add(int x,int y){     E[++ecnt].to=x;      E[ecnt].nxt=hd[y];     hd[y]=ecnt; }
   | 
 
排序算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
   | const int CP=1e3+3; const int CE=CP*CP;
  class node{   public:     int cde;     void init(int c){cde=c;}     bool operator < (const node a)const     {         return cde<a.cde;      } };
  int in[CP]; int ans[CP];
  prioirty_queue<node>Q;
  void tsort() {     int pos=n;           for(int i=1;i<=n;i++)         for(int k=hd[i]; k; k=E[k].nxt)             in[E[k].to]++;            for(int i=1;i<=n;i++)         if(!in[i])         {             node temp;             temp.init(i);             Q.push(temp);         }          while(!Q.empty())     {         node cur=Q.top();         Q.pop();                  ans[pos--]=cur.cde;                   for(int k=hd[cur]; k; k=E[k].nxt)         {             int to=E[k].to;             in[to]--;              if(!in[to])              {                 node temp;                 temp.init(to);                 Q.push(temp);             }         }     } }
   | 
 
5 例题2-建立反图解决拓扑问题
「HNOI2015」菜肴制作
知名美食家小A被邀请至ATM 大酒店,为其品评菜肴。 ATM 酒店为小A准备了 N 道菜肴,酒店按照为菜肴预估的质量从高到低给予1到N的顺序编号,预估质量最高的菜肴编号为1。
由于菜肴之间口味搭配的问题,某些菜肴必须在另一些菜肴之前制作,具体的,一共有 M 条形如”i 号菜肴’必须’先于 j 号菜肴制作“的限制,我们将这样的限制简写为<i,j>。
现在,给出若干组形如<i,j>的限制,酒店希望能求出一个最优的菜肴的制作顺序,使得小A能尽量先吃到质量高的菜肴
 
最小编号最靠前,明显是一个反图拓扑序问题。对于每组限制<i,j>,从j向i连边,然后求最大字典序,反着输出即可。