2 条题解

  • 2
    @ 2023-8-7 14:45:50

    吐槽一下,这个质检员不会被开除么。。。。

    这个题很明显的二分枚举,但是还有一个前缀和有点坑人。

    这题题其实点不多,就两个关键点: 第一:二分的判断。

    可以看到:在W取0时,所有的区间内的矿石都可以选上,

    而在W大于最大的质量时,所有的矿石都选不上。

    然后简单算一下就发现:

    W越大,矿石选的越少,W越小,矿石选的越多。 所以,随着W增大,Y值减小;

    所以:二分的判断条件出来了:

    当Y>sY>s 时,需要增大WW来减小YY,从而∣Y−s∣∣Y−s∣变小;

    当YsYs时,∣Y−s∣0∣Y−s∣0;

    当Y<sY<s时,需要减小W来增大YY,从而∣Y−s∣∣Y−s∣变大; 第二:前缀和。

    我们在计算一个区间的和时(虽然这里是两个区间和再相乘,但没关系)

    通常是用前缀和的方法来缩减时间,直接模拟是n2n2的,而前缀和成了2∗n2∗n

    大大的优化了时间,前缀和不会的去先学前缀和,我默认大家都会,就不赘述了。

    很显然:

    在w[i]>=Ww[i]>=W时这个i矿石会在统计里(若<W<W就不管它了直接pre[i]=pre[i−1]pre[i]=pre[i−1]),

    矿石价值和是:prev[i]=prev[i−1]+v[i]prev​[i]=prev​[i−1]+v[i],前面的和加上当前这一个i矿石;

    矿石数量和是:pren[i]=pren[i−1]+1pren​[i]=pren​[i−1]+1,数量加1嘛。

    然后最后算的时候用右端点r-(左端点l-1)就可以了 注意:谨记所前缀和时要pre[r]-pre[l-1],这个‘-1’不要忘!

    然后就没啥了,给上代码:

    (当然,一些基础的“lld”之类的你要注意,不多赘述了)

    #include<bits/stdc++.h>
    using namespace std;
    const int maxn=200010;
    int w[maxn],v[maxn],l[maxn],r[maxn];
    long long pre_n[maxn],pre_v[maxn];
    long long Y,s,sum;
    int n,m,mx=-1,mn=2147483647;
    bool check(int W)
    {	
    Y=0,sum=0;
    memset(pre_n,0,sizeof(pre_n));
    memset(pre_v,0,sizeof(pre_v));
    for(int i=1;i<=n;i++)
    {
    if(w[i]>=W) pre_n[i]=pre_n[i-1]+1,pre_v[i]=pre_v[i-1]+v[i];
    else pre_n[i]=pre_n[i-1],pre_v[i]=pre_v[i-1];
    }
    for(int i=1;i<=m;i++)
    Y+=(pre_n[r[i]]-pre_n[l[i]-1])*(pre_v[r[i]]-pre_v[l[i]-1]);
    
    sum=llabs(Y-s);
    if(Y>s) return true;
    else return false;
    
    }
    int main(){
    //	freopen("qc.in","r",stdin);
    //	freopen("qc.out","w",stdout);
    scanf("%d %d %lld",&n,&m,&s);
    for(int i=1;i<=n;i++)
    {
    scanf(" %d %d",&w[i],&v[i]);
    mx=max(mx,w[i]);
    mn=min(mn,w[i]);	
    }
    for(int i=1;i<=m;i++)
    scanf(" %d %d",&l[i],&r[i]);
    int left=mn-1,right=mx+2,mid;  //这里有的人说要特判左右端点的check,但是其实你把left开成mn-1,right开成mx+2(注意取mx+1时即为W比所有都大,Y是0,这个情况要考虑,所以+2包含mx+1)就可以包含左右端点的check了,会简单点。
    long long ans=0x3f3f3f3f3f3f3f3f;//ll 范围内的无穷大,近似于(maxll/2)的大小
    while(left<=right)
    {
    mid=(left+right)>>1;
    if(check(mid)) 	left=mid+1;
    else right=mid-1;
    if(sum<ans) ans=sum;
    }
    printf("%lld",ans);
    return 0;
    }
    
    • 0
      @ 2023-11-29 18:09:23

      二分+前缀和

      这道题让我深刻感受到了前缀和的灵活性,和对二分的恨。我有一点不解:按理来说(sums>s) 等价于 (sums-s),但是代码中如果换成后者就wa了。😕 具体题解楼上说过了。那我就分析一下题目好了:那一长串公式实际就是有一个不确定的W。然后在一个区间内(区间为矿石的编号,端点可取)找到重量>=W的所有矿石,然后将他们的数量*他们所有价值之和,得到一个y值。然后像这样计算所有区间的y后把他们加在一起得到sum。最后的检验值就是|sum-s|。 难点在于对前缀和的运用和理解,这是个好东西。真正理解后你会对前缀和有一个更深刻的认识。

      #include<bits/stdc++.h>
      using namespace std;
      #define ll long long
      ll n,m,s,A[200010][2],B[200010][2],minn=LONG_LONG_MAX,maxx=-1,Asum[200010][2]={0},ans = LONG_LONG_MAX ,sum,sums;
      bool check(ll k){
      	sums=0,sum=0;
      	memset(Asum,0,sizeof(Asum));
      	for(int i=1;i<=n;i++){    //前缀和的计算 
      		if(A[i][0] >= k){
      			Asum[i][0] = Asum[i-1][0] + 1;
      			Asum[i][1] = Asum[i-1][1] + A[i][1];
      		}
      		else{
      			Asum[i][0] = Asum[i-1][0];
      			Asum[i][1] = Asum[i-1][1];
      		}
      	}
      	for(int i=1;i<=m;i++){
      		sums += ( Asum[B[i][1]][0] - Asum[B[i][0]-1][0] )*( Asum[B[i][1]][1] - Asum[B[i][0]-1][1] );
      	}
      	sum = abs(sums-s);
      	if(sums>s) return 1;
      	return 0;
      } 
      int main(){
      	scanf("%lld%lld%lld",&n,&m,&s);
      	for(int i=1;i<=n;i++){
      		scanf("%lld%lld",&A[i][0],&A[i][1]);
      		minn = min(minn,A[i][0]);
      		maxx = max(maxx,A[i][0]);
      	}
      	for(int i=1;i<=m;i++){
      		scanf("%lld%lld",&B[i][0],&B[i][1]);
      	}
      	ll l = minn-1, r = maxx+2,mid=0;
      	while(l<=r){    //二分 
      		mid = (l + r) >> 1;
      		if(check(mid)) l = mid + 1;
      		else r = mid - 1;
      		ans = min(ans,sum);
      	}
      	printf("%lld",ans);
      	return 0;
      } 
      
      • 1

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

      信息

      ID
      1545
      时间
      1000ms
      内存
      128MiB
      难度
      5
      标签
      递交数
      71
      已通过
      25
      上传者