从斜率DP讲起

来源:互联网 发布:初创公司如何管理 知乎 编辑:程序博客网 时间:2024/06/08 06:48

也有一阵子没写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 1100struct 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 1100struct 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方程,并且将其变形。

X()Y()=<or>=i

这种形式可以一开始就拿去套,之后便是单调队列,或者平衡树来维护了。

0 0
原创粉丝点击