2 条题解

  • 0
    @ 2026-3-26 11:31:19

    你好,同学!我是你的金牌教练。

    在解决“珂珂吃香蕉”这类最优化问题时,我们的思维路径通常是从 “模拟所有可能”“利用单调性缩小搜索范围”。这不仅是为了通过题目,更是为了锻炼你在考场上写出高效率、高鲁棒性代码的能力。

    下面我将通过三个版本演示代码的进化。


    版本 1:暴力枚举速度(线性搜索)

    思路提示: 珂珂的速度 kk 最小为 11,最大没必要超过香蕉堆中最大的那一堆(因为一小时只能吃一堆)。我们从 11 开始逐个尝试每一个速度,第一个能满足在 HH 小时内吃完的速度就是答案。

    复杂度分析:

    • 时间复杂度O(Nmax(Piles))O(N \cdot \max(Piles))。在本题中最大约为 104109=101310^4 \cdot 10^9 = 10^{13},在 NOI 1秒时限内(约 10810^8 次运算)会严重超时。
    • 空间复杂度O(N)O(N)
    /*
     * 暴力枚举版
     * 适用场景: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 推荐写法)

    思路提示: 观察到:速度越快,总时间越短。如果速度 kk 可以吃完,那么速度 k+1k+1 也一定可以。这满足单调性。我们可以在 [1,max(Piles)][1, \max(Piles)] 范围内二分 kk

    复杂度分析:

    • 时间复杂度O(Nlog(max(Piles)))O(N \cdot \log(\max(Piles)))。运算量约为 10430=310510^4 \cdot 30 = 3 \cdot 10^5,秒过。
    • 空间复杂度O(N)O(N)
    /*
     * 二分答案标准版
     * 关键点:二分范围的确定以及 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 竞赛中,如果 NN 达到 10610^6 级别,cin 可能会变慢。同时,我们可以通过更精准的边界缩小搜索范围。

    复杂度分析:

    • 时间复杂度O(Nlog(max(Piles)))O(N \cdot \log(\max(Piles))),常数极小。
    • 空间复杂度O(N)O(N)
    /*
     * 优化版:加入快读、更严密的边界判断
     */
    #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;
    }
    

    时间复杂度优化建议

    1. 向上取整的坑:不要使用 ceil((double)a/b)。浮点数运算慢且存在精度风险,在 10910^9 规模下可能出现 999.9999999 被截断的情况。请务必使用 (a + b - 1) / b
    2. Long Long 溢出:在判定函数里,total_hours 必须是 long long。因为如果 k=1,piles[i]=109,n=104k=1, piles[i]=10^9, n=10^4,总和会达到 101310^{13},远超 int 范围。
    3. 提前返回(剪枝):在 check 里的 for 循环中,一旦累加值超过 hh,立即 return false。这在“答案很大但速度很慢”的测试点能节省大量时间。
    4. 二分边界:本题中,如果 h=nh=n,答案一定是 max(Piles)\max(Piles)。虽然二分能处理,但读题时思考边界情况有助于理解单调性。

    总结:这道题是学习“二分答案”的入室之作。核心就在于:如果寻找答案很难,就去尝试验证答案。 加油!


    版本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' 的数字,读入的 N,HN, H 和数组元素全变样了(比如 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 辅导中,这个案例非常有价值,请同学记住以下几点:

    1. 快读是把双刃剑

      • 快读(getchar)确实能比 cin 快 5-10 倍,但在考场上写错一个字符(如把 '9' 写成 '1')就会导致全盘皆输
      • 建议:除非题目数据量达到 10610^6 且时限极紧,否则优先使用 scanf 或者 cin.tie(0),因为库函数是最稳健的。
    2. 为什么版本 2 能 AC?

      • 版本 2 使用的是 cin,它能正确读入所有数字。虽然慢一点,但在 N=104N=10^4 的规模下完全够用。
    3. 如何调试这类问题?

      • 如果你发现代码在本地能过样例,但在 OJ 上大量 TLE/WA,第一个要怀疑的就是快读函数
      • 可以在本地写一个 printf("debug: n=%d h=%lld\n", n, h); 来检查读入是否正确。
    4. 数据溢出的隐患

      • 虽然本题 HH10910^9,但如果题目以后变形成 N=105,H=105,Pi=109N=10^5, H=10^5, P_i=10^9,此时 TotalHoursTotalHourskk 很小时会非常大。剪枝 if (total_hours > h) return false; 既是时间优化,也是安全性保障。

    请再次尝试这个修正后的版本,它在逻辑和速度上都是 NOI 的工业级标准。加油!

    信息

    ID
    19501
    时间
    1000ms
    内存
    128MiB
    难度
    10
    标签
    递交数
    5
    已通过
    2
    上传者