2334. Subarray With Elements Greater Than Varying Threshold
Problem Description
You are given an integer array nums
and an integer threshold
.
Your task is to find a contiguous subarray of nums
with length k
where every element in that subarray is strictly greater than threshold / k
.
The key constraint is that all elements in the chosen subarray must satisfy the condition: each element > threshold / k
, where k
is the length of the subarray.
Return the size (length) of any such valid subarray. If no such subarray exists, return -1
.
For example:
- If you have a subarray of length 3, each element in it must be greater than
threshold / 3
- If you have a subarray of length 5, each element in it must be greater than
threshold / 5
The problem asks you to find any valid subarray (not necessarily the smallest or largest), and return its length. The subarray must be contiguous, meaning the elements must be adjacent in the original array.
Intuition
The key insight is to think about this problem from a different angle. Instead of checking every possible subarray (which would be inefficient), we can observe that for a subarray of length k
to be valid, its minimum element must be greater than threshold / k
.
This means: min_element_in_subarray > threshold / k
Rearranging: min_element_in_subarray * k > threshold
Now, here's the clever part: if we process elements from largest to smallest, we can build connected components gradually. When we add an element to our structure, it can either:
- Stand alone as a subarray of length 1
- Connect with already processed neighbors (which are all larger than the current element)
Since we process from largest to smallest, when we add element v
at position i
, it becomes the minimum element in any connected component it joins. The size of this component represents a potential subarray length k
.
By using Union-Find (Disjoint Set Union), we can efficiently:
- Track connected components of processed elements
- Merge adjacent components when we process a new element
- Keep track of component sizes
At each step, when processing element v
with its component of size k
, we check if v > threshold / k
. If true, we've found a valid subarray where v
is the minimum element and all other elements in the component are larger than v
(since we processed them earlier).
This approach is optimal because:
- We only need to check each element once as a potential minimum
- Union-Find operations are nearly constant time
- We can stop as soon as we find any valid subarray
Learn more about Stack, Union Find and Monotonic Stack patterns.
Solution Approach
The implementation uses a Union-Find data structure with a greedy approach, processing elements from largest to smallest:
1. Data Structure Setup:
p
: Parent array for Union-Find, initiallyp[i] = i
(each element is its own parent)size
: Array tracking the size of each component, initially all 1vis
: Boolean array marking which positions have been processed
2. Sorting Strategy:
Create arr
by sorting (value, index)
pairs in descending order by value. This ensures we process elements from largest to smallest.
3. Union-Find Operations:
find(x)
: Finds the root of component containingx
with path compressionmerge(a, b)
: Merges two components, updating the size of the root
4. Main Algorithm:
For each element (v, i)
in sorted order:
- Check left neighbor: If
i > 0
andvis[i-1]
is true (left neighbor already processed), merge current position with left neighbor - Check right neighbor: If
i < n-1
andvis[i+1]
is true (right neighbor already processed), merge current position with right neighbor - Validate subarray: After merging, check if
v > threshold // size[find(i)]
- If true, return
size[find(i)]
as we found a valid subarray - The current element
v
is the minimum in this component (all others are larger)
- If true, return
- Mark as visited: Set
vis[i] = True
to indicate this position is processed
5. Key Observations:
- When we process element
v
, all previously processed elements are≥ v
- The connected component containing
v
forms a contiguous subarray v
is the minimum element in its component- The condition
v > threshold / k
becomesv > threshold // k
using integer division
6. Time Complexity:
- Sorting:
O(n log n)
- Union-Find operations:
O(n × α(n))
whereα
is the inverse Ackermann function (nearly constant) - Overall:
O(n log n)
The algorithm returns -1
if no valid subarray is found after processing all elements.
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 walk through a small example with nums = [3, 5, 2, 4]
and threshold = 10
.
Initial Setup:
- Array:
[3, 5, 2, 4]
with indices[0, 1, 2, 3]
- Create sorted array by value (descending):
[(5, 1), (4, 3), (3, 0), (2, 2)]
- Initialize Union-Find:
p = [0, 1, 2, 3]
,size = [1, 1, 1, 1]
,vis = [False, False, False, False]
Step 1: Process (5, 1)
- Value = 5, Index = 1
- Check neighbors:
- Left (index 0): Not visited yet, skip
- Right (index 2): Not visited yet, skip
- Component size = 1
- Check condition: 5 > 10/1? → 5 > 10? → No
- Mark
vis[1] = True
- State:
vis = [False, True, False, False]
Step 2: Process (4, 3)
- Value = 4, Index = 3
- Check neighbors:
- Left (index 2): Not visited yet, skip
- Right: Out of bounds
- Component size = 1
- Check condition: 4 > 10/1? → 4 > 10? → No
- Mark
vis[3] = True
- State:
vis = [False, True, False, True]
Step 3: Process (3, 0)
- Value = 3, Index = 0
- Check neighbors:
- Left: Out of bounds
- Right (index 1): Visited! Merge components 0 and 1
- After merge: Component rooted at 1 has size 2
- Check condition: 3 > 10/2? → 3 > 5? → No
- Mark
vis[0] = True
- State:
vis = [True, True, False, True]
, subarray [3, 5] forms a component
Step 4: Process (2, 2)
- Value = 2, Index = 2
- Check neighbors:
- Left (index 1): Visited! Merge with component containing indices 0 and 1
- Right (index 3): Visited! Also merge with component at index 3
- After merges: All indices form one component of size 4
- Check condition: 2 > 10/4? → 2 > 2.5? → No (using integer division: 2 > 2? → No)
- Mark
vis[2] = True
Result: Return -1 (no valid subarray found)
Another example where we find a valid subarray:
nums = [6, 5, 6, 5, 8]
and threshold = 7
Processing in order: [(8, 4), (6, 0), (6, 2), (5, 1), (5, 3)]
- Process (8, 4): 8 > 7/1? → Yes! Return 1 immediately
- The single element subarray [8] at index 4 satisfies the condition
The algorithm efficiently finds valid subarrays by ensuring that when we check element v
with its component of size k
, v
is guaranteed to be the minimum in that component, making it sufficient to only check if v > threshold/k
.
Solution Implementation
1class Solution:
2 def validSubarraySize(self, nums: List[int], threshold: int) -> int:
3 """
4 Find the size of a subarray where all elements are greater than threshold/k,
5 where k is the size of the subarray.
6
7 Strategy: Process elements from largest to smallest, building connected components
8 of visited elements. Check if current element satisfies the threshold condition
9 for its component size.
10 """
11
12 def find_root(node: int) -> int:
13 """Find the root of the set containing node with path compression."""
14 if parent[node] != node:
15 parent[node] = find_root(parent[node]) # Path compression
16 return parent[node]
17
18 def union_sets(node_a: int, node_b: int) -> None:
19 """Merge the sets containing node_a and node_b."""
20 root_a = find_root(node_a)
21 root_b = find_root(node_b)
22
23 if root_a == root_b:
24 return # Already in the same set
25
26 # Merge set a into set b
27 parent[root_a] = root_b
28 component_size[root_b] += component_size[root_a]
29
30 # Initialize variables
31 array_length = len(nums)
32 parent = list(range(array_length)) # Each element is initially its own parent
33 component_size = [1] * array_length # Each component initially has size 1
34
35 # Sort elements by value in descending order, keeping track of original indices
36 sorted_elements = sorted(zip(nums, range(array_length)), reverse=True)
37
38 # Track which indices have been visited/processed
39 visited = [False] * array_length
40
41 # Process elements from largest to smallest
42 for value, index in sorted_elements:
43 # Merge with left neighbor if it has been visited
44 if index > 0 and visited[index - 1]:
45 union_sets(index, index - 1)
46
47 # Merge with right neighbor if it has been visited
48 if index < array_length - 1 and visited[index + 1]:
49 union_sets(index, index + 1)
50
51 # Check if current value satisfies the threshold condition
52 # For a subarray of size k, all elements must be > threshold/k
53 current_component_size = component_size[find_root(index)]
54 if value > threshold // current_component_size:
55 return current_component_size
56
57 # Mark current index as visited
58 visited[index] = True
59
60 # No valid subarray found
61 return -1
62
1class Solution {
2 // Union-Find parent array to track component representatives
3 private int[] parent;
4 // Size array to track the size of each component
5 private int[] componentSize;
6
7 public int validSubarraySize(int[] nums, int threshold) {
8 int n = nums.length;
9
10 // Initialize Union-Find data structures
11 parent = new int[n];
12 componentSize = new int[n];
13 for (int i = 0; i < n; i++) {
14 parent[i] = i; // Each element is initially its own parent
15 componentSize[i] = 1; // Each component initially has size 1
16 }
17
18 // Create array of [value, index] pairs for sorting
19 int[][] valueIndexPairs = new int[n][2];
20 for (int i = 0; i < n; i++) {
21 valueIndexPairs[i][0] = nums[i];
22 valueIndexPairs[i][1] = i;
23 }
24
25 // Sort pairs by value in descending order
26 Arrays.sort(valueIndexPairs, (a, b) -> b[0] - a[0]);
27
28 // Track which indices have been processed
29 boolean[] visited = new boolean[n];
30
31 // Process elements from largest to smallest
32 for (int[] pair : valueIndexPairs) {
33 int currentValue = pair[0];
34 int currentIndex = pair[1];
35
36 // Merge with left neighbor if it has been processed
37 if (currentIndex > 0 && visited[currentIndex - 1]) {
38 merge(currentIndex, currentIndex - 1);
39 }
40
41 // Merge with right neighbor if it has been processed
42 if (currentIndex < n - 1 && visited[currentIndex + 1]) {
43 merge(currentIndex, currentIndex + 1);
44 }
45
46 // Check if current component satisfies the condition
47 // For a subarray of size k, all elements must be > threshold/k
48 int currentComponentSize = componentSize[find(currentIndex)];
49 if (currentValue > threshold / currentComponentSize) {
50 return currentComponentSize;
51 }
52
53 // Mark current index as processed
54 visited[currentIndex] = true;
55 }
56
57 // No valid subarray found
58 return -1;
59 }
60
61 /**
62 * Find operation with path compression
63 * Returns the root/representative of the component containing x
64 */
65 private int find(int x) {
66 if (parent[x] != x) {
67 parent[x] = find(parent[x]); // Path compression
68 }
69 return parent[x];
70 }
71
72 /**
73 * Union operation to merge two components
74 * Merges component containing a with component containing b
75 */
76 private void merge(int a, int b) {
77 int rootA = find(a);
78 int rootB = find(b);
79
80 // Already in the same component
81 if (rootA == rootB) {
82 return;
83 }
84
85 // Merge by making rootA point to rootB
86 parent[rootA] = rootB;
87 // Update the size of the merged component
88 componentSize[rootB] += componentSize[rootA];
89 }
90}
91
1class Solution {
2private:
3 vector<int> parent; // Parent array for Union-Find
4 vector<int> componentSize; // Size of each component in Union-Find
5
6public:
7 int validSubarraySize(vector<int>& nums, int threshold) {
8 int n = nums.size();
9
10 // Initialize Union-Find data structures
11 parent.resize(n);
12 componentSize.assign(n, 1);
13 for (int i = 0; i < n; ++i) {
14 parent[i] = i; // Each element is initially its own parent
15 }
16
17 // Create pairs of (value, index) for sorting
18 vector<pair<int, int>> valueIndexPairs(n);
19 for (int i = 0; i < n; ++i) {
20 valueIndexPairs[i] = {nums[i], i};
21 }
22
23 // Sort pairs by value in ascending order
24 sort(valueIndexPairs.begin(), valueIndexPairs.end());
25
26 // Track which indices have been visited/processed
27 vector<bool> visited(n, false);
28
29 // Process elements from largest to smallest value
30 for (int j = n - 1; j >= 0; --j) {
31 int currentValue = valueIndexPairs[j].first;
32 int currentIndex = valueIndexPairs[j].second;
33
34 // Merge with left neighbor if it has been visited
35 if (currentIndex > 0 && visited[currentIndex - 1]) {
36 merge(currentIndex, currentIndex - 1);
37 }
38
39 // Merge with right neighbor if it has been visited
40 if (currentIndex < n - 1 && visited[currentIndex + 1]) {
41 merge(currentIndex, currentIndex + 1);
42 }
43
44 // Check if current component satisfies the condition
45 // For a subarray of size k with minimum value v: v * k > threshold
46 int currentComponentSize = componentSize[find(currentIndex)];
47 if (currentValue > threshold / currentComponentSize) {
48 return currentComponentSize;
49 }
50
51 // Mark current index as visited
52 visited[currentIndex] = true;
53 }
54
55 // No valid subarray found
56 return -1;
57 }
58
59private:
60 // Find operation with path compression
61 int find(int x) {
62 if (parent[x] != x) {
63 parent[x] = find(parent[x]); // Path compression
64 }
65 return parent[x];
66 }
67
68 // Union operation by merging two components
69 void merge(int a, int b) {
70 int rootA = find(a);
71 int rootB = find(b);
72
73 // Already in the same component
74 if (rootA == rootB) {
75 return;
76 }
77
78 // Merge component A into component B
79 parent[rootA] = rootB;
80 componentSize[rootB] += componentSize[rootA];
81 }
82};
83
1// Parent array for Union-Find
2let parent: number[] = [];
3// Size of each component in Union-Find
4let componentSize: number[] = [];
5
6function validSubarraySize(nums: number[], threshold: number): number {
7 const n = nums.length;
8
9 // Initialize Union-Find data structures
10 parent = new Array(n);
11 componentSize = new Array(n).fill(1);
12 for (let i = 0; i < n; i++) {
13 parent[i] = i; // Each element is initially its own parent
14 }
15
16 // Create pairs of (value, index) for sorting
17 const valueIndexPairs: [number, number][] = [];
18 for (let i = 0; i < n; i++) {
19 valueIndexPairs.push([nums[i], i]);
20 }
21
22 // Sort pairs by value in ascending order
23 valueIndexPairs.sort((a, b) => a[0] - b[0]);
24
25 // Track which indices have been visited/processed
26 const visited: boolean[] = new Array(n).fill(false);
27
28 // Process elements from largest to smallest value
29 for (let j = n - 1; j >= 0; j--) {
30 const currentValue = valueIndexPairs[j][0];
31 const currentIndex = valueIndexPairs[j][1];
32
33 // Merge with left neighbor if it has been visited
34 if (currentIndex > 0 && visited[currentIndex - 1]) {
35 merge(currentIndex, currentIndex - 1);
36 }
37
38 // Merge with right neighbor if it has been visited
39 if (currentIndex < n - 1 && visited[currentIndex + 1]) {
40 merge(currentIndex, currentIndex + 1);
41 }
42
43 // Check if current component satisfies the condition
44 // For a subarray of size k with minimum value v: v * k > threshold
45 const currentComponentSize = componentSize[find(currentIndex)];
46 if (currentValue > threshold / currentComponentSize) {
47 return currentComponentSize;
48 }
49
50 // Mark current index as visited
51 visited[currentIndex] = true;
52 }
53
54 // No valid subarray found
55 return -1;
56}
57
58// Find operation with path compression
59function find(x: number): number {
60 if (parent[x] !== x) {
61 parent[x] = find(parent[x]); // Path compression
62 }
63 return parent[x];
64}
65
66// Union operation by merging two components
67function merge(a: number, b: number): void {
68 const rootA = find(a);
69 const rootB = find(b);
70
71 // Already in the same component
72 if (rootA === rootB) {
73 return;
74 }
75
76 // Merge component A into component B
77 parent[rootA] = rootB;
78 componentSize[rootB] += componentSize[rootA];
79}
80
Time and Space Complexity
Time Complexity: O(n log n)
The time complexity is dominated by the sorting operation sorted(zip(nums, range(n)), reverse=True)
which takes O(n log n)
time. After sorting, the code iterates through the sorted array once, performing union-find operations. Each union-find operation (both find
and merge
) has an amortized time complexity of O(α(n))
where α
is the inverse Ackermann function, which is effectively constant for practical purposes. Since we perform at most 2n
union operations (each element can be merged with its left and right neighbors at most once) and n
find operations, the total time for union-find operations is O(n × α(n)) ≈ O(n)
. Therefore, the overall time complexity is O(n log n) + O(n) = O(n log n)
.
Space Complexity: O(n)
The space complexity consists of:
p
array:O(n)
for storing parent pointers in the union-find structuresize
array:O(n)
for storing the size of each componentarr
sorted array:O(n)
for storing the sorted (value, index) pairsvis
array:O(n)
for tracking visited elements- Recursion stack for
find
function:O(log n)
in average case with path compression,O(n)
in worst case without optimization
The total space complexity is O(n) + O(n) + O(n) + O(n) + O(n) = O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Division vs Float Division
The most critical pitfall is using integer division (//
) instead of proper comparison with floating-point division.
Problem: The code uses value > threshold // current_component_size
, which performs integer division and can incorrectly reject valid subarrays.
Example:
threshold = 7
,current_component_size = 3
,value = 3
- Integer division:
7 // 3 = 2
, so3 > 2
returnsTrue
✓ - Actual requirement:
3 > 7/3 = 2.333...
returnsTrue
✓ - But consider
threshold = 8
,current_component_size = 3
,value = 3
- Integer division:
8 // 3 = 2
, so3 > 2
returnsTrue
✓ (WRONG!) - Actual requirement:
3 > 8/3 = 2.666...
returnsTrue
✓
Solution: Use multiplication to avoid division precision issues:
# Instead of: value > threshold // current_component_size # Use: value * current_component_size > threshold if value * current_component_size > threshold: return current_component_size
2. Handling Edge Cases with Zero or Negative Values
If the array contains zeros or negative numbers, the algorithm might behave unexpectedly.
Problem: The comparison logic assumes positive values. With negative numbers, larger absolute values processed first might not form valid subarrays.
Solution: Add validation or handle negative cases explicitly:
# If problem guarantees positive integers, add assertion
assert all(num > 0 for num in nums), "All numbers must be positive"
# Or handle negative threshold cases
if threshold < 0:
# All positive numbers satisfy the condition for negative threshold
return 1
3. Union-Find Path Compression Inconsistency
The current implementation doesn't consistently use path compression in all find operations.
Problem: The component_size
array is accessed using find_root(index)
but not all paths are compressed optimally.
Solution: Ensure consistent path compression:
def find_root(node: int) -> int:
if parent[node] != node:
parent[node] = find_root(parent[node])
return parent[node]
# Always use find_root when accessing component_size
root = find_root(index)
current_component_size = component_size[root]
4. Overflow Risk with Large Values
When using multiplication to avoid division issues, there's a risk of integer overflow.
Problem: value * current_component_size
might overflow for large values.
Solution: Use careful comparison or Python's arbitrary precision integers:
# Python handles large integers automatically, but in other languages: # Check for potential overflow if current_component_size > 0 and value > threshold // current_component_size: # Additional check for exact division if value * current_component_size > threshold: return current_component_size
5. Empty Array or Invalid Input
The code doesn't handle edge cases like empty arrays or invalid threshold values.
Problem: Empty array will cause the function to return -1, but might need explicit handling.
Solution: Add input validation:
if not nums:
return -1
if threshold < 0:
return len(nums) # All subarrays valid for negative threshold
How would you design a stack which has a function min
that returns the minimum element in the stack, in addition to push
and pop
? All push
, pop
, min
should have running time O(1)
.
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
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
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!