Files
codetutor/backend/data/questions/132-pattern.yaml

252 lines
11 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
title: 132 Pattern
slug: 132-pattern
difficulty: medium
leetcode_id: 456
leetcode_url: https://leetcode.com/problems/132-pattern/
categories:
- arrays
- stack
patterns:
- monotonic-stack
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
&nbsp;
**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
&nbsp;
**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`
&nbsp;
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.