记录114
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
long long a[N];
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
//sort(a+1,a+n+1);
int L=1,R=4e5,mid=0;//二分答案,mid当作可能的结果带入
long long sum=0,ans=0;//sum注意数据范围
while(L<=R){
sum=0;
mid=(L+R)/2;
for(int i=1;i<=n;i++){//存每棵树的多余高度
if(a[i]-mid>0) sum+=a[i]-mid;
}
if(sum>=m){//长度至少m,总和>=m
ans=mid;//存符合条件的值,然后更新
L=mid+1;
}
else R=mid-1;
}
cout<<ans;//输出
return 0;//结束程序
}
前言
我是一名专注信奥赛(CSP-J/S、NOIP)的教练。
- 如果你觉得这篇题解对你有帮助,欢迎点击关注我的CSDN账号,我会持续更新高质量算法解析。
- 我深知算法思维的构建远比单纯通过题目更重要,本系列题解不局限于AC代码的堆砌,而是致力于拆解题目背后的逻辑链条与核心知识点
- 备赛路上若遇瓶颈,欢迎随时评论或私信,我将甄选典型疑难问题,通过视频讲解或撰写专项文章的形式,为你提供深度答疑。
题目传送门
https://www.luogu.com.cn/problem/P1873
突破口
伐木工人 Mirko 需要砍 M 米长的木材。对 Mirko 来说这是很简单的工作,因为他有一个漂亮的新伐木机,可以如野火一般砍伐森林。不过,Mirko 只被允许砍伐一排树。
Mirko 的伐木机工作流程如下:Mirko 设置一个高度参数 H(米),伐木机升起一个巨大的锯片到高度 H,并锯掉所有树比 H 高的部分(当然,树木不高于 H 米的部分保持不变)。Mirko 就得到树木被锯下的部分。例如,如果一排树的高度分别为 20,15,10 和 17,Mirko 把锯片升到 15 米的高度,切割后树木剩下的高度将是 15,15,10 和 15,而 Mirko 将从第 1 棵树得到 5 米,从第 4 棵树得到 2 米,共得到 7 米木材。
Mirko 非常关注生态保护,所以他不会砍掉过多的木材。这也是他尽可能高地设定伐木机锯片的原因。请帮助 Mirko 找到伐木机锯片的最大的整数高度 H,使得他能得到的木材至少为 M 米。换句话说,如果再升高 1 米,他将得不到 M 米木材。
🔍 一、题目本质与目标
🎯 问题重述
- 有
N棵树,高度为a[1..N] - 设置一个锯片高度 H
- 对每棵树:若
a[i] > H,则获得a[i] - H米木材;否则获得 0 - 目标:找到最大的整数 H ,使得 总木材 ≥ M
✅ 关键词:
- "最大 H" → 最优化目标
- "至少 M 米" → 约束条件
- "再升高 1 米就不够 " → H 是满足条件的上界
📌 样例解析(输入 #1)
cpp
树高:20, 15, 10, 17
M = 7
- H = 15 → 得到 (20-15)+(17-15) = 5+2 = 7 ✅
- H = 16 → (20-16)+(17-16) = 4+1 = 5 < 7 ❌
- 所以最大 H = 15
🧠 二、核心思路:二分答案(Binary Search on Answer)
为什么能用二分?
观察 H 与 总木材量 sum(H) 的关系:
- H 越大 → 锯得越少 →
sum(H)单调不增 - H 越小 → 锯得越多 →
sum(H)单调不减
💡 单调性是二分搜索的前提!
我们要找:最大的 H,使得 sum(H) ≥ M
这正是典型的 "二分答案" 问题。
二分策略
- 搜索空间 :H ∈ [0, max_height]
(题目中树高 ≤ 4e5,但代码用了R=1e9,更保险) - 判断函数 :给定 H,计算
sum = Σ max(0, a[i] - H) - 调整方向 :
- 若
sum ≥ M→ H 可能还能更高 →L = mid + 1 - 否则 → H 太高了 →
R = mid - 1
- 若
✅ 最终答案记录在
ans中(每次满足条件时更新)
代码分析
cpp
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
long long a[N]; // 存储树高,注意:虽然树高 ≤4e5,但用 long long 无妨
cpp
int main(){
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++)
cin >> a[i];
- 读入数据,1-based 存储
cpp
int L = 1, R = 4e5, mid = 0; // 二分边界
long long sum = 0, ans = 0; // sum 必须 long long!
⚠️ 关键细节:
R = 4e5:虽然树高 ≤ 4e5,但设大一点无妨(安全)sum类型为long long:因为 M 可达 2e9,且每棵树最多贡献 4e5,N=1e6 → 最大 sum ≈ 4e11,必须用 long long!ans = 0:初始化答案
cpp
while(L <= R){
sum = 0;
mid = (L + R) / 2; // 当前尝试的高度 H = mid
cpp
for(int i = 1; i <= n; i++){
if(a[i] - mid > 0)
sum += a[i] - mid;
}
- 计算在高度
mid下能获得的总木材量 - 等价于:
sum += max(0LL, a[i] - mid);
cpp
if(sum >= m){ // 满足"至少 M 米"
ans = mid; // 记录当前可行解
L = mid + 1; // 尝试更高的 H(更优解)
}
else
R = mid - 1; // H 太高,降低
}
🔥 这是二分答案的核心逻辑!
- 我们要最大化 H ,所以当
sum ≥ M时,不 return,而是继续尝试更大的 H- 最终
ans保存的是最后一个满足条件的 H
cpp
cout << ans;
return 0;
}
⚠️ 四、关键问题与优化建议
| 问题 | 说明 |
|---|---|
| 二分下界 | L = 1 不够严谨,应设 L = 0(H 可为 0) 但题目保证"总和 > M",且树高 ≥1,所以 H=0 总能满足,不影响答案 |
| mid 溢出 | (L+R)/2 在 L,R ≤1e9 时安全(和 ≤2e9 < 2^31) |
| 时间复杂度 | O(log(maxH) × N) ≈ log₂(1e9) × 1e6 ≈ 30 × 1e6 = 3e7,C++ 可接受 |
与其他方法对比
| 方法 | 可行性 | 缺点 |
|---|---|---|
| 暴力枚举 H | ❌ 不可行(H 可达 4e5,N=1e6 → 4e11 操作) | |
| 二分答案 + 暴力求和 | ✅ 本题解法,O(N log maxH) | |
| 二分 + 前缀和优化 | ✅ 可进一步优化到 O(N log N + log maxH × log N) (排序后用 upper_bound 找分界点,再用前缀和快速计算) |
💡 前缀和优化思路(供拓展):
- 排序数组
- 预处理前缀和
pre[i] = a[1]+...+a[i]- 对给定 H,用
upper_bound找第一个 > H 的位置pos- 则
sum = (pre[n] - pre[pos-1]) - H × (n - pos + 1)- 这样每次 check 为 O(log N),总复杂度 O(N log N)
但本题 N=1e6,O(30×1e6)=3e7 已足够,无需复杂优化。