2863. Maximum Length of Semi-Decreasing Subarrays 🔒
Problem Description
You are given an integer array nums
.
Your task is to find the length of the longest semi-decreasing subarray in nums
. If no such subarray exists, return 0
.
Key definitions:
- A subarray is a contiguous sequence of elements from the array (cannot be empty)
- A semi-decreasing subarray has its first element strictly greater than its last element
For example:
- If we have subarray
[5, 3, 2]
, it's semi-decreasing because5 > 2
(first > last) - If we have subarray
[3, 4, 1]
, it's semi-decreasing because3 > 1
(first > last) - If we have subarray
[2, 2]
, it's NOT semi-decreasing because2
is not strictly greater than2
- A single element
[5]
is NOT semi-decreasing (needs at least 2 elements to compare first and last)
The goal is to find the maximum possible length among all semi-decreasing subarrays in the given array.
Intuition
The key insight is that for a semi-decreasing subarray, we need the first element to be strictly greater than the last element. This means we're looking for pairs of indices (i, j)
where i < j
and nums[i] > nums[j]
.
To maximize the length of such a subarray, we want to maximize j - i + 1
. This suggests we should try to find pairs where i
is as small as possible and j
is as large as possible, while maintaining the condition nums[i] > nums[j]
.
Let's think about this differently: for each value in the array, we can track all positions where it appears. If we process values from largest to smallest, we can maintain the following strategy:
- For a larger value, we want to use its leftmost occurrence as the starting point of a potential subarray
- For a smaller value, we want to use its rightmost occurrence as the ending point of a potential subarray
Why does this work? When we process values in descending order:
- We first encounter larger values that can serve as starting points
- As we move to smaller values, they can potentially be endpoints for subarrays starting with any previously seen larger value
- By keeping track of the minimum starting index seen so far (
k
), we can calculate the maximum subarray length ending at the current value
For example, if we have value 10
at index 2
and value 5
at index 7
, the subarray from index 2
to 7
has length 7 - 2 + 1 = 6
, and it's semi-decreasing because 10 > 5
.
This approach transforms the problem from checking all possible subarrays (which would be O(n²)) to a more efficient solution using sorting and tracking minimum indices.
Learn more about Stack, Sorting and Monotonic Stack patterns.
Solution Approach
Let's walk through the implementation step by step:
Step 1: Build a Hash Table
d = defaultdict(list)
for i, x in enumerate(nums):
d[x].append(i)
We create a hash table d
where each key is a value from the array, and the corresponding value is a list of all indices where that number appears. For example, if nums = [3, 1, 3, 2, 1]
, then d
would be {3: [0, 2], 1: [1, 4], 2: [3]}
.
Step 2: Sort and Process Values in Descending Order
ans, k = 0, inf
for x in sorted(d, reverse=True):
We initialize:
ans
to track the maximum subarray length foundk
toinf
(infinity) to track the minimum starting index seen so far
We then iterate through the unique values in descending order. This ensures we process larger values before smaller ones.
Step 3: Calculate Maximum Length for Current Value
ans = max(ans, d[x][-1] - k + 1)
For the current value x
:
d[x][-1]
gives us the rightmost (last) occurrence ofx
- This position can serve as the ending point of a semi-decreasing subarray
- The subarray would start at index
k
(the minimum index from all larger values seen so far) - The length would be
d[x][-1] - k + 1
- We update
ans
if this length is larger
Step 4: Update Minimum Starting Index
k = min(k, d[x][0])
After processing value x
, we update k
to include the leftmost occurrence of x
(which is d[x][0]
). This ensures that when we process smaller values later, they can potentially form subarrays starting from this position.
Example Walkthrough:
Consider nums = [5, 3, 5, 2, 3, 1]
:
d = {5: [0, 2], 3: [1, 4], 2: [3], 1: [5]}
- Process values in order:
5, 3, 2, 1
- Value
5
:ans = max(0, 2 - inf + 1) = 0
,k = 0
- Value
3
:ans = max(0, 4 - 0 + 1) = 5
,k = 0
- Value
2
:ans = max(5, 3 - 0 + 1) = 5
,k = 0
- Value
1
:ans = max(5, 5 - 0 + 1) = 6
,k = 0
- Value
- Final answer:
6
(subarray from index 0 to 5)
The time complexity is O(n log n)
due to sorting, and space complexity is O(n)
for the hash table.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with nums = [7, 2, 5, 2, 7, 1, 4]
:
Step 1: Build the hash table
d = {7: [0, 4], 2: [1, 3], 5: [2], 1: [5], 4: [6]}
Each value maps to its indices in the array.
Step 2: Sort values in descending order
sorted values: [7, 5, 4, 2, 1]
Step 3: Process each value
Initialize: ans = 0
, k = inf
-
Process value 7:
- Rightmost index of 7:
d[7][-1] = 4
- Calculate length:
4 - inf + 1
→ not valid (k is still inf) - Update
ans = 0
- Update
k = min(inf, d[7][0]) = min(inf, 0) = 0
- Rightmost index of 7:
-
Process value 5:
- Rightmost index of 5:
d[5][-1] = 2
- Calculate length:
2 - 0 + 1 = 3
(subarray from index 0 to 2:[7, 2, 5]
) - Since
nums[0]=7 > nums[2]=5
, this is valid! - Update
ans = max(0, 3) = 3
- Update
k = min(0, d[5][0]) = min(0, 2) = 0
- Rightmost index of 5:
-
Process value 4:
- Rightmost index of 4:
d[4][-1] = 6
- Calculate length:
6 - 0 + 1 = 7
(subarray from index 0 to 6:[7, 2, 5, 2, 7, 1, 4]
) - Since
nums[0]=7 > nums[6]=4
, this is valid! - Update
ans = max(3, 7) = 7
- Update
k = min(0, d[4][0]) = min(0, 6) = 0
- Rightmost index of 4:
-
Process value 2:
- Rightmost index of 2:
d[2][-1] = 3
- Calculate length:
3 - 0 + 1 = 4
(subarray from index 0 to 3:[7, 2, 5, 2]
) - Since
nums[0]=7 > nums[3]=2
, this is valid! - Update
ans = max(7, 4) = 7
- Update
k = min(0, d[2][0]) = min(0, 1) = 0
- Rightmost index of 2:
-
Process value 1:
- Rightmost index of 1:
d[1][-1] = 5
- Calculate length:
5 - 0 + 1 = 6
(subarray from index 0 to 5:[7, 2, 5, 2, 7, 1]
) - Since
nums[0]=7 > nums[5]=1
, this is valid! - Update
ans = max(7, 6) = 7
- Update
k = min(0, d[1][0]) = min(0, 5) = 0
- Rightmost index of 1:
Final Answer: 7
The longest semi-decreasing subarray is [7, 2, 5, 2, 7, 1, 4]
with length 7, where the first element (7) is strictly greater than the last element (4).
Solution Implementation
1from collections import defaultdict
2from typing import List
3
4class Solution:
5 def maxSubarrayLength(self, nums: List[int]) -> int:
6 # Build a dictionary mapping each value to its list of indices
7 value_to_indices = defaultdict(list)
8 for index, value in enumerate(nums):
9 value_to_indices[value].append(index)
10
11 # Initialize variables
12 max_length = 0 # Maximum subarray length found
13 min_start_index = float('inf') # Minimum starting index seen so far
14
15 # Process values in descending order
16 for value in sorted(value_to_indices, reverse=True):
17 # Calculate the length of subarray from min_start_index to the last occurrence of current value
18 # The "+1" converts from index difference to actual length
19 max_length = max(max_length, value_to_indices[value][-1] - min_start_index + 1)
20
21 # Update the minimum starting index with the first occurrence of current value
22 min_start_index = min(min_start_index, value_to_indices[value][0])
23
24 return max_length
25
1class Solution {
2 public int maxSubarrayLength(int[] nums) {
3 // Create a map to store indices grouped by value, sorted in descending order of values
4 // Key: array value, Value: list of indices where this value appears
5 TreeMap<Integer, List<Integer>> valueToIndicesMap = new TreeMap<>(Comparator.reverseOrder());
6
7 // Group all indices by their corresponding values
8 for (int i = 0; i < nums.length; i++) {
9 valueToIndicesMap.computeIfAbsent(nums[i], value -> new ArrayList<>()).add(i);
10 }
11
12 // Initialize result and minimum starting index tracker
13 int maxLength = 0;
14 int minStartIndex = Integer.MAX_VALUE; // Use MAX_VALUE instead of 1 << 30 for clarity
15
16 // Process values in descending order
17 for (List<Integer> indicesList : valueToIndicesMap.values()) {
18 // Calculate the length from the minimum start index seen so far
19 // to the last occurrence of current value
20 int lastIndex = indicesList.get(indicesList.size() - 1);
21 maxLength = Math.max(maxLength, lastIndex - minStartIndex + 1);
22
23 // Update the minimum start index with the first occurrence of current value
24 int firstIndex = indicesList.get(0);
25 minStartIndex = Math.min(minStartIndex, firstIndex);
26 }
27
28 return maxLength;
29 }
30}
31
1class Solution {
2public:
3 int maxSubarrayLength(vector<int>& nums) {
4 // Map to store indices for each value, sorted by value in descending order
5 // Key: element value (sorted descending), Value: list of indices where it appears
6 map<int, vector<int>, greater<int>> valueToIndices;
7
8 // Populate the map with indices for each value
9 for (int i = 0; i < nums.size(); ++i) {
10 valueToIndices[nums[i]].push_back(i);
11 }
12
13 // Initialize result and minimum index tracker
14 int maxLength = 0;
15 int minIndexSoFar = 1 << 30; // Start with a large value
16
17 // Process values in descending order
18 for (auto& [value, indices] : valueToIndices) {
19 // Calculate subarray length from minIndexSoFar to last occurrence of current value
20 maxLength = max(maxLength, indices.back() - minIndexSoFar + 1);
21
22 // Update minimum index seen so far
23 minIndexSoFar = min(minIndexSoFar, indices[0]);
24 }
25
26 return maxLength;
27 }
28};
29
1/**
2 * Finds the maximum length of a subarray containing the largest element
3 * @param nums - Input array of numbers
4 * @returns Maximum length of subarray containing the largest element
5 */
6function maxSubarrayLength(nums: number[]): number {
7 // Map to store indices of each number in the array
8 const indicesMap: Map<number, number[]> = new Map();
9
10 // Build the map with all indices for each unique number
11 for (let i = 0; i < nums.length; ++i) {
12 if (!indicesMap.has(nums[i])) {
13 indicesMap.set(nums[i], []);
14 }
15 indicesMap.get(nums[i])!.push(i);
16 }
17
18 // Get all unique numbers and sort them in descending order
19 const sortedUniqueNumbers: number[] = Array.from(indicesMap.keys()).sort((a, b) => b - a);
20
21 // Track the maximum subarray length found
22 let maxLength: number = 0;
23
24 // Track the minimum starting index for valid subarrays
25 let minStartIndex: number = Infinity;
26
27 // Process each unique number from largest to smallest
28 for (const currentNumber of sortedUniqueNumbers) {
29 const currentIndices: number[] = indicesMap.get(currentNumber)!;
30
31 // Calculate subarray length from minStartIndex to last occurrence of current number
32 maxLength = Math.max(maxLength, currentIndices.at(-1)! - minStartIndex + 1);
33
34 // Update minimum starting index with first occurrence of current number
35 minStartIndex = Math.min(minStartIndex, currentIndices[0]);
36 }
37
38 return maxLength;
39}
40
Time and Space Complexity
Time Complexity: O(n × log n)
The algorithm consists of the following steps:
- Building a dictionary where each element maps to a list of its indices: This requires iterating through the array once, taking
O(n)
time. - Sorting the dictionary keys in descending order: The dictionary has at most
n
unique keys (in the worst case, all elements are unique), so sorting takesO(n × log n)
time. - Iterating through the sorted keys to compute the maximum subarray length: This iteration goes through each unique key once, taking
O(n)
time in the worst case.
The dominant operation is the sorting step, resulting in an overall time complexity of O(n × log n)
.
Space Complexity: O(n)
The space usage comes from:
- The dictionary
d
that stores lists of indices for each unique element: In the worst case where all elements are the same, we storen
indices for one key. In the best case where all elements are unique, we storen
keys with one index each. Either way, the total space used isO(n)
. - The sorted list of keys: This contains at most
n
elements, requiringO(n)
space. - Variables
ans
andk
: These useO(1)
space.
Therefore, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Misunderstanding the Initial Value of min_start_index
The Problem:
A common mistake is initializing min_start_index
to 0
instead of infinity
. This would incorrectly assume there's always a valid starting position at index 0, even before processing any values.
Why It's Wrong:
When processing the largest value first, we need to check if there's a valid semi-decreasing subarray. If min_start_index
is initialized to 0
, we might incorrectly calculate a length for subarrays where the first element equals the last element (not strictly greater).
Incorrect Code:
min_start_index = 0 # Wrong initialization
for value in sorted(value_to_indices, reverse=True):
max_length = max(max_length, value_to_indices[value][-1] - min_start_index + 1)
# This would give wrong answer for arrays like [3, 3, 3]
Correct Approach:
min_start_index = float('inf') # Correct initialization
# Now when processing the largest value, min_start_index - value_to_indices[value][-1] + 1
# will be negative (since inf - any_number + 1 > 0), so max_length stays 0
Pitfall 2: Not Handling Arrays with All Same Elements
The Problem:
When all elements in the array are identical (e.g., [5, 5, 5, 5]
), there can be no semi-decreasing subarray since we need the first element to be strictly greater than the last.
Why It Matters:
Without proper initialization of min_start_index
to infinity, the code might incorrectly return a non-zero length for such arrays.
Test Case to Verify:
nums = [5, 5, 5, 5] # Expected output: 0 # With wrong initialization, might return 4
Pitfall 3: Forgetting the "+1" in Length Calculation
The Problem:
Computing the subarray length as end_index - start_index
instead of end_index - start_index + 1
.
Example:
# Wrong:
max_length = max(max_length, value_to_indices[value][-1] - min_start_index)
# Correct:
max_length = max(max_length, value_to_indices[value][-1] - min_start_index + 1)
For a subarray from index 2 to index 5, the length should be 4 (indices 2, 3, 4, 5), not 3.
Pitfall 4: Processing Values in Ascending Order
The Problem: Processing values in ascending order (smallest to largest) instead of descending order would completely break the algorithm logic.
Why It's Wrong: The algorithm relies on maintaining the minimum index from all larger values seen so far. If we process in ascending order, we'd be tracking indices from smaller values, which cannot form valid semi-decreasing subarrays with the current value as the ending element.
Incorrect Code:
for value in sorted(value_to_indices): # Wrong: ascending order
# This would look for subarrays where first element < last element
Correct Code:
for value in sorted(value_to_indices, reverse=True): # Correct: descending order
# This correctly looks for subarrays where first element > last element
Which two pointer techniques do you use to check if a string is a palindrome?
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Monotonic Stack Deque Intro If you'd prefer a video div class responsive iframe iframe src https www youtube com embed Dq_ObZwTY_Q title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div The word monotonic means a list or
Want a Structured Path to Master System Design Too? Don’t Miss This!