题解:洛谷 P1314 聪明的质监员
【题目来源】
洛谷:P1314 [NOIP 2011 提高组] 聪明的质监员 - 洛谷
【题目描述】
小T 是一名质量监督员,最近负责检验一批矿产的质量。这批矿产共有 \(n\) 个矿石,从 \(1\) 到 \(n\) 逐一编号,每个矿石都有自己的重量 \(w_i\) 以及价值 \(v_i\)。检验矿产的流程是:
- 给定 \(m\) 个区间 \([l_i,r_i]\);
- 选出一个参数 \(W\);
- 对于一个区间 \([l_i,r_i]\),计算矿石在这个区间上的检验值 \(y_i\):
\(y_i=\sum\limits_{j=l_i}^{r_i}[w_j \ge W] \times \sum\limits_{j=l_i}^{r_i}[w_j \ge W]v_j\)
其中 \(j\) 为矿石编号。
这批矿产的检验结果 \(y\) 为各个区间的检验值之和。即:\(\sum\limits_{i=1}^m y_i\)
若这批矿产的检验结果与所给标准值 s 相差太多,就需要再去检验另一批矿产。小T 不想费时间去检验另一批矿产,所以他想通过调整参数 \(W\) 的值,让检验结果尽可能的靠近标准值 \(s\),即使得 \(|s-y|\) 最小。请你帮忙求出这个最小值。
【输入】
第一行包含三个整数 \(n,m,s\),分别表示矿石的个数、区间的个数和标准值。
接下来的 \(n\) 行,每行两个整数,中间用空格隔开,第 \(i+1\) 行表示 \(i\) 号矿石的重量 \(w_i\) 和价值 \(v_i\)。
接下来的 \(m\) 行,表示区间,每行两个整数,中间用空格隔开,第 \(i+n+1\) 行表示区间 \([l_i,r_i]\) 的两个端点 \(l_i\) 和 \(r_i\)。注意:不同区间可能重合或相互重叠。
【输出】
一个整数,表示所求的最小值。
【输入样例】
5 3 15
1 5
2 5
3 5
4 5
5 5
1 5
2 4
3 3
【输出样例】
10
【核心思想】
-
问题分析:给定 \(n\) 个矿石(重量 \(w_i\)、价值 \(v_i\))和 \(m\) 个区间 \([l_i, r_i]\)。选定参数 \(W\) 后,每个区间 \(i\) 的检验值为 \(y_i = (\sum_{j=l_i}^{r_i} [w_j \ge W]) \times (\sum_{j=l_i}^{r_i} [w_j \ge W] v_j)\),总检验值 \(y = \sum_{i=1}^m y_i\)。目标是使 \(y\) 与标准值 \(s\) 的绝对差 \(|s-y|\) 最小化。本质是在重量阈值 \(W\) 的单调变化下,检验值 \(y\) 也单调变化,因此可以用二分答案找到最接近 \(s\) 的 \(y\)。
-
算法选择:
- 二分答案 + 前缀和:随着 \(W\) 增大,满足 \(w_j \ge W\) 的矿石数量减少,因此 \(y\) 单调递减。利用单调性二分 \(W\),在 \(O(n+m)\) 的检查函数中,用前缀和快速计算每个区间内满足条件的矿石数量与价值,从而得到 \(y\)。
- 精确搜索:二分出最后一个使 \(y \ge s\) 的 \(W\)(或第一个使 \(y \le s\) 的 \(W\)),并在检查过程中不断更新 \(|s-y|\) 的最小值,最终答案即为所有检查过的 \(y\) 与 \(s\) 的绝对差的最小值。
-
关键步骤:
- 读入数据:\(n, m, s\),矿石重量 \(w_i\)、价值 \(v_i\),以及 \(m\) 个区间 \([l_i, r_i]\)。
- 定义检查函数
check(W):- 计算前缀和数组
sn[i](满足重量 \(\ge W\) 的矿石数量)和sv[i](对应价值之和)。遍历 \(i=1..n\),若 \(w_i \ge W\),则sn[i] = sn[i-1] + 1,sv[i] = sv[i-1] + v[i];否则继承前一个值。 - 初始化 \(y=0\),遍历每个区间 \([l_i, r_i]\),计算区间内满足条件的矿石数量
cnt = sn[r_i] - sn[l_i-1],价值之和sum = sv[r_i] - sv[l_i-1],累加 \(y\) +=cnt * sum。 - 更新全局最小绝对差
ans = min(ans, abs(y - s))。 - 返回
y <= s(用于二分判定)。
- 计算前缀和数组
- 二分查找:初始左边界
L = 0,右边界R = max(w_i) + 1(或 1e6+1)。当L + 1 < R时,取mid = (L+R)/2,若check(mid)为真(即 \(y \le s\)),说明 \(W\) 可以更大(\(y\) 更小),令R = mid;否则令L = mid。循环结束后,ans即为答案。
-
时间/空间复杂度:
- 时间复杂度:\(O((n+m) \log \max(w_i))\),\(\log\) 约为 20 次,\(n, m \le 2\times10^5\),完全可行。
- 空间复杂度:\(O(n+m)\),存储数组和区间。
-
二分答案 + 前缀和的核心思想:
- 单调性:检验值 \(y\) 随 \(W\) 增大而单调不增。这个单调性是二分的基础,使得我们可以通过 \(y\) 与 \(s\) 的关系来调整 \(W\)。
- 前缀和优化:每次检查都需要快速计算多个区间的子段和(满足条件的数量与价值)。使用前缀和将区间查询降为 \(O(1)\),使检查复杂度为 \(O(n+m)\)。
- 最优解位置:二分过程会不断接近使 \(y\) 最接近 \(s\) 的 \(W\),且在每次检查中更新 \(ans\),因此二分结束后 \(ans\) 即为最小值。
- 适用场景:适用于在单调函数中搜索最接近目标值的参数,且检查函数需要通过区间查询累加的场景(如带权区间统计问题)。
【算法标签】
普及+ #整数二分
【代码详解】
#include <bits/stdc++.h>
using namespace std;
#define int long long // 定义int为long long类型
// 变量定义
int n, m, s; // n: 矿石数量, m: 区间数量, s: 目标值
int sn[200005]; // 前缀和数组:满足条件的矿石数量
int sv[200005]; // 前缀和数组:满足条件的矿石价值
int w[200005]; // 矿石重量数组
int v[200005]; // 矿石价值数组
int l[200005], r[200005]; // 每个区间的左右端点
int ans = 1e18; // 存储最小差值,初始化为极大值
/**
* 检查当前重量阈值W是否满足条件
* @param W 当前尝试的重量阈值
* @return 是否当前计算值小于等于目标值s
*/
bool check(int W)
{
// 初始化前缀和数组
memset(sn, 0, sizeof(sn));
memset(sv, 0, sizeof(sv));
// 计算前缀和
for (int i = 1; i <= n; i++)
{
if (w[i] >= W)
{
sn[i] = sn[i - 1] + 1; // 满足条件的矿石数量
sv[i] = sv[i - 1] + v[i]; // 满足条件的矿石价值
}
else
{
sn[i] = sn[i - 1];
sv[i] = sv[i - 1];
}
}
// 计算所有区间的加权和
int y = 0;
for (int i = 1; i <= m; i++)
{
y += (sn[r[i]] - sn[l[i] - 1]) * (sv[r[i]] - sv[l[i] - 1]);
}
// 更新最小差值
ans = min(ans, abs(y - s));
return y <= s;
}
/**
* 二分查找最优的重量阈值
* @return 找到的最小差值
*/
int find()
{
int l = 0, r = 1000000 + 1; // 重量阈值的搜索范围
// 二分查找
while (l + 1 < r)
{
int mid = (l + r) / 2;
if (check(mid))
{
r = mid; // 尝试更小的阈值
}
else
{
l = mid; // 需要更大的阈值
}
}
return ans;
}
signed main()
{
// 输入数据
cin >> n >> m >> s;
for (int i = 1; i <= n; i++)
{
cin >> w[i] >> v[i];
}
for (int i = 1; i <= m; i++)
{
cin >> l[i] >> r[i];
}
// 查找并输出结果
cout << find() << endl;
return 0;
}
// 使用acwing模板二刷
#include <bits/stdc++.h>
using namespace std;
#define int long long // 定义int为long long类型
const int N = 200005; // 定义最大矿石数量
// 全局变量声明
int n, m, s; // n:矿石数量, m:区间数量, s:目标值
int sn[N], sv[N]; // 前缀和数组:sn记录数量, sv记录价值
int w[N], v[N]; // w:矿石重量数组, v:矿石价值数组
int l[N], r[N]; // 每个区间的左右端点
int ans = 1e18; // 存储最小差值,初始化为极大值
/**
* 检查函数,验证当前重量阈值W是否满足条件
* @param W 当前尝试的重量阈值
* @return 返回当前计算值是否小于等于目标值s
*/
bool check(int W)
{
// 初始化前缀和数组
memset(sn, 0, sizeof(sn));
memset(sv, 0, sizeof(sv));
// 计算前缀和
for (int i = 1; i <= n; i++)
{
if (w[i] >= W)
{
sn[i] = sn[i - 1] + 1; // 满足条件的矿石数量
sv[i] = sv[i - 1] + v[i]; // 满足条件的矿石价值
}
else
{
sn[i] = sn[i - 1];
sv[i] = sv[i - 1];
}
}
// 计算所有区间的加权和
int y = 0;
for (int i = 1; i <= m; i++)
{
y += (sn[r[i]] - sn[l[i] - 1]) * (sv[r[i]] - sv[l[i] - 1]);
}
// 更新最小差值
ans = min(ans, abs(y - s));
return y <= s;
}
/**
* 二分查找函数,寻找最优的重量阈值
* @return 返回找到的最小差值
*/
int find()
{
int l = 0, r = 1000000; // 重量阈值的搜索范围
// 二分查找过程
while (l < r)
{
int mid = (l + r) >> 1; // 等价于(l + r)/2
if (check(mid))
{
r = mid; // 尝试更小的阈值
}
else
{
l = mid + 1; // 需要更大的阈值
}
}
return ans;
}
signed main()
{
// 输入数据
cin >> n >> m >> s;
for (int i = 1; i <= n; i++)
{
cin >> w[i] >> v[i];
}
for (int i = 1; i <= m; i++)
{
cin >> l[i] >> r[i];
}
// 查找并输出结果
cout << find() << endl;
return 0;
}
【运行结果】
5 3 15
1 5
2 5
3 5
4 5
5 5
1 5
2 4
3 3
10
浙公网安备 33010602011771号