浅谈单调队列优化dp

什么是单调队列?


举个浅显的例子:

滑动窗口:给出n个数,要求从每个位置开始到其后的k位中的最大值。


显然这题可以用RMQ问题的做法解,但用单调队列效率最高。

我们维护一个队列,这个队列有两个参数,一个是位置编号,一个是其值,这个队列具有“编号单调增,值单调减”的特点,因为如果i>j,且a[i]>a[j],显然只要i和j在一个窗口中,j就不会是最大值,我们可以把j删掉(对于冗余状态的清除就是优化).然后在队首取出最大值,若最大值已经不在当前窗口,则删去,取下一个。因为每个值最多进队一次,出队一次,所以效率达到O(n)下界。


那么则么用单调队列优化DP呢?


对于一些转移方程可以写成DP(i)=F(j)+a[i],F(j)是一些只于j有关的数,那么我们可以很直接的用单调队列,但如果是下面这道题呢?


给出n个数,要求分成m组,使得每一组的最大值减去最小值的平方之和最小。


分析:

首先对a进行排序,很直接的有dp[i][j]=min{dp[i-1][k]+(a[j]-a[k+1])^2},其中i表示当前分成i组,j表示当前在第j个数。

我们无法直接优化,那么我们分析一下什么条件下的k是最优的,假设另一个选择l,

因为k是最优的,所以:

dp[k]+(a[j]-a[k+1])^2-[dp[l]+(a[j]-a[l+1])^2]<0

dp[k]+a[k+1]^2-2*a[j]*a[k+1]-(dp[l]+a[l+1]^2-2*a[j]*a[l+1])<0

dp[k]+a[k+1]^2-(dp[l]+a[l+1]^2)<2*a[j]*(a[k+1]-a[l+1])

为了简化式子,令yk=dp[k]+a[k+1]^2,xk=2*a[k+1],同理有yl和xl

最终有(yk-yl)/(xk-xl)<a[j]

看看左式想到什么?没错,斜率。
那么令g[k,l]=(yk-yl)/(xk-xl)
若g[k,l]<a[j],说明k是比l更优的选择,我们可以排除l的冗余选择。
对应的,若有k<j<i,且g[i,j]<g[j,k],则j同样是冗余选择。
因为若g[i,j]<a[i],则说明i是比j更优的选择,
若g[j,k]>g[i,j]>a[i],则说明j不如k优,总之j是冗余选择。

那么右式的a[j]是已知的,而且a[j]是随j的增大而增大的,故g应满足:



即斜率单调增,整个图像呈下凸形式,所以我们就可以有单调队列的优化。


总结解题步骤:


1.删去队首已经不满足g[head,head+1]<a[i]的head

2.用q[head]更新dp[i][j]

3.删去队尾已经不满足g[i,tail]<g[tail-1,tail]的tail

4.将dp[i][j]加到队尾


参考程序:

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=5100;
int f[maxn][maxn];
int a[maxn];
int n,m,head,tail;
int q[6*maxn];
int yy(int i,int j,int k){
	return f[i-1][j]+a[j+1]*a[j+1]-(f[i-1][k]+a[k+1]*a[k+1]);
}
int xx(int j,int k){
	return 2*(a[j+1]-a[k+1]);
}
int main(){
	int T;
	scanf("%d",&T);
	int cnt=0;
	while (T--){
		scanf("%d%d",&n,&m);
		for (int i=1;i<=n;i++){
			scanf("%d",&a[i]);
		}
		sort(a+1,a+n+1);
		for (int i=1;i<=n;i++){
			f[1][i]=(a[i]-a[1])*(a[i]-a[1]);
		}
		for (int i=2;i<=m;i++){
			head=tail=0;
			q[tail++]=i-1;
		    for (int j=i;j<=n;j++){
			    while (head+1<tail && yy(i,q[head+1],q[head])<a[j]*xx(q[head+1],q[head]))
					head++;
				int k=q[head];
			    f[i][j]=f[i-1][k]+(a[j]-a[k+1])*(a[j]-a[k+1]);
			    while (head+1<tail && yy(i,j,q[tail-1])*xx(q[tail-1],q[tail-2])<=xx(j,q[tail-1])*yy(i,q[tail-1],q[tail-2]))
					tail--;
			    q[tail++]=j;
		    }
	    }
		printf("Case %d: %d\n",++cnt,f[m][n]);
	}
	return 0;
}

通过这一道题,我们可以发现一些寻找隐藏单调的方法:

1.根据题意,列出状态转移方程

2.先尽力优化

3.若仍无法通过,考虑满足什么条件的选择是较优的,将条件整理(将已知的写在一边,未知的写在另一边)并简化(换元)一下

4.分析简化后的条件,挖掘出单调性。


当然此题也可以

dp[i]= Min (dp[j] -2*s[i]*s[j] +s[j]^2) + M + s[i]^2
现在我们单独考虑Min()里面的内容。
如果我们设 k=2*s[i],x=s[j],y=dp[j]+s[j]^2的话(注意将和i有关的量以及和j有关的量分别结合到一起)
再令G=dp[i],那么将得到:
G=-k*x+y
移项得:
y=k*x+G。
然后用凸包做。

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值