P1314 聪明的质监员
原题连接 https://www.luogu.org/problemnew/show/P1314
首先题号好评QwQ~ 1314
乍一看此题,发现题目好像有点难理解,先带大家理解一下吧:
意思就是:我们要在第 i 个区间 [ Li , Ri ] 里找到所有的 j,使得 wj >= W,求出这些 j 的价值总和及符合条件的 j 的个数,那么这个区间的贡献就是这个价值总和乘上 j 的个数,然后我们要算所有区间的贡献的总和 Y ,最后输出 Y - S 的绝对值的最小值,其中 W 可以自己定 。
既然 W 可以自己定,那么一个很显然的思路就是我们二分枚举 W,看看哪个 W 最合适不就好啦?
枚举的上下界
我们再回头看那个公式,显然若 W 变大,则 Y 变小(符合条件的 j 的数量少了); 若 W 变小,则 Y 变大;
那么如果 W 一直变小,直到所有矿石的 wi 都大于我们枚举的这个 W 的话,那么不就是所有的矿石都被我们给选上了?这时候就会产生最大的 Y;
同理,当 W 比所有矿石的 wi 都大时,所有的矿石我们都没选,这时候就会产生最小的 Y ;
所以我们便得出来上下界:
下界:最小的 wi - 1 ;
上界:最大的 wi + 2,这里为什么是 + 2 呢?因为 + 1就是所有矿石都不选的情况,我们枚举的时候也要考虑到这种情况。所以再 + 1 就囊括了所有的情况了;
for(int i=1;i<=n;i++)
{
a[i].w=read();
a[i].v=read();
if(a[i].w>maxn_w) maxn_w=a[i].w; //找最大重量
if(a[i].w<minx_w) minx_w=a[i].w; //找最小重量
}
for(int i=1;i<=m;i++) //m个区间
{
nl[i]=read();
nr[i]=read();
}
long long left=minx_w-1; //找上下界,这样就能包含所有情况哦
long long right=maxn_w+2;
二分过程
当我们枚举的 W 求出的 Y 过大时,我们应该增大 W 来使得 Y 变小来尽量接近 S;当我们枚举的 W 求出的 Y 过小时,我们应该减小 W 来使得 Y 变大来尽量接近 S;然后我们在求过过程中对 | Y - S | 一直取min 就行了。
求前缀和
最后一部分就是求每个区间的贡献,显然我们要求出每个区间符合条件的 j 的个数和价值总和,如果我们对于每个区间我们都要 O(n)的枚举一遍的话, 总的时间复杂度应该是 O(nm log n),log n 是二分次数。对于 2e6 的数据,显然炸的妥妥的,那怎么办呢?
我们可以用前缀和鸭,对于我们枚举的每个 W ,我们都先预处理出 价值总和 ,符合条件的个数 这两个前缀和,然后每个区间我们 O(1)算出其贡献值,这样的时间复杂度应该是 O((n+m) log n),比较保险~
对于上面的时间复杂度的估算,本蒟蒻很可能算错了,若发现错误请大佬们指出,感激不尽QwQ~
那么怎么求前缀和?
我们设: pre_num [ i ] 为第 1 个矿石到第 i 个矿石中符合条件的矿石数(条件:wj >= W),pre_v [ i ] 为第 1 个矿石到第 i 个矿石中所有符合条件的矿石的价值总和。
那么我们可以得出递推方程(状态转移方程):
for(int i=1;i<=n;i++)
{
if(a[i].w>=W) //如果第i个矿石满足条件
{
pre_num[i]=pre_num[i-1]+1; //在上一个的基础上加上自己本身
pre_v[i]=pre_v[i-1]+a[i].v;
}
else //不满足条件的话
{
pre_num[i]=pre_num[i-1]; //继承上一个
pre_v[i]=pre_v[i-1];
}
}
这个还是比较显然的吧QwQ~
算区间贡献
前缀和做差大家应该都会,我们有了1~n的前缀和了,那么我们求区间 [ Li , Ri ] 内的符合条件的矿石数,就是区间 [ 1 , Ri ] 内符合条件的矿石数减去区间 [ 1 , Li -1] 内符合条件的矿石数,求区间的价值总和同理:
for(int i=1;i<=m;i++) //m个区间
{
Y+=(pre_num[nr[i]]-pre_num[nl[i]-1])*(pre_v[nr[i]]-pre_v[nl[i]-1]); //求每个区间的贡献和
}
上完整的AC代码:
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
long long read()
{
char ch=getchar();
long long a=0,x=1;
while(ch<'0'||ch>'9')
{
if(ch=='-') x=-x;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
a=(a<<3)+(a<<1)+(ch-'0');
ch=getchar();
}
return a*x;
}
long long n,m,l,r;
long long pre_num[2000001];
long long nl[2000001],nr[2000001];
long long maxn_w,minx_w,minx,S;
long long pre_v[2000001];
struct node
{
long long w,v; //重量w,价值v
}a[2000001];
int work(long long W) //我们枚举的W
{
for(int i=1;i<=n;i++)
{
pre_num[i]=0;
pre_v[i]=0;
}
for(int i=1;i<=n;i++) //预处理
{
if(a[i].w>=W) //如果第i个矿石满足条件
{
pre_num[i]=pre_num[i-1]+1; //在上一个的基础上加上自己本身
pre_v[i]=pre_v[i-1]+a[i].v;
}
else //不满足条件的话
{
pre_num[i]=pre_num[i-1]; //继承上一个
pre_v[i]=pre_v[i-1];
}
}
long long Y=0;
for(int i=1;i<=m;i++) //m个区间
{
Y+=(pre_num[nr[i]]-pre_num[nl[i]-1])*(pre_v[nr[i]]-pre_v[nl[i]-1]); //求每个区间的贡献和
}
long long s=Y-S;
if(s<0) s=-s; //手写取绝对值,用函数总有一堆奇怪的错误
if(s<minx) minx=s; //更新答案
if(Y>S) return 1; //Y太大,需要增大W来减小Y
if(Y<S) return 0; //Y太小,需要减小W来增大Y
if(Y==S) return -1; //正好,一定是最优解
}
int main()
{
n=read();m=read();S=read();
maxn_w=-1314;minx_w=1e18;minx=1e18;
for(int i=1;i<=n;i++)
{
a[i].w=read();
a[i].v=read();
if(a[i].w>maxn_w) maxn_w=a[i].w; //找最大重量
if(a[i].w<minx_w) minx_w=a[i].w; //找最小重量
}
for(int i=1;i<=m;i++) //m个区间
{
nl[i]=read();
nr[i]=read();
}
long long left=minx_w-1; //找上下界,这样就能包含所有情况哦
long long right=maxn_w+2;
while(left<=right)
{
long long mid=(left+right)>>1;
int p=work(mid);
if(p==1) left=mid+1;
if(p==0) right=mid-1;
if(p==-1) break; //找到了最优解,不必再搜了
}
printf("%lld",minx);
return 0;
}
转载于//www.cnblogs.com/xcg123/p/11162052.html
还没有评论,来说两句吧...