没错,这个时间复杂度垃圾的做法就是我的做法。
我们用double进行二分,二分可能的平均值,这个平均值是否满足要求是满足二分性的。
但是\(check\)函数怎么打呢?也就是说我们要确认一个数列能否构成这样的平均值\(x\),该怎么做呢?我们只需要把每个数字减去\(x\),然后判断是否有区间和\(>=0\)即可,而对于判断区间和,其实就是叫我们找到最大的区间和,我们先做一遍前缀和得到\(f\)数组,然后对于\(f[i]\),我们如何确定以\(i\)为结尾的最大的前缀和呢?很明显我们只需要减去\([1,i-F]\)区间中的最小的\(f\)就行了,而对于后面这个最小值,我们可以\(O(n)\)预处理,也可以边查询边处理,都没有问题。
那么,一个十分讨厌的事情来了,精度问题。
那么到了最后是输出\(l\)还是输出\(r\)呢?(这里以\(eps=1e-8\)为例子),看到大佬们都输出了\(r\),然后我果断的输出了\(l\),炸了。。。
那么为什么输出\(l\)会炸,\(r\)不会炸呢?
思考一下,题目相当于确认小数点后\(3\)位,而我们的二分可以确认答案在\([l,r]\)区间,而\(r-l<=eps\),一般情况下,\(l,r\)前\(3\)位是一样的,就算因为\(eps\)导致第五第六位不一样也比较正常,但是恰恰就存在几种情况,会使\(l,r\)小数点后三位不同。
注:这里的分母的数域是在\(1-100000\),位于为什么,因为是平均数啊。
首先是\(r\)是正确的,但是\(l\)错了,所以\(r-eps\)在减法中的前\(3\)位退位了,很容易知道,第\(4\)位到第\(8\)位都是\(0\),否则不可能会让前\(3\)位退位,那么现在就分成了两种情况。(注:如果第\(1-3\)位都是\(0\)的话会导致整数位出现差错,但是都一样,反正有差错就对了)
所以,输出\(l\),在一情况下会导致错误,那\(r\)呢?如果\(l\)可以\(r\)不行呢?
仔细考虑,貌似仅当\(y\)的\(4-8\)位都是\(9\)的情况才有可能。
所以保险起见,在二分除法的结果时,如果数域是\(1-10^q\),而要求到小数点后\(p\)位,那么这边建议\(eps\)最好等于\(10^{-q-p}\)比较保险,当然爆精度当我没说,而且在输出的时候输出\(r\)会整体比\(l\)优秀。
还有就是基础精度\(eps\)不要设置太小,否则真的可能出现精度误差导致错误的。(不要问我为什么知道)
#include<cstdio>
#include<cstring>
#define N 110000
using namespace std;
double eps=1e-8,a[N],b[N],c[N];
inline double mymax(double x,double y){return x>y?x:y;}
inline double mymin(double x,double y){return x<y?x:y;}
int n,m;
bool check(double x)
{
for(int i=1;i<=n;i++)b[i]=b[i-1]+a[i]-x;
for(int i=1;i<=n;i++)c[i]=mymin(c[i-1],b[i]);
for(int i=m;i<=n;i++)
{
if(b[i]-c[i-m]>=0)return 1;
}
return 0;
}
int main()
{
// freopen("cowfnc.8.in","r",stdin);
double l=0,r=0,mid;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){scanf("%lf",&a[i]);r=mymax(r,a[i]);}
while(r-l>eps)
{
mid=(l+r)/2;
if(check(mid))l=mid;
else r=mid;
}
int x=r*1000;
printf("%d\n",x);
return 0;
}
这个又要STOhttps://www.acwing.com/solution/content/3115/奆佬的了。
---来自奆佬说的话:
算法 前缀和+DP
使用DP,dp[i], 表示第 dp[i] 个位置到 第 i 个位置的最优。(dp[i], i].例如 dp[9] = 3, 是表示(3, 9]也就就是,4~9,6个数的最优。那么对于 第 i 个位置来说,有两种情况 要么根据第 i - 1 个位置的最优推导,要么自己和前 k - 1 个,组成 k 个一起。
时间复杂度分析:O(n)
当然,至于为什是对的,仍然会在这里推导一下,因为我太菜了,所以要写证明QAQ。(注,这里我的\(dp[i]\)是表示\([dp[i],i]\)是最优的平均值,而不是\((dp[i],i]\),而\(c[i]\)则是表示构成最优平均值的数量)
对于\(dp[i]\),我们已经知道了\(dp[i-1]\)了,那么\(dp[i]\)的最优情况是什么呢?(为了方便证明,设\(f\)数组为\(dp\)数组构成的平均值,\(t\)数组为\(dp\)的最有平均值的和,如\(dp[i]\)),我们假设\(dp[i-1]\)的确是最优情况(这里的最优情况还包括平均值一样时个数最少)。
那么,对于那个不能证明的我们应该怎么处理呢?
我们只需要明白,这个不能处理的东西不会对我们的答案作出任何的负贡献,因为\([dp[i],dp[i-1]-1]\)这一个部分的平均值肯定小于\(f[i-1]\),所以对于最终答案\(ans(ans>=f[i-1])\),肯定不会作出任何的贡献,所以删掉也没有什么所谓,而且删掉不会影响\(dp[i]>dp[i-1]\)的证明过程,而第一个情况的第一种情况呢(如果是第一种情况中的第二种情况就是又把原来删除的部分的左端点延长了,右端点不变),我们就自动认为在\(i\)的世界中,\(dp[i-1]\)就是所谓的\(1\)吧,真正的\(dp[i]\)就不管它了,所以\(dp[i]=dp[i-1]\)(接下来也这么认为)。
那么对于出现\(ans\)的节点,如果它前面是\(?\)节点(\(?\)表示祖先是\(i\)的某个节点,即从\(i\)开始一直用第一种方式更新到的某个位置),我们要明确一个事情,他肯定是通过二方式继承的,因为\([dp[i],?]\)肯定不会对\(ans\)作出任何贡献,所以\(ans\)肯定要带的累赘越少越好,那么肯定不会把\([dp[i],?]\)全部带上的,那最好的肯定就是用第二种方法啦。(当然,其实只要中间有一个点采用了二方法,就可以跳出这个删除数字的怪圈了)
那么其实从结果上看,反正是采用第二种方法,\(dp[i]\)的取值其实也没有那么必要了(而且也不会影响证明),但是为了代码的简便以及方法简便,我们还是采用\(dp[i]=dp[i-1]\)吧。
说了这么多,对于那个不能证明的概括起来就一句话:这个位置\(i\)怎么处理无所谓,反正对于结果没有多大影响。
这里顺便点一下这种做法的一些性质:
这个DP的思路很巧妙,他不是站在了加一个数字然后把分母\(+1\)这么一个思路,而是站在了看哪些数字对答案有贡献,简称:上帝视角做题。巧妙的把有贡献的收入了囊中,对于绝对没有贡献的,则抛弃。(如果有贡献的话就不敢这么乱处理了。)
/*
最佳牛围栏
*/
#include <bits/stdc++.h>
#define LL long long int
using namespace std;
const int N = 100010;
double sum[N]; //前缀和
int dp[N];
int main(){
int n, k;
cin >> n >> k;
sum[0] = 0.0;
for (int i = 1; i <= n; i++) {
cin >> sum[i];
sum[i] += sum[i - 1];
}
dp[k] = 0; //第k个位置,只可能有一个最优。
for (int i = k + 1; i <= n; i++) {
double tx = double(sum[i] - sum[i - k] ) / k;
double ty = double(sum[i] - sum[dp[i - 1]]) / (i - dp[i - 1]);
if (tx > ty) dp[i] = i - k;
else dp[i] = dp[i - 1];
}
double res = 0;
for (int i = k; i <= n; i++) {
res = max(res, double(sum[i] - sum[dp[i]]) / double(i - dp[i]) );
}
int ans = res * 1000;
printf("%d\n", ans);
return 0;
}
作者:gznb
链接:https://www.acwing.com/solution/content/3115/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
原文:https://www.cnblogs.com/zhangjianjunab/p/13402626.html