Luogu P4017 最大食物链计数
题目分析
首先,我们需要正确理解题意。题目要求计算一个“食物网”中“最大食物链”的数量。
-
食物网是什么? 题目描述了生物之间的吃与被吃关系。例如,“A 被 B 吃”可以理解为有一个从 A 指向 B 的箭头。这样,整个食物网就构成了一个有向图。每个生物是一个节点,每条捕食关系是一条有向边。
-
什么是“最大食物链”? 题目定义为“最左端是不会捕食其他生物的生产者,最右端是不会被其他生物捕食的消费者。”
- 生产者:不会捕食其他生物。在图中,这意味着它没有入边(没有箭头指向它),即入度为 0 的节点。
- 最终消费者:不会被其他生物捕食。在图中,这意味着它没有出边(没有箭头从它发出),即出度为 0 的节点。
- 因此,一条“最大食物链”就是图中的一条从任意一个入度为 0 的节点出发,到任意一个出度为 0 的节点结束的路径。
-
问题核心:题目要求的就是这种路径的总数量。
-
重要提示:“数据中不会出现环”。这是一个关键信息,说明这个有向图是一个有向无环图(DAG)。在 DAG 上求解路径计数问题,通常使用拓扑排序 + 动态规划的方法。
思路详解:拓扑排序 + 动态规划
这个问题的本质是在一个 DAG 中,统计从所有入度为 0 的点到所有出度为 0 的点的路径总数。
我们可以使用动态规划来解决。
状态定义
我们定义 dp[i] 表示从生产者(入度为0的节点)到节点 i 有多少条不同的食物链(路径),out_degree[i] 表示一个节点的出度。
状态转移方程
要计算到达节点 i 的路径数 dp[i],我们可以考虑所有能够直接到达 i 的前驱节点。假设节点 j 有一条边指向 i(即 j -> i),那么任何一条以 j 结尾的食物链,都可以通过 j -> i 这条边延伸,形成一条以 i 结尾的食物链。
因此,到达节点 i 的总路径数,等于所有能到达 i 的前驱节点 j 的路径数之和。
状态转移方程为:
其中,\(j \to i\) 表示存在一条从节点 j 到节点 i 的有向边。
初始化(DP 边界)
食物链必须从生产者(入度为 0 的节点)开始。对于任何一个生产者节点 p,它自身就构成了一条长度为 1 的食物链的起点。因此,我们将所有入度为 0 的节点 p 的 dp[p] 初始化为 1。其他节点的 dp 值初始为 0。
计算顺序
为了确保在计算 dp[i] 时,所有前驱节点 j 的 dp[j] 值都已经被计算出来,我们需要按照图的拓扑序来进行计算。拓扑排序保证了我们访问任何一个节点时,它的所有前驱节点都已经被访问过。
最终答案
根据 dp[i] 的定义,dp 数组计算完成后,dp[i] 存储了从生产者到节点 i 的路径总数。题目要求的是“最大食物链”的数量,即终点必须是“最终消费者”(出度为 0 的节点)。
所以,最终答案就是所有出度为 0 的节点 k 的 dp[k] 值之和。
别忘了,在每一步计算中都要对 80112002 取模。
算法步骤总结
- 建图:读入
n和m,以及m条边。使用邻接表存储图,并同时计算每个节点的入度和出度。 - 拓扑排序:找到一个图中所有节点的拓扑序列。可以通过 DFS 或者基于队列的 Kahn 算法实现。代码中使用的是 DFS 方法。
- 动态规划:
- 创建一个
dp数组,全部初始化为 0。 - 遍历拓扑序列中的每个节点
u。 - 如果节点
u是生产者(入度为 0),则设置dp[u] = 1。 - 对于节点
u的每一个后继节点v(即存在边u -> v),执行状态转移:dp[v] = (dp[v] + dp[u]) % MOD。
- 创建一个
- 统计结果:遍历所有节点
i(从 1 到n),如果节点i是最终消费者(出度为 0),则将dp[i]累加到最终答案ans中。 - 输出:输出
ans。
代码解析
import sys
# 增加递归深度限制,防止大数据下 DFS 爆栈 (虽然本题数据范围较小,但这是个好习惯)
sys.setrecursionlimit(100000)
input = sys.stdin.readline
# 1. 初始化和建图
n, m = map(int, input().split())
graph = [[] for _ in range(n + 1)] # 邻接表存图
in_degree, out_degree = [0] * (n + 1), [0] * (n + 1) # 统计入度和出度
for _ in range(m):
x, y = map(int, input().split()) # x 被 y 吃,即 x -> y
graph[x].append(y)
in_degree[y] += 1
out_degree[x] += 1
# 2. 拓扑排序 (基于 DFS)
visit, topo, MOD = [False] * (n + 1), [], 80112002
def dfs(u: int):
visit[u] = True
for v in graph[u]:
if not visit[v]:
dfs(v)
# 在节点u的所有后继都访问完后,再将 u 入栈
# 这会得到一个逆拓扑序
topo.append(u)
# 从所有生产者(入度为 0 的点)开始 DFS,保证能遍历到所有可达节点
for i in range(1, n + 1):
if not in_degree[i] and not visit[i]: # visit 判断避免重复访问
dfs(i)
# 将逆拓扑序反转,得到正确的拓扑序
topo.reverse()
# 3. 动态规划
dp, ans = [0] * (n + 1), 0
# 按照拓扑序进行 DP
for u in topo:
# 初始化:如果 u 是生产者,路径数为 1
if in_degree[u] == 0:
dp[u] = 1
# 状态转移:将 u 的路径数贡献给它的所有后继节点 v
for v in graph[u]:
dp[v] = (dp[v] + dp[u]) % MOD
# 4. 统计答案
# 累加所有最终消费者(出度为 0 的点)的 dp 值
for i in range(1, n + 1):
if out_degree[i] == 0:
ans = (ans + dp[i]) % MOD
# 5. 输出结果
print(ans)
代码细节说明
- 拓扑排序:代码中的 DFS 实现方式,在一个节点的所有子节点都处理完毕后,才将该节点加入
topo列表。这会产生一个逆向的拓扑序列。因此,在dfs调用结束后需要topo.reverse()来得到正确的拓扑序列(生产者在前,消费者在后)。 - DP 初始化:代码中没有显式地初始化
dp数组,而是在遍历拓扑序列时,遇到生产者 (in_degree[u] == 0) 时才设置dp[u] = 1。这与我们的思路是等价的,因为在拓扑序中,生产者一定出现在所有能被它到达的节点之前。 - DP 转移:
for v in graph[u]: dp[v] = (dp[v] + dp[u]) % MOD这行代码完美地实现了状态转移方程。因为是按照拓扑序遍历,当计算dp[v]时,dp[u]的值已经是最终确定的。

浙公网安备 33010602011771号