【洛谷 P1314 】[NOIP2011 提高组] 聪明的质检员
一、题目完整解读
1. 题意梳理
现有 n 个矿石,每个矿石有重量 $w_i$、价值 $v_i$;给出 m 个查询区间 $[l_i,r_i]$。
我们选定一个参数 W,对每个区间计算检验值 $y_i$:
$$
y_i=\left(\sum_{j=l_i}^{r_i}[w_j\ge W]\right) \times \left(\sum_{j=l_i}^{r_i}[w_j\ge W]v_j\right)
$$
其中 $[p]$ 是指示函数:条件成立返回 1,否则 0。
总检验值 $y=\sum_{i=1}^m y_i$。
给定标准值 s,要求找到合适的 W,使得 $|y-s|$ 最小,输出这个最小差值。
2. 样例拆解(输入 #1)
输入:
5 3 15
1 5
2 5
3 5
4 5
5 5
1 5
2 4
3 3
当 $W=4$ 时:
- 重量≥4 的矿石:4 号、5 号
- 区间 [1,5]:数量 = 2,总价值 = 10 → $y_1=2\times10=20$
- 区间 [2,4]:数量 = 1,总价值 = 5 → $y_2=1\times5=5$
- 区间 [3,3]:无矿石≥4 → $y_3=0$
总和 $y=20+5+0=25$,$|25-15|=10$,是所有 W 中最小差值,输出 10。
3. 数据范围与暴力缺陷
$n,m \le 2\times 10^5$,矿石重量最大 $10^6$
暴力思路:枚举所有W,每次遍历 n、m 计算 y,复杂度$O(10^6 \times (n+m))$,严重超时,必须优化。
二、核心解题思路:二分答案
1. 单调性分析(二分成立关键)
我们观察总检验值 y 和参数 W 的变化关系:
- W 越大,满足 $w_j \ge W$ 的矿石越少
- 区间内合格矿石数量、总价值都会减小
- 乘积 $y_i$ 减小,总和 y 单调递减
单调性结论:$W \uparrow \implies y \downarrow$,函数单调,满足二分答案使用条件。
2. 二分目标
二分枚举 W 的取值(值域 $[0,10^6+1]$),每次二分中点 mid 作为候选 W,计算当前总 y:
- 若 $y \le s$:当前 W 偏大,总检验值偏小,尝试缩小 W(往左找更小 W,让 y 变大靠近 s)
- 若 $y > s$:当前 W 偏小,总检验值偏大,需要增大 W(往右找更大 W,让 y 变小靠近 s)
每次计算完 y 后,更新全局最小差值 $|y-s|$,二分结束后输出最小值。
3. 快速计算 y:双前缀和优化
每次 check 函数中,如果暴力遍历每个区间统计合格矿石,单次 check 复杂度 $O(n+m)$,总复杂度 $O((n+m)\log(maxW))$,完全可以通过 $2\times10^5$ 的数据。
我们维护两个前缀数组:
- $sn[i]$:前 i 个矿石中,重量≥W 的矿石数量前缀和
- $sv[i]$:前 i 个矿石中,重量≥W 的矿石总价值前缀和
区间 $[l,r]$ 内:
- 合格数量 = $sn[r]-sn[l-1]$
- 合格总价值 = $sv[r]-sv[l-1]$
- 区间检验值 = 数量 × 总价值
累加所有区间得到总 y。
三、完整代码逐行详解
#include <bits/stdc++.h>
using namespace std;
typedef long long ll; // 数据极大,必须开long long防溢出
const int N = 200010; // n,m上限2e5,数组开2e5+10
int n, m, w[N], v[N], l[N], r[N];
ll s, sn[N], sv[N], ans = 1e18; // ans初始无穷大,记录最小差值
// check函数:给定参数z(当前二分的W),计算总y,返回y<=s用于二分判断
bool check(int z) {
memset(sn, 0, sizeof sn); // 每次重置前缀数组
memset(sv, 0, sizeof sv);
// 预处理数量、价值前缀和
for (int i = 1; i <= n; i++) {
if (w[i] >= z) { // 当前矿石合格
sn[i] = sn[i-1] + 1;
sv[i] = sv[i-1] + v[i];
} else { // 不合格,继承前一位前缀和
sn[i] = sn[i-1];
sv[i] = sv[i-1];
}
}
ll y = 0; // 总检验值,开long long!
// 遍历所有区间,累加每个区间的y_i
for (int i = 1; i <= m; i++) {
ll cnt = sn[r[i]] - sn[l[i]-1]; // 区间合格矿石数
ll val = sv[r[i]] - sv[l[i]-1]; // 区间合格矿石总价值
y += cnt * val; // 区间检验值累加到总和
}
ans = min(ans, llabs(y - s)); // 更新全局最小差值
return y <= s; // 二分判断:y小于等于标准值,说明W太大,往左二分
}
// 二分主函数,二分W值域 [0, 1e6+1]
ll find() {
int l = 0, r = 1e6 + 1;
while (l + 1 < r) { // 二分模板:左闭右开,最终l、r为相邻值
int mid = l + r >> 1; // 等价于 (l+r)/2,位运算更快
if (check(mid)) r = mid; // y<=s,W偏大,右边界左移
else l = mid; // y>s,W偏小,左边界右移
}
return ans; // 返回全程记录的最小|y-s|
}
int main() {
ios::sync_with_stdio(false); // 大数据加速cin
cin.tie(0);
cin >> n >> m >> s;
// 读入n个矿石重量、价值
for (int i = 1; i <= n; i++) {
cin >> w[i] >> v[i];
}
// 读入m个查询区间
for (int i = 1; i <= m; i++) {
cin >> l[i] >> r[i];
}
cout << find(); // 输出最小差值
return 0;
}
关键细节说明
-
long long 强制使用:$n,m$ 均为 $2\times10^5$,每个区间乘积可达 $(2e5)\times(2e5 \times 1e9)$,数值会远超 int 范围,所有前缀和、总 y、s、差值都要用 long long,否则直接溢出 WA。
-
二分边界:W 最小可取 0(所有矿石都合格,y 最大),最大取 $10^6+1$(无矿石合格,y=0),覆盖全部可能取值。
-
二分循环条件 l+1<r:这是整数二分经典模板,循环结束后 l 和 r 是相邻两个数,覆盖所有候选 W,不会漏掉任何可能解。
-
memset 重置前缀数组:每次二分的 W 不同,合格矿石集合变化,前缀数组必须清零重新计算。
-
llabs 取绝对值:y 和 s 都是 long long,不能用 abs(仅支持 int),必须用 llabs 求长整型绝对值。
![请添加图片描述]()
四、算法复杂度分析
- 二分次数:矿石重量值域 $10^6$,二分次数 $\log_2(10^6) \approx 20$ 次
- 单次 check 函数:遍历 n 个矿石预处理前缀 $O(n)$,遍历 m 个区间计算总和 $O(m)$
- 总时间复杂度:$O((n+m) \log(maxW))$,$20\times 4\times105=8\times106$,完全满足时间限制
五、易错点总结(考场避坑)
- 数据溢出:忘记把 y、sn、sv、ans 开 long long,90% 选手初次写会踩坑
- abs/llabs 混用:long long 数值求绝对值必须用 llabs
- 二分单调性搞反:误以为 W 越大 y 越大,二分左右边界更新写反
- 前缀和下标错误:区间 $[l,r]$ 要减 sn[l-1],错写成 sn[l] 会少统计左端点矿石
- 二分边界设置过小:r 只开到 1e6,漏掉 W=1e6+1 的情况
- 未每次重置前缀数组:memset 漏掉,前缀和残留上一轮数据导致答案错误
六、拓展思考
- 能否离散化优化二分值域? 矿石 w 的取值最多 n 种,可把所有 w 排序去重,二分离散化后的下标,二分次数略减少,对本题影响不大
- 有没有不用二分的做法? 直接枚举所有 w 复杂度太高,无更优线性做法,二分是本题最优标准解法
- 多组区间重叠会影响前缀和吗? 不会,前缀和是静态预处理数组,每个区间独立查询,重叠区间不干扰计算
七、题目总结
本题是 NOIP 提高组经典二分答案 + 前缀和模板题,核心考察两点:
- 识别变量单调性,确定二分答案解题框架
- 使用前缀和快速区间统计,把单次检验的复杂度从 $O(nm)$ 降到 $O(n+m)$

浙公网安备 33010602011771号