1671. Minimum Number of Removals to Make Mountain Array
Problem Description
You need to find the minimum number of elements to remove from an array to make it a "mountain array".
A mountain array is defined as an array that:
- Has at least 3 elements
- Has a peak element at some index
i
(where0 < i < arr.length - 1
) - All elements before the peak are in strictly increasing order:
arr[0] < arr[1] < ... < arr[i-1] < arr[i]
- All elements after the peak are in strictly decreasing order:
arr[i] > arr[i+1] > ... > arr[arr.length-1]
Given an integer array nums
, you need to determine the minimum number of elements that must be removed to transform it into a valid mountain array.
The solution uses dynamic programming to find the longest mountain subsequence. It calculates:
left[i]
: the length of the longest increasing subsequence ending at positioni
right[i]
: the length of the longest decreasing subsequence starting at positioni
For each position i
that can serve as a peak (where left[i] > 1
and right[i] > 1
), the length of the mountain subsequence with peak at i
is left[i] + right[i] - 1
. The answer is the total length n
minus the maximum mountain subsequence length found.
Intuition
The key insight is to think about what we want to keep rather than what we want to remove. Instead of directly calculating removals, we can find the longest valid mountain subsequence and then subtract its length from the total array length.
A mountain has two parts: an ascending slope and a descending slope. Any valid mountain subsequence must have elements that form an increasing sequence up to a peak, then a decreasing sequence down from that peak.
For each potential peak position i
, we need to know:
- How long can the ascending part be if it ends at position
i
? - How long can the descending part be if it starts at position
i
?
This naturally leads us to the Longest Increasing Subsequence (LIS) problem. We can compute:
left[i]
: the LIS ending at positioni
(the ascending part)right[i]
: the Longest Decreasing Subsequence starting at positioni
(the descending part)
If position i
is a valid peak, it must have at least one element before it in the ascending part (left[i] > 1
) and at least one element after it in the descending part (right[i] > 1
).
The total length of a mountain with peak at i
would be left[i] + right[i] - 1
(we subtract 1 because the peak is counted in both sequences).
By finding the maximum possible mountain length across all valid peaks, we can determine the minimum removals needed: n - max(mountain_length)
.
Learn more about Greedy, Binary Search and Dynamic Programming patterns.
Solution Approach
The solution uses dynamic programming to compute the longest increasing and decreasing subsequences for each position.
Step 1: Initialize arrays
- Create
left[i]
array to store the length of the longest increasing subsequence ending at positioni
- Create
right[i]
array to store the length of the longest decreasing subsequence starting at positioni
- Initialize both arrays with 1s (each element is a subsequence of length 1 by itself)
Step 2: Compute longest increasing subsequences (left to right)
for i in range(1, n):
for j in range(i):
if nums[i] > nums[j]:
left[i] = max(left[i], left[j] + 1)
For each position i
, check all previous positions j < i
. If nums[i] > nums[j]
, we can extend the increasing subsequence ending at j
by including nums[i]
. Take the maximum length possible.
Step 3: Compute longest decreasing subsequences (right to left)
for i in range(n - 2, -1, -1):
for j in range(i + 1, n):
if nums[i] > nums[j]:
right[i] = max(right[i], right[j] + 1)
For each position i
, check all positions j > i
. If nums[i] > nums[j]
, we can extend the decreasing subsequence starting at j
by including nums[i]
at the beginning. Take the maximum length possible.
Step 4: Find the maximum mountain length
return n - max(a + b - 1 for a, b in zip(left, right) if a > 1 and b > 1)
For each position that can be a valid peak (left[i] > 1
and right[i] > 1
), calculate the mountain length as left[i] + right[i] - 1
. The minimum removals equal n
minus the maximum mountain length found.
The time complexity is O(n²)
due to the nested loops in computing the subsequences, and the space complexity is O(n)
for storing the left
and right
arrays.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with a concrete example: nums = [2, 1, 5, 6, 2, 3, 1]
Step 1: Initialize arrays
n = 7
left = [1, 1, 1, 1, 1, 1, 1]
(each element forms a subsequence of length 1)right = [1, 1, 1, 1, 1, 1, 1]
Step 2: Compute longest increasing subsequences (left array)
For i = 1
: nums[1]=1
- Check j=0: nums[1]=1 not > nums[0]=2, skip
left[1] = 1
For i = 2
: nums[2]=5
- Check j=0: nums[2]=5 > nums[0]=2, so left[2] = max(1, left[0]+1) = 2
- Check j=1: nums[2]=5 > nums[1]=1, so left[2] = max(2, left[1]+1) = 2
left[2] = 2
For i = 3
: nums[3]=6
- Check j=0: nums[3]=6 > nums[0]=2, so left[3] = max(1, left[0]+1) = 2
- Check j=1: nums[3]=6 > nums[1]=1, so left[3] = max(2, left[1]+1) = 2
- Check j=2: nums[3]=6 > nums[2]=5, so left[3] = max(2, left[2]+1) = 3
left[3] = 3
Continuing this process:
left[4] = 2
(can extend from nums[0]=2 or nums[1]=1)left[5] = 3
(best is 1→2→3 or 2→3)left[6] = 1
(1 is smallest, can't extend any sequence)
Final: left = [1, 1, 2, 3, 2, 3, 1]
Step 3: Compute longest decreasing subsequences (right array)
For i = 5
: nums[5]=3
- Check j=6: nums[5]=3 > nums[6]=1, so right[5] = max(1, right[6]+1) = 2
right[5] = 2
For i = 4
: nums[4]=2
- Check j=5: nums[4]=2 not > nums[5]=3, skip
- Check j=6: nums[4]=2 > nums[6]=1, so right[4] = max(1, right[6]+1) = 2
right[4] = 2
For i = 3
: nums[3]=6
- Check j=4: nums[3]=6 > nums[4]=2, so right[3] = max(1, right[4]+1) = 3
- Check j=5: nums[3]=6 > nums[5]=3, so right[3] = max(3, right[5]+1) = 3
- Check j=6: nums[3]=6 > nums[6]=1, so right[3] = max(3, right[6]+1) = 3
right[3] = 3
Continuing backwards:
right[2] = 4
(can go 5→2→1 or 5→3→1)right[1] = 1
(1 cannot start a decreasing sequence to any element after it)right[0] = 5
(can go 2→1 directly, or through other paths)
Final: right = [5, 1, 4, 3, 2, 2, 1]
Step 4: Find maximum mountain length
Check each position as a potential peak:
- i=0: left[0]=1, right[0]=5 → Not valid (left[0] ≤ 1)
- i=1: left[1]=1, right[1]=1 → Not valid (both ≤ 1)
- i=2: left[2]=2, right[2]=4 → Valid! Mountain length = 2 + 4 - 1 = 5
- i=3: left[3]=3, right[3]=3 → Valid! Mountain length = 3 + 3 - 1 = 5
- i=4: left[4]=2, right[4]=2 → Valid! Mountain length = 2 + 2 - 1 = 3
- i=5: left[5]=3, right[5]=2 → Valid! Mountain length = 3 + 2 - 1 = 4
- i=6: left[6]=1, right[6]=1 → Not valid (both ≤ 1)
Maximum mountain length = 5
Answer: 7 - 5 = 2 (minimum removals needed)
One possible mountain subsequence of length 5 would be [2, 5, 6, 2, 1] with peak at 6.
Solution Implementation
1class Solution:
2 def minimumMountainRemovals(self, nums: List[int]) -> int:
3 n = len(nums)
4
5 # left[i] stores the length of the longest increasing subsequence ending at index i
6 left = [1] * n
7
8 # right[i] stores the length of the longest decreasing subsequence starting at index i
9 right = [1] * n
10
11 # Calculate LIS (Longest Increasing Subsequence) for each position from left
12 for i in range(1, n):
13 for j in range(i):
14 if nums[i] > nums[j]:
15 left[i] = max(left[i], left[j] + 1)
16
17 # Calculate LIS from right (which gives us longest decreasing subsequence from left perspective)
18 for i in range(n - 2, -1, -1):
19 for j in range(i + 1, n):
20 if nums[i] > nums[j]:
21 right[i] = max(right[i], right[j] + 1)
22
23 # Find the maximum mountain length
24 # A valid mountain must have left[i] > 1 and right[i] > 1 (peak cannot be at endpoints)
25 # Mountain length at position i = left[i] + right[i] - 1 (subtract 1 as peak is counted twice)
26 max_mountain_length = max(
27 left[i] + right[i] - 1
28 for i in range(n)
29 if left[i] > 1 and right[i] > 1
30 )
31
32 # Minimum removals = total elements - maximum mountain length
33 return n - max_mountain_length
34
1class Solution {
2 public int minimumMountainRemovals(int[] nums) {
3 int n = nums.length;
4
5 // Arrays to store the length of longest increasing subsequence
6 // ending at each index (from left) and starting at each index (from right)
7 int[] lisFromLeft = new int[n];
8 int[] lisFromRight = new int[n];
9
10 // Initialize all positions with 1 (each element is a subsequence of length 1)
11 Arrays.fill(lisFromLeft, 1);
12 Arrays.fill(lisFromRight, 1);
13
14 // Calculate longest increasing subsequence ending at each position
15 // Dynamic programming: for each position i, check all previous positions j
16 for (int i = 1; i < n; ++i) {
17 for (int j = 0; j < i; ++j) {
18 // If current element is greater than previous element,
19 // we can extend the increasing subsequence
20 if (nums[i] > nums[j]) {
21 lisFromLeft[i] = Math.max(lisFromLeft[i], lisFromLeft[j] + 1);
22 }
23 }
24 }
25
26 // Calculate longest decreasing subsequence starting at each position
27 // (equivalently, longest increasing subsequence from right to left)
28 for (int i = n - 2; i >= 0; --i) {
29 for (int j = i + 1; j < n; ++j) {
30 // If current element is greater than next element,
31 // we can extend the decreasing subsequence
32 if (nums[i] > nums[j]) {
33 lisFromRight[i] = Math.max(lisFromRight[i], lisFromRight[j] + 1);
34 }
35 }
36 }
37
38 // Find the maximum length of a mountain subsequence
39 int maxMountainLength = 0;
40 for (int i = 0; i < n; ++i) {
41 // A valid mountain peak must have at least one element on each side
42 // (lisFromLeft[i] > 1 means there's at least one element before i in the increasing part)
43 // (lisFromRight[i] > 1 means there's at least one element after i in the decreasing part)
44 if (lisFromLeft[i] > 1 && lisFromRight[i] > 1) {
45 // Mountain length = increasing part + decreasing part - 1 (peak counted twice)
46 int mountainLength = lisFromLeft[i] + lisFromRight[i] - 1;
47 maxMountainLength = Math.max(maxMountainLength, mountainLength);
48 }
49 }
50
51 // Minimum removals = total elements - maximum mountain length
52 return n - maxMountainLength;
53 }
54}
55
1class Solution {
2public:
3 int minimumMountainRemovals(vector<int>& nums) {
4 int n = nums.size();
5
6 // leftLIS[i] stores the length of longest increasing subsequence ending at index i
7 vector<int> leftLIS(n, 1);
8
9 // rightLIS[i] stores the length of longest decreasing subsequence starting at index i
10 vector<int> rightLIS(n, 1);
11
12 // Calculate LIS from left to right for each position
13 for (int i = 1; i < n; ++i) {
14 for (int j = 0; j < i; ++j) {
15 if (nums[i] > nums[j]) {
16 leftLIS[i] = max(leftLIS[i], leftLIS[j] + 1);
17 }
18 }
19 }
20
21 // Calculate LIS from right to left (decreasing subsequence) for each position
22 for (int i = n - 2; i >= 0; --i) {
23 for (int j = i + 1; j < n; ++j) {
24 if (nums[i] > nums[j]) {
25 rightLIS[i] = max(rightLIS[i], rightLIS[j] + 1);
26 }
27 }
28 }
29
30 // Find the maximum length of mountain subsequence
31 int maxMountainLength = 0;
32 for (int i = 0; i < n; ++i) {
33 // A valid mountain peak must have at least one element on both sides
34 // leftLIS[i] > 1 ensures there's at least one element before peak
35 // rightLIS[i] > 1 ensures there's at least one element after peak
36 if (leftLIS[i] > 1 && rightLIS[i] > 1) {
37 // Total mountain length with peak at i
38 // Subtract 1 because peak is counted in both leftLIS and rightLIS
39 maxMountainLength = max(maxMountainLength, leftLIS[i] + rightLIS[i] - 1);
40 }
41 }
42
43 // Minimum removals = total elements - maximum mountain length
44 return n - maxMountainLength;
45 }
46};
47
1/**
2 * Finds the minimum number of elements to remove to make the array a mountain array.
3 * A mountain array has at least 3 elements where elements strictly increase to a peak,
4 * then strictly decrease.
5 *
6 * @param nums - The input array of numbers
7 * @returns The minimum number of elements to remove
8 */
9function minimumMountainRemovals(nums: number[]): number {
10 const arrayLength: number = nums.length;
11
12 // leftLIS[i] stores the length of longest increasing subsequence ending at index i
13 const leftLIS: number[] = Array(arrayLength).fill(1);
14
15 // rightLIS[i] stores the length of longest decreasing subsequence starting at index i
16 const rightLIS: number[] = Array(arrayLength).fill(1);
17
18 // Calculate longest increasing subsequence for each position from left
19 for (let currentIndex = 1; currentIndex < arrayLength; ++currentIndex) {
20 for (let previousIndex = 0; previousIndex < currentIndex; ++previousIndex) {
21 if (nums[currentIndex] > nums[previousIndex]) {
22 leftLIS[currentIndex] = Math.max(
23 leftLIS[currentIndex],
24 leftLIS[previousIndex] + 1
25 );
26 }
27 }
28 }
29
30 // Calculate longest decreasing subsequence for each position from right
31 for (let currentIndex = arrayLength - 2; currentIndex >= 0; --currentIndex) {
32 for (let nextIndex = currentIndex + 1; nextIndex < arrayLength; ++nextIndex) {
33 if (nums[currentIndex] > nums[nextIndex]) {
34 rightLIS[currentIndex] = Math.max(
35 rightLIS[currentIndex],
36 rightLIS[nextIndex] + 1
37 );
38 }
39 }
40 }
41
42 // Find the maximum length of mountain subsequence
43 let maxMountainLength: number = 0;
44
45 for (let peakIndex = 0; peakIndex < arrayLength; ++peakIndex) {
46 // A valid mountain peak must have at least one element on each side
47 if (leftLIS[peakIndex] > 1 && rightLIS[peakIndex] > 1) {
48 // Mountain length = left side + right side - 1 (peak counted twice)
49 const mountainLength: number = leftLIS[peakIndex] + rightLIS[peakIndex] - 1;
50 maxMountainLength = Math.max(maxMountainLength, mountainLength);
51 }
52 }
53
54 // Minimum removals = total elements - maximum mountain length
55 return arrayLength - maxMountainLength;
56}
57
Time and Space Complexity
The time complexity is O(n²)
, where n
is the length of the array nums
. This is because:
- The first nested loop (computing
left
array) iterates through each elementi
from 1 to n-1, and for eachi
, it checks all previous elementsj
from 0 to i-1, resulting inO(n²)
operations. - The second nested loop (computing
right
array) iterates through each elementi
from n-2 to 0, and for eachi
, it checks all subsequent elementsj
from i+1 to n-1, also resulting inO(n²)
operations. - The final list comprehension to find the maximum value runs in
O(n)
time. - Overall:
O(n²) + O(n²) + O(n) = O(n²)
The space complexity is O(n)
, where n
is the length of the array nums
. This is because:
- The
left
array usesO(n)
space to store the length of the longest increasing subsequence ending at each position. - The
right
array usesO(n)
space to store the length of the longest decreasing subsequence starting at each position. - The list comprehension creates temporary values but doesn't store them, so it doesn't add to the space complexity.
- Overall:
O(n) + O(n) = O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to validate peak position constraints
A common mistake is not checking that left[i] > 1
and right[i] > 1
when finding the maximum mountain length. This validation ensures:
- The peak is not at the first or last position (mountain needs at least one element on each side)
- There actually exists an increasing sequence before the peak and a decreasing sequence after it
Incorrect approach:
# This would include invalid mountains with peaks at endpoints
max_mountain_length = max(left[i] + right[i] - 1 for i in range(n))
Correct approach:
max_mountain_length = max(
left[i] + right[i] - 1
for i in range(n)
if left[i] > 1 and right[i] > 1
)
2. Double-counting the peak element
When calculating the mountain length, remember that the peak element is counted in both left[i]
(as the end of increasing subsequence) and right[i]
(as the start of decreasing subsequence). Failing to subtract 1 leads to an incorrect mountain length.
Incorrect:
mountain_length = left[i] + right[i] # Peak counted twice!
Correct:
mountain_length = left[i] + right[i] - 1 # Subtract 1 for the peak
3. Using non-strict comparisons
Mountain arrays require strictly increasing and strictly decreasing sequences. Using >=
or <=
instead of >
would allow duplicate values, violating the mountain array definition.
Incorrect:
if nums[i] >= nums[j]: # This allows equal elements
left[i] = max(left[i], left[j] + 1)
Correct:
if nums[i] > nums[j]: # Strictly greater for valid mountain
left[i] = max(left[i], left[j] + 1)
4. Edge case: No valid mountain exists
If the array cannot form any valid mountain (e.g., all elements are equal or monotonic), the code might fail. Always ensure the generator expression has valid values:
Safe implementation:
valid_mountains = [
left[i] + right[i] - 1
for i in range(n)
if left[i] > 1 and right[i] > 1
]
if not valid_mountains:
# Handle case where no valid mountain can be formed
# This might mean removing all but 3 elements in worst case
return n - 3 if n >= 3 else 0
return n - max(valid_mountains)
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Don’t Miss This!