2 条题解
-
0
你好!我是金牌教练。为了方便你创建 OJ(在线判题系统)测试数据,我为你编写了一个集成化的数据生成器与标程(Generator & Solver)。
这个程序将自动生成 10 组符合 NOI 规范的测试点(
1.in/out~10.in/out)。为了体现竞赛的梯度,我对数据进行了精细化分布,以区分暴力枚举、优先队列模拟和二分答案三种复杂度的解法。一、 测试点设计矩阵
测试点 规模 规模 特点 预期通过算法 1-2 100 极小规模 全部算法 3-4 1,000 中等规模 模拟、二分 5 1 边界:只需一趟 二分 6 1 边界:只有一辆车 7 均匀分布: 均相同 8 极限稀疏: 极大 9-10 极限综合随机数据 仅限二分
二、 数据生成器 C++ 代码
该程序采用 C++14 标准,包含了生成输入数据和利用标准二分算法生成输出答案的完整逻辑。
#include <iostream> #include <fstream> #include <vector> #include <string> #include <random> #include <algorithm> #include <ctime> using namespace std; typedef long long ll; /** * 标准答案:二分查找 + 贪心验证 * 时间复杂度: O(N log(min_time * totalTrips)) */ bool check(ll mid, ll totalTrips, int n, const vector<int>& time) { ll count = 0; for (int i = 0; i < n; i++) { count += (mid / time[i]); // 剪枝点:防止累加和超过 long long 范围 // 同时也符合“只要大于等于就可行”的逻辑 if (count >= totalTrips) return true; } return count >= totalTrips; } ll solve(int n, ll totalTrips, const vector<int>& time) { int min_t = time[0]; for (int x : time) if (x < min_t) min_t = x; ll low = 1; // 右边界最坏情况:由最慢的一辆车跑完所有趟数 // 此处设为 min_t * totalTrips 是为了更紧确,防止 mid 计算溢出 ll high = (ll)min_t * totalTrips; ll ans = high; while (low <= high) { ll mid = low + (high - low) / 2; if (check(mid, totalTrips, n, time)) { ans = mid; high = mid - 1; } else { low = mid + 1; } } return ans; } /** * 数据生成逻辑 */ void generate_test_cases() { mt19937_64 rng(time(0)); for (int t = 1; t <= 10; t++) { string in_name = to_string(t) + ".in"; string out_name = to_string(t) + ".out"; ofstream fout(in_name); ofstream fans(out_name); int n; ll totalTrips; vector<int> time_arr; // 根据测试点分布设定规模 if (t <= 2) { // 暴力解法可通过 n = 100; totalTrips = 100; } else if (t <= 4) { // 模拟解法可通过 n = 1000; totalTrips = 100000; } else if (t == 5) { // 边界:趟数极少 n = 100000; totalTrips = 1; } else if (t == 6) { // 边界:车数极少 n = 1; totalTrips = 10000000; } else { // 极限规模 n = 100000; totalTrips = 10000000; } // 填充 time 数组 if (t == 7) { // 均匀分布 int fixed_t = rng() % 10000000 + 1; for (int i = 0; i < n; i++) time_arr.push_back(fixed_t); } else if (t == 8) { // 极限大值 for (int i = 0; i < n; i++) time_arr.push_back(10000000); } else { // 随机分布 for (int i = 0; i < n; i++) time_arr.push_back(rng() % 10000000 + 1); } // 1. 写入输入文件 fout << n << " " << totalTrips << "\n"; for (int i = 0; i < n; i++) { fout << time_arr[i] << (i == n - 1 ? "" : " "); } fout << endl; // 2. 运行标程并写入输出文件 ll result = solve(n, totalTrips, time_arr); fans << result << endl; cout << "Generated Case " << t << " [N=" << n << ", T=" << totalTrips << "] OK." << endl; fout.close(); fans.close(); } } int main() { cout << "Starting data generation..." << endl; generate_test_cases(); cout << "All 10 cases generated successfully." << endl; return 0; }
三、 生成器关键点与优化说明
-
文件大小控制:
- 本题最大的开销在于第 7-10 组数据的
time数组。 - ,每个数字最大 8 位(含空格),单个
.in文件约为 。 - 全部 10 组数据的总大小(压缩前)约为 5MB。如果你需要严格控制在 2MB 以内,建议将后几组的 改为 ,这依然能有效卡掉 的暴力,并对 的模拟造成压力。
- 本题最大的开销在于第 7-10 组数据的
-
防止溢出(Exception Control):
- 计算溢出:在
check函数中,我们使用了if (count >= totalTrips) return true;进行剪枝。这不仅能节约计算时间,更重要的是防止了count累加超过long long的最大值(),因为在极端情况下( 辆车,每辆 ),不剪枝会导致数据错误。 - 二分溢出:
high动态设为min_t * totalTrips。若设为固定的1e18,虽然不会溢出long long,但会让check函数内部计算更易发生越界。
- 计算溢出:在
-
区分算法复杂度:
- 暴力枚举时间:在测试点 1-4 之后会因为 达到 而彻底失效。
- 堆/优先队列模拟:在 的情况下,模拟需要 次堆操作,耗时约 2-3 秒以上,在 NOI 严格的 1 秒限时下通常无法全部 AC。
-
非递归写法:
- 标程中的二分查找采用
while循环迭代实现,确保不会因为递归深度过大(虽然 的深度本就不深)导致系统栈溢出。
- 标程中的二分查找采用
四、 如何使用
- 编译运行此
generator.cpp。 - 将生成的
1.in~10.out存入测试数据目录。 - 设置题目时限为 1.0s,内存限制为 256MB。
-
-
0
你好!我是金牌教练。在处理这种“完成任务所需的最少时间”问题时,理解从“模拟过程”到“验证性质”的思维转变是进阶 NOI 的必经之路。
下面我为你演示这道题的代码演进过程。
版本 1:暴力枚举时间 (Linear Search)
思路: 最直观的想法:既然求最少时间,我们就从 开始往后试。对于每一个 ,计算所有公交车完成的旅途总数,一旦总数 ,当前的 就是答案。
复杂度分析:
- 时间复杂度:。其中 是最终答案,最大可能达到 。
- 空间复杂度:。
- 评价:只能通过 和 极小的测试点(约 10% 的分数)。
/* * 暴力枚举法:从 1 分钟开始尝试 * 适用场景:n, totalTrips 极小的情况 */ #include <iostream> #include <vector> using namespace std; typedef long long ll; int main() { int n; ll totalTrips; if (!(cin >> n >> totalTrips)) return 0; vector<int> t(n); for (int i = 0; i < n; i++) cin >> t[i]; // 从 1 开始暴力枚举时间 for (ll time_spent = 1; ; time_spent++) { ll current_trips = 0; for (int i = 0; i < n; i++) { current_trips += (time_spent / t[i]); } if (current_trips >= totalTrips) { cout << time_spent << endl; break; } } return 0; }
版本 2:优先队列模拟 (Min-Heap Simulation)
思路: 我们可以看作是“哪个公交车先跑完下一趟”。用一个小根堆维护
{下一次完成时间, 车的单次耗时}。每次从堆顶取出完成时间最早的车,旅途数 +1,然后将这辆车完成下一趟的时间再次放入堆中。复杂度分析:
- 时间复杂度:。
- 空间复杂度:。
- 评价:当 时,运算量约 ,在 1s 时限内非常危险,可能拿到 40-60% 的分数。
/* * 优先队列模拟:每次找最早完成下一趟的车 * 易错点:这里记录的是“绝对完成时刻”,需要用 long long */ #include <iostream> #include <vector> #include <queue> using namespace std; typedef long long ll; struct Bus { ll next_finish; // 该公交车下一次跑完的绝对时刻 int cost; // 跑一趟需要的固定时间 bool operator>(const Bus& other) const { return next_finish > other.next_finish; } }; int main() { int n; ll total; cin >> n >> total; priority_queue<Bus, vector<Bus>, greater<Bus>> pq; for (int i = 0; i < n; i++) { int c; cin >> c; pq.push({(ll)c, c}); } ll count = 0; ll last_time = 0; while (count < total) { Bus top = pq.top(); pq.pop(); count++; last_time = top.next_finish; // 这辆车跑完后,立刻开始下一趟 pq.push({top.next_finish + top.cost, top.cost}); } cout << last_time << endl; return 0; }
版本 3:最优解 - 二分答案 (Binary Search on Answer)
思路: 如果我们知道时间 ,可以很容易算出一共能跑多少趟。且时间 越多,趟数越多,具有单调性。因此我们直接二分查找这个 。
复杂度分析:
- 时间复杂度:。
- 空间复杂度:。
- 评价:运算量约 ,NOI 满分解法。
/* * 二分答案:通过验证时间 T 是否可行来逼近最优解 * 关键点 1:确定右边界 High = 最小时间 * 总趟数 (最坏情况) * 关键点 2:Check 函数中累加 Sum 时,若 Sum >= target 立即停止,防止 ll 溢出 */ #include <iostream> #include <vector> #include <algorithm> using namespace std; typedef long long ll; int n; ll totalTrips; int bus_time[100005]; bool check(ll mid) { ll sum = 0; for (int i = 0; i < n; i++) { sum += (mid / bus_time[i]); // 关键优化:已经达到目标就直接返回,防止累加过大导致 long long 溢出 if (sum >= totalTrips) return true; } return sum >= totalTrips; } int main() { // 提速:大数据量建议开启 ios::sync_with_stdio(false); cin.tie(0); if (!(cin >> n >> totalTrips)) return 0; int min_val = 1e9; for (int i = 0; i < n; i++) { cin >> bus_time[i]; if (bus_time[i] < min_val) min_val = bus_time[i]; } ll low = 1; // 右边界:用最快的车完成所有任务所需的时间,这是一个安全的上界 ll high = (ll)min_val * totalTrips; ll ans = high; while (low <= high) { ll mid = low + (high - low) / 2; if (check(mid)) { ans = mid; // 时间足够,尝试更短的时间 high = mid - 1; } else { low = mid + 1; // 时间不够,必须延长 } } cout << ans << endl; return 0; }
时间复杂度优化建议
-
数据溢出 (NOI 重灾区):
1e7 * 1e7 = 1e14。这已经超过了int的 。- 在
check函数中,如果不加if (sum >= totalTrips) return true;,当 时,sum会达到 ,甚至超过unsigned long long的上限。提前剪枝是保证正确性的核心。
-
右边界的选择:
- 不要盲目设
1e18。虽然二分是对数级别的,但更紧确的边界(如min_element * totalTrips)可以减少几次迭代,并降低溢出风险。
- 不要盲目设
-
I/O 优化:
- 对于 的数组读入,
cin较慢。使用ios::sync_with_stdio(false)或scanf可以显著节省时间。
- 对于 的数组读入,
-
总结思考:
- 什么时候用二分答案? 当题目求“最小的可能值”,且如果你给定一个答案,你能很容易地判定它是否合法(满足单调性)时。
同学,理解了从“挨个试”到“对半砍”的这种跳跃了吗?这就是算法的魅力!加油。
- 1
信息
- ID
- 19500
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者