斜率优化dp总结

_zjz

2018-11-22 21:36:54

Personal

首先以玩具装箱为例: 题目链接:[玩具装箱](https://www.lydsy.com/JudgeOnline/problem.php?id=1010) 设${f[i]}$为到前i个玩具的最小价值 易得:${f[i]=min(f[i],f[j]+(a[i]-b[j])^2)}$ 其中${a[i]=sum[i]+i,b[i]=sum[i]+i+l+1}$; 但时间复杂度为O(n^2),需要优化; 将上式展开,得: ${f[i]=f[j]+a[i]^2-2*a[i]*b[j]+b[j]^2}$ 可以将${2*a[i]}$ 看为斜率,${(b[j],f[j]+b[j]^2)}$ 看成平面直角坐标系上的点 因此就把上述该式转为形如y=kx+b的式子 我们要让f[i]最小, 所以就要找一个合法的j,使得该直线在y轴上的截距最小 ![](https://cdn.luogu.com.cn/upload/pic/13267.png) 当枚举到i点时,最优的点组成了一个下凸包 显然,凸包中相邻两点的斜率单调递增, 目标直线2*a[i]也是单调递增 根据线性规划知识易知,满足最优pj的点一定是第一个 第一个slope(Pj,Pj+1)>2*a[i]的点 因为加入slope(Pj,Pj+1) <= 2*a[i],我们可以通过将Pj移动到Pj+1来让答案更优 那么如何维护凸包??? 单调队列: ```cpp 1.while(head < tail && slope(q[head],q[head + 1]) < 2 *a(i))++ head; 2.f[i] = f[q[head]] + (a(i) - b(q[head])) * (a(i) - b(q[head])); 3.while(head < tail && slope(i,q[tail - 1]) < slope(q[tail - 1],q[tail])) -- tail; 4.q[++ tail] = i; ``` 解释: 若slope(Pj,Pj+1)<2*a[i],显然Pj不是最优直接删掉 因为目标直线斜率单调递增,所以当前删去的Pj 一定对之后的dp[i]也不是最优,不会造成影响 而操作3的理由如下: ![](https://cdn.luogu.com.cn/upload/pic/13443.png) 显然,满足操作3的要求时,P_tail凸包内部,一定不是最优,因此可以删去 以上也就是斜率优化的基本思想. 放上该题的代码: ```cpp #include<bits/stdc++.h> #define int long long using namespace std; inline int read() { int X = 0,w = 0;char ch = 0; while(!isdigit(ch)){w |= ch == '-';ch = getchar();} while(isdigit(ch)){X = X * 10 + ch - '0';ch = getchar();} return w ? -X:X; } const int N = 5e5 + 101; int f[N], sum[N]; int n, l, q[N]; double a(int x){return sum[x] + x;} double b(int x){return sum[x] + x + l + 1;} double X(int x){return b(x);} double Y(int x){return f[x] + b(x) * b(x);} double slope(int i,int j){return (Y(i) - Y(j)) / (X(i) - X(j));} signed main() { n = read();l = read(); for(int i = 1;i <= n;i ++) scanf("%lld",&sum[i]),sum[i] += sum[i - 1]; int head = 1, tail = 1; for(int i = 1;i <= n;i ++) { while(head < tail && slope(q[head],q[head + 1]) < 2 * a(i)) ++ head; f[i] = f[q[head]] + (a(i) - b(q[head])) * (a(i) - b(q[head])); while(head < tail && slope(i,q[tail - 1]) < slope(q[tail - 1],q[tail])) -- tail; q[++ tail] = i; } cout << f[n]; } ``` ### 一些例题: 1.APIO特别行动队: 原题链接:[特别行动队](https://www.lydsy.com/JudgeOnline/problem.php?id=1911) 我们设${f[i]}$为到i的最大价值; 易得,${f[i]=min(f[j]+a*x*x+b*x+c,f[i])}$; 其中x为${sum[i]-sum[j]}$ 考虑斜率优化: 我们将上述dp方程展开就可已得到: ${(2*sum[i]+b)*sum[j]+???=f[j]+a*sum[j]^2}$ 具体怎么推导就不细说了: 将${2*sum[i]+b}$看作斜率,${(sum[i],f[i]+a*sum[j]^2)}$看为点 维护一个下凸包即可,具体的直接套板子就行 详见代码: ```cpp #include<bits/stdc++.h> #define int long long using namespace std; inline int read() { int X=0,w=0;char ch=0; while(!isdigit(ch)){w|=ch=='-';ch=getchar();} while(isdigit(ch)){X=X*10+ch-'0';ch=getchar();} return w?-X:X; } const int N=2e6+101; int n,a,b,c; int val[N],f[N],sum[N],q[N]; template<typename T>T Max(T a,T b){return a>b?a:b;} double X(int i){return sum[i];} double Y(int i){return f[i]+a*sum[i]*sum[i];} double slope(int i,int j){return (Y(j)-Y(i))/(X(j)-X(i));} int Val(int x){return a*x*x+b*x+c;} signed main() { n=read();a=read();b=read();c=read(); for(int i=1;i<=n;i++)val[i]=read(); for(int i=1;i<=n;i++)sum[i]=sum[i-1]+val[i]; int head=1,tail=1; for(int i=1;i<=n;i++) { while(head<tail&&slope(q[head],q[head+1])>2*a*sum[i]+b)++head; f[i]=f[q[head]]+Val(sum[i]-sum[q[head]]); while(head<tail&&slope(i,q[tail-1])>slope(q[tail-1],q[tail]))--tail; q[++tail]=i; } printf("%lld\n",f[n]); return 0; } ``` 2.ZJOI仓库建设: 题目链接:[仓库建设](https://www.luogu.org/problemnew/show/P2120) 我们设${f[i]}$为到i点的最小价值 易得到: ${f[i]=min(f[i],f[j]+x[i]*(sum1[i]-sum1[j])-(sum2[i]-sum2[j]+c[i])}$ 其中,${sum1[i]}$为1到i的${p[i]}$前缀和 ${sum2[i]}$为1到i的${p[i]*c[i]}$的前缀和 将上式转化,得: ${x[i]*sum1[j]+???=f[i]+sum2[i]}$ 依旧套上面的板子即可: 详见代码: ```cpp #include<bits/stdc++.h> #define ll long long using namespace std; inline int read() { int X=0,w=0;char ch=0; while(!isdigit(ch)){w|=ch=='-';ch=getchar();} while(isdigit(ch)){X=X*10+ch-'0';ch=getchar();} return w?-X:X; } const int N=1e6+101; ll x[N],p[N],c[N],sum1[N],sum2[N],f[N],q[N]; int n; double X(int i){return sum1[i];} double Y(int i){return f[i]+sum2[i];} double slope(int i,int j){return (Y(i)-Y(j))/(X(i)-X(j));} ll Val(int l,int r) { return x[r]*(sum1[r]-sum1[l-1])-sum2[r]+sum2[l-1]+c[r]; } int main() { n=read(); for(int i=1;i<=n;i++){ x[i]=read();p[i]=read();c[i]=read(); } for(int i=1;i<=n;i++){ sum1[i]=sum1[i-1]+p[i];sum2[i]=sum2[i-1]+p[i]*x[i]; } int head=1,tail=1; for(int i=1;i<=n;i++) { while(head<tail&&slope(q[head],q[head+1])<x[i])++head; f[i]=f[q[head]]+Val(q[head]+1,i); while(head<tail&&slope(q[tail],q[tail-1])>slope(q[tail],i))--tail; q[++tail]=i; } cout<<f[n]; } ``` 3.征途 [征途](https://www.lydsy.com/JudgeOnline/problem.php?id=4518) 首先直接求方差显然不好求, 于是可以先化简方差的式子 设分的每一段和为x 所以: ${s^2=m^2*(1/m)*((x_1-x)^2+......(x_m-x)^2)}$ 于是: ${s^2=m*(x_1^2-2*x_1*x+x^2+.......x_m-2*x_m*x+x^2)}$ 将m乘进去: ${s^2=m*(x_1^2+.......+x_m^2)-2*m*x*sum+sum^2}$ 其中${sum}$为${1-n}$的总和 这样我们只需让分的每一段的和的平方的总和最小即可; 答案即为$f[m][n]*m-sum[n]*sum[n]$; 直接dp是${O(n^2m)}$需要优化 考虑斜率优化: 在枚举${f[j][i]}$最优何时取到可以斜率优化; 因此:设$z=j-1$ 所以: $f[j][i]=f[z][k]+(sum[i]-sum[k])^2$ 展开: $2*sum[i]*sum[k]+???=f[z][k]+sum[k]^2$ 把$2*sum[i]$看为斜率,$(sum[k],f[z][k]+sum[k]^2)$ 看成一个点 每次枚举$j$时直接${O(n)}$的维护下凸包即可; 其他的就是套斜率优化的板子即可; ``` #include<bits/stdc++.h> #define int long long using namespace std; inline int read() { int X=0,w=0;char ch=0; while(!isdigit(ch)){w|=ch=='-';ch=getchar();} while(isdigit(ch)){X=X*10+ch-'0';ch=getchar();} return w?-X:X; } //f[i]=min(f[i],f[j]+(sum[i]-sum[j])^2; const int N=4005; int f[N][N],sum[N],q[N],z,n,m; double X(int i){return sum[i];} double Y(int i){return f[z][i]+sum[i]*sum[i];} double slope(int i,int j){return (Y(j)-Y(i))/(X(j)-X(i));} int pi(int x){return x*x;} signed main() { n=read();m=read(); for(int i=1;i<=n;i++) sum[i]=read(),sum[i]+=sum[i-1]; f[0][0]=0; for(int i=1;i<=n;i++)f[1][i]=sum[i]*sum[i]; for(int j=2;j<=m;j++) { int head=1,tail=1; z=j-1; memset(q,0,sizeof(q)); for(int i=1;i<=n;i++) { while(head<tail&&slope(q[head],q[head+1])<2*sum[i])++head; f[j][i]=f[z][q[head]]+pi(sum[i]-sum[q[head]]); while(head<tail&&slope(q[tail],q[tail-1])>slope(q[tail-1],i))--tail; q[++tail]=i; } } cout<<f[m][n]*m-sum[n]*sum[n]; } ``` 4.土地购买 [土地购买](https://www.lydsy.com/JudgeOnline/problem.php?id=1597) 这题好像无法直接dp; 没关系,我们转化一下即可 首先我们需要由题意知道: 假如一个矩形被另一个矩形所嵌套 则这个矩形对答案一定没有贡献; 我们首先将这些矩形中没有用的全部删去 将x为第一关键字从小到大排序,y为第二关键字从大到小排序; 于是我们得到的序列一定是x单增,y单减的序列 于是得到方程$f[i]=min(f[i],f[j]+x[i]*y[j+1]);$ 套斜率优化即可, 具体怎么优化就不再赘述了. 放上本题代码: ``` #include<bits/stdc++.h> #define int long long using namespace std; inline int read() { int X=0,w=0;char ch=0; while(!isdigit(ch)){w|=ch=='-';ch=getchar();} while(isdigit(ch)){X=X*10+ch-'0';ch=getchar();} return w?-X:X; } const int N=2e6+101; struct node{ int x,y,flag; bool operator < (const node &a)const{ return x==a.x?y<a.y:x<a.x; } }b[N],c[N]; int n,cnt,q[N],f[N],e[N],maxn; bool cmp(node a,node b){return a.x==b.x?a.y>b.y:a.x<b.x;} double X(int i){return -c[i+1].y;} double Y(int i){return f[i];} double slope(int i,int j){return (Y(i)-Y(j))/(X(i)-X(j));} #define lowbit(i) i&-i void update(int x) { ++x; for(int i=x;i<=maxn+1;i+=lowbit(i)) e[i]++; } int query(int x) { int sum=0;++x; for(int i=x;i;i-=lowbit(i)) sum+=e[i]; return sum; } signed main() { n=read(); for(int i=1;i<=n;i++) b[i].x=read(),b[i].y=read(),maxn=max(maxn,b[i].y); sort(b+1,b+n+1); for(int i=n;i>=1;i--) { if((n-i)-query(b[i].y-1)>0)b[i].flag=1; update(b[i].y); } for(int i=1;i<=n;i++) if(!b[i].flag)c[++cnt]=b[i]; int head=1,tail=1; for(int i=1;i<=cnt;i++) { while(head<tail&&slope(q[head],q[head+1])<c[i].x)++head; f[i]=f[q[head]]+c[i].x*c[q[head]+1].y; while(head<tail&&slope(q[tail],q[tail-1])>slope(q[tail-1],i))--tail; q[++tail]=i; } cout<<f[cnt]; } ``` ------------ 小结: 1.斜率优化的数的方式是比较好理解的,我们每次找到动态规划的状态转移方程后就可以直接转换 2.斜率优化只适用于具有单调性的转移方程,即最后简化后的等式左右中的一方必定有一个是给定单调的函数,譬如前缀和。 3.每次斜率优化都要设更优解的自变量,然后用更优解来维护斜率 4.保证储存斜率的队列单调 5.很值得注意到一点,对于初学者而言我们可能会想,为什么可以把斜率转换成这样的形式,其实不然,斜率的形式并不唯一,我们不需要去刻意仿照别人的斜率等式,只要二者等价,即都可以求出正解 6.注意正负,可以看见斜率的会有除法运算,记得不等式除负数时要变号(可能就我这么傻,不会变号),不然公式推错那就GG了 7.最后就是要在做题中理解斜率优化的妙用,可以参考一下他人的代码,或许会大有启发。 8.斜率优化关键在于式子的推导,只要推导正确其他就是板子了