BZOJ4518 && SDOi2016 征途

题目的意思就是给出n段路程:
如样例:1、2、5、8、6
要求分m天走完,使得走完的方差最小,并输出最小方差*m^2。
比如第一天走:1、2、5,总计8,第二天走8、6,总计14。
已知平均值为11,那么答案等于2^2*((8-11)^2+(14-11)^2)/2=36。
我们先准备出前缀和数组sum[n]。
比如对于样例:
sum[1]=1
sum[2]=3
sum[3]=8
sum[4]=16
sum[5]=22
我们将答案进行变形:
ans
=m^2*((d1-平均)^2+(d2-平均)^2+(d3-平均)^2+……+(dm-平均)^2)/m(我们把平方打开)
=m*(d1^2+d2^2+d3^2+……+dm^2+m×平均^2-2×(d1+d2+…+dm)×平均)
=m*(d1^2+d2^2+d3^2+……+dm^2)-sum[n]×sum[n]
好了由于sum[n]是常量,我们只要使d1^2+d2^2+d3^2+……+dm^2最小即可。
动态规划f[i][j]表示走了i段,分成j天,d1^2+d2^2+d3^2+……+dj^2的最小值。
那么显而易见对于递推公式:
f[i][j]=min(f[k][j-1]+(sum[i]-sum[k])^2)其中j-1<= k < i
好了这明显是一个n^3的时间复杂度这是不能接受的。
我们考虑斜率优化。
假设 q < w 且由f[w][j-1]更新到f[i][j]比f[q][j-1]更新到f[i][j]更优,那么有以下式子
f[w][j-1]+(sum[i]-sum[w])^2 < f[q][j-1]+(sum[i]-sum[q])^2
将该式子化简:
(f[w][j]+sum[w]^2-f[q][j]+sum[q]^2)/(2*(sum[w]-sum[q])) < sum[i]
我们将这个东东看成一个斜率yw-yq/xw-xq < sum[i]
既g(w,q)=(f[w][j]+sum[w]^2-f[q][j]+sum[q]^2)/(2*(sum[w]-sum[q]))。
我们要剔除一些点:
设k < j < i,如果g(i,j) < g(j,k),那么j点便永远不可能成为最优解,可以直接将它踢出。
我们假设g(i,j) < sum[i],那么就是说i点要比j点优,排除j点。
如果g(i,j) >=sum[i],那么j点此时是比i点要更优,但是同时g(j,k)>g(i,j)>sum[i]。这说明还有k点会比j点更优,同样排除j点。
这样我们保证当前的序列中都满足如下关系:g(i,i-1) < g(i+1,i),图像是个下凸包。
这里写图片描述
我们看图中ABCD,满足g(i,i-1) < g(i+1,i),图像是个下凸包。
好了我们如何维护呢?
当我们推完f[i][j]时,并准备推f[i+1][j],那么f[i][j-1]应该加入决策点集中,这时假如f[i][j-1]是E点。
如果g(C,D)>g(D,E),我们发现D不可能成为决策点,刚才上面证的。
我们将D踢掉,并判断E能否接着踢掉C,一直到不能踢掉为止。

好了,维护搞定了,那我们如何动态规划呢?
我们正着来:从ABC…往后看。
如果g(B,A)< sum[i],那么B更优,A踢掉,由于下凸包斜率单增,那么不断向后寻找。
知道发现g(s,s-1)>=sum[i],那么说明s-1暂时是最优的就由s-1来动态规划。
具体的维护我们用队列来维护定义head与tail。
当队尾加入新的点时更新tail。
当队首要踢掉点时如g(B,A) < sum[i]时的点A,就++head。
好了见代码:

#include<iostream> 
#include<cstring> 
#include<algorithm> 
#include<cstdio> 
#include<cmath> 
#include<queue> 
#include<vector> 
#include<climits> 
typedef long long ll; 
using namespace std; 
inline ll read() 
{ 
  char ls=getchar();for (;ls<'0'||ls>'9';ls=getchar()); 
  ll x=0;for (;ls>='0'&&ls<='9';ls=getchar()) x=x*10+ls-'0'; 
  return x; 
} 


ll n,m;
ll a[33333];
ll b[33333];
ll sum[33333];
ll best;
ll q[33333];
ll head;
ll tail;
inline double solo(ll x,ll y)
{
    return ((a[x]-a[y]+sum[x]*sum[x]-sum[y]*sum[y])/(2*(sum[x]-sum[y])));
}

int main()
{
    n=read(),m=read();
    for(int i=1;i<=n;++i)
    sum[i]=read(),sum[i]+=sum[i-1];
    for(int i=1;i<=n;++i)
    a[i]=sum[i]*sum[i];

    for(int j=2;j<=m;++j)
    {
        memset(q,0,sizeof(q));
        head=1;tail=1;
        q[head]=j-1;//队列q
        for(int i=j;i<=n;++i)
        {

            //cout<<i<<":"<<endl;
            //cout<<head<<" "<<tail<<endl;
            //for(int i=1;i<=n;++i)
            //{
            //  cout<<q[i]<<" ";
            //} 
            //cout<<endl;

            while(head+1<=tail)
            if(solo(q[head+1],q[head])<sum[i])//判断是否踢掉队首
            ++head;
            else
            break;

            b[i]=a[q[head]]+(sum[i]-sum[q[head]])*(sum[i]-sum[q[head]]);

            while(head+1<=tail)
            if(solo(i,q[tail])<solo(q[tail],q[tail-1]))//判断是否踢掉队尾
            --tail;
            else
            break;

            q[++tail]=i;
        }
        for(int i=1;i<=n;++i)
        a[i]=b[i];//我用了滚动数组
    }
    ll s=m*a[n]-sum[n]*sum[n];
    printf("%lld",s);
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值