单调队列优化dp与斜率优化dp
单调队列优化dp是个相对比较不显然的优化。
例题:P2034 选择数字
题意:一串正整数,选择若干个数使和最大,且没有连续的超过k个数被选择。
首先显然是个dp题。方程也比较显然。设\(dp[i][1]\)为选择第i个数后最大值,
\(dp[i][0]\)为不选第\(i\)个数的最大值,\(s\)数组存前缀和。则方程也很显然:
\[dp[i][0]=\max(dp[i-1][0],dp[i-1][1])
\]
\[dp[i][1]=\max_{j=i-k}^i(dp[j][0]+s[i]-s[j])
\]
暴力枚举时间复杂度为\(O(n^2)\),1e5数据显然会超时。考虑优化。
使用单调队列可以快速排出过时决策。具体流程为:
- \(dp[i][0]\)赋初值,\(dp[i][1]=dp[i-1][0]+s[i]-s[j]\)。
- 如果队首元素与当前元素距离超过k则出队(排除过时决策)。
- 用队首元素更新\(dp[i][1]\)。
- 更新队尾。我们在一次计算中设原本的外层循环i为常量,j为变量,用单调队列维护j的值。由于这个dp方程中i与j不相关,我们就维护一个单调递减的队列,保存\(dp[j][0]-s[j]\)。
- 将i入队。
于是代码实现为:
l=1;
for(int i=1;i<=n;i++){
dp[i][0]=max(dp[i-1][0],dp[i-1][1]);
dp[i][1]=dp[i-1][0]+s[i]-s[i-1];
while(l<=r()&&q[l]<i-k)l++;
if(l<=r)dp[i][1]=dp[q[l]][0]+s[i]-s[q[l]];
while(l<=r&&dp[i][0]-s[i]>=dp[q[r]][0]-s[q[r]])r--;
q[++r]=i;
}
接下来是斜率。还是例题:P3195 玩具装箱
一般这种题的方程大概都不会太难。这个题的方程也很显然:设\(dp[i]\)为到第i件物品的最小费用。于是:
\[dp[i]=\min^i_{j=0}(dp[j]+(s[i]+i-s[j]-j-l-1)^2)
\]
可以看到这个式子是有同时出现i和j的项的。此时单调队列显然不可行。于是考虑斜率优化。把这个式子拆开,化成一个关于i,j的式子。其中,只有j的项为y,只有i的项为b,同时含有i,j的项中,i为k,j为x。这样我们便得到了一个一次函数。
对于外层循环的每个i,内部的k都会变化。我们需要的是大于等于此时k的第一个值。显然用单调队列维护。直接上代码:
inline int y(int x){return dp[x]+(s[x]+x)*(s[x]+x);}
inline int x(int a){return s[a]+a;}
inline int k(int x){return 2*(s[x]+x-len-1);}
inline double slope(int x1,int x2){return (y(x2)-y(x1))*1.0/(x(x2)-x(x1));}
inline int solve(int x1,int x2){int ss=x1-x2+s[x1]-s[x2]-len-1;return dp[x2]+ss*ss;}
for(int i=1;i<=n;i++){
while(l<r&&slope(q[l],q[l+1])<k(i))l++;
dp[i]=solve(i,q[l]);
while(l<r&&slope(q[r-1],q[r])>=slope(q[r],i))r--;
q[++r]=i;
}
可以看到,我们实际上维护的是一个下凸壳。另外注意一下,当k有正有负的时候,不能用单调队列,要用二分来查找最小值。还有的题是维护上凸壳。
快踩