斜率DP总结
chunlvxiong的博客
T1:防御准备
三个月后第一次写博客,我们从这个题开始:http://www.lydsy.com/JudgeOnline/problem.php?id=3156。
这道题DP方程比较好写:用dp[i]表示1到i全部被控制的最小代价,那么dp[i]=min{dp[j]+(i-j)*(i-j-1)/2+a[i]}/*表明j+1到i被i守卫*/
然后O(N^2)大T特T。
这里就要用到斜率优化DP,下面给出我推导这道题的过程。
设i从j转移比从k转移要优,那么:
dp[j]+(i-j)*(i-j-1)/2<dp[k]+(i-k)*(i-k-1)/2
dp[j]+[i^2-2ij+j^2-i+j]/2<dp[k]+[i^2-2ik+k^2-i+k]/2
dp[j]-dp[k]<[-2ik+k^2+k+2ij-j^2-j]/2
dp[j]-dp[k]<[k(k+1)-j(j+1)]/2-(ik-ij)
i(k-j)<{k(k+1)/2+dp[k]}-{j(j+1)/2+dp[j]}
j<k:{k(k+1)/2+dp[k]}-{j(j+1)/2+dp[j]}/(k-j)>i-->{j(j+1)/2+dp[j]}-{k(k+1)/2+dp[k]}/(j-k)>i
j>k:{k(k+1)/2+dp[k]}-{j(j+1)/2+dp[j]}/(k-j)<i-->{j(j+1)/2+dp[j]}-{k(k+1)/2+dp[k]}/(j-k)<i
由此可以得到一个很像斜率的东西:令yi=i(i+1)/2+dp[i],xi=i,那么你发现这个式子变成了:
j<k:(yj-yk)/(xj-xk)>i
j>k:(yj-yk)/(xj-xk)<i
这个东西维护起来要好很多,因此yj,yk,xj,xk都是不受i的影响的,如果你能把式子化成类似这样的形式,那么你几乎已经成功了。
一般斜率DP使用单调队列进行维护。(下面描述中,我们用g(a,b)表示(ya-yb)/(xa-xb))
队头维护:本题中优于i是递增的,用a表示队列第一项,用b表示队列第二项(a<b),那么g(b,a)<i时,则以后g(b,a)一定一直小于i,也就是说以后b一定一直优于a,那么可以将a弹出。
队尾维护:(这个你也可以画图维护一个类似凸包的东西,但我更喜欢直接推)
用a表示队尾倒数第二项,用b表示队尾最后一项,用c表示当前要插入的元素(a<b<c)。
可以发现当g(a,b)>g(b,c)时,b不可能成为最优解。
1、当g(a,b)>i,表明a优于b,b不是最优解。
2、当g(a,b)<i,则g(b,c)<g(a,b)<i,表明b劣于c,b不是最优解。
因此可以将b弹出。
转移的时候直接取出队头元素进行转移即可,整个DP复杂度变为O(N),A掉此题。
注意点:上述过程中,请重视正负性的问题,这也许会导致不等式变号,从而改变整个式子。
来个简单点的题目:http://www.lydsy.com/JudgeOnline/problem.php?id=1597(其实这道才是我的入门题啊!)
贴代码:(注:可以用乘积式代替g函数)
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int maxn=1000005; int n,front,rear; ll a[maxn],dp[maxn],q[maxn]; ll y(ll a){ return a*(a+1)/2+dp[a]; } ll x(ll a){ return a; } double g(ll a,ll b){ return (1.0*(y(a)-y(b)))/(1.0*(x(a)-x(b))); } int main(){ scanf("%d",&n); for (int i=1;i<=n;i++) scanf("%lld",&a[i]); dp[0]=0; front=rear=0,q[rear++]=0; for (ll i=1;i<=n;i++){ while (front<rear-1 && g(q[front+1],q[front])<1.0*i) front++; ll t=q[front]; dp[i]=dp[t]+(i-t)*(i-t-1)/2+a[i]; while (front<rear-1 && g(q[rear-2],q[rear-1])>g(q[rear-1],i)) rear--; q[rear++]=i; } printf("%lld\n",dp[n]); return 0; }
T2:小P的牧场
题目链接:http://www.lydsy.com/JudgeOnline/problem.php?id=3437
这题比上一题不同之处在于,它到控制它的控制站之间的牧场数目(不包括自身,但包括控制站所在牧场)乘上该牧场的放养量这句话有点碍眼。
单单使用一个sum前缀和似乎不能表示出DP方程。
如果i控制j+1到i之间的牧场,那么新的cost就是(i-j-1)*b[j+1]+(i-j-2)*b[j+2]+……+1*b[i-1]+0*b[i]+a[i](a[i]后面忽略不计)
你发现b[j+1]到b[i]的系数刚好都是-1下去的,所以你考虑维护一个square数组,square[i]=Σb[x]*x|1<=x<=i。
然后square[i]-square[j]=b[i]*i+b[i-1]*(i-1)+……+b[j+1]*(j+1)。可以用(sum[i]-sum[j])*i-(square[i]-square[j])来表示上面那个式子。
那么DP方程写出来了,用dp[i]表示1到i的站被控制的代价,那么dp[i]=min{dp[j]+(sum[i]-sum[j])*i-(square[i]-square[j])+a[i]}
请仿照上题自行整理成斜率DP的形式,顺便总结一下:
1、首先要化成(yj-yk)/(xj-xk)<a或者>a的形式,其中yi,xi是只跟i有关的式子,为了化成这样的式子,有时还需要引进前缀和等数组。
2、队头处理:考虑a递增或是递减的性质,然后通过g(x,y)与a的关系来判断是否弹出x。
3、队尾处理:类似于几何上凸包的形状,尽管我个人直接推导更不容易错,即g(x,y)<g(y,z)或g(x,y)>g(y,z)的形式。
4、DP时直接取出队头元素计算即可。
5、千万注意正负性问题,这是最大的易错点。(当初之所以搞不懂斜率DP就是因为忽视了正负性的因素)
类似的一题:http://www.lydsy.com/JudgeOnline/problem.php?id=1096
再来一题:http://www.lydsy.com/JudgeOnline/problem.php?id=1010
贴本题代码:
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int maxn=1000005; int n,front,rear,q[maxn]; ll a[maxn],b[maxn],sum[maxn],square[maxn],dp[maxn]; ll x(ll a){ return sum[a]; } ll y(ll a){ return dp[a]+square[a]; } double g(ll a,ll b){ return (1.0*(y(a)-y(b)))/(1.0*(x(a)-x(b))); } int main(){ scanf("%d",&n); for (int i=1;i<=n;i++) scanf("%lld",&a[i]); square[0]=sum[0]=0; for (ll i=1;i<=n;i++){ scanf("%lld",&b[i]); sum[i]=sum[i-1]+b[i],square[i]=square[i-1]+b[i]*i; } dp[0]=0; front=rear=0,q[rear++]=0; for (ll i=1;i<=n;i++){ while (front<rear-1 && g(q[front+1],q[front])<1.0*i) front++; int j=q[front]; dp[i]=dp[j]-(square[i]-square[j])+i*(sum[i]-sum[j])+a[i]; while (front<rear-1 && g(q[rear-2],q[rear-1])>g(q[rear-1],i)) rear--; q[rear++]=i; } printf("%lld\n",dp[n]); return 0; }
T3:[Apio2014]序列分割
题目链接:http://www.lydsy.com/JudgeOnline/problem.php?id=3675
这个题比较复杂,我们慢慢推。
最暴力的DP:需要三维,分别存储本次割点,上次割点,分割次数,然后再穷举上上次割点,进行转移,复杂度O(N^3K)=T到无边无际。
然后第一步非常的巧,我们画个图来说明吧:
割裂顺序有两种,得到的价值分别如下:
1、先割裂sum1和sum2,再割裂sum2和sum3,价值为sum1*(sum2+sum3)+sum2*sum3
2、先割裂sum2和sum3,再割裂sum1和sum2,价值为sum3*(sum1+sum2)+sum1*sum2
发现了什么-->两种割裂方式价值是一样的-->确定了割裂位置的话,割裂得到价值与割裂顺序无关。
这个结论的好处就是我们可以直接从开头一刀刀割向结尾,DP方程变为:(用dp[i][c]表示序列前i项已经割好,且割了c次的最大价值)
利用前缀和优化:dp[i][c]=max{dp[j][c-1]+(sum[i]-sum[j])*sum[j]}
复杂度是O(N^2K)的,仍然T的厉害,我们必须再去掉一个N,O(NK)才能A掉此题。
利用上述的斜率优化尝试一下:
dp[j][c-1]+(sum[i]-sum[j])*sum[j]>dp[k][c-1]+(sum[i]-sum[k])*sum[k]
(dp[j][c-1]-sum[j]^2)-(dp[k][c-1]-sum[k]^2)>sum[i]*(sum[k]-sum[j])
令yi=dp[i][c-1]-sum[i]^2,xi=sum[i]
j<k:(yj-yk)/(xk-xj)>sum[i]-->(yj-yk)/(xj-xk)<-sum[i]
j>k:(yj-yk)/(xk-xj)<sum[i]-->(yj-yk)/(xj-xk)>-sum[i]
后面队头队尾的维护请自行推导,复杂度可以少一个N,O(NK)应该能过去。
本题空间限制128MB,如果开一个100000*200的long long数组=MLE,因此需要滚动数组。
但是这题我调了很久,下面来好好说一说关于0的问题(本题(xj-xk)可能为0)
到(dp[j][c-1]-sum[j]^2)-(dp[k][c-1]-sum[k]^2)>sum[i]*(sum[k]-sum[j])这一步为止,我们只进行加减法,所以这一步的式子是可靠的。
那么xj-xk=0,所以要判断yj-yk是否大于0即可,如果yj-yk>0,那么无论sum[i]等于几,j都优于k。
好像用乘积式可以解决问题,但是我用乘积式一直WA,所以换了一种更好的解决0问题的方法(对于本题而言),由于本题0的存在毫无意义,在输入时把ai=0的全部删去,以保证xj-xk不等于0。
贴代码:
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int maxn=100005; int n,k,front,rear,q[maxn]; ll a[maxn],sum[maxn],dp[maxn],Dp[maxn]; ll x(ll a){ return sum[a]; } ll y(ll a){ return dp[a]-sum[a]*sum[a]; } double g(ll a,ll b){ return (1.0*(y(a)-y(b)))/(1.0*(x(a)-x(b))); } int main(){ scanf("%d%d",&n,&k),sum[0]=0; for (int i=1;i<=n;i++){ scanf("%lld",&a[i]); if (!a[i]) n--,i--; else sum[i]=sum[i-1]+a[i]; } memset(dp,0,sizeof(dp)); for (int c=1;c<=k;c++){ front=rear=0,q[rear++]=0; for (int i=1;i<=n;i++){ while (front<rear-1 && g(q[front+1],q[front])>-sum[i]) front++; int j=q[front]; Dp[i]=dp[j]+(sum[i]-sum[j])*sum[j]; while (front<rear-1 && g(q[rear-2],q[rear-1])<g(q[rear-1],i)) rear--; q[rear++]=i; } for (int i=1;i<=n;i++) dp[i]=Dp[i]; } printf("%lld\n",Dp[n]); return 0; }
下面给一道相对简单的题目:http://www.lydsy.com/JudgeOnline/problem.php?id=1911