POJ2018 Best Cow Fences - 二分【带长度限制的最大平均数子段】
POJ2018 Best Cow Fences
Sol:
题目要求一个平均数最大且长度不小于L的子段,并输出平均数*1000的整数部分(直接截取而非舍入)
以下思路来自PKU李煜东神犇的《算法竞赛进阶指南》
我们将问题转化一下:相当于将序列中每个数都减去同一个值,且最大子段和非负(即能够找到一个子段,使得该子段中所有数都大于等于这个减去的数)。可以发现,这个数具有单调性:对于同一个序列,减去一个大的数后最大子段和非负,则减去一个较小的数后最大子段和必定非负。因此我们考虑二分这个数。
若没有子段长度不小于L的限制,我们可以O(n)扫一遍整个序列,每次将当前元素加进备选答案,遇到负数就用备选答案更新最终答案,然后将备选答案清空。
对于有子段长度限制的情况,我们将子段和转化为前缀和的形式,则答案为\(\max_{1 \leq i \leq n}(sum_i-\min_{1 \leq j \leq i-L}(sum_j))\)
观察到每次\(sum_j\)的决策只会从\(1 \leq j \leq i-L\)到\(1 \leq j \leq i-L+1\),即每次只有一个元素加入决策集合,而没有元素从集合中删除,因此我们可以记录一个minval,表示当前决策集合的最优决策,然后O(n)枚举每一个元素,先更新minval,然后用\(sum_i-minval\)更新ans。若ans非负,说明当前二分的平均数mid还有增大的可能,否则只能减小。
时间复杂度为\(O(nlogn)\)。
Tips:
1.思路: 子段和 -> 前缀和相减
2.思路: 求平均数的做法
3.%.lf 自动进行四舍五入 而%d int()则直接截取整数部分。
4.实数域上的二分写法
double l=-(1<<30),r=(1<<30),mid;
double eps=1e-5;//一般精度eps设置为1e-(精确位数+2)
while(r-l>eps){
mid=(l+r)/2;
if(check(mid)) l=mid;
else r=mid;
}
5.INF开到(1<<30) -INF开到-(1<<30) 若-INF开到0,可能在出现负数解的情况下得到错误答案。
AC CODE:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 100000 + 10;
int read(){
int x=0,f=1;char ch=' ';
while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^'0');ch=getchar();}
return x*f;
}
int n,L;
double a[N];
double tp[N];
double s[N];
bool check(double x){
for(int i=1;i<=n;i++) tp[i]=a[i]-x;
s[0]=0;for(int i=1;i<=n;i++) s[i]=s[i-1]+tp[i];
double ans=-(1<<30),minval=(1<<30);
for(int i=L;i<=n;i++){
minval=min(minval,s[i-L]);
ans=max(ans,s[i]-minval);
}
if(ans>=0) return true;
else return false ;
}
int main(){
// freopen("data.in","r",stdin);
// freopen("sol.out","w",stdout);
n=read(),L=read();
for(int i=1;i<=n;i++){
scanf("%lf",&a[i]);
}
double l=-(1<<30),r=(1<<30),mid;
double eps=1e-5;
while(r-l>eps){
mid=(l+r)/2;
if(check(mid)) l=mid;
else r=mid;
}
printf("%d",int(1000*r));
return 0;
}