[斜率优化DP] BZOJ1096: [ZJOI2007]仓库建设

题意

有N个工厂按编号顺序排成一条直线,第1个工厂到第i个工厂的距离为dis[i], 第i个工厂一开始堆积着p[i]个货物,现在要紧急把所有货物装进仓库里。在第i个工厂建仓库需要c[i]的代价,对于没有建立仓库的工厂,其货物应被运往其他的仓库进行储藏,且只能往编号大仓库的运,一件货物运送1个单位距离的代价是1。求把所有货物装进仓库的最小代价。
(n<=100000)

题解

显然是DP,要用到斜率优化。
很容易设计出O(n^2)的算法,f[i]表示前i个工厂送完的最优解。
转移方程:f[i]=c[i]+min{ f[j]+ ix=j+1p[x](dis[i]dis[x]) }
先把方程进行整理:
f[i]=c[i]+min{ f[j]+ dis[i]ix=j+1p[x]ix=j+1p[x]dis[x]) }
写成前缀和的形式,设sum_p[i]为 p[i]的前缀和,sum_pd[i]为p[i]*dis[i]的前缀和。
f[i]=c[i]+min{ f[j]+dis[i]*( sum_p[i] - sum_p[j] )+ sum_pd[i] - sum_pd[j] }
f[i]=c[i] + dis[i]*sum_p[i] + sum_pd[i] + min{ f[j] - sum_pd[j] - dis[i]*sum_p[j] }
我们现在只需关注min中的内容,设 M = f[j] - sum_pd[j] - dis[i]*sum_p[j]
整理得 dis[i]*sum_p[j] + M = f[j] - sum_pd[j]
直线的方程 k * x + b = y
我们把一个决策j转化成了点(sum_p[j], f[j] - sum_pd[j])
那么找当前状态i的最小的M转化为:将一条斜率为dis[i]的直线从下向上移动,碰到第一个点就是最优决策。因为这时直线的截距b最小,对应M最小。
显然能成为最优决策的点一定是凸包(下凸壳)上的点。
因为点的x坐标有单调性,新点只在末端插入,所以可以用一个栈来维护凸壳(Graham)。
又因为斜率dis[i]是单调的,所以决策点也是单调的,即随着i的增加在凸壳上往右动。所以找最优点时只需记一个决策指针在栈中一步步推就行了。
因为有决策单调和x单调这两个很好的性质,复杂度可优化为O(n)。

#include<cstdio>
#include<algorithm>
typedef long long LL;
using namespace std;
const int maxn=1000005;
int n;
LL f[maxn],d[maxn],p[maxn],c[maxn],sum_p[maxn],sum_pd[maxn];
struct Point{
    LL x,y;
    Point(LL x=0,LL y=0):x(x),y(y){}
} que[maxn];
typedef Point Vector;
Vector operator - (Point A,Point B){ return Vector(A.x-B.x,A.y-B.y); }
LL Cross(Vector A,Vector B){ return A.x*B.y-A.y*B.x; }
LL getM(int now,int k){
    return que[now].y-que[now].x*k;
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%lld%lld%lld",&d[i],&p[i],&c[i]);
        sum_p[i]=sum_p[i-1]+p[i];
        sum_pd[i]=sum_pd[i-1]+p[i]*d[i];
    }
    while(n&&!p[n]) n--;
    que[0].x=que[0].y=0;
    int head=0,tail=0;
    for(int i=1;i<=n;i++){
        while(head<tail&&getM(head,d[i])>getM(head+1,d[i])) head++;
        f[i]=getM(head,d[i])+c[i]+d[i]*sum_p[i]-sum_pd[i];
        Point now(sum_p[i],f[i]+sum_pd[i]);
        while(tail>0&&Cross(que[tail]-que[tail-1],now-que[tail])<0) tail--;
        que[++tail]=now;
        if(head>tail) head=tail;
    }
    printf("%lld\n",f[n]);
    return 0;
} 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值