现在的位置: 首页 > 综合 > 正文

单调队列

2018年02月23日 ⁄ 综合 ⁄ 共 2985字 ⁄ 字号 评论关闭

网上很多讲单调队列的资料,但是感觉讲的不是很清楚,大多都是只讲了思路,没有用实际的例子来阐述单调队列。

单调队列,顾名思义是指队列内的元素是有序的,队头为当前的最大值(单调递减队列)或最小值(单调递增序列),以单调递减队列为例来看队列的入队和出队操作:

1、入队:

     如果当前元素要进队,把当前元素和队尾元素比较,如果当前元素小于队尾元素,那么当前元素直接进队,如果当前元素大于队尾元素,那么队尾出队,将当前元素和新的队尾再做比较,直到当前元素大于队尾元素或者队列为空。单调队列只能在队尾插入元素,队尾和队头都可以删除元素。

2、出队:

     出队直接取队头即可,因为用单调队列就是为了取最值,而队头就是最值。

例子:将数组a[] = {3, 5, 2, 8, 1, 4, 7}依次入队,并保证队列为一个单调递减队列。

  • 3入队,队列为空直接入队,队列元素为:3;
  • 5入队,5和队尾比较,5大于3,3出队,队为空,5入队,队列元素为:5;
  • 2入队,2和队尾比较,2小于5,直接入队,队列元素为:5,2;
  • 8入队,8和队尾比较,2出队,8再和队尾比较,5出队,队为空,8入队,队列元素为:8;
  • 1入队,...,队列元素为:8,1;
  • 4入队,...,队列元素为:8,4;
  • 7入队,...,队列元素为:8,7。
实例应用:
1、给定一个数组a[]和一个长度为k的滑动窗口,该窗口从最左端移到最右端,找出窗口在每个位置是的最大值。
     例如:a[] = {7, 3, 2, 5, 6},k = 3。我们可以维护一个单调递减队列,这样对于每个位置,当前队列的队头就是最大值,只不过在入队的时候要检查一下队头的下标是否已经超出窗口的范围,如果超出就删除队头元素即可。为了方便写代码,单调队列很多时候保存的是下标,而不是数值本身。
  • i = 0,初始队列为空,7入队,队列为:{ 0(7) },(0为下标,括号为下标对应的值),窗口区间为[0],最大值为队头a[0] = 7;
  • i = 1,队头下标没有超出k,3入队,队列为:{ 0(7), 1(3) },窗口区间为[0,1],最大值为队头a[0] = 7;
  • i = 2,队头下标没有超出k,2入队,队列为:{ 0{7), 1(3), 2(2) },窗口区间为[0,1,2],最大值为队头a[0] = 7;
  • i = 3,队头下标为0超出k,删除队头,5入队,队列为:{ 3(5) },窗口区间为[1,2,3],最大值为队头a[3] = 5;
  • i = 4,队头下标没有超出k,6入队,队列为:{ 4(6) },窗口区间为[2, 3, 4],最大值为队头a[4] = 6;
  • 窗口继续向右滑动,如果当前队头下标超出范围就删除队头,然后删除队头,没有超出范围就直接取队头。
这样整个算法就是O(n)的。代码如下:
#include <iostream>
#include <vector>
#include <queue>
#include <assert.h>
using namespace std;

vector<int> WinMax(vector<int>& ivec, int k)
{
	assert(!ivec.empty());
	int size = ivec.size();
	vector<int> res(size + k - 1);
	deque<int> que;
	//首先将下标0入队
	que.push_back(0);
	res[0] = ivec[que.front()];
	for (int i = 1; i < size; ++i)
	{
		//如果当前下标超出范围就删除队头
		if (que.front() < i - k + 1)
			que.pop_front();
		//将当前元素入队
		while (!que.empty())
		{
			if (ivec[i] > ivec[que.back()])
				que.pop_back();
			else
				break;
		}
		que.push_back(i);
		res[i] = ivec[que.front()];
	}
	//当窗口右端超出数组范围,进行特殊处理
	for (int i = size; i < size + k - 1; ++i)
	{
		if (que.front() < i - k + 1)
			que.pop_front();
		res[i] = ivec[que.front()];
	}
	return res;
}

void main()
{
	int a[] = {7, 3, 2, 5, 6};
	vector<int> ivec(a, a + 5);
	vector<int> res = WinMax(ivec, 3);
	for (int i = 0; i < res.size(); ++i)
		cout << res[i] << endl;
}

2、给定一个数组a[n],要求生成b[n],其中b[i] = a[j],且j >= i,a[i~j] >= a[i],a[j+1] < a[i]。如果对于某个i,a[i~n]都大于a[i],那么b[i] = a[n]。

     题目的意思:对于某个a[i],如果a[i]右边(下标大于i)第一个比它小的数是a[j],那么b[i] = a[j-1]。也就是求右边大于当前数的连续数的最后一个,用实例来说明。
     a[0~6] = {3, 5, 4, 7, 1, 6, 2},那么b[0] = 7(右边第一个比3小的数是1,所以b[0]为7),同理可知:b[1] = 5,b[2] = 7,b[3] = 7,b[4] = 2,b[5] = 6,b[6] = 2。
     这个题为什么能用单调队列来做呢。我们只要找到右边第一个比它小的数即可。我们维护一个单调递增序列,对于a[i],如果碰到大于a[i]的数直接入队,如果碰到小于a[i]的数,设为a[j],那么当a[j]入队的时候,由单调队列的性质可知a[i]必定要出队,这时b[i]的值就是a[j-1]。当j == n时,将队列所有元素出队,并将出队的元素的b[]赋值为a[j]。这样算法的复杂度为O(n)。代码如下:
#include <iostream>
#include <vector>
#include <deque>
#include <assert.h>
using namespace std;

vector<int> RightMin(vector<int>& ivec)
{
	assert(!ivec.empty());
	int size = ivec.size();
	vector<int> res(size);
	deque<int> que;
	//先将下标0入队
	que.push_back(0);
	for (int i = 1; i < size; ++i)
	{
		while (!que.empty())
		{
			//如果当前元素大于队尾,直接入队
			if (ivec[i] >= ivec[que.back()])
			{
				que.push_back(i);
				break;
			}
			//否则就出队,并将对应的值保存
			else
			{
				res[que.back()] = ivec[i - 1];
				que.pop_back();
			}
		}
		if (que.empty())
			que.push_back(i);
	}
	//当元素遍历完之后,将队列的所有元素出队,并保存对应的值
	while (!que.empty())
	{
		res[que.back()] = ivec[size - 1];
		que.pop_back();
	}
	return res;
}


void main()
{
	int a[] = {3, 5, 4, 7, 1, 6, 2};
	vector<int> ivec(a, a + 7);
	vector<int> res = RightMin(ivec);
	for (int i = 0; i < res.size(); ++i)
		cout << res[i] << endl;
}

抱歉!评论已关闭.