2 条题解
-
0
为了方便你创建 OJ(Online Judge)测试数据,我为你编写了一个基于 C++14 标准的数据生成器。该生成器会循环生成 10 组测试点,涵盖了从最小规模()到最大规模()以及包含负数、大整数等边界情况。
数据生成器代码 (gen.cpp)
该程序会自动在当前目录下生成
1.in,1.out到10.in,10.out。#include <iostream> #include <fstream> #include <vector> #include <string> #include <set> #include <random> #include <algorithm> using namespace std; // 解决逻辑:用于生成标准输出 (.out) void generate_output(int n, const vector<int>& nums, const string& out_name) { ofstream fout(out_name); // 使用位运算生成子集,确保逻辑稳健 for (int i = 0; i < (1 << n); ++i) { bool first = true; for (int j = 0; j < n; ++j) { if ((i >> j) & 1) { if (!first) fout << " "; fout << nums[j]; first = false; } } fout << "\n"; // 输出子集,空集则输出一个空行 } fout.close(); } int main() { // 准备不同规模的 n int n_list[] = {1, 2, 3, 5, 8, 10, 12, 14, 16, 16}; // 随机数生成器 mt19937 rng(1337); // 固定种子保证数据可重复 uniform_int_distribution<int> dist(-1e9, 1e9); for (int t = 1; t <= 10; ++t) { int n = n_list[t-1]; string in_name = to_string(t) + ".in"; string out_name = to_string(t) + ".out"; // 1. 生成输入数据 ofstream fin(in_name); fin << n << "\n"; set<int> unique_elements; vector<int> nums; while (unique_elements.size() < n) { int val = dist(rng); if (unique_elements.find(val) == unique_elements.end()) { unique_elements.insert(val); nums.push_back(val); } } // 特殊处理:样例数据放入第3个测试点 if (t == 3) { nums = {1, 2, 3}; } for (int i = 0; i < n; ++i) { fin << nums[i] << (i == n - 1 ? "" : " "); } fin << "\n"; fin.close(); // 2. 生成对应的标准输出 generate_output(n, nums, out_name); cout << "Generated Test Case " << t << ": n=" << n << endl; } return 0; }
测试点设计说明
测试点编号 规模 数据特征 目的 1 最小规模边界 检查对仅包含一个元素集合的处理 2 极小规模 检查基础逻辑 3 样例数据 确保通过题目给出的样例 4-5 中等规模 验证递归深度和路径保存 6-8 较大规模 验证程序的时间常数 9 最大规模 满负荷测试,检查是否超时( 行输出) 10 包含大负数 确保 int类型处理正确且数据不重复
如何使用此生成器
- 环境准备:确保你的环境支持 C++11 或 C++14(NOI 现行标准)。
- 编译:
g++ gen.cpp -o gen -O2 -std=c++14 - 运行:
执行后,文件夹下会出现./gen1.in,1.out...10.out共 20 个文件。
OJ 配置建议
- 比较器 (Checker):由于题目要求“子集顺序不作要求”,如果你的 OJ 支持 Special Judge (SPJ),建议编写一个简单的 SPJ。
- 如果不使用 SPJ:请告知选手必须按照某种特定顺序输出(例如位运算顺序或 DFS 顺序),或者在后台使用
diff -b -B -w忽略行末空格和空行差异。
- 如果不使用 SPJ:请告知选手必须按照某种特定顺序输出(例如位运算顺序或 DFS 顺序),或者在后台使用
- 输出限制:注意 时输出文件约为几兆字节,请确保 OJ 的
Output Limit设置在 128MB 以上,避免因输出文件过大导致 OLE (Output Limit Exceeded)。 - 时间限制:由于主要耗时在 I/O(打印 行),建议时限设为 1.0s,内存设为 256MB。
-
0
在 NOI 竞赛中,虽然 的数据范围非常小,但理解从“搜索”到“状态压缩”的演变是极其重要的。下面我将为你演示从最基础的递归回溯,到竞赛中更常用的位运算(状态压缩)的代码实现。
版本一:基础回溯(选与不选决策树)
这是最直观的 DFS,每个元素只有“选”或“不选”两种状态。
思考过程:
- 时间复杂度:每个位置有两种选择,总共有 个状态。每个状态在递归边界需要 的时间进行输出。总时间 。
- 空间复杂度:递归深度为 ,临时存储路径的
path空间为 。
#include <iostream> #include <vector> using namespace std; int n; int nums[20]; vector<int> path; // u 表示当前处理到第几个元素 void dfs(int u) { // 易错点:递归边界,当处理完所有元素时输出 if (u == n) { for (int i = 0; i < path.size(); ++i) { cout << path[i] << (i == path.size() - 1 ? "" : " "); } cout << endl; // 输出空行代表空集或行末换行 return; } // 分支 1:不选择当前的 nums[u] dfs(u + 1); // 分支 2:选择当前的 nums[u] path.push_back(nums[u]); dfs(u + 1); // 关键点:回溯,恢复现场,保证对后续决策不产生影响 path.pop_back(); } int main() { // 竞赛建议:优化输入输出效率 ios::sync_with_stdio(false); cin.tie(0); if (!(cin >> n)) return 0; for (int i = 0; i < n; ++i) { cin >> nums[i]; } dfs(0); return 0; }
版本二:组合式搜索(按起始位置枚举)
这种写法在处理“组合”问题时更常见,通过限制下一次搜索的起始位置来避免重复。
思考过程:
- 时间复杂度:。
- 空间复杂度:。
- 差异:这种方式每进入一次
dfs函数就会直接记录一个结果,而不是等到递归到底部。
#include <iostream> #include <vector> using namespace std; int n; int nums[20]; vector<int> path; void dfs(int start) { // 每进入一个节点,当前的 path 都是一个合法的子集 for (int i = 0; i < path.size(); ++i) { cout << path[i] << (i == path.size() - 1 ? "" : " "); } cout << "\n"; // 关键点:从 start 开始往后找,避免选到重复组合 for (int i = start; i < n; ++i) { path.push_back(nums[i]); dfs(i + 1); path.pop_back(); // 回溯 } } int main() { ios::sync_with_stdio(false); cin.tie(0); cin >> n; for (int i = 0; i < n; ++i) cin >> nums[i]; dfs(0); return 0; }
版本三:最优解——位运算状态压缩 (Bitmask)
在 NOI 竞赛中,如果 较小(通常 ),直接使用位运算枚举子集是最简洁且常数最小的方法。
思考过程:
- 核心逻辑:一个长度为 的集合有 个子集。我们可以用 到 的整数来代表每个子集。
- 位运算技巧:整数 的二进制表示中,如果第 位是 ,则代表选中
nums[j]。 - 时间复杂度:。
- 空间复杂度:(除去存储输入的数组),没有递归开销。
#include <iostream> using namespace std; int main() { int n; int nums[20]; if (!(cin >> n)) return 0; for (int i = 0; i < n; ++i) cin >> nums[i]; // 1 << n 相当于 2的n次方 // i 遍历从 0 到 2^n - 1 的每一个状态 for (int i = 0; i < (1 << n); ++i) { bool first = true; for (int j = 0; j < n; ++j) { // 易错点:检查第 j 位是否为 1 // (i >> j) & 1 是判断 i 的二进制第 j 位是否为 1 的常用写法 if ((i >> j) & 1) { if (!first) cout << " "; cout << nums[j]; first = false; } } cout << "\n"; // 每个状态对应一行 } return 0; }
复杂度分析与优化建议总结
1. 时间复杂度分析
- 计算状态数:对于 个元素,每个元素有“在”或“不在”两种状态,总状态数为 。
- 单次操作:无论是递归还是位运算,构造一个子集并输出都需要遍历 个元素。
- 总复杂度:。
- 数据量评估:当 时,,乘以 约为 次操作。在 1.0s 时限内绰绰有余(NOI 机器通常每秒处理 次运算)。
2. 空间复杂度分析
- 递归版:,主要是系统栈空间和
path数组。 - 位运算版:,不需要额外空间。
- 内存建议:在 NOI 中,如果 很大(如 ),不要试图把所有子集存在
vector<vector<int>>里,否则会造成 MLE (Memory Limit Exceeded)。
3. 性能优化建议
- I/O 优化:子集问题的输出量非常大。使用
cout << endl会频繁刷新缓冲区,导致效率低下。建议使用\n或者printf。 - 位运算优势:位运算版本由于没有函数调用的压栈开销,常数项极小。在 接近 20-22 的压测环境下,位运算优势明显。
- 剪枝思考:本题要求输出所有子集,因此无法剪枝。但如果是“求和为 K 的子集”,则可以在递归中加入
if (current_sum > K) return;这样的剪枝逻辑。
教练提示:在考场上,如果 很小,位运算是你的首选,因为它代码量少且不易出错。如果题目要求按特定顺序(如字典序)输出,记得先对原数组
sort一下。
- 1
信息
- ID
- 19423
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 3
- 已通过
- 1
- 上传者