也有一阵子没写blog了,重新开坑。
首先明确一个概念,比起插头dp、轮廓线dp惨不忍睹的考频,裸单调队列优化dp那样太过简单(容易看出、模拟),以及拥有替代所有四边形不等式优化dp来说,斜率dp一定是一个可以高度结合数学图形与代数的重要模块。
所以说,什么是斜率dp呢?
假设b > 0(反之亦然),则我们的任务是使得这条直线的纵截距最小。可以想象有一组斜率相同的直线自负无穷向上平移,所碰到的第一个数据点就是最优决策。
这个时候,有一个重要的性质,那就是:所有最优决策点都在平面点集的凸包上。基于这个事实,我们可以开发出很多令人满意的算法。
当决策直线的斜率与二元组的横坐标同时满足单调性时,这个时候由于斜率变化是单调的,所以决策点必然在凸壳上单调移动。我们只需要维护一个单调队列和一个决策指针,每次决策时作这样几件事:
1、决策指针(也就是队首)后移,直至最佳决策点。2、进行决策。3、将进行决策之后的新状态的二元组加入队尾,同时作Graham-Scan式的更新操作维护凸壳。(注意此时当前指针所在二元组有可能被抛弃)
算法的时间复杂度为O(n)
这些都是一些有的没的定义,其实放在程序中就是在
judge时以
X()Y()=<or>=i
来决定是否将当前状态加入到队列中(以此来影响之后的dp值)。
来一道很经典的题:
https://cn.vjudge.net/problem/HDU-2829
Lawrence
下面给出三种做法:
四边形优化并没有想象中那么因为减小了很多常数而变快,总用时260ms+
这种方法是最简单的,主要是减少枚举k的次数。cost[i][j]是某段区间的权值,当区间变大,权值也随之变大,区间变小,权值也随之变小,此时就可以用四边形不等式优化。
我们设s[i][j]为dp[i][j]的前导状态,即dp[i][j] = dp[s[i][j][j-1] + cost[s[i][j]+1][j]。之后我们枚举k的时候只要枚举s[i][j-1]<=k<=s[i+1][j],此时j必须从小到大遍历,i必须从大到小。
#include<cstdio>
#define MAX 1100
#define INF (1<<30)
int n,m,sum[MAX],cost[MAX][MAX];
int arr[MAX],dp[MAX][MAX],s[MAX][MAX];
inline int read(){
static char ch;static int res;
while((ch=getchar())<'0'||ch>'9');res=ch-48;
while((ch=getchar())<='9'&&ch>='0')res=res*10+ch-48;
return res;
}
int main(){
while(scanf("%d%d",&n,&m),n+m){
for(register int i=1;i<=n;++i)
arr[i]=read(),sum[i]=arr[i]+sum[i-1];
for(register int i=1;i<=n;++i)
for(register int j=1;j<=n;++j)
if(j<i)cost[i][j]=0;
else cost[i][j]=cost[i][j-1]+arr[j]*(sum[j-1]-sum[i-1]);
for(register int i=1;i<=n;++i)
dp[i][0]=cost[1][i],s[i][0]=0,s[n+1][i]=n;
for(register int j=1;j<=m;++j)
for(register int i=n;i>=1;--i){
dp[i][j]=INF;
for(register int k=s[i][j-1];k<=s[i+1][j];++k)
if(dp[k][j-1]+cost[k+1][i]<dp[i][j])
s[i][j]=k,dp[i][j]=dp[k][j-1]+cost[k+1][i];
}
printf("%d\n",dp[n][m]);
}
}
接下来也就是斜率优化一
//sum[i] = arr[1] + .. arr[i]^2
//sum2[i] = arr[1]^2 + .. arr[i]^2;
//dp[i][j] = min{dp[k][j-1] -sum[i] * sum[k] + (suma[k] - sum[k]^2)/2 + (sum[k]^2 - suma[k])/2};
#include<cstdio>
#define MAX 1100
struct point {
long long x,y;
}pot[MAX];
int head,tail,qu[MAX];
int n,m,arr[MAX];
long long sum[MAX],sum2[MAX],dp[MAX][MAX];
int judge1(point p0,point p1,point p2){
return(p0.x-p1.x)*(p0.y-p2.y)-(p0.y-p1.y)*(p0.x-p2.x)<=0;
}
int judge2(point p0,point p1,int k){
return p0.y-k*p0.x>p1.y-k*p1.x;
}
inline int read(){
static char ch;static int res;
while((ch=getchar())<'0'||ch>'9');res=ch-48;
while((ch=getchar())<='9'&&ch>='0')res=res*10+ch-48;
return res;
}
int main(){
while(scanf("%d%d",&n,&m),n+m) {
for(register int i=1;i<=n;++i){
arr[i]=read(),sum[i]=arr[i]+sum[i-1];
sum2[i]=arr[i]*arr[i]+sum2[i-1];
dp[i][0]=dp[i-1][0]+arr[i]*sum[i-1];
}
for(register int j=1;j<=m;++j){
head=tail=qu[tail]=0;
for(register int i=j+1;i<=n;++i){
pot[i].x=sum[i-1];
pot[i].y=dp[i-1][j-1]+((sum[i-1]*sum[i-1]+sum2[i-1])>>1);
while(head<=tail-1&&judge1(pot[qu[tail-1]],pot[qu[tail]],pot[i]))--tail;
qu[++tail]=i;
while(head+1<=tail&&judge2(pot[qu[head]],pot[qu[head+1]],sum[i]))++head;
dp[i][j]=pot[qu[head]].y-sum[i]*pot[qu[head]].x+((sum[i]*sum[i]-sum2[i])>>1);
}
}
printf("%d\n",dp[n][m]);
}
}
当然我不准备细讲,总用时109ms
下面我要重点讲的是斜率优化二
定义状态dp[i][j]表示前i点,分为j组的最小代价。
cost[i][j]表示i到j分为一组的代价。
dp[i][j] = min(dp[k][j-1] + cost[k+1][i])
cost[1][i] = cost[1][k] + cost[k+1][i] + sum[k] * (sum[i]-sum[k])
cost[k+1][i] = cost[1][i] - cost[1][k] - sum[k]*(sum[i]-sum[k])
得出
dp[i][j] = min(dp[k][j-1] + cost[1][i] – cost[1][k] – sum[k] * (sum[i]-sum[k]) )
dp[i][j] = min(dp[k][j-1] – cost[1][k] – sum[k] * sum[i] + sum[k] * sum[k]) ) + cost[1][i]
//cost[k+1][i]=cost[1][i]-cost[1][k]-sum[k]*(sum[i]-sum[k])
//dp[i][j]=dp[k][j-1]+cost[1][i]-cost[1][k]-sum[k]*(sum[i]-sum[k])
// =dp[k][j-1]-cost[1][k]+sum[k]^2-sum[i]*sum[k]+cost[1][i]
#include<cstdio>
#define MAX 1100
struct point {
int x,y;
}pot[MAX];
int head,tail,qu[MAX],n,m,arr[MAX],cost[MAX];
int sum[MAX],dp[MAX][MAX];
int judge1(point p0,point p1,point p2){
return (p0.x-p1.x)*(p0.y-p2.y)-(p0.y-p1.y)*(p0.x-p2.x)<=0;
}
int judge2(point p0,point p1,int k){
return p0.y-k*p0.x>p1.y-k*p1.x;
}
inline int read(){
static char ch;static int res;
while((ch=getchar())<'0'||ch>'9');res=ch-48;
while((ch=getchar())<='9'&&ch>='0')res=res*10+ch-48;
return res;
}
int main(){
while(scanf("%d%d",&n,&m),n+m){
for(register int i=1;i<=n;++i){
arr[i]=read(),sum[i]=arr[i]+sum[i-1];
dp[i][0]=cost[i]=cost[i-1]+arr[i]*sum[i-1];
}
for(register int j=1;j<=m;++j){
head=tail=qu[tail]=0;
for(register int i=j+1;i<=n;++i){
pot[i].x=sum[i-1];
pot[i].y=dp[i-1][j-1]-cost[i-1]+sum[i-1]*sum[i-1];
while(head<=tail-1&&judge1(pot[qu[tail-1]],pot[qu[tail]],pot[i]))--tail;
qu[++tail]=i;
while(head+1<=tail&&judge2(pot[qu[head]],pot[qu[head+1]],sum[i]))++head;
dp[i][j]=pot[qu[head]].y-sum[i]*pot[qu[head]].x+cost[i];
}
}
printf("%d\n",dp[n][m]);
}
}
总用时64ms
可见斜率优化是有保障的,在于我们如果能正确地推导出dp方程,并且将其变形。
这种形式可以一开始就拿去套,之后便是单调队列,或者平衡树来维护了。