浅说单调队列

海哥  论坛元老 | 2024-12-16 03:23:36 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 1014|帖子 1014|积分 3042

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
初步感知

单调队列的含义

(一)定义

单调队列(Monotonic Queue)是一种特殊的数据结构,它是一个队列,并且队列中的元素具有单调性。这种单调性可以是单调递增大概单调递减或呈现出其他的样子。例如,在一个单调递减的队列中,队首元素是队列中最大的元素,并且随着元素的入队和出队操作,队列始终保持递减的性质。
(二)示例阐明

假设我们有一个数列[5, 3, 4, 1, 2],假如我们要构建一个单调递减的队列,且要保证队首维护最大值的话,就有以下操作。


  • 开始时,队列空,当5进入队列,队列中只有一个元素5。
  • 接着3要入队,因为3 < 5,为了保持单调递减的性质,5出队,3入队,此时队列中元素为3。
  • 当4入队时,4 > 3,3不出队,4入队,队列变为[3, 4]。
  • 对于1入队,4和3依次出队,1入队,队列变为1。
  • 最后2入队,1出队,队列变为2。
从这个过程可以看出,单调队列能够动态地维护队列元素的单调性。
但是他有什么用处呢?
深入思考

办理的问题范例

滑动窗口问题

给定一个数组和一个固定巨细的滑动窗口,要求在窗口滑动过程中,快速求出窗口内的最大值或最小值等统计信息。例如,在一个长度为                                    n                              n                  n 的数组                                    n                         u                         m                         s                              nums                  nums 中,有一个巨细为                                    k                              k                  k 的滑动窗口,需要求出每个滑动窗口内的最大值。
假如使用暴力解法,对于每个滑动窗口,需要遍历窗口内的                                    k                              k                  k 个元素来找到最大值,时间复杂度为                                    O                         (                         n                         ×                         k                         )                              \cal{O(n \times k)}                  O(n×k)。而使用单调队列可以将时间复杂度降低到                                    O                         (                         n                         )                              \cal{O(n)}                  O(n) (原理背面再说)。
动态规划中的优化问题

在一些动态规划问题中,状态转移方程可能涉及到一个区间内的最优值选择。单调队列可以资助我们高效地维护这个区间内的最优值,从而优化动态规划的盘算过程。
怎样写单调队列

着实对于单调队列而言,只有两个点需要考虑——一个是入队,一个是出队,只要我们把这两个点给考虑周全了,就没有问题了
入队操作思路

这里又要考虑两个点了,而且非常紧张!(敲黑板)
维护单调性

当一个新元素要入队时,我们需要检查队列的单调性是否会被破坏。假如是单调递减队列,且新元素大于队尾元素,那么就需要将队尾元素依次出队,直到新元素可以入队而不破坏单调性
例如,在单调递减队列中,队列为[5, 3, 2],当元素 4 要入队时,因为 4 > 2,所以 2 出队,此时队列变为[5, 3],由于 4 > 3,3 也出队,队列变为[5],最后 4 入队,队列变为[5, 4]。
记载有效信息

单调队列中的元素不仅要保持单调性,还需要记载有效的位置信息。在滑动窗口问题中,我们通常会记载元素在原始数组中的下标。如许,当窗口滑动时,我们可以根据下标的范围来判断队列中的元素是否还在当前窗口内。
例如,在一个长度为                                    n                              n                  n 的数组上有一个巨细为                                    k                              k                  k 的滑动窗口,队列中存储元素的下标。当窗口滑动时,我们可以通过检查下标的范围来确定元素是否已经滑出窗口。假如队首元素的下标小于当前窗口的起始下标,那么队首元素就已经不在当前窗口内,需要出队。
  1. int tail=-1;
  2. int a[INF],v[INF];//v为单调队列
  3. while (tail>=0&&a[i]>a[v[tail]])tail--;
  4. v[++tail]=i;//记录下标
复制代码
出队操作思路

窗口滑动导致的出队

在滑动窗口问题中,当窗口滑动时,可能会导致队首元素离开窗口。我们需要检查队首元素的位置信息,判断它是否还在窗口内。假如不在,就将队首元素出队。
例如,在一个巨细为 3 的滑动窗口从数组[1, 2, 3, 4, 5]的第一个位置开始滑动,初始时单调递减队列可能存储了元素[3, 2, 1]及其下标。当窗口向右滑动一个位置时,元素 1 的下标已经不在当前窗口范围内,所以 1 需要从队首出队。
新元素入队导致的出队

就像前面说的,当新元素入队时,为了维护队列的单调性,可能会导致队尾元素出队。这种出队操作是为了确保队列始终保持单调的性质。
例如,在单调递减队列中,队列为 [6, 4, 3] ,当元素 5 要入队时,为了保持单调递减,3 和 4 需要依次出队,然后 5 入队,队列变为[6, 5]。
  1. while (tail>=head&&v[head]-i>m)head++;
复制代码
例题实战

例题1

给定一个数组,再给定三个操作:
                                    1                              1                  1                                    x                              x                  x:在数组末尾插入一个数                                    x                              x                  x
                                    2                              2                  2:删除数组中的第一个数,假如数组为空则忽略。
                                    3                              3                  3:查询当前数组中的最大值,并输出。
初始时数组为空。
解析

这是一道非常裸的单调队列的题目,把题目翻译一下就是要求实现一个可以举行特定操作的数据结构模拟,通过给定的三种操作来对一个初始为空的数组举举措态处理,并能随时查询当前数组中的最大值。整体解题思路围绕着怎样高效地维护数组中的元素以及快速获取最大值展开,而单调队列在这里就起到了关键作用。
当接收到操作 1 时,意味着要向数组末尾插入一个新的数 ,此时就是对单调队列举行操作的机会。因为新插入的元素可能会影响到当前数组(从单调队列角度看就是队列所代表的元素集合)中的最大值情况,所以需要实时更新单调队列来维持其能正确反映数组最大值的功能,因此只要队尾元素小于即将插入的新元素,就将队尾元素出队(通过 tail-- 操作),把那些比新元素小的、不可能再成为最大值的元素从队列中移除掉。然后再将新元素 通过 v[++tail]=x[cnt] 插入到队尾,如许就保证了队列在插入新元素后依然保持单调递减的特性,从而队首元素始终为当前数组中的最大值。
当接收到操作 2 时,即要删除数组中的第一个数,这时需要考虑对单调队列举行相应调整,因为数组的元素状态发生了改变,单调队列所代表的元素集合也需要同步更新,以确保其能继承正确反映数组的最大值情况。
起首举行了一些边界判断,如 if (st==cnt+1)continue; 来处理数组为空等特殊情况。重点在于当要删除的数组中的第一个数(即 x[st])恰恰等于单调队列的队首元素(即 v[head] )时,阐明当前代表最大值的元素要被移除了,那么就需要通过 head++ 操作将队首元素出队,使单调队列更新到下一个可能为最大值的元素成为队首,以此来保持与数组元素删除操作后的状态一致性,让单调队列依然能正确体现剩余数组元素中的最大值情况。
ACcode

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. const int INF=2e6+10;
  4. int v[INF],x[INF],head=0,tail=-1;
  5. int st=1,cnt;
  6. int main(){
  7.         int n;
  8.         scanf("%d",&n);
  9.         for (int i=1;i<=n;i++){
  10.                 int opt;
  11.                 scanf("%d",&opt);
  12.                 if (opt==1){
  13.                         scanf("%d",&x[++cnt]);
  14.                         while (tail>=head&&v[tail]<x[cnt])tail--;
  15.                         v[++tail]=x[cnt];
  16.                 }else if (opt==2){
  17.                         if (st==cnt+1)continue;
  18.                         if (x[st]==v[head]){
  19.                                 head++;
  20.                         }
  21.                         st++;       
  22.                 }else {
  23.                         printf("%d\n",v[head]);
  24.                 }
  25.         }
  26.         return 0;
  27. }
复制代码
例题2

wwx家里有一个长度为                                    N                              N                  N 的衣柜,里面装有透明程度不同的黑丝,如今衣柜上只有一个长度为                                    K                              K                  K 的滑动窗体从最左端移至最右端,wwx只能看到窗口中的                                    K                              K                  K 个数,每次窗体向右移动一位,都会有一条新的黑丝出现,一条之前已经出现过的黑丝消失。
因为wwx在每次能选择的时候要光着脚来拍视频给你看,所以你想知道每个窗口内透明程度最高的黑丝是那一条,并输出它的透明程度,如许才能使你和wwx的关系更近一步。
解析

这道题也是非常裸的滑动窗口的题目,本质上就是一个滑动窗口求最值的问题,给定一个长度为                                    N                              N                  N 的数列(代表衣柜里黑丝的透明程度序列),有一个长度为 K 的滑动窗口从左至右移动,要求在每个窗口位置找出该窗口内透明程度最高的黑丝对应的透明程度值。
整体思路是使用单调队列这个数据结构来高效地维护窗口内的最值情况。单调队列在这里用于保持队列中的元素呈现一种单调性(在本题中是从队头到队尾元素值单调递减,以方便快速找到最大值),随着窗口的滑动动态更新最值信息,避免了每次窗口移动都去遍历窗口内所有元素来求最值这种低效的做法。
每次窗口向右移动一位,有新元素进入窗口范围时,就需要实行入队操作。也就是在代码中遍历输入数列的循环里,当                                    i                              i                  i 从                                    1                              1                  1 开始渐渐增加到                                    n                              n                  n 的过程中,每一次遇到新的元素                                    a                         [                         i                         ]                              a                  a 且当前窗口还未完全滑过整个数列时(即                                    i                              i                  i 的取值范围在合理区间内),都要调用 push 函数将这个新元素尝试加入到单调队列中。例如,最开始先对窗口内初始的                                    k                              k                  k 个元素依次实行入队操作,后续随着窗口继承右移,每出现一个新元素也实行同样的入队操作。
当窗口向右移动,最左边的元素要移出窗口范围时,就需要实行出队操作。我们可以通过一个指针 st 来标记窗口最左边元素的位置,每当窗口整体右移一位(也就是新元素入队后),要检查当前窗口最左边的元素(即 a[st] )是否恰恰是单调队列中队头对应的元素,假如是,就阐明这个元素要随着窗口移动而移出窗口范围了,此时就需要将队头元素出队,并且同时更新窗口最左边元素的标记指针,使得后续操尴尬刁难应的窗口范围始终是正确的。
怎样举行操作
通过如许合理地在窗口滑动过程中适时举行单调队列的入队和出队操作,就能高效地维护每个窗口内的最大值情况,终极输出每个窗口对应的最大值(即透明程度最高的黑丝的透明程度值)。
ACcode

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. const int INF=1e6+10;
  4. int a[INF],vmi[INF],vmx[INF];
  5. int ans1[INF],ans2[INF];
  6. int t1=-1,h1=0,t2=-1,h2=0,st=1;
  7. void push(int i){
  8.         while (t2>=h2&&vmx[t2]<a[i])t2--;
  9.         vmx[++t2]=a[i];
  10.         return;
  11. }
  12. void pop(){
  13.         if (a[st]==vmx[h2]){
  14.                 h2++;
  15.         }
  16.         st++;
  17. }
  18. int main(){
  19.         int n,k;
  20.         cin>>n>>k;
  21.         for (int i=1;i<=n;i++){
  22.                 cin>>a[i];
  23.         }
  24.         for (int i=1;i<=k;i++){
  25.                 push(i);
  26.         }
  27.         ans2[k]=vmx[h2];
  28.         for (int i=k+1;i<=n;i++){
  29.                 push(i);
  30.                 pop();
  31.                 ans2[i]=vmx[h2];
  32.         }
  33.         for (int i=k;i<=n;i++){
  34.                 cout<<ans2[i]<<" ";
  35.         }
  36.         return 0;
  37. }
复制代码
例题3

本日是yjq的约会日,wwx同意给他穿他喜欢的衣服。wwx家里有一个长度为                                    N                              N                  N 的衣柜,里面装有性感程度不同的衣服,譬如黑丝,白丝,jk等(请自行脑补),
yjq作为wwx的boyfriend,自然希望wwx的性感程度总和最大,但wwx最多又只能穿                                    m                         (                         m                         <                         n                         )                              m(m<n)                  m(m<n) 件衣服。
请你帮他从这                                    n                              n                  n 件衣服中找出连续的                                    k                         (                         1                         ≤                         k                         ≤                         m                         )                              k(1 \le k\le m)                  k(1≤k≤m) 件衣服,使得其上的总性感程度最大。
形式化地,在数列                                    {                                   p                            n                                  }                              \{p_n\}                  {pn​} 中,找出一个子段                                    [                         l                         ,                         r                         ]                         (                         r                         −                         l                         +                         1                         ≤                         m                         )                              [l,r](r-l+1\le m)                  [l,r](r−l+1≤m),最大化                                              ∑                                       i                               =                               l                                      r                                            p                            i                                       \sum\limits_{i=l}^rp_i                  i=l∑r​pi​。
解析

这道题和上道题差不多,一个是求最大值,一个是求和最大,没有本质上的区别,所以就不外多赘述了。
ACcode

  1. #include<bits/stdc++.h>
  2. const int INF=1e6+10;
  3. using namespace std;
  4. int ans=INT_MIN,n,m;
  5. int x[INF],sum[INF],l,r;
  6. deque<int>q;
  7. int main(){
  8.         cin>>n>>m;
  9.     for(int i=1;i<=n;i++){
  10.         cin>>x[i];
  11.         sum[i]=sum[i-1]+x[i];
  12.     }
  13.     q.push_back(0);
  14.     for(int i=1;i<=n;i++){
  15.         while(q.front()+m<i)q.pop_front();
  16.         if (ans<sum[i]-sum[q.front()]){
  17.                 ans=sum[i]-sum[q.front()];
  18.                 }
  19.         while(!q.empty()&&sum[q.back()]>=sum[i])q.pop_back();
  20.         q.push_back(i);
  21.     }
  22.     cout<<ans;
  23.     return 0;
  24. }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

海哥

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表