5 条题解
-
0
数据生成: 仍然卡住
Case 5 卡住的原因主要在于 I/O 操作和数据查找的效率问题,而非算法死循环。 在 的情况下:
std::set的瓶颈:原代码使用std::set来存储和查找宝物位置。构建set需要 ,并且在输出循环中进行了 次查找,总耗时较高。对于 5 组数据,这会产生显著延迟。vector<vector<int>>的频繁构造:在gen_treasures中,即使是随机撒点(不需要图结构),原代码也会构建一个包含 个 vector 的邻接表,这非常消耗内存分配时间。- 用户感知:没有任何进度提示,处理 5 组 规模的数据可能需要 3-5 秒,让用户感觉“卡死”。
下面的代码进行了 极速优化:
- 移除
std::set:改用vector<int>标记宝物,将 的查找降为 。 - 按需构建图:在生成宝物时,只有需要路径算法的策略(如
all_yes)才构建邻接表,Case 5(随机撒点)直接跳过此步骤。 - 增加进度条:在控制台输出当前进度(如
Test 1/5),让你知道程序在运行。
🚀 极速版数据生成器
/** * P11249 [GESP202409 七级] 小杨寻宝 - 极速数据生成器 * * 优化点: * 1. 移除输出时的 std::set 查找,改为 O(N) 的数组标记,极大加速 IO 准备。 * 2. gen_treasures 中按需构建邻接表,避免 Case 5 进行无意义的 10万次 vector 分配。 * 3. 增加实时进度显示。 */ #include <iostream> #include <vector> #include <string> #include <algorithm> #include <fstream> #include <random> #include <ctime> #include <queue> #include <numeric> // for std::iota using namespace std; // ========================================== // Part 1: 标准题解 Solver (迭代 BFS 版,防爆栈) // ========================================== namespace Solver { const int MAXN = 100005; vector<int> adj[MAXN]; bool has_treasure[MAXN]; vector<int> treasure_nodes; int distU[MAXN], distV[MAXN]; int n; void init(int _n) { n = _n; for (int i = 1; i <= n; ++i) { adj[i].clear(); has_treasure[i] = false; } treasure_nodes.clear(); } void add_edge(int u, int v) { adj[u].push_back(v); adj[v].push_back(u); } void set_treasure(int u) { if (!has_treasure[u]) { has_treasure[u] = true; treasure_nodes.push_back(u); } } int bfs(int start, int* dist) { for (int i = 1; i <= n; ++i) dist[i] = -1; queue<int> q; q.push(start); dist[start] = 0; int max_dist = -1; int farthest_node = start; while (!q.empty()) { int u = q.front(); q.pop(); if (has_treasure[u]) { if (dist[u] > max_dist) { max_dist = dist[u]; farthest_node = u; } } for (int v : adj[u]) { if (dist[v] == -1) { dist[v] = dist[u] + 1; q.push(v); } } } return farthest_node; } string solve() { if (treasure_nodes.size() <= 1) return "Yes"; int U = bfs(treasure_nodes[0], distU); int V = bfs(U, distU); bfs(V, distV); int path_len = distU[V]; for (int t : treasure_nodes) { if (distU[t] + distV[t] != path_len) return "No"; } return "Yes"; } } // ========================================== // Part 2: 数据生成器 Generator // ========================================== mt19937 rng(time(0)); int random_int(int l, int r) { if (l > r) return l; return uniform_int_distribution<int>(l, r)(rng); } // 优化:使用 BFS 找路径 vector<int> get_path_bfs(int n, int start, int end, const vector<vector<int>>& adj) { vector<int> parent(n + 1, 0); vector<bool> visited(n + 1, false); queue<int> q; q.push(start); visited[start] = true; bool found = false; while(!q.empty()) { int u = q.front(); q.pop(); if(u == end) { found = true; break; } for(int v : adj[u]) { if(!visited[v]) { visited[v] = true; parent[v] = u; q.push(v); } } } vector<int> path; if(found) { int curr = end; while(curr != 0) { path.push_back(curr); if(curr == start) break; curr = parent[curr]; } } return path; } vector<pair<int, int>> gen_tree(int n, string type) { vector<pair<int, int>> edges; edges.reserve(n - 1); // 预分配内存 if (type == "line") { for (int i = 2; i <= n; ++i) edges.push_back({i - 1, i}); } else if (type == "star") { for (int i = 2; i <= n; ++i) edges.push_back({1, i}); } else { for (int i = 2; i <= n; ++i) { int parent = random_int(1, i - 1); if (type == "deep" && i > 5) parent = random_int(max(1, i - 5), i - 1); edges.push_back({parent, i}); } } return edges; } vector<int> gen_treasures(int n, const vector<pair<int, int>>& edges, string type) { vector<int> t_nodes; // 优化:只有需要图结构的生成策略才构建邻接表 // Case 5 (random) 不需要这步,能省下大量时间 vector<vector<int>> local_adj; if (type == "all_yes" || type == "force_no") { local_adj.resize(n + 1); for (auto& e : edges) { local_adj[e.first].push_back(e.second); local_adj[e.second].push_back(e.first); } } if (type == "all_yes") { int u = random_int(1, n); int v = random_int(1, n); vector<int> path = get_path_bfs(n, u, v, local_adj); if (!path.empty()) { int count = random_int(1, path.size()); shuffle(path.begin(), path.end(), rng); for(int k=0; k<count; ++k) t_nodes.push_back(path[k]); } else { t_nodes.push_back(1); } } else if (type == "force_no") { int center = -1; vector<int> candidates; for(int i=1; i<=n; ++i) if(local_adj[i].size() >= 3) candidates.push_back(i); if (!candidates.empty()) { center = candidates[random_int(0, candidates.size()-1)]; t_nodes.push_back(center); int branch_count = 0; for(int neighbor : local_adj[center]) { if(branch_count >= 3) break; t_nodes.push_back(neighbor); branch_count++; } } else { vector<int> pool(n); iota(pool.begin(), pool.end(), 1); shuffle(pool.begin(), pool.end(), rng); int count = min(n, 3); for(int i=0; i<count; ++i) t_nodes.push_back(pool[i]); } } else { // random int count = random_int(1, min(n, 20)); if (random_int(1, 10) > 8) count = random_int(1, n); // 20% 概率生成海量宝物 vector<int> pool(n); iota(pool.begin(), pool.end(), 1); shuffle(pool.begin(), pool.end(), rng); for(int i=0; i<count; ++i) t_nodes.push_back(pool[i]); } if (t_nodes.empty()) t_nodes.push_back(1); return t_nodes; } void generate_file(int file_id) { string in_name = to_string(file_id) + ".in"; string out_name = to_string(file_id) + ".out"; ofstream fin(in_name); ofstream fout(out_name); int T = 10; if (file_id > 2) T = 5; fin << T << endl; cout << "Generating Case " << file_id << " (" << T << " tests)... "; for (int t = 0; t < T; ++t) { // 简单的进度反馈 cout << "." << flush; int n; string tree_type = "random"; string treasure_type = "random"; if (file_id == 1) { n = random_int(2, 5); } else if (file_id == 2) { n = 5; tree_type = (t < 5) ? "line" : "star"; } else if (file_id == 3) { n = random_int(500, 1000); } else if (file_id == 4) { n = random_int(800, 1000); treasure_type = "all_yes"; } else if (file_id == 5) { n = random_int(50000, 100000); tree_type = "line"; treasure_type = "random"; } else if (file_id == 6) { n = random_int(50000, 100000); tree_type = "star"; treasure_type = (t % 2 == 0) ? "force_no" : "random"; } else if (file_id == 7) { n = random_int(80000, 100000); tree_type = "deep"; treasure_type = "all_yes"; } else if (file_id == 8) { n = random_int(80000, 100000); treasure_type = "force_no"; } else if (file_id == 9) { n = 100000; if (t < 3) treasure_type = "all_yes"; else if (t < 6) treasure_type = "force_no"; } else { // Case 10 n = 100000; if (t == 0) { // 极简 Case: 1个宝物 fin << n << "\n"; for(int i=1; i<=n; ++i) fin << (i==1) << (i==n?"":" "); fin << "\n"; vector<pair<int,int>> edges = gen_tree(n, "random"); for(auto& e : edges) fin << e.first << " " << e.second << "\n"; Solver::init(n); for(auto& e : edges) Solver::add_edge(e.first, e.second); Solver::set_treasure(1); fout << Solver::solve() << "\n"; continue; } } vector<pair<int, int>> edges = gen_tree(n, tree_type); vector<int> treasures = gen_treasures(n, edges, treasure_type); fin << n << "\n"; // 【关键优化】使用 vector 代替 set 进行 O(1) 查找 // 解决 N=10^5 时输出极慢的问题 static vector<int> is_treasure_map; if(is_treasure_map.size() <= n) is_treasure_map.resize(n + 1); // 清空标记(只清空用到的部分) // 注意:这里为了效率,不使用全局清空,而是先全部置0太慢 // 更好方法:先全置0? 不,用 fill 也可以,因为 n 很大 // 为了安全,每次 resize 后 fill 0 fill(is_treasure_map.begin(), is_treasure_map.begin() + n + 1, 0); for(int x : treasures) is_treasure_map[x] = 1; for (int i = 1; i <= n; ++i) { fin << is_treasure_map[i] << (i == n ? "" : " "); } fin << "\n"; for (auto& e : edges) { fin << e.first << " " << e.second << "\n"; } Solver::init(n); for (auto& e : edges) Solver::add_edge(e.first, e.second); for (int x : treasures) Solver::set_treasure(x); fout << Solver::solve() << "\n"; } fin.close(); fout.close(); cout << " Done." << endl; } int main() { ios::sync_with_stdio(false); cin.tie(0); for (int i = 1; i <= 10; ++i) { generate_file(i); } return 0; } -
0
数据生成:case5卡住太久了
生成 Case 5 卡住的主要原因在于随机数生成的策略。
在之前的代码中,当需要生成大量宝物(例如
count接近 )时,使用while (set.size() < count) set.insert(rand())的方式效率极低。这就好比你在集齐 100 张不同的卡片,当你手里已经有 99 张时,想抽到最后一张没重复的卡片非常难(赠券收集者问题),导致程序陷入死循环般的重试中。此外,针对 Case 7 这种深树/长链情况,之前的递归 DFS 找路径也可能导致栈溢出 (Stack Overflow)。
下面的版本做了两个核心优化:
- 优化撒点算法:使用
std::shuffle洗牌算法, 时间内完成随机取点,无论取多少个都瞬间完成。 - 优化路径查找:将递归 DFS 改为 BFS + 路径回溯,防止深树导致程序崩溃。
请使用这份优化后的代码:
/** * P11249 [GESP202409 七级] 小杨寻宝 - 高性能数据生成器 * * 优化说明: * 1. 修复 Case 5 卡顿:使用 shuffle 洗牌算法替代 set 碰撞采样,解决 N 接近 10^5 时的死循环问题。 * 2. 修复 Case 7 崩溃:将递归找路径改为 BFS 迭代,防止长链导致栈溢出。 */ #include <iostream> #include <vector> #include <string> #include <algorithm> #include <fstream> #include <random> #include <ctime> #include <queue> #include <set> #include <numeric> // for std::iota using namespace std; // ========================================== // Part 1: 标准题解 Solver (用于生成 .out) // ========================================== namespace Solver { const int MAXN = 100005; vector<int> adj[MAXN]; bool has_treasure[MAXN]; vector<int> treasure_nodes; int distU[MAXN], distV[MAXN]; int n; void init(int _n) { n = _n; for (int i = 1; i <= n; ++i) { adj[i].clear(); has_treasure[i] = false; } treasure_nodes.clear(); } void add_edge(int u, int v) { adj[u].push_back(v); adj[v].push_back(u); } void set_treasure(int u) { if (!has_treasure[u]) { has_treasure[u] = true; treasure_nodes.push_back(u); } } int bfs(int start, int* dist) { for (int i = 1; i <= n; ++i) dist[i] = -1; queue<int> q; q.push(start); dist[start] = 0; int max_dist = -1; int farthest_node = start; while (!q.empty()) { int u = q.front(); q.pop(); if (has_treasure[u]) { if (dist[u] > max_dist) { max_dist = dist[u]; farthest_node = u; } } for (int v : adj[u]) { if (dist[v] == -1) { dist[v] = dist[u] + 1; q.push(v); } } } return farthest_node; } string solve() { if (treasure_nodes.size() <= 1) return "Yes"; int U = bfs(treasure_nodes[0], distU); int V = bfs(U, distU); bfs(V, distV); int path_len = distU[V]; for (int t : treasure_nodes) { if (distU[t] + distV[t] != path_len) return "No"; } return "Yes"; } } // ========================================== // Part 2: 数据生成器 Generator // ========================================== mt19937 rng(time(0)); int random_int(int l, int r) { if (l > r) return l; return uniform_int_distribution<int>(l, r)(rng); } struct TestCase { int n; vector<pair<int, int>> edges; vector<int> treasures; }; // 生成树结构 vector<pair<int, int>> gen_tree(int n, string type) { vector<pair<int, int>> edges; if (type == "line") { for (int i = 2; i <= n; ++i) edges.push_back({i - 1, i}); } else if (type == "star") { for (int i = 2; i <= n; ++i) edges.push_back({1, i}); } else { // random or deep for (int i = 2; i <= n; ++i) { int parent = random_int(1, i - 1); if (type == "deep" && i > 5) parent = random_int(max(1, i - 5), i - 1); edges.push_back({parent, i}); } } return edges; } // 优化:使用 BFS 找路径,避免递归爆栈 vector<int> get_path_bfs(int n, int start, int end, const vector<vector<int>>& adj) { vector<int> parent(n + 1, 0); vector<bool> visited(n + 1, false); queue<int> q; q.push(start); visited[start] = true; bool found = false; while(!q.empty()) { int u = q.front(); q.pop(); if(u == end) { found = true; break; } for(int v : adj[u]) { if(!visited[v]) { visited[v] = true; parent[v] = u; q.push(v); } } } vector<int> path; if(found) { int curr = end; while(curr != 0) { path.push_back(curr); if(curr == start) break; curr = parent[curr]; } } return path; } // 生成宝物分布 vector<int> gen_treasures(int n, const vector<pair<int, int>>& edges, string type) { vector<int> t_nodes; // 构建临时邻接表 vector<vector<int>> local_adj(n + 1); for (auto& e : edges) { local_adj[e.first].push_back(e.second); local_adj[e.second].push_back(e.first); } if (type == "all_yes") { int u = random_int(1, n); int v = random_int(1, n); // 使用 BFS 获取路径上的点 vector<int> path = get_path_bfs(n, u, v, local_adj); if (!path.empty()) { int count = random_int(1, path.size()); // 简单的 shuffle 取点 shuffle(path.begin(), path.end(), rng); for(int k=0; k<count; ++k) t_nodes.push_back(path[k]); } else { t_nodes.push_back(1); } } else if (type == "force_no") { int center = -1; vector<int> candidates; for(int i=1; i<=n; ++i) if(local_adj[i].size() >= 3) candidates.push_back(i); if (!candidates.empty()) { center = candidates[random_int(0, candidates.size()-1)]; t_nodes.push_back(center); int branch_count = 0; for(int neighbor : local_adj[center]) { if(branch_count >= 3) break; t_nodes.push_back(neighbor); branch_count++; } } else { // 没有度数>=3的点,退化为随机取 3 个点 vector<int> pool(n); iota(pool.begin(), pool.end(), 1); shuffle(pool.begin(), pool.end(), rng); int count = min(n, 3); for(int i=0; i<count; ++i) t_nodes.push_back(pool[i]); } } else { // random // 优化:使用 shuffle 避免 while 循环死锁 int count = random_int(1, min(n, 20)); // 20% 概率生成大量宝物 if (random_int(1, 10) > 8) count = random_int(1, n); vector<int> pool(n); iota(pool.begin(), pool.end(), 1); // 填充 1, 2, ..., n shuffle(pool.begin(), pool.end(), rng); // 洗牌 for(int i=0; i<count; ++i) t_nodes.push_back(pool[i]); } // 保证至少一个宝物 if (t_nodes.empty()) t_nodes.push_back(1); return t_nodes; } void generate_file(int file_id) { string in_name = to_string(file_id) + ".in"; string out_name = to_string(file_id) + ".out"; ofstream fin(in_name); ofstream fout(out_name); int T = 10; if (file_id > 2) T = 5; fin << T << endl; cout << "Generating Case " << file_id << " (" << T << " tests)... "; for (int t = 0; t < T; ++t) { int n; string tree_type = "random"; string treasure_type = "random"; if (file_id == 1) { n = random_int(2, 5); } else if (file_id == 2) { n = 5; tree_type = (t < 5) ? "line" : "star"; } else if (file_id == 3) { n = random_int(500, 1000); } else if (file_id == 4) { n = random_int(800, 1000); treasure_type = "all_yes"; } else if (file_id == 5) { n = random_int(50000, 100000); tree_type = "line"; treasure_type = "random"; // 这里曾是卡顿点 } else if (file_id == 6) { n = random_int(50000, 100000); tree_type = "star"; treasure_type = (t % 2 == 0) ? "force_no" : "random"; } else if (file_id == 7) { n = random_int(80000, 100000); tree_type = "deep"; treasure_type = "all_yes"; } else if (file_id == 8) { n = random_int(80000, 100000); treasure_type = "force_no"; } else if (file_id == 9) { n = 100000; if (t < 3) treasure_type = "all_yes"; else if (t < 6) treasure_type = "force_no"; } else { // Case 10 n = 100000; if (t == 0) { // 极简 Case: 1个宝物 fin << n << "\n"; for(int i=1; i<=n; ++i) fin << (i==1) << (i==n?"":" "); fin << "\n"; vector<pair<int,int>> edges = gen_tree(n, "random"); for(auto& e : edges) fin << e.first << " " << e.second << "\n"; Solver::init(n); for(auto& e : edges) Solver::add_edge(e.first, e.second); Solver::set_treasure(1); fout << Solver::solve() << "\n"; continue; } } vector<pair<int, int>> edges = gen_tree(n, tree_type); vector<int> treasures = gen_treasures(n, edges, treasure_type); fin << n << "\n"; // 优化输出:不需要开大数组,直接判断集合 set<int> t_set(treasures.begin(), treasures.end()); for (int i = 1; i <= n; ++i) { fin << (t_set.count(i) ? 1 : 0) << (i == n ? "" : " "); } fin << "\n"; for (auto& e : edges) { fin << e.first << " " << e.second << "\n"; } Solver::init(n); for (auto& e : edges) Solver::add_edge(e.first, e.second); for (int x : treasures) Solver::set_treasure(x); fout << Solver::solve() << "\n"; } fin.close(); fout.close(); cout << "Done." << endl; } int main() { ios::sync_with_stdio(false); cin.tie(0); for (int i = 1; i <= 10; ++i) { generate_file(i); } return 0; } - 优化撒点算法:使用
-
0
这是一个功能完备的C++数据生成器。它整合了标准题解逻辑(用于生成正确答案)和多样化的数据生成策略(覆盖各类测试场景),会自动生成
1.in/1.out到10.in/10.out。🛠️ 功能特点
- 覆盖题意:生成的数据严格遵循题目规则(树结构、宝物分布)。
- 多级难度:
- Subtask 1 ():穷举小图、边界情况。
- Subtask 2 ():中等规模随机树、链状、星状图。
- Subtask 3 ():极限数据,包含精心构造的“必Yes”路径数据和“必No”分叉数据。
- 构造策略:
- Random Tree:标准的随机父节点生成法。
- Line (链):测试必然有解的情况。
- Star (菊花图):测试分支判定逻辑。
- Force Yes:先随机选路径,再只在路径上放宝物。
- Force No:强制在三个不同的子树分支上放宝物。
📄 Generator 代码
/** * P11249 [GESP202409 七级] 小杨寻宝 - 数据生成器 * * 包含: * 1. Solver: 标准题解逻辑,用于生成 .out * 2. Generator: 针对不同测试点生成 .in * * 使用方法:编译并运行,当前目录下生成 1.in/out ~ 10.in/out */ #include <iostream> #include <vector> #include <string> #include <algorithm> #include <fstream> #include <random> #include <ctime> #include <queue> #include <set> using namespace std; // ========================================== // Part 1: 标准题解 Solver (用于生成 .out) // ========================================== namespace Solver { const int MAXN = 100005; vector<int> adj[MAXN]; bool has_treasure[MAXN]; vector<int> treasure_nodes; int distU[MAXN], distV[MAXN]; int n; // 初始化函数 void init(int _n) { n = _n; for (int i = 1; i <= n; ++i) { adj[i].clear(); has_treasure[i] = false; } treasure_nodes.clear(); } // 加边 void add_edge(int u, int v) { adj[u].push_back(v); adj[v].push_back(u); } // 设置宝物 void set_treasure(int u) { if (!has_treasure[u]) { has_treasure[u] = true; treasure_nodes.push_back(u); } } // BFS int bfs(int start, int* dist) { for (int i = 1; i <= n; ++i) dist[i] = -1; queue<int> q; q.push(start); dist[start] = 0; int max_dist = -1; int farthest_node = start; while (!q.empty()) { int u = q.front(); q.pop(); if (has_treasure[u]) { if (dist[u] > max_dist) { max_dist = dist[u]; farthest_node = u; } } for (int v : adj[u]) { if (dist[v] == -1) { dist[v] = dist[u] + 1; q.push(v); } } } return farthest_node; } // 主求解函数 string solve() { if (treasure_nodes.size() <= 1) return "Yes"; // 找端点 U int U = bfs(treasure_nodes[0], distU); // 找端点 V int V = bfs(U, distU); // 验证 bfs(V, distV); int path_len = distU[V]; for (int t : treasure_nodes) { if (distU[t] + distV[t] != path_len) return "No"; } return "Yes"; } } // ========================================== // Part 2: 数据生成器 Generator // ========================================== mt19937 rng(time(0)); int random_int(int l, int r) { if (l > r) return l; return uniform_int_distribution<int>(l, r)(rng); } struct TestCase { int n; vector<pair<int, int>> edges; vector<int> treasures; }; // 生成树结构 vector<pair<int, int>> gen_tree(int n, string type) { vector<pair<int, int>> edges; if (type == "line") { for (int i = 2; i <= n; ++i) edges.push_back({i - 1, i}); } else if (type == "star") { for (int i = 2; i <= n; ++i) edges.push_back({1, i}); } else { // random for (int i = 2; i <= n; ++i) { // 稍微偏向生成深树或宽树 int parent = random_int(1, i - 1); if (type == "deep" && i > 5) parent = random_int(i - 5, i - 1); edges.push_back({parent, i}); } } return edges; } // 辅助:获取两点间路径上的所有点 void get_path_nodes(int u, int target, int p, vector<vector<int>>& adj, vector<int>& path, bool& found) { path.push_back(u); if (u == target) { found = true; return; } for (int v : adj[u]) { if (v == p) continue; get_path_nodes(v, target, u, adj, path, found); if (found) return; } path.pop_back(); } // 生成宝物分布 vector<int> gen_treasures(int n, const vector<pair<int, int>>& edges, string type) { vector<int> t_nodes; set<int> unique_t; // 构建临时邻接表方便操作 vector<vector<int>> local_adj(n + 1); for (auto& e : edges) { local_adj[e.first].push_back(e.second); local_adj[e.second].push_back(e.first); } if (type == "all_yes") { // 构造必定 Yes 的情况:随机选路径,宝物只撒在路径上 int u = random_int(1, n); int v = random_int(1, n); vector<int> path; bool found = false; get_path_nodes(u, v, 0, local_adj, path, found); int count = random_int(1, path.size()); for (int k = 0; k < count; ++k) { unique_t.insert(path[random_int(0, path.size() - 1)]); } } else if (type == "force_no") { // 构造必定 No 的情况:找个度数>=3的点,向三个分支各放一个宝物 // 如果找不到度数>=3的点(比如链),就随机撒点 int center = -1; vector<int> candidates; for(int i=1; i<=n; ++i) if(local_adj[i].size() >= 3) candidates.push_back(i); if (!candidates.empty()) { center = candidates[random_int(0, candidates.size()-1)]; // 找三个不同的邻居作为分支入口 int branch_count = 0; unique_t.insert(center); // 中心也可以放 for(int neighbor : local_adj[center]) { if(branch_count >= 3) break; // 在该分支深处找一个点(简单起见直接选邻居,或者邻居的邻居) unique_t.insert(neighbor); branch_count++; } } else { // 退化为随机(对于链表可能是 Yes,但概率上由外部控制类型) int count = min(n, random_int(3, 10)); while(unique_t.size() < count) unique_t.insert(random_int(1, n)); } } else { // random int count = random_int(1, min(n, 20)); // 默认随机撒少量宝物 // 特殊:有时撒很多 if (random_int(1, 10) > 8) count = random_int(1, n); while (unique_t.size() < count) { unique_t.insert(random_int(1, n)); } } // 保证至少一个宝物 if (unique_t.empty()) unique_t.insert(1); for (int x : unique_t) t_nodes.push_back(x); return t_nodes; } void generate_file(int file_id) { string in_name = to_string(file_id) + ".in"; string out_name = to_string(file_id) + ".out"; ofstream fin(in_name); ofstream fout(out_name); int T = 10; // 默认每组 10 个 case if (file_id <= 2) T = 10; // Subtask 1 else T = 5; // Subtask 2 & 3 稍微少一点以免文件过大 fin << T << endl; for (int t = 0; t < T; ++t) { int n; string tree_type = "random"; string treasure_type = "random"; // ================= 配置策略 ================= if (file_id == 1) { // Subtask 1: N <= 5, 基础测试 n = random_int(2, 5); tree_type = "random"; } else if (file_id == 2) { // Subtask 1: N <= 5, 边界测试 n = 5; // 一半链(Yes),一半菊花(混合) if (t < 5) tree_type = "line"; else tree_type = "star"; } else if (file_id == 3) { // Subtask 2: N <= 1000, 随机 n = random_int(500, 1000); tree_type = "random"; } else if (file_id == 4) { // Subtask 2: N <= 1000, 构造 Yes n = random_int(800, 1000); tree_type = "random"; treasure_type = "all_yes"; } else if (file_id == 5) { // Subtask 3: N <= 10^5, 链图 (全部应该是 Yes) n = random_int(50000, 100000); tree_type = "line"; treasure_type = "random"; } else if (file_id == 6) { // Subtask 3: N <= 10^5, 菊花图 (中心辐射) n = random_int(50000, 100000); tree_type = "star"; // 菊花图如果不加控制随机撒点,大概率是 No // 偶数个 case 强制 No,奇数个随机 if (t % 2 == 0) treasure_type = "force_no"; else treasure_type = "random"; } else if (file_id == 7) { // Subtask 3: N <= 10^5, 构造必定 Yes n = random_int(80000, 100000); tree_type = "deep"; // 深树,更容易产生长路径 treasure_type = "all_yes"; } else if (file_id == 8) { // Subtask 3: N <= 10^5, 构造必定 No n = random_int(80000, 100000); tree_type = "random"; treasure_type = "force_no"; } else if (file_id == 9) { // Subtask 3: 混合压力测试 n = 100000; tree_type = "random"; if (t < 3) treasure_type = "all_yes"; else if (t < 6) treasure_type = "force_no"; else treasure_type = "random"; } else { // file_id == 10: 极限数据 + 极简边界 (1个宝物, 2个宝物) n = 100000; tree_type = "random"; // 前两个 case 测试极少宝物 (Always Yes) if (t == 0) { // 1 treasure vector<pair<int,int>> edges = gen_tree(n, tree_type); fin << n << "\n"; // 输出宝物数组 for(int i=1; i<=n; ++i) fin << (i==1 ? 1 : 0) << (i==n?"":" "); fin << "\n"; for(auto& e : edges) fin << e.first << " " << e.second << "\n"; // Solver call Solver::init(n); for(auto& e : edges) Solver::add_edge(e.first, e.second); Solver::set_treasure(1); fout << Solver::solve() << "\n"; continue; } } // ================= 生成数据 ================= vector<pair<int, int>> edges = gen_tree(n, tree_type); vector<int> treasures = gen_treasures(n, edges, treasure_type); // ================= 写入输入文件 ================= fin << n << "\n"; // 构造宝物 0/1 数组 vector<int> is_treasure(n + 1, 0); for (int x : treasures) is_treasure[x] = 1; for (int i = 1; i <= n; ++i) { fin << is_treasure[i] << (i == n ? "" : " "); } fin << "\n"; for (auto& e : edges) { fin << e.first << " " << e.second << "\n"; } // ================= 运行 Solver 计算输出 ================= Solver::init(n); for (auto& e : edges) Solver::add_edge(e.first, e.second); for (int x : treasures) Solver::set_treasure(x); fout << Solver::solve() << "\n"; } fin.close(); fout.close(); cout << "Generated Test Case " << file_id << endl; } int main() { ios::sync_with_stdio(false); cin.tie(0); // 生成 1 到 10 号测试点 for (int i = 1; i <= 10; ++i) { generate_file(i); } return 0; }💡 生成策略详解
-
文件 1-2 (Subtask 1):
- 1.in: 纯随机小数据,用于基本的逻辑验证。
- 2.in: 特殊结构(纯链表、菊花图),测试算法在这些典型图上的表现。例如链表上任意宝物组合都应输出
Yes。
-
文件 3-4 (Subtask 2):
- 3.in: 的随机树,宝物随机分布。通常大概率是
No,用于测试“拒绝逻辑”。 - 4.in: Force Yes 模式。先随机选一条路径,然后只在这条路径上撒点。确保算法能正确识别复杂的
Yes情况。
- 3.in: 的随机树,宝物随机分布。通常大概率是
-
文件 5-8 (Subtask 3 专项):
- 5.in (链图): 的长链。验证大数据量下的栈深度和递归/BFS效率。答案应全为
Yes。 - 6.in (菊花图):中心节点连 个叶子。强制在三个不同分支放宝物,验证是否能秒判
No。 - 7.in (构造 Yes):在 规模的随机树上构造长路径宝物,测试大规模数据的路径判定准确性。
- 8.in (构造 No):在 规模随机树上寻找度数 的节点构造分叉宝物,测试大规模数据的反例判定。
- 5.in (链图): 的长链。验证大数据量下的栈深度和递归/BFS效率。答案应全为
-
文件 9-10 (混合与边界):
- 9.in: 混合模式,模拟真实的比赛测试数据分布。
- 10.in: 包含边界情况(如只有 1 个宝物,按题意必为
Yes),以及最大规模的随机压力测试。
⚠️ 使用提示
- 编译环境:代码使用了 C++11 特性(
mt19937,to_string等),请确保编译器支持(G++ 4.9+ 默认支持)。 - 运行时间:生成 的大规模数据时,因为要跑 BFS 计算标准答案,可能会花费几秒钟,请耐心等待直到控制台输出 "Generated Test Case 10"。
-
0
这是一个符合 NOIP C++14 竞赛标准的题解代码。
👨🏫 教练的解题分析
1. 核心思路:虚树直径 + 路径判定
题目要求“至多经过每条边一次”,意味着行走路线必须是一条简单路径。要一次性拿完所有宝物,说明所有宝物点必须分布在树上的一条单链上。
这就转化为了一个经典几何/图论问题:判断一组点集是否共线(在树上即共路径)。 这条“最长链”的两个端点,一定是宝物点集中距离最远的两个点。这类似于求树的直径。
算法步骤:
- 找端点 U:任选一个有宝物的点 ,进行第一次 BFS,找到距离 最远的宝物点 。
- 找端点 V:从 出发进行第二次 BFS,找到距离 最远的宝物点 。此时, 和 确立了覆盖所有宝物的唯一可能路径的主轴。
- 验证:进行第三次 BFS(从 出发),获取所有点到 的距离。对于每一个宝物点 ,检查是否满足:
如果所有宝物点都满足该条件,输出
Yes,否则输出No。
2. 复杂度分析
- 时间复杂度:。
- 每次查询需要做 3 次 BFS(求 U,求 V,求验证距离)。
- BFS 的复杂度是 ,树上 ,所以是 。
- 数据范围 ,总运算量约 ,远小于 1秒限时(约 次运算)。
- 空间复杂度:。
- 主要用于存储邻接表
vector<int> adj[MAXN]和距离数组。
- 主要用于存储邻接表
💻 标准题解代码 (C++14)
/** * P11249 [GESP202409 七级] 小杨寻宝 * 知识点:树的直径、BFS、路径性质 * * 核心逻辑: * 1. 所有宝物点必须在同一条简单路径上。 * 2. 这条路径的两端一定是宝物点中距离最远的两个点 (U, V)。 * 3. 验证所有宝物点是否满足 dist(U, i) + dist(i, V) == dist(U, V)。 */ #include <iostream> #include <vector> #include <queue> #include <algorithm> #include <cstring> // for memset (如果需要) using namespace std; // 定义最大节点数,稍微开大一点防止越界 const int MAXN = 100005; // 图的邻接表 vector<int> adj[MAXN]; // 标记该节点是否有宝物 bool has_treasure[MAXN]; // 存储所有宝物点的列表,方便遍历 vector<int> treasure_nodes; // 距离数组:distU 存到 U 的距离,distV 存到 V 的距离 int distU[MAXN]; int distV[MAXN]; int n; /** * BFS 函数 * @param start: 起点 * @param dist: 用于存储距离结果的数组指针 * @return: 返回距离 start 最远的【宝物节点】的编号 */ int bfs(int start, int* dist) { // 初始化距离为 -1,表示未访问 for (int i = 1; i <= n; ++i) { dist[i] = -1; } queue<int> q; q.push(start); dist[start] = 0; int max_dist = -1; int farthest_treasure_node = start; // 默认最远是自己 while (!q.empty()) { int u = q.front(); q.pop(); // 【关键点1】在遍历过程中动态更新离起点最远的宝物点 // 注意:我们只关心宝物点之间的最远距离,而不是树的几何直径端点 if (has_treasure[u]) { if (dist[u] > max_dist) { max_dist = dist[u]; farthest_treasure_node = u; } } for (int v : adj[u]) { if (dist[v] == -1) { // 如果未被访问 dist[v] = dist[u] + 1; q.push(v); } } } return farthest_treasure_node; } void solve() { cin >> n; // 【易错点1】多组数据,必须彻底清空全局容器和状态 for (int i = 1; i <= n; ++i) { adj[i].clear(); has_treasure[i] = false; } treasure_nodes.clear(); // 读入宝物信息 for (int i = 1; i <= n; ++i) { int a; cin >> a; if (a == 1) { has_treasure[i] = true; treasure_nodes.push_back(i); } } // 读入树边 for (int i = 0; i < n - 1; ++i) { int u, v; cin >> u >> v; adj[u].push_back(v); adj[v].push_back(u); } // 【边界情况】如果宝物少于等于 1 个,肯定可以拿完 if (treasure_nodes.size() <= 1) { cout << "Yes" << endl; return; } // Step 1: 从任意一个宝物点出发,找到最远的宝物点 U // 这里复用 distU 数组作为临时存储,只需要返回值 int root = treasure_nodes[0]; int U = bfs(root, distU); // Step 2: 从 U 出发,计算所有点到 U 的距离,并找到最远的宝物点 V int V = bfs(U, distU); // Step 3: 从 V 出发,计算所有点到 V 的距离 bfs(V, distV); // 此时,路径 U -> V 的长度即为 distU[V] int path_len = distU[V]; // Step 4: 验证所有宝物点是否都在 U -> V 的路径上 bool ok = true; for (int t : treasure_nodes) { // 【核心判定】点 t 在 U-V 路径上 <=> dist(U,t) + dist(t,V) == dist(U,V) if (distU[t] + distV[t] != path_len) { ok = false; break; } } if (ok) cout << "Yes" << endl; else cout << "No" << endl; } int main() { // 【优化建议】关闭同步流,加速大量数据的 IO ios::sync_with_stdio(false); cin.tie(0); int t; if (cin >> t) { while (t--) { solve(); } } return 0; }🔍 关键点与易错点注释
-
初始起点的选择:
- 第一次 BFS 的起点
root必须从treasure_nodes中取(即必须是一个宝物点)。 - 错误做法:如果从节点 1 开始 BFS,而节点 1 没有宝物且位于树的一个很偏僻的分支,找到的最远点可能并不是“宝物分布区”的端点,导致后续找出的直径错误。
- 第一次 BFS 的起点
-
多测清空:
adj数组需要对 进行clear()。has_treasure数组同样需要重置。- 如果不清空,上一组数据的边会残留,导致图结构混乱(变稀疏图为稠密图或错误连边),引发 WA 或 TLE。
-
直径的定义微调:
- 通常树的直径是指树上任意两点最远距离。
- 本题需要的是**“包含所有宝物点的最小连通子图(虚树)的直径”**。
- 因此,
bfs函数中更新max_dist时,加上了if (has_treasure[u])的判断条件。
-
IO 优化:
- 数据量 ,如果用
cin不加同步流关闭,可能会稍微慢一点(虽然通常不会超时,但养成好习惯很重要)。ios::sync_with_stdio(false); cin.tie(0);是标配。
- 数据量 ,如果用
-
0
你好!我是你的OI金牌教练。很高兴能带你分析这道非常有意思的图论思维题。
这道题考察的是树上路径的性质以及树的直径相关知识的灵活运用。我们不要急着写代码,先拿出草稿纸,把解题的逻辑画出来。
一、 核心关键词与题意解码
- “树” & “至多经过每条边一次”
- 隐含意义:这告诉我们,小杨行走的路线必须是一条简单路径(不能有环,不能走回头路,不能分叉去捡宝物再回来)。
- “取得所有宝物”
- 隐含意义:所有标记为
1的节点,必须全部落在上述的那条简单路径上。
- 隐含意义:所有标记为
- 判断 Yes/No
- 隐含意义:我们需要找到一种高效的方法来验证这些点是否共线。
二、 预备知识点
- 树的基本性质:树上任意两点之间有且仅有一条简单路径。
- 树的直径(变形):通常指树上最远两点的距离。这里我们需要用到求直径的思想——两次 BFS/DFS 法。
- 路径判定:判断点 是否在点 和点 的路径上,满足条件:
dist(U, X) + dist(X, V) == dist(U, V)。
三、 启发式教学:草稿纸上的推理演练
假设我们在黑板上画图,我会这样引导你:
1. 简单情况分析
- 情况 A:宝物节点排成一条直线(例如 1-2-3-4,宝物在 1, 2, 4)。
- 能不能一次拿完?能。从 1 走到 4,中间经过 2。
- 情况 B:宝物节点分叉了(例如 1-2-3-4 是主干,2 号点连着个 5 号点,宝物在 1, 4, 5)。
- 能不能一次拿完?不能。
- 如果你从 1 走到 4,必须经过 2,但没法去 5(去了 5 就回不来 2 继续去 4 了,因为边只能走一次)。
- 如果你从 5 走到 4,必须经过 2,但没法去 1。
- 结论:所有宝物节点必须在一条链上。
2. 寻找“链”的端点
既然必须是一条链,那这条链的两个端点肯定是所有宝物里距离最远的那两个点。
- 这不就是求**“宝物构成的虚树的直径”**吗?
3. 算法流程推导(在草稿纸上画步骤)
步骤一:找端点 U
- 随便选一个有宝物的点(比如第一个读入的宝物点 )。
- 在整棵树上跑 BFS,找到距离 最远的宝物点。记为 。
- 思考: 一定是最终路径的一个端点(或者是端点的候选)。
步骤二:找端点 V
- 从 出发,再跑一遍 BFS,找到距离 最远的宝物点。记为 。
- 结论:如果存在一条路径能覆盖所有宝物,那么这条路径的起点和终点一定是 和 。
步骤三:验证中间点
- 现在我们确定了最长链 。
- 我们需要检查:是不是所有的宝物点都在 这条路径上?
- 验证公式:对于任意宝物点 ,如果
dist(U, T) + dist(T, V) == dist(U, V),说明 在路径上。如果不相等,说明 在分叉上,输出No。
4. 复杂度分析
- 我们需要跑几次 BFS?
- 从任意宝物 找 。
- 从 找 (顺便求出所有点到 的距离
distU)。 - 从 跑 BFS(求出所有点到 的距离
distV)。
- 总共 3 次 BFS。
- 时间复杂度:。对于 , 的数据,完全可以接受。
- 空间复杂度: 存图和距离数组。
四、 读题关键词总结
做这类题,眼睛要抓几个词:
- “边至多经过一次” 简单路径。
- “树” 路径唯一性,距离性质。
- “覆盖特定点集” 通常涉及求点集内的最远点对(直径),或者最近公共祖先(LCA)性质。
五、 题解标准代码 (C++14)
#include <iostream> #include <vector> #include <queue> #include <algorithm> using namespace std; const int MAXN = 100005; const int INF = 0x3f3f3f3f; // 全局变量,避免栈溢出,方便清空 vector<int> adj[MAXN]; int has_treasure[MAXN]; // 标记是否是宝物点 vector<int> treasures; // 存储所有宝物点的编号 int distU[MAXN]; // 存储到端点 U 的距离 int distV[MAXN]; // 存储到端点 V 的距离 int n; // BFS 函数:计算从 start 节点到树上所有节点的距离,结果存入 dist_array // 返回值:距离 start 最远的那个【宝物节点】的编号 int bfs(int start, int* dist_array) { // 初始化距离数组 for (int i = 1; i <= n; ++i) { dist_array[i] = -1; } queue<int> q; q.push(start); dist_array[start] = 0; int max_dist = -1; int farthest_treasure = start; while (!q.empty()) { int u = q.front(); q.pop(); // 如果当前点是宝物点,更新最远宝物记录 if (has_treasure[u]) { if (dist_array[u] > max_dist) { max_dist = dist_array[u]; farthest_treasure = u; } } for (int v : adj[u]) { if (dist_array[v] == -1) { // 未访问过 dist_array[v] = dist_array[u] + 1; q.push(v); } } } return farthest_treasure; } void solve() { cin >> n; // 清空上一轮的数据 treasures.clear(); for (int i = 1; i <= n; ++i) { adj[i].clear(); has_treasure[i] = 0; } // 读入宝物分布 for (int i = 1; i <= n; ++i) { int a; cin >> a; if (a == 1) { has_treasure[i] = 1; treasures.push_back(i); } } // 读入边 for (int i = 0; i < n - 1; ++i) { int u, v; cin >> u >> v; adj[u].push_back(v); adj[v].push_back(u); } // 特判:如果没有宝物或只有1个宝物,肯定 Yes if (treasures.size() <= 1) { cout << "Yes" << endl; return; } // 步骤 1: 从任意一个宝物点 (treasures[0]) 出发,找最远的宝物点 U // 这里借用 distU 数组暂存距离,其实只需要返回值 int U = bfs(treasures[0], distU); // 步骤 2: 从 U 出发,找最远的宝物点 V,并计算所有点到 U 的距离 int V = bfs(U, distU); // 步骤 3: 从 V 出发,计算所有点到 V 的距离 bfs(V, distV); // 路径 U -> V 的长度 int path_len = distU[V]; // 步骤 4: 检查所有宝物点是否都在 U -> V 的路径上 // 判定条件:dist(U, T) + dist(T, V) == dist(U, V) bool ok = true; for (int t : treasures) { if (distU[t] == -1 || distV[t] == -1) { // 理论上连通图不会出现 -1,除非图不连通(题目保证是树,所以不连通不可能) ok = false; break; } if (distU[t] + distV[t] != path_len) { ok = false; break; } } if (ok) cout << "Yes" << endl; else cout << "No" << endl; } int main() { // 开启 IO 优化,防止因为输入输出慢导致 TLE ios::sync_with_stdio(false); cin.tie(0); int t; cin >> t; while (t--) { solve(); } return 0; }六、 代码细节与优化建议
- 关于 BFS 的起点:
- 第一次 BFS 的起点必须是某个宝物点。如果从非宝物点(如节点 1,但节点 1 没宝物)出发,可能会找到一个离根远但不在“宝物分布区域”边缘的点,导致错误的直径判定。
- 空间与时间:
- 时间复杂度 。,总计算量约 级别,非常稳。
- 空间使用
vector存图,动态分配,不会浪费。
- 易错点:
- 多测清空:
adj数组和has_treasure标记在每组数据开始前一定要清空。 - 特判:虽然题目说至少有一个宝物,但代码里加上
<= 1个宝物的判断会让逻辑更严密。
- 多测清空:
希望这套思路能帮你彻底搞懂这类“树上路径覆盖”的问题!如果有哪里卡住了,随时在草稿纸上画个图验证一下。
- “树” & “至多经过每条边一次”
- 1
信息
- ID
- 15713
- 时间
- 1000ms
- 内存
- 32MiB
- 难度
- 5
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者