2 条题解
-
0
这是一份包含完整数据生成与标程解题功能的 C++ 工具代码。
使用说明
- 功能:编译并运行此代码后,它会在当前目录下自动生成
1.in/1.out到10.in/10.out共20个文件。 - 覆盖度:数据点设计完全贴合 NOI/NOIP 考查标准,涵盖了:
- 边界情况:。
- 特殊数值:全0、全重复、正负混合。
- 溢出检测:极大值(和超过
int)与极小值。 - 压力测试: 且解非常多(稠密解)的情况。
- 安全性:使用了
mt19937_64生成高质量 64 位随机数,避免了rand()在大范围数据下的分布不均问题。
数据生成器代码 (C++14)
/** * Title: NOI 4Sum Data Generator & Solver * Author: OI Coach * Standard: C++14 * Description: Generates 10 sets of .in and .out files for the 4Sum problem. */ #include <iostream> #include <vector> #include <algorithm> #include <fstream> #include <string> #include <random> #include <chrono> #include <set> using namespace std; typedef long long ll; // 全局随机数引擎 mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count()); // 生成指定范围内的随机整数 [min, max] ll random_ll(ll min, ll max) { if (min > max) return min; uniform_int_distribution<ll> dist(min, max); return dist(rng); } // ========================================== // 标准题解逻辑 (用于生成 .out 文件) // ========================================== void solve(string in_filename, string out_filename) { ifstream cin(in_filename); ofstream cout(out_filename); if (!cin.is_open() || !cout.is_open()) { cerr << "Error opening files: " << in_filename << " / " << out_filename << endl; return; } int n; ll target; if (!(cin >> n >> target)) return; vector<ll> nums(n); for (int i = 0; i < n; ++i) { cin >> nums[i]; } // 核心逻辑开始 sort(nums.begin(), nums.end()); if (n < 4) return; // 无输出 for (int i = 0; i < n - 3; ++i) { if (i > 0 && nums[i] == nums[i - 1]) continue; if (nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) break; if (nums[i] + nums[n - 3] + nums[n - 2] + nums[n - 1] < target) continue; for (int j = i + 1; j < n - 2; ++j) { if (j > i + 1 && nums[j] == nums[j - 1]) continue; if (nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target) break; if (nums[i] + nums[j] + nums[n - 2] + nums[n - 1] < target) continue; int left = j + 1; int right = n - 1; while (left < right) { ll sum = nums[i] + nums[j] + nums[left] + nums[right]; if (sum == target) { cout << nums[i] << " " << nums[j] << " " << nums[left] << " " << nums[right] << "\n"; while (left < right && nums[left] == nums[left + 1]) left++; while (left < right && nums[right] == nums[right - 1]) right--; left++; right--; } else if (sum < target) { left++; } else { right--; } } } } // 核心逻辑结束 cin.close(); cout.close(); cout << "Generated: " << out_filename << endl; } // ========================================== // 数据生成逻辑 // ========================================== void generate_data(int case_id) { string in_file = to_string(case_id) + ".in"; string out_file = to_string(case_id) + ".out"; ofstream fout(in_file); int n; ll target; vector<ll> nums; // 根据测试点 ID 定制数据策略 switch (case_id) { case 1: // 边界测试:N < 4 n = 3; target = 10; for(int i=0; i<n; ++i) nums.push_back(random_ll(-10, 10)); break; case 2: // 基础测试:N = 4,恰好有解 n = 4; nums = {1, 5, -2, 8}; target = 1 + 5 - 2 + 8; // = 12 break; case 3: // 基础测试:N = 4,无解 n = 4; nums = {1, 2, 3, 4}; target = 100; break; case 4: // 特殊值:全 0 n = 20; target = 0; for(int i=0; i<n; ++i) nums.push_back(0); break; case 5: // 重复元素处理:大量重复数字 n = 50; target = 8; for(int i=0; i<n; ++i) nums.push_back(2); // nums 中全是 2,结果应该只有一行 2 2 2 2 break; case 6: // 常规随机小数据:N=50,范围小 n = 50; for(int i=0; i<n; ++i) nums.push_back(random_ll(-100, 100)); // 随机选4个数的和作为target,保证大概率有解 target = nums[0] + nums[10] + nums[20] + nums[30]; break; case 7: // 溢出测试 (MAX):正数大值 // 数值接近 10^9,4个数相加会爆 int n = 100; for(int i=0; i<n; ++i) nums.push_back(random_ll(900000000, 1000000000)); // 构造解 target = nums[0] + nums[1] + nums[2] + nums[3]; break; case 8: // 溢出测试 (MIN):负数大值 // 数值接近 -10^9 n = 100; for(int i=0; i<n; ++i) nums.push_back(random_ll(-1000000000, -900000000)); target = nums[0] + nums[1] + nums[2] + nums[3]; break; case 9: // 极限数据:N=200,大范围随机,可能有解也可能无解 n = 200; target = random_ll(-1000000, 1000000); for(int i=0; i<n; ++i) nums.push_back(random_ll(-1000000000, 1000000000)); break; case 10: // 压力测试:N=200,稠密解 (解非常多) // 范围很小 [-10, 10],导致大量组合和为 target n = 200; target = 0; for(int i=0; i<n; ++i) nums.push_back(random_ll(-10, 10)); break; } // 写入输入文件 fout << n << " " << target << endl; for (int i = 0; i < n; ++i) { fout << nums[i] << (i == n - 1 ? "" : " "); } fout << endl; fout.close(); cout << "Generated: " << in_file << endl; // 调用解题逻辑生成输出文件 solve(in_file, out_file); } int main() { // 生成 1 到 10 个测试点 for (int i = 1; i <= 10; ++i) { generate_data(i); } cout << "All 10 test cases generated successfully." << endl; return 0; }必读:测试点详细设计思路
- TestCase 1 (边界): 。测试程序是否在 时能正确退出,不进行非法下标访问。
- TestCase 2 (基础): 且恰好有解。测试最基本的逻辑正确性。
- TestCase 3 (基础): 但无解。测试无解时的输出(应为空)。
- TestCase 4 (特殊值): 全为0,目标为0。测试去重逻辑,标准输出应仅为一行
0 0 0 0。 - TestCase 5 (重复): 全为2,目标为8。测试大量重复元素下的去重和指针移动效率。
- TestCase 6 (常规): ,混合正负数。通过选取数组中实际存在的4个数之和作为 Target,保证了至少有一组解,用于常规逻辑验证。
- TestCase 7 (溢出): 元素在 级别。 超过了 32位有符号整数范围。测试代码是否使用了
long long存储中间结果。 - TestCase 8 (溢出): 元素在 级别。测试负数溢出情况。
- TestCase 9 (综合): ,全范围随机。模拟随机情况,测试剪枝效率。
- TestCase 10 (高压): ,但数值范围极小 。这会产生极其大量的合法四元组(稠密图效果),这是最考验程序去重逻辑效率和 I/O 速度的测试点。
- 功能:编译并运行此代码后,它会在当前目录下自动生成
-
0
你好!作为金牌教练,我为你准备了这份完全符合 NOIP/NOI 竞赛标准的 C++14 题解代码。
这份代码不仅通过了算法逻辑,还特别强调了在赛场上必须注意的数据类型溢出、边界检查和常数级优化(剪枝),这些往往是决定你能否拿满分的细节。
NOIP 标准题解代码 (C++14)
/** * 题目: [NOI 模拟] 四数之和 (4Sum) * 语言: C++14 * 算法: 排序 + 双指针 + 剪枝优化 * 时间复杂度: O(N^3) * 空间复杂度: O(log N) (排序栈空间) */ #include <iostream> #include <vector> #include <algorithm> using namespace std; // 建议使用 long long 替代 int,防止加法运算溢出 // 题目中虽说单个元素是 10^9,但在计算和时 4 * 10^9 会超过 int (约 2.1 * 10^9) 的范围 typedef long long ll; int main() { // 【NOI 技巧】关闭同步流,加速 cin/cout,防止大数据量 IO 超时 ios::sync_with_stdio(false); cin.tie(nullptr); int n; ll target; // 输入检查,增强鲁棒性 if (!(cin >> n >> target)) return 0; vector<ll> nums(n); for (int i = 0; i < n; ++i) { cin >> nums[i]; } // 【关键点 1】排序 // 排序是使用双指针和去重逻辑的前提 sort(nums.begin(), nums.end()); // 特判:如果数据量不足 4 个,直接结束,避免下标越界 if (n < 4) return 0; // 第一层循环:固定第一个数 nums[i] for (int i = 0; i < n - 3; ++i) { // 【关键点 2】第一层去重 // 如果当前数和上一个数相同,跳过,因为上一个数已经覆盖了所有组合 if (i > 0 && nums[i] == nums[i - 1]) continue; // 【优化技巧 1】最小性剪枝 (Min Pruning) // 如果当前数 + 紧跟的三个最小数 > target,说明后续无论怎么选都大了,直接结束本层循环 // 这里的加法必须用 long long if (nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) break; // 【优化技巧 2】最大性剪枝 (Max Pruning) // 如果当前数 + 最大的三个数 < target,说明当前数太小了,配不上 target,跳过看下一个更大的 nums[i] if (nums[i] + nums[n - 3] + nums[n - 2] + nums[n - 1] < target) continue; // 第二层循环:固定第二个数 nums[j] for (int j = i + 1; j < n - 2; ++j) { // 【关键点 3】第二层去重 // 注意范围限定:j > i + 1,即 j 不是刚开始的那一次 if (j > i + 1 && nums[j] == nums[j - 1]) continue; // 【优化技巧 3】第二层剪枝 // 逻辑同上,针对 nums[j] 进行判断 if (nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target) break; if (nums[i] + nums[j] + nums[n - 2] + nums[n - 1] < target) continue; // 【关键点 4】双指针法 (Two Pointers) // 将 O(N^2) 的暴力搜索转化为 O(N) 的线性扫描 int left = j + 1; int right = n - 1; while (left < right) { ll sum = nums[i] + nums[j] + nums[left] + nums[right]; if (sum == target) { // 找到答案,直接输出 cout << nums[i] << " " << nums[j] << " " << nums[left] << " " << nums[right] << "\n"; // 【关键点 5】双指针去重 // 记录当前值,指针跳过所有重复值,防止输出重复的四元组 while (left < right && nums[left] == nums[left + 1]) left++; while (left < right && nums[right] == nums[right - 1]) right--; // 移动到下一个不同的位置 left++; right--; } else if (sum < target) { // 和太小,左指针右移变大 left++; } else { // 和太大,右指针左移变小 right--; } } } } return 0; }
复杂度分析与思考过程
作为教练,我希望你能不仅会写代码,还能在纸上推导出它的性能表现。
1. 时间复杂度分析
-
推导过程:
- 排序:
std::sort的时间复杂度是 。 - 循环结构:
- 第一层循环
i从0到N。 - 第二层循环
j从i+1到N。 - 内层是一个
while双指针循环,left和right从两端向中间逼近,每个元素最多被访问一次,复杂度为 。
- 第一层循环
- 总计:总时间复杂度约为 。
- 排序:
-
数据规模验证:
- 题目给定的 。
- 最坏计算次数: (8百万次)。
- 竞赛标准:通常 C++ 程序 1秒钟可以处理 (1亿次) 运算。
- 结论:,即使在不进行剪枝的最坏情况下,该算法也能轻松在 100ms 内通过,绝对安全。
2. 空间复杂度分析
- 分析:
- 我们只使用了几个变量 (
i, j, left, right, sum),这是 。 - 但是,
std::sort并不是完全原地的,C++ 的introsort实现通常需要 的栈空间来进行递归。 - 如果题目要求存储答案再输出(而不是像上面代码那样直接打印),还需要额外的空间存储答案。
- 我们只使用了几个变量 (
- 结论:辅助空间复杂度 (用于排序)。
时间复杂度优化建议(剪枝详解)
在 较小(如 200)时, 足够。但如果 增大到 1000 左右,或者针对特殊构造的数据,我们需要利用剪枝 (Pruning) 来大幅降低实际运行时间。
代码中已经集成了四个核心剪枝点:
-
Min Check (下界剪枝):
if (nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target) break;- 思考:当前的
nums[i]已经是能选到的最小的数了,如果连紧跟它后面最小的三个数加起来都超过了target,那后面无论怎么选只会更大,完全没有必要继续尝试i及其后面的循环了。直接break掉第一层循环。
-
Max Check (上界剪枝):
if (nums[i] + nums[n-3] + nums[n-2] + nums[n-1] < target) continue;- 思考:当前的
nums[i]加上数组里最大的三个数都还不到target,说明这个nums[i]太小了,没有任何组合能满足条件。应该直接continue尝试更大的nums[i+1]。
同理,这两个剪枝策略也应用在了第二层
j的循环中。实战效果:加上这几行剪枝代码,对于随机数据,算法的期望运行时间会大幅缩短,甚至接近 的表现,这是体现竞赛选手“常数优化”能力的关键点。
-
- 1
信息
- ID
- 19343
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者