跳转至

BISHI144 食物链计数

较难通过率 35.52%python3样例通过牛客 AC

牛客原题  源码

讲解章节区间、树形与状压 DP

一句话

DAG 上从「入度为 0 的点」到「出度为 0 的点」的路径条数。

解题思路

这题考什么

DAG 上的路径计数 DP + 拓扑排序。

f[x] = 从 x 出发、沿有向边一直走到某个**出度为 0** 的点的路径条数
f[x] = 1                (x 出度为 0,自己就是一条长度为 0 的链)
f[x] = Σ_{x -> y} f[y]  (否则)

答案 = Σ_{入度为 0 的 x} f[x]。

注意题面文字与样例说明略有出入(说明里把 8 也列成了生产者), 以「出度为 0 = 链的一端、入度为 0 = 链的另一端」的形式化定义为准, 按样例验算:f(3)=f(5)=1, f(8)=f(9)=f(6)=1, f(2)=2, f(4)=3, f(7)=2, f(10)=4, f(1)=2+3+4=9,与期望输出 9 一致。

实现上按「出度」做 Kahn:先把出度为 0 的点入队, 出队 v 时把 f[v] 累加给所有 u(u -> v 的前驱),并把 u 的出度减 1, 减到 0 就入队。这样天然是逆拓扑序,不需要显式排序。

数据规模与复杂度

n, m <= 1e5,O(n + m)。答案保证 <= 1e9,不需要取模(Python 也不会溢出)。

坑在哪

  1. 方向别搞反:输入 u v 表示「v 捕食 u」,即有向边 u -> v; f 是沿边正向走到出度 0 的点的方案数,所以要用反向邻接表来递推;
  2. 孤立点不算食物链。链的定义是「由一条或多条边构成的路径」, 入度出度都是 0 的点连一条边都没有,不能算一条链。 不特判的话 f[x] = 1 会被算进答案,n 很大而边很少时会整体偏大;
  3. m 可以为 0;
  4. 迭代式队列,别写递归 DFS(n = 1e5 会爆栈)。

参考实现

solutions/BISHI144.py
import sys


def main() -> None:
    data = sys.stdin.buffer.read().split()
    n = int(data[0]); m = int(data[1])
    es = list(map(int, data[2:2 + 2 * m]))

    # 邻接表用 CSR(压缩稀疏行):不开 n 个小 list,而是把所有前驱首尾相接放进
    # 一个扁平数组 radj,再用 start 记下每个点那一段的起点,切片即可取用。
    # n, m 到 1e5 时,这样能省掉 1e5 个列表对象的内存与指针开销。
    # 第一遍:数出度、入度,以及每个 v 有多少个前驱
    outdeg = [0] * (n + 1)
    indeg = [0] * (n + 1)
    cnt = [0] * (n + 2)                      # 反向邻接表(按 v 分组存 u)的度数
    for i in range(0, 2 * m, 2):
        u = es[i]; v = es[i + 1]             # 输入 u v 的含义是 v 捕食 u,即有向边 u -> v
        outdeg[u] += 1
        indeg[v] += 1
        cnt[v] += 1
    # 对 cnt 做前缀和得到各段起点,start[n+1] 就是扁平数组的总长度
    start = [0] * (n + 2)
    acc = 0
    for v in range(1, n + 1):
        start[v] = acc
        acc += cnt[v]
    start[n + 1] = acc
    # 第二遍:pos 是各段的写入游标,把每条边填进它归属的那一段
    pos = start[:]
    radj = [0] * acc
    for i in range(0, 2 * m, 2):
        u = es[i]; v = es[i + 1]
        radj[pos[v]] = u                     # v 的前驱里有 u
        pos[v] += 1

    # 孤立点(入度出度都是 0)不构成食物链:链至少要有一条边
    isolated = bytearray(n + 1)
    for x in range(1, n + 1):
        if indeg[x] == 0 and outdeg[x] == 0:
            isolated[x] = 1

    f = [0] * (n + 1)
    # 按「出度」做 Kahn 拓扑:出度为 0 的点先入队,出队顺序天然是逆拓扑序,
    # 轮到 v 时它的所有后继都已结算完,f[v] 已是终值,不必显式排序
    queue = [v for v in range(1, n + 1) if outdeg[v] == 0]
    for v in queue:
        f[v] = 1                             # 出度为 0:自己就是链的起点
    head = 0                                 # 队首游标,queue 同时充当队列与访问序
    while head < len(queue):
        v = queue[head]; head += 1
        fv = f[v]
        for i in range(start[v], start[v + 1]):
            u = radj[i]                      # u -> v,把 v 的方案数并进前驱 u
            f[u] += fv
            outdeg[u] -= 1                   # u 的一条出边结算完毕
            if outdeg[u] == 0:               # 出边全部结算,f[u] 定型,可以入队
                queue.append(u)
    ans = 0
    for x in range(1, n + 1):
        # 链的另一端是入度为 0 的点;孤立点连一条边都没有,不算一条链
        if indeg[x] == 0 and not isolated[x]:
            ans += f[x]
    sys.stdout.write("%d\n" % ans)


main()
[:octicons-arrow-left-16: BISHI143](BISHI143.md) [BISHI145 :octicons-arrow-right-16:](BISHI145.md)