NC16597 [NOIP2011]聪明的质监员

题目

题目描述

小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\)。注意:不同区间可能重合或相互重叠。

输出格式

一个整数,表示所求的最小值。

样例 #1

样例输入 #1

5 3 15 
1 5 
2 5 
3 5 
4 5 
5 5 
1 5 
2 4 
3 3

样例输出 #1

10

提示

【输入输出样例说明】

\(W\)\(4\) 的时候,三个区间上检验值分别为 \(20,5 ,0\) ,这批矿产的检验结果为 \(25\),此时与标准值 \(S\) 相差最小为 \(10\)

【数据范围】

对于 $10% $ 的数据,有 \(1 ≤n ,m≤10\)

对于 $30% $的数据,有 \(1 ≤n ,m≤500\)

对于 $50% $ 的数据,有 $ 1 ≤n ,m≤5,000$;

对于 \(70\%\) 的数据,有 \(1 ≤n ,m≤10,000\)

对于 \(100\%\) 的数据,有 $ 1 ≤n ,m≤200,000$,\(0 < w_i,v_i≤10^6\)\(0 < s≤10^{12}\)\(1 ≤l_i ≤r_i ≤n\)

题解

知识点:二分。

由 $$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$$ ,发现 \(y_i\)\(W\) 增大而减小,所以 \(S-Y\)\(W\) 增大而增大,而答案就在 \(S-Y\) 的零点附近,可以二分 \(W\) ,得到最优的 \(W\) 来求得最优的 \(S-Y\)。由于答案并非严格零点,因此最终需要对零点两侧都要计算一遍取最合适的值。

然后,预处理出 \([w_i \ge W]\) 的前缀和 \(cnt[i]\)\([w_i \ge W]v_i\) 的前缀和 \(vsum[i]\) ,能优化复杂度。

于是有 \(y_i=(cnt[r_i] - cnt[l_i-1]) \times (vsum[r_i] - vsum[l_i-1])\) ,得到 \(Y\) 返回 \(S-Y\) 的正负情况。

时间复杂度 \(O(n+m)\)

空间复杂度 \(O(n+m)\)

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;

int n, m;
ll s;
int w[200007], v[200007];
ll cnt[200007], vsum[200007];
ll ans = ~(1LL << 63);

struct area {
    int l, r;
}a[200007];

ll check(int mid) {
    for (int i = 1;i <= n;i++) {
        cnt[i] = cnt[i - 1] + (w[i] >= mid);
        vsum[i] = vsum[i - 1] + (w[i] >= mid) * v[i];
    }
    ll y = 0;
    for (int i = 1;i <= m;i++) {
        y += (cnt[a[i].r] - cnt[a[i].l - 1]) * (vsum[a[i].r] - vsum[a[i].l - 1]);
    }
    ans = min(ans, abs(s - y));///记录最小值,收敛点附近两个点的值刚好能被mid经过
    return s - y >= 0;
    /* check(mid)代表W=mid的s-y 是否>= 0
    若是,则W需要变小;若否,则W需要变大
    Y随W增大而减少, 但增量未知,而我们要求的是|s-y|而非W, 所以需要记录收敛点(变号点)左右两点的最小值 */
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);

    cin >> n >> m >> s;
    for (int i = 1;i <= n;i++) cin >> w[i] >> v[i];
    for (int i = 1;i <= m;i++) cin >> a[i].l >> a[i].r;

    int l = 1, r = 1e6;
    while (l <= r) { ///二分W,因为Y(W)单调减,找到s-y的零点附近即可
        int mid = l + r >> 1;
        if (check(mid)) r = mid - 1;
        else l = mid + 1;
    }
    cout << ans << '\n';
    return 0;
}
posted @ 2022-06-28 19:19  空白菌  阅读(28)  评论(0编辑  收藏  举报