斜率优化DP方法总结
目录
{ 一. 概念入门 }
【洛谷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] 。画图可看出:
- 连接j1、j2的线段与连接j2、j3的线段构成上凸形状,j2不可能是最优决策。
- 连接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)):
- 将数据进行预处理,优化序列。
- 写状态转移方程(一维)。
- 对于j1<j2<i且j2的决策优于j1...
- 推导不等式,化成斜率的一般式。
- 从而得到X,Y的定义式,用double类型表示出来。
- 建立一个类似优先队列的斜率优先队列。
- 判断头尾的出队入队,维护斜率单调性。
- 储存最优答案并输出。
五. 具体代码实现
#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;
}
——时间划过风的轨迹,那个少年,还在等你。