2 条题解
-
0
这是一个完整的 C++ 数据生成器,专门为“质粒切割计划”这道题设计。
它会自动生成
1.in~10.in和对应的1.out~10.out。测试数据覆盖了样例、小规模、乱序、已排序、逆序、最大跨度在边界、聚集分布以及 规模的大数据压力测试。数据生成器代码 (Generator.cpp)
你需要将此代码保存为
generator.cpp,编译并运行即可。/** * 题目:质粒切割计划 (Plasmid Cutting) * 功能:自动生成 1.in ~ 10.in 及对应的标准答案 .out 文件 * 覆盖策略: * 1. 样例数据 * 2. 最小边界 (N=2) * 3. 基础随机 * 4. 输入已有序 (升序) * 5. 输入已逆序 (降序) * 6. 最大片段跨越零点 (测试环形逻辑) * 7. 聚集分布 (所有切点集中在一小块,测试大空档) * 8. 大数据随机 (N=10^5) * 9. 大数据 (N=10^5) 且 L 极大 * 10. 极限数据 */ #include <iostream> #include <fstream> #include <vector> #include <string> #include <algorithm> #include <random> #include <ctime> #include <set> using namespace std; // --- 全局随机数引擎 --- mt19937 rng(time(0)); // 辅助函数:生成范围 [min, max] 内的随机整数 int rand_int(int min_val, int max_val) { uniform_int_distribution<int> dist(min_val, max_val); return dist(rng); } // --- 核心解题算法 (用于生成 .out) --- int solve(int L, int N, vector<int> cuts) { // 1. 排序 sort(cuts.begin(), cuts.end()); int max_len = 0; // 2. 计算中间段 for (int i = 1; i < N; ++i) { int seg = cuts[i] - cuts[i-1]; if (seg > max_len) max_len = seg; } // 3. 计算首尾相连段 (关键逻辑) int tail_to_head = (L - cuts[N-1]) + cuts[0]; if (tail_to_head > max_len) max_len = tail_to_head; return max_len; } // --- 生成唯一切点坐标 --- // range_end 是坐标上限 (不包含),即 [0, range_end) vector<int> generate_unique_cuts(int n, int range_end, int offset = 0) { set<int> s; while (s.size() < n) { // 保证坐标 < range_end int val = rand_int(0, range_end - 1); s.insert(val + offset); } // 转为 vector return vector<int>(s.begin(), s.end()); } // --- 主生成逻辑 --- void make_case(int id) { int L, N; vector<int> cuts; // 根据编号定制数据特征 switch(id) { case 1: // 【样例数据】 L = 100; N = 3; cuts = {80, 10, 35}; break; case 2: // 【最小数据】N=2,测试基础逻辑 L = 50; N = 2; cuts = generate_unique_cuts(N, L); break; case 3: // 【小规模随机】N=10,普通乱序 L = 1000; N = 10; cuts = generate_unique_cuts(N, L); // 默认 set 生成的是有序的,打乱它以模拟真实乱序输入 shuffle(cuts.begin(), cuts.end(), rng); break; case 4: // 【输入已有序】测试是否会因为已经是升序而出错(不应出错) L = 10000; N = 50; cuts = generate_unique_cuts(N, L); // 此时 cuts 已经是升序,无需 shuffle break; case 5: // 【输入为逆序】测试完全降序输入 L = 10000; N = 50; cuts = generate_unique_cuts(N, L); sort(cuts.rbegin(), cuts.rend()); // 逆序排列 break; case 6: // 【最大跨度在边界】 // 特意让中间都很紧凑,把最大空档留在首尾连接处 L = 10000; N = 10; // 在 [1000, 2000] 之间生成所有点,这样首尾差距巨大 cuts = generate_unique_cuts(N, 1000, 1000); shuffle(cuts.begin(), cuts.end(), rng); break; case 7: // 【聚集分布】 // 切点全部集中在质粒的前 10% 区域,测试长距离空白 L = 100000; N = 100; cuts = generate_unique_cuts(N, L / 10); shuffle(cuts.begin(), cuts.end(), rng); break; case 8: // 【大数据压力 1】普通随机 // N = 10^5, L = 10^8 L = 100000000; N = 100000; cuts = generate_unique_cuts(N, L); shuffle(cuts.begin(), cuts.end(), rng); break; case 9: // 【大数据压力 2】极端稀疏 // N = 50000, L = 10^9 (最大范围) L = 1000000000; N = 50000; cuts = generate_unique_cuts(N, L); shuffle(cuts.begin(), cuts.end(), rng); break; case 10: // 【极限数据】 // N = 100,000 (最大值), L = 10^9 L = 1000000000; N = 100000; cuts = generate_unique_cuts(N, L); shuffle(cuts.begin(), cuts.end(), rng); break; default: L = 100; N = 5; cuts = generate_unique_cuts(N, L); break; } // --- 写入 .in 文件 --- string in_filename = to_string(id) + ".in"; ofstream fout_in(in_filename); fout_in << L << " " << N << "\n"; // 输出 cuts,用空格分隔 for (size_t i = 0; i < cuts.size(); ++i) { fout_in << cuts[i] << (i == cuts.size() - 1 ? "" : " "); } fout_in << "\n"; fout_in.close(); // --- 计算并写入 .out 文件 --- // 注意:传给 solve 的 cuts 是按值传递的副本,不会影响写入 .in 的原始顺序 int ans = solve(L, N, cuts); string out_filename = to_string(id) + ".out"; ofstream fout_out(out_filename); fout_out << ans << "\n"; fout_out.close(); cout << "Generated Case " << id << ": L=" << L << ", N=" << N << " -> Ans=" << ans << endl; } int main() { cout << "Start generating data for Plasmid Cutting..." << endl; for (int i = 1; i <= 10; ++i) { make_case(i); } cout << "All 10 test cases generated successfully!" << endl; return 0; }详细说明
-
输入乱序模拟:
std::set生成的随机数默认是有序的。为了模拟真实考试中“坐标不一定有序”的情况,我在大多数测试点(Case 3, 6, 7, 8, 9, 10)使用了shuffle函数将数组打乱。- Case 4 保留了升序,Case 5 制造了降序,用来测试选手的排序算法在这些特定情况下的稳定性(尽管
std::sort通常都没问题,但这是好的测试习惯)。
-
边界情况处理 (Case 6):
- 这是本题的逻辑核心(首尾相连)。
- 在这个测试点中,我将 个切点全部限制在 范围内,而总长度 。这意味着 这段跨越零点的区域将是最大的片段,强制程序必须正确处理
(L - last) + first才能得到正确答案。
-
大数据生成效率:
- 对于 的情况,使用
std::set判重生成 10万个随机数可能需要几十毫秒,这是完全可接受的。 - 设为 (10亿),确保坐标值分布足够稀疏,且不会发生整数溢出(
int最大 21亿)。
- 对于 的情况,使用
-
使用步骤:
- 将代码保存为
gen.cpp。 - 编译:
g++ gen.cpp -o gen -O2 - 运行:
./gen - 你会看到目录下多了 20 个文件,可以直接导入 OJ 系统。
- 将代码保存为
-
-
0
这是一个经典的 “环形问题线性化” 题目。核心在于处理环的首尾连接处。
以下是符合 NOIP/GESP 要求的 C++14 标准题解代码。
核心代码 (C++14 Standard)
/** * 题目:质粒切割计划 (Plasmid Cutting) * 算法:排序 + 线性扫描 (Sorting + Linear Scan) * 难度:GESP 5级 / CSP-J T2 */ #include <iostream> #include <vector> #include <algorithm> // 必须包含,用于 std::sort #include <cmath> // 用于 std::max using namespace std; int main() { // 1. I/O 提速:关闭同步,对于大数据量(N=10^5)输入很有必要 ios_base::sync_with_stdio(false); cin.tie(NULL); int L, N; if (!(cin >> L >> N)) return 0; // 使用 vector 动态数组存储切割点坐标 // 预分配内存是个好习惯,避免 push_back 扩容带来的开销 vector<int> cuts(N); for (int i = 0; i < N; ++i) { cin >> cuts[i]; } // --- 关键步骤一:排序 --- // 题目明确说“坐标不一定有序”,而我们需要计算相邻切点的距离。 // 所以必须先排序。复杂度 O(N log N)。 sort(cuts.begin(), cuts.end()); int max_len = 0; // --- 关键步骤二:计算线性部分的相邻距离 --- // 遍历排序后的数组,计算 cuts[i] 和 cuts[i-1] 之间的距离 for (int i = 1; i < N; ++i) { int current_segment = cuts[i] - cuts[i-1]; // 更新最大值 if (current_segment > max_len) { max_len = current_segment; } } // --- 关键步骤三:处理环形接口(易错点) --- // 环的最后一段是从 最后一个切点(cuts[N-1]) 到 0,再从 0 到 第一个切点(cuts[0]) // 距离公式 = (总周长 - 最后一个坐标) + 第一个坐标 int tail_to_head = (L - cuts[N-1]) + cuts[0]; if (tail_to_head > max_len) { max_len = tail_to_head; } // 输出结果 cout << max_len << endl; return 0; }
代码详细分析
1. 易错点与关键点注释
- 排序的重要性 (
std::sort):- 这是解题的前提。如果输入序列是
80, 10, 35,直接相减是没有任何物理意义的。必须变为10, 35, 80才能计算中间片段。
- 这是解题的前提。如果输入序列是
- 首尾相连的处理:
- 很多同学容易漏掉最后一段。
- 代码中的
(L - cuts[N-1])代表从最后一个点走到终点(即起点0)的距离。 + cuts[0]代表从起点0走到第一个点的距离。- 两段加起来才是跨越零点的那个片段长度。
- 数据类型:
- 题目中 ()。C++ 中
int的最大值约为 。所以使用int是安全的,不需要long long(除非涉及加和可能超过范围,但这里是求最大单个长度,肯定小于 )。
- 题目中 ()。C++ 中
2. 时间和空间复杂度分析
空间复杂度分析:
- 我们需要存储 个切割点坐标。
- 使用了
vector<int>,大小为 。 - 结论:。对于 ,内存占用极小(约 400KB),远低于竞赛通常的 256MB 限制。
时间复杂度分析思考过程:
- 输入阶段:读取 个整数,复杂度 。
- 排序阶段:这是程序中最耗时的部分。
std::sort使用的是快速排序/堆排序/插入排序的混合(Introsort),平均和最坏复杂度均为 。 - 扫描阶段:遍历数组计算差值,复杂度 。
- 总运算量:
- 主要由排序决定:。
- 当 时,。
- 运算次数 ,远小于 1秒限时(通常可处理 次运算)。
- 结论:。程序运行非常快。
3. 时间复杂度优化建议
对于本题, 已经是标准最优解。
- 为什么不能 ?
- 因为要在数轴上计算相邻距离,本质上依赖于元素的有序性。在基于比较的排序模型中,下界就是 。
- 非主流优化(基数排序):
- 如果非常极端地追求 ,可以使用基数排序(Radix Sort)。因为坐标是整数。
- 但是! 高达 ,如果用桶排序会爆内存。用基数排序虽然理论是 ,但常数较大且代码复杂,在 这个量级下,不一定比高度优化的
std::sort快多少,且容易写错。 - 竞赛建议:直接用
std::sort,稳准狠。
模拟数据示例(草稿纸验证)
输入:
100 3 80 10 35- 读入:
cuts = {80, 10, 35} - 排序:
cuts变为{10, 35, 80} - 循环扫描:
- :
cuts[1] - cuts[0]= 。max_len = 25。 - :
cuts[2] - cuts[1]= 。max_len = 45。
- :
- 首尾处理:
- ,
cuts.back() = 80,cuts.front() = 10。 - 距离 = 。
- 比较
max(45, 30),结果保持 45。
- ,
- 输出:45。正确。
- 排序的重要性 (
- 1
信息
- ID
- 19302
- 时间
- 1000ms
- 内存
- 32MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者