题目来源
https://www.luogu.com.cn/problem/P5788
题目背景
模板题,无背景。
题目描述
给出项数为 n 的整数数列 a1...n
定义函数 f(i) 代表数列中第 i 个元素之后第一个大于 ai的元素的下标,即 f(i)=min
i<j≤n,aj>ai{j}。若不存在,则 f(i)=0。
试求出 f(1...n)。
输入格式
第一行一个正整数 n。
第二行 n 个正整数 a1...n
输出格式
一行 n 个整数表示 f(1),f(2),...,f(n) 的值。
输入输出样例
输入 #1
5
1 4 2 3 5
输出 #1
2 5 4 5 0
说明/提示
【数据规模与约定】
对于 30% 的数据,n≤100;
对于 60% 的数据,n≤5×10^3;
对于 100% 的数据,1≤n≤3×10^6,1≤ai≤10^9。
算法分析
一、题目背景与核心需求
首先,我们来明确题目要求,这是解题的第一步。洛谷 P5788 的题目描述如下:
给定一个长度为 n
的数组
a
,请你找到每个元素右边第一个比它大的元素的值,如果不存在则输出
0
。
输入格式:第一行输入一个整数
n
(
1≤n≤3×10
6
),第二行输入
n
个整数,表示数组
a
。
输出格式:输出一行,包含
n
个整数,依次表示每个元素右边第一个比它大的元素的值。
从题目中我们能提炼出两个关键信息:
问题本质:对于数组中的每个元素
a[i]
,寻找其右侧第一个大于
a[i]
的元素,记为
ans[i]
;若没有,则
ans[i]=0
。
数据规模:
n
最大可达
3×10
6
,这意味着我们必须设计时间复杂度为
O(n)
的算法 ------ 如果使用暴力枚举(对每个元素遍历其右侧所有元素),时间复杂度为
O(n
2
)
,在
n=3e6
时会严重超时,因此需要更高效的解法,而单调栈正是解决这类问题的 "利器"。
二、单调栈原理:什么是单调栈?
在讲解解题思路前,我们先明确 "单调栈" 的定义。单调栈是一种特殊的栈结构,其核心特性是:栈内的元素始终保持严格的单调性(递增或递减)。根据单调性的不同,单调栈可分为 "单调递增栈" 和 "单调递减栈":
单调递增栈:栈内元素从栈底到栈顶依次递增(即每次入栈的元素必须大于栈顶元素,若不满足则弹出栈顶元素,直到满足条件或栈为空)。
单调递减栈:栈内元素从栈底到栈顶依次递减(同理,每次入栈的元素必须小于栈顶元素,否则弹出栈顶元素)。
单调栈的优势在于,它能在线性时间内处理数组中 "Next Greater Element""Next Smaller Element" 等问题,其本质是通过 "维护栈的单调性",避免了对元素的重复遍历,将暴力解法的
O(n
2
)
时间复杂度优化到
O(n)
。
三、解题思路:如何用单调栈解决本题?
回到洛谷 P5788,我们需要寻找每个元素 "右边第一个更大的元素",此时应选择单调递减栈,原因如下:
单调递减栈中,栈顶元素是当前未找到 "右边更大元素" 的元素中最小的一个。当遇到一个新元素
a[i]
时,如果
a[i]
大于栈顶元素,说明
a[i]
就是栈顶元素的 "右边第一个更大元素";此时弹出栈顶元素,并记录答案,直到栈顶元素大于
a[i]
或栈为空,再将
a[i]
入栈。
这样一来,每个元素只会入栈一次、出栈一次,整个过程的时间复杂度为
O(n)
,完全能满足
n=3e6
的数据规模要求。
具体解题步骤可拆解为以下四步:
初始化:创建一个空栈(用于存储未找到 "右边更大元素" 的元素下标,而非元素值,这样便于后续记录答案),创建一个长度为
n
的答案数组
ans
,初始值均为
0
(对应 "没有更大元素" 的情况)。
遍历数组:从数组的第一个元素到最后一个元素,依次处理每个元素
a[i]
(
i
为当前元素下标)。
维护栈的单调性:
当栈不为空,且当前元素
a[i]
大于栈顶下标对应的元素
a[stack.top()]
时:
弹出栈顶下标
top
,此时
a[i]
就是
a[top]
的 "右边第一个更大元素",因此令
ans[top]=a[i]
。
重复此过程,直到栈为空或
a[i]≤a[stack.top()]
。
将当前元素的下标
i
入栈。
输出答案:遍历结束后,答案数组
ans
中存储的就是每个元素对应的结果,直接输出即可。
举个例子:直观理解过程
为了让大家更清晰地掌握步骤,我们以一个小规模数组为例:
a=[3,1,2,4]
,
n=4
。
初始化:栈为空,
ans=[0,0,0,0]
。
处理
i=0
(
a[0]=3
):栈为空,直接入栈,栈:
0
。
处理
i=1
(
a[1]=1
):
a[1]≤a[0]
(1≤3),栈保持递减,入栈,栈:
0,1
。
处理
i=2
(
a[2]=2
):
栈顶下标为 1,
a[2]=2>a[1]=1
,弹出 1,令
ans[1]=2
;
此时栈顶下标为 0,
a[2]=2≤a[0]=3
,停止弹出,将 2 入栈,栈:
0,2
。
处理
i=3
(
a[3]=4
):
栈顶下标为 2,
a[3]=4>a[2]=2
,弹出 2,令
ans[2]=4
;
栈顶下标为 0,
a[3]=4>a[0]=3
,弹出 0,令
ans[0]=4
;
栈为空,将 3 入栈,栈:
3
。
遍历结束:栈中剩余下标 3(
a[3]=4
),其右侧无元素,
ans[3]
保持 0。
最终答案:
ans=[4,2,4,0]
,与预期结果一致。
四、代码实现:从基础版到优化版
理解了思路后,我们来编写代码。需要注意的是,由于
n
最大为
3e6
,在 C++ 中需注意输入输出效率(使用 scanf/printf 而非 cin/cout,否则可能超时),同时栈的实现可直接使用 STL 的 stack,但为了进一步提升效率,也可使用数组模拟栈(减少 STL 的开销)。
Code
cpp
#include <bits/stdc++.h>
using namespace std;
const int maxn=3e6+5;
int idx[maxn], a[maxn];
int stk[maxn];
int n,top;
int main() {
cin>>n;
for(int i=1; i<=n; i++) cin>>a[i];
for(int i=n;i>=1;i--) {
while(top && a[stk[top]]<=a[i]) {
top--;
}
if(top)idx[i]=stk[top];
else idx[i]=0;
stk[++top]=i;
}
for(int i=1; i<=n; i++) cout<<idx[i]<<" ";
return 0;
}