单调队列优化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数据显然会超时。考虑优化。

使用单调队列可以快速排出过时决策。具体流程为:

  1. \(dp[i][0]\)赋初值,\(dp[i][1]=dp[i-1][0]+s[i]-s[j]\)
  2. 如果队首元素与当前元素距离超过k则出队(排除过时决策)。
  3. 用队首元素更新\(dp[i][1]\)
  4. 更新队尾。我们在一次计算中设原本的外层循环i为常量,j为变量,用单调队列维护j的值。由于这个dp方程中i与j不相关,我们就维护一个单调递减的队列,保存\(dp[j][0]-s[j]\)
  5. 将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有正有负的时候,不能用单调队列,要用二分来查找最小值。还有的题是维护上凸壳。

posted @ 2022-09-03 11:39  gtm1514  阅读(25)  评论(0编辑  收藏  举报