$Dynamic Planning Optimization$ 关于动态规划的优化方案(%$color{red}{rqy}$)
Posted yeasio-nein
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了$Dynamic Planning Optimization$ 关于动态规划的优化方案(%$color{red}{rqy}$)相关的知识,希望对你有一定的参考价值。
关于动态规划的优化方案(%(color{red}{rqy}))
1.单调队列
单调队列是一种具有单调性的队列,其中的元素全部按照递增或者递减的顺序排列,就比如下面这个递减队列。
假如说我们要在队尾加入一个(5),那么我们入队的步骤就是这样的:
发现队尾(1),(q[tail]),(1<5),则将1退出(tail--)
发现队尾(2),(q[tail]),(2<5),则将2退出(tail--)
发现队尾(3),(q[tail]),(3<5),则将3退出(tail--)
发现队尾(8),(q[tail]),(8>5),停止退出队尾,将(5)入队。
经过上述步骤之后队列变为了{8,5},依然满足递减的单调性,而实际上这也就是单调队列的基本操作。而维护递增的方式也是一样的。
#define MAXN 100010
int n,a[MAXN];
int q[MAXN],head=1,tail=1;
for(int i=1;i<=n;i++){
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);//输入
q[1]=a[1];//将第一个元素入队
for(int i=2;i<=n;i++){
while(head<=tail&&q[tail]<a[i])
//如果队列不为空并且队尾元素小于a[i]
tail--;//弹出队尾元素
q[++tail]=a[i];//入队
}
}
【例题1】
我们现在有一个整数序列(A(a[MAXN])),长度为(n),又知两个参数(k)和(m),要求:从(A)序列中找出(k)个不相交的区间,每段区间长度(len)<=(m),要求所有k个区间的区间和最大。
考虑最基本的(DP),设(dp[i][j])表示从前(j)个数里面选出来(i)个长度不超过m的不相交区间的区间和最大值,然后我们再枚举一个(k),指选择([k+1,j])这个子区间。然后我们创造一个前缀和数组(sum[MAXN]),那么([k+1,j])这个区间的区间和就是(sum[j]-sum[k])。子问题分为两块:(j)选入子区间,或者(j)不选入子区间,从(j-m)到(j)范围内枚举一个(k)使得(dp[i-][k]+sum[j]-sum[k])最大,然后与(dp[i][j-1])取一个(max)可得答案。
for(int i=1;i<=n;i++)
sum[i]=sum[i-1]+a[i];//前缀和数组
for(int i=1;i<=k;i++)
for(int j=1;j<=n;j++){
int ans=-INF;
for(int k=j-m;k<=j;k++){
ans=max(ans,dp[i-1][k]+sum[j]-sum[j]);
}
dp[i][j]=max(ans,dp[i][j-1]);
}
这样的(DP)时间复杂度为(O(nmk)),显然太大,于是我们考虑优化。
我们可以看到(DP)的原式子是(color{red}{dp[i][j]=max(dp[i][j-1],max(f[i-1][k]+sum[j]-sum[k]));})
我们发现在里面的(k)的最优化枚举当中,sum[j]是不随k的枚举变化的,所以我们可以将sum[j]提出来变成:(color{red}{dp[i][j]=max(dp[i][j-1],sum[j]+max(f[i-1][k]-sum[k]));})
可以知道在整个式子里面最耗时间的就是最后关于(dp[i-1][k]-sum[k])最大值的枚举,所以只要快速计算出来了(dp[i-1][k]-sum[k])就可以快速计算整个式子。我们来看(dp[i-1][k]-sum[k])的范围是在([0][0],[0][1],....[0][m-1],[1][m],[2][m+1],...,[n-m][n-1])这些区间上的最大值,也就是所有的([j][i+j-1])的区间。
我们发现这些区间的左右端点都是单调递增的,所以我们可以利用单调队列在(O(1))的时间内解决这些区间。然后我们就将时间优化到了(O(nk))。
【例题2】([NOI2005])瑰丽华尔兹(link)
一个(n×m)的矩形网格。你初始站在((x,y))这。有些格子有障碍而有些没有。有(K)个时间段。第(i)个时间段从(s[i])持续到(t[i])(包括两端)这段时间内网格会向某个方向(上下左右之一)倾斜。所以每个时间段内的每个时间单位,你可以选择在原地不动,或者向倾斜的方向走一格(当然你不能走到障碍上或是走出网格)。
求你最多能走多少格。
如上图所示,黑色方块为障碍,(S)为起始点。
按照最常的(DP)思路来看,我们设(dp[k][i][j])为在k时间点,从((x,y))节点走到了((i,j))节点的时候最长走了多长。初始化(dp[0][i][j])全部为(?∞),而(dp[0][x1][y1]0)=(0)((x1,y1)为初始位置),考虑子问题就是:从那边来?(k)时刻是从那个方向来还是不动?我们以第(k)时刻向右倾斜为例。
如果是向右倾斜,那么上一层状态就是在((i,j-1))地点,那么结合两个子问题我们可以得出(DP)方程式:(dp[k][i][j]=max(dp[k-1][i][j],dp[k-1][i][j-1]+1);)
for(int k=1;k<=len;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
dp[k][i][j]=max(dp[k-1][i][j],dp[k-1][i][j-1]+1);
那么这样的时间复杂度就是(O(nmsum_{i=1}^{K}(t[i]-s[i]+1))),是无法通过这个题的全部数据的。然后我们紧接着考虑怎么优化。关于位置的(n^2)枚举我们没有什么办法,但是关于(K)我们可以进行优化,时间点很多有(sum_1^K(t[i]-s[i]+1))个,但是时间段(K)却<=(200),那么我们可以将一段时间的转移全部合并起来一起算,那么就快得多了。
我们设(dp[k][i][j])为在第(k)个时间段末尾,从((x,y))走到了((i,j))点,(len[k])为第(k)个时间段的持续时间,可以算出是(t[k]-s[k]+1)。
首先还是(n^2)的枚举,和(k)时间段的枚举,之后我们还有一个(l)的枚举,这个(l)枚举的是上一个状态加上在当前这个(k)的时间段内一共走的步数对应倾斜方向的横、竖坐标,如果我们继续以右倾为例,那么(j-len[j]<=l<=j),就是从完全不动到走了最多的(len[k])步,那么我们有了状态转移方程式:(color{red}{dp[k][i][j]=max_{j-len[k]<=l<=j}(dp[k-1][i][l]+j-l)}),由于其中的+(j)与l的枚举并无关联,所以提出来就变成了(color{red}{dp[k][i][j]=max_{j-len[k]<=l<=j}(dp[k-1][i][l]-l)+j})。其实也就是枚举这个时间段之前这个人的位置在哪,也就知道了当前的(dp[k][i][j])是从哪里转移过来的。
之后,我们回过头来看上一道题的最后的(DP)方程式:(color{red}{dp[i][j]=max(dp[i][j-1],sum[j]+max(f[i-1][k]-sum[k]));})
是不是发现格式非常的相似呢?,我们固定住(i)之后的状态转移方程式基本是和上题一样的,所以一样可以使用单调队列优化到(O(nmK))。
下面针对一组样例,我们进行一遍手动模拟,以帮助更好的理解。
就用洛谷的样例吧。(第一行分别为n,m,x1,y1,k)
4 5 4 1 3
. . xx.
. . . . .
. . . x.
. . . . .
1 3 4
4 5 1
6 7 3
那么画完图之后就是这个样子:
从(1)~(3)时刻的倾斜方向是右,那么纵坐标是你不变的,我们枚举纵坐标。
for(int i=1;i<=k;i++){
int s; int t; int dir;
scanf("%d%d%d",&s,&t,&dir);
//注意要反着DP,也就是倒退
int len=t-s+1;
if(dir==1) //北面(上)
for(int j=1;j<=m;j++)//北面的话横坐标不变,那么我们枚举纵坐标
DP(i,n,j,dir,len);
if(dir==2) //南面(下)
for(int j=1;j<=m;j++)//南面的话横坐标不变,那么我们枚举纵坐标
DP(i,1,j,dir,len);
if(dir==3) //西面(左)
for(int j=1;j<=n;j++)//西面的话纵坐标不变,那么我们枚举横坐标
DP(i,j,m,dir,len);
if(dir==4) //东面(右)
for(int j=1;j<=n;j++)//东面的话纵坐标不变,那么我们枚举横坐标
DP(i,j,1,dir,len);
}
然后当我们的横坐标x枚举到1的时候,我们在DP函数里面定义一个now,然后是(while(x>=1)&&(x<=n)&&(y>=1)&&(y<=m)),因为首先要保证不超过边界。然后如果我们发现右面是可以走的,那么我们就进行一个push操作。也就是关于dp[p-1][x][y]在单调队列里面的入队操作。在最前面我们已经介绍了。
void push(int now,int value){
if(value==-INF) return ;
//如果压根做不到这里,那么直接返回
while(head<=tail&&value-now>=q[tail])
tail--;//弹出队尾
q[++tail]=value-now;
pos[tail]=now;
//pos记录位置,用来判断是不是可以滑
}
而至于为什么要在(while)里面减去一个(now),是因为(x,y)这个位置不一定是在当前方向的起点上,因为之后某一步的步数减去当前的步数得到的值就是(x,y)到那一步在的点的距离,相当于一个化简~
由于(dp[0][i][j])=-(INF),当前的(p)=(1)所以(p)-(1)的时候(value)就是-(INF),所以在第0个时间段到不了这个地方,我们直接返回。然后下面其实就没什么事了,所有的push全部直接返回,最后退出DP函数。就这样进行到(x)(即(j))=(3)的时候,我们发现(map[3][4])是一个障碍点,那么也就是说我们之前进行的所有工作全部无效,然后我们将整个队列清空,即(head)=(1,tail)=(0);
然后接着进行到(x)=(4),(y)=(1)((4)行(1)列)的时候,我们到了起始点,而起始点的dp[0][4][1]是0,所以(value)!=-(INF),我们终于将一个值(value)-(now)=-1入队了,那么我们当前的队列是这个样子的:
加上步数之后我们发现(dp[p][x][y]=q[head]+now)依然是(0),所以(ans)没有被更新(废话,你从起点走到起点需要更新(ans)嘛),所以我们继续向下进行,因为每次(now)都会++,所以下面的(dp[p][x][y])加上(now)之后就可以更新(ans)的值了。然后进行到(x)=(4),(y)=(5)的时候,我们发现(now-pos[head]=4),大于可以(len),也就是说超过了可以滑动的区间。(一共就三秒你怎么滑第四块啊~)那么我们将队首弹出,接下来我们就不能再更新ans的最大值了,(x)=(4)时完美结束。这个时候我们的行走路径大概如下:
(蓝色方块为当前方块,黄色方块为路径)
也就是说从(1)~(3s)我们最多可以走3块。(真是麻烦啊~)
(i)继续走,我们进行到下一个时间段。(4)~(5s)的时候是向北倾斜的。那么我们进行(DP(i,n,j,dir,len)),我们从(n)行(j)列开始(DP),第一次将(tail)弹出后又入队我们不管,因为(j=1)、(2)的时候都不能更新(ans),然后到了(j=3)的时候,我们将(dp[1][4][3]-now=1)入队了。
然后当(now)进行到第三次的时候我们就可以更改ans值为4了。
之后结束了第二个时间段。此时的路径大概是这样的:
最后在第三个时间段内,我们将路径更改为如下:
那么以上就是整个样例的模拟,最终我们得到(ans)数为6.
关于单调队列优化的一点总结
鉴于两者之间的(DP)转移方程的相似性,我们成功的利用单调队列优化了问题,那么回过头来看看,什么样的问题可以利用单调队列进行优化呢?我们最上面讲的单调队列是具有单调性的一种数据结构,他可以保证数据的单调性,自然也就可以留下数据的最大值或者最小值,利用了单调性,就是减少了一位枚举,减去一维,直接获得单调队列里面的最优解。并且DP可以使用单调队列优化,当且仅当(DP)式的格式基本满足(color{red}{dp[i]=a[i]+max_{l[i]<=j<=r[i]}b[j]})的时候。即“(dp[i])=(A(i))+(B(j))中的最小/大值 ((i-k<=j<i,k)为常数())”,当你发现要求(max)而且求可能拓展的状态有线性关系的时候,你就可以考虑单调队列优化了。
以上是关于$Dynamic Planning Optimization$ 关于动态规划的优化方案(%$color{red}{rqy}$)的主要内容,如果未能解决你的问题,请参考以下文章