单调队列即队列内元素单调递增或递减,删除数据可以在队头或者队尾,加入元素只能在队尾加入。
  由于单调队列的队头一定是最小值,故查询为O(1);每个元素最多进队一次,出队一次,摊排分析下来仍然是O(1).

 

例1. 广告印刷
【问题描述】
最近,afy决定给TOJ印刷广告,广告牌是刷在城市的建筑物上的,城市里有紧靠着的N(N<=400000)个建筑。afy决定在上面找一块尽可能大的矩形放置广告牌。我们假设每个建筑物都有一个高度,从左到右给出每个建筑物的高度H1,H2…HN,0<Hi<=1,000,000,000,并且我们假设每个建筑物的宽度均为1。要求输出广告牌的最大面积。
【输入样例】
6
5 8 4 4 8 4
【输出样例】
24

【分析】
    这道题目是要求每个建筑物向左向右能扩展到的最大宽度,即左右两边比它高的连续的宽度。显然暴力枚举O(n^2)的复杂度是不可行的。
    考虑构造一个单调非递减队列,从左至右,依次加入到队列中,肯定会有元素出队列,设当前要插入的数为a,要出队列的数为b,必有b>=a,则b向右能到达的最远距离就是b-a。注意在求解时,让0先入队列,这样保证每个数据都会出队列。同理,左极限也可求出。


例2. POJ2823
【题意】
移动区间(长度固定)最值问题。

【分析】
    这类思想在单调队列优化思想中尤其重要:区间长度为k,求区间内的最大值,考虑第i个数和第j个数,j-i<k,若a[i]<a[j],那么a[i]将毫无用处。直觉上理解,因为窗口的移动,a[i]要比a[j]先移出去,无论如何,区间的最大值都不可能是a[i]。
    这样,考虑构造一个单调递增的队列,存放相应的序号,当a[队尾]>=要入队数据a[i],删除队尾元素;当队头<=i-k时,删除队头元素。

 

Cpp代码  收藏代码
  1. /** 
  2. 单调队列: 
  3. 加入找最小数,考虑顺序a,b(b在a的后面),若b<a,当b入队列后,a不可能称为最小值(a比b先出),删去。 
  4. 每个元素出队列和入队列一次,时间复杂度为O(n) 
  5. */  
  6. #include <iostream>  
  7. #include <cstdio>  
  8. #include <cstring>  
  9. using namespace std;  
  10. const int N = 1100000;  
  11. int n,k;  
  12. int a[N];  
  13. int DanDiao_Que[N];     //单调递减队列(最大),单调递增队列(最小)  
  14. int head,tail;  
  15.   
  16. //递增  
  17. void Min()  
  18. {  
  19.     int i;  
  20.     int head = 1;  
  21.     int tail = 0;  
  22.     for(i = 0; i < k-1; i++)  
  23.     {  
  24.         while(head<=tail && a[DanDiao_Que[tail]]>=a[i]) tail--;  
  25.         tail++;  
  26.         DanDiao_Que[tail] = i;  
  27.     }  
  28.     for(i = k-1; i < n; i++)  
  29.     {  
  30.         while(head<=tail && a[DanDiao_Que[tail]]>=a[i]) tail--;  
  31.         tail++;  
  32.         DanDiao_Que[tail] = i;  
  33.         while(DanDiao_Que[head]< i-k+1) head++;  
  34.         printf("%d",a[DanDiao_Que[head]]);  
  35.         printf("%c",i==n-1?'\n':' ');  
  36.     }  
  37. }  
  38.   
  39. //递减  
  40. void Max()  
  41. {  
  42.     int i;  
  43.     int head = 1;  
  44.     int tail = 0;  
  45.     for(i = 0; i < k-1; i++)  
  46.     {  
  47.         while(head<=tail && a[DanDiao_Que[tail]]<=a[i]) tail--;  
  48.         tail++;  
  49.         DanDiao_Que[tail] = i;  
  50.     }  
  51.     for(i = k-1; i < n; i++)  
  52.     {  
  53.         while(head<=tail && a[DanDiao_Que[tail]]<=a[i]) tail--;  
  54.         tail++;  
  55.         DanDiao_Que[tail] = i;  
  56.         while(DanDiao_Que[head]< i-k+1) head++;  
  57.         printf("%d",a[DanDiao_Que[head]]);  
  58.         printf("%c",i==n-1?'\n':' ');  
  59.     }  
  60. }  
  61.   
  62. int main()  
  63. {  
  64.     scanf("%d %d",&n,&k);  
  65.     for(int i = 0 ;i < n; i++)  
  66.         scanf("%d",&a[i]);  
  67.     Min();  
  68.     Max();  
  69.     return 0;  
  70. }  

 
例3. HDU3415
【题意】
求长度不超过k的连续最大子段和。

【分析】
    设dp[i]表示以i结尾的满足约束的最大子段和,sum[i]表示从0到第i个数的和,状态转移方程:dp[i]=sum[i]-min{sum[j]},i-k<=j<i,最后结果为max(dp[i]).
   直接二重循环超时。设b[i]=i-k,b[i]随i的增长单调不降,可以考虑用单调队列优化。构造一个单调递减队列,维护操作类似例2.

 

Cpp代码  收藏代码
  1. /** 
  2. 题意:有n(<=200000)个数排成一列,求长度不超过k(1<=k<=n)的连续的子串的和的最大值. 
  3. 设dp[i]表示以a[i]为结尾的连续子串和的最大值,状态转移方程: 
  4. dp[i] = sum[i] - min{sum[j-1]}, max(0,i-k+1)<=j<i. dp[0]=0 最后结果为max{dp[i],1<=i<=n} 
  5.  
  6. 分析: 
  7. 单纯二重循环O(n*k)肯定超时. 
  8. 考虑求sum[j-1]的最小值 max(0,i-k)<=j<i,是否可以优化? 
  9. 1.显然优先级队列可以适用,维护堆,时间复杂度优化为nlogk 
  10. 2.考虑求解的单调性,若i<j且sum[i]>sum[j],则i可以被舍弃.只要维护一个递增的单调队列即可! 
  11. */  
  12. #include <iostream>  
  13. #include <cstdio>  
  14. #include <cstring>  
  15. using namespace std;  
  16. const int INF = 0x7fffffff;  
  17. const int N = 200005;  
  18. int n,k;  
  19. int a[N];  
  20. int sum[N];  
  21. int que[N];  
  22. int result,st,end;  
  23.   
  24. void solve()  
  25. {  
  26.     int head=1,tail=0;  
  27.     result = -INF;  
  28.     st = INF;  
  29.     for(int i = 1; i <= n+k; i++)  
  30.     {  
  31.         while(head<=tail&&sum[i-1]<sum[que[tail]])  
  32.             tail--;  
  33.         while(head<=tail&&que[head]<i-k)  
  34.             head++;  
  35.         tail++;  
  36.         que[tail] = i-1;  
  37.         //output start  
  38.         if(sum[i]-sum[que[head]]>result)  
  39.         {  
  40.             result = sum[i]-sum[que[head]];  
  41.             st = que[head]+1;  
  42.             end = i;  
  43.         }  
  44.         //output end  
  45.     }  
  46.     if(end>n)  
  47.         end -= n;  
  48. }  
  49.   
  50. int main()  
  51. {  
  52.     int i;  
  53.     int t;  
  54.     scanf("%d",&t);  
  55.     while(t--)  
  56.     {  
  57.         sum[0] = 0;  
  58.         scanf("%d %d",&n,&k);  
  59.         for(i = 1; i <= n; i++)  
  60.         {  
  61.             scanf("%d",&a[i]);  
  62.             sum[i] = sum[i-1] + a[i];  
  63.         }  
  64.         for(i = n+1; i <= n+k; i++)  
  65.             sum[i] = sum[i-1] + a[i-n];  
  66.         solve();  
  67.         printf("%d %d %d\n",result,st,end);  
  68.     }  
  69.     return 0;  
  70. }  

 

 

 

 

例4. HDU3474
【题意】
由n(10^6)个数据组成的圆环,数据为1和-1,问从一个点开始顺时针或逆时针,能遍历完所有点,并且保证中间过程中sum>=0。
【分析】
    首先O(n^2)可定不可行。假设从i点开始,这里仅考虑向左,必须保证sum(j,i)>=0,i-n<j<=i.设sum[i]为从开始到i的和,即保证sum[i]-sum[j]>=0,i-n<=j<i,即只要保证sum[i]-max{sum[j]}>=0即可。求移动区间(固定长度)最值问题,就可以联想到单调队列了。维护操作略。注意顺时针和逆时针计数时判重。

 

Cpp代码  收藏代码
  1. /** 
  2. 题意:由n(10^6)个数据组成的圆环,数据为1和-1,问从一个点开始顺时针或逆时针,能遍历完所有点,并且保证中间过程中sum>=0。 
  3. 分析:首先暴力O(n^2)是不可行的。 
  4.     假设从i点开始,这里仅考虑向左,必须保证sum(j,i)>=0, i-n <j <= i. 
  5.     设sum[i]表示从0到i点的和,即保证sum[i]-sum[j]>=0,即sum[i] - max(sum[j])>=0. 
  6.     要求区间[i-n,i-1]最大值,维护单调递减队列即可。 
  7. */  
  8. #include <iostream>  
  9. #include <cstdio>  
  10. #include <cstring>  
  11. using namespace std;  
  12. const int N = 2100000;  
  13. int n;  
  14. char a[N/2];  
  15. int num[N];  
  16. int sum[N];  
  17. int que[N/2];  
  18. bool f[N/2],f2[N/2];  
  19.   
  20. void solve(bool t)  
  21. {  
  22.     int i;  
  23.     int head=1,tail=0;  
  24.     for(i = 1; i < n; i++)  
  25.     {  
  26.         while(head<=tail&&sum[que[tail]]<=sum[i]) tail--;  
  27.         tail++;  
  28.         que[tail] = i;  
  29.     }  
  30.     for(i = n; i <= 2*n; i++)  
  31.     {  
  32.         while(head<=tail&&que[head]<i-n) head++;  
  33.         while(head<=tail&&sum[que[tail]]<=sum[i]) tail--;  
  34.         tail++;  
  35.         que[tail] = i;  
  36.         if(sum[i]-sum[que[head]]>=0)  
  37.         {  
  38.             if(t)  
  39.                 f[i-n] = true;  
  40.             else  
  41.                 f2[i-n] = true;  
  42.         }  
  43.     }  
  44. }  
  45.   
  46. int main()  
  47. {  
  48.     int i;  
  49.     int t;  
  50.     int cases = 0;  
  51.     scanf("%d",&t);  
  52.     while(t--)  
  53.     {  
  54.         memset(f,0,sizeof(f));  
  55.         memset(f2,0,sizeof(f2));  
  56.         sum[0] = 0;  
  57.         scanf("%s",a+1);  
  58.         n = strlen(a+1);  
  59.         for(i = 1; i <= n; i++)  
  60.         {  
  61.             if(a[i]=='C')  
  62.                 num[i] = 1;  
  63.             else  
  64.                 num[i] = -1;  
  65.         }  
  66.         for(i = 1; i <= n; i++)  
  67.             num[i+n] = num[i];  
  68.         for(i = 1; i <= n*2; i++)  
  69.             sum[i] = sum[i-1] + num[i];  
  70.         //1234512345  
  71.         int result = 0;  
  72.         solve(true);  
  73.         //reverse  
  74.         for(i = 1; i <= n; i++)  
  75.             sum[i] = sum[i-1] + num[n+1-i];  
  76.         for(i = n+1; i <=2*n; i++)  
  77.             sum[i] = sum[i-1] + num[2*n+1-i];  
  78.         //5432154321  
  79.         solve(false);  
  80.         result = 0;  
  81.         for(i = 0; i < n; i++)  
  82.             if(f[i]||f2[n-i])  
  83.                 result++;  
  84.         printf("Case %d: %d\n",++cases,result);  
  85.     }  
  86.     return 0;  
  87. }  
转自http://yzmduncan.iteye.com/blog/1545671
posted on 2013-03-09 13:33  PegasusWang  阅读(391)  评论(0)    收藏  举报