【暖*墟】 #动态规划# 斜率优化DP方法总结

版权声明:本文为博主原创文章,未经博主允许不得转载qwq。https://blog.csdn.net/flora715 https://blog.csdn.net/flora715/article/details/82464287

斜率优化DP方法总结

          目录

          { 一. 概念入门 }

         【洛谷p2365】任务安排1

         【poj1180****】任务安排2【大数据】

【实现】“及时排除无用决策”的思想

         【bzoj2726**】任务安排3【大数据】【负数】

         【拓展********】任务安排4【多重负数】

【总结归纳】斜率优化的实现方式

           { 二. 例题详解 }

      一. 基础DP实现

      二. 方程变形实现

1.证明决策单调性

2.求斜率方程

      三. 具体方法整理

      四. 具体步骤归纳【精】

      五. 具体代码实现

           { 三. 习题归纳 }

【练习1】洛谷 p2900 土地购买

【例题2】CF311B 小猫运输

【例题3】洛谷 p3195 玩具装箱

【例题4】洛谷 p3628 特别行动队

【例题5】洛谷 p4360 锯木厂选址


{ 一. 概念入门 }

【洛谷p2365】任务安排1

  • N个任务排成序列在一台机器上等待完成(顺序不得改变)。
  • 从时刻0开始任务被[分批]加工,第i个任务单独完成所需的时间是Ti。
  • 任务开始前,启动时间S,同一批任务将在同一时刻(启动+sum所需时间)完成。
  • 每个任务的费用是它的[完成时刻乘以一个费用系数Ci],规划分组方案。

【分析】处理前缀和,直接dp即可,需要优化一下S。

/*【p2365】任务安排1
N个任务排成序列在一台机器上等待完成(顺序不得改变)。
从时刻0开始任务被[分批]加工,第i个任务单独完成所需的时间是Ti。
任务开始前,启动时间S,同一批任务将在同一时刻(启动+sum所需时间)完成。
每个任务的费用是它的[完成时刻乘以一个费用系数Ci],规划分组方案。*/

void reads(int &x){ //读入优化(正负整数)
    int fx=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    x*=fx; //正负号
}

int t[5019],c[5019]; //t[i]指每个任务用时,c[i]指费用系数f[i]
int sumt[5019],sumc[5019];
int f[5019]; //已安排1~i个任务的最优值

int main(){
    int n,s; reads(n),reads(s);
    for(int i=1;i<=n;i++){
        reads(t[i]),reads(c[i]);
        sumt[i]=sumt[i-1]+t[i]; //求sum,便于转移
        sumc[i]=sumc[i-1]+c[i];
    }
    memset(f,63,sizeof(f)); f[0]=0;//初始化+起点
    for(int i=1;i<=n;i++) //枚举现在要算的任务
        for(int j=0;j<i;j++) //枚举断点:前面分组隔断的位置
            f[i]=min(f[i],f[j]+sumt[i]*(sumc[i]-sumc[j])+s*(sumc[n]-sumc[j]));      
            //因为这批任务的执行,机器的启动时间s会影响第j+1个之后的所有任务
            //直接用sum差值计算:当前一步产生的增长对于后方答案的所有影响
    printf("%d\n",f[n]); 
    return 0;
}

【poj1180】任务安排2 【大数据】

f [ i ] 表示把前 i 个任务分成若干批执行得到的min费用。

f[i]=min(f[i],f[j]+sumt[i]*(sumc[i]-sumc[j])+s*(sumc[n]-sumc[j]));

稍微处理一下,合并系数,方程可以改变为:

f[i]=min{f[j]-(s+sumt[i])*sumc[j]+sumt[i]*sumc[i]+s*sumc[n]};

可以发现 sumt[i]*sumc[i]+s*sumc[n] s+sumt[i] 对于不同的 j 都不会改变。

那么只把 f[j]、sumc[j] 看成变量:

f[j]=(s+sumt[i])*sumc[j]+f[i]-sumt[i]*sumc[i]-s*sumc[n]。

sumc[j]为横坐标,f[j]为纵坐标,斜率=s+sumt[i],截距=f[i]-sumt[i]*sumc[i]-s*sumc[n]

可以看成:对于每个i,每个决策点j都在固定斜率的线上,要使f[i]min,即直线的截距min,

用一条斜率固定(为正整数)的直线从下往上平移,第一次接触决策点时即为答案。


【实现】“及时排除无用决策”的思想

对于任意三个决策点(sumc[j1],f[j1]),(sumc[j2],f[j2]),(sumc[j3],f[j3]),

不妨设 j1<j2<j3,那么sumc[j1]<sumc[j2]<sumc[j3] 。画图可看出:

  1. 连接j1、j2的线段与连接j2、j3的线段构成上凸形状,j2不可能是最优决策。
  2. 连接j1、j2的线段与连接j2、j3的线段构成下凸形状,三者都可能是最优决策。

那么考虑j2可能成为最优决策的条件(三条线段呈下凸结构):

(f[j2]-f[j1])/(sumc[j2]-sumc[j1])<(f[j3]-f[j2])/(sumc[j3]-sumc[j2]);

即:形成“连接相邻两点的线段斜率”单调递增的一个“下凸壳”。

 --->维护斜率单调递增的决策j的队列。那么在这个队列中:

对于要判断的斜率k,寻找某个顶点左侧线段斜率<k,右侧线段斜率>k,则它就是最优决策。

注意到需要判断的斜率=s+sumt[i]单调递增的,可以用类似单调队列的方法维护斜率单调队列。

特殊的是,判断的时候要结合s+sumt[i]检测队头[两个元素]之间的斜率,队尾[两个元素]间的斜率。


【代码实现】

/*【poj1180】任务安排2 【大数据】【斜率优化】
N个任务排成序列在一台机器上等待完成(顺序不得改变)。
从时刻0开始任务被[分批]加工,第i个任务单独完成所需的时间是Ti。
任务开始前,启动时间S,同一批任务将在同一时刻(启动+sum所需时间)完成。
每个任务的费用是它的[完成时刻乘以一个费用系数Ci],规划分组方案。*/

void reads(ll &x){ //读入优化(正负整数)
    int fx=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    x*=fx; //正负号
}

ll sumt[500019],sumc[500019]; //t[i]指每个任务用时,c[i]指费用系数
ll f[500019],q[500019]; //f[i]是已安排1~i个任务的最优值,q[i]是斜率递增队列

int main(){
    ll n,s,t,c; reads(n),reads(s);
    for(ll i=1;i<=n;i++){
        reads(t),reads(c);
        sumt[i]=sumt[i-1]+t; //求sum,便于转移
        sumc[i]=sumc[i-1]+c;
    }
    memset(f,0x3f,sizeof(f)); f[0]=0;//初始化+起点
    int head=1,tail=1; q[1]=0; //这个队列比较神奇,必须从tail=1开始
    for(int i=1;i<=n;i++){
        while(head<tail&&(f[q[head+1]]-f[q[head]])  //*化除为乘*
            <=(s+sumt[i])*(sumc[q[head+1]]-sumc[q[head]])) head++;
        f[i]=f[q[head]]-(s+sumt[i])*sumc[q[head]]
            +sumt[i]*sumc[i]+s*sumc[n];
        while(head<tail&&(f[q[tail]]-f[q[tail-1]])*(sumc[i]-sumc[q[tail]])
            >=(f[i]-f[q[tail]])*(sumc[q[tail]]-sumc[q[tail-1]])) tail--;
        q[++tail]=i; //新元素入队尾
    }
    printf("%lld\n",f[n]); 
    return 0;
}

【bzoj2726】任务安排3【大数据】【负数】

与任务安排2的差别是,Ti可以为负数,那么s+sumt[i]不具有单调性。


【分析】单调队列中不能只维护大于s+sumt[i]的部分,需要维护整个下凸壳。

此时队头不一定是最优决策。可以在队列中二分查找,找出p位置满足:

左侧线段斜率小于s+sumt[i],右侧线段斜率大于s+sumt[i],此时p为最优决策。


【代码实现】

void reads(ll &x){ //读入优化(正负整数)
    int fx=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    x*=fx; //正负号
}

ll sumt[500019],sumc[500019]; //t[i]指每个任务用时,c[i]指费用系数
ll f[500019],q[500019]; //f[i]是已安排1~i个任务的最优值,q[i]是斜率递增队列
ll head=1,tail=1;

ll binary_search(ll k){ //在单调队列中二分查找接近斜率k的线段
    if(head==tail) return q[head];
    ll L=head,R=tail;
    while(L<R){
        ll mid=(L+R)>>1;
        if(f[q[mid+1]]-f[q[mid]]<=k* //k大于区间的中间点和后一个点构成的斜率
            (sumc[q[mid+1]]-sumc[q[mid]])) L=mid+1; 
        else R=mid; //缩小区间
    }
    return q[L];
}

int main(){
    ll n,s,t,c; reads(n),reads(s);
    for(ll i=1;i<=n;i++){
        reads(t),reads(c);
        sumt[i]=sumt[i-1]+t; //求sum,便于转移
        sumc[i]=sumc[i-1]+c;
    }
    memset(f,0x3f,sizeof(f)); f[0]=0;//初始化+起点
    head=1; tail=1; q[1]=0; //这个队列比较神奇,必须从tail=1开始
    for(ll i=1;i<=n;i++){
        int p=binary_search(s+sumt[i]); //二分查找最优决策
        f[i]=f[p]-(s+sumt[i])*sumc[p]+sumt[i]*sumc[i]+s*sumc[n];
        while(head<tail&&(f[q[tail]]-f[q[tail-1]])*(sumc[i]-sumc[q[tail]])
            >=(f[i]-f[q[tail]])*(sumc[q[tail]]-sumc[q[tail-1]])) tail--;
        q[++tail]=i; //新元素入队尾
    }
    printf("%lld\n",f[n]); 
    return 0;
}

【拓展】任务安排4

1.如果Ti是正整数,Ci可能是负数呢?

  • 倒序DP,设计状态转移方程,让sumt是横坐标中的一项、
  • sumc是斜率中的一项,之后的方法同任务安排3。

2.如果Ti、Ci都可能是负数呢?

  • 要实现在凸壳任意位置动态差点、动态查询,用平衡树维护。

【总结归纳】斜率优化的实现方式

这里写图片描述

所以这种情况时,我们可以直接把j点删除,最后能够转移的点集只会存在这种图形,

这里写图片描述
所以最后我们维护一个下凸集即可。

但是此时我们还是没有解决最终问题,如何才能找到转移到i点的最优的点呢。

可以发现最后的点集一定是一个凸集,也就是斜率单调。

这样对于k < j, grad(j,k) < f(i),时更优。

从图形特点我们可以发现如果j比k优,那么j点比所有比k小的点都优,

所以对于每一个f(i),我们维护一个所有比i点小的凸集,

二分查找斜率比f(i)小的编号最大的点,就是最优的转移点。

如果f(i)也满足单调性,可以直接维护一个单调队列就能解决问题。


{ 二. 例题详解 }

【caioj 1138】http://caioj.cn/problem.php?id=1138

  • 容量为L的包装盒。如果要装容量为X的物品,则花费为(X - L)^2。
  • 现在有N个物品需要装入包装盒,每个物品的容量为Ci。目标:总花费最小。
  • (1)可以多个物品装入一个包装盒。(2)同一个包装盒的物品编号必须是【连续】的。
  • (3)同个包装盒的物品排成直线,相邻两个物品之间要加一块容量为1的隔板。

一. 基础DP实现

//f[i]表示1~i的最小花费。枚举j小于i,表示一段(j+1)~i放在一个盒子里。

//预处理出sum[i],先列出方程:

f[i]=min(f[j]+(sum[i]-sum[j]+i-(j+1)-L)^2) (j<i)

//状态转移的程序实现:

for(int i=1;i<=n;i++)

    for(int j=0;j<=i-1;j++)

        f[i]=min(f[i],f[j]+(sum[i]-sum[j]+(i-(j+1))-l)*(sum[i]-sum[j]+(i-(j+1))-l));

二. 方程变形实现

变为:f[i]=min(f[j]+(sum[i]+i-sum[j]-j-1-L)^2) (j<i);

令:s[i]=sum[i]+i,L=1+L;

则:f[i]=min(f[j]+(s[i]-s[j]-L)^2);

1.证明决策单调性

假设j1<j2<i,在状态i处,j2决策优于j1(即j2花费较少),

即要满足:f[j2]+(s[i]-s[j2]-L)^2 < f[j1]+(s[i]-s[j1]-L)^2 ;

则对于i后的所有状态t,是否j2决策都优于j1?(证明决策单调性)

即:f[j2]+(s[t]-s[j2]-L)^2 < f[j1]+(s[t]-s[j1]-L)^2 ; ?

---> 证明过程:容易理解s[t]=s[i]+v(v是不确定的增量);

所以得到(1)不等式:f[j2]+(s[i]-s[j2]-L+v)^2 < f[j1]+(s[i]-s[j1]-L+v)^2 ;

因为已知(2)不等式:f[j2]+(s[i]-s[j2]-L  )^2 < f[j1]+(s[i]-s[j1]-L  )^2 ;

化简(1)不等式得到:

f[j2] + (s[i]-s[j2]-L)^2 + 2*v*(s[i]-s[j2]-L) + v^2 <

                        f[j1] + (s[i]-s[j1]-L)^2 + 2*v*(s[i]-s[j1]-L) + v^2 ;

化简的(1)与(2)不等式比较:

左边多了一部分:2*v*(s[i]-s[j2]-L)+v^2 ;

右边多了一部分:2*v*(s[i]-s[j1]-L)+v^2 ;

证明转化为:

2*v*(s[i]-s[j2]-L)+v^2 <= 2*v*(s[i]-s[j1]-L)+v^2

即: (s[i]-s[j2]-L)    <=     (s[i]-s[j1]-L)

即:      -s[j2]       <=         -s[j1]

即: s[j1]<s[j2] <------ 这是肯定的,所以得证。

总结:对于当前i,得到j2比j1好,那么对于t(i<t)来说一样。

所以,在循环到i的阶段就可以永久淘汰j1,因为它以后不再会成为最优解。


2.求斜率方程

f[j2]+(s[i]-s[j2]-L)^2 < f[j1]+(s[i]-s[j1]-L)^2 ; 展开得到:

f[j2]+(s[i]-L)^2-2*(s[i]-L)*s[j2]+s[j2]^2 < f[j1]+(s[i]-L)^2-2*(s[i]-L)*s[j1]+s[j1]^2 ;

即f[j2]-2*(s[i]-L)*s[j2]+s[j2]^2 < f[j1]-2*(s[i]-L)*s[j1]+s[j1]^2 ;

即[(f[j2]+s[j2]^2)-(f[j1]+s[j1]^2)] < 2*(s[i]-L)*s[j2]-2*(s[i]-L)*s[j1] ;

即[(f[j2]+s[j2]^2)-(f[j1]+s[j1]^2)]/(s[j2]-s[j1]) < 2*(s[i]-L) ;

对于j来说,制造斜率需要的点坐标:

Y=f[j]+s[j]^2 ;  X=s[j] ;

三. 具体方法整理

用队列list储存有意义的决策点,list中相邻两点的斜率递增(点形成下凸壳)。

而且都 大于2*(s[i]-L),那么【队列头】对于i来说就是最优决策点。

对于新的i,在list中按顺序寻找到第一个大于2*(s[i]-L)的位置,将其设为新的head

加入决策i时,令队尾为list[tail],队尾元素的前一个为list[tail-1]

建立斜率函数slop,储存信息为:(点1,点2)

满足: slop(list[tail-1],list[tail]) > slop(list[tail],i)  时,

那么队尾list[tail]对于未来的t绝对不会是最优的策略,所以将其弹出tail--;

最后遇到了:slop(list[tail-1],list[tail]) < slop(list[tail],i) ;

保证了队列的相邻两点的斜率递增时,加入i: list[++tail]=i 。


四. 具体步骤归纳

对于每个斜率方程(Y(j2)-Y(j1))/(X(j2)-X(j1))

  1. 将数据进行预处理,优化序列。
  2. 写状态转移方程(一维)。
  3. 对于j1<j2<i且j2的决策优于j1...
  4. 推导不等式,化成斜率的一般式。
  5. 从而得到X,Y的定义式,用double类型表示出来。
  6. 建立一个类似优先队列的斜率优先队列。
  7. 判断头尾的出队入队,维护斜率单调性。
  8. 储存最优答案并输出。

五. 具体代码实现

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
using namespace std;
typedef long long ll;

void reads(int &x){
  int f=1;x=0;char s=getchar();
  while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
  while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
  x*=f; 
}

int list[50019],head,tail;
ll sum[50019],s[50019],f[50019];

double Y(int j){ return f[j]+s[j]*s[j]; } //Y坐标

double X(int j){ return s[j]; } //X坐标

double slop(int j1,int j2){ //斜率函数
    return (Y(j2)-Y(j1))/(X(j2)-X(j1));
}

int main(){
    
    int n,l,x; reads(n); reads(l); //盒子容量为L
    sum[0]=0; for(int i=1;i<=n;i++){
        reads(x); sum[i]=sum[i-1]+x; //只用记录前缀和
    }
    for(int i=1;i<=n;i++) s[i]=sum[i]+i;
    l++;  //按照所求,建立s数组,并将l+1
    
    head=1;tail=1;list[1]=0;
    for(int i=1;i<=n;i++){ //↓↓↓对于状态i,更新它的最优方案list[head]
        while(head<tail&&slop(list[head],list[head+1])<=2.0*(s[i]-l)) head++;
        f[i]=f[list[head]]+(s[i]-s[list[head]]-l)*(s[i]-s[list[head]]-l);
        //↑↑↑将(list[head]+1)~i的所有物品放在一个盒子,答案最优
        while(head<tail&&slop(list[tail-1],list[tail])>slop(list[tail],i)) tail--;
        list[++tail]=i; //↑↑↑需要将i入队,但要保持队列斜率单调性
    }

    printf("%lld\n",f[n]); return 0;

}

当然,也可以把X、Y函数写在slop函数里面,注意要用double。


{ 三. 习题归纳 }

【练习1】洛谷 p2900 土地购买

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
using namespace std;
typedef long long ll;

/*【p2900】土地购买
有N块长方形的土地。每块土地的长宽满足 1 <= x <= 1,000,000。 
每块土地的价格是它的面积,但FJ可以同时购买多块土地。
这些土地的价格是它们最大的长乘以它们最大的宽, 但长宽不能交换。
如果FJ买一块3×5的地和一块5×3的地,则他需要付5×5=25。
FJ希望买下所有的土地,但是他发现[分组来买]这些土地可以节省经费。
他需要你帮助他找到最小的经费。(fj辣么强还问我??) */

//【分析】先按土地的长排序,找到宽度递减的保留,存入新的数组。

/* <斜率模型> (Y(j2)-Y(j1))/(X(j2)-X(j1));

状态转移方程:f[i]=min(f[i],f[j]+b[j+1].y*b[i].x); //将(j+1)~i看成一段
因为满足长度递增、宽度递减,所以可以直接把b[j+1].y、b[i].x相乘。

对于 j1<j2<i 且 f[j1]+b[j1+1].y*b[i].x > f[j2]+b[j2+1].y*b[i].x ;
得出关系式:(f[j2]-f[j1])/(b[j1+1].y-b[j2+1].y) < b[i].x;
进而可以得到X,Y的定义式:Y=f[j]; X=b[j+1].y; 注意要用double储存。 */

void reads(int &x){
  int f=1;x=0;char s=getchar();
  while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
  while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
  x*=f; 
}

void read2(ll &x){
  int f=1;x=0;char s=getchar();
  while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
  while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
  x*=f; 
}

struct node{ ll x,y; };

node a[51009],b[51009];

bool cmp(node a,node b){ return a.x<b.x; } //相等的可以在下面筛去

long long f[51009];
int list[51009];

double slop(int j1,int j2){ //j2优于j1
	return double(f[j2]-f[j1])/double(b[j1+1].y-b[j2+1].y);
}

int main(){
	int n; reads(n);
	for(int i=1;i<=n;i++) read2(a[i].x),read2(a[i].y);
	sort(a+1,a+n+1,cmp); //按长度从小到大排序
	int cnt=1; b[1]=a[1]; //神奇的方式...
	for(int i=2;i<=n;i++){
		while(cnt>0&&b[cnt].y<=a[i].y) cnt--; //宽没有递减
		b[++cnt]=a[i]; //结构体也可以这样完全复制哦
	}
	int head=1,tail=1; list[1]=0; //广搜初始化
	for(int i=1;i<=cnt;i++){ //list[head+1]优于list[head],list[head]无用
		while(head<tail&&slop(list[head],list[head+1])<b[i].x) head++;
		f[i]=f[list[head]]+b[list[head]+1].y*b[i].x;
		while(head<tail&&slop(list[tail-1],list[tail])>slop(list[tail],i)) tail--;
		list[++tail]=i;
	}
	printf("%lld\n",f[cnt]); //必须全部买完
	return 0;
}

【例题2】CF311B 小猫运输

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
#include<deque>
using namespace std;
typedef long long ll;

/*【CF311B】小猫运输
m只猫,p个饲养员,n座山。第i~i-1座山之间的距离为di。
第i只猫去hi号山玩,玩到ti停止,开始等待饲养员。
规划每个饲养员[从1号山出发]的时间,使得所有猫等待时间总和最小。*/

/*【分析】对于每只猫,设a[i]=t[i]-∑(j=1~hi)d[i],即猫不等待的出发时刻。
假设饲养员在t时刻出发,接到第i只猫时,猫的等待时间为t-a[i]。
因为每个饲养员带走的一定是按照a[i]排序后的连续若干只猫【巧妙】,
所以可以把a[i]从小到大排序,求前缀和,记录在数组s中,每次用前缀和表示。
状态:f[i][j]表示[前i个饲养员带走前j只猫]的最小sum等待时间。
枚举第i个饲养员带走的猫的起始位置k,则他带走的一定是(k~j),累计答案。
在a[j]时候出发可以让最后一只猫不等待(当前情况的最优方案),
但对此人带的其他猫的总影响为a[j]*(j-k)-(s[j]-s[k])。
f[i][j]=min(f[i][j],f[i-1][k]+a[j]*(j-k)-(s[j]-s[k]));
将方程转化为:f[i-1][k]+s[k]=a[j]*k+f[i][j]-a[j]*j;
以k为横坐标,f[i-1][k]+s[k]为纵坐标建系,斜率为a[j],截距为f[i][j]-a[j]*j。
当截距最小化时,f[i][j]取到min值。维护一个单调队列【下凸壳】。*/

void reads(ll &x){ //读入优化(正负整数)
    int fx=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    x*=fx; //正负号
}

ll f[110][100010],g[100010]; //g数组保存斜率纵坐标f[i-1][k]+s[k]
ll s[100010],a[100010],d[100010],q[100010];

int n,m,p,x,y,head,tail;

int main(){
    cin>>n>>m>>p;
    for(int i=2;i<=n;i++){ cin>>x; d[i]=d[i-1]+x; }
    for(int i=1;i<=m;i++){ cin>>x>>y; a[i]=y-d[x]; }
    sort(a+1,a+m+1); //每人带走的一定是按照a[i]排序后的连续若干只猫
    for(int i=1;i<=m;i++) s[i]=s[i-1]+a[i];
    memset(f,0x3f,sizeof(f)); f[0][0]=0; //起点
    for(int i=1;i<=p;i++){
        for(int j=1;j<=m;j++) g[j]=f[i-1][j]+s[j];
        head=1; tail=1; q[1]=0; //注意斜率优化要从tail=1写起,且要写q[1]=0
        for(int j=1;j<=m;j++){ //[化除为乘]避免比较的误差产生
            while(head<tail&&g[q[head+1]]-g[q[head]]
                <=a[j]*(q[head+1]-q[head])) head++;
            f[i][j]=min(f[i-1][j],g[q[head]]+a[j]*(j-q[head])-s[j]); 
            //↑↑↑用min{f[i-1][k]+s[k]+a[j]*(j-k)-s[j])};更新答案
            if(g[j]>=0x3f3f3f3f3f3f3f3fll) continue; //超界
            while(head<tail&&(g[j]-g[q[tail]])*(q[tail]-q[tail-1])
                <=(g[q[tail]]-g[q[tail-1]])*(j-q[tail])) tail--;
            q[++tail]=j; //新元素入队
        }
    }
    cout<<f[p][m]<<endl; return 0;
}

【例题3】洛谷 p3195 玩具装箱

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
#include<deque>
using namespace std;
typedef long long ll;

/*【p3195】玩具装箱
N件玩具,第i件玩具经过压缩后变成一维,长度为Ci。
如果将第i到第j个玩具放到一个容器中,那么容器的长度将为 x=j-i+Sigma(Ck) i<=K<=j 
制作容器的费用与容器的长度有关,如果容器长度为x,其制作费用为(X-L)^2。
可以制作出任意长度的容器。希望费用最小。*/

/*【分析】sum[i]存c[i]前缀和。f[i]表示前i个玩具的min费用。
方程:f[i]=min{ f[k]+( i-k+sum[i]-sum[k-1] -L )^2 }; 
优化:f[i]=f[k]+(i-k+sum[i]-sum[k-1] -L )^2 }; 
用数组a[i]保存i+sum[i],那么:
f[i]=f[k]+a[i]^2+a[k]^2+L^2-2*a[i]*a[k]-2*L*a[i]+2*L*a[k];
【f[k]+a[k]*a[k]+2*L*a[k]】=2*a[i]*【a[k]】+f[i]-a[i]^2-L^2+2*L*a[i];
f[k]+a[k]*a[k]+2*L*a[k]为纵坐标,a[k]为横坐标。*/

void reads(ll &x){ //读入优化(正负整数)
    int fx=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    x*=fx; //正负号
}

const int maxn=50019;
ll s[maxn],L,a[maxn],f[maxn];
ll n,q[maxn];

double slop(ll p,ll t){
    ll y1=f[p]+a[p]*a[p]+2*a[p]*L,x1=a[p];
    ll y2=f[t]+a[t]*a[t]+2*a[t]*L,x2=a[t];
    return((double)(y2-y1)/(double)(x2-x1));
} //↑↑↑记得计算的时候要在里面写double

int main(){
    reads(n); reads(L); L++; 
    ll head=1,tail=1; q[1]=0;
    for(ll i=1;i<=n;++i){
        reads(s[i]); s[i]+=s[i-1]; a[i]=s[i]+i; 
        while(head<tail&&slop(q[head],q[head+1])<2*a[i]) head++;
        f[i]=f[q[head]]+a[q[head]]*a[q[head]]+
            2*a[q[head]]*L-2*a[i]*a[q[head]]+(a[i]-L)*(a[i]-L);
        while(tail>head&&slop(q[tail-1],q[tail])>=slop(q[tail-1],i)) tail--;
        q[++tail]=i; //新决策入队
    }
    printf("%lld\n",f[n]);
    return 0;
}

【例题4】洛谷 p3628 特别行动队

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
#include<deque>
using namespace std;
typedef long long ll;

/*【p3628】特别行动队
有n个数,分成连续的若干段,每段的分数为a*x^2+b*x+c(a,b,c是给出的常数),
其中x为该段的各个数的和。求如何分才能使得各个段的分数的总和最大。*/

/*【分析】f[i]表示前i段的分数max总和。 
f[i]=min{f[j]+a*s[i]*s[i]-2*a*s[j-1]*s[i]+
            a*s[j-1]*s[j-1]+b*s[i]-b*s[j-1]+c};
【f[j]+a*s[j-1]^2-b*s[j-1]】=2*a*s[i]*【s[j-1]】+(f[i]-a*s[i]^2+b*s[i]+c) 
*/

void reads(long long &x){ //读入优化(正负整数)
    int fx=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    x*=fx; //正负号
}

const int maxn=1010000;
 
int q[maxn],head,tail;
ll f[maxn],s[maxn],a,b,c;

double Y(int j){return f[j]+a*s[j]*s[j]-b*s[j];}
double X(int j){return s[j];}
double slop(int j1,int j2){return (Y(j2)-Y(j1))/(X(j2)-X(j1));}

int main(){
  int n; cin>>n; s[0]=0;
  reads(a); reads(b); reads(c);
  for(int i=1;i<=n;i++){ reads(s[i]); s[i]+=s[i-1]; }
  memset(f,0,sizeof(f));
  head=tail=1; q[1]=0;
  for(int i=1;i<=n;i++){
    while(head<tail&&slop(q[head],q[head+1])>2*a*s[i]) head++;
    int j=q[head]; f[i]=f[j]+a*(s[i]-s[j])*(s[i]-s[j])+b*(s[i]-s[j])+c;
    while(head<tail&&slop(q[tail-1],q[tail])<slop(q[tail],i)) tail--;
    q[++tail]=i;
  }
  printf("%lld\n",f[n]); return 0;
}

【例题5】洛谷 p4360 锯木厂选址

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
#include<deque>
using namespace std;
typedef long long ll;

/*【p4360】锯木厂选址
从山顶上到山底下沿着一条直线种植了n棵老树。决定把他们砍下来。
山脚下有一个锯木厂,山路上有另外两个。运输木材每公斤每米需要一分钱。
你必须决定在哪里修建这两个锯木厂,使得运输的费用总和最小。*/

/*【分析】s[i]表示前i棵树的重量之和。d[i]表示第i棵树到第一棵树的距离。
c[i]前i棵树运到i的费用。c[i]=c[i-1]+s[i-1]·d;(树的编号从高处到低处依次++)
假设新建造的第一个锯木厂在i,第二个锯木厂在j(j>i),
那么i~j的树木运到j的费用就是c[j]-c[i]-s[i]·(d[j]-d[i])。
总费用为:c[i] + c[j]-c[i]-s[i]·(d[j]-d[i]) + c[n+1]-c[j]-s[j]·(d[n+1]-d[j])。
加起来就是:c[n+1]-s[i]·(d[j]-d[i])-s[j]·(d[n+1]-d[j])。*/

/*【方程】我们用f[i]表示第二个锯木厂建在i的最小费用,那么:
f[i]=min(c[n+1]-s[j]·(d[i]-d[j])-s[i]·(d[n+1]-d[i]))(0<j<i)
当我们枚举j时,如果k比j(k>j)更优则有:
c[n+1]-s[j]·(d[i]-d[j])-s[i]·(d[n+1]-d[i]) > c[n+1]-s[k]·(d[i]-d[k])-s[i]·(d[n+1]-d[i])
推导出:s[j]·d[j]−s[k]·d[k] > d[i]∗(s[j]−s[k]);
因为k>j,s[k]>s[j] -> 斜率可以表示为:(s[j]·d[j]−s[k]·d[k])/(s[j]−s[k])<d[i];
在枚举j的时候的d[i]是不变的,这样我们可以用单调队列来维护,
且队列中的斜率按照编号递增顺序递减。 */

void reads(int &x){ //读入优化(正负整数)
    int fx=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    x*=fx; //正负号
}

int c[20100],s[20100],d[20100];
int q[101000],f[20100],ans=0x7fffffff; 
//注意ans的初始化数字!!!(设成99999999结果只有65分...)

double slop(int j,int k){ return (s[j]*d[j]-s[k]*d[k])/(s[j]-s[k]); }

int main(){
    int n; reads(n);
    for(int i=1;i<=n;i++){
        int x,y; reads(x); reads(y);
        s[i]=s[i-1]+x; d[i+1]=d[i]+y;
        c[i+1]=c[i]+s[i]*y;
    }
    int head=1,tail=1; q[1]=0;
    for(int i=1;i<=n;i++){
        while(head<tail&&slop(q[head],q[head+1])<d[i]) head++;
        f[i]=c[n+1]-s[q[head]]*(d[i]-d[q[head]])-s[i]*(d[n+1]-d[i]);
        while(head<tail&&slop(q[tail-1],q[tail])>slop(q[tail],i)) 
            tail--; //单调队列维护,把斜率不优的踢出去
        q[++tail]=i;
    }
    for(int i=1;i<=n;i++) ans=min(ans,f[i]);
    cout<<ans<<endl; return 0;
}

                                                   ——时间划过风的轨迹,那个少年,还在等你。

猜你喜欢

转载自blog.csdn.net/flora715/article/details/82464287
今日推荐