274 lines
12 KiB
YAML
274 lines
12 KiB
YAML
title: Find in Mountain Array
|
|
slug: find-in-mountain-array
|
|
difficulty: hard
|
|
leetcode_id: 1095
|
|
leetcode_url: https://leetcode.com/problems/find-in-mountain-array/
|
|
categories:
|
|
- arrays
|
|
- binary-search
|
|
patterns:
|
|
- slug: binary-search
|
|
is_optimal: true
|
|
|
|
function_signature: "def find_in_mountain_array(target: int, mountain_arr: MountainArray) -> int:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { mountain_arr: [1, 2, 3, 4, 5, 3, 1], target: 3 }
|
|
expected: 2
|
|
- input: { mountain_arr: [0, 1, 2, 4, 2, 1], target: 3 }
|
|
expected: -1
|
|
hidden:
|
|
- input: { mountain_arr: [1, 2, 3, 4, 5, 3, 1], target: 1 }
|
|
expected: 0
|
|
- input: { mountain_arr: [1, 2, 3, 4, 5, 3, 1], target: 5 }
|
|
expected: 4
|
|
- input: { mountain_arr: [1, 5, 2], target: 2 }
|
|
expected: 2
|
|
- input: { mountain_arr: [1, 5, 2], target: 1 }
|
|
expected: 0
|
|
- input: { mountain_arr: [1, 5, 2], target: 5 }
|
|
expected: 1
|
|
- input: { mountain_arr: [1, 2, 3, 4, 3, 2, 1], target: 0 }
|
|
expected: -1
|
|
- input: { mountain_arr: [0, 5, 3, 1], target: 0 }
|
|
expected: 0
|
|
|
|
description: |
|
|
*(This problem is an **interactive problem**.)*
|
|
|
|
You may recall that an array `arr` is a **mountain array** if and only if:
|
|
|
|
- `arr.length >= 3`
|
|
- There exists some `i` with `0 < i < arr.length - 1` such that:
|
|
- `arr[0] < arr[1] < ... < arr[i - 1] < arr[i]`
|
|
- `arr[i] > arr[i + 1] > ... > arr[arr.length - 1]`
|
|
|
|
Given a mountain array `mountainArr`, return the **minimum** index such that `mountainArr.get(index) == target`. If such an index does not exist, return `-1`.
|
|
|
|
**You cannot access the mountain array directly.** You may only access the array using a `MountainArray` interface:
|
|
|
|
- `MountainArray.get(k)` returns the element of the array at index `k` (0-indexed).
|
|
- `MountainArray.length()` returns the length of the array.
|
|
|
|
Submissions making more than `100` calls to `MountainArray.get` will be judged *Wrong Answer*. Also, any solutions that attempt to circumvent the judge will result in disqualification.
|
|
|
|
constraints: |
|
|
- `3 <= mountainArr.length() <= 10^4`
|
|
- `0 <= target <= 10^9`
|
|
- `0 <= mountainArr.get(index) <= 10^9`
|
|
|
|
examples:
|
|
- input: "mountainArr = [1,2,3,4,5,3,1], target = 3"
|
|
output: "2"
|
|
explanation: "3 exists in the array, at index=2 and index=5. Return the minimum index, which is 2."
|
|
- input: "mountainArr = [0,1,2,4,2,1], target = 3"
|
|
output: "-1"
|
|
explanation: "3 does not exist in the array, so we return -1."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Imagine a mountain with a single peak. You're standing at the base and need to find a specific elevation marker — but you can only check the elevation at a limited number of points (100 checks maximum).
|
|
|
|
The key insight is that a mountain array is actually **two sorted arrays joined at the peak**: the left side is strictly increasing, and the right side is strictly decreasing. This structure is perfect for binary search!
|
|
|
|
Think of it like this: if we can find the peak, we've essentially split the problem into two simpler binary searches:
|
|
1. Search the ascending left side (standard binary search)
|
|
2. If not found, search the descending right side (reversed binary search)
|
|
|
|
But here's the crucial detail: we want the **minimum index**. Since the left side has smaller indices than the right side, we should search the left side first. If we find the target there, we're done — no need to check the right side.
|
|
|
|
The challenge is that each `get()` call is expensive (limited to 100 total), so we must use binary search for all three operations: finding the peak and searching both sides.
|
|
|
|
approach: |
|
|
We solve this using **Three Binary Searches**:
|
|
|
|
**Step 1: Find the peak index**
|
|
|
|
- Use binary search to locate the peak (maximum element)
|
|
- At each midpoint, compare `arr[mid]` with `arr[mid + 1]`
|
|
- If `arr[mid] < arr[mid + 1]`, we're on the ascending side — peak is to the right
|
|
- If `arr[mid] > arr[mid + 1]`, we're on the descending side or at the peak — search left
|
|
- When `left == right`, we've found the peak
|
|
|
|
|
|
|
|
**Step 2: Binary search the ascending (left) side**
|
|
|
|
- Search from index `0` to `peak` using standard binary search
|
|
- If `arr[mid] < target`, move right; if `arr[mid] > target`, move left
|
|
- If found, return immediately (this guarantees minimum index)
|
|
|
|
|
|
|
|
**Step 3: Binary search the descending (right) side**
|
|
|
|
- Only if not found on the left side
|
|
- Search from index `peak + 1` to `n - 1`
|
|
- Since this side is **decreasing**, the comparisons are reversed:
|
|
- If `arr[mid] > target`, move right (smaller values are to the right)
|
|
- If `arr[mid] < target`, move left
|
|
|
|
|
|
|
|
**Step 4: Return the result**
|
|
|
|
- If found on either side, return that index
|
|
- Otherwise, return `-1`
|
|
|
|
common_pitfalls:
|
|
- title: Exceeding the Call Limit
|
|
description: |
|
|
With at most `100` calls to `MountainArray.get()` and array length up to `10^4`, a linear scan is not an option.
|
|
|
|
Three binary searches use at most `3 * log2(10^4) ≈ 3 * 14 = 42` calls, well within the limit. But caching values you've already fetched can help reduce redundant calls further.
|
|
wrong_approach: "Linear scan or excessive get() calls"
|
|
correct_approach: "Three binary searches with O(log n) calls each"
|
|
|
|
- title: Forgetting to Search Left Side First
|
|
description: |
|
|
The problem asks for the **minimum index**. If the target appears on both the ascending and descending sides (like `3` in `[1,2,3,4,5,3,1]`), you must return the smaller index.
|
|
|
|
Always search the left (ascending) side first and return immediately if found. Only search the right side if the left search fails.
|
|
wrong_approach: "Searching right side first or both sides without priority"
|
|
correct_approach: "Search ascending side first, return immediately if found"
|
|
|
|
- title: Incorrect Binary Search Direction on Descending Side
|
|
description: |
|
|
The descending (right) side of the mountain is sorted in **reverse order**. Standard binary search logic must be inverted:
|
|
|
|
- In ascending order: `arr[mid] < target` means move right
|
|
- In descending order: `arr[mid] < target` means move **left** (larger values are to the left)
|
|
|
|
Mixing up these directions causes incorrect results.
|
|
wrong_approach: "Using same comparison logic for both sides"
|
|
correct_approach: "Invert comparisons for the descending side"
|
|
|
|
- title: Off-by-One in Peak Finding
|
|
description: |
|
|
When finding the peak, be careful with boundary conditions. The peak can never be at index `0` or `n-1` (by definition of mountain array), so initialize `left = 1` and `right = n - 2` for safety.
|
|
|
|
Also, when comparing `arr[mid]` with `arr[mid + 1]`, ensure `mid + 1` is within bounds.
|
|
|
|
key_takeaways:
|
|
- "**Decompose the problem**: A mountain array is two sorted subarrays — find the peak first, then binary search each half"
|
|
- "**Binary search on structure**: When data has a predictable structure (sorted, bitonic, rotated), binary search can dramatically reduce search time"
|
|
- "**Order matters for ties**: When finding minimum/maximum index, search the appropriate half first to short-circuit early"
|
|
- "**Interactive problems**: Limited API calls force O(log n) solutions — linear scans are not acceptable"
|
|
|
|
time_complexity: "O(log n). Three binary searches, each taking O(log n) time in the worst case."
|
|
space_complexity: "O(1). We only use a constant number of variables for indices and bounds."
|
|
|
|
solutions:
|
|
- approach_name: Triple Binary Search
|
|
is_optimal: true
|
|
code: |
|
|
# MountainArray interface is provided by the judge:
|
|
# class MountainArray:
|
|
# def get(self, index: int) -> int: ...
|
|
# def length(self) -> int: ...
|
|
|
|
class Solution:
|
|
def findInMountainArray(self, target: int, mountain_arr: 'MountainArray') -> int:
|
|
n = mountain_arr.length()
|
|
|
|
# Step 1: Find the peak index using binary search
|
|
left, right = 0, n - 1
|
|
while left < right:
|
|
mid = (left + right) // 2
|
|
# If mid is less than mid+1, peak is to the right
|
|
if mountain_arr.get(mid) < mountain_arr.get(mid + 1):
|
|
left = mid + 1
|
|
else:
|
|
# Peak is at mid or to the left
|
|
right = mid
|
|
peak = left
|
|
|
|
# Step 2: Binary search on ascending (left) side [0, peak]
|
|
left, right = 0, peak
|
|
while left <= right:
|
|
mid = (left + right) // 2
|
|
val = mountain_arr.get(mid)
|
|
if val == target:
|
|
return mid # Found on left side = minimum index
|
|
elif val < target:
|
|
left = mid + 1
|
|
else:
|
|
right = mid - 1
|
|
|
|
# Step 3: Binary search on descending (right) side [peak+1, n-1]
|
|
left, right = peak + 1, n - 1
|
|
while left <= right:
|
|
mid = (left + right) // 2
|
|
val = mountain_arr.get(mid)
|
|
if val == target:
|
|
return mid
|
|
# Descending order: larger values on left, smaller on right
|
|
elif val > target:
|
|
left = mid + 1 # Move right to find smaller values
|
|
else:
|
|
right = mid - 1 # Move left to find larger values
|
|
|
|
return -1 # Target not found in either half
|
|
explanation: |
|
|
**Time Complexity:** O(log n) — Three binary searches, each O(log n).
|
|
|
|
**Space Complexity:** O(1) — Only constant extra space for variables.
|
|
|
|
We first locate the peak using binary search by comparing adjacent elements. Then we search the ascending left side with standard binary search. If not found, we search the descending right side with inverted comparisons. The total number of `get()` calls is at most `2 * log(n) + 2 * log(n) + 2 * log(n) ≈ 6 * log(10^4) ≈ 84`, well within the 100-call limit.
|
|
|
|
- approach_name: Triple Binary Search with Caching
|
|
is_optimal: false
|
|
code: |
|
|
class Solution:
|
|
def findInMountainArray(self, target: int, mountain_arr: 'MountainArray') -> int:
|
|
n = mountain_arr.length()
|
|
cache = {} # Cache to avoid redundant get() calls
|
|
|
|
def get(i: int) -> int:
|
|
if i not in cache:
|
|
cache[i] = mountain_arr.get(i)
|
|
return cache[i]
|
|
|
|
# Find peak
|
|
left, right = 0, n - 1
|
|
while left < right:
|
|
mid = (left + right) // 2
|
|
if get(mid) < get(mid + 1):
|
|
left = mid + 1
|
|
else:
|
|
right = mid
|
|
peak = left
|
|
|
|
# Search ascending side
|
|
left, right = 0, peak
|
|
while left <= right:
|
|
mid = (left + right) // 2
|
|
val = get(mid)
|
|
if val == target:
|
|
return mid
|
|
elif val < target:
|
|
left = mid + 1
|
|
else:
|
|
right = mid - 1
|
|
|
|
# Search descending side
|
|
left, right = peak + 1, n - 1
|
|
while left <= right:
|
|
mid = (left + right) // 2
|
|
val = get(mid)
|
|
if val == target:
|
|
return mid
|
|
elif val > target:
|
|
left = mid + 1
|
|
else:
|
|
right = mid - 1
|
|
|
|
return -1
|
|
explanation: |
|
|
**Time Complexity:** O(log n) — Same as the optimal solution.
|
|
|
|
**Space Complexity:** O(log n) — Cache stores at most O(log n) values.
|
|
|
|
This variation adds a cache dictionary to avoid redundant `get()` calls. While the asymptotic complexity is the same, caching can reduce the actual number of API calls when the same index is accessed multiple times (e.g., the peak index might be checked during both the peak-finding phase and the left-side search). This is a practical optimisation for interactive problems with strict call limits.
|