3134. Find the Median of the Uniqueness Array
Problem Description
You are given an integer array nums. Your task is to find the median of a special array called the "uniqueness array".
The uniqueness array is constructed as follows:
- For every possible subarray of
nums(including single elements and the entire array), count the number of distinct elements in that subarray - Collect all these counts into an array
- Sort this array in non-decreasing order
For example, if nums = [1, 2, 1]:
- Subarray
[1]has 1 distinct element - Subarray
[2]has 1 distinct element - Subarray
[1]has 1 distinct element - Subarray
[1, 2]has 2 distinct elements - Subarray
[2, 1]has 2 distinct elements - Subarray
[1, 2, 1]has 2 distinct elements - The uniqueness array would be
[1, 1, 1, 2, 2, 2]after sorting
The median of the uniqueness array is:
- The middle element if the array has an odd number of elements
- The smaller of the two middle elements if the array has an even number of elements
Since there are n * (n + 1) / 2 total subarrays for an array of length n, the uniqueness array will have exactly this many elements.
Your goal is to return the median value from this uniqueness array without actually constructing the entire array (which would be inefficient for large inputs).
Intuition
The key insight is that we don't need to generate the entire uniqueness array to find its median. Instead, we can use binary search to directly find the median value.
Think about it this way: if we pick any value x, we can count how many elements in the uniqueness array are less than or equal to x. For the median, we need the value where exactly (m + 1) / 2 elements are less than or equal to it, where m = n * (n + 1) / 2 is the total number of subarrays.
The crucial observation is that as x increases, the count of elements ≤ x also increases (monotonic property). This makes binary search perfect for finding the smallest x where at least (m + 1) / 2 elements are ≤ x.
But how do we count subarrays with at most x distinct elements efficiently? Here's where the sliding window technique comes in. We can maintain a window that expands to the right and contracts from the left to ensure it never has more than x distinct elements.
For each position r as the right endpoint:
- We expand the window to include
nums[r] - If the window has more than
xdistinct elements, we shrink from the left until it's valid again - Once valid, all subarrays ending at
rwith starting positions fromltorare valid (that'sr - l + 1subarrays)
This two-pointer approach lets us count all valid subarrays in O(n) time for a given x. Combined with binary search over possible values of x (which ranges from 1 to n), we get an efficient solution without explicitly constructing the massive uniqueness array.
The beauty of this approach is that we transform the problem from "find the median of a huge array" to "find the first value where enough subarrays satisfy a condition" - a much more manageable task.
Learn more about Binary Search and Sliding Window patterns.
Solution Implementation
1from typing import List 2from collections import defaultdict 3 4class Solution: 5 def medianOfUniquenessArray(self, nums: List[int]) -> int: 6 n = len(nums) 7 total_subarrays = n * (n + 1) // 2 8 9 def count_subarrays_with_at_most_k_distinct(max_unique: int) -> int: 10 """Count subarrays with at most max_unique distinct elements.""" 11 element_count = defaultdict(int) 12 subarrays_count = 0 13 left = 0 14 15 for right, current_num in enumerate(nums): 16 element_count[current_num] += 1 17 18 while len(element_count) > max_unique: 19 left_num = nums[left] 20 element_count[left_num] -= 1 21 if element_count[left_num] == 0: 22 del element_count[left_num] 23 left += 1 24 25 subarrays_count += right - left + 1 26 27 return subarrays_count 28 29 def feasible(mid: int) -> bool: 30 """Check if at least half of subarrays have uniqueness <= mid.""" 31 return count_subarrays_with_at_most_k_distinct(mid) >= (total_subarrays + 1) // 2 32 33 # Binary search using the standard template 34 left, right = 1, n 35 first_true_index = -1 36 37 while left <= right: 38 mid = (left + right) // 2 39 if feasible(mid): 40 first_true_index = mid 41 right = mid - 1 42 else: 43 left = mid + 1 44 45 return first_true_index 461class Solution { 2 private long totalSubarrays; 3 private int[] nums; 4 5 /** 6 * Finds the median of uniqueness values for all subarrays. 7 * Uses binary search to find the minimum uniqueness value such that 8 * at least half of all subarrays have uniqueness <= this value. 9 */ 10 public int medianOfUniquenessArray(int[] nums) { 11 int n = nums.length; 12 this.nums = nums; 13 totalSubarrays = (1L + n) * n / 2; 14 15 // Binary search using the standard template 16 int left = 1; 17 int right = n; 18 int firstTrueIndex = -1; 19 20 while (left <= right) { 21 int mid = left + (right - left) / 2; 22 if (feasible(mid)) { 23 firstTrueIndex = mid; 24 right = mid - 1; 25 } else { 26 left = mid + 1; 27 } 28 } 29 30 return firstTrueIndex; 31 } 32 33 /** 34 * Check if at least half of subarrays have uniqueness <= maxUniqueness. 35 */ 36 private boolean feasible(int maxUniqueness) { 37 return countSubarrays(maxUniqueness) >= (totalSubarrays + 1) / 2; 38 } 39 40 /** 41 * Count subarrays with at most maxUniqueness distinct elements. 42 */ 43 private long countSubarrays(int maxUniqueness) { 44 Map<Integer, Integer> frequencyMap = new HashMap<>(); 45 long subarrayCount = 0; 46 int left = 0; 47 48 for (int right = 0; right < nums.length; right++) { 49 int currentElement = nums[right]; 50 frequencyMap.merge(currentElement, 1, Integer::sum); 51 52 while (frequencyMap.size() > maxUniqueness) { 53 int leftElement = nums[left]; 54 left++; 55 if (frequencyMap.merge(leftElement, -1, Integer::sum) == 0) { 56 frequencyMap.remove(leftElement); 57 } 58 } 59 60 subarrayCount += right - left + 1; 61 } 62 63 return subarrayCount; 64 } 65} 661class Solution { 2public: 3 int medianOfUniquenessArray(vector<int>& nums) { 4 int n = nums.size(); 5 using ll = long long; 6 ll totalSubarrays = (1LL + n) * n / 2; 7 8 // Count subarrays with at most maxDistinct distinct elements 9 auto countSubarrays = [&](int maxDistinct) -> ll { 10 unordered_map<int, int> frequencyMap; 11 ll subarrayCount = 0; 12 13 for (int windowStart = 0, windowEnd = 0; windowEnd < n; ++windowEnd) { 14 int currentElement = nums[windowEnd]; 15 ++frequencyMap[currentElement]; 16 17 while (frequencyMap.size() > maxDistinct) { 18 int leftElement = nums[windowStart++]; 19 if (--frequencyMap[leftElement] == 0) { 20 frequencyMap.erase(leftElement); 21 } 22 } 23 24 subarrayCount += windowEnd - windowStart + 1; 25 } 26 return subarrayCount; 27 }; 28 29 // Check if at least half of subarrays have uniqueness <= mid 30 auto feasible = [&](int mid) -> bool { 31 return countSubarrays(mid) >= (totalSubarrays + 1) / 2; 32 }; 33 34 // Binary search using the standard template 35 int left = 1; 36 int right = n; 37 int firstTrueIndex = -1; 38 39 while (left <= right) { 40 int mid = left + (right - left) / 2; 41 if (feasible(mid)) { 42 firstTrueIndex = mid; 43 right = mid - 1; 44 } else { 45 left = mid + 1; 46 } 47 } 48 49 return firstTrueIndex; 50 } 51}; 521/** 2 * Finds the median of the uniqueness array for all subarrays. 3 * Uses binary search to find the minimum uniqueness value such that 4 * at least half of all subarrays have uniqueness <= this value. 5 */ 6function medianOfUniquenessArray(nums: number[]): number { 7 const arrayLength: number = nums.length; 8 const totalSubarrays: number = Math.floor(((1 + arrayLength) * arrayLength) / 2); 9 10 /** 11 * Count subarrays with at most maxUniqueness distinct elements. 12 */ 13 const countSubarrays = (maxUniqueness: number): number => { 14 const frequencyMap = new Map<number, number>(); 15 let windowStart: number = 0; 16 let subarrayCount: number = 0; 17 18 for (let windowEnd = 0; windowEnd < arrayLength; ++windowEnd) { 19 const currentElement: number = nums[windowEnd]; 20 frequencyMap.set(currentElement, (frequencyMap.get(currentElement) || 0) + 1); 21 22 while (frequencyMap.size > maxUniqueness) { 23 const elementToRemove: number = nums[windowStart++]; 24 const updatedFrequency: number = frequencyMap.get(elementToRemove)! - 1; 25 frequencyMap.set(elementToRemove, updatedFrequency); 26 if (updatedFrequency === 0) { 27 frequencyMap.delete(elementToRemove); 28 } 29 } 30 31 subarrayCount += windowEnd - windowStart + 1; 32 } 33 34 return subarrayCount; 35 }; 36 37 /** 38 * Check if at least half of subarrays have uniqueness <= mid. 39 */ 40 const feasible = (mid: number): boolean => { 41 return countSubarrays(mid) >= Math.floor((totalSubarrays + 1) / 2); 42 }; 43 44 // Binary search using the standard template 45 let left: number = 1; 46 let right: number = arrayLength; 47 let firstTrueIndex: number = -1; 48 49 while (left <= right) { 50 const mid: number = Math.floor((left + right) / 2); 51 if (feasible(mid)) { 52 firstTrueIndex = mid; 53 right = mid - 1; 54 } else { 55 left = mid + 1; 56 } 57 } 58 59 return firstTrueIndex; 60} 61Solution Approach
The solution uses binary search combined with a sliding window technique to efficiently find the median without constructing the entire uniqueness array.
Binary Search Template
We use the standard binary search template to find the smallest uniqueness value where at least half of subarrays have that many or fewer distinct elements:
left, right = 1, n first_true_index = -1 while left <= right: mid = (left + right) // 2 if feasible(mid): first_true_index = mid right = mid - 1 else: left = mid + 1 return first_true_index
The feasible(mid) function returns True if at least (total_subarrays + 1) / 2 subarrays have at most mid distinct elements.
The Feasible Function
The feasible function creates a boolean pattern over the search space:
false, false, false, ..., true, true, true, ...
We find the first true position, which is exactly the median uniqueness value.
Counting Subarrays with Sliding Window
For a given max_unique, we count subarrays with at most max_unique distinct elements using a sliding window:
-
Expand the window: For each position
right, addnums[right]to the frequency map -
Shrink if needed: While the window has more than
max_uniquedistinct elements:- Remove elements from the left
- If count becomes 0, delete from the map
- Move left pointer forward
-
Count valid subarrays: All subarrays ending at
rightwith start positions fromlefttorightare valid, contributingright - left + 1subarrays
Time Complexity
- Binary search:
O(log n)iterations - Each feasible check:
O(n)time (sliding window traverses array once) - Overall:
O(n log n)
Space Complexity
O(n)for the frequency map in the worst case (all elements distinct)
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 the solution with nums = [1, 2, 1].
Step 1: Setup
- Array length
n = 3 - Total subarrays:
m = 3 * 4 / 2 = 6 - Median position:
(6 + 1) / 2 = 3.5, so we need the 4th smallest element (ceiling of 3.5) - Binary search range:
l = 0,r = 3
Step 2: Binary Search Iterations
First iteration: mid = (0 + 3) / 2 = 1
- Check if at least 4 subarrays have ≤ 1 distinct element
- Using sliding window with
mx = 1:- Window
[1]: 1 distinct element, valid → count = 1 - Window
[2]: 1 distinct element, valid → count = 2 - Window
[1]: 1 distinct element, valid → count = 3 - Total: 3 subarrays with ≤ 1 distinct element
- Window
- Since 3 < 4,
check(1) = False - Update:
l = 2(search in upper half)
Second iteration: mid = (2 + 3) / 2 = 2
- Check if at least 4 subarrays have ≤ 2 distinct elements
- Using sliding window with
mx = 2:r = 0: Window[1]→ 1 distinct, add 1 subarray → count = 1r = 1: Window[1,2]→ 2 distinct, add 2 subarrays ([2]and[1,2]) → count = 3r = 2: Window[1,2,1]→ 2 distinct, add 3 subarrays ([1],[2,1],[1,2,1]) → count = 6- Total: 6 subarrays with ≤ 2 distinct elements
- Since 6 ≥ 4,
check(2) = True - Update:
r = 2(search in lower half to find smallest valid value)
Third iteration: Binary search converges
- We've found that 2 is the smallest value where at least 4 subarrays have that many or fewer distinct elements
- This means the median is 2
Verification: The actual uniqueness array (sorted) would be:
[1]→ 1 distinct[2]→ 1 distinct[1]→ 1 distinct[1,2]→ 2 distinct[2,1]→ 2 distinct[1,2,1]→ 2 distinct
Sorted: [1, 1, 1, 2, 2, 2] The 4th element (median for even-length array) is indeed 2.
The algorithm correctly found the median without constructing the entire array!
Time and Space Complexity
The time complexity is O(n × log n), where n is the length of the array nums.
Time Complexity Analysis:
- The outer binary search using
bisect_leftsearches over the range[0, n), which takesO(log n)iterations. - For each binary search iteration, the
checkfunction is called. - Inside the
checkfunction:- There's a sliding window approach with two pointers
landrthat traverse the array once. - Each element is visited at most twice (once when
rmoves right, once whenlmoves right). - The operations inside the loop (dictionary updates, comparisons) are
O(1)on average. - Therefore, each call to
checktakesO(n)time.
- There's a sliding window approach with two pointers
- Total time complexity:
O(log n) × O(n) = O(n × log n).
The space complexity is O(n).
Space Complexity Analysis:
- The
cntdictionary (defaultdict) stores the frequency of distinct elements in the current window. - In the worst case, all elements in the array are distinct, so the dictionary can contain up to
nentries. - The binary search and other variables use
O(1)additional space. - Total space complexity:
O(n).
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Using Wrong Binary Search Template Variant
Pitfall: Using the while left < right with right = mid pattern instead of the standard template:
# WRONG - Non-standard template variant while left < right: mid = (left + right) // 2 if feasible(mid): right = mid else: left = mid + 1 return left
Correct approach using the standard template:
left, right = 1, n first_true_index = -1 while left <= right: mid = (left + right) // 2 if feasible(mid): first_true_index = mid right = mid - 1 else: left = mid + 1 return first_true_index
The standard template explicitly tracks the answer with first_true_index, uses while left <= right, and always moves bounds by exactly 1 (right = mid - 1).
2. Incorrect Median Position Calculation
Pitfall: Using total_subarrays // 2 instead of (total_subarrays + 1) // 2 for the median position. This leads to finding the wrong element in the sorted uniqueness array.
Why it happens: For even-length arrays, we need the smaller of the two middle elements. The position (total_subarrays + 1) // 2 correctly gives us this position.
Solution: Always use (total_subarrays + 1) // 2 for the median position check.
3. Memory Leak with defaultdict
Pitfall: Not removing elements from the dictionary when their count reaches 0, causing incorrect distinct element counting.
# Wrong approach while len(element_count) > max_unique: element_count[nums[left]] -= 1 # Missing: del element_count[nums[left]] when count is 0 left += 1
Solution: Always remove dictionary entries when their count reaches 0:
while len(element_count) > max_unique: element_count[nums[left]] -= 1 if element_count[nums[left]] == 0: del element_count[nums[left]] left += 1
4. Misunderstanding the Binary Search Target
Pitfall: Thinking we're searching for the exact median value rather than the minimum number of distinct elements that satisfies our condition.
Solution: Remember that we find the first (smallest) value where the condition becomes True, which correctly gives us the median of the sorted uniqueness array.
Which of the following uses divide and conquer strategy?
Recommended Readings
https assets algo monster cover_photos Binary_Search svg Binary Search Intuition Binary search is an efficient array search algorithm It works by narrowing down the search range by half each time If you have looked up a word in a physical dictionary you've already used binary search in real life Let's
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!