单调队列
- 两个单调
- 一是队列中的数据,其相对位置与原数列中的是相同的,即位置的单调性
- 一是队列中数据大小的单调性
这样就能解决很多其他数据结构要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;
}
未完待续……