【专辑】单调队列+斜率优化的DP

【专辑】单调队列+斜率优化的DP这里有个不错的总结,我只是做个补充。

单调队列

  • 两个单调
    • 一是队列中的数据,其相对位置与原数列中的是相同的,即位置的单调性
    • 一是队列中数据大小的单调性
这样就使得每次最大的元素都在队首,实现O(1)的查找,由于每个元素出队入队仅一次,所以平坦复杂度是O(1),所以总的复杂度就是O(n)。

这样就能解决很多其他数据结构要O(nlogn)及以上的复杂度才能解决的问题。而且实现起来也很简单。简单即是一种美。。。

具体的讲解看上面那个blog里的ppt(这个这个)和百度百科里的讲解。

以下是对那个blog里题目的解题报告。。

单纯的单调队列:
志愿者选拔 O(n)
最最入门的单调队列,而且是很形象的排队问题
新建一个空队列,队列里的每个元素有两个数据,一个是第几个进队的,一个是其身高。
对于C操作,从队尾逐个比较其与新加入人的身高,若队尾的那个元素的身高低于新人的身高,则让其出队,一直这样做直到队尾的元素身高大于此人身高,或者队列为空。然后将此人添加到队尾。
对于Q操作,直接将队首元素的身高输出。
对于G操作,比较队首元素的入队次序与当前轮到第几个人出队,判断其是否要出队。


代码如下

#include <iostream>
#include <stdio .h>
#include <string .h>
#define N 1001000
using namespace std;
struct data
{
    int pos,value;          //pos第几个进队的,value身高
}q[N];
int main()
{
    int t,head,tail,i,j,a;
    char st[10];
    scanf("%d",&t);
    while( t-- )
    {
        scanf("%s",st);
        head = 0 ;
        tail = -1;                //tail<head 代表队列为空
        i = j = 0;               //i为进队的总人数,即下一个进队的人的pos,j为出队的人数,即当前该出队的人的pos
        while(scanf("%s",st),strcmp(st,"END"))
        {
            if(st[0] == 'C')
            {
                scanf("%s %d",st,&a);
                while(head <= tail && q[ tail ].value < a) tail --;           //踢掉队尾的元素来保证value的单调性
                q[ ++tail ].value = a;                //将新人加入,更新value和pos
                q[ tail ].pos = i++;
            }
            else
            if(st[ 0 ] == 'Q')
            {
                if( head > tail ) puts("-1");   //队列为空则输出-1
                else printf("%d\n",q[ head ].value);  //否则输出队首的元素
            }
            else
            {
                j++;
                while(head < = tail && q[head].pos < j)head++;   //将队首该出队的出队,其实这里写成if就行
            }
        }
    }
    return 0;
}


Sliding Window O(n)

同上题

查询一系列区间里最大元素的值,这些区间长度相同,左值从小到大。
新建一个空队列,每个元素两个值,位置 和 大小。
先把第一个区间里的数入队,入队规则:比较队尾元素与要入队元素的大小,小则将队尾元素出队,一直重复这个过程直到队为空,或者队尾元素大于当前要入队元素,把此元素插入队尾。
对于后面的区间,先看队首元素是否已经出了当前区间,然后插入新元素。
每次查询输出队首元素。
代码如下:

#include <iostream>
#include <stdio .h>
#define N 1000100
using namespace std;
struct data
{
    int value,pos;
}q[ N ];
int a[ N ];
int main()
{
    int n,m,head,tail,i;
    while(scanf("%d %d",&n,&m)==2)
    {
        head = 0;
        tail = -1;
        for( i = 0 ; i < n ; i ++ )
        {
            scanf("%d",a+i);
        }
//求每个区间最小值
        for( i = 0 ; i < m - 1 ; i ++)
        {
            while( head <= tail && q[ tail ].value > a[ i ] ) tail--;
            q[ ++tail ].value = a[ i ];
            q[ tail ].pos = i;
        }
        for( ; i < n ; i ++ )
        {
            while( head <= tail && q[ tail ].value > a[ i ] ) tail--;
            q[ ++tail ].value = a[ i ];
            q[ tail ].pos = i;
            if( q[ head ].pos < = i - m ) head++;
            printf("%d",q[ head ].value);
            if( i == n-1)putchar('\n');
            else putchar(' ');
        }
//求每个区间最大值
        head = 0;
        tail = -1;
        for( i = 0 ; i < m - 1 ; i ++)
        {
            while( head <= tail && q[ tail ].value < a[ i ] ) tail--;
            q[ ++tail ].value = a[ i ];
            q[ tail ].pos = i;
        }
        for( ; i < n ; i ++ )
        {
            while( head <= tail && q[ tail ].value < a[ i ] ) tail--;
            q[ ++tail ].value = a[ i ];
            q[ tail ].pos = i;
            if( q[ head ].pos <= i - m ) head++;
            printf("%d",q[ head ].value);
            if( i == n-1)putchar('\n');
            else putchar(' ');
        }
    }
    return 0;
}


Max Sum of Max-K-sub-sequenceO(n)

差不多同上题,不过就是先求[1,i]的和,然后循环的,延迟一倍处理一下 
先写出dp方程
dp[ i ] = max( sum[ i ] - sum[ j ] ) = sum[ i ] - min( sum[ j ] );
sum[ i ]表示前i个元素的和。
由dp方程可以看出队列要存sum的值,有小到大排列。
出队则判断长度是否大于k了。

#include <iostream>
#include <stdio .h>
#define N 200010
using namespace std;
struct data
{
    int pos,value;
}q[ N ];
int a[N];
int sum[N];
int main()
{
    int t,i,n,k,head,tail,ans,ti,tj;
    scanf("%d",&t);
    while( t--)
    {
        scanf("%d %d",&n ,&k);
        for( i = 1 ; i < = n ; i ++ )
        {
            scanf("%d",a+i);
            a[ i + n ] = a[ i ];
        }
        n <<= 1;
        sum[ 0 ] = 0;
        for( i = 1 ; i <= n ; i ++ )
            sum[ i ] = a[ i ] + sum[ i - 1 ];
        head = tail = 0;
        q[ 0 ].pos = 0;
        q[ 0 ].value = 0;
        ans = a[ 1 ] - 1;
        for( i = 1 ; i <= n ; i ++ )
        {
            if( q[ head ].pos + k < i ) head++;
            if( sum[ i ] - q[ head ].value > ans )
            {
                ans = sum[ i ] - q[ head ].value;
                ti = q[ head ].pos;
                tj = i - 1;
            }
            while( head < = tail && q[ tail ].value >= sum[ i ])tail--;
            q[ ++tail ].pos = i;
            q[ tail ].value = sum[ i ];
        }
        n >>= 1;
        printf("%d %d %d\n",ans,ti % n + 1,tj % n + 1);
    }
    return 0;
}


单调队列优化的DP:
Trade O(n)
本来是O(MaxP *MaxP *T*T)的,T的那一维比较好优化,要取W前天最好的,那么每次都记录前W天最好的,而MaxP那一维的话我是买入做一次单调队列,卖出再做一次.最后复杂度O(Maxp*T)

这个就有点难度了。
还是要先写出dp方程
dp[ i ][ j ] = max( dp[ k ][ j ] , dp[ k ][ j - p ] - ap[ i ] * p , dp[ k ][ j + q ] + bp[ i ] * q );
表示第i天有j个stock拥有的钱的最大值(可能是负值,代表欠着钱)0 < k < i - m , 0 <= p <= as[ i ] , 0 <= q <= bs[ i ]
可见时间复杂度为O(Maxp*MaxP*T*T);


然后我们来降低复杂度:
先减小T那一维复杂度
dp[ i ][ j ]也就是前i天中,有j个stock拥有钱的最大值。
故求dp[ i ][ j ] 与 dp[ i - 1 ][ j ] 只有一天之差,即求完第i - 1天后,已经找完了最大的那个dp[ k ][ j ],只需比较i - m - 1那天的j,即选max(dp[ k ][ j ] , dp[ i - m - 1 ][ j ]),不妨用一个数组g来表示前几天的值,g[ i ]表示有j个stock拥有的钱的最大值。对于每次i,只需更新一遍g即可,而不用k的那重循环来找前k天的最大值。这样复杂度减小了一个T


接着利用单调队列降低p那层复杂度
将求dp[ i ][ j ]分为三个步骤:
dp[ i ][ j ] = max( dp[ k ][ j ] ,dp[ i ][ j ]);
dp[ i ][ j ] = max( dp[ k ][ j - p ] - ap[ i ] * p , dp[ i ][ j ]);
dp[ i ][ j ] = max( dp[ k ][ j + q] +bp[ i ] * q , dp[ i ][ j ]);
求第三个过程和第二个类似,下面重点说第二个过程。
求dp[ i ][ j ]与求dp[ i ][ j - 1 ]有很多重合的部分:
<ul>
<li>求dp[ i ][ j - 1 ]时p的范围是 [ j - 1 - as[ i ] , j - 1 ]</li>
<li>求dp[ i ][ j - 1 ]时p的范围是 [ j - as[ i ] , j ]</li>


此时,去掉i那一维,现在这个样子就和上面的题目差不多了吧,查询区间的最大值,区间的性质也跟上一题相同,但你这么就开始做了就错了(我就这么错的)。这里有点不同啊,就是ap[ i ] 后多乘了一个p,而p是可变的,怎样去维护那个单调队列呢?
先不说怎么去维护,此时想到用单调队列就已经将p那一维优化掉了。
现在说怎么维护队列,比较队列中元素与要新加入元素的关系,他们对于dp[ i ][ j ]的维护就差在那个p上,而两个p值之差就是j值之差,即:
g[ j1 ] - ap[ i ] * p1   j1 + p1 = j 和
g[ j2 ] - ap[ i ] * p2   j2 + p2 = j
故入队时,不能只比较g[ j ]的大小,还要算上j的差值*ap[ i ]。。。
while( head < = tail && q[ tail ].value <= g[ j ] + p[ i ].ap * ( j - q[ tail ].pos ) ) tail--;


这样复杂度就降低到maxP*T了,可见这个优化是很NB的。。。。
代码如下:

#include <iostream>
#include <stdio .h>
#include <string .h>
#define N 2010
using namespace std;
struct data
{
    int ap, bp, as, bs;
    void input()
    {
        scanf("%d %d %d %d", &ap, &bp, &as, &bs);
    }
}p[ N ];
struct da
{
    int pos,value;
}q[ N ];
int f[ N ][ N ];
int g[ N ];
int main()
{
    int t,n,maxP,w,i,j,head,tail,ans;
    //freopen("data.txt","r",stdin);
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d %d %d", &n, &maxP, &w);
        for( i = 0 ; i < n ; i ++ )
        {
            p[ i ].input();
        }
        ans = 0;
        memset(f, 131 , sizeof( f ));
        memset(g, 131 , sizeof( g ));
        g[ 0 ] = 0;
        for( i = 0 ; i <= w ; i ++ )
        {
            for( j = 0 ; j <= maxP && j <= p[ i ].as ; j ++ )
                f[ i ][ j ] = - p[ i ].ap * j;
        }
        tail = 0;
        for( ; i < n ; i ++ )
        {
            for( j = 0 ; j <= maxP ; j ++ )
                if( g[ j ] < f[ i - w - 1 ][ j ] )
                    g[ j ] = f[ i - w - 1 ][ j ];
            head = 0;
            tail = -1;
            for( j = 0 ; j <= maxP ; j ++ )
            {
                if( head <= tail && q[ head ].pos + p[ i ].as < j ) head++;
                while( head <= tail && q[ tail ].value <= g[ j ] + p[ i ].ap * ( j - q[ tail ].pos ) ) tail--;
                q[ ++tail ].pos = j;
                q[ tail ].value = g[ j ];
                if( f[ i ][ j ] + p[ i ].ap * ( j - q[ head ].pos) < q[ head ].value )
                    f[ i ][ j ] = q[ head ].value - p[ i ].ap * ( j - q[ head ].pos);
            }
            head = 0;
            tail = -1;
            for( j = maxP ; j >= 0 ; j -- )
            {
                if( head < = tail && q[ head ].pos - p[ i ].bs > j ) head++;
                while( head < = tail && q[ tail ].value <= g[ j ] - p[ i ].bp * ( q[ tail ].pos - j ) ) tail--;
                q[ ++tail ].pos = j;
                q[ tail ].value = g[ j ];
                f[ i ][ j ] = max( f[ i ][ j ] , q[ head ].value + p[ i ].bp * ( q[ head ].pos - j ));
            }
            ans = max( ans, f[ i ][ 0 ] );
        }
        printf("%d\n", ans);
    }
    return 0;
}

未完待续……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值