Learning from Monotonic Queues - Sliding Window for Maximum/Minimum

In the past few days, I have been learning about monotonic queues and monotonic stacks, and I feel that the following blogs are better.
http://www.cnblogs.com/saywhy/p/6726016.html
http://dping26.blog.163.com/blog/static/17956621720139289634766/

Monotonic queues are not really queues. Because the queues are all FIFO, they are uniformly entered from the end of the queue and dequeued from the beginning. But monotonic queues are queued from the end of the queue and dequeued from the head or tail of the queue, so monotonic queues do not obey FIFO.

1) For monotonic (increasing) queues, compare with the tail of the queue each time a new element is added. If the value of the element at the tail of the queue is greater than the element to be enqueued, the tail element will be popped from the queue, and this operation will be repeated until the queue is empty or The trailing element is smaller than the element to be enqueued. Then, add the element to be enqueued to the end of the queue.
Monotonically increasing means increasing from the beginning of the line to the end of the line. For example [1,3,4,5], 1 is the head of the team and 5 is the tail of the team.
The monotonic (decreasing) queue is similar, but the words big and small are interchanged.

2) Monotonic (increasing) queues can be used to find the minimum value of a sliding window . Similarly, monotonic (decreasing) queues can be used to find the maximum value of a sliding window . The algorithmic complexity is O(n). Note that if we use min-heap or max-heap to maintain the max/min value of the sliding window, the complexity is O(nlogn), because the heap query operation is O(1), but the heap needs to be adjusted in and out of the heap , the complexity of the adjustment is O(logn).

3) One use of monotonic queues is to use their sliding window maxima to optimize the time complexity of dynamic programming problems.

How to implement a monotonic queue? Taking a monotonically increasing queue as an example, I see some implementation codes on the Internet that are similar to the following:

int q[maxn];
int front, tail, cur;
front = tail = 0;
while (front < tail && q[tail-1] > cur)
     --tail; 
q[tail++]=cur; 

The monotonically decreasing queue is similar, except that the comparison of q[tail-1] elements is changed to <.

But in fact, the above code is not enough for the monotonic queue, because the window of the monotonic queue is limited, so when the tail moves, it will also move the front, if the distance between them exceeds the window size. So we have to remember to update the front. Note that the front and tail pointers of the monotonic queue are constantly moving in the same direction.

How to update it? We must save the index of each element in the queue in the original array. We can save these coordinates in q[] (note that q[] in the above code is the value of the saved element), and then a[q[i]] is the value of the corresponding element. When we find that the gap of the array elements pointed to by head and tail is larger than the size of the sliding window, we must adjust the head.

Taking the monotonically increasing queue to find the minimum value of the sliding window as an example, the code is as follows:

#include <iostream>
using namespace std;
#define maxn 1000006
int a[maxn];
int q[maxn];

int main()
{
    int n,k,i;
    int head=0, tail=0;

    scanf("%d%d",&n,&k);  //array size and sliding window size

    for(i=0;i<n;i++)
        scanf("%d",&a[i]);

    for (i=0; i<n; i++)
    {
        while(head<tail && a[i]<=a[q[tail-1]])
            tail--;

        q[tail++]=i;

        //head和tail的间隔已超出sliding window的size, 需要更新head, 使得     
        //head和tail的间隔刚好是sliding window的size。
        if (tail> 0 && (q[tail-1]-q[head]+1>k))
           head += q[tail-1] + 1 -k -q[head];

        if (i>=k-1)
           cout<<a[q[head]]<<" ";
    }
    cout<<endl;
    return 0;
}

There are a few points to note:
1) Both head and tail move in the direction of increasing, tail is always >= head.
2) Pay attention to the usage of tail-1, because tail++.
3) One step is enough when adjusting the head, but because tail-1, a judgment condition tail>0 should be added to prevent the head from being adjusted at the beginning.
4) If we want to implement a monotonically decreasing queue to find the maximum value of the sling window, then use the above

while(head<tail && a[i]<=a[q[tail-1]])

change to

while(head<tail && a[i]>=a[q[tail-1]])

That's it, nothing else changes.
5) The above algorithm still has room for optimization. exist

while(head<tail && a[i]<=a[q[tail-1]])

Inside you can use binary search. Because it is a monotonic queue.

The complexity of this algorithm is discussed below. The above code has a while loop in the for loop. It seems that the complexity is O(nk). If binary search is used to find it, it is also O(nlogk). But let’s think about it from another angle, each element is enqueued at most once and dequeued once, so there are a total of 2n dequeue and enqueue operations, and these 2n operations are evenly distributed to n elements, so the complexity is actually O(k) . For specific analysis, please refer to the amortized analysis in the algorithm. If we use binary search in the while loop, for the case of large sliding window, the speed can be accelerated, but the complexity is still O(k).

In addition, monotonic queues can also be implemented with deques. Because deque supports dequeuing and enqueuing operations on both sides of the queue tail and the queue head. The following is the code to find the minimum value of the sliding window using a monotonically increasing queue:

#include <iostream>
#include <deque>
using namespace std;

#define maxn 1000006
int a[maxn];
deque<int> dq;


int main()
{
    int n,k,i;
    scanf("%d%d",&n,&k);  //array size and sliding window size

    for(i=0;i<n;i++)
    {
        scanf("%d",&a[i]);
    }

    for (i=0; i<n; i++)
    {
       while(!dq.empty() && a[i]<a[dq.back()])
           dq.pop_back();

       dq.push_back(i);

       while(dq.back() - dq.front() + 1 > k)
          dq.pop_front();

       if (i>=k-1)
           cout<<a[dq.front()]<<" ";
    }

    cout<<endl;
    return 0;
}

Note:
1) In the operation of pop_front(), the front() must be popped out one by one, and the position of the front cannot be adjusted directly, because it will destroy the iterator inside the deque. The complexity here is still O(n), see the amortized analysis above for details.
2) Use deque without worrying about tail-1, because deque will handle it internally.
3) If you use the monotonically decreasing queue to find the maximum value of the sliding window, you only need to add the following in the while loop to

      while(!dq.empty() && a[i]<a[dq.back()])

<Change to> is enough, other things remain unchanged.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325640853&siteId=291194637