OneCoder Avatar
OneCodercoderli.com · 937 篇博文
🧮 Algo

数据结构与算法

双指针、回溯剪枝、图论与搜索

🎨 视觉封面

【算法/二分】luogu-P1873 [COCI 2011/2012

📅 2026-09-12·✍️ OneCoder·计算中...·⏱️ 14 分钟
#COCI#洛谷#C++#二分答案#算法

COCI 2011/2012 #5 经典题目,洛谷 P1873,是学习“二分答案(Binary Search on Answer)”必须掌握的教科书级经典例题。适合 GESP 五级、CSP-J 及算法初学者练习。题目难度⭐⭐☆☆☆,洛谷难度等级普及-

P1873 [COCI 2011/2012 #5] EKO / 砍树

题目要求

题目描述

伐木工人 Mirko 需要砍 MM 米长的木材。对 Mirko 来说这是很简单的工作,因为他有一个漂亮的新伐木机,可以如野火一般砍伐森林。不过,Mirko 只被允许砍伐一排树。

Mirko 的伐木机工作流程如下:Mirko 设置一个高度参数 HH(米),伐木机升起一个巨大的锯片到高度 HH,并锯掉所有树比 HH 高的部分(当然,树木不高于 HH 米的部分保持不变)。Mirko 就得到树木被锯下的部分。

例如,如果一排树的高度分别为 20,15,1020,15,101717,Mirko 把锯片升到 1515 米的高度,切割后树木剩下的高度将是 15,15,1015,15,101515,而 Mirko 将从第 11 棵树得到 55 米,从第 44 棵树得到 22 米,共得到 77 米木材。

Mirko 非常关注生态保护,所以他不会砍掉过多的木材。这也是他尽可能高地设定伐木机锯片的原因。请帮助 Mirko 找到伐木机锯片的最大的整数高度 HH,使得他能得到的木材至少为 MM 米。换句话说,如果再升高 11 米,他将得不到 MM 米木材。

输入格式

1122 个整数 NNMMNN 表示树木的数量,MM 表示需要的木材总长度。

22NN 个整数表示每棵树的高度。

输出格式

11 个整数,表示锯片的最高高度。

输入输出样例 #1

输入 #1
TEXT
4 7
20 15 10 17
输出 #1
TEXT
15

输入输出样例 #2

输入 #2
TEXT
5 20
4 42 40 26 46
输出 #2
TEXT
36

说明/提示

对于 100%100\% 的测试数据,1N1061\le N\le10^61M2×1091\le M\le2\times10^9,树的高度 4×105\le 4\times 10^5(部分强化数据中树高可达 10910^9),所有树的高度总和 >M>M


题目分析

本题属于经典的**二分答案(Binary Search on Answer)**问题。

1. 为什么不能暴力枚举?

题目要求我们寻找伐木机锯片的最大高度 HH

如果我们从 00 开始逐个枚举高度 HH(或者从树木的最大高度从大往小枚举),每枚举一个高度,都需要遍历这 NN 棵树来累加获得的木材总量:

f(H)=i=1Nmax(0,tree[i]H)f(H) = \sum_{i=1}^N \max(0, \text{tree}[i] - H)

  • 树木数量 N106N \le 10^6
  • 树木高度最大可达 4×1054\times 10^5(甚至 10910^9);
  • 暴力枚举的总计算次数为 O(N×maxHi)106×4×105=4×1011O(N \times \max H_i) \approx 10^6 \times 4\times 10^5 = 4 \times 10^{11} 次,计算机 1 秒通常只能处理 10810^8 次左右的基本运算,因此暴力枚举必然导致超时(TLE)

2. 函数的单调性分析

我们观察锯片高度 HH 与砍下的木材总量 f(H)f(H) 之间的关系:

  • 锯片升得越高(HH 增大):每棵树被锯下的部分就会减少或不变,因此最终收集到的总木材量就会减少
  • 锯片降得越低(HH 减小):每棵树被锯下的部分就会增加或不变,因此最终收集到的总木材量就会增多

因此,f(H)f(H) 是关于 HH单调不增函数

当我们检查“在高度 HH 下,得到的木材量是否不少于 MM(即 f(H)Mf(H) \ge M)”时,判定结果呈现明显的单调分界:

锯片高度 HH 00 11 ... 最优解 HansH_{ans} Hans+1H_{ans} + 1 ... max(Hi)\max(H_i)
是否 M\ge M true true true true false false false

题目要求的是“满足条件的最大的整数高度 HH”,正好对应最后一个返回 true 的分界点

只要一个解空间满足这种单调性,我们就可以使用二分答案,将“求最值”转化为“猜答案 + 判定(Check)”,将线性的逐个排查降维成对数级的二分折半收敛!


解题步骤与核心细节

1. 确定二分区间的上下界

  • 下界 LLL=0L = 0。极端情况下,即使把锯片贴在地面(H=0H=0)把树齐根锯掉,也是合法的操作。
  • 上界 RRR=max(tree[i])R = \max(\text{tree}[i])。锯片的高度显然不可能超过森林中最高的树的高度,因为超过最高树木后,一米木材也锯不到。

2. 编写判定函数 check(H)

对于候选高度 HH,遍历 NN 棵树:

  • 如果树高 tree[i]>H\text{tree}[i] > H,则将多出的部分 (tree[i]H)(\text{tree}[i] - H) 累加进 sum
  • 如果树高 tree[i]H\text{tree}[i] \le H,则无法提供木材,跳过。
  • 判断最终累加得到的 sum 是否 M\ge M

⚠️ 核心避坑点(必看):整型溢出
题目中 M2×109M \le 2 \times 10^9N106N \le 10^6,树高最大可达 4×1051094\times 10^5 \sim 10^9
如果在 H=0H = 0 附近,累加和 sum 最大可达 106×109=101510^6 \times 10^9 = 10^{15},这远远超过了 32 位有符号整型 int 的最大表示范围(23112.14×1092^{31}-1 \approx 2.14 \times 10^9)。
如果 sum 使用 int,会直接溢出变成负数,从而导致判定函数返回 false,得出完全错误的答案!因此,累加和 sum 以及木材需求量 MM 必须定义为 64 位整型 long long

3. 常数优化小技巧

check(H) 函数内部累加时,并不需要傻傻地把所有树都加完。只要在循环过程中发现当前的 sum >= M,就可以立即提前返回 true。这一步剪枝可以在测试数据中减少大量无谓的累加计算。

4. 二分收敛过程

采用经典的左闭右闭区间 [l, r] 二分模板:

CPP
long long l = 0, r = max_val;
long long ans = 0;
while (l <= r) {
    long long mid = l + (r - l) / 2;
    if (check(mid)) {
        ans = mid;     // mid 高度符合要求,记录下当前可行答案
        l = mid + 1;   // 尝试寻找更高的锯片高度
    } else {
        r = mid - 1;   // mid 高度切出来的木材不够,必须降低锯片
    }
}

完整参考代码 (C++)

CPP
/**
 * Problem: luogu P1873 [COCI 2011/2012 #5] EKO / 砍树
 * Algorithm: 二分答案 (Binary Search on Answer)
 * Author: OneCoder
 */

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

// 树木数量最大为 10^6
const int MAXN = 1000005;
int trees[MAXN];
int n;
long long m;

/**
 * @brief 判定函数:检查锯片设定在高度 mid 时,能否锯出至少 m 米木材
 * @param mid 猜测的锯片高度
 * @return true 表示能够得到至少 m 米木材;false 表示木材不足
 */
bool check(int mid) {
    long long sum = 0; // 必须使用 long long 累加,防止溢出
    for (int i = 0; i < n; i++) {
        if (trees[i] > mid) {
            sum += (trees[i] - mid);
            // 贪心剪枝:一旦满足所需木材量,立刻提前返回,提高常数效率
            if (sum >= m) {
                return true;
            }
        }
    }
    return sum >= m;
}

int main() {
    // 针对百万级输入规模开启 IO 流加速
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    if (!(cin >> n >> m)) {
        return 0;
    }

    int max_val = 0;
    for (int i = 0; i < n; i++) {
        cin >> trees[i];
        if (trees[i] > max_val) {
            max_val = trees[i];
        }
    }

    // 二分区间:高度下界为 0,上界为最高树的高度
    int l = 0, r = max_val;
    int ans = 0;

    while (l <= r) {
        int mid = l + (r - l) / 2;
        if (check(mid)) {
            ans = mid;     // 当前高度可行,记录最优解
            l = mid + 1;   // 贪心尝试更高的锯片高度
        } else {
            r = mid - 1;   // 木材不够,锯片过高,向左半区间收缩
        }
    }

    cout << ans << "\n";

    return 0;
}

复杂度分析

  • 时间复杂度
    • 二分区间长度为 max(Hi)\max(H_i),在最坏情况下二分查找需要执行 log2(maxHi)\log_2(\max H_i) 次。
    • 当树高达到 10910^9 级别时,log2(109)30\log_2(10^9) \approx 30 次;若树高在 4×1054\times 10^5 级别时,log2(4×105)19\log_2(4\times 10^5) \approx 19 次。
    • 每次判定函数 check 遍历一次 NN 棵树,耗时 O(N)O(N)
    • 因此总时间复杂度为 O(Nlog(maxHi))\mathcal{O}(N \log (\max H_i))。代入 N=106N = 10^6,总基本运算次数约在 2×1072\times 10^7 次左右,配合 I/O 加速与提前剪枝,耗时仅需约 100~200ms,在 1.0 秒时限内绰绰有余。
  • 空间复杂度
    • 仅使用了一个大小为 10610^6 的全局 int 数组存储树高,内存消耗约为 4 MB4\text{ MB},远低于题目常见的 128 MB256 MB128\text{ MB} \sim 256\text{ MB} 空间限制,空间复杂度为 O(N)\mathcal{O}(N)

总结与同类拓展

二分答案是算法竞赛与考级中最核心的“解题模式”之一。当你遇到以下特征的问题时,应第一时间联想到二分答案:

  1. 问题要求**“求最大值的最小值”“求最小值的最大值”**;
  2. 直接构造最优解或者动态规划非常困难,但给定一个具体答案后,写出验证函数 check() 却极其直观简单
  3. 答案的可行性具备明显的单调性质(即随着参数增大,条件从成立逐渐变为不成立,或反之)。

经典同类推荐练习题

💡 OneCoder 资源指引

所有代码开源上传至 GitHub:yummy-code 仓库 · GESP 专题站:GESP WIKI

🤝 技术交流与答疑

欢迎加入:C++ GESP/CSP 考级答疑群(688906745)Java/Python交流群(982860385),点击可直接加群。

📚

猜你想读 · 相关文章推荐

OneCoder

OneCoder (lihongzheshuai)

一个中年人的自留地,记录学习 C++、GESP/NOI、Java、Python 与算法架构的心得体会。本站唯一网址:coderli.com

💬 读者留言与交流

0 条讨论
✨ 支持 Markdown 语法格式
还没有留言,快来成为第一个讨论者吧!