题解:洛谷 P1314 聪明的质监员

【题目来源】

洛谷:P1314 [NOIP 2011 提高组] 聪明的质监员 - 洛谷

【题目描述】

小T 是一名质量监督员,最近负责检验一批矿产的质量。这批矿产共有 \(n\) 个矿石,从 \(1\)\(n\) 逐一编号,每个矿石都有自己的重量 \(w_i\) 以及价值 \(v_i\)。检验矿产的流程是:

  1. 给定 \(m\) 个区间 \([l_i,r_i]\)
  2. 选出一个参数 \(W\)
  3. 对于一个区间 \([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

【核心思想】

  1. 问题分析:给定 \(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\)

  2. 算法选择

    • 二分答案 + 前缀和:随着 \(W\) 增大,满足 \(w_j \ge W\) 的矿石数量减少,因此 \(y\) 单调递减。利用单调性二分 \(W\),在 \(O(n+m)\) 的检查函数中,用前缀和快速计算每个区间内满足条件的矿石数量与价值,从而得到 \(y\)
    • 精确搜索:二分出最后一个使 \(y \ge s\)\(W\)(或第一个使 \(y \le s\)\(W\)),并在检查过程中不断更新 \(|s-y|\) 的最小值,最终答案即为所有检查过的 \(y\)\(s\) 的绝对差的最小值。
  3. 关键步骤

    • 读入数据\(n, m, s\),矿石重量 \(w_i\)、价值 \(v_i\),以及 \(m\) 个区间 \([l_i, r_i]\)
    • 定义检查函数 check(W)
      1. 计算前缀和数组 sn[i](满足重量 \(\ge W\) 的矿石数量)和 sv[i](对应价值之和)。遍历 \(i=1..n\),若 \(w_i \ge W\),则 sn[i] = sn[i-1] + 1sv[i] = sv[i-1] + v[i];否则继承前一个值。
      2. 初始化 \(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
      3. 更新全局最小绝对差 ans = min(ans, abs(y - s))
      4. 返回 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 即为答案。
  4. 时间/空间复杂度

    • 时间复杂度:\(O((n+m) \log \max(w_i))\)\(\log\) 约为 20 次,\(n, m \le 2\times10^5\),完全可行。
    • 空间复杂度:\(O(n+m)\),存储数组和区间。
  5. 二分答案 + 前缀和的核心思想

    • 单调性:检验值 \(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
posted @ 2026-08-30 09:20  团爸讲算法  阅读(7)  评论(0)    收藏  举报