2 条题解
-
0
你好!我是OI金牌教练。
为了帮助你创建OJ测试数据,我编写了一个全自动的数据生成器。这个程序会一次性生成
1.in/1.out到10.in/10.out共10组测试数据。设计思路
- 覆盖全面:
- 边界情况:(空链表),(单节点),(满数据)。
- 极端值:包含最小值
-100和最大值100。 - 特殊分布:全相等(如
1 1 1)、全不等(如1 2 3)、只有两个值(如1...1 2...2)。
- 保证有序:题目要求输入是“已排序”的,生成随机数后必须使用
std::sort排序。 - 标准解法集成:为了生成
.out文件,我在生成器内部内置了一个“绝对正确”的解法(利用std::vector和std::unique特性,等价于链表去重逻辑),确保输出数据的正确性。
C++ 数据生成器代码 (Data Generator)
请将以下代码保存为
gen.cpp,编译运行即可在当前目录下生成所有文件。/** * NOIP/NOI 风格题目数据生成器 * 题目: LeetCode 83 - 删除排序链表中的重复元素 * 生成: 1.in/1.out ~ 10.in/10.out * 环境: C++14 */ #include <iostream> #include <fstream> #include <vector> #include <algorithm> #include <random> #include <string> #include <set> using namespace std; // 配置参数 const int MAX_N = 300; // 最大节点数 const int MIN_VAL = -100; // 节点最小值 const int MAX_VAL = 100; // 节点最大值 // 随机数生成器初始化 mt19937 rng(random_device{}()); /** * 辅助函数:生成指定范围内的随机整数 [l, r] */ int random_int(int l, int r) { uniform_int_distribution<int> dist(l, r); return dist(rng); } /** * 核心逻辑:解决问题并生成 .out 内容 * 这里的逻辑等价于链表去重,但为了生成器稳定性,使用 vector 操作 */ void solve_and_write(const vector<int>& input_data, string out_filename) { ofstream fout(out_filename); // 复制一份数据以免修改原数据 vector<int> result = input_data; // 使用 std::unique 去重,原理和链表去重完全一致: // 只有当当前元素 != 前一个元素时保留 auto last = unique(result.begin(), result.end()); result.erase(last, result.end()); // 输出结果 for (size_t i = 0; i < result.size(); ++i) { if (i > 0) fout << " "; fout << result[i]; } fout << endl; fout.close(); } /** * 生成单组测试数据 * id: 测试点编号 * n: 节点数量 * type: 数据类型模式 */ void generate_case(int id, int n, int type) { string in_file = to_string(id) + ".in"; string out_file = to_string(id) + ".out"; ofstream fin(in_file); // 1. 生成输入数据 vector<int> data; if (n > 0) { if (type == 1) { // 模式1: 全相等 (Worst Case for duplicates) int val = random_int(MIN_VAL, MAX_VAL); for(int i=0; i<n; ++i) data.push_back(val); } else if (type == 2) { // 模式2: 全不等 (Best Case, 尽量不重复) // 由于范围 -100~100 只有201个数,如果 n > 201 必然重复,这里尽量取不同 set<int> s; while(data.size() < n) { // 如果把范围取完了,只能随机填充 if (s.size() >= (MAX_VAL - MIN_VAL + 1)) { data.push_back(random_int(MIN_VAL, MAX_VAL)); } else { int val = random_int(MIN_VAL, MAX_VAL); if (s.find(val) == s.end()) { s.insert(val); data.push_back(val); } } } } else if (type == 3) { // 模式3: 两极分化 (前一半最小值,后一半最大值) for(int i=0; i<n/2; ++i) data.push_back(MIN_VAL); for(int i=n/2; i<n; ++i) data.push_back(MAX_VAL); } else { // 模式0: 完全随机 (Random) for(int i=0; i<n; ++i) { data.push_back(random_int(MIN_VAL, MAX_VAL)); } } } // 题目关键要求:输入必须是有序的 sort(data.begin(), data.end()); // 2. 写入 .in 文件 fin << n << endl; for (size_t i = 0; i < data.size(); ++i) { if (i > 0) fin << " "; fin << data[i]; } // 即便 n=0 也要换行,保持格式整洁 fin << endl; fin.close(); // 3. 写入 .out 文件 (调用解法) solve_and_write(data, out_file); cout << "Generated Test Case " << id << ": N = " << n << endl; } int main() { // 优化生成速度 ios::sync_with_stdio(false); // --- 构造测试点 --- // 1. 边界:空链表 generate_case(1, 0, 0); // 2. 边界:单个节点 generate_case(2, 1, 0); // 3. 边界:两个节点,值相同 generate_case(3, 2, 1); // 4. 边界:两个节点,值不同 generate_case(4, 2, 2); // 5. 小数据:全相等 generate_case(5, 10, 1); // 6. 小数据:全不等 (密集去重测试) generate_case(6, 20, 2); // 7. 中等数据:随机 generate_case(7, 50, 0); // 8. 满数据:只有两个值 (测试大量跳跃) generate_case(8, 300, 3); // 9. 满数据:随机分布 generate_case(9, 300, 0); // 10. 满数据:随机分布 (再次验证) generate_case(10, 300, 0); cout << "All 10 test cases generated successfully." << endl; return 0; }数据点详细说明 (Coverage Analysis)
测试点 ID N (节点数) 数据特征 考察点 1 0 空 边界处理:程序不应崩溃,输出应为空或仅换行。 2 1 随机值 边界处理:单个节点不应触发 next->val检查。3 2 值相同 (如 5 5)基础逻辑:能否删除末尾的重复元素。 4 值不同 (如 1 2)基础逻辑:不删除的情况。 5 10 全相等 (如 3 3 ... 3)循环删除:能否处理连续的重复,只保留一个。 6 20 全不等 (如 -10 -9 ...)遍历逻辑:每个节点都保留,指针移动是否正确。 7 50 随机 常规情况:混合了重复和不重复。 8 300 两段式 (如 -100... 100...)特殊构造:前半段大量删除,后半段大量删除,中间跳跃。 9 随机 压力测试:最大数据量下的常规表现。 10 压力测试:验证解的稳定性。 使用方法
- 将代码保存为
gen.cpp。 - 编译:
g++ gen.cpp -o gen -O2 -std=c++14。 - 运行:
./gen(Windows 下是gen.exe)。 - 你的目录下会出现
1.in,1.out...10.out。 - 将这些文件打包上传到你的 OJ 系统后台即可。
- 覆盖全面:
-
0
你好!作为金牌教练,我直接给出这份符合 NOIP/NOI 竞赛标准 的标准代码(Standard Solution)。
这份代码采用了 C++14 标准,不仅实现了核心逻辑,还包含了完整的输入输出处理框架。代码中我特意在容易写挂的地方加了详细注释。
一、 NOIP 标准题解代码 (C++14)
/* * 题目: 有序链表去重 (Remove Duplicates from Sorted List) * 标签: 链表 (Linked List), 模拟 (Simulation) * 难度: NOIP 普及组- / 入门 * 作者: OI金牌教练 */ #include <iostream> // 标准输入输出流 #include <algorithm> // 通用算法库 #include <vector> using namespace std; // --- 结构体定义 --- struct ListNode { int val; ListNode *next; // 构造函数:初始化值和空指针 ListNode(int x) : val(x), next(nullptr) {} }; // --- 核心算法函数 --- ListNode* deleteDuplicates(ListNode* head) { // [边界判断] 如果链表为空或只有一个节点,无需处理,直接返回 if (head == nullptr || head->next == nullptr) { return head; } ListNode* cur = head; // [循环条件] 必须保证当前节点(cur)和下一个节点(cur->next)都存在 // 否则比较 cur->next->val 时会发生段错误 (Segmentation Fault) while (cur->next != nullptr) { // [核心逻辑] if (cur->val == cur->next->val) { // 发现重复:当前值 == 下一个值 ListNode* duplicate = cur->next; // 1. 暂存待删除节点 cur->next = duplicate->next; // 2. 修改指针:跳过重复节点 delete duplicate; // 3. 释放内存 (养成好习惯) // [易错点警告] // 删除后,不要移动 cur 指针! // 因为新的 cur->next 可能仍然和 cur->val 相等(例如 1->1->1 的情况) // 下一次循环需要再次检查当前的 cur 和 新的 next } else { // 没有重复:才移动指针到下一位 cur = cur->next; } } return head; } int main() { // [IO优化] 关闭stdio同步,加快读写速度,这是竞赛必备习惯 ios::sync_with_stdio(false); cin.tie(nullptr); int n; // 处理可能的读取失败或EOF if (!(cin >> n)) return 0; // 特判 n=0 的情况,虽然题目范围通常有保障,但在竞赛中不仅要看范围,还要防一手空数据 if (n == 0) { return 0; } // --- 建表过程 (尾插法) --- ListNode* head = nullptr; ListNode* tail = nullptr; for (int i = 0; i < n; ++i) { int val; cin >> val; // 动态开辟节点 ListNode* newNode = new ListNode(val); if (head == nullptr) { // 第一个节点既是头也是尾 head = newNode; tail = newNode; } else { // 将新节点挂在尾部,并更新尾指针 tail->next = newNode; tail = newNode; } } // --- 调用核心算法 --- head = deleteDuplicates(head); // --- 输出结果 --- ListNode* cur = head; bool first = true; // 用于控制空格格式 while (cur != nullptr) { if (!first) cout << " "; cout << cur->val; first = false; cur = cur->next; } cout << endl; // 结尾换行,符合竞赛输出规范 // 竞赛中通常由操作系统统一回收进程内存, // 手动释放整个链表的代码在限时比赛中通常省略。 return 0; }
二、 复杂度分析思考过程
在草稿纸上推演算法时,我们是这样分析复杂度的:
1. 时间复杂度分析
- 思考过程:
- 代码主要有一个
while循环:while (cur->next != nullptr)。 - 在循环体内,我们做的事情是比较
cur->val和cur->next->val。 - 如果不相等,
cur向后移动一步。 - 如果相等,我们删除了一个节点,虽然
cur没动,但链表的总长度 减少了 1。 - 这意味着:每个节点要么被
cur访问并跳过,要么被删除。 - 任何一个节点都不会被反复处理超过常数次。
- 代码主要有一个
- 结论:时间复杂度为 ,其中 是链表的长度。这是最优复杂度,因为我们必须至少读取一次所有数据。
2. 空间复杂度分析
- 思考过程:
- 我们定义了
head,tail,cur,duplicate等几个指针变量。 - 这些变量占用的空间是固定的,不会随着数据量 的增大而增大。
- 注意:虽然链表本身占用了 的空间,但那是存储输入数据必须的。算法的额外空间复杂度(Auxiliary Space Complexity)只看我们算法运行时额外申请的部分。
- 我们定义了
- 结论:空间复杂度为 (原地去重)。
三、 时间复杂度优化建议(进阶)
虽然 已经是理论最优,但在**实际竞赛(如 NOIP/NOI)**中,如果数据量极其巨大(例如 或 ),频繁的
new和delete操作(动态内存分配)会非常慢,甚至导致超时(TLE)。优化方案:静态数组模拟链表(Memory Pool)
这是高阶选手的通用技巧。我们不使用结构体指针,而是用两个数组
val[N]和next_idx[N]来模拟。- 优点:
- 没有动态分配内存的开销,速度极快。
- 内存连续,缓存命中率高(Cache Friendly)。
- 代码片段示意:
const int MAXN = 300005; int val[MAXN]; // 存储节点的值 int nxt[MAXN]; // 存储下一个节点的下标 (代替指针) int head = 1; // 头结点下标 int idx = 0; // 内存池计数器 // 核心逻辑变化 int curr = head; while (nxt[curr] != 0) { // 0 表示 nullptr if (val[curr] == val[nxt[curr]]) { nxt[curr] = nxt[nxt[curr]]; // 直接修改下标指向,跳过 } else { curr = nxt[curr]; } }总结:对于本题 的范围,使用标准的
struct指针写法更清晰、更符合题目本意。但如果遇到 ,请务必切换到静态数组模拟链表。 - 思考过程:
- 1
信息
- ID
- 19332
- 时间
- 1000ms
- 内存
- 32MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者