Sorting Three Groups
MedPrompt
You are given an integer array nums. Each element in nums is 1, 2 or 3. In each operation, you can remove an element from nums. Return the minimum number of operations to make nums non-decreasing.
Example 1:
Input: nums = [2,1,3,2,1]
Output: 3
Explanation:
One of the optimal solutions is to remove nums[0], nums[2] and nums[3].
Example 2:
Input: nums = [1,3,2,1,3,3]
Output: 2
Explanation:
One of the optimal solutions is to remove nums[1] and nums[2].
Example 3:
Input: nums = [2,2,2,2,3,3]
Output: 0
Explanation:
nums is already non-decreasing.
Constraints:
1 <= nums.length <= 1001 <= nums[i] <= 3
Follow-up: Can you come up with an algorithm that runs in
O(n) time complexity?Approaches
3 approaches with complexity analysis and trade-offs.
This approach considers all possible ways to partition the array into three segments. The final non-decreasing array will consist of some number of 1s, followed by 2s, then 3s. We can imagine two split points, i and j, that define these three groups. The first group (indices 0 to i-1) contributes 1s, the second (i to j-1) contributes 2s, and the third (j to n-1) contributes 3s. We iterate through all possible pairs of (i, j) and calculate how many elements we would keep for that partition. The goal is to maximize the number of kept elements.
Algorithm
- Initialize
maxKeptto 0 andnto the length ofnums. - Use a loop to iterate
ifrom0ton. Thisirepresents the boundary after which we no longer keep1s. - Inside this loop, use another loop to iterate
jfromiton. Thisjrepresents the boundary after which we no longer keep2s. - For each pair of
(i, j), we calculate the number of elements we can keep:- Initialize
currentKeptto 0. - A third loop iterates from
k = 0toi-1to count the1s. - A fourth loop iterates from
k = itoj-1to count the2s. - A fifth loop iterates from
k = jton-1to count the3s.
- Initialize
- Update
maxKept = Math.max(maxKept, currentKept). - After all loops complete, the minimum number of removals is
n - maxKept.
Walkthrough
The core idea is to exhaustively check every possible structure of the final sorted array. A non-decreasing array of 1s, 2s, and 3s must be of the form [1,...,1, 2,...,2, 3,...,3]. This structure can be defined by two boundaries: one where the 1s end and 2s begin, and another where the 2s end and 3s begin. We can represent these boundaries with indices i and j.
We use two nested loops to iterate through all possible split points i and j, where 0 <= i <= j <= n.
For each pair (i, j), we define three segments of the original array: nums[0...i-1], nums[i...j-1], and nums[j...n-1].
We then iterate through these segments to count the number of elements we can keep:
- Count the number of
1s in the first segment. - Count the number of
2s in the second segment. - Count the number of
3s in the third segment.
The sum of these counts gives the size of the non-decreasing subsequence for this specific partition. We keep track of the maximum size found across all partitions. Finally, the minimum number of removals is the total number of elements n minus the maximum size of the subsequence we found.
class Solution { public int minimumOperations(int[] nums) { int n = nums.length; int maxKept = 0; // i is the split point before which we only keep 1s // j is the split point before which we only keep 1s or 2s for (int i = 0; i <= n; i++) { for (int j = i; j <= n; j++) { int currentKept = 0; // Count 1s in the first part [0, i-1] for (int k = 0; k < i; k++) { if (nums[k] == 1) { currentKept++; } } // Count 2s in the second part [i, j-1] for (int k = i; k < j; k++) { if (nums[k] == 2) { currentKept++; } } // Count 3s in the third part [j, n-1] for (int k = j; k < n; k++) { if (nums[k] == 3) { currentKept++; } } maxKept = Math.max(maxKept, currentKept); } } return n - maxKept; }}Complexity
Time
O(n³). The two outer loops for `i` and `j` run `O(n²)` times. Inside, the counting loops take `O(n)` time in total, leading to a cubic time complexity.
Space
O(1). We only use a few variables to store counts and loop indices.
Trade-offs
Pros
Simple to understand and implement.
Correctly explores all possible valid final array structures.
Cons
Extremely inefficient with a time complexity of O(n³).
Will result in a 'Time Limit Exceeded' error on platforms like LeetCode for the given constraints.
This approach is an optimization of the brute force method. The bottleneck in the previous approach was re-calculating the counts of 1s, 2s, and 3s for each partition (i, j). We can pre-calculate the counts of each number up to every index in the array. This is known as a prefix sum (or prefix count) array. With these pre-calculated values, we can find the count of a number within any range [start, end] in O(1) time, which reduces the overall complexity.
Algorithm
- First, create three prefix count arrays
prefixOnes,prefixTwos, andprefixThrees, each of sizen+1. - Iterate through
numsfromi = 0ton-1to populate these arrays. For each indexk,prefixOnes[k+1]will store the count of1s innums[0...k], and similarly for the other numbers. - Initialize
maxKeptto 0. - Use two nested loops to iterate through all split points
ifrom0tonandjfromiton. - Inside the loops, calculate the number of kept elements in O(1) time:
keptOnes = prefixOnes[i]keptTwos = prefixTwos[j] - prefixTwos[i]keptThrees = prefixThrees[n] - prefixThrees[j]
- Update
maxKept = Math.max(maxKept, keptOnes + keptTwos + keptThrees). - The final result is
n - maxKept.
Walkthrough
To improve upon the O(n³) brute-force solution, we can optimize the counting step. Instead of recounting elements for every pair of (i, j), we can precompute the counts. We'll use three arrays to store the prefix counts of 1s, 2s, and 3s.
prefixOnes[k]will store the total count of1s innums[0...k-1].prefixTwos[k]will store the total count of2s innums[0...k-1].prefixThrees[k]will store the total count of3s innums[0...k-1].
These arrays can be built in a single pass through nums in O(n) time. After this precomputation, we can find the count of any number in any subarray nums[start...end] in O(1) time by subtracting prefix counts. For example, the number of 2s in nums[i...j-1] is prefixTwos[j] - prefixTwos[i].
The main logic of iterating through all (i, j) pairs remains, but the work inside the loops is now constant time. This brings the total time complexity down from O(n³) to O(n²).
class Solution { public int minimumOperations(int[] nums) { int n = nums.length; int[] prefixOnes = new int[n + 1]; int[] prefixTwos = new int[n + 1]; int[] prefixThrees = new int[n + 1]; for (int i = 0; i < n; i++) { prefixOnes[i + 1] = prefixOnes[i] + (nums[i] == 1 ? 1 : 0); prefixTwos[i + 1] = prefixTwos[i] + (nums[i] == 2 ? 1 : 0); prefixThrees[i + 1] = prefixThrees[i] + (nums[i] == 3 ? 1 : 0); } int maxKept = 0; for (int i = 0; i <= n; i++) { for (int j = i; j <= n; j++) { int keptOnes = prefixOnes[i]; int keptTwos = prefixTwos[j] - prefixTwos[i]; int keptThrees = prefixThrees[n] - prefixThrees[j]; maxKept = Math.max(maxKept, keptOnes + keptTwos + keptThrees); } } return n - maxKept; }}Complexity
Time
O(n²). Pre-computation takes `O(n)`. The nested loops for `i` and `j` run `O(n²)` times, with `O(1)` work inside.
Space
O(n). We use three arrays of size `n+1` to store the prefix counts.
Trade-offs
Pros
Significantly faster than the naive O(n³) brute force approach.
The logic is still based on the intuitive partitioning idea.
Cons
Has a quadratic time complexity, which is not optimal.
Uses extra space proportional to the input size.
The most efficient solution uses dynamic programming. The problem is equivalent to finding the longest non-decreasing subsequence of nums. Since the elements are only 1, 2, or 3, the structure of this subsequence is simple: a block of 1s, followed by a block of 2s, followed by a block of 3s. We can keep track of the length of the longest non-decreasing subsequence ending in 1, 2, or 3 as we iterate through the array, which allows us to solve the problem in a single pass.
Algorithm
- Initialize three counters:
len1,len2,len3to 0. These will track the length of the longest non-decreasing subsequence ending in 1, 2, and 3, respectively. - Iterate through each
numin the input arraynums. - Inside the loop, update the counters based on the value of
num:- If
num == 1: We can extend a sequence of 1s. Incrementlen1. - If
num == 2: A 2 can follow a 1 or another 2. To get the longest sequence, we takemax(len1, len2)and add 1. Updatelen2with this value. - If
num == 3: A 3 can follow a 1, 2, or 3. We takemax(len1, len2, len3)and add 1. Updatelen3with this value.
- If
- After the loop, the length of the longest non-decreasing subsequence is
max(len1, len2, len3). - The minimum number of operations is
n - max(len1, len2, len3).
Walkthrough
This approach rephrases the problem from minimizing removals to maximizing the number of elements kept. The elements we keep must form a non-decreasing subsequence. Given the constraints, this subsequence will be composed of zero or more 1s, followed by zero or more 2s, followed by zero or more 3s.
We can find the length of the longest such subsequence using dynamic programming. We maintain three variables representing the state:
len1: The length of the longest non-decreasing subsequence seen so far that consists only of1s.len2: The length of the longest non-decreasing subsequence seen so far that consists of1s and2s, ending with a2.len3: The length of the longest non-decreasing subsequence seen so far, ending with a3.
We iterate through the input array nums once. For each element num:
- If
numis 1: It can only extend a sequence of1s. So, we incrementlen1. - If
numis 2: It can start a new sequence of2s after a sequence of1s (lengthlen1 + 1) or extend an existing sequence of2s (lengthlen2 + 1). We take the maximum, solen2 = max(len1, len2) + 1. - If
numis 3: It can follow any of the previous subsequences. We take the maximum length so far (max(len1, len2, len3)) and add 1. So,len3 = max(len1, len2, len3) + 1.
After iterating through all numbers, the overall longest non-decreasing subsequence length is the maximum of len1, len2, and len3. The minimum removals is n minus this length.
class Solution { public int minimumOperations(int[] nums) { int n = nums.length; // len1: length of LIS ending with 1 // len2: length of LIS ending with 2 // len3: length of LIS ending with 3 int len1 = 0, len2 = 0, len3 = 0; for (int num : nums) { if (num == 1) { len1++; } else if (num == 2) { len2 = Math.max(len1, len2) + 1; } else { // num == 3 len3 = Math.max(len1, Math.max(len2, len3)) + 1; } } // The length of the longest non-decreasing subsequence is the max of the lengths of subsequences ending in 1, 2, or 3. // Note that a sequence of only 1s is a valid non-decreasing subsequence. int maxLen = Math.max(len1, Math.max(len2, len3)); return n - maxLen; }}Complexity
Time
O(n). We iterate through the input array only once.
Space
O(1). We only use a few constant extra variables to store the DP state.
Trade-offs
Pros
Optimal solution with linear time complexity.
Extremely efficient in terms of space, using only a constant number of variables.
Satisfies the follow-up question for an O(n) solution.
Cons
The logic might be slightly less intuitive to come up with compared to the brute-force approaches.
Solutions
Solution
class Solution {public int minimumOperations(List<Integer> nums) { int[] f = new int[3]; for (int x : nums) { int[] g = new int[3]; if (x == 1) { g[0] = f[0]; g[1] = Math.min(f[0], f[1]) + 1; g[2] = Math.min(f[0], Math.min(f[1], f[2])) + 1; } else if (x == 2) { g[0] = f[0] + 1; g[1] = Math.min(f[0], f[1]); g[2] = Math.min(f[0], Math.min(f[1], f[2])) + 1; } else { g[0] = f[0] + 1; g[1] = Math.min(f[0], f[1]) + 1; g[2] = Math.min(f[0], Math.min(f[1], f[2])); } f = g; } return Math.min(f[0], Math.min(f[1], f[2])); }}class Solution {public: int minimumOperations(vector<int> &nums) { vector<int> f(3); for (int x : nums) { vector<int> g(3); if (x == 1) { g[0] = f[0]; g[1] = min(f[0], f[1]) + 1; g[2] = min({f[0], f[1], f[2]}) + 1; } else if (x == 2) { g[0] = f[0] + 1; g[1] = min(f[0], f[1]); g[2] = min(f[0], min(f[1], f[2])) + 1; } else { g[0] = f[0] + 1; g[1] = min(f[0], f[1]) + 1; g[2] = min(f[0], min(f[1], f[2])); } f = move(g); } return min({f[0], f[1], f[2]}); }};class Solution: def minimumOperations(self, nums: List[int]) -> int: f = g = h = 0 for x in nums: ff = gg = hh = 0 if x == 1: ff = f gg = min(f, g) + 1 hh = min(f, g, h) + 1 elif x == 2: ff = f + 1 gg = min(f, g) hh = min(f, g, h) + 1 else: ff = f + 1 gg = min(f, g) + 1 hh = min(f, g, h) f, g, h = ff, gg, hh return min(f, g, h)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.