蓝桥杯每日一题----单调栈和单调队列

单调栈和单调队列

单调栈

单调栈即栈内的元素是单调递减或者单调递增的,我们通过一个题目来理解。

单调栈模板题

题目描述

给出项数为 n 的整数数列 a 1 … a n a_1…a_n a1an

定义函数 f ( i ) f(i) f(i)代表数列中第 i 个元素之后第一个大于 a i a_i ai 的元素的下标,即 f ( i ) = m i n i < j < = n , a j > a i j f(i)=min_{i<j<=n,a_j>a_i}{j} f(i)=mini<j<=n,aj>aij。若不存在,则 f ( i ) = 0 f(i)=0 f(i)=0

试求出 f ( 1 … n ) f(1…n) f(1n)

输入格式

第一行一个正整数 n

第二行 n 个正整数 a 1 … a n a_1…a_n a1an

输出格式

一行n个整数表示 f ( 1 ) , f ( 2 ) , … , f ( n ) f(1),f(2),…,f(n) f(1),f(2),,f(n)的值。

题目分析

题目要求第i个数后面第一个比 a [ i ] a[i] a[i]大的数。首先从头遍历数组,将第一个元素入栈,接着遍历下一个元素,假设当前入栈的元素下标为i,当前遍历到了 i + 1 i+1 i+1,如果 a [ i ] > a [ i + 1 ] a[i]>a[i+1] a[i]>a[i+1],那么 a [ i + 1 ] a[i+1] a[i+1]就不入栈接着向后遍历,如果 a [ i ] < a i + 1 a[i]<ai+1 a[i]<ai+1,那么 a [ i + 1 ] a[i+1] a[i+1]入栈,也就是说栈中的元素是单调递增的。入栈入的是数组的下标,那么在遍历数组的过程中第 i i i个数后面第一个比 a [ i ] a[i] a[i]大的数也就是遍历到 i i i后,后面第一个入栈的元素对应的值。但是什么时候会有第一个元素入栈,这个不好判断。

换个思路,假设我们是找 i i i左边第一个比 i i i小的数字,从头开始遍历数组,将第一个元素入栈,接着遍历下一个元素,假设当前入栈的元素下标为 i i i,当前遍历到了 i + 1 i+1 i+1,如果 a [ i + 1 ] > = a [ i ] a[i+1]>=a[i] a[i+1]>=a[i],则将 a [ i ] a[i] a[i]从栈中弹出,因为 a [ i + 1 ] a[i+1] a[i+1] a [ i ] a[i] a[i]的右边,对于求左边第一个比 a [ i + 2 ] a[i+2] a[i+2]大的数字,如果 a [ i ] > a [ i + 2 ] a[i]>a[i+2] a[i]>a[i+2],必然会有 a [ i + 1 ] > a [ i + 2 ] a[i+1]>a[i+2] a[i+1]>a[i+2],而 a [ i + 1 ] a[i+1] a[i+1]从左边更靠近 a [ i + 2 ] a[i+2] a[i+2],所以它会成为答案,也就是只要 a [ i + 1 ] a[i+1] a[i+1]在这里, a [ i ] a[i] a[i]永远不会成为答案,所以直接弹出就行了。那么最后栈顶的元素要么为空要么大于 a [ i + 1 ] a[i+1] a[i+1],并且是从 a [ i + 1 ] a[i+1] a[i+1]往左数第一个大于 a [ i + 1 ] a[i+1] a[i+1]的数字,所以栈顶元素即为我们想要的答案。

上述思路用一个图来表示,1,2,3,4,5表示的是数组下标,矩形的高度表示的是数组中值的大小,值越大,矩形越高。

当遍历到3的时候,2比3小,所以要从栈里面弹出,后续遍历到4,即便2要比4大,但是因为3的存在只要2符合条件3一定符合条件,但是3要比2更靠近4,也就是更靠近后面的数,所以3永远不会成为答案,把3弹出栈没有问题。遍历到5的时候,虽然3要比4大,也比5大,但是4比5大且更靠近5,所以4是答案。而如果5大于4小于3时,3又会成为答案,所以此时无论是4还是3都有可能成为答案,所以他们留在栈中。

换个形象点的方式,就是站在4上向左看,比3小的数字一定会被3挡住,所以从栈中拿掉就可以了,比如站在4中向左看,只能看到1和3,此时栈中也就只剩下了1和3。站在5中向左看,可以看见1,3,4。

刚刚讲的是求一个数字左边第一小的数字,但是题目要求的是右边第一小的数字,那么只需要对原数组倒序遍历就是求左边第一小的数字了。

题目代码

package 单调队列和单调栈;
import java.util.Scanner;
import java.util.Stack;
public class 单调栈模板 {
public static void main(String[] args) {
	Scanner scanner = new Scanner(System.in);
	int n = scanner.nextInt();
	int a[] = new int[n+1];
	int f[] = new int[n+1];
	for(int i = 1;i <= n;i++) a[i] = scanner.nextInt();
	Stack<Integer> stack = new Stack<Integer>();
	for(int i = n;i > 0;i--) {
		while(!stack.isEmpty()&&a[stack.peek()]<=a[i]) stack.pop();
		if(!stack.isEmpty())
		       f[i] = stack.peek();
		stack.push(i);
	}
	for(int i = 1;i <= n;i++)
	     System.out.print(f[i] + " ");
}
}

ps:感觉洛谷对非c++不太友好,很容易超时或者超内存,这个代码就有几个样例超内存了。

单调队列实现滑动窗口

单调栈值从栈顶弹元素和进元素,单调队列从队头弹元素,从队尾进元素。

单调队列模板

题目描述

有一个长为 n 的序列 a,以及一个大小为 k 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。

例如,对于序列 [ 1 , 3 , − 1 , − 3 , 5 , 3 , 6 , 7 ] [1,3,−1,−3,5,3,6,7] [1,3,1,3,5,3,6,7]以及 k=3,有如下过程:

输入格式

输入一共有两行,第一行有两个正整数 n,k。 第二行 n 个整数,表示序列 a

输出格式

输出共两行,第一行为每次窗口滑动的最小值
第二行为每次窗口滑动的最大值

题目分析

用队列来模拟一下滑动窗口的过程。两个判断

第一个判断是否还在窗口内,因为随着窗口的滑动必然会有一些数字已经离开了窗口,这个时候就要从队列里面删掉。

第二个判断是否有无效数字。对于求最大值的队列来说,假设窗口内的数字是[1,2,1],那么第一个数字1可以提早删掉,因为此时的2是窗口内的最大值并且只要第一个1没有被移出窗口那么2必然也还在,也就是第一个1受2的“压制”永远不会成为窗口内的最大值,直接删掉就可以了。变成了[2,1],那么此时剩下的这个1需要删掉吗?不能删掉,因为数字2会比1先离开窗口,当2离开窗口后,1仍然有机会成为最大值。也就是说这里存的应该是一个单调递减序列,当前待入队数字比队尾数字大,队尾数字就出队。

那么最大值在哪?单调递减序列,最大值自然是第一个数字也就是队头元素。

同样队列里面存的是数组的下标,因为我可以通过数组下标快速判断数字是否还在窗口内,假设当前数字下标为5,窗口大小为3,那么下标小于等于5-3=2的直接出队。

求最小值也是同样的分析方式。

题目代码

package 单调队列和单调栈;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Scanner;

public class 单调队列模板 {
public static void main(String[] args) throws IOException {
	Scanner scanner = new Scanner(System.in);
	BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
	String[] strings = br.readLine().split(" ");
	int n = Integer.parseInt(strings[0]);
	int k = Integer.parseInt(strings[1]);
	int a[] = new int[n+1];
	int max[] = new int[n+1];
	int min[] = new int[n+1];
	strings = br.readLine().split(" ");
	for(int i = 1;i <= n;i++) a[i] = Integer.parseInt(strings[i-1]);
	Deque<Integer> qDeque = new ArrayDeque<Integer>();
	for(int i = 1;i <= n;i++) {
		while(!qDeque.isEmpty()&&i-qDeque.peekFirst()>=k) {
			qDeque.pollFirst();
		}
		while(!qDeque.isEmpty()&&a[qDeque.peekLast()]<=a[i]) {
			qDeque.pollLast();
		}
		qDeque.addLast(i);
		max[i] = a[qDeque.peekFirst()];
	}
	qDeque.clear();
	for(int i = 1;i <= n;i++) {
		while(!qDeque.isEmpty()&&i-qDeque.peekFirst()>=k) qDeque.pollFirst();
		while(!qDeque.isEmpty()&&a[qDeque.peekLast()]>=a[i]) qDeque.pollLast();
		qDeque.addLast(i);
		if(i>=k)
		min[i] = a[qDeque.peekFirst()];
	}
	for(int i = k;i <= n;i++)
		 System.out.print(min[i] + " ");
	System.out.println();
	for(int i = k;i <= n;i++)
		 System.out.print(max[i] + " ");
}
}

ps:洛谷提交会超时

  • 19
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值