题目描述
给定一个整数序列 $a_1, a_2, …, a_n$,132
模式是 $a$ 的子序列 $a_i, a_j, a_k$,满足 $i < j < k$,并且 $a_i < a_k < a_j$。设计一个算法,读入 $n$ 个整数的数字序列,判断是否存在 132
模式。
样例
输入:[1, 2, 3, 4]
输出:False
解释:序列中没有132模式。
输入:[3, 1, 4, 2]
输出:True
解释:序列中的132模式为: [1, 4, 2]。
输入:[-1, 3, 2, 0]
输出:True
解释:序列中有三个132模式: [-1, 3, 2], [-1, 3, 0] 和 [-1, 2, 0]。
说明
n
不超过15000
。
算法1
(离散化 + 树状数组) $O(n \log n)$
- 首先将原数组元素离散化到区间
[1, n]
中,并将除了首元素外每个数字的值加入到树状数组的对应位置上,即对所有 $i>0$ 执行update(a[i], 1)
。 - 定义
left_min
表示数组前i
个元素的最小值,初始令left_min=nums[0]
。 - 从位置 1 开始枚举
高峰
的位置,然后需要用树状数组寻找 i 之后是否有元素在[left_min+1, nums[i]-1]
中。枚举时,首先将nums[i]
从树状数组中删除,然后判断num[i]
是否小于left_min
,若为真,则更新left_min
,继续枚举下一个位置;若为假,判断query(nums[i] - 1) - query(left_max) > 0
,查看找到了132
模式。
时间复杂度
- 离散化时间复杂度为 $O(n \log n)$,树状数组每次操作时间复杂度为 $O(\log n)$,故总时间复杂度为 $O(n \log n)$。
空间复杂度
- 需要额外 $O(n)$ 的空间存储离散化数组和树状数组。
C++ 代码
class Solution {
public:
vector<int> f;
int m;
void update(int x, int y) {
for (; x <= m; x += x & -x)
f[x] += y;
}
int query(int x) {
int tot = 0;
for (; x; x -= x & -x)
tot += f[x];
return tot;
}
bool find132pattern(vector<int>& nums) {
int n = nums.size();
if (n < 3)
return false;
vector<int> b(nums);
sort(b.begin(), b.end());
m = unique(b.begin(), b.end()) - b.begin();
b.resize(m);
f = vector<int>(m + 1, 0);
for (auto &x : nums)
x = lower_bound(b.begin(), b.end(), x) - b.begin() + 1;
for (int i = 1; i < n; i++)
update(nums[i], 1);
int left_min = nums[0];
for (int i = 1; i < n; i++) {
update(nums[i], -1);
if (nums[i] < left_min)
left_min = nums[i];
else {
if (query(nums[i] - 1) - query(left_min) > 0)
return true;
}
}
return false;
}
};
算法2
(单调栈) $O(n)$
- 算法 1 中时间效率的瓶颈在查找 i 之后是否有数字位于
[left_min+1, nums[i]-1]
。这一步可以利用单调栈来处理。首先我们可以发现,在从左往右遍历最高峰时,left_min
是单调递减的,所以nums[i]
右边的数如果可以以nums[i]
左边的数为高峰,构成132
模式,则一定可以以nums[i]
为高峰构成132
模式。 - 从数组末尾开始维护一个单调栈,如果发现
nums[i]
比栈顶元素严格大,则栈顶元素出栈,出栈时,记录下right_min[i]
为栈顶元素。不断迭代出栈直到栈空或nums[i]
不再比栈顶元素严格大。此时right_min[i]
记录了右边第一个比它大的元素之前,比它小的最大的数(比较绕,大家可以仔细斟酌)。这里有一点需要注意一下,假设右边第一个比nums[i]
大的数是nums[k]
,则nums[k]
右边的数不需要考虑,因为我们会在枚举到nums[k]
时考虑它们。 - 从数组开头开始枚举
高峰
位置,同时维护left_min
表示从数组开头到高峰
位置中的最小元素,判断left_min < right_min[i] && right_min[i] < nums[i]
成立则找到了132
模式。
时间复杂度
- 维护单调栈是每个数字最多进栈一次,出栈一次,剩余部分都是在线性遍历数组,故总时间复杂度为 $O(n)$。
空间复杂度
- 需要额外 $O(n)$ 的空间存储
right_min
和单调栈。
C++ 代码
class Solution {
public:
bool find132pattern(vector<int>& nums) {
int n = nums.size();
if (n < 3)
return false;
vector<int> right_min(n, INT_MAX);
stack<int> st;
for (int i = n - 1; i >= 1; i--) {
while (!st.empty() && nums[i] > nums[st.top()]) {
right_min[i] = nums[st.top()];
st.pop();
}
st.push(i);
}
int left_min = nums[0];
for (int i = 1; i < n; i++) {
if (nums[i] < left_min)
left_min = nums[i];
else {
if (left_min < right_min[i] && right_min[i] < nums[i])
return true;
}
}
return false;
}
};
感谢 wls ,法二的第二点讲的太棒了 Orz
栈比 树状数组的难想一些
FYI
class Solution {
public:
bool find132pattern(vector[HTML_REMOVED]& nums) {
int third = INT_MIN;
stack[HTML_REMOVED] st;
for (int i = nums.size() - 1; i >= 0; –i) {
if (nums[i] < third) return true;
while (!st.empty() && nums[i] > st.top()) {
third = st.top(); st.pop();
}
st.push(nums[i]);
}
return false;
}
};
非常清晰!…怎么想到的....
好评!
请教一下大神如果是要找出所有的132pattern呢?该如何去思考比暴力枚举好一些的法子哇
132pattern的个数可能达到 $O(n^3)$ 的级别,如果要输出所有这样的132pattern,任何算法都和暴力枚举一样。
如果只需要输出个数,一种 $O(n^2\log n)$ 的做法和本题中的树状数组做法基本一致,其他复杂度更低的方法可能也需要用到高级数据结构
原来提交评论还会带有VIP式闪动hhhhh
@wzc1995 单调栈找nums[i]右边,比nums[i]小的最大的那个数:倒着遍历数组,维护一个单调栈,如果当前数比栈顶的大,则出栈,否则入栈,在出栈过程中最后出栈那个应该就是比当前数小的最大的那个吧。也就是在出栈过程中都和当前数左边最小的比较一下就可以了。出栈的过程中只要有一次比当前数左边的最小的大即可(也不一定是比当前数小的最大的那个)
真的好难想,感觉木有办法举一反三
这个题,单调栈核心思想就是找 nums[i] 右边,比 nums[i] 小的数中最大的那个数。
但这个用单调栈实现不了,所以将问题弱化,假设存在最小的 j > i 使得 nums[j] >= nums[i],我们在[i + 1, j - 1]中寻找最大的数 nums[k] 作为 right_min[i],一定满足 nums[k] < nums[i]。
可以将问题弱化的原因在题解中也已经谈到。
感觉这个题很难想哇。怎么就想到要从后面走呢?