洛谷 [NOIP2011 提高组] 聪明的质监员

115 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情 这应该是本蒟蒻第一道不看题解写出来的绿题(虽然大佬可能觉得很水(轻点骂)),我们来看题。

[NOIP2011 提高组] 聪明的质监员

题目描述

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

1 、给定m m 个区间 [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 为矿石编号。

这批矿产的检验结果 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,000 1 ≤n ,m≤5,000

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

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

PS:PS:有一说一,受高中数学的影响,我第一眼竟然看不懂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 这个式子,后来发现这个式子意思是在[li,ri][l_i,r_i]区间内wwW≥W的矿产品的个数之和与这些矿产品对应的vv值的和的乘积。

分析

现在我们正式对这题分析一下,这题的题意就是给定每个矿产品的wwvv值,给了mm个区间,要我们找出一个参数WW,使得每个区间里面W≥W的矿产品数量之和与它们对应的vv值之和的乘积之和与给定的SS差的绝对值最小。看似有点绕,其实,第一思路很直接,从11开始枚举WW的值,然后取最小的sy|s-y|的值......(编不下去了,复杂度直接爆炸),这里代码就不展示了,然后第二思路,我们发现对WW的取值明显是一个单调的过程,因此我们想到了二分答案,二分WW的值,然后枚举每个区间,找到最小的那个sy|s-y|,具体代码实现

代码1

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string> 
#include <queue>
#include <vector>
#include <map>
#include <set>
#include <stack> 
#include <cmath>
#include <iomanip>
#define int long long
#define AC return
#define Please 0
using namespace std;
const int N=201010;
const double eps=1e-6;
int n,m,s;
int le[N],ri[N],maxn=-1;
typedef pair<int,int>PII;
typedef unsigned long long ull; 
bool flag[N];
inline int read(){//快读 
    int x=0,f=1;char ch=getchar();
    while(ch<'0' || ch>'9'){
        if(ch=='-') f=-1;
        ch=getchar();
    }
    while(ch>='0' && ch<='9'){
        x=x*10+ch-'0'; 
        ch=getchar();
    }
    AC x*f;
}
struct good{
	int w,v;
}a[N];
good b[N];
int check(int x){
	int sum=0;
	for(int i=1;i<=n;i++){
		b[i]=a[i];
	}
	for(int i=1;i<=m;i++){
		int num=0,val=0;
		for(int j=le[i];j<=ri[i];j++){
			if(a[j].w>=x){
				num++;
				val+=a[j].v;
			}
		}
		sum+=num*val;
	}
	return sum;
}
signed main(){
	cin>>n>>m>>s;
	for(int i=1;i<=n;i++){
		cin>>a[i].w>>a[i].v;
		maxn=max(maxn,a[i].w);
	}
	for(int i=1;i<=m;i++){
		cin>>le[i]>>ri[i];
	}
	int l=0,r=maxn+1,ans=1e18;
	while(l<r){
		int mid=l+r+1>>1;
		ans=min(ans,abs(check(mid)-s));
		if(check(mid)>=s){
			l=mid;
		}
		else r=mid-1;
	}
	cout<<ans<<endl;
    AC Please;
}

当然这个复杂度显然过不去因为O(mnlogn)O(mnlogn)也几乎tt飞了,于是得到了5050分,随后我就在思考怎么优化(其实大佬一眼就能看出来这个优化),我们发现由于是要W≥W对应的ww的值相加,因此我们考虑到前缀和,只要把W<W的值的vvww都变成00,再把W≥Wvv值都变成11,然后可以用前缀和预处理出vvww的和,那样O(1)O(1)的复杂度就可以把每个yiy_i的值算出来了,最后的总时间复杂度是O(mlogn)O(mlogn),完美解决问题~。

代码

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string> 
#include <queue>
#include <vector>
#include <map>
#include <set>
#include <stack> 
#include <cmath>
#include <iomanip>
#define int long long
#define AC return
#define Please 0
using namespace std;
const int N=201010;
const double eps=1e-6;
int n,m,s;
int le[N],ri[N],maxn=-1,num[N],value[N];
typedef pair<int,int>PII;
typedef unsigned long long ull; 
bool flag[N];
inline int read(){//快读 
    int x=0,f=1;char ch=getchar();
    while(ch<'0' || ch>'9'){
        if(ch=='-') f=-1;
        ch=getchar();
    }
    while(ch>='0' && ch<='9'){
        x=x*10+ch-'0'; 
        ch=getchar();
    }
    AC x*f;
}
struct good{
	int w,v;
}a[N];
good b[N];
int check(int x){
	int sum=0;
	for(int i=1;i<=n;i++){
		b[i]=a[i];//先要复制一份值,防止a发生改变
	}
	for(int i=1;i<=n;i++){
		if(b[i].w<x) b[i].w=b[i].v=0;
		else b[i].w=1;
	}
	for(int i=1;i<=n;i++){
		num[i]=num[i-1]+b[i].w;
		value[i]=value[i-1]+b[i].v;
	}
	for(int i=1;i<=m;i++){
		sum+=(num[ri[i]]-num[le[i]-1])*(value[ri[i]]-value[le[i]-1]);
	}
	return sum;
}
signed main(){
	cin>>n>>m>>s;
	for(int i=1;i<=n;i++){
		cin>>a[i].w>>a[i].v;
		maxn=max(maxn,a[i].w);
	}
	for(int i=1;i<=m;i++){
		cin>>le[i]>>ri[i];
	}
	int l=0,r=maxn+1,ans=1e18;//l是可以取到0滴,QAQ。
	while(l<r){
		int mid=l+r+1>>1;
		ans=min(ans,abs(check(mid)-s));
		if(check(mid)>=s){
			l=mid;
		}
		else r=mid-1;
	}
	cout<<ans<<endl;
    AC Please;
}

这题我还学到了一点,就是在这种取绝对值最小的二分,每次都要取每个midmidminmin而不是和古老的二分一样,因为在00的左右都可能出现正确答案。 希望能帮助到大家(QAQQAQ)!