WEEK(5)作业——单调栈/单调队列/尺取法/前缀和差分

A-最大矩形

题目描述

 给一个直方图,求直方图中的最大矩形的面积。例如,下面这个图片中直方图的高度从左到右分别是2, 1, 4, 5, 1, 3, 3, 他们的宽都是1,其中最大的矩形是阴影部分。

在这里插入图片描述

Input

输入包含多组数据。每组数据用一个整数n来表示直方图中小矩形的个数,你可以假定1 <= n <= 100000. 
然后接下来n个整数h1, ..., hn, 满足 0 <= hi <= 1000000000. 
这些数字表示直方图中从左到右每个小矩形的高度,每个小矩形的宽度为1。 测试数据以0结尾。

Output

对于每组测试数据输出一行一个整数表示答案。

Sample Input

7 2 1 4 5 1 3 3
4 1000 1000 1000 1000
0

Sample Output

8
4000

解题思路

对于每一个小矩形,以它为大矩形的高,要使大矩形面积最大,那么左端点一定是越靠左,右端点越靠右。**由图形可知,左端点可以确定为往左数第一个小于此高度的点,右端点可以确定为往右数第一个小于此高度的点。**那么我们如何来找到矩形两侧第一个小于此高度的点呢?
单调栈的知识可以很好地解决此类问题!且其为线性O(n)复杂度!
根据性质我们可以得到,对每一个栈顶的小矩形,其往栈底方向相邻的矩形为往左数第一个小于此高度的点;而当后续矩形入栈时,若不满足入栈条件(入栈矩形高度>=栈顶矩形高度),则该矩形即为往右数第一个小于此高度的点!
清楚了这些,我们可以遍历每一个矩形,根据矩形的高度将他们的序号依次加入递减栈(即栈顶>栈底)。在加入的过程中,首先要维护递增栈,即栈空或入栈矩形高度>=栈顶矩形高度,可直接入栈;
若不满足,那么此时要加入的矩形,就为栈内所有高度大于此矩形的矩形的右端点。此时我们要弹出这些矩形,弹出前记录它的序号,根据其往栈底方向相邻的矩形为左端点,弹出后,此时的栈顶矩形就为它的左端点(若弹出后栈空,左端点就为0)。对每一个弹出的矩形,我们都计算了一次以它为高的最大矩形的面积。
**当所有矩形都加入栈后,栈最终必为递减栈,第n个矩形必为栈顶,即为此时栈内的最高矩形。对此时栈内的所有小矩形,往右没有小于自己高度的点,那么它们的右端点r=n,**左端点与上述原理相同。最终我们对所有小矩形都计算了一次以它为高的最大矩形的面积。

注意

每组数据计算前一定记得要初始化ans!!!为这个bug爆炸 (学习)了好久orz。

总结

单调栈的作用:
(1)线性时间复杂度O(n)
(2)单调递增栈(栈顶<栈底):可以找到往左/往右第一个比当前元素的元素;
单调递减栈(栈顶>栈底):可以找到往左/往右第一个比当前元素的元素。
(3)可求的以当前元素为最值的最大区间

代码

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<stack>

using namespace std;

stack<int> s;
long long ans,h[100010];
int n;

int main() {
	int l=0,r=0,cur=0;
	long long s_temp=0;
	while(scanf("%d",&n)!=EOF&&n!=0) {
		ans=0;//每组数据一定记得要初始化ans!!!
		for(int i=1;i<=n;++i) 
			scanf("%lld",h+i);
		for(int i=1;i<=n;++i) {
			//递减栈 
			if(s.empty()||h[i]>=h[s.top()])
				s.push(i);
			else {
				r=i-1;
				while(!s.empty()&&h[i]<h[s.top()]) {
					//此时i为这些小矩形右侧第一个小于其高度的点 
					cur=s.top();
					s.pop();
					if(s.empty()) l=0;
					else l=s.top();
					s_temp=h[cur]*(r-l);
					ans=max(s_temp,ans);
				}
				s.push(i);
			}
		}
		
		/*栈最终必为递减栈,且最高高度为h[n]
		  对此时栈内的所有小矩形,右边没有小于自己高度的点,则r=n*/
		r=n;
		while(!s.empty()) {
			cur=s.top();
			s.pop();
			if(s.empty()) l=0;
			else l=s.top();
			s_temp=h[cur]*(r-l);
			ans=max(s_temp,ans);
		}
		printf("%lld\n",ans);
	}
	return 0;
}

B-TT’s Magic Cat

题目描述

One day, the magic cat decided to investigate TT’s ability by giving a problem to him. That is select cities from the world map, and represents the asset value owned by the -th city.

Then the magic cat will perform several operations. Each turn is to choose the city in the interval
and increase their asset value by . And finally, it is required to give the asset value of each city after
operations.
Could you help TT find the answer?
题目大意:长度为n的数组,一共q次操作。每次操作给出l,r,c,表示区间[l,r]中各个数均加上c。求q次操作结束后,数组中各个元素值?

Input

The first line contains two integers  — the number of cities and operations.
The second line contains elements of the sequence : integer numbers     
Then lines follow, each line represents an operation. The -th line contains three integers   and for the  -th operation.

Output

Print integers one per line, and should be equal to the final asset value of the -th city.

Examples

Input
4 2
-3 6 8 4
4 4 -2
3 3 1
Output
-3 6 9 2

Input
2 1
5 -2
1 2 4
Output
9 2

Input
1 2
0
1 1 -8
1 1 -6
Output
-14

解题思路

此题若暴力做法,复杂度O(qn)超时。
本题是一道典型的差分题,可以通过构建原数组的差分数组,即B[1]=A[1];B[i]=A[i]-A[i-1]…
通过差分数组的特点:
(1)B数组的前缀和sum{B[1~i]}=A数组元素值A[i];
(2)A数组的区间加(A[l] ~A[r]均加上c)等价于B数组的单点修改:B[l]+=c,B[r+1]-=c,

由此对每一次区间+的操作,我们只需要对B数组相应位置单点修改即可,B数组前缀和即为A数组最终数值,复杂度为O(q+n)。

总结

前缀和作用:
(1)O(1)求出一个区域内所有元素数值之和,sum[L,R]=sum[R]-sum[L-1](需要O(n)数据预处理)
(2)当涉及快速求取某一区域和时,可考虑使用前缀和进行计算,即前缀和通常用于优化算法中的某一步骤,进而降低复杂度

一维前缀和:

sum[i]=sum[i-1]+a[i];
sum[L,R]=sum[R]-sum[L-1]

代码

#include<iostream>
#include<cstdio>
#include<algorithm>

using namespace std;

int n,q,B[1000010];//数组开大亿点点

int main() {
	scanf("%d %d",&n,&q);
	scanf("%d",B+1);
	if(n>=2) {
		int temp=B[1],a=0;
		for(int i=2;i<=n;++i) {
			scanf("%d",&a);
			//B数组初始化
			B[i]=a-temp;
			temp=a;
		}
	}
	int l=0,r=0,c=0;
	while(q--) {
		scanf("%d %d %d",&l,&r,&c);
		B[l]+=c;
		//r+1未超过n可修改
		if(r<n) B[r+1]-=c;
	}
	long long sum=B[1];
	for(int i=2;i<=n;++i) {
		//输出前缀和
		printf("%lld ",sum);
		sum+=B[i];
	}
	printf("%lld\n",sum);
	return 0;
}

C-平衡字符串

题目描述

一个长度为 n 的字符串 s,其中仅包含 ‘Q’, ‘W’, ‘E’, ‘R’ 四种字符。
如果四种字符在字符串中出现次数均为 n/4,则其为一个平衡字符串。
现可以将 s 中连续的一段子串替换成相同长度的只包含那四个字符的任意字符串,使其变为一个平衡字符串,问替换子串的最小长度?
如果 s 已经平衡则输出0。

Input

一行字符表示给定的字符串s

Output

一个整数表示答案

Examples

Input
QWER
Output
0

Input
QQWE
Output
1

Input
QQQW
Output
2

Input
QQQQ
Output
3

Note

1<=n<=10^5

n是4的倍数

字符串中仅包含字符 'Q', 'W', 'E' 和 'R'.

解题思路

总结

首先我们分析这道题能不能使用尺取法(双指针法,遍历过程中两个相同方向进行扫描)。
一、我们最终的求解答案为一个连续的区间
二、区间左右端点移动有明确方向
(1)当前[L,R]满足要求,L++(选最短)
(2)当前[L,R]不满足要求,R++(扩大范围)

根据以上两条性质本题可以用尺取法求解!

思路

接下来的问题就是如何判断给定的[L,R]是否满足要求了。
(1)用sum[1],sum[2],sum[3],sum[4]分别记录在[L,R]之外,四类字符的个数。(这里通过map将四个字符映射为数字,其出现的次数作为sum数组中数字对应元素的值)
(2)先通过在[L,R]内进行替换,使四类字符数量一致,具体的做法是令MAX=max(max(sum[1],sum[2]),max(sum[3],sum[4])),用[L,R]中的字符替换为sum<MAX的字符,来将这些字符补充到MAX个,[L,R]中原字符个数total为R-L+1,替换补充所需字符个数为(MAX-sum[1])+(MAX-sum[2])+(MAX-sum[3])+(MAX-sum[4])
(3)再判断[L,R]中剩余的空闲位置是否为4的倍数或0(是4的倍数就可以直接平均分配给四类字符,最终四类字符个数想到),令free=total-替换补充所给字符,判断是否free>=0&&free%4==0即可。
(4)满足则用更小的total更新ans,L++,左端点向右移动;否则R++,右端点向右移动。(移动时注意更新相应字符的sum)

代码

#include<iostream>
#include<cstdio>
#include<string>
#include<algorithm>
#include<map>

using namespace std;
string s;
int ans=100000,sum[5];
map<char,int> mp{
	{'Q',1},{'W',2},{'E',3},{'R',4}
};

int main() {
	int l=0,r=0;
	cin>>s;
	for(int i=0;i<s.size();++i) 
		//记录四类字符个数
		sum[mp[s[i]]]++;
	if(sum[1]==sum[2]&&sum[2]==sum[3]&&sum[3]==sum[4]) {//已平衡
		printf("0\n");
		return 0;
	}
	sum[mp[s[0]]]--;
	while(1) {
		int MAX=max(max(sum[1],sum[2]),max(sum[3],sum[4])),total=r-l+1;
		int free=total-(MAX-sum[1])-(MAX-sum[2])-(MAX-sum[3])-(MAX-sum[4]);
		if(free>=0&&free%4==0) {
			//满足要求
			ans=min(total,ans);
			sum[mp[s[l]]]++;
			l++;
			if(l>r) break;//只需一个字符替换即为最短
		}else {//不满足
			r++;
			if(r==s.size()) break;//到达末尾字符串末尾,终止
			sum[mp[s[r]]]--;
		}
	}
	printf("%d\n",ans);
	return 0;
}

D-滑动窗口

题目描述

ZJM 有一个长度为 n 的数列和一个大小为 k 的窗口, 窗口可以在数列上来回移动. 现在 ZJM 想知道在窗口从左往右滑的时候,每次窗口内数的最大值和最小值分别是多少. 例如:
数列是 [1 3 -1 -3 5 3 6 7], 其中 k 等于 3.
在这里插入图片描述

Input

输入有两行。第一行两个整数n和k分别表示数列的长度和滑动窗口的大小,1<=k<=n<=1000000。第二行有n个整数表示ZJM的数列。

Output

输出有两行。第一行输出滑动窗口在从左到右的每个位置时,滑动窗口中的最小值。第二行是最大值。

Sample Input

8 3
1 3 -1 -3 5 3 6 7

Sample Output

-1 -3 -3 -3 3 3
3 3 5 5 6 7

解题思路

寻找窗口内的最值,是一个局部的概念,可以使用单调队列。
首先维护一个单调递增队列,对于每个窗口,队首元素即为当前窗口的最小值 。
维护单增队列的过程首先是维护当前窗口,若当前元素与队首元素的距离超过窗口长度k时,说明队首元素已不属于该窗口,将队首元素弹出即可。因为每次窗口只向前滑动一个单位,此时对内元素均属于该窗口。再维护队列的单增性,即弹出队内所有>=当前元素的,队首元素即为当前窗口的最小值,对每个窗口输出最小值即可。
最大值同理,维护一个单调递减队列即可。

总结

单调队列的维护过程与单调栈相似。
区别在于:

(1)单调栈只维护一端(栈顶),而单调队列可以维护两端(队首和队尾)
(2)单调栈通常维护全局的单调性(即不关心栈顶与栈底的实际距离),而单调队列通常维护局部的单调性(关注队尾和队首的实际距离,只维持部分元素单调)
(3)单调栈大小没有上限,而单调队列通常有大小限制(队尾和队首的实际距离)

由于单调队列可以队首出队以及前面的元素一定比后面的元素先入队的性质,使得它可以维护局部的单调性。
队首元素不在区间之内则出队。
时间复杂度与单调栈一直O(n)线性时间复杂度。

代码

#include<iostream>
#include<cstdio>
#include<deque>

using namespace std;

int n,k,a[1000010];
deque<int> q;

int main() {
	scanf("%d %d",&n,&k);
	for(int i=1;i<=n;++i)
		scanf("%d",a+i); 
	for(int i=1;i<=n;++i) {
		//维护当前窗口 
		if(!q.empty()&&(i-q.front())>=k) q.pop_front();
		//维护单调递增队列 
		while(!q.empty()&&a[i]<=a[q.back()])
			q.pop_back();
		q.push_back(i);
		if(i>=k) printf("%d ",a[q.front()]);//开始输出每个窗口最小值 
	}
	printf("\n");
	q.clear();
	for(int i=1;i<=n;++i) {
		//维护当前窗口
		if(!q.empty()&&(i-q.front())>=k) q.pop_front();
		//维护单调递减队列
		while(!q.empty()&&a[i]>=a[q.back()])
			q.pop_back();
		q.push_back(i);
		if(i>=k) printf("%d ",a[q.front()]); //开始输出每个窗口最大值 
	}
	printf("\n");
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值