2 条题解
-
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可以显著节省时间。
- 对于 的数组读入,
-
总结思考:
- 什么时候用二分答案? 当题目求“最小的可能值”,且如果你给定一个答案,你能很容易地判定它是否合法(满足单调性)时。
同学,理解了从“挨个试”到“对半砍”的这种跳跃了吗?这就是算法的魅力!加油。
信息
- ID
- 19500
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者