洛谷P1314 [NOIP 2011 提高组] 聪明的质监员(二分+前缀和)

58 阅读1分钟

原题:[www.luogu.com.cn/problem/P13…] 这是一道浪漫的题目

题面:

P1314 [NOIP 2011 提高组] 聪明的质监员

题目描述

小 T 是一名质量监督员,最近负责检验一批矿产的质量。这批矿产共有 nn 个矿石,从 11nn 逐一编号,每个矿石都有自己的重量 wiw_i 以及价值 viv_i。检验矿产的流程是:

  1. 给定 mm 个区间 [li,ri][l_i,r_i]
  2. 选出一个参数 WW
  3. 对于一个区间 [li,ri][l_i,r_i],计算矿石在这个区间上的检验值 yiy_i

yi=j=liri[wjW]×j=liri[wjW]vjy_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

其中 jj 为矿石编号,[p][p] 是指示函数,若条件 pp 为真返回 11,否则返回 00

这批矿产的检验结果 yy 为各个区间的检验值之和。即:i=1myi\sum\limits_{i=1}^m y_i

若这批矿产的检验结果与所给标准值 ss 相差太多,就需要再去检验另一批矿产。小 T 不想费时间去检验另一批矿产,所以他想通过调整参数 WW 的值,让检验结果尽可能的靠近标准值 ss,即使得 sy|s-y| 最小。请你帮忙求出这个最小值。

输入格式

第一行包含三个整数 n,m,sn,m,s,分别表示矿石的个数、区间的个数和标准值。

接下来的 nn 行,每行两个整数,中间用空格隔开,第 i+1i+1 行表示 ii 号矿石的重量 wiw_i 和价值 viv_i

接下来的 mm 行,表示区间,每行两个整数,中间用空格隔开,第 i+n+1i+n+1 行表示区间 [li,ri][l_i,r_i] 的两个端点 lil_irir_i。注意:不同区间可能重合或相互重叠。

输出格式

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

输入输出样例 #1

输入 #1

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

输出 #1

10

说明/提示

【输入输出样例说明】

WW44 的时候,三个区间上检验值分别为 20,5,020,5,0,这批矿产的检验结果为 2525,此时与标准值 SS 相差最小为 1010

【数据范围】

对于 10%10\% 的数据,有 1n,m101 ≤n,m≤10

对于 30%30\% 的数据,有 1n,m5001 ≤n,m≤500

对于 50%50\% 的数据,有 1n,m5,0001 ≤n,m≤5,000

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

对于 100%100\% 的数据,有 1n,m200,0001 ≤n,m≤200,0000<wi,vi1060 < w_i,v_i≤10^60<s10120 < s≤10^{12}1lirin1 ≤l_i ≤r_i ≤n

SolutionSolution

题目要求我们让 yy 的值尽可能靠近 ss ,然后分析一波,可以发现如果 yy 的值比 ss 大,说明此时 WW 的值定低了,导致有多余的矿石被选上了;反之,如果 yy 的值比 ss 小,说明此时 WW 的值定高了,导致有些矿石没被选上。那么这个时候我们可以发现,yy 的值是随 WW 的值一同变化的,当 WW 增大,则 yy 的值减小,反之当 WW 减小,则 yy 的值增大。这个函数对应关系满足单调性,所以我们可以考虑二分 WW 来求解。

具体我们需要确定二分的上下界,显然我们只要找到所有矿石中的最小重量和最大重量就可以了,因为取更小或更大的上下界所得到的效果都是一样的,这两个值就已经涵盖了所有矿石的范围。

那么新的问题出现了,我们应该如何根据每一次二分给出的 WW 的值来计算这么多区间上分别的贡献值,如果直接暴力枚举的话,显然时间复杂度是 O(nm)O(nm) 最坏,不可行,所以我们考虑进行优化。

分解一下题目给出的式子:

yi=j=liri[wjW]×j=liri[wjW]vj 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

对于每个区间,我们需要快速计算出其中满足 wiWw_i \ge Wii 的数量,以及这些位置的和 j=liri[wjW]vj\sum_{j=l_i}^{r_i}[w_j \ge W]v_j 。来回想一下我们有什么办法可以用来快速求区间和,我们可以用线段树、树状数组、前缀和。那么这里由于我们是静态区间,不要求修改所以线段树和树状数组是不必要的,且查询速度为 O(logn)O(logn) 慢于前缀和的 O(1)O(1) ,所以我们考虑使用前缀和来维护区间信息。

对于每一个 WW ,我们首先扫一遍 ww 数组,定义 preCntipreCnt_i 为位置 11 到位置 ii 出现的所有满足条件的位置的个数,preValipreVal_i 为位置 11 到位置 ii 出现的所有满足条件的位置的价值即 viv_i 之和。则有预处理:

preCnti=preCnti1+[wiW] preCnt_i=preCnt_{i-1}+[w_i \ge W]
preVali=preVali1+[wiW]vi preVal_i=preVal_{i-1}+[w_i \ge W]v_i

综上我们就可以做到快速求出每个区间的对应信息,则 checkcheck 函数的时间复杂度为 O(n+m)O(n+m) ,总体时间复杂度为 O((n+m)logW)O((n+m)logW)

其实一开始我还直接把全部的出现次数乘上所有区间的价值和来着...没看清公式。

CodingCoding

#include <iostream>
#include <cstring>
#include <iomanip>
#include <cmath>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;

#define ll long long
#define ull unsigned long long
#define debug(x) cout << #x << "=" << x << "\n";

int n, m;
ll s;
const int maxn = 2e5 + 10;
ll w[maxn], v[maxn];
int l[maxn], r[maxn];
ll diff[maxn], cnt[maxn];
ll max_val = 0, min_val = LLONG_MAX;
ll ans = LLONG_MAX;

ll calc(ll W)
{
    static ll pre_cnt[maxn], pre_val[maxn];
    pre_cnt[0] = pre_val[0] = 0;

    for (int i = 1; i <= n; i++)
    {
        pre_cnt[i] = pre_cnt[i - 1] + (w[i] >= W);
        pre_val[i] = pre_val[i - 1] + (w[i] >= W ? v[i] : 0);
    }

    ll res = 0;
    for (int i = 1; i <= m; i++)
    {
        ll now_cnt = pre_cnt[r[i]] - pre_cnt[l[i] - 1];
        ll now_val = pre_val[r[i]] - pre_val[l[i] - 1];
        res += now_cnt * now_val;
    }

    return res;
}

void binary(ll left, ll right)
{
    ll mid;

    while (left <= right)
    {
        mid = ((left + right) >> 1);
        ll res = calc(mid);

        ans = min(ans, llabs(res - s));

        if (res > s)
            left = mid + 1;
        else
            right = mid - 1;
    }
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m >> s;
    for (int i = 1; i <= n; i++)
    {
        cin >> w[i] >> v[i];
        max_val = max(max_val, w[i]);
        min_val = min(min_val, w[i]);
    }

    for (int i = 1; i <= m; i++)
        cin >> l[i] >> r[i];

    binary(min_val, max_val);

    cout << ans;

    return 0;
}

注意开 longlonglonglong血的教训