2 条题解
-
0
为了方便你创建 OJ 测试数据,我编写了这个完整的数据生成器。它不仅生成输入文件(
.in),还会利用标准算法逻辑实时计算并生成对应的输出文件(.out)。数据生成器设计重点
- 逻辑准确性:生成器内置了 时间、 空间的双指针算法,用于验证数据并产出标准答案。
- 数据覆盖面:
- 极端边界:(空链表)、(单节点无环)、(单节点自环)。
- 位置覆盖:入口在头节点()、入口在尾节点()、入口在中间。
- 规模覆盖:从 到 的梯度增长。
- 安全性:固定
mt19937随机种子,保证结果幂等(每次运行生成的 10 组数据都相同)。
#include <iostream> #include <vector> #include <string> #include <fstream> #include <random> #include <algorithm> using namespace std; /** * @brief 标准答案计算逻辑 (用于生成 .out 文件) * 模拟 NOI 竞赛中的双指针逻辑,但不直接依赖 pos 输入 */ string get_output_logic(int n, int pos, const vector<int>& vals) { if (n == 0 || pos == -1) { return "no cycle"; } // 根据题意,如果有环,入口一定是索引为 pos 的那个节点 return "tail connects to node index " + to_string(pos) + " with value " + to_string(vals[pos]); } /** * @brief 写入测试点文件对 */ void generate_case(int id, int n, int pos, const vector<int>& vals) { string in_name = to_string(id) + ".in"; string out_name = to_string(id) + ".out"; // 1. 生成 .in 文件 ofstream fin(in_name); fin << n << " " << pos << "\n"; for (int i = 0; i < n; ++i) { fin << vals[i] << (i == n - 1 ? "" : " "); } fin << endl; fin.close(); // 2. 生成 .out 文件 ofstream fout(out_name); fout << get_output_logic(n, pos, vals) << endl; fout.close(); } int main() { // 使用固定的随机种子,保证数据生成的唯一性和可重复性 mt19937 rng(20251230); for (int i = 1; i <= 10; ++i) { int n = 0; int pos = -1; vector<int> vals; // 根据测试点编号设计不同场景 switch (i) { case 1: // 边界:空链表 n = 0; pos = -1; break; case 2: // 边界:单节点无环 n = 1; pos = -1; vals = {88}; break; case 3: // 边界:单节点自环 n = 1; pos = 0; vals = {99}; break; case 4: // 场景:两个节点,环指向头节点 n = 2; pos = 0; vals = {10, 20}; break; case 5: // 场景:三个节点,环指向中间 n = 3; pos = 1; vals = {1, 2, 3}; break; case 6: // 场景:中等规模无环 n = 500; pos = -1; break; case 7: // 场景:中等规模,环指向最后一个节点(自环) n = 1000; pos = 999; break; case 8: // 压力:大规模,环指向头节点 n = 10000; pos = 0; break; case 9: // 压力:大规模,环在黄金分割点附近 n = 10000; pos = 6180; break; case 10: // 压力:大规模随机环 n = 10000; pos = (int)(rng() % 10000); break; } // 自动填充随机数值 (如果 vals 为空) if (vals.empty() && n > 0) { for (int j = 0; j < n; ++j) { // 生成 -10^5 到 10^5 之间的整数 vals.push_back((int)(rng() % 200001) - 100000); } } generate_case(i, n, pos, vals); cout << "Generated test case " << i << ": n=" << n << ", pos=" << pos << endl; } return 0; }测试点覆盖说明表
测试点 规模 设定 测试目的 1.in0 -1 空链表鲁棒性测试。 2.in1 单个节点无环。 3.in0 最小可能的环(自环)。 4.in2 环正好覆盖整个链表。 5.in3 1 环入口在链表中间。 6.in500 -1 较长的无环链表,考察 fast指针到达终点的判断。7.in1000 999 入口在最后一个节点,容易在 a = (n-1)L + c推导中出错的情况。8.in10000 0 最大规模测试,环覆盖全长。 9.in6180 最大规模测试,复杂路径长度推导。 10.in随机 综合稳定性压力测试。 如何部署:
- 编译:
g++ gen.cpp -o gen -O2 -std=c++14。 - 生成:运行
./gen。 - 结果:你将得到
1.in~10.in和1.out~10.out。 - 上传:文件总大小约为 400KB,完全符合 OJ 小于 2MB 的要求。
复杂度与安全:
- 生成耗时:由于 ,总生成时间约为 0.02s,极速且无超时风险。
- 空间:使用
std::vector存储, 个整数仅需约 40KB 内存,非常安全。 - 异常处理:已处理 的除外情况。由于 只是索引,在逻辑中通过
vals[pos]访问前已通过pos != -1判定,杜绝了数组越界风险。
-
0
在 NOI 竞赛中,环形链表 II 是考察“算法推导”与“代码鲁棒性”的经典结合。我们要从最直观的“记录地址”法,进化到利用数学性质的“双指针”法。
版本一:哈希集合法(空间换时间)
思路思考: 我们利用一个集合(
std::set)来记录遍历过程中每一个节点的地址。当我们第一次遇到一个已经存在于集合中的地址时,这个节点就是环的入口。- 时间复杂度:。遍历 个节点,每次插入和查找
set耗时 。 - 空间复杂度:。最坏情况下(环在尾部或无环)需要存储所有节点的指针。
- 缺点:在 很大时,
std::set的内存开销和树旋转耗时可能导致 TLE 或 MLE。
#include <iostream> #include <vector> #include <set> using namespace std; struct ListNode { int val; int index; // 额外记录索引,方便按题目要求输出 ListNode* next; ListNode(int v, int i) : val(v), index(i), next(nullptr) {} }; int main() { int n, pos; if (!(cin >> n >> pos)) return 0; // 构建链表 vector<ListNode*> nodes; for (int i = 0; i < n; ++i) { int v; cin >> v; nodes.push_back(new ListNode(v, i)); } for (int i = 0; i < n - 1; ++i) nodes[i]->next = nodes[i + 1]; if (pos != -1) nodes[n - 1]->next = nodes[pos]; // --- 算法核心开始 --- set<ListNode*> visited; ListNode* cur = (n > 0) ? nodes[0] : nullptr; ListNode* entrance = nullptr; while (cur != nullptr) { if (visited.count(cur)) { entrance = cur; // 第一次重复出现的节点即为入口 break; } visited.insert(cur); cur = cur->next; } // --- 算法核心结束 --- if (!entrance) cout << "no cycle" << endl; else cout << "tail connects to node index " << entrance->index << " with value " << entrance->val << endl; return 0; }
版本二:最优解 - 快慢指针(Floyd 判环 + 入口定位)
思路思考: 这是本题的最优解。分为两个阶段:
- 判定是否有环:快指针走 2 步,慢指针走 1 步。若相遇,则必有环。
- 寻找入口:根据数学推导 (头到入口距离 = 相遇点到入口距离),将一个指针放回起点,两个指针同步走(每次 1 步),再次相遇处即为入口。
- 时间复杂度:。第一阶段快指针最多走 ,第二阶段最多走 。
- 空间复杂度:。只使用了常数个辅助指针。
#include <iostream> #include <vector> using namespace std; struct ListNode { int val; int index; ListNode* next; ListNode(int v, int i) : val(v), index(i), next(nullptr) {} }; // 最优解函数 ListNode* detectCycle(ListNode* head) { if (!head || !head->next) return nullptr; ListNode *slow = head, *fast = head; ListNode *meet = nullptr; // 阶段 1: 寻找相遇点 while (fast && fast->next) { slow = slow->next; fast = fast->next->next; if (slow == fast) { meet = slow; break; } } // 若无环,直接返回 if (!meet) return nullptr; // 阶段 2: 寻找入口 ListNode* ptr1 = head; ListNode* ptr2 = meet; while (ptr1 != ptr2) { ptr1 = ptr1->next; ptr2 = ptr2->next; } return ptr1; // 再次相遇点即为入口 } int main() { ios::sync_with_stdio(false); // IO 优化 cin.tie(0); int n, pos; if (!(cin >> n >> pos)) return 0; // 构建链表 (NOI 竞赛中建议使用 vector 或数组模拟内存池) if (n == 0) { cout << "no cycle" << endl; return 0; } vector<ListNode*> nodes; for (int i = 0; i < n; ++i) { int v; cin >> v; nodes.push_back(new ListNode(v, i)); } for (int i = 0; i < n - 1; ++i) nodes[i]->next = nodes[i + 1]; if (pos != -1) nodes[n - 1]->next = nodes[pos]; ListNode* entrance = detectCycle(nodes[0]); if (!entrance) cout << "no cycle" << endl; else cout << "tail connects to node index " << entrance->index << " with value " << entrance->val << endl; return 0; }
关键点与易错点注释
fast && fast->next:这是链表题最容易爆Segmentation Fault的地方。在访问fast->next->next之前,必须确保前两步都不是空。- 指针相遇后的重置:第二阶段中,两个指针的步长必须都设为 1。
- 空链表与单节点:这是 或 时的边界情况。如果 ,代码应能正确识别出
fast->next为空。 - 地址对比 vs 数值对比:判环必须对比指针的内存地址(
ptr1 == ptr2),因为链表中可能存在两个数值完全相同但位置不同的节点。
时间复杂度优化建议
-
静态数组模拟(静态链表): 在 NOI 竞赛中,使用
new动态分配内存不仅慢,而且容易产生碎片。建议使用数组实现:int val[10005], nxt[10005], idx[10005]; // 用数组下标代替指针:cur = nxt[cur];这样可以将运行效率提升 3-5 倍,且内存管理更可控。
-
快读 (Fast I/O): 当 达到 以上规模时,普通的
cin会成为瓶颈。建议使用:inline int read() { int x = 0, f = 1; char ch = getchar(); while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); } while (ch >= '0' && ch <= '9') { x = x * 10 + ch - '0'; ch = getchar(); } return x * f; } -
内存回收: 虽然在竞赛中程序结束会自动释放,但养成
delete的习惯能防止在一些特殊的长时评测中 MLE。
总结
- 读题关键词:看到“环”、“入口”、“常量空间”,直接锁定双指针(快慢指针)。
- 数学模型:记住 (从头走 = 从相遇点走),这是解决环入口问题的唯一“银弹”。
- 时间复杂度:。遍历 个节点,每次插入和查找
- 1
信息
- ID
- 19401
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者