斜率优化啊,这个东西挺玄学的.....我这个蒟蒻也是一知半解,现在把我会的都写一下,希望神犇指教。
jyf说,斜率优化如果用平衡树维护的话方程不一定要求具有单调性,但是如果用单调队列维护的话就要。
总结
能斜率优化的题目的状态转移方程通常是要选择连续的一段,方程可以写成f[i]表示前i个东西的最优值的方式
而所谓“打包类”问题就是指的可以用单调队列维护的斜率优化题,因为它们的方程通常长成这样:f[i]=min/max{与j有关的量+与j有关的量*与i有关的量}+与i有关的量,假如我们看做这类问题是把[j+1,i]打包,而与j有关的量*与i有关的量是打包的代价的话,本蒟蒻就私自给这种方程命名叫做“打包”(或许是因为玩具装箱那题的影响吧)
例题:HDU3507
设f[i]表示前i个数字打印的花费,sum是打印每个数字花费的前缀和,那么我们很容易可以写出状态转移方程:
f[i]=min(f[j]+M+(sum[i]-sum[j])^2);
以下内容建议在纸上书写!!!!更加清晰!!!!这该死的排版!!!
设有k<j<i,k和j是f[i]的决策且决策j比i要好,那么就有f[j]+M+(sum[i]-sum[j])^2<f[k]+M+(sum[i]-sum[k])^2
展开,并消除一些同类项:f[j]+sum[j]^2-2*sum[i]*sum[j]<f[k]+sum[k]^2-2*sum[i]*sum[k]
移项:(f[j]+sum[j]^2)-(f[k]+sum[k]^2)<sum[i]*2*(sum[j]-sum[k])
再移项:((f[j]+sum[j]^2)-(f[k]+sum[k]^2))/2*(sum[j]-sum[k])<sum[i]
现在我们令x1=2*sum[j],x2=2*sum[k],y1=f[i]+sum[j]^2y2=f[k]+sum[k]^2
那么左边的式子就变成了(y1-y2)/(x1-x2),这不就是个斜率式吗?现在我们令g[i,j]=刚才的斜率式。由于我们求出这个不等式的前提是j处决策比i要好,所以我们知道g[j,k]<sum[i]这个式子成立的前提条件是j处决策比k好。
可以证明:设k<j<i,若g[i,j]<g[j,k],那么j不可能是最优解,为什么呢?
假设g[i,j]<sum[i]说明i处决策比j处更优。
假设g[i,j]>=sum[i],那么j比i优,但是又因为g[j,k]>g[i,j],所以g[j,k]>sum[i],所以k处决策比j处优,排除j点。
排除多余的状态,就是斜率优化的优化方式。
可以用单调队列维护,提取队首元素。假如队首有a,b两个元素,而g[b,a]<sum[i],说明b比a优,a出队,以此类推找到队列中最优状态。
要把新元素插入队列时,假如队尾有b,a两个元素(从后往前),g[i,b]<g[b,a],说明b不是最优状态,b出队。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<climits>
#include<iomanip>
#include<vector>
#include<cmath>
#include<algorithm>
#include<map>
using namespace std;
int n,m,head,tail;
int sum[500005],f[500005],que[500005];//单调队列维护
int up(int j,int k){return f[j]+sum[j]*sum[j]-(f[k]+sum[k]*sum[k]);}//分数线上方
int low(int j,int k){return (sum[j]-sum[k])<<1;}//分数线下方
int dpz(int i,int j){return f[j]+m+(sum[i]-sum[j])*(sum[i]-sum[j]);}//dp值
int main()
{
int i,j,x;
while(scanf("%d%d",&n,&m)==2){
for(i=1;i<=n;i++){scanf("%d",&x);sum[i]=sum[i-1]+x;}
head=tail=1;que[1]=0;
for(i=1;i<=n;i++){
while(head+1<=tail&&up(que[head+1],que[head])<=sum[i]*low(que[head+1],que[head]))
head++;
f[i]=dpz(i,que[head]);
while(head+1<=tail&&up(i,que[tail])*low(que[tail],que[tail-1])<=low(i,que[tail])*up(que[tail],que[tail-1]))
tail--;
tail++;que[tail]=i;
}
printf("%d\n",f[n]);
}
return 0;
}
/*
因为要用单调队列维护,所以要具有单调性?
*/
例题:bzoj1010玩具装箱
这个也是入门题,斜率方程很容易写,一步一步来就行了。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<climits>
#include<iomanip>
#include<vector>
#include<cmath>
#include<algorithm>
#include<map>
using namespace std;
int n,m;
long long sum[50005],x,f[50005];
int que[50005];
long long getdp(int i,int j){return f[j]+(sum[i]-sum[j]-m-1)*(sum[i]-sum[j]-m-1);}
long long up(int j,int k){
return (f[j]+sum[j]*sum[j]+2*sum[j]*(m+1))-(f[k]+sum[k]*sum[k]+2*sum[k]*(m+1));
}
long long down(int j,int k){
return 2*(sum[j]-sum[k]);
}
int main()
{
int i,j,head=1,tail=1;
scanf("%d%d",&n,&m);
for(i=1;i<=n;i++){scanf("%lld",&x);sum[i]=sum[i-1]+x+1;}//记得方程要-1
for(i=1;i<=n;i++){
while(head+1<=tail&&up(que[head+1],que[head])<=sum[i]*down(que[head+1],que[head]))
head++;
f[i]=getdp(i,que[head]);
while(head+1<=tail&&up(i,que[tail])*down(que[tail],que[tail-1])<=up(que[tail],que[tail-1])*down(i,que[tail]))
tail--;
tail++;que[tail]=i;
}
printf("%lld",f[n]);
return 0;
}
例题:bzoj1597土地购买(洛谷P2900土地征用)
这道题就是把土地按长从小到大排序后容易发现如果有一块土地长和宽都小于另一块土地,那么就是可以免费购买的,我们删去这样的土地后得到的长是递增的,宽是递减的,再用斜率优化dp就行了,状态转移方程比上两题还简单。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<climits>
#include<iomanip>
#include<vector>
#include<cmath>
#include<algorithm>
#include<map>
using namespace std;
int n,cnt=0;
struct node{
int x;int y;
}art[50005];
long long f[50005];
int que[50005];
bool cmp(node a,node b){
if(a.y==b.y)return a.x>b.x;
return a.y>b.y;
}
long long getdp(int i,int j){return f[j]+(long long)art[i].x*art[j+1].y;}
long long up(int j,int k){return (long long)f[j]-f[k];}
long long down(int j,int k){return (long long)art[k+1].y-art[j+1].y;}
int main()
{
int i,j,head=1,tail=1,x,y;
scanf("%d",&n);
for(i=1;i<=n;i++){scanf("%d%d",&art[i].x,&art[i].y);}
sort(art+1,art+1+n,cmp);
cnt=1;
for(i=2;i<=n;i++)
if(art[cnt].x<=art[i].x)art[++cnt]=art[i];
for(i=1;i<=cnt;i++){
while(head<tail&&
(long long)up(que[head+1],que[head])<=(long long)art[i].x*down(que[head+1],que[head]))
head++;
f[i]=getdp(i,que[head]);
while(head<tail&&
(long long)up(i,que[tail])*down(que[tail],que[tail-1])<=(long long)down(i,que[tail])*up(que[tail],que[tail-1]))
tail--;
tail++;que[tail]=i;
}
printf("%lld",f[cnt]);
return 0;
}
/*按照长排个序后删去长和宽都小于另一块土地的土地,那么宽就是递减的,那么每次肯定是买一个连续区间*/
例题:bzoj3156
用f[i]表示第i个检查点修筑防御塔,前i个检查点的最少费用,那么:
f[i]=min(f[j]+a[i]+∑(i-k)[j<k<i]);[0<=j<i]
注意是大于等于0,因为i可能是第一个防御塔!!!!!
化简一下得到:f[i]=min(f[j]+a[i]+(i-j-1)*i-∑k[j<k<i]);[0<=j<i]
然后我们可以把∑k[j<k<i]用前缀和搞一搞,这样就能斜率优化了!!!!
#include<iostream>
#include<cstdio>
#include<climits>
#include<cstring>
#include<algorithm>
using namespace std;
#define ll long long
int read(){
int q=0,w=1;char ch=' ';
while(ch!='-'&&(ch<'0'||ch>'9'))ch=getchar();
if(ch=='-')w=-1,ch=getchar();
while(ch>='0'&&ch<='9')q=q*10+ch-'0',ch=getchar();
return w*q;
}
const int N=1000005;
ll f[N],q[N],a[N],s[N];
int n;
ll up(int j,int k){return (f[j]+s[j])-(f[k]+s[k]);}
ll down(int j,int k){return j-k;}
int main()
{
int i,j,k,he=0,ta=0;
n=read();
for(i=1;i<=n;i++)a[i]=read(),s[i]=s[i-1]+i;
q[ta++]=0;//0要放进去!!!因为前面可以不修防御塔!!!!
for(i=1;i<=n;i++){
while(he+1<ta&&up(q[he+1],q[he])<=(ll)i*down(q[he+1],q[he]))
++he;
f[i]=f[q[he]]+(ll)(i-q[he]-1)*i-(s[i-1]-s[q[he]])+a[i];
while(he+1<ta&&up(i,q[ta-1])*down(q[ta-1],q[ta-2])<=down(i,q[ta-1])*up(q[ta-1],q[ta-2]))
--ta;
q[ta++]=i;
}
printf("%lld",f[n]);
return 0;
}