NOIP2011 聪明的质检员 二分 前缀和

题目链接:https://www.luogu.com.cn/problem/P1314
题目大意:现有n个矿石从左到右排列,每个矿石有自己的重量 w i w_i wi和价值 v i v_i vi,现给定 m m m个区间 [ l i , r i ] [l_i,r_i] [li,ri]和预期标准值 S S S,我们需要选出一个参数 W W W,使每个区间检验值 y i y_i yi之和最接近预期标准值 S S S(即作差的绝对值 ∣ s − y ∣ |s-y| sy最小),检验值的计算方法如下:

对于一个区间而言: y i = ∑ j = l i r i [ w j ≥ W ] × ∑ j = l i r i [ w j ≥ W ] v j y_i=\sum\limits^{r_i}_{j=l_i}[w_j \geq W]\times\sum\limits^{r_i}_{j=l_i}[w_j \geq W]v_j yi=j=liri[wjW]×j=liri[wjW]vj
这个算式可以这样理解:一个区间中重量大于标准值的个数乘总价值。
(到这里前缀和求区间和的思路就比较明显了,先往下看)

数据范围如下图所示:
在这里插入图片描述
对于 100 % 100\% 100%的数据,需要我们用 O ( n log ⁡ n ) O(n\log n) O(nlogn)的算法,我们自然可以想到二分法。对于二分答案法,我们首先需要考虑单调性,我们可以知道,给出的矿石重量是一定的,当我们增加参数 W W W的大小时,每个单调区间的满足 w j ≥ W w_j\geq W wjW的矿石数量与增加前一定减少或相等,那么每个区间的检验值一定减少或相等。我们可以有以下结论:

选取的参数越高,我们得到的检验结果 y y y(即所有区间检验值之和)越小。这个具有单调性的结论说明了我们使用二分答案做法的合理性。

那么我们二分答案的边界修改应该如何操作呢?在一开始时,我们定义 l = 1 l=1 l=1 r = + i n f r=+inf r=+inf m i d = l + r > > 1 mid=l+r>>1 mid=l+r>>1。在题目中我们需要尽量地靠近一个值 s s s,也就是尽可能地减小绝对值。
当基于 W = m i d W=mid W=mid的检测结果 y y y的值比预期标准值 s s s大时,说明更优的答案应当在 m i d + 1 mid+1 mid+1 r r r之间,修改边界: l = m i d + 1 l=mid+1 l=mid+1,并比较 W W W与当前最优答案 a n s ans ans的值。
当基于 W = m i d W=mid W=mid的检测结果 y y y的值比预期标准值 s s s小时,说明更优的答案应当在 l l l m i d − 1 mid-1 mid1之间,修改边界: r = m i d − 1 r=mid-1 r=mid1,并比较 W W W与当前最优答案 a n s ans ans的值。
y = = s y==s y==s时,直接输出0然后结束程序。

这样我们的二分答案的思路就有了。考虑在 O ( N ) O(N) O(N)的时间复杂度内计算出给定 W W W时的检测结果 y y y。我们进一步简化问题,在 W W W已知的情况下,我们对与 W W W有关的特殊数组进行m次区间查询。问题的重心就转移到如何在O(1)的时间复杂度计算出一个区间。此时我们就应该联想到一些特殊的数据处理方法,诸如前缀和,差分,树状数组,线段树等等,这里肯定不会大材小用后两个,后两个的区间查询复杂度也比较高。这里直接引入前缀和,为什么是前缀和呢?
在这里插入图片描述
假设当前有这样的一个原始数组。
在这里插入图片描述
我们做这样的处理后获得前缀和数组,该数组每个元素 s u m [ i ] = a [ 1 ] + a [ 2 ] + . . . + a [ i ] sum[i]=a[1]+a[2]+...+a[i] sum[i]=a[1]+a[2]+...+a[i]。这样处理的好处是什么呢?我们有了这个数组,就可以在O(1)的复杂度内求出原始数组的一个区间和。即 a [ i ] + a [ i + 1 ] + a [ i + 2 ] + . . . + a [ j − 1 ] + a [ j ] = s u m [ j ] − s u m [ i − 1 ] , ( i < j ) a[i]+a[i+1]+a[i+2]+...+a[j-1]+a[j]=sum[j]-sum[i-1], (i<j) a[i]+a[i+1]+a[i+2]+...+a[j1]+a[j]=sum[j]sum[i1],(i<j),便求出了原始数组中 [ i ,   j ] [i,\space j] [i, j]的区间数值之和

我们观察检验值的计算公式,其中包含有两个未知项,分别是一个满足条件的数量区间和与一个满足条件的价值区间和,我们分两个数组 i t e m s u m [ i ] item_sum[i] itemsum[i] v a l u e s u m [ i ] value_sum[i] valuesum[i]分别储存两种前缀和。具体的处理方法是这样:

for(int i=1;i<=n;i++)
    {
        if(w[i]>=x)
        {
            value_sum[i]=value_sum[i-1]+1;
            item_sum[i]=item_sum[i-1]+v[i];
        }
        else
        {
            value_sum[i]=value_sum[i-1];
            item_sum[i]=item_sum[i-1];
        }
    }

可以看出,我们遍历了两个前缀和数组,若遍历到当前的矿石满足重量条件,则将他的值加入前缀和数组中,若不满足,则保持之前的前缀和数组不变。这样我们就完成了前缀和数组的初始化。
接下来就是查询操作,这一步很简单:

long long cal()
{
    long long ans=0;
    for(int i=1;i<=m;i++)
    {
        ans+=(item_sum[r[i]]-item_sum[l[i]-1])*(value_sum[r[i]]-value_sum[l[i]-1]);
    }
    return ans;
}

如上,每个区间的查询结果就是两个区间的前缀和相乘,把所有结果相加就是取当前参数 W W W的检验值。记得开 l o n g   l o n g long\space long long long,不然会炸裂。计算结束后回到之前的二分思路,就完成解题了。

下面给出AC代码:

#include<bits/stdc++.h>
using namespace std;
int w[200200],v[200200];
int l[200200],r[200200];
int n,m;
long long s;
long long item_sum[200200],value_sum[200200];
int max_w=-1;
long long min(long long x,long long y)
{
    if(x>y) return y;
    else return x;
}
void modify(int x)
{
    for(int i=1;i<=n;i++)
    {
        if(w[i]>=x)
        {
            value_sum[i]=value_sum[i-1]+1;
            item_sum[i]=item_sum[i-1]+v[i];
        }
        else
        {
            value_sum[i]=value_sum[i-1];
            item_sum[i]=item_sum[i-1];
        }
    }
}
long long cal()
{
    long long ans=0;
    for(int i=1;i<=m;i++)
    {
        ans+=(item_sum[r[i]]-item_sum[l[i]-1])*(value_sum[r[i]]-value_sum[l[i]-1]);
    }
    return ans;
}
int main()
{
	cin>>n>>m>>s;
	for(int i=1;i<=n;i++)
	{
		cin>>w[i]>>v[i];
		max_w=max(max_w,w[i]);
	}
	for(int i=1;i<=m;i++)
	{
		cin>>l[i]>>r[i];
	}
	int begin=1,end=max_w+1,mid;
	long long anss=9999999999999999;
	while(begin<=end)
	{
		mid=begin+end>>1;
		modify(mid);
		long long ans=cal();
		if(ans>=s)
		{
			begin=mid+1;
		}
		else
		{
		    end=mid-1;
		}
        anss=min(anss,fabs(s-ans));
	}
	cout<<anss;
 }

有疏漏和讲解不清的地方请私信留言,祝你新的一天RP++。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值