1 条题解
-
0
一、 思路提示
-
图的表示:
- 题目给出的关系是 " 吃 "。我们可以把它看作一条有向边 。
- 为了计算 的等级,我们需要知道它吃的 的等级。这是一种递归的关系。
- 数据结构:我们可以用邻接表(
vector<vector<int>>)来存储图。为了方便计算,存$U$ 吃 $V_1, V_2...$比较好。
-
递归与重复计算:
- 如果我们想算“老鹰”的等级,就要算“蛇”的等级;算“蛇”就要算“青蛙”……直到算出“草”(生产者)。
- 如果直接写递归函数
getLevel(u),可能会有大量重复计算。比如“蛇”既被“老鹰”吃,也被“猫头鹰”吃,那“蛇”的等级会被算两次。 - 在 的情况下,普通递归会超时。
-
记忆化搜索(Memoization):
- 这是解决此类问题的金钥匙。
- 开一个数组
memo[N],初始化为 0。 - 当我们需要算
getLevel(u)时:- 先检查
memo[u]是否不为 0。如果是,直接返回(之前算过了)。 - 如果没算过,遍历 吃的所有猎物 ,找出 中最大的等级
max_v_level。 - 计算出结果
ans = 1 + max_v_level。 - 存入
memo[u] = ans。 - 返回
ans。
- 先检查
-
寻找生产者:
- 递归的终止条件是什么?
- 如果一个生物 的邻接表是空的(它不吃任何东西),那么它的等级就是 1。
二、 预备知识点总结
- 图的存储:邻接表
vector<int> adj[N]。 - 深度优先搜索 (DFS):递归函数的编写。
- 记忆化 (Memoization):用数组记录函数返回值,避免重复计算,将指数级复杂度降为线性复杂度 。
三、 启发式教学:草稿纸上的推理过程
教练:“我们把生物画成圆圈,谁吃谁就画个箭头。”
场景:
- 草(1)
- 兔子(2) 吃 草(1)
- 狼(3) 吃 兔子(2)
- 蘑菇(4) [分解者,这里假设它也吃兔子尸体?] -> 不,题目说生产者不捕食。那假设有个特殊的 蘑菇(4) 吃 草(1)。
画图:
3(狼) -> 2(兔) -> 1(草) ^ | 4(狐狸)假设关系:狼吃兔,狐狸吃兔。
推理:
- 我想知道狼是几级?
- 看狼吃谁?吃兔。
- 那兔是几级?不知道,先去算兔。
- 算兔:
- 看兔吃谁?吃草。
- 那草是几级?不知道,算草。
- 算草:
- 草吃谁?谁都不吃。
- Bingo! 草是 1 级。
- 回得去告诉兔:草是 1。
- 回溯到兔:
- 兔 = 1 + 草(1) = 2 级。
- 记下来!兔是 2 级。(下次狐狸来问的时候,直接告诉它是 2,不用再去找草了)。
- 回溯到狼:
- 狼 = 1 + 兔(2) = 3 级。
算法总结: 遍历每一个生物,问它“你是几级?”。它会去问它的午餐。如果问过了(有记录),直接回答;没问过,就一路问到底。
四、 读题关键词总结
- “最大” / “最高”
max操作,通常涉及 DP 或 贪心,这里是 DP/记忆化。 - “” 必须是线性复杂度 ,不能用 。
- “无环” 放心用递归,不会死循环。
- “捕食” 有向边的方向。输入是 吃 ,还是 被 吃,一定要看清。本题是 吃 。
五、 标准题解代码 (C++14)
/** * 题目:复杂的食物网 (The Complex Food Web) * 算法:图论 / 记忆化搜索 (Graph DFS + Memoization) * 难度:GESP 5级 / CSP-J T3 */ #include <iostream> #include <vector> #include <algorithm> using namespace std; const int MAXN = 100005; // 邻接表:adj[u] 存储 u 捕食的所有猎物 v vector<int> adj[MAXN]; // 记忆化数组:memo[u] 存储 u 的营养级 // 初始化为 0,表示尚未计算 int memo[MAXN]; // 深度优先搜索函数,计算并返回 u 的营养级 int get_level(int u) { // 1. 记忆化检查:如果已经算过,直接返回 if (memo[u] != 0) { return memo[u]; } // 2. 递归终止条件(隐式): // 如果 u 不捕食任何生物 (adj[u] 为空),循环不会执行, // max_prey_level 保持为 0,最后返回 1。符合生产者定义。 int max_prey_level = 0; // 3. 遍历 u 的所有猎物 v for (int v : adj[u]) { // 递归计算猎物的等级,并取最大值 max_prey_level = max(max_prey_level, get_level(v)); } // 4. 计算当前等级并存储 memo[u] = 1 + max_prey_level; return memo[u]; } int main() { // I/O 加速 ios_base::sync_with_stdio(false); cin.tie(NULL); int N, M; if (!(cin >> N >> M)) return 0; // 读入捕食关系 for (int i = 0; i < M; ++i) { int u, v; cin >> u >> v; // u 捕食 v,建立有向边 u -> v adj[u].push_back(v); } int max_trophic_level = 0; // 遍历每一个物种,计算其营养级 // 因为我们不知道谁是最高级消费者,所以需要全部扫描一遍 // 由于有记忆化,总时间复杂度依然是线性的 for (int i = 1; i <= N; ++i) { max_trophic_level = max(max_trophic_level, get_level(i)); } cout << max_trophic_level << endl; return 0; }时间与空间复杂度分析
- 时间复杂度:
- 每个节点(物种)的
get_level函数只会被完整执行一次。后续访问都会直接返回memo值。 - 每条边(捕食关系)只会被遍历一次。
- 总时间复杂度:。对于 的数据量,运行极快。
- 每个节点(物种)的
- 空间复杂度:
- 邻接表存储 条边。
- 递归栈深度最大为 (一条长链)。
- 记忆化数组 。
- 总空间:。
六、 数据生成器 (Generator.cpp)
为了保证生成的图是有向无环图 (DAG),生成器的策略是: 只生成 的边,其中保证 (或者通过随机映射打乱编号,但底层逻辑依然是大的指向小的,或者小的指向大的),这样绝对不会有环。
/** * 题目:复杂的食物网 (The Complex Food Web) * 修复版数据生成器 * * 修复说明: * 1. 将求解算法从 递归DFS 改为 拓扑排序(Kahn算法)。 * - 避免了深度递归导致的 Stack Overflow (Case 7等长链数据曾因此崩溃)。 * - 避免了潜在的死循环风险。 * 2. 优化了图的生成策略,确保严格生成 DAG (有向无环图)。 */ #include <iostream> #include <fstream> #include <string> #include <vector> #include <algorithm> #include <random> #include <ctime> #include <queue> using namespace std; mt19937 rng(time(0)); // --- 核心解法:拓扑排序 + 动态规划 --- // 只要是 DAG 最长路问题,用这个方法最稳健 int solve_iterative(int N, const vector<pair<int, int>>& edges) { // 1. 建图:我们需要“能量流动”的方向 // 题目输入:U 吃 V (U -> V 依赖关系) // 能量流动:V -> U (V 提供能量给 U) // 为了从生产者(Level 1)开始递推,我们构建 V -> U 的图 vector<vector<int>> energy_flow(N + 1); vector<int> indegree(N + 1, 0); // 入度:表示 U 还需要等待多少个猎物被计算 for (auto p : edges) { int predator = p.first; int prey = p.second; // 能量从 prey 流向 predator energy_flow[prey].push_back(predator); indegree[predator]++; // 捕食者依赖猎物,所以捕食者入度+1 } // 2. 初始化队列 // 入度为 0 的点就是生产者(不捕食任何生物) queue<int> q; vector<int> level(N + 1, 1); // 默认为 1 for (int i = 1; i <= N; ++i) { if (indegree[i] == 0) { q.push(i); level[i] = 1; // 生产者等级为 1 } } // 3. 拓扑排序 int max_level = 1; int processed_count = 0; while (!q.empty()) { int u = q.front(); q.pop(); processed_count++; // 更新全局最大值 if (level[u] > max_level) { max_level = level[u]; } // 能量流向捕食者 v for (int v : energy_flow[u]) { // v 的等级 = max(当前v等级, u等级 + 1) if (level[u] + 1 > level[v]) { level[v] = level[u] + 1; } // v 的一个猎物处理完了 indegree[v]--; if (indegree[v] == 0) { q.push(v); } } } // 如果图中有环,processed_count 会小于 N,但在本生成器中我们保证无环 return max_level; } // --- 数据生成逻辑 --- void make_case(int id) { int N, M; vector<pair<int, int>> edges; // 随机映射表:用于打乱节点编号,防止“编号大的一定吃编号小的”这种明显规律 vector<int> p(200005); auto init_perm = [&](int n) { for(int i=1; i<=n; ++i) p[i] = i; shuffle(p.begin() + 1, p.begin() + n + 1, rng); }; switch(id) { case 1: // 样例 N = 5; M = 4; edges = {{2,1}, {3,2}, {4,2}, {5,3}}; break; case 2: // 简单链 N = 100; M = 99; init_perm(N); // 构造 p[i] 吃 p[i-1],保证无环 for(int i=2; i<=N; ++i) edges.push_back({p[i], p[i-1]}); break; case 3: // 倒锥形 (1个吃99个) N = 100; M = 99; init_perm(N); for(int i=1; i<N; ++i) edges.push_back({p[N], p[i]}); break; case 4: // 很多生产者 (随机稀疏) N = 100; M = 50; init_perm(N); for(int i=0; i<M; ++i) { int u = rng() % (N-1) + 2; // 2..N int v = rng() % (u-1) + 1; // 1..u-1 // 永远保证 u > v (在原始序中),映射后则无规律 // 这样绝对无环 edges.push_back({p[u], p[v]}); } break; case 5: // 随机中等 DAG N = 1000; M = 2000; init_perm(N); for(int i=0; i<M; ++i) { int u = rng() % (N-1) + 2; int v = rng() % (u-1) + 1; edges.push_back({p[u], p[v]}); } break; case 6: // 分层图 (之前超时的地方) N = 1000; // 构造 20 层,每层 50 个点 // 每层的点只捕食上一层的点 { int layers = 20; int per_layer = 50; init_perm(N); for(int l=1; l<layers; ++l) { // 层级 1..19 for(int k=0; k<150; ++k) { // 每层之间生成150条边 // u 在当前层 l int u_idx = l * per_layer + rng() % per_layer + 1; // v 在上一层 l-1 int v_idx = (l-1) * per_layer + rng() % per_layer + 1; // u 捕食 v (u 的层级比 v 高) -> 无环 // 使用 p 映射打乱编号 edges.push_back({p[u_idx], p[v_idx]}); } } M = edges.size(); } break; case 7: // 大数据长链 (压力测试递归深度) N = 100000; M = 99999; init_perm(N); // 形成一条超长链,拓扑排序能秒解,递归会爆栈 for(int i=2; i<=N; ++i) edges.push_back({p[i], p[i-1]}); break; case 8: // 大数据随机 N = 50000; M = 100000; init_perm(N); for(int i=0; i<M; ++i) { int u = rng() % (N-1) + 2; int v = rng() % (u-1) + 1; edges.push_back({p[u], p[v]}); } break; case 9: // 稀疏图 N = 100000; M = 50000; init_perm(N); for(int i=0; i<M; ++i) { int u = rng() % (N-1) + 2; int v = rng() % (u-1) + 1; edges.push_back({p[u], p[v]}); } break; case 10: // 稠密图 (相对) N = 2000; M = 100000; init_perm(N); for(int i=0; i<M; ++i) { int u = rng() % (N-1) + 2; int v = rng() % (u-1) + 1; edges.push_back({p[u], p[v]}); } break; } string in_file = to_string(id) + ".in"; ofstream fout_in(in_file); fout_in << N << " " << M << "\n"; for(auto p : edges) fout_in << p.first << " " << p.second << "\n"; fout_in.close(); string out_file = to_string(id) + ".out"; ofstream fout_out(out_file); fout_out << solve_iterative(N, edges) << "\n"; fout_out.close(); cout << "Generated Case " << id << " (N=" << N << ", M=" << M << ") - Done." << endl; } int main() { // 提速 ios_base::sync_with_stdio(false); for(int i=1; i<=10; ++i) make_case(i); return 0; } /* 为什么这个版本更安全? Iterative (迭代):使用了 while 循环和 queue,不再使用函数自身的递归调用。这意味着栈空间的消耗是常数级的,彻底消除了 Case 7 (长链) 可能导致的 Stack Overflow 问题。 Topological Sort (拓扑排序):这本身就是解决 DAG 上最长路径(依赖链)的标准算法,比 DFS 记忆化搜索更直观,且更容易处理超大规模数据。 构造策略:在生成数据时,核心逻辑是 u 在 v 之后(u > v),然后通过 p[] 数组映射打乱 ID。这在数学上保证了绝对没有环,解决了任何潜在的死循环问题。 这个版本应该能顺利瞬间生成所有 10 个测试点 */这道题以生态系统食物网为模型,非常自然地引入了图的遍历和记忆化搜索算法。通过“营养级”的计算,学生不仅能巩固 DFS 的写法,还能深刻理解为什么需要“记忆化”来降低复杂度。希望这道题能丰富你的 GESP 5级题库!
-
- 1
信息
- ID
- 19315
- 时间
- 1000ms
- 内存
- 32MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者