253 lines
11 KiB
YAML
253 lines
11 KiB
YAML
title: 132 Pattern
|
||
slug: 132-pattern
|
||
difficulty: medium
|
||
leetcode_id: 456
|
||
leetcode_url: https://leetcode.com/problems/132-pattern/
|
||
categories:
|
||
- arrays
|
||
- stack
|
||
patterns:
|
||
- slug: monotonic-stack
|
||
is_optimal: true
|
||
|
||
function_signature: "def find132pattern(nums: list[int]) -> bool:"
|
||
|
||
test_cases:
|
||
visible:
|
||
- input: { nums: [1, 2, 3, 4] }
|
||
expected: false
|
||
- input: { nums: [3, 1, 4, 2] }
|
||
expected: true
|
||
- input: { nums: [-1, 3, 2, 0] }
|
||
expected: true
|
||
hidden:
|
||
- input: { nums: [1, 2] }
|
||
expected: false
|
||
- input: { nums: [3, 5, 0, 3, 4] }
|
||
expected: true
|
||
- input: { nums: [1, 0, 1, -4, -3] }
|
||
expected: false
|
||
- input: { nums: [1, 2, 2, 2] }
|
||
expected: false
|
||
- input: { nums: [-2, 1, 2, -2, 1, 2] }
|
||
expected: true
|
||
- input: { nums: [1, 3, 2, 4, 5, 6, 7, 8, 9, 10] }
|
||
expected: true
|
||
|
||
description: |
|
||
Given an array of `n` integers `nums`, a **132 pattern** is a subsequence of three integers `nums[i]`, `nums[j]` and `nums[k]` such that `i < j < k` and `nums[i] < nums[k] < nums[j]`.
|
||
|
||
Return `true` *if there is a **132 pattern** in* `nums`, *otherwise, return* `false`.
|
||
|
||
The "132" name comes from the value ordering: the 1<sup>st</sup> element is smallest, the 3<sup>rd</sup> element (middle index) is largest, and the 2<sup>nd</sup> element (last index) is in between.
|
||
|
||
constraints: |
|
||
- `n == nums.length`
|
||
- `1 <= n <= 2 * 10^5`
|
||
- `-10^9 <= nums[i] <= 10^9`
|
||
|
||
examples:
|
||
- input: "nums = [1,2,3,4]"
|
||
output: "false"
|
||
explanation: "There is no 132 pattern in the sequence. The array is strictly increasing, so we can never find nums[k] < nums[j] where k > j."
|
||
- input: "nums = [3,1,4,2]"
|
||
output: "true"
|
||
explanation: "There is a 132 pattern in the sequence: [1, 4, 2]. Here i=1, j=2, k=3, and nums[1]=1 < nums[3]=2 < nums[2]=4."
|
||
- input: "nums = [-1,3,2,0]"
|
||
output: "true"
|
||
explanation: "There are three 132 patterns in the sequence: [-1, 3, 2], [-1, 3, 0] and [-1, 2, 0]."
|
||
|
||
explanation:
|
||
intuition: |
|
||
Imagine you're looking for a specific "shape" in a sequence of numbers: a valley followed by a peak, with something in between on the right side.
|
||
|
||
The pattern we seek is **"small, big, medium"** in terms of values, appearing at indices **i < j < k**. Think of it like finding a mountain range where:
|
||
- Position `i` is in a valley (smallest value)
|
||
- Position `j` is at the peak (largest value)
|
||
- Position `k` is partway up the mountain (between the two)
|
||
|
||
The key insight is that if we scan from **right to left**, we can maintain candidates for the "2" (middle value, `nums[k]`) using a **monotonic stack**. As we pop elements from the stack, we're finding elements that could serve as valid `nums[k]` values — elements that are smaller than some `nums[j]` to their left.
|
||
|
||
By tracking the largest popped value (our best candidate for `nums[k]`), we just need to find any `nums[i]` to the left that's smaller than this candidate. If we find one, we've completed the 132 pattern!
|
||
|
||
approach: |
|
||
We solve this using a **Monotonic Stack (Right to Left)** approach:
|
||
|
||
**Step 1: Initialise tracking variables**
|
||
|
||
- `stack`: A monotonic decreasing stack to track potential `nums[j]` candidates
|
||
- `third`: The best candidate for `nums[k]` (the "2" in 132), initialised to negative infinity
|
||
|
||
|
||
|
||
**Step 2: Iterate from right to left**
|
||
|
||
- For each element `num` at position `i`, we check if it could be `nums[i]` (the "1" in 132)
|
||
- If `num < third`, we found a valid pattern! The current `num` is smaller than our `third` candidate, and `third` came from being popped by some larger element (which serves as `nums[j]`)
|
||
- Otherwise, maintain the monotonic stack:
|
||
- While `num` is greater than the stack top, pop elements
|
||
- Each popped element becomes a candidate for `third` (keep the maximum)
|
||
- Push `num` onto the stack
|
||
|
||
|
||
|
||
**Step 3: Return the result**
|
||
|
||
- If we found `num < third` at any point, return `true`
|
||
- If we finish the loop without finding a pattern, return `false`
|
||
|
||
|
||
|
||
Why does this work? When we pop an element from the stack, it means we found a larger element to its left (the current `num`). This popped element becomes a valid `third` because there exists some `nums[j] > nums[k]` with `j < k`. We then just need `nums[i] < nums[k]` with `i < j`.
|
||
|
||
common_pitfalls:
|
||
- title: The Brute Force Trap
|
||
description: |
|
||
A natural first approach is three nested loops checking all combinations of `i < j < k`:
|
||
|
||
```python
|
||
for i in range(n):
|
||
for j in range(i+1, n):
|
||
for k in range(j+1, n):
|
||
if nums[i] < nums[k] < nums[j]:
|
||
return True
|
||
```
|
||
|
||
This is **O(n³)** and with `n <= 2 * 10^5`, that's up to 8 × 10^15 operations — guaranteed **Time Limit Exceeded**.
|
||
wrong_approach: "Triple nested loops checking all triplets"
|
||
correct_approach: "Monotonic stack achieving O(n) time"
|
||
|
||
- title: Confusing the Index Order vs Value Order
|
||
description: |
|
||
The "132" refers to the **value ordering**, not the index ordering:
|
||
- Index order: `i < j < k` (always left to right)
|
||
- Value order: `nums[i] < nums[k] < nums[j]` (small, medium, big → positions 1, 3, 2)
|
||
|
||
A common mistake is looking for values in ascending order `nums[i] < nums[j] < nums[k]`, which would be a "123" pattern instead.
|
||
wrong_approach: "Looking for strictly increasing subsequence"
|
||
correct_approach: "Looking for small-big-medium pattern at indices i < j < k"
|
||
|
||
- title: Forgetting to Track the Maximum Popped Element
|
||
description: |
|
||
When using the monotonic stack approach, some implementations only check the most recently popped element. However, we need to track the **maximum** of all popped elements.
|
||
|
||
Consider `[1, 0, 1, -4, -3]`: we need to ensure our `third` candidate is the best possible one to maximise our chances of finding a valid `nums[i]`.
|
||
wrong_approach: "Only using the last popped element"
|
||
correct_approach: "Tracking the maximum of all popped elements as third"
|
||
|
||
- title: Handling Duplicate Values
|
||
description: |
|
||
The pattern requires **strict inequalities**: `nums[i] < nums[k] < nums[j]`. Equal values don't count.
|
||
|
||
For `[1, 2, 2, 2]`, there's no valid 132 pattern because we can't have `nums[k] < nums[j]` when they're equal.
|
||
wrong_approach: "Using <= comparisons"
|
||
correct_approach: "Using strict < comparisons throughout"
|
||
|
||
key_takeaways:
|
||
- "**Monotonic stack mastery**: This problem demonstrates how a monotonic stack can track relationships between elements in O(n) time"
|
||
- "**Right-to-left traversal**: Sometimes scanning backwards simplifies the logic — here it lets us build up `third` candidates before we need them"
|
||
- "**The popped element insight**: Elements popped from a monotonic stack have a guaranteed relationship with the element that caused the pop"
|
||
- "**Pattern recognition**: The 132 pattern appears in many variations (stock prices, sequences) — recognising this structure is valuable"
|
||
|
||
time_complexity: "O(n). Each element is pushed and popped from the stack at most once, giving us linear time."
|
||
space_complexity: "O(n). In the worst case (strictly decreasing array), the stack holds all n elements."
|
||
|
||
solutions:
|
||
- approach_name: Monotonic Stack (Right to Left)
|
||
is_optimal: true
|
||
code: |
|
||
def find132pattern(nums: list[int]) -> bool:
|
||
n = len(nums)
|
||
if n < 3:
|
||
return False
|
||
|
||
stack = [] # Monotonic decreasing stack for nums[j] candidates
|
||
third = float('-inf') # Best candidate for nums[k] (the "2" in 132)
|
||
|
||
# Scan from right to left
|
||
for i in range(n - 1, -1, -1):
|
||
# If current num is less than third, we found the pattern!
|
||
# Current num is nums[i], third is nums[k], and nums[j] exists
|
||
# (it's the element that popped third from the stack)
|
||
if nums[i] < third:
|
||
return True
|
||
|
||
# Maintain monotonic decreasing stack
|
||
# Pop smaller elements — they become candidates for third
|
||
while stack and nums[i] > stack[-1]:
|
||
third = stack.pop() # Update third to the largest popped value
|
||
|
||
# Push current element as potential nums[j]
|
||
stack.append(nums[i])
|
||
|
||
return False
|
||
explanation: |
|
||
**Time Complexity:** O(n) — Each element is pushed and popped at most once.
|
||
|
||
**Space Complexity:** O(n) — Stack can hold up to n elements.
|
||
|
||
The key insight is that when we pop elements from the stack, those elements are smaller than the current element (potential `nums[j]`), making them valid candidates for `nums[k]`. We track the largest such candidate in `third`. Then any element smaller than `third` that we encounter later completes the pattern.
|
||
|
||
- approach_name: Prefix Minimum with Stack
|
||
is_optimal: true
|
||
code: |
|
||
def find132pattern(nums: list[int]) -> bool:
|
||
n = len(nums)
|
||
if n < 3:
|
||
return False
|
||
|
||
# Precompute minimum from the left for each position
|
||
min_left = [0] * n
|
||
min_left[0] = nums[0]
|
||
for i in range(1, n):
|
||
min_left[i] = min(min_left[i - 1], nums[i])
|
||
|
||
# Use stack to find valid j, k pairs where min_left[j] < nums[k] < nums[j]
|
||
stack = [] # Stack of indices for potential nums[k]
|
||
|
||
# Scan from right to left
|
||
for j in range(n - 1, -1, -1):
|
||
# We need nums[i] < nums[k] < nums[j]
|
||
# min_left[j] gives us the minimum nums[i] to the left of j
|
||
if nums[j] > min_left[j]:
|
||
# Pop elements that are too small to be nums[k]
|
||
while stack and nums[stack[-1]] <= min_left[j]:
|
||
stack.pop()
|
||
|
||
# Check if top of stack is a valid nums[k]
|
||
if stack and nums[stack[-1]] < nums[j]:
|
||
return True
|
||
|
||
stack.append(j)
|
||
|
||
return False
|
||
explanation: |
|
||
**Time Complexity:** O(n) — Two passes: one for prefix minimum, one with stack.
|
||
|
||
**Space Complexity:** O(n) — For the prefix minimum array and stack.
|
||
|
||
This approach explicitly tracks the minimum to the left of each position. For each potential `nums[j]`, we know exactly what `nums[i]` would need to beat (`min_left[j]`). The stack maintains candidates for `nums[k]` that satisfy the constraints.
|
||
|
||
- approach_name: Brute Force
|
||
is_optimal: false
|
||
code: |
|
||
def find132pattern(nums: list[int]) -> bool:
|
||
n = len(nums)
|
||
|
||
for j in range(1, n - 1):
|
||
# Find minimum to the left of j
|
||
min_i = min(nums[:j])
|
||
|
||
# Check if any element to the right satisfies the pattern
|
||
for k in range(j + 1, n):
|
||
if min_i < nums[k] < nums[j]:
|
||
return True
|
||
|
||
return False
|
||
explanation: |
|
||
**Time Complexity:** O(n²) — For each j, we scan left for min and right for valid k.
|
||
|
||
**Space Complexity:** O(1) — No additional data structures.
|
||
|
||
This optimised brute force fixes `j` as the middle element, finds the minimum to its left, then searches for a valid `nums[k]` to its right. While better than O(n³), it's still too slow for large inputs and will TLE on LeetCode. Included to show the progression toward the optimal solution.
|