数据结构--单调栈

本文为转载,出处:https://blog.csdn.net/liujian20150808/article/details/50752861

                              https://blog.csdn.net/lucky52529/article/details/89155694

一、单调栈的定义

     从名字上就听的出来,单调栈中存放的数据应该是有序的,所以单调栈也分为单调递增栈单调递减栈

  • 单调递增栈:数据出栈的序列为单调递增序列
  • 单调递减栈:数据出栈的序列为单调递减序列

     ps:这里一定要注意所说的递增递减指的是出栈的顺序,而不是在栈中数据的顺序

     为了更好的理解单调栈,则可将单调栈用生活情形模拟实现,例如:

       我们借用拿号排队的场景来说明下。现在有很多人在排队买可乐,每个人手里都拿着号,越靠前的人手里的号越小,但是号不一定是连续的。小明拿了号后并没有去排队,而是跑去约会了。等他回来后,发现队伍已经排得很长了,他不能直接插入到队伍里,不然人家以为他是来插队的。小明只能跑到队伍最后,挨个询问排队人手里的号,小明认为号比他大的人都是“插队”的,于是小明就会施魔法把这些人变消失,直到小明找到号比他小的为止。在上面这个场景里,大家排的队伍就像是单调栈,因为大家手里拿的号是单调递增的。而小明找自己位置的这个过程就是元素加入单调栈的过程。新加入的元素如果加到栈顶后,如果栈里的元素不再是单调递增了,那么我们就删除加入前的栈顶元素,就像小明施魔法把“插队”的人变消失一样。直到新元素加入后,栈依然是单调递增时,我们才把元素加进栈里。

(这样做的目的是“维护”单调栈,是单调栈保持原来的单调性不变)

1.从数组的角度阐述单调栈的性质:

      给定一个包含若干个整数的数组,我们从第 1 个元素开始依次加入单调栈里,并且加入后更新单调栈。那么单调栈有这样的性质:对于单调递增的栈,如果此时栈顶元素为 b,加入新元素 a 后进行更新时,如果 a 大于 b,说明 a 在数组里不能再往左扩展了(由于单调栈的单调递增性质,b前面的元素均小于a),也就是说,如果从 a 在数组中的位置开始往左边遍历,则 a 一定是第一个比 b 大的元素;如果 a 小于 b,说明在数组里,a 前面至少有一个元素不能扩展到 a 的位置(至少有b元素,因为b的值要大于a,如果此时再加入新的a,那么单调栈便不再单调,所以元素a此时不能压入栈顶,因为这并不是元素a"应该"在的位置,只有当元素a找到自己的位置时元素a方能压入栈中,而这样做的前提是不改变单调栈的单调性),也就是对于这些元素来说,a 是其在数组右侧第一个比它小的元素。

       单调栈的维护是 O(n) 级的时间复杂度,因为所有元素只会进入栈一次,并且出栈后再也不会进栈了。

      模拟实现一个递增单调栈:

现在有一组数10,3,7,4,12。从左到右依次入栈,则如果栈为空或入栈元素值小于栈顶元素值,则入栈;
否则,如果入栈则会破坏栈的单调性,则需要把比入栈元素小的元素全部出栈。单调递减的栈反之。

10入栈时,栈为空,直接入栈,栈内元素为10。

3入栈时,栈顶元素10比3大,则入栈,栈内元素为10,3。

7入栈时,栈顶元素3比7小,则栈顶元素出栈,此时栈顶元素为10,比7大,则7入栈,栈内元素为10,7。

4入栈时,栈顶元素7比4大,则入栈,栈内元素为10,7,4。

12入栈时,栈顶元素4比12小,4出栈,此时栈顶元素为7,仍比12小,栈顶元素7继续出栈,此时栈顶元素为10,仍比12小,10出栈,此时栈为空,12入栈,栈内元素为12。

2.单调栈的伪代码

stack st;
//此处一般需要给数组最后添加结束标志符,具体下面例题会有详细讲解
for (遍历这个数组)
{
	if (栈空 || 栈顶元素大于等于当前比较元素)
	{
		入栈;
	}
	else
	{
		while (栈不为空 && 栈顶元素小于当前元素)
		{
			栈顶元素出栈;
			更新结果;
		}
		当前数据入栈;
	}
}

3.单调栈的性质

     1.单调栈里的元素具有单调性

     2.元素加入栈前,会在栈顶端把破坏栈单调性的元素都删除

     3.使用单调栈可以找到元素向左遍历第一个比他小的元素,也可以找到元素向左遍历第一个比他大的元素。

     对于第三条性质的解释:

     1.当单调栈中的元素是单调递增的时候,根据上面我们从数组的角度阐述单调栈的性质的叙述,可以得出:

      (1)当a > b 时,则将元素a插入栈顶,新的栈顶则为a

      (2)当a < b 时,则将从当前栈顶位置向前查找(边查找,栈顶元素边出栈),直到找到第一个比a小的数,停止查找,将元素a插入栈顶(在当前找到的数之后,即此时元素a找到了自己的“位置”)

     2.当单调栈中的元素是单调递减的时候,则有:
      (1)当a < b 时,则将元素a插入栈顶,新的栈顶则为a

      (2)当a > b 时,则将从当前栈顶位置向前查找(边查找,栈顶元素边出栈),直到找到第一个比a大的数,停止查找,将元素a插入栈顶(在当前找到的数之后,即此时元素a找到了自己的“位置”)

二、单调栈的例题
     1.https://www.luogu.com.cn/problem/P1901

           某地有 N个能量发射站排成一行,每个发射站 i都有不相同的高度 Hi​,并能向两边(两端的发射站只能向一边)同时发射能量值为 Vi​ 的能量,发出的能量只被两边最近的且比它高的发射站接收。显然,每个发射站发来的能量有可能被0或1或2个其他发射站所接受。请计算出接收最多能量的发射站接收的能量是多少。

#include
using namespace std;
const int N = 1000010;
int h[N], v[N], ans[N], mx;
stack s; //STL中的栈
int main()
{
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> h[i] >> v[i]; //h为此发射台的高度,v为此发射台的能量
    for (int i = 1; i <= n; i++)
    {
        while (!s.empty() && h[s.top()] < h[i])
        {
        	ans[i] += v[s.top()];
        	s.pop();
		}
		if (!s.empty()) ans[s.top()] += v[i];
        s.push(i);
    }
    for (int i = 1; i <= n; i++) mx = max(mx, ans[i]);
    cout << mx << endl;
}

      2.视野总和

         有n个人站队,所有的人全部向右看,个子高的可以看到个子低的发型,给出每个人的身高,问所有人能看到其他人发现总和是多少。
         输入:4 3 7 1
         输出:2
         解释:个子为4的可以看到个子为3的发型,个子为7可以看到个子为1的身高,所以1+1=2。
         思路:观察题之后,我们发现实际上题目转化为找当前数字向右查找的第一个大于他的数字之间有多少个数字,然后将每个结果累计就是答案,但是这里时间复杂度为O(N^2),所以我们使用单调栈来解决这个问题。

         1.设置一个单调递增的栈(栈内0~n为单调递减)
         2.当遇到大于栈顶的元素,开始更新之前不高于当前人所能看到的值

int FieldSum(vector& v)
{
	v.push_back(INT_MAX);//这里可以理解为需要一个无限高的人挡住栈中的人,不然栈中元素最后无法完全出栈
	stack st;
	int sum = 0;
	for (int i = 0; i < (int)v.size(); i++)
	{
		if (st.empty() || v[st.top()] > v[i])//小于栈顶元素入栈
		{
			st.push(i);
		}
		else
		{
			while (!st.empty() && v[st.top()] <= v[i])
			{
				int top = st.top();//取出栈顶元素
				st.pop();
				sum += (i - top - 1);//这里需要多减一个1
			}
			st.push(i);
		}
	}
	return sum;
}

          3.柱状图中的最大矩形https://leetcode-cn.com/problems/largest-rectangle-in-histogram/

         思路:当前的数字可以向两边拓展,遇到比自己大的就接着拓展,小的就停止,然后用自己的高度乘以拓展的宽度,每次都更新最大面积,时间复杂度同样为O(N^2),所以我们接着借助单调栈。

         1.设置一个单调递减的栈(栈内0~n为单调递增)
         2.当遇到小于栈顶元素的值,我们开始更新数据,因为有可能最大面积就会出现在栈中的序列里
         3.牢记栈中数据永远是有序的,这个问题比较复杂,所以读者不妨对照着代码来理解问题

int largestRectangleArea(vector& heights) {
	heights.push_back(-1);/同理,我们希望栈中所有数据出栈,所以给数组最后添加一个负数
	stack st;
	int ret = 0, top;
	for (int i = 0; i < heights.size(); i++)
	{
		if (st.empty() || heights[st.top()] <= heights[i])
		{
			st.push(i);
		}
		else
		{
			while (!st.empty() && heights[st.top()] > heights[i])
			{
				top = st.top();
				st.pop();
				//i-top指的是当前矩形的宽度,heights[top]就是当前的高度
				//再次强调栈中现在为单调递增
				int tmp = (i - top)*heights[top];
				if (tmp > ret)
					ret = tmp;
			}
			st.push(top);
			heights[top] = heights[i];
		}
	}
	return ret;
}

         3.求最大区间


            描述:给出一组数字,求一区间,使得区间元素和乘以区间最小值最大,结果要求给出这个最大值和区间的左右端点
            输入:3 1 6 4 5 2
            输出:60
                   3 5
            解释:将3到5(6+4+5)这段区间相加,将和与区间内最小元素相乘获得最大数字60
            思路:使用暴力解法求出所有区间,再求出区间的最小值相乘跟新数据,并不是一种很好的算法,所以经过上面俩题的磨炼,此时我们应该使用一个单调递减栈

            1.设置一个单调递减的栈(栈内0~n为单调递增)
            2.当遇到小于栈顶元素的值,我们开始更新数据,因为当前遇到的值一定是当前序列最小的

int GetMaxSequence(vector& v)
{
	stack st;
	vector vs(v.size()+1);
	vs[0] = 0;
	for (int i = 1; i < vs.size(); i++)
	{
			vs[i] = vs[i - 1] + v[i-1];
	}
	v.push_back(-1);
	int top, start, end, ret = 0;
	for (int i = 0; i < v.size(); i++)
	{
		if (st.empty() || v[st.top()] <= v[i])
		{
			st.push(i);
		}
		else
		{
			while (!st.empty() && v[st.top()] > v[i])
			{
				top = st.top();
				st.pop();
				int tmp = vs[i] - vs[top];
				tmp = tmp * v[top];
				if (tmp > ret)
				{
					ret = tmp;
					start = top+1;
					end = i;
				}
			}
			st.push(top);
			v[top] = v[i];//与第二题相同的道理,将当前数据的更改最左的top下标,防止出现比当前数据更小的数据
			//这句在这道题里真的超级难理解,但是只要你有耐心相信你可以理解的
		}
	}
	return ret
}

你可能感兴趣的:(数据结构,c++)