bzoj 4518: [Sdoi2016]征途(斜率优化)

4518: [Sdoi2016]征途

Time Limit: 10 Sec   Memory Limit: 256 MB
Submit: 333   Solved: 222
[ Submit][ Status][ Discuss]

Description

Pine开始了从S地到T地的征途。
从S地到T地的路可以划分成n段,相邻两段路的分界点设有休息站。
Pine计划用m天到达T地。除第m天外,每一天晚上Pine都必须在休息站过夜。所以,一段路必须在同一天中走完。
Pine希望每一天走的路长度尽可能相近,所以他希望每一天走的路的长度的方差尽可能小。
帮助Pine求出最小方差是多少。
设方差是v,可以证明,v×m^2是一个整数。为了避免精度误差,输出结果时输出v×m^2。

Input

第一行两个数 n、m。
第二行 n 个数,表示 n 段路的长度

Output

 一个数,最小方差乘以 m^2 后的值

Sample Input

5 2
1 2 5 8 6

Sample Output

36

HINT

1≤n≤3000,保证从 S 到 T 的总路程不超过 30000

Source

[ Submit][ Status][ Discuss]


题解:斜率优化

状态转移方程: f[i][j]=f[i-1][k]+(sum[j]-sum[k])^2  f[i][j]表示到第j个点,分成了i段,sum[j]表示1-j的前缀和

f[i][j]=f[i-1][k]+(sum[j]-sum[k])^2 

=f[i-1][k]+sum[j]^2+sum[k]^2-2sum[j]sum[k]

=-2sum[k]sum[j]+f[i-1][k]+sum[k]^2+sum[j]^2

这个方程很显然可以用斜率优化,f[i-1][k]是上一层已经记录过的答案,我们本来需要枚举i,j,k,但是发现k一定是在j之前的,那么我们可以用单调队列维护当前层的j,计算时只需要从队列中找出使答案增加最小的j'即可,其实就相当于j,k合并成了一层循环,k就属于这一层计算过的j 。

因为要枚举i,j所以sum[j]相当于常量

-2sum[k]当作k, sum[j]当作x,f[i-1][k]+sum[k]^2当作b

那么f[i][j]就相当于一条kx+b的直线,因为sum[k]随着k的增加而增加,而-2sum[k]随着k的增加而减小,所有我们就得到了一组斜率递减的直线。

因为x(即sum[j]是单调递增的,即X向x轴的正半轴移动),当q[head+1]的函数值小于q[head]的函数值的时候,因为斜率是单调递减的,所有q[head]对之后的答案都没有贡献了,所有就可以弹出队列。如图


当我们要加入一条直线的时候,如果是下面这幅图的情况,可以发现q[tail-1]和q[j]包含了最小值的全部,那么q[tail]就可以弹出了。(蓝色部分为最小值)


那么如何判断呢?求出交点的坐标,然后带入之后比较大小即可

q[tail-1]   : k1x+b1

q[j] :k1x+b1

两个连立  k1x+b1=k1x+b1  推出 x=(b3-b1)/(k1-k3)

然后把x带入q[tail]和q[j]

k3((b3-b1)/(k1-k3))+b3<=k2((b3-b1)/(k1-k3)+b2

(k3-k2)*((b3-b1)/(k1-k3)<=b2-b3

(k3-k2)*(b3-b1)<=(b2-b3)*(k1-k3)

如果不等式成立,则q[tail]出队。

然后这道题就可以在o(n^2)的时间内完美解决了。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#define N 3003
#define ll long long 
#define inf 1e9
using namespace std;
int n,m,q[N];
ll a[N],sum[N],f[N],g[N];
ll calc(int x,int y)//计算函数值
{
	return -2*sum[x]*sum[y]+g[x]+sum[x]*sum[x]+sum[y]*sum[y];
}
ll s(int x)
{
	return (ll)g[x]+sum[x]*sum[x];
}
ll K(int x)
{
	return (ll)-2*sum[x];
}
ll B(int x)
{
	return (ll)g[x]+sum[x]*sum[x];
}
bool pd(int x1,int x2,int x3)
{
    ll w1=(ll)(K(x3)-K(x2))*(B(x3)-B(x1));
    ll w2=(ll)(K(x1)-K(x3))*(B(x2)-B(x3));
    //cout<<w1<<" "<<w2<<endl;
	return w1<=w2;
} 
int main()
{
	scanf("%d%d",&n,&m);
	for (int i=1;i<=n;i++)
	 {
	 	scanf("%lld",&a[i]);
	 	sum[i]=sum[i-1]+a[i];
	 }
	ll ans=inf;
	for (int i=1;i<=n;i++)
	 g[i]=inf;
	g[0]=0;
	for (int i=1;i<=m;i++)
	{
	 int head=0,tail=0;
	 for (int j=1;j<=n;j++)
	  {
	  	while (head<tail&&calc(q[head+1],j)<=calc(q[head],j))
	  	 head++;
	  	f[j]=calc(q[head],j);
	  	while (head<tail&&pd(q[tail-1],q[tail],j))
	  	 tail--;
	  	tail++; q[tail]=j;
	  }
	  ans=min(ans,f[n]);
	  for (int i=0;i<=n;i++)  g[i]=f[i];
    }
    printf("%lld\n",ans*m-sum[n]*sum[n]);
}



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值