题目描述
题目分析
我觉得这道题应该是我做过最难的中等题之一了,这是昨天的每日一题,但是昨天用nlogn
的做法做出来以后在看题解,发现有些看不懂(觉得题解有点故弄玄虚)。然后今天中午又花了一点时间才搞懂,感觉从这道题里面学到一点东西。
刚拿到这道题的时候我没有什么思路,第一想法当然是n3n^3n3的暴力,但是显然是不行的,第二想法是,用线段树维护区间最大值,然后遍历1
和2
,这样的复杂度是n2lognn^2lognn2logn,虽然好了一点但仍然会超时。我自己没有想到枚举一个,然后通过数据结构求其他两个的思路,觉得这方面有点欠缺,即这种思路的转换,仿佛变量比较多就必须每一个都枚举才可以。
遍历3
,用红黑树维护2
看到题解从左往右枚举3
,然后用一个变量保存1
(左边的最小值),用一个红黑树(使用set
)保存右边所有的值,在里面查找比1
大比3
小的数。这样的做法很符合直觉。
我用这样的思路很快写了一个和题解差不多的代码:
O(nlogn)代码
class Solution {
public:bool find132pattern(vector<int>& nums) {using vector_size = vector<int>::size_type;vector_size n = nums.size();if (n < 3) return false;int left_min = nums[0];multiset<int> right_all(nums.begin() + 2, nums.end());for (vector_size i = 1; i < n - 1; ++i) {if (left_min < nums[i]) {auto iter = right_all.upper_bound(left_min);if (iter != right_all.end() && *iter < nums[i]) {return true;}}left_min = min(left_min, nums[i]);right_all.erase(right_all.find(nums[i + 1]));}return false;}
};
红黑树的查找和删除都是logn
的,真不错。
其他思路都统统使用了单调栈,其实单调栈本身的非常简单,就是一个栈,里面是有序的,如果一个新加入的元素破坏了顺序,就弹出,直到不破坏为止,再入栈。
遍历1
,用单调栈维护3
和2
这道题的神奇之处在于用了一个递减的单调栈,并用一个变量保存所有出栈元素的最大值,因为入栈、出栈操作的先后顺序导致这个出栈元素的最大值肯定比栈中某个元素小而且在其前面,因为是那个元素将它出栈的,而这样就得到了3
和2
,我们只要遍历到比2
小的1
即可。这种方法的复杂度应该是最低的。
O(n)代码
class Solution {
public:bool find132pattern(vector<int>& nums) {int n = nums.size();if (n < 3) return false;stack<int> stk; //单调栈int max_pop = INT_MIN; //弹出的元素的最大值for (int i = n - 1; i >= 0; --i) {if (max_pop != INT_MIN) {if (nums[i] < max_pop) return true;}while (!stk.empty() && nums[i] > stk.top()) {max_pop = max(max_pop, stk.top());stk.pop();}stk.push(nums[i]);}return false;}
};
刚才习惯性地使用vector<int>::size_type
类型,但是循环的时候却RE
,这是因为size_type
类型是一个unsigned
类型,在--
到-1
的时候会变成一个非常大的数字导致死循环。应该尽量还是用int
才好。
遍历3
,用单调栈维护2
,用前缀数组维护1
虽然同样是使用单调栈解决问题,但是用单调栈维护2
的思路则和上面完全不同。我在看题解的时候一度非常迷惑,但是想明白原理以后才发现原来两者之间没有什么关系。
上面已经提到了,上面的解法的精髓在于维护了所有弹出元素的最大值,通过入栈、出栈的逻辑顺序得到了数据的物理顺序。但是现在这种解法则是完全不一样的角度。
我们用一个前缀数组处理出任意位置左边的最小值,得到1
,用单调栈维护右边的比本身3
小的最大值,得到2
,通过判断1
和2
的大小来决定是否存在132
序列。
问题的难点在于如何用单调栈维护右边比当前遍历到的3
小的最大值,做法如下:将把当前值插入到单调栈中弹出的数据的最大值作为2
。
我认为难以理解的地方在于,把一些比较小的数组弹出了,怎么能够保证后面不会用到这些数字呢?
想要理解,可以用一个简单的数学归纳:
- 初始的时候栈中是有元素的
- 新插入的当前元素可能没有进行弹出操作,这意味着右边全都是比当前值大的数字
- 当第一次插入
x
发生弹出时,我们保存了弹出元素的最大值right_max
,并且和其左边最小值left_min
进行比较,如果right_max>left_min
,则出现132
序列,如果没有出现,则意味着x
左边全部元素大于等于right_max
,也就是说right_max
以及其他这次弹出的数据无论如何都不会成为2
了,因此可以丢弃。
如果理解了上面的话,代码就比较简单了:
O(nlogn)代码
class Solution {
public:bool find132pattern(vector<int>& nums) {int n = nums.size();if (n < 3) return false;stack<int> stk; //单调栈维护2int max_pop; //一次pop的最大值vector<int> left_min;left_min.reserve(nums.size());left_min.push_back(INT_MAX);for (int i = 1; i <n; ++i) {left_min[i] = min(left_min[i - 1], nums[i - 1]);}stk.push(nums[n - 1]);for (int i = n - 2; i >= 0; --i) {max_pop = INT_MIN;while (!stk.empty() && nums[i] > stk.top()) {max_pop = max(max_pop, stk.top());stk.pop();}if (left_min[i] < max_pop) return true;stk.push(nums[i]);}return false;}
};
时间复杂度应该是比第二种解法要高一个O(n)
项