码迷,mamicode.com
首页 > 其他好文 > 详细

$Dynamic Planning Optimization$ 关于动态规划的优化方案(%$\color{red}{rqy}$)

时间:2018-08-04 18:58:16      阅读:206      评论:0      收藏:0      [点我收藏+]

标签:三次   http   路径   ==   www.   cti   表示   数组   多少   

关于动态规划的优化方案(%\(\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)\)

一个\(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(nm\sum_{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}$)

标签:三次   http   路径   ==   www.   cti   表示   数组   多少   

原文地址:https://www.cnblogs.com/Yeasio-Nein/p/DP-Dev.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!