知识点:斜率优化DP
前言
最近刷BZOJ的题目的时候,发现做到了很多题目都是用到了斜率优化,这个优化很早也接触过,但也没有仔细地去学。最近认真的去学了一下,就在这里做个整理
概要
斜率优化是基于单调队列或单调栈的一种优化DP的方式,当DP的决策具有单调性的时候,我们就可以通过维护单调队列或者单调栈来维护这个最优的决策,最终实现\(O(1)\)的DP转移。
知识点讲解
斜率优化如果空着讲实际上有点虚的,所以这里以BZOJ1597这题比较基础的斜率优化DP为例。
单调性归纳证明
之前说过,在使用斜率优化之前,应该证明这个DP转移是具有决策的单调性的。首先我们先知道什么是决策单调性。决策单调性大致就是指对于\(dp[i]\)的转移,如果任意两种转移方式\(dp[j]\rightarrow dp[i]\)、\(dp[k]\rightarrow dp[i]\),\(j<k\)并且从\(k\)转移至\(i\)要优于\(j\),那么对于\(dp[i+1]\)的转移中,从\(k\)转移至\(i+1\)也一定优于\(j\)。
下面我们开始证明:
对于题目,我们可以先把所有的土地按\(a\)升序排序,然后对于\(b\)取出递减的一串(因为如果不是递减的话,那么就可以和别的土地一并购买,对答案并没有贡献,所以可以不用计算)。这样我们就得到了一个\(a\)递增,\(b\)递减的序列。这样我们就可以很简单的列出\(dp\)转移方程。记\(dp[i]\)为枚举到第\(i\)块土地时,最少的花费为\(dp[i]\),\(a[i],b[i]\)分别为土地的长和宽,于是我们很容易得到转移方程:\(dp[i]=min(dp[j]+a[i]*b[j+1])\),并且这里的\(b\)是递增的。
现在我们假设有\(j\),\(k\)\((j<k)\)两种方式转移至\(i\),并且\(k\)转移要优于\(j\),所以我们可以得到:
/**************************************************************
Problem: 1597
User: czl2333
Language: C++
Result: Accepted
Time:168 ms
Memory:5612 kb
****************************************************************/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
bool Finish_read;
template<class T>inline void read(T &x){Finish_read=0;x=0;int f=1;char ch=getchar();while(!isdigit(ch)){if(ch=='-')f=-1;if(ch==EOF)return;ch=getchar();}while(isdigit(ch))x=x*10+ch-'0',ch=getchar();x*=f;Finish_read=1;}
template<class T>inline void print(T x){if(x/10!=0)print(x/10);putchar(x%10+'0');}
template<class T>inline void writeln(T x){if(x<0)putchar('-');x=abs(x);print(x);putchar('\n');}
template<class T>inline void write(T x){if(x<0)putchar('-');x=abs(x);print(x);}
/*================Header Template==============*/
#define PAUSE printf("Press Enter key to continue..."); fgetc(stdin);
const int maxn=1e5+500;
int n,tot;
struct plot {
ll a,b;
bool operator < (const plot &rhs) const {
return a!=rhs.a?a<rhs.a:b<rhs.b;
}
}p[maxn],pp[maxn];
ll f[maxn];
int l,r;
int que[maxn];
/*==================Define Area================*/
double Cal(int x,int y) {
return (double)(f[y]-f[x])/(double)(pp[x+1].b-pp[y+1].b);
}
int main() {
read(n);
for(int i=1;i<=n;i++) {
read(p[i].a);read(p[i].b);
}
sort(p+1,p+1+n);
for(int i=1;i<=n;i++) {
while(tot&&pp[tot].b<=p[i].b) tot--;
pp[++tot]=p[i];
}
for(int i=1;i<=tot;i++) {
while(l<r&&Cal(que[l],que[l+1])<pp[i].a) l++;
int t=que[l];
f[i]=f[t]+pp[i].a*pp[t+1].b;
while(l<r&Cal(que[r],i)<Cal(que[r-1],que[r])) r--;
que[++r]=i;
}
printf("%lld\n",f[tot]);
return 0;
}
练习题
BZOJ1096 仓库建设
解题思路
很容易推出DP转移方程:
/**************************************************************
Problem: 1096
User: czl2333
Language: C++
Result: Accepted
Time:2032 ms
Memory:52100 kb
****************************************************************/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
bool Finish_read;
template<class T>inline void read(T &x){Finish_read=0;x=0;int f=1;char ch=getchar();while(!isdigit(ch)){if(ch=='-')f=-1;if(ch==EOF)return;ch=getchar();}while(isdigit(ch))x=x*10+ch-'0',ch=getchar();x*=f;Finish_read=1;}
template<class T>inline void print(T x){if(x/10!=0)print(x/10);putchar(x%10+'0');}
template<class T>inline void writeln(T x){if(x<0)putchar('-');x=abs(x);print(x);putchar('\n');}
template<class T>inline void write(T x){if(x<0)putchar('-');x=abs(x);print(x);}
/*================Header Template==============*/
#define PAUSE printf("Press Enter key to continue..."); fgetc(stdin);
const int maxn=1e6+500;
int n;
ll dis[maxn],c[maxn],p[maxn],f[maxn],sum[maxn],b[maxn];
int l,r;
int que[maxn];
/*==================Define Area================*/
double Cal(int x,int y) {
return (double)(f[y]-f[x]+b[y]-b[x])/(double)(sum[y]-sum[x]);
}
int main() {
read(n);
for(int i=1;i<=n;i++) {
read(dis[i]);read(p[i]);read(c[i]);
}
for(int i=1;i<=n;i++) {
sum[i]=sum[i-1]+p[i];
b[i]=b[i-1]+p[i]*dis[i];
}
for(int i=1;i<=n;i++) {
while(l<r&&Cal(que[l],que[l+1])<dis[i]) l++;
int t=que[l];
f[i]=f[t]-b[i]+b[t]+(sum[i]-sum[t])*dis[i]+c[i];
while(l<r&&Cal(que[r-1],que[r])>Cal(que[r],i)) r--;
que[++r]=i;
}
printf("%lld\n",f[n]);
return 0;
}
BZOJ1911 特别行动队
解题思路
还是同普通的DP一样,我们记\(dp[i]\)为枚举到第\(i\)个士兵时,能够得到的最大战斗力是多少。同时我们记\(sum[i]\)表示前\(i\)个士兵战斗力之和。那么转移方程就是\(dp[i]=min(dp[j]+a*(sum[i]-sum[j])^2+b*(sum[i]-sum[j])+c)\)。如果\(k>j\)并且\(k\)比\(j\)更优,那么:
\(dp[j]+a*(sum[i]-sum[j])^2+b*(sum[i]-sum[j])+c\leq dp[j]+a*(sum[i]-sum[k])^2+b*(sum[i]-sum[k])+c\)
整理之后得出:
/**************************************************************
Problem: 1911
User: czl2333
Language: C++
Result: Accepted
Time:1220 ms
Memory:48180 kb
****************************************************************/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
bool Finish_read;
template<class T>inline void read(T &x){Finish_read=0;x=0;int f=1;char ch=getchar();while(!isdigit(ch)){if(ch=='-')f=-1;if(ch==EOF)return;ch=getchar();}while(isdigit(ch))x=x*10+ch-'0',ch=getchar();x*=f;Finish_read=1;}
template<class T>inline void print(T x){if(x/10!=0)print(x/10);putchar(x%10+'0');}
template<class T>inline void writeln(T x){if(x<0)putchar('-');x=abs(x);print(x);putchar('\n');}
template<class T>inline void write(T x){if(x<0)putchar('-');x=abs(x);print(x);}
/*================Header Template==============*/
#define PAUSE printf("Press Enter key to continue..."); fgetc(stdin);
const int maxn=2e6+500;
int n;
int a,b,c;
int x[maxn];
int l,r;
int que[maxn];
ll f[maxn],sum[maxn];
/*==================Define Area================*/
ll Sqr(ll x) {
return x*x;
}
double Cal(int x,int y) {
return (double)(f[y]-f[x]+a*(Sqr(sum[y])-Sqr(sum[x]))-b*(sum[y]-sum[x]))/(double)(2*a*(sum[y]-sum[x]));
}
int main() {
read(n);
read(a);read(b);read(c);
for(int i=1;i<=n;i++) {
read(x[i]);
}
for(int i=1;i<=n;i++) sum[i]=sum[i-1]+x[i];
for(int i=1;i<=n;i++) {
while(l<r&&Cal(que[l],que[l+1])<sum[i]) l++;
int t=que[l];
f[i]=f[t]+a*Sqr(sum[i]-sum[t])+b*(sum[i]-sum[t])+c;
while(l<r&&Cal(que[r-1],que[r])>Cal(que[r],i)) r--;
que[++r]=i;
}
printf("%lld\n",f[n]);
return 0;
}