# DAG/拓扑排序

1. 每两个节点之间都有一端指向另一端的边，这条边有可能有权重。比如上图从A到B要花1块钱。
2. **图中没有环**。也就是说不可能从一个节点出发再回到这个节点。

{% hint style="info" %}
特别是因为第二条**图中没有环**这个性质，DAG的最值可以通过把节点进行排序来解决。这样的思路类似于动态规划中《最优子结构》的假设，把问题分解为子问题来归纳。我们之后会讲有向图环的检测。
{% endhint %}

## 基础：拓扑排序

通过拓扑排序，DAG的节点是有先后次序的。如此遍历就保证不会漏掉我们要找的答案。最简单的例子是二叉树，父节点总是先于儿子节点和孙子节点。如此一来，无论是先序遍历（自顶向下）或者后序遍历（自底向上）都能给我们正确答案。

我们首先来介绍拓扑排序。拓扑排序有两种基本算法：深度优先，广度优先（Khan‘s Algorithm）。

### 基于深度优先的拓扑排序

```python
def dfs(i):
    vis.add(i)
    for j in graph[i]:
        if j not in vis:
            dfs(j)
    res.append(i) # 后序遍历

vis, res = set(), []
dfs(0)
return res[::-1]
```

其中深度优先函数`dfs`相当于进行了后序遍历，所以最后我们把结果反转了一下。最终结果`res`就是从左到右拓扑排序过的节点。

### 基于广度优先的拓扑排序

这个算法又叫做[Khan's Algorithm](https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm)，引用了入度的概念。一个节点的入度也就是有几个箭头指着它。这里我们讨论的是无环图，所以一个节点不可能指着自己。

```python
# 计算初始入度
nodes = range(len(graph))
indegrees = {n: 0 for n in nodes}
for i in nodes:
    for j in graph[i]:
        indegrees[j] += 1

# 开始排序，先遍历队列中一直存着入度为0的节
queue = [n for n in n if indegrees[n]==0]
res = []
while queue:
    res.append(queue.pop())
    for j in graph[i]:
        indegrees[j] -= 1
        if indegrees[j]==0:
            queue.append(j)
return res
```

到此，拓扑排序就完成了。接下来我们可以计算最值且不用担心错过答案了。

## DAG的最短或最长路径

假设图中每条路径的花费永远是1（换句话说，我们的总花费=经过节点的个数），那么我们就可以说这个有向无环图是《无权值》的。

> **无论有否加权，DAG的最短或最长路径都可以用拓扑排序来计算。**

{% hint style="info" %}
**讨论：关于图中有否环的存在 -** 幸好是有向无环图，我们可以通过拓扑排序来解决这个问题。如果有向图中有环，那么解决方案和是否能够容易地找到答案要分情况讨论。我们把这个讨论留到下一节《无向图的最值》。由于图的边没有指向，一次遍历可以从每一个节点出发再返回到原节点，所以无向图可以考虑成带环有向图的特例。
{% endhint %}

下面我们用拓扑排序计算DAG最长路径。

```python
nodes = range(len(graph))
dist = {n: float('inf') for n in nodes}
dist[start] = 0

queue = topoSortDFS(graph)
while queue:
    i = queue.pop(0)
    for j, w in graph[i]:
        dist[j] = min(dist[j], w+dist.get(i, 0))
return dist
```

返回的dist是每个节点到最短路径。

其他情况的处理很简单：

* 如果是无权值的DAG，把`graph`中权重w设置成1即可。
* 如果求最长路径，把`dist`初始成最小值，然后比较函数`min`改为`max`即可。
