Find the Maximum Length of a Good Subsequence I
MedPrompt
You are given an integer array nums and a non-negative integer k. A sequence of integers seq is called good if there are at most k indices i in the range [0, seq.length - 2] such that seq[i] != seq[i + 1].
Return the maximum possible length of a good subsequence of nums.
Example 1:
Input: nums = [1,2,1,1,3], k = 2
Output: 4
Explanation:
The maximum length subsequence is [1,2,1,1,3].
Example 2:
Input: nums = [1,2,3,4,5,1], k = 0
Output: 2
Explanation:
The maximum length subsequence is [1,2,3,4,5,1].
Constraints:
1 <= nums.length <= 5001 <= nums[i] <= 1090 <= k <= min(nums.length, 25)
Approaches
2 approaches with complexity analysis and trade-offs.
This approach uses a straightforward dynamic programming solution. We define a 2D DP table, dp[i][j], to store the maximum length of a good subsequence that ends with the element nums[i] and has exactly j mismatches. To compute dp[i][j], we iterate through all previous elements nums[p] (where p < i) and consider extending the subsequences ending at p. This leads to a cubic time complexity relative to the input size and k.
Algorithm
- State Definition: Let
dp[i][j]be the maximum length of a good subsequence that ends with the elementnums[i]and has exactlyjmismatches. - Initialization:
- Create a 2D array
dpof sizen x (k+1), wherenis the length ofnums. - Initialize all entries
dp[i][j]to 1. This represents the base case where the subsequence consists of only the elementnums[i], which has a length of 1 and 0 mismatches.
- Create a 2D array
- Transitions:
- Iterate through the array
numswith indexifrom0ton-1. - For each
i, iterate through all previous indicespfrom0toi-1. - For each pair
(i, p), we consider extending the subsequences that ended atpwith the elementnums[i].- Case 1:
nums[i] == nums[p]- If the current element
nums[i]is the same as the previous elementnums[p], no new mismatch is created. - We can extend any subsequence ending at
pwithjmismatches. The new length will be1 + dp[p][j]. - So, for each
jfrom0tok, we update:dp[i][j] = max(dp[i][j], 1 + dp[p][j]).
- If the current element
- Case 2:
nums[i] != nums[p]- If
nums[i]is different fromnums[p], a new mismatch is created. - We can extend a subsequence ending at
pthat hadj-1mismatches. This is only possible ifj > 0. - The new length will be
1 + dp[p][j-1]. - So, for each
jfrom1tok, we update:dp[i][j] = max(dp[i][j], 1 + dp[p][j-1]).
- If
- Case 1:
- Iterate through the array
- Final Answer:
- The problem asks for the maximum length of a good subsequence with at most
kmismatches. This means we need to find the maximum value in the entiredptable, as any entrydp[i][j]represents a valid good subsequence. - The overall maximum length is the maximum value found in
dpafter all iterations.
- The problem asks for the maximum length of a good subsequence with at most
Walkthrough
The core idea is to build up solutions for subsequences of increasing length. For each element nums[i], we consider it as the potential end of a new, longer subsequence. We look back at all preceding elements nums[p] and see if we can append nums[i] to a subsequence ending at nums[p]. The number of mismatches j is a crucial part of our state, as it determines whether we can append nums[i] if it's different from nums[p]. By systematically building this dp table, we ensure that we have considered all possible valid subsequences.
class Solution { public int maximumLength(int[] nums, int k) { int n = nums.length; // dp[i][j]: max length of a good subsequence ending at index i with j mismatches. int[][] dp = new int[n][k + 1]; int maxLength = 0; for (int i = 0; i < n; i++) { // Initialize dp table for index i. A single element has length 1 and 0 mismatches. for (int j = 0; j <= k; j++) { dp[i][j] = 1; } // Iterate through all previous elements to extend their subsequences. for (int p = 0; p < i; p++) { if (nums[i] == nums[p]) { // No new mismatch. for (int j = 0; j <= k; j++) { dp[i][j] = Math.max(dp[i][j], 1 + dp[p][j]); } } else { // New mismatch is created. for (int j = 1; j <= k; j++) { dp[i][j] = Math.max(dp[i][j], 1 + dp[p][j - 1]); } } } } // The answer is the maximum value in the entire dp table. for (int i = 0; i < n; i++) { for (int j = 0; j <= k; j++) { maxLength = Math.max(maxLength, dp[i][j]); } } return maxLength; }}Complexity
Time
O(n^2 * k), where `n` is the number of elements in `nums`. We have three nested loops: iterating through `i` from `0` to `n-1`, `p` from `0` to `i-1`, and `j` from `0` to `k`.
Space
O(n * k), where `n` is the number of elements in `nums`. This is for the 2D `dp` array.
Trade-offs
Pros
The logic is a direct translation of the problem's recursive structure, making it relatively easy to understand.
It correctly solves the problem and passes within the given constraints.
Cons
The time complexity of
O(n^2 * k)can be slow if the constraints onnwere larger.It is less efficient than the optimized DP approach.
This approach optimizes the previous DP solution by eliminating the O(n) inner loop. Instead of re-scanning all previous elements, we maintain the necessary information to make the transition in O(1) time. We keep track of two things: the maximum length of a subsequence ending with a specific value for a given number of mismatches, and the overall maximum length for a given number of mismatches.
Algorithm
- State Definition: We use two data structures to track the state:
dp[j]: A hash map for each mismatch countj.dp[j][v]stores the maximum length of a good subsequence with exactlyjmismatches that ends with the valuev.maxLen[j]: An array wheremaxLen[j]stores the maximum length of any good subsequence withjmismatches found so far, irrespective of its ending value.
- Initialization:
- Initialize
dpas an array ofk+1empty hash maps. - Initialize
maxLenas an array of sizek+1with all zeros. - Initialize
ans = 1(assumingn > 0), as the minimum possible answer is 1.
- Initialize
- Transitions:
- Iterate through each
numin the input arraynums. - For each
num, iterate through the number of mismatchesjfromkdown to0. The downward iteration is crucial to prevent using information from the currentnum's updates in the same pass. - For each
j, calculate the new maximum length for a subsequence ending innum:len_same = 1 + dp[j].getOrDefault(num, 0): This is the length if we extend a previous subsequence that also ended innum(no new mismatch).len_diff = 1 + maxLen[j-1](ifj > 0): This is the length if we extend any subsequence withj-1mismatches (a new mismatch is formed).
- The new length for a subsequence ending in
numwithjmismatches iscurrentMax = max(len_same, len_diff). - Update the state:
dp[j].put(num, currentMax)andmaxLen[j] = max(maxLen[j], currentMax).
- Iterate through each
- Final Answer:
- The overall maximum length is tracked in a variable
ans, which is updated in each step:ans = max(ans, currentMax). After iterating through all numbers,ansholds the result.
- The overall maximum length is tracked in a variable
Walkthrough
The key insight for optimization is that to calculate the new length for a subsequence ending in num with j mismatches, we only need two pieces of information from the state before processing num:
- The maximum length of a subsequence ending in
numwithjmismatches. - The maximum length of any subsequence with
j-1mismatches.
By storing these two types of information and updating them as we iterate through nums, we avoid the expensive O(n) scan for every element. We use an array of hash maps, dp, to store the first piece of information, and a simple array, maxLen, for the second. This reduces the time complexity from O(n^2 * k) to O(n * k).
import java.util.HashMap;import java.util.Map; class Solution { public int maximumLength(int[] nums, int k) { int n = nums.length; if (n == 0) { return 0; } // dp[j] is a map from value to max length of a subsequence ending with that value, using j mismatches. Map<Integer, Integer>[] dp = new HashMap[k + 1]; for (int j = 0; j <= k; j++) { dp[j] = new HashMap<>(); } // maxLen[j] stores the max length of any subsequence with j mismatches. int[] maxLen = new int[k + 1]; int ans = 1; for (int num : nums) { // Iterate j from k down to 0 to use results from the previous element's iteration. for (int j = k; j >= 0; j--) { // Case 1: Extend a subsequence ending with the same value `num`. // No new mismatch is created. int lenSame = 1 + dp[j].getOrDefault(num, 0); // Case 2: Extend a subsequence ending with a different value. // A new mismatch is created. We need to have used j-1 mismatches before. int lenDiff = 0; if (j > 0) { lenDiff = 1 + maxLen[j - 1]; } // The new max length for a subsequence ending in `num` with `j` mismatches. int currentMax = Math.max(lenSame, lenDiff); // Update the DP state for the current number and mismatch count. dp[j].put(num, currentMax); // Update the overall max length for `j` mismatches. maxLen[j] = Math.max(maxLen[j], currentMax); // Update the global answer. ans = Math.max(ans, currentMax); } } return ans; }}Complexity
Time
O(n * k), where `n` is the length of `nums`. We iterate through each of the `n` numbers, and for each, we perform `k+1` updates. Hash map operations take, on average, `O(1)` time.
Space
O(n * k) in the worst case. If all elements in `nums` are distinct, each of the `k+1` maps could store up to `n` entries. The `maxLen` array takes `O(k)` space.
Trade-offs
Pros
Highly efficient with a time complexity of
O(n * k), which is optimal for the given constraints.Effectively handles large value ranges for
nums[i]by using hash maps.
Cons
The state management is more complex, involving hash maps and an auxiliary array.
The space complexity can be
O(n*k)in the worst-case scenario where all numbers are distinct.
Solutions
Solution
class Solution {public int maximumLength(int[] nums, int k) { int n = nums.length; int[][] f = new int[n][k + 1]; int ans = 0; for (int i = 0; i < n; ++i) { for (int h = 0; h <= k; ++h) { for (int j = 0; j < i; ++j) { if (nums[i] == nums[j]) { f[i][h] = Math.max(f[i][h], f[j][h]); } else if (h > 0) { f[i][h] = Math.max(f[i][h], f[j][h - 1]); } } ++f[i][h]; } ans = Math.max(ans, f[i][k]); } return ans; }}class Solution {public: int maximumLength(vector<int> &nums, int k) { int n = nums.size(); int f[n][k + 1]; memset(f, 0, sizeof(f)); int ans = 0; for (int i = 0; i < n; ++i) { for (int h = 0; h <= k; ++h) { for (int j = 0; j < i; ++j) { if (nums[i] == nums[j]) { f[i][h] = max(f[i][h], f[j][h]); } else if (h) { f[i][h] = max(f[i][h], f[j][h - 1]); } } ++f[i][h]; } ans = max(ans, f[i][k]); } return ans; }};class Solution: def maximumLength(self, nums: List[int], k: int) -> int: n = len(nums) f = [[1] * (k + 1) for _ in range(n)] ans = 0 for i, x in enumerate(nums): for h in range(k + 1): for j, y in enumerate(nums[: i]): if x == y: f[i][h] = max(f[i][h], f[j][h] + 1) elif h: f[i][h] = max(f[i][h], f[j][h - 1] + 1) ans = max(ans, f[i][k]) return ansVideo 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.