一、前缀/后缀最大最小值数组是什么?
假设原始数组为:
a[1..n]
我们希望提前构造出如下数组:
数组名 | 定义(基于下标从 1 开始) |
---|---|
pre_max[i] | max(a[1], a[2], ..., a[i]) ,即前缀最大值 |
suf_max[i] | max(a[i], a[i+1], ..., a[n]) ,即后缀最大值 |
pre_min[i] | min(a[1], a[2], ..., a[i]) ,即前缀最小值 |
suf_min[i] | min(a[i], a[i+1], ..., a[n]) ,即后缀最小值 |
这种结构常用于:
- 快速查询任意区间的最值(配合 RMQ 或滑窗技巧);
- 判断局部性质(是否为最大/最小);
- 用于划分数组、处理某种单调性。
二、构建过程详细讲解
设数组 a[1..n]
,我们一步步构建 4 个数组。
1. 构造前缀最大值数组 pre_max
- 初值设为
pre_max[1] = a[1]
- 从前往后递推:
pre_max[i] = max(pre_max[i - 1], a[i])
2. 构造后缀最大值数组 suf_max
- 初值设为
suf_max[n] = a[n]
- 从后往前递推:
suf_max[i] = max(suf_max[i + 1], a[i])
3. 构造前缀最小值数组 pre_min
- 初值设为
pre_min[1] = a[1]
- 从前往后递推:
pre_min[i] = min(pre_min[i - 1], a[i])
4. 构造后缀最小值数组 suf_min
- 初值设为
suf_min[n] = a[n]
- 从后往前递推:
suf_min[i] = min(suf_min[i + 1], a[i])
三、完整 C++ 实现(下标从 1 开始)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
int n = 6;
// 数组下标从 1 开始,a[0] 不使用
vector<int> a = {0, 5, 3, 8, 6, 2, 7};
vector<int> pre_max(n + 1), suf_max(n + 2); // suf 多开一个防止越界
vector<int> pre_min(n + 1), suf_min(n + 2);
// 前缀最大值初始化
pre_max[1] = a[1];
for (int i = 2; i <= n; i++)
{
pre_max[i] = max(pre_max[i - 1], a[i]);
}
// 后缀最大值初始化
suf_max[n] = a[n];
for (int i = n - 1; i >= 1; i--)
{
suf_max[i] = max(suf_max[i + 1], a[i]);
}
// 前缀最小值初始化
pre_min[1] = a[1];
for (int i = 2; i <= n; i++) {
pre_min[i] = min(pre_min[i - 1], a[i]);
}
// 后缀最小值初始化
suf_min[n] = a[n];
for (int i = n - 1; i >= 1; i--)
{
suf_min[i] = min(suf_min[i + 1], a[i]);
}
// 输出展示
cout << "原数组: ";
for (int i = 1; i <= n; i++) cout << a[i] << " ";
cout << endl;
cout << "前缀最大值: ";
for (int i = 1; i <= n; i++) cout << pre_max[i] << " ";
cout << endl;
cout << "后缀最大值: ";
for (int i = 1; i <= n; i++) cout << suf_max[i] << " ";
cout << endl;
cout << "前缀最小值: ";
for (int i = 1; i <= n; i++) cout << pre_min[i] << " ";
cout << endl;
cout << "后缀最小值: ";
for (int i = 1; i <= n; i++) cout << suf_min[i] << " ";
cout << endl;
return 0;
}
四、输出样例解释
以数组 a = [5, 3, 8, 6, 2, 7]
为例:
原数组: 5 3 8 6 2 7
前缀最大值: 5 5 8 8 8 8
后缀最大值: 8 8 8 7 7 7
前缀最小值: 5 3 3 3 2 2
后缀最小值: 2 2 2 2 2 7
五、时间复杂度与空间复杂度
- 时间复杂度:O(n)O(n)O(n),每个数组构造只遍历一次;
- 空间复杂度:O(n)O(n)O(n),额外存储 4 个长度为 nnn 的数组。
六、典型应用场景
示例 1:区间极值查询的预处理基础
利用前缀最大/最小值和后缀最大/最小值数组,可快速求得某区间最大值或最小值(一般结合 RMQ 或稀疏表实现更灵活)。
示例:
- 查询区间 [1..i][1..i][1..i] 的最大值,只需 O(1)O(1)O(1):
int max_in_prefix = pre_max[i];
- 查询区间 [i..n][i..n][i..n] 的最大值:
int max_in_suffix = suf_max[i];
这样大大减少了暴力扫描的时间复杂度。
示例 2:判断某元素是否为局部极值(峰/谷点)
判断某个位置 iii 的元素是否为左侧所有元素的最大值且为右侧所有元素的最小值,常用前缀最大值和后缀最小值数组:
if (a[i] == pre_max[i] && a[i] == suf_min[i]) {
// a[i] 是局部极值
}
示例 3:配合二分做最优区间判断
若需要快速判断某一段区间 [l,r][l, r][l,r] 的最大值是否小于等于 KKK,可利用差分结构结合前缀最大值数组:
if (pre_max[r] - pre_max[l - 1] <= K) {
// 满足条件
}
注:此处判断条件依赖具体差分定义,实际问题中可能需配合区间最大值数据结构(如线段树、稀疏表)更准确。
示例 4:求分割点使数组分成两段满足单调条件
将数组划分成左右两部分,使左边最大值不大于右边最小值:
for (int i = 1; i < n; i++)
{
if (pre_max[i] <= suf_min[i + 1])
{
// i 是一个合法的分割点
}
}
此技巧常见于排序问题和划分问题中。
示例 5:区间约束判断与单调栈结合优化
前缀后缀极值数组常配合单调栈使用,以优化区间极值相关问题。
示例:单调栈求最近更大元素时,可以利用 pre_max
和 suf_max
快速限制搜索范围,减少重复计算。
示例 6:计算满足区间极值限制的区间数量或长度
通过前缀最大/最小和后缀最大/最小的预处理,可以快速统计满足极值条件的子区间数量,配合双指针或二分法加速计算。
示例:统计所有最大值小于某阈值的子区间数量。
七、小结
1.应用场景
应用场景 | 说明 |
---|---|
区间极值快速查询 | 利用前缀最大/最小值和后缀最大/最小值数组,实现 O(1)O(1)O(1) 级别的区间极值查询预处理,常配合 RMQ 或稀疏表使用。 |
判断局部峰谷点 | 判断某元素是否同时为左侧所有元素的最大值和右侧所有元素的最小值,用于发现局部极值、峰值或谷值。 |
数组划分、分割点搜索 | 寻找满足单调性(如左段最大值 ≤ 右段最小值)或极值约束的分割点,常用于排序块划分、区间划分问题。 |
优化双指针、单调栈算法 | 利用极值数组快速判断区间边界条件,减少双指针或单调栈中的重复扫描与计算,提高整体效率。 |
区间数量统计、限制条件判断 | 结合前缀/后缀极值数组,快速统计满足极值限制条件的子区间数量,辅助实现复杂区间计数和范围判定。 |
区间最值与动态规划状态转移 | 在某些动态规划问题中,使用极值数组维护区间状态,快速求解转移条件,提高 DP 求解效率。 |
结合线段树或树状数组等数据结构 | 极值数组作为辅助数组,与线段树、树状数组结合,实现复杂区间查询、修改操作的优化。 |
2.构建注意事项
结构 | 构建方向 | 初始位置 | 转移表达式 |
---|---|---|---|
pre_max | 从左往右 | pre_max[1] = a[1] | max(pre_max[i - 1], a[i]) |
suf_max | 从右往左 | suf_max[n] = a[n] | max(suf_max[i + 1], a[i]) |
pre_min | 从左往右 | pre_min[1] = a[1] | min(pre_min[i - 1], a[i]) |
suf_min | 从右往左 | suf_min[n] = a[n] | min(suf_min[i + 1], a[i]) |