题目链接
题目描述
给定一张包含 个生物和
条捕食关系的食物网 (DAG)。一条食物链被定义为从一个“生产者”到一个“顶级消费者”的、由一条或多条边构成的路径。
- 生产者: 出度为 0 的节点。
- 顶级消费者: 入度为 0 的节点。
你需要计算图中这样的食物链共有多少条。
输入:
- 第一行输入两个整数
。
- 接下来
行,输入
,表示存在有向边
。
输出:
- 输出满足定义的食物链数量。
解题思路
本题虽然文字描述与示例存在矛盾,但根据更广泛的测试数据可推断,其真实意图是统计从所有“生产者”(出度为0)到所有“顶级消费者”(入度为0)的路径总数,并且路径长度必须大于0(即至少包含一条边)。
最适合解决此类DAG路径计数问题的方法是拓扑排序。
-
反向建图与拓扑排序:
- 为了从“生产者”出发正向递推,我们建立一个反向图。
- 使用拓扑排序来处理节点,确保在计算一个节点的路径数之前,其所有前驱节点的路径数都已计算完毕。
- 拓扑排序的初始队列由所有生产者(在反向图中入度为0)构成。
-
动态规划:
- DP状态:
表示从所有生产者到节点
的路径总数。
- 初始化: 对所有生产者
,
。这代表一条从生产者到其自身的长度为0的路径。
- 状态转移: 在拓扑排序过程中,当我们处理完节点
,就更新其在反向图中的后继节点
:
。
- DP状态:
-
修正与最终结果:
- 拓扑排序结束后,
包含了所有从生产者到
的路径,包括长度为0的路径。
- 我们首先累加所有顶级消费者(原始入度为0)的
值,得到一个初步总数。
- 根据题意“依次连接”,我们需要排除长度为0的路径。一个节点
的路径长度为0,当且仅当它自己就是生产者。因此,如果一个顶级消费者
同时也是一个生产者,它的
值里就包含了这条不应被计数的路径。
- 所以,最后的修正步骤是:遍历所有顶级消费者,如果它同时也是生产者,则从总数中减去1。
- 拓扑排序结束后,
代码
#include <iostream>
#include <vector>
#include <numeric>
#include <queue>
using namespace std;
int main() {
int N, M;
cin >> N >> M;
vector<vector<int>> rev_adj(N + 1);
vector<int> in_degree(N + 1, 0);
vector<int> out_degree(N + 1, 0);
for (int i = 0; i < M; ++i) {
int u, v;
cin >> u >> v;
rev_adj[v].push_back(u); // 构建反向图 u -> v
out_degree[u]++;
in_degree[v]++;
}
vector<long long> dp(N + 1, 0);
queue<int> q;
vector<int> rev_in_degree = out_degree; // 反向图的入度等于原图出度
for (int i = 1; i <= N; ++i) {
if (rev_in_degree[i] == 0) { // 拓扑排序的起点是生产者
q.push(i);
dp[i] = 1;
}
}
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : rev_adj[u]) {
dp[v] += dp[u];
if (--rev_in_degree[v] == 0) {
q.push(v);
}
}
}
long long total_chains = 0;
for (int i = 1; i <= N; ++i) {
if (in_degree[i] == 0) { // 终点是顶级消费者
total_chains += dp[i];
// 如果该顶级消费者同时也是生产者,说明有一条长度为0的路径被计算
// 题目要求“依次连接”,意味着路径长度至少为1,需减去此情况
if (out_degree[i] == 0) {
total_chains--;
}
}
}
cout << total_chains << "\n";
return 0;
}
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int N = sc.nextInt();
int M = sc.nextInt();
List<List<Integer>> revAdj = new ArrayList<>(N + 1);
for (int i = 0; i <= N; i++) {
revAdj.add(new ArrayList<>());
}
int[] inDegree = new int[N + 1];
int[] outDegree = new int[N + 1];
for (int i = 0; i < M; i++) {
int u = sc.nextInt();
int v = sc.nextInt();
revAdj.get(v).add(u); // u -> v
outDegree[u]++;
inDegree[v]++;
}
long[] dp = new long[N + 1];
Queue<Integer> q = new LinkedList<>();
int[] revInDegree = outDegree.clone(); // 反向图的入度等于原图的出度
for (int i = 1; i <= N; i++) {
if (revInDegree[i] == 0) { // 拓扑排序的起点是生产者
q.add(i);
dp[i] = 1;
}
}
while (!q.isEmpty()) {
int u = q.poll();
for (int v : revAdj.get(u)) {
dp[v] += dp[u];
if (--revInDegree[v] == 0) {
q.add(v);
}
}
}
long totalChains = 0;
for (int i = 1; i <= N; i++) {
if (inDegree[i] == 0) { // 终点是顶级消费者
totalChains += dp[i];
// 排除长度为0的路径(节点既是生产者也是顶级消费者)
if (outDegree[i] == 0) {
totalChains--;
}
}
}
System.out.println(totalChains);
}
}
from collections import deque
N, M = map(int, input().split())
rev_adj = [[] for _ in range(N + 1)]
in_degree = [0] * (N + 1)
out_degree = [0] * (N + 1)
for _ in range(M):
u, v = map(int, input().split())
rev_adj[v].append(u) # u -> v
out_degree[u] += 1
in_degree[v] += 1
dp = [0] * (N + 1)
q = deque()
rev_in_degree = list(out_degree)
for i in range(1, N + 1):
if rev_in_degree[i] == 0: # 拓扑排序的起点是生产者
q.append(i)
dp[i] = 1
while q:
u = q.popleft()
for v in rev_adj[u]:
dp[v] += dp[u]
rev_in_degree[v] -= 1
if rev_in_degree[v] == 0:
q.append(v)
total_chains = 0
for i in range(1, N + 1):
if in_degree[i] == 0: # 累加所有终点为顶级消费者的路径
total_chains += dp[i]
# 排除孤立节点(既是生产者也是顶级消费者)导致的长度为0的路径
if out_degree[i] == 0:
total_chains -= 1
print(total_chains)
算法及复杂度
- 算法:拓扑排序 + 动态规划
- 时间复杂度:
- 构建图和拓扑排序的过程都与图的节点数和边数呈线性关系。
- 空间复杂度:
- 主要开销来自存储图的邻接表、度数组、DP数组以及拓扑排序的队列。