Facebook Pixel

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Stand alone as a subarray of length 1
  2. 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:

  1. We only need to check each element once as a potential minimum
  2. Union-Find operations are nearly constant time
  3. 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, initially p[i] = i (each element is its own parent)
  • size: Array tracking the size of each component, initially all 1
  • vis: 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 containing x with path compression
  • merge(a, b): Merges two components, updating the size of the root

4. Main Algorithm: For each element (v, i) in sorted order:

  1. Check left neighbor: If i > 0 and vis[i-1] is true (left neighbor already processed), merge current position with left neighbor
  2. Check right neighbor: If i < n-1 and vis[i+1] is true (right neighbor already processed), merge current position with right neighbor
  3. 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)
  4. 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 becomes v > 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 Evaluator

Example 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)]

  1. 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 structure
  • size array: O(n) for storing the size of each component
  • arr sorted array: O(n) for storing the sorted (value, index) pairs
  • vis 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, so 3 > 2 returns True
  • Actual requirement: 3 > 7/3 = 2.333... returns True
  • But consider threshold = 8, current_component_size = 3, value = 3
  • Integer division: 8 // 3 = 2, so 3 > 2 returns True ✓ (WRONG!)
  • Actual requirement: 3 > 8/3 = 2.666... returns True

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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More