作为斜率优化dp的入门题,可以说的非常经典了。斜率dp是一类数形结合的题目,数的方面让你写出程序,而形的方面则让你更清楚的理解正确性。
首先,这题的状态转移方程是很好写的:
dp[i]=min(dp[i],dp[j]+(sum[j]−sum[i])2+M)
我们优化的最终目的就是要在o(1)的时间内可以求出最优的dp[j]。
下面的做法就是斜率dp的一般套路:
首先,在斜率dp中,一定要同时讨论两个单调性(自变量的单调性和因变量的单调性,对应于这题就是,i(下标是一个个递增的),dp[i]),不同时讨论这两个变量的单调性都是在耍流氓。
先假设j<k,同时dp[k]优于dp[j]。(同时满足单调性)
然后就可以列出方程:dp[k]+(sum[k]−sum[i])2+M < dp[j]+(sum[j]−sum[i])2+M .
然后就是化简(化简的时候不要忘记j,k的大小,因为会影响不等号方向)
得:
dp[j]+sum[j]^2-(dp[k]+sum(k)^2) }/( sum[j]-sum[k] ) < 2*sum[i];
也就是说,只要满足上面的不等式,k就会优于j,那么我们就可以直接忽略j。
只要维护上面的性质,我们就可以在o(1)时间找到最优的值。
下面考虑,把第i项加入到队列中的情况:
令 solve( j,k ) = { dp[j]+sum[j]^2-(dp[k]+sum(k)^2) }/( sum[j]-sum[k] );
对于任意的 j,k,p ( j<k<p ),有solve( j,k ),solve( k,p );
如果 solve(j,k) > solve(k,p) ,那么有如下两种情况:
if( solve( k,p )<2*sum[i] ) p优于k,则k是无用的决策 if( solve( k,p )>2*sum[i] ) 能推出solve( j,k )>2*sum[i],也就是说k不比j优,同样的k是无用的决策,可以去掉 so,只要满足这个大的IF条件,那么k就是无用的决策
上面的情况画出图就是说,只要构成一个上凸点,那么上凸的点必然不是最优的点,可以去除。
其实这也是一个结论:最优值只会在下凸点取得,所以思路和维护一个凸包的点很类似。
总结一下就是:分两步,1 先维护队首元素的最优性质,得出dp[i]后,考虑将第i个点放入队列中,同时利用类似凸包的方法维护。
下面介绍一下一位大佬做出的数形解释:
https://blog.csdn.net/qq_34542903/article/details/54563477
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int inf=0x3f3f3f3f;
const int maxn=5e5+7;
const int mod=1e9+7;
ll dp[maxn],sum[maxn];
int q[maxn];
int n,m;
int solve(int j,int k) //k优于j
{
if(sum[j]==sum[k])
{
if(dp[j]>dp[k])
{
return -1;
}
else
{
return inf;
}
}
return (dp[j]+sum[j]*sum[j]-(dp[k]+sum[k]*sum[k]))/(sum[j]-sum[k]);
}
int main()
{
#ifndef ONLINE_JUDGE
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
#endif
while(scanf("%d%d",&n,&m)!=EOF)
{
sum[0]=0;
for(int i=1;i<=n;i++)
{
scanf("%lld",&sum[i]);
sum[i]+=sum[i-1];
}
int head,tail;
head=tail=0;
dp[0]=0;
for(int i=1;i<=n;i++)
{
while(head<tail&&solve(q[head],q[head+1])<2*sum[i])
{
head++;
}
dp[i]=dp[q[head]]+(sum[i]-sum[q[head]])*(sum[i]-sum[q[head]])+m;
while(head<tail&&solve(q[tail-1],q[tail])>solve(q[tail],i))
{
tail--;
}
q[++tail]=i;
}
printf("%lld\n",dp[n]);
}
return 0;
}