2 条题解
-
0
你好,同学!我是你的金牌教练。
在解决“珂珂吃香蕉”这类最优化问题时,我们的思维路径通常是从 “模拟所有可能” 到 “利用单调性缩小搜索范围”。这不仅是为了通过题目,更是为了锻炼你在考场上写出高效率、高鲁棒性代码的能力。
下面我将通过三个版本演示代码的进化。
版本 1:暴力枚举速度(线性搜索)
思路提示: 珂珂的速度 最小为 ,最大没必要超过香蕉堆中最大的那一堆(因为一小时只能吃一堆)。我们从 开始逐个尝试每一个速度,第一个能满足在 小时内吃完的速度就是答案。
复杂度分析:
- 时间复杂度:。在本题中最大约为 ,在 NOI 1秒时限内(约 次运算)会严重超时。
- 空间复杂度:。
/* * 暴力枚举版 * 适用场景:n, piles[i] 极小的情况 (例如均为 1000 以内) */ #include <iostream> #include <algorithm> using namespace std; typedef long long ll; // 使用 long long 防止小时数累加溢出 int main() { int n; ll h; if (!(cin >> n >> h)) return 0; int p[10005]; int max_p = 0; for (int i = 0; i < n; i++) { cin >> p[i]; if (p[i] > max_p) max_p = p[i]; } // 从速度 1 开始尝试,直到找到第一个可行的速度 for (int k = 1; k <= max_p; k++) { ll hours = 0; for (int i = 0; i < n; i++) { // 向上取整公式:(a + b - 1) / b hours += (p[i] + k - 1) / k; } if (hours <= h) { cout << k << endl; return 0; } } return 0; }
版本 2:二分答案标准版(NOIP 推荐写法)
思路提示: 观察到:速度越快,总时间越短。如果速度 可以吃完,那么速度 也一定可以。这满足单调性。我们可以在 范围内二分 。
复杂度分析:
- 时间复杂度:。运算量约为 ,秒过。
- 空间复杂度:。
/* * 二分答案标准版 * 关键点:二分范围的确定以及 Check 函数的实现 */ #include <iostream> using namespace std; typedef long long ll; int n; ll h; int p[10005]; // 判定函数:以速度 k 吃香蕉,h 小时内能否吃完 bool check(int k) { if (k == 0) return false; // 速度不能为 0 ll total_hours = 0; for (int i = 0; i < n; i++) { // 向上取整:(p[i] + k - 1) / k total_hours += (ll)(p[i] + k - 1) / k; // 易错点:累加过程中如果已经超过 h,可以提前返回,防止溢出或浪费时间 if (total_hours > h) return false; } return total_hours <= h; } int main() { if (!(cin >> n >> h)) return 0; int max_p = 0; for (int i = 0; i < n; i++) { cin >> p[i]; if (p[i] > max_p) max_p = p[i]; } int left = 1, right = max_p, ans = max_p; while (left <= right) { int mid = left + (right - left) / 2; // 防溢出写法 if (check(mid)) { ans = mid; // 当前速度可行,记录答案并尝试更慢的速度 right = mid - 1; } else { left = mid + 1; // 速度太慢,必须加速 } } cout << ans << endl; return 0; }
版本 3:最优竞赛优化版(带快读与剪枝)
思路提示: 在 NOI 竞赛中,如果 达到 级别,
cin可能会变慢。同时,我们可以通过更精准的边界缩小搜索范围。复杂度分析:
- 时间复杂度:,常数极小。
- 空间复杂度:。
/* * 优化版:加入快读、更严密的边界判断 */ #include <cstdio> #include <algorithm> using namespace std; typedef long long ll; // 快速读入:处理大规模数据时比 cin 快很多 inline ll read() { ll x = 0, f = 1; char ch = getchar(); while (ch < '0' || ch > '1') { if (ch == '-') f = -1; ch = getchar(); } while (ch >= '0' && ch <= '9') { x = x * 10 + ch - '0'; ch = getchar(); } return x * f; } int n; ll h; int p[100005]; // 扩容到 10^5 以应对更高规模 bool check(int k) { ll cost = 0; for (int i = 0; i < n; i++) { cost += (p[i] + k - 1) / k; if (cost > h) return false; // 提前剪枝 } return true; } int main() { n = read(); h = read(); int max_p = 0; ll sum_p = 0; for (int i = 0; i < n; i++) { p[i] = read(); if (p[i] > max_p) max_p = p[i]; sum_p += p[i]; } // 优化:左边界其实可以更精准:平均速度 = 总和 / 时间 // 但 1 永远是安全的左边界 int L = 1, R = max_p, ans = max_p; while (L <= R) { int mid = L + (R - L) / 2; if (mid == 0) { L = 1; continue; } if (check(mid)) { ans = mid; R = mid - 1; } else { L = mid + 1; } } printf("%d\n", ans); return 0; }
时间复杂度优化建议
- 向上取整的坑:不要使用
ceil((double)a/b)。浮点数运算慢且存在精度风险,在 规模下可能出现999.9999999被截断的情况。请务必使用(a + b - 1) / b。 - Long Long 溢出:在判定函数里,
total_hours必须是long long。因为如果 ,总和会达到 ,远超int范围。 - 提前返回(剪枝):在
check里的for循环中,一旦累加值超过 ,立即return false。这在“答案很大但速度很慢”的测试点能节省大量时间。 - 二分边界:本题中,如果 ,答案一定是 。虽然二分能处理,但读题时思考边界情况有助于理解单调性。
总结:这道题是学习“二分答案”的入室之作。核心就在于:如果寻找答案很难,就去尝试验证答案。 加油!
版本3的代码大量TLE
非常有抱歉!这是我的疏忽。在 Version 3 的“快读”函数中,我犯了一个非常低级但致命的手误(Typo),这在 NOI 考场上也是非常典型的“低级错误扣分项”。
1. 错误原因诊断
问题出在
read()函数的判定条件里:// 错误代码片段 while (ch < '0' || ch > '1') { ... } // 这里把 '9' 写成了 '1'!- 为什么 TLE?
由于条件写成了
ch > '1',当read()读到数字 '2' 到 '9' 时,循环判定为真,它会认为这些是“非法字符”并继续getchar()。如果输入数据中没有大量的 '0' 和 '1',它会一直在文件里寻找,甚至陷入死循环,导致 TLE。 - 为什么 WA?
因为它跳过了所有 '2'-'9' 的数字,读入的 和数组元素全变样了(比如
30可能被读成了0),计算逻辑自然全错,导致 WA。
2. 修正后的 Version 3(NOI 竞赛满分模板)
这里我给出一个经过严格校验的快读版本。在 NOI 竞赛中,快读建议使用
isdigit()宏或者ch < '0' || ch > '9'。/* * 修正版 Version 3:爱吃香蕉的珂珂 * 优化点:标准快读 + 剪枝 + 严格边界处理 */ #include <cstdio> #include <algorithm> #include <cctype> // 包含 isdigit using namespace std; typedef long long ll; // --- 修正后的快读 (NOI 标准写法) --- inline ll read() { ll x = 0; int f = 1; char ch = getchar(); while (!isdigit(ch)) { // 使用 isdigit 更加稳健 if (ch == '-') f = -1; ch = getchar(); } while (isdigit(ch)) { x = (x << 3) + (x << 1) + (ch ^ 48); // 位运算加速 x*10 + ch-'0' ch = getchar(); } return x * f; } int n; ll h; int p[100005]; // 判定函数 bool check(int k) { if (k == 0) return false; ll total_hours = 0; for (int i = 0; i < n; i++) { // 向上取整公式 total_hours += (ll)(p[i] + k - 1) / k; // 关键剪枝:一旦超过 h 立即返回,防止 long long 溢出风险 if (total_hours > h) return false; } return total_hours <= h; } int main() { // 1. 读入数据 n = (int)read(); h = read(); int max_p = 0; for (int i = 0; i < n; i++) { p[i] = (int)read(); if (p[i] > max_p) max_p = p[i]; } // 2. 二分答案 int L = 1, R = max_p, ans = max_p; // 如果 H 很大,速度 1 即可;如果 H = N,速度必须是 max_p // 特判处理(可选优化):如果 H == N,直接输出 max_p if (h == (ll)n) { printf("%d\n", max_p); return 0; } while (L <= R) { int mid = L + (R - L) / 2; if (check(mid)) { ans = mid; R = mid - 1; // 尝试更慢 } else { L = mid + 1; // 必须加速 } } // 3. 输出结果 printf("%d\n", ans); return 0; }
3. 教练的深度反思与总结(教学点)
在 NOI 辅导中,这个案例非常有价值,请同学记住以下几点:
-
快读是把双刃剑:
- 快读(
getchar)确实能比cin快 5-10 倍,但在考场上写错一个字符(如把 '9' 写成 '1')就会导致全盘皆输。 - 建议:除非题目数据量达到 且时限极紧,否则优先使用
scanf或者cin.tie(0),因为库函数是最稳健的。
- 快读(
-
为什么版本 2 能 AC?
- 版本 2 使用的是
cin,它能正确读入所有数字。虽然慢一点,但在 的规模下完全够用。
- 版本 2 使用的是
-
如何调试这类问题?
- 如果你发现代码在本地能过样例,但在 OJ 上大量 TLE/WA,第一个要怀疑的就是快读函数。
- 可以在本地写一个
printf("debug: n=%d h=%lld\n", n, h);来检查读入是否正确。
-
数据溢出的隐患:
- 虽然本题 是 ,但如果题目以后变形成 ,此时 在 很小时会非常大。剪枝
if (total_hours > h) return false;既是时间优化,也是安全性保障。
- 虽然本题 是 ,但如果题目以后变形成 ,此时 在 很小时会非常大。剪枝
请再次尝试这个修正后的版本,它在逻辑和速度上都是 NOI 的工业级标准。加油!
信息
- ID
- 19501
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- 递交数
- 5
- 已通过
- 2
- 上传者