• 最佳牛围栏(二分除法时的精度判断和神奇的DP方式)


    题意

    题目

    做法

    做法1

    没错,这个时间复杂度垃圾的做法就是我的做法。

    我们用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)的话会导致整数位出现差错,但是都一样,反正有差错就对了)

    1. 整除。(这种情况很明显存在,而且数据中一般都会有这种情况。)
    2. 无限循环小数或者没除完。
      这种情况我们是可以证明不存在的。
      我们首先要搞明白,对于一个已知(k)位的小数(y)而言,我们如何确定存在(x)使得(?÷x)的商的小数点第(k)位以前都和(y)一致(包括整数位和小数点第(k)位),那么我们就要从余数下手:(?÷x=y……t)((t)是余数),因为进行到了第(k)位的小数除法,根据余数的性质,不难得到下面这个式子:(t*10^k<x(t*10^k是整数)),而且因为(?)是整数,还必须要求(t+x*y)是个整数,要是对于(x)能找到满足要求的(t),就可以说(x)是满足要求的了。
      而对于这里,我们都说了非整除情况,所以(t>0),考虑一个第(4-8)位都是(0)的小数(y),乘以(1-100000)中的任意一个数字,因为(t*10^k<x(t*10^k是整数)),所以(t)(4-8)位才允许有数字,而小数点后第(1-3)位包括整数位都不允许有数字,但是(t+x*y)为整数,而(y)(4-8)都是(0),这样(t+x*y)绝对不是个整数,矛盾,所以当(k=8)时,这种情况不存在,而当(k<8)时,一样存在,如:(100÷33333=0.003000余0.001)

    所以,输出(l),在一情况下会导致错误,那(r)呢?如果(l)可以(r)不行呢?

    仔细考虑,貌似仅当(y)(4-8)位都是(9)的情况才有可能。

    1. 整除,那么就变成了:(frac{y*10^8}{10^8}),但是因为(y)的小数点后(4-8)位都是(9),所以分子分母没法约分,而(10^8)不在我们的数域当中,所以不存在。
    2. 无限循环小数或者没除完。
      这种情况我们一样可以证明不存在,根据(t+x*y)为整数,可以得到(整数-x*y)就是(t),因为(y)(4-8)位都是(9),所以整数(-y)中的第(8)位一定是(1),而在整数(-x*y)中,前三位对余数是没有影响的,只需要考虑前三位能不能变成(0)的问题(这里默认乘完之后在考虑进位了之后可以变成(0),否则直接不存在了)所以不难知道余数其实就是(10^{-8}*x)但是,这又违背了(t*10^k<x(t*10^k是整数)),所以不存在。
      当然,当(k<8)时存不存在呢?存在这种情况,因为其实这里的等于(10^{-8}*x)是采用了类似同余的手法求出来的,余数是(10^{-3}),但是如果(k<8),那么余数就会到(10^{-2})甚至更大,这样子的话就会把小数点后三位考虑进来,而这三位并不是严格非(0)的,考虑一下的例子:(0.89999*90001=80999.99999,0.089999*90001=8099.999999,0.0089999*90001=809.9999999)很明显,当(k<8)时,均存在反例(其实就是不同严格证明余数是(10^{-k}*x)了),但是数据较水,概率较小,可能可以AC吧。

    所以保险起见,在二分除法的结果时,如果数域是(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
    ",x);
    	return  0; 
    }
    

    做法2

    这个又要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])的确是最优情况(这里的最优情况还包括平均值一样时个数最少)。

    1. dp[i]<dp[i-1] (f[i]>f[i-1])
      这种分两种情况,一种是(a[i]>=f[i-1]),那么这个时候,(dp[i])$(dp[i-1]-1)$能够加入进来,说明$dp[i]$((dp[i-1]-1))是能为平均值做贡献的,设(k=dp[i-1]-dp[i]),那么表达一下就是:(frac{a[dp[i]]+...+a[dp[i]+k-1]}{k}>f[i]),此时我们称它为能作贡献,但是这样同样能为(dp[i-1])作贡献,违背了我们的定义,所以不存在这种情况。
      另外一种是(a[i]<f[i-1]),我们接着证不存在,但是它就是存在的啊,这种情况,因此我们先放着。
    2. dp[i]>dp[i-1](f[i]>=f[i-1])
      还是分三种情况:(a[i]<f[i-1]),由于(a[i])一定会拖后腿,所以(f[i-1]<f[i])(因为如果(f[i-1])很优秀,那把(a[i])删了变成(f[i-1])不是更优秀吗,而且在上面的第二种情况中,即使违背定义,也是(f[i]<f[i-1]),得证)
      那么这种情况说明(frac{t[i-1]+a[i]}{c[i]}<f[i]<f[i-1]),所以(t[i-1]+a[i]<c[i]*f[i-1]),这只能说明一个事情,就是([dp[i-1],dp[i]-1])这一部分在拖后腿!!!所以(dp[i])不是最优秀的,违背定义,不存在。
      a[i]=f[i-1],就和上面第一种情况一样的证法,你不能给我做贡献同样不能给(dp[i-1])做贡献。
      a[i]>f[i-1](不可能出现(f[i]=f[i-1]),把(f[i-1])算上(a[i])都大于(f[i-1])了),这种情况说明,对于([dp[i-1],dp[i]-1])中的数字而言,能为(i-1)做贡献,但是不能为(i)做贡献,说明这些数字刚好就是卡在(f[i-1])(f[i])之间,那么就说明(dp[i])(i)中一定有可以给平均值作贡献的数字,看我们的更新方法有两种,另外一种就是([i-k+1,i])的,所以,我们现在就是要证明:(dp[i]+k-1)一定触发了([i-k+1,i])这个更新方式,因为如果不触发,说明(dp[i])也不能做贡献((dp[i]+1)(dp[i]+k-1)那叫可能),那么不就自相矛盾了吗,所以按照这个更新思路,可以说明(dp[i-1]=dp[i])(如果你不理解,那么这个至少可以说明(dp[i-1]>=dp[i]),结合前面的就可以说明这个等式的),所以这个情况也是不存在的。(其实这种情况也是唯一一种会用到第二种更新方式的情况了)

    那么,对于那个不能证明的我们应该怎么处理呢?

    我们只需要明白,这个不能处理的东西不会对我们的答案作出任何的负贡献,因为([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)怎么处理无所谓,反正对于结果没有多大影响。

    这里顺便点一下这种做法的一些性质:

    1. 对于(dp[i]),如果他是采用第一种方法更新的话,(a[dp[i]]>f[i])
    2. 对于区间([i,j]),他能更新平均值(x)当且仅当区间([i,j])(a)之和大于(x*(j-i+1))
    3. 如果一个区间的平均值是(y)且这个区间能给平均值(x)做贡献,那么(x<y)且贡献后的平均值小于(x)
    4. (a[dp[i]-1]<=f[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
    ", ans);  
      return 0;
    }
    
    作者:gznb
    链接:https://www.acwing.com/solution/content/3115/
    来源:AcWing
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    
  • 相关阅读:
    IOC架构设计之ButterKnife源码&原理(二)下篇
    IOC架构设计之ButterKnife源码&原理(二)中篇
    IOC架构设计之ButterKnife源码&原理(二)上篇
    IOC架构设计之控制反转和依赖注入(一)
    RXJava之线程控制Scheduler(四)
    RXJava之变换(三)
    RXJava之扩展的观察者模式(二)
    微信小程序弹框提示绑定手环实例
    SpringBoot进阶教程 | 第四篇:整合Mybatis实现多数据源
    SpringCloud核心教程 | 第一篇: 使用Intellij中的Spring Initializr来快速构建Spring Cloud工程
  • 原文地址:https://www.cnblogs.com/zhangjianjunab/p/13402626.html
Copyright © 2020-2023  润新知