0
点赞
收藏
分享

微信扫一扫

详解为何使用「单调栈」来找最大的 K 是正确的|Java


题目描述

这是 LeetCode 上的 ​​456. 132 模式​​ ,难度为 中等

Tag : 「单调栈」

给你一个整数数组 nums ,数组中共有 n 个整数。132 模式的子序列 由三个整数 nums[i]、nums[j] 和 nums[k] 组成,并同时满足:i < j < k 和 nums[i] < nums[k] < nums[j] 。

如果 nums 中存在 132 模式的子序列 ,返回 true ;否则,返回 false 。

进阶:很容易想到时间复杂度为 的解决方案,你可以设计一个时间复杂度为 或 的解决方案吗?

示例 1:

输入:nums = [1,2,3,4]
输出:false
解释:序列中不存在 132 模式的子序列。

示例 2:

输入:nums = [3,1,4,2]
输出:true
解释:序列中有 1 个 132 模式的子序列: [1, 4, 2] 。

示例 3:

输入:nums = [-1,3,2,0]
输出:true
解释:序列中有 3 个 132 模式的的子序列:[-1, 3, 2]、[-1, 3, 0] 和 [-1, 2, 0] 。

提示:

  • n == nums.length
  • 1 <= n <=
  • -<= nums[i] <=

基本思路

朴素的做法是分别对三个数进行枚举,这样的做法是 的,数据范围是 ,稳稳超时。

事实上,这样的数据范围甚至不足以我们枚举其中两个数,然后优化找第三个数的 做法。

这时候根据数据范围会联想到树状数组,使用树状数组的复杂度是 的,可以过。但是代码量会较多一点,还需要理解离散化等前置知识。题解也不太好写。

因此,我们可以从 132 的大小特性去分析,如果在确定一个数之后,如何快速找到另外两个数(我们使用 ​​ijk​​ 来代指 132 结构):

  1. 枚举 ​​i​​:由于 ​​i​​ 是 132 结构中最小的数,那么相当于我们要从 i 后面,找到一个对数 ​​(j,k)​​,使得 ​​(j,k)​​ 都满足比 ​​i​​ 大,同时 ​​j​​ 和 ​​k​​ 之间存在 ​​j > k​​ 的关系。由于我们的遍历是单向的,因此我们可以将问题转化为找 ​​k​​,首先 ​​k​​ 需要比 ​​i​​ 大,同时在 ​​[i, k]​​ 之间存在比 ​​k​​ 大的数即可。
  2. 枚举 ​​j​​:由于 ​​j​​ 是 132 结构里最大的数,因此我们需要在 ​​j​​ 的右边中比 ​​j​​ 小的「最大」的数,在 ​​j​​ 的左边找比 ​​j​​ 小的「最小」的数。这很容易联想到单调栈,但是朴素的单调栈是帮助我们找到左边或者右边「最近」的数,无法直接满足我们「最大」和「最小」的要求,需要引入额外逻辑。
  3. 枚举 ​​k​​:由于 ​​k​​ 是 132 结构中的中间值,这里的分析逻辑和「枚举 i」类似,因为遍历是单向的,我们需要找到 ​​k​​ 左边的 ​​i​​,同时确保 ​​[i,k]​​ 之间存在比 ​​i​​ 和 ​​k​​ 大的数字。

以上三种分析方法都是可行的,但「枚举 i」的做法是最简单的。

因为如果存在 (j,k) 满足要求的话,我们只需要找到一个最大的满足条件的 ​k​,通过与 ​i​ 的比较即可。

也许你还不理解是什么意思。没关系,我们一边证明一边说。

过程 & 证明

先说处理过程吧,我们从后往前做,维护一个「单调递减」的栈,同时使用 ​​k​​​ 记录所有出栈元素的最大值(​​k​​ 代表满足 132 结构中的 2)。

那么当我们遍历到 ​​i​​​,只要满足发现满足 ​​nums[i] < k​​​,说明我们找到了符合条件的 ​​i j k​​。

举个????,对于样例数据 ​​[3, 1, 4, 2]​​​,我们知道满足 132 结构的子序列是 ​​[1, 4, 2]​​,其处理逻辑是(遍历从后往前):

  1. 枚举到 2:栈内元素为 [2],​​k​​ = INF
  2. 枚举到 4:不满足「单调递减」,2 出栈更新​​k​​​,4 入栈。栈内元素为 [4],​​k​​ = 2
  3. 枚举到 1:满足​​nums[i] < k​​​,说明对于​​i​​​ 而言,后面有一个比其大的元素(满足​​i < k​​​ 的条件),同时这个​​k​​​ 的来源又是因为维护「单调递减」而弹出导致被更新的(满足​​i​​​ 和​​k​​​ 之间,有比​​k​​ 要大的元素)。因此我们找到了满足 132 结构的组合。

这样做的本质是:我们通过维护「单调递减」来确保已经找到了有效的 (j,k)。换句话说如果 ​k​ 有值的话,那么必然是因为有 ​j > k​,导致的有值。也就是 132 结构中,我们找到了 32,剩下的 ​i​ (也就是 132 结构中的 1)则是通过遍历过程中与 ​k​ 的比较来找到。这样做的复杂度是 的,比树状数组还要快。

从过程上分析,是没有问题的。

搞清楚了处理过程,证明也变得十分简单。

我们不失一般性的考虑任意数组 ​​nums​​​,假如真实存在 ​​ijk​​​ 符合 132 的结构(这里的 ​​ijk​​​ 特指所有满足 132 结构要求的组合中 ​​k​​ 最大的那个组合)。

由于我们的比较逻辑只针对 ​​i​​​ 和 ​​k​​​,而 ​​i​​​ 是从后往前的处理的,必然会被遍历到;漏掉 ​​ijk​​​ 的情况只能是:在遍历到 ​​i​​​ 的时候,我们没有将 ​​k​​ 更新到变量中:

  1. 这时候变量的值要比真实情况下的​​k​​​ 要小,说明​​k​​​ 还在栈中,而遍历位置已经到达了​​i​​​,说明​​j​​​ 和​​k​​ 同时在栈中,与「单调递减」的性质冲突。
  2. 这时候变量的值要比真实情况下的​​k​​​ 要大,说明在​​k​​​ 出栈之后,有比​​k​​​ 更大的数值出栈了(同时必然有比变量更大的值在栈中),这时候要么与我们假设​​ijk​​​ 是​​k​​​ 最大的组合冲突;要么与我们遍历到的位置为​​i​​ 冲突。

综上,由于「单调递减」的性质,我们至少能找到「遍历过程中」所有符合条件的 ijk​k​ 最大的那个组合。

单调栈

代码:

class Solution {
public boolean find132pattern(int[] nums) {
int n = nums.length;
Deque<Integer> d = new ArrayDeque<>();
int k = Integer.MIN_VALUE;
for (int i = n - 1; i >= 0; i--) {
if (nums[i] < k) return true;
while (!d.isEmpty() && d.peekLast() < nums[i]) {
// 事实上,k 的变化也具有单调性,直接使用 k = pollLast() 也是可以的
k = Math.max(k, d.pollLast());
}
d.addLast(nums[i]);
}
return false;
}
}
  • 时间复杂度:
  • 空间复杂度:

最后

这是我们「刷穿 LeetCode」系列文章的第 ​​No.456​​ 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先将所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:​​github.com/SharingSour…​​

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

举报

相关推荐

0 条评论