3177. Find the Maximum Length of a Good Subsequence II
Problem Description
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]
where seq[i] != seq[i + 1]
. In other words, a good sequence can have at most k
positions where adjacent elements are different.
Your task is to find the maximum possible length of a good subsequence of nums
.
A subsequence is a sequence that can be derived from the array by deleting some or no elements without changing the order of the remaining elements.
For example:
- If we have a sequence
[1, 1, 2, 2, 3]
andk = 2
, there are exactly 2 positions where adjacent elements differ: between the second1
and first2
, and between the second2
and3
. So this would be a valid good sequence. - If
k = 0
, then the good sequence must have all elements the same (no position where adjacent elements differ). - If
k = 1
, then the good sequence can have at most one position where adjacent elements change.
The goal is to select a subsequence from nums
that satisfies the "good" property while having the maximum possible length.
Intuition
The key insight is to think about this problem in terms of dynamic programming where we track the longest good subsequence ending at each position with a specific number of "changes" used.
Let's think about building a subsequence step by step. When we're at position i
and want to include nums[i]
in our subsequence, we need to decide what element came before it. We have two choices:
-
Same element: If the previous element in our subsequence is the same as
nums[i]
, we don't use any additional "change" positions. We want to find the longest subsequence ending with the same value that uses at mosth
changes. -
Different element: If the previous element is different from
nums[i]
, we consume one "change" position. So we need to look at subsequences with at mosth-1
changes and addnums[i]
to them.
This leads us to define f[i][h]
as the length of the longest good subsequence ending at index i
with at most h
changes used.
For each position i
and change count h
, we examine all previous positions j < i
:
- If
nums[i] == nums[j]
, we can extend the subsequence ending atj
without using an additional change:f[i][h] = max(f[i][h], f[j][h] + 1)
- If
nums[i] != nums[j]
andh > 0
, we can extend the subsequence ending atj
by using one change:f[i][h] = max(f[i][h], f[j][h-1] + 1)
The challenge with this approach is efficiency. Checking all previous positions for each i
gives us O(n²k)
complexity, which is too slow for large inputs.
The optimization comes from recognizing patterns in what we're looking for:
- For same elements, we just need the maximum
f[j][h]
wherenums[j] == nums[i]
. We can maintain this using a map for each change count. - For different elements, we need the maximum
f[j][h-1]
across all different values. Since we only care about the maximum (and sometimes second maximum when the maximum corresponds tonums[i]
), we can maintain just the top two values for each change count.
This optimization reduces our lookups from O(n)
to O(1)
for each state calculation.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution implements the optimized dynamic programming approach using several key data structures:
Data Structures
-
f[i][h]
: A 2D array wheref[i][h]
represents the length of the longest good subsequence ending at indexi
with at mosth
changes. -
mp[h]
: An array of dictionaries wheremp[h][x]
stores the maximum length of a good subsequence ending with valuex
using at mosth
changes. This helps us quickly find the best subsequence to extend when we encounter the same value. -
g[h]
: A 2D array of size(k+1) × 3
that tracks:g[h][0]
: The value corresponding to the maximum lengthg[h][1]
: The maximum length of subsequences with at mosth
changesg[h][2]
: The second maximum length (needed when the maximum corresponds to the current value)
Algorithm Steps
For each element nums[i]
and each possible change count h
from 0 to k
:
-
Initialize with same value extension:
f[i][h] = mp[h][x]
This handles extending subsequences that end with the same value
x = nums[i]
. -
Consider different value extension (if
h > 0
):if g[h-1][0] != nums[i]: f[i][h] = max(f[i][h], g[h-1][1]) else: f[i][h] = max(f[i][h], g[h-1][2])
- If the best subsequence with
h-1
changes doesn't end withnums[i]
, we can use it directly - Otherwise, we use the second-best to avoid counting the same value twice
- If the best subsequence with
-
Add current element:
f[i][h] += 1
-
Update the map for future same-value lookups:
mp[h][nums[i]] = max(mp[h][nums[i]], f[i][h])
-
Update the global maximum tracker
g[h]
:- If
x != g[h][0]
(different from current maximum value):- If
f[i][h] >= g[h][1]
, update both maximum and second maximum - Otherwise, just update second maximum if needed
- If
- If
x == g[h][0]
(same as current maximum value):- Just update the maximum length
- If
-
Track the overall answer:
ans = max(ans, f[i][h])
State Transition
The core state transition follows:
But instead of checking all j < i
, we use the optimized lookups through mp
and g
.
Time and Space Complexity
- Time Complexity:
O(n × k)
wheren
is the length of the array andk
is the maximum allowed changes - Space Complexity:
O(n × k)
for the DP table, plusO(k × m)
for the maps wherem
is the number of distinct values
This optimization reduces the time complexity from O(n² × k)
to O(n × k)
, making it efficient enough to handle large inputs.
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, 1, 3]
and k = 2
.
We want to find the longest subsequence where adjacent elements can differ at most 2 times.
Initialization:
f[i][h]
: DP table (5×3 since we have 5 elements and k+1=3 possible change counts)mp[h]
: Array of 3 dictionaries for quick same-value lookupsg[h]
: Array tracking [best_value, max_length, second_max_length] for each h
Processing each element:
i=0, nums[0]=1:
- For h=0,1,2:
- No previous elements, so
f[0][h] = 0 + 1 = 1
- Update
mp[h][1] = 1
- Update
g[h] = [1, 1, 0]
(value 1 has length 1)
- No previous elements, so
i=1, nums[1]=2:
- For h=0:
- Same value:
mp[0][2] = 0
(no previous 2's) - Different value: Can't use (need h>0 for changes)
f[1][0] = 0 + 1 = 1
- Same value:
- For h=1:
- Same value:
mp[1][2] = 0
- Different value: Best with h=0 is
g[0][1] = 1
(from value 1) f[1][1] = max(0, 1) + 1 = 2
(subsequence: [1,2])
- Same value:
- For h=2:
- Similar logic:
f[1][2] = 2
- Similar logic:
- Update maps and g arrays accordingly
i=2, nums[2]=1:
- For h=0:
- Same value:
mp[0][1] = 1
(from i=0) f[2][0] = 1 + 1 = 2
(subsequence: [1,1])
- Same value:
- For h=1:
- Same value:
mp[1][1] = 1
- Different value: Best with h=0 is from value 2,
g[0][1] = 1
f[2][1] = max(1, 1) + 1 = 2
- Same value:
- For h=2:
- Same value:
mp[2][1] = 1
- Different value: Best with h=1 is
g[1][1] = 2
(from value 2) f[2][2] = max(1, 2) + 1 = 3
(subsequence: [1,2,1])
- Same value:
i=3, nums[3]=1:
- For h=0:
- Same value:
mp[0][1] = 2
(updated from i=2) f[3][0] = 2 + 1 = 3
(subsequence: [1,1,1])
- Same value:
- For h=1:
- Same value:
mp[1][1] = 2
- Different value: Best with h=0 from non-1 value is 1
f[3][1] = max(2, 1) + 1 = 3
- Same value:
- For h=2:
- Same value:
mp[2][1] = 3
(updated from i=2) - Different value: Best with h=1 from non-1 value is 2
f[3][2] = max(3, 2) + 1 = 4
(subsequence: [1,2,1,1])
- Same value:
i=4, nums[4]=3:
- For h=0:
- Same value:
mp[0][3] = 0
f[4][0] = 0 + 1 = 1
- Same value:
- For h=1:
- Same value:
mp[1][3] = 0
- Different value: Best with h=0 is
g[0][1] = 3
(from value 1) f[4][1] = max(0, 3) + 1 = 4
(subsequence: [1,1,1,3])
- Same value:
- For h=2:
- Same value:
mp[2][3] = 0
- Different value: Best with h=1 is
g[1][1] = 3
(from value 1) f[4][2] = max(0, 3) + 1 = 4
- Same value:
Final Answer: Maximum across all f[i][h] values = 4
The optimal subsequence is [1,2,1,1] or [1,1,1,3], both having length 4 with at most 2 changes between adjacent elements.
Solution Implementation
1class Solution:
2 def maximumLength(self, nums: List[int], k: int) -> int:
3 n = len(nums)
4
5 # dp[i][j] = maximum length of subsequence ending at index i with at most j adjacent differences
6 dp = [[0] * (k + 1) for _ in range(n)]
7
8 # max_len_by_value[j][value] = maximum length of subsequence ending with 'value' using at most j differences
9 max_len_by_value = [defaultdict(int) for _ in range(k + 1)]
10
11 # top_two[j] stores the top two maximum lengths for j differences:
12 # top_two[j][0] = value that achieves the maximum length
13 # top_two[j][1] = maximum length
14 # top_two[j][2] = second maximum length (for different value)
15 top_two = [[0] * 3 for _ in range(k + 1)]
16
17 max_length = 0
18
19 for i, current_value in enumerate(nums):
20 for num_differences in range(k + 1):
21 # Start with the best length ending with the same value
22 dp[i][num_differences] = max_len_by_value[num_differences][current_value]
23
24 # If we can use one more difference, consider transitioning from a different value
25 if num_differences > 0:
26 # If the best value from (num_differences - 1) is different from current value
27 if top_two[num_differences - 1][0] != current_value:
28 # Use the best length from previous difference count
29 dp[i][num_differences] = max(dp[i][num_differences],
30 top_two[num_differences - 1][1])
31 else:
32 # Current value matches the best, so use second best
33 dp[i][num_differences] = max(dp[i][num_differences],
34 top_two[num_differences - 1][2])
35
36 # Add current element to the subsequence
37 dp[i][num_differences] += 1
38
39 # Update the maximum length for current value with num_differences
40 max_len_by_value[num_differences][current_value] = max(
41 max_len_by_value[num_differences][current_value],
42 dp[i][num_differences]
43 )
44
45 # Update top two maximum lengths for num_differences
46 if top_two[num_differences][0] != current_value:
47 # Current value is different from the best value
48 if dp[i][num_differences] >= top_two[num_differences][1]:
49 # New best found, shift previous best to second
50 top_two[num_differences][2] = top_two[num_differences][1]
51 top_two[num_differences][1] = dp[i][num_differences]
52 top_two[num_differences][0] = current_value
53 else:
54 # Update second best if necessary
55 top_two[num_differences][2] = max(top_two[num_differences][2],
56 dp[i][num_differences])
57 else:
58 # Current value matches the best value, update the best length
59 top_two[num_differences][1] = max(top_two[num_differences][1],
60 dp[i][num_differences])
61
62 # Update global maximum
63 max_length = max(max_length, dp[i][num_differences])
64
65 return max_length
66
1class Solution {
2 public int maximumLength(int[] nums, int k) {
3 int n = nums.length;
4
5 // dp[i][j] represents the maximum length of subsequence ending at index i with j differences
6 int[][] dp = new int[n][k + 1];
7
8 // maxLengthByValue[j] stores the maximum length for each value at j differences
9 Map<Integer, Integer>[] maxLengthByValue = new HashMap[k + 1];
10 Arrays.setAll(maxLengthByValue, i -> new HashMap<>());
11
12 // topTwo[j] maintains top 2 maximum lengths at j differences
13 // topTwo[j][0]: value with maximum length
14 // topTwo[j][1]: maximum length
15 // topTwo[j][2]: second maximum length
16 int[][] topTwo = new int[k + 1][3];
17
18 int maxLength = 0;
19
20 // Process each element in the array
21 for (int i = 0; i < n; ++i) {
22 // Try all possible number of differences from 0 to k
23 for (int diff = 0; diff <= k; ++diff) {
24 // Case 1: Extend a subsequence ending with the same value
25 dp[i][diff] = maxLengthByValue[diff].getOrDefault(nums[i], 0);
26
27 // Case 2: Extend a subsequence with one less difference by adding a different value
28 if (diff > 0) {
29 if (topTwo[diff - 1][0] != nums[i]) {
30 // Current value is different from the value with max length
31 dp[i][diff] = Math.max(dp[i][diff], topTwo[diff - 1][1]);
32 } else {
33 // Current value is same as the value with max length, use second max
34 dp[i][diff] = Math.max(dp[i][diff], topTwo[diff - 1][2]);
35 }
36 }
37
38 // Include current element in the subsequence
39 ++dp[i][diff];
40
41 // Update the maximum length for current value at diff differences
42 maxLengthByValue[diff].merge(nums[i], dp[i][diff], Integer::max);
43
44 // Update top two maximum lengths for diff differences
45 if (topTwo[diff][0] != nums[i]) {
46 // Current value is different from the tracked value
47 if (dp[i][diff] >= topTwo[diff][1]) {
48 // New maximum found
49 topTwo[diff][2] = topTwo[diff][1];
50 topTwo[diff][1] = dp[i][diff];
51 topTwo[diff][0] = nums[i];
52 } else {
53 // Update second maximum if needed
54 topTwo[diff][2] = Math.max(topTwo[diff][2], dp[i][diff]);
55 }
56 } else {
57 // Current value is same as the tracked value, update its maximum
58 topTwo[diff][1] = Math.max(topTwo[diff][1], dp[i][diff]);
59 }
60
61 // Update global maximum length
62 maxLength = Math.max(maxLength, dp[i][diff]);
63 }
64 }
65
66 return maxLength;
67 }
68}
69
1class Solution {
2public:
3 int maximumLength(vector<int>& nums, int k) {
4 int n = nums.size();
5
6 // dp[i][j] represents the maximum length of subsequence ending at index i with j changes
7 vector<vector<int>> dp(n, vector<int>(k + 1));
8
9 // lastMaxLength[j][value] stores the maximum length of subsequence ending with 'value' using j changes
10 vector<unordered_map<int, int>> lastMaxLength(k + 1);
11
12 // topTwo[j] stores the top two maximum lengths for j changes
13 // topTwo[j][0]: the value that achieves the maximum length
14 // topTwo[j][1]: the maximum length
15 // topTwo[j][2]: the second maximum length
16 vector<vector<int>> topTwo(k + 1, vector<int>(3));
17
18 int maxLength = 0;
19
20 // Iterate through each element in the array
21 for (int i = 0; i < n; ++i) {
22 // Try all possible number of changes from 0 to k
23 for (int changes = 0; changes <= k; ++changes) {
24 // Case 1: Extend a subsequence ending with the same value (no additional change needed)
25 dp[i][changes] = lastMaxLength[changes][nums[i]];
26
27 // Case 2: Extend a subsequence ending with a different value (requires one more change)
28 if (changes > 0) {
29 // If the current value is different from the value achieving max length
30 if (topTwo[changes - 1][0] != nums[i]) {
31 dp[i][changes] = max(dp[i][changes], topTwo[changes - 1][1]);
32 } else {
33 // If current value is same as the max value, use second max
34 dp[i][changes] = max(dp[i][changes], topTwo[changes - 1][2]);
35 }
36 }
37
38 // Include current element in the subsequence
39 ++dp[i][changes];
40
41 // Update the maximum length for subsequences ending with nums[i] using 'changes' changes
42 lastMaxLength[changes][nums[i]] = max(lastMaxLength[changes][nums[i]], dp[i][changes]);
43
44 // Update top two maximum lengths for 'changes' changes
45 if (topTwo[changes][0] != nums[i]) {
46 // Current value is different from the stored max value
47 if (dp[i][changes] >= topTwo[changes][1]) {
48 // New maximum found
49 topTwo[changes][2] = topTwo[changes][1]; // Previous max becomes second max
50 topTwo[changes][1] = dp[i][changes]; // Update max
51 topTwo[changes][0] = nums[i]; // Update value achieving max
52 } else {
53 // Update second max if necessary
54 topTwo[changes][2] = max(topTwo[changes][2], dp[i][changes]);
55 }
56 } else {
57 // Current value is same as the stored max value, just update the max length
58 topTwo[changes][1] = max(topTwo[changes][1], dp[i][changes]);
59 }
60
61 // Update global maximum
62 maxLength = max(maxLength, dp[i][changes]);
63 }
64 }
65
66 return maxLength;
67 }
68};
69
1function maximumLength(nums: number[], k: number): number {
2 const n = nums.length;
3
4 // dp[i][j] represents the maximum length of subsequence ending at index i with j changes
5 const dp: number[][] = Array.from({ length: n }, () => Array(k + 1).fill(0));
6
7 // valueToMaxLength[j] stores the maximum length for each value at j changes
8 const valueToMaxLength: Map<number, number>[] = Array.from({ length: k + 1 }, () => new Map());
9
10 // globalMax[j] stores [value, maxLength, secondMaxLength] for j changes
11 // globalMax[j][0]: the value with maximum length
12 // globalMax[j][1]: the maximum length
13 // globalMax[j][2]: the second maximum length
14 const globalMax: number[][] = Array.from({ length: k + 1 }, () => Array(3).fill(0));
15
16 let maxLength = 0;
17
18 // Iterate through each element in the array
19 for (let i = 0; i < n; i++) {
20 // Try all possible number of changes from 0 to k
21 for (let changes = 0; changes <= k; changes++) {
22 // Get the maximum length ending with the same value (nums[i]) at current changes
23 dp[i][changes] = valueToMaxLength[changes].get(nums[i]) || 0;
24
25 // If we can use a change, consider transitioning from previous state
26 if (changes > 0) {
27 // If the best value from previous state is different from current value
28 if (globalMax[changes - 1][0] !== nums[i]) {
29 // Use the maximum length from previous state
30 dp[i][changes] = Math.max(dp[i][changes], globalMax[changes - 1][1]);
31 } else {
32 // Current value matches the best value, use second maximum
33 dp[i][changes] = Math.max(dp[i][changes], globalMax[changes - 1][2]);
34 }
35 }
36
37 // Include current element in the subsequence
38 dp[i][changes]++;
39
40 // Update the maximum length for current value at current changes
41 valueToMaxLength[changes].set(
42 nums[i],
43 Math.max(valueToMaxLength[changes].get(nums[i]) || 0, dp[i][changes])
44 );
45
46 // Update global maximum and second maximum for current changes
47 if (globalMax[changes][0] !== nums[i]) {
48 // Current value is different from the tracked best value
49 if (dp[i][changes] >= globalMax[changes][1]) {
50 // New maximum found, shift previous max to second max
51 globalMax[changes][2] = globalMax[changes][1];
52 globalMax[changes][1] = dp[i][changes];
53 globalMax[changes][0] = nums[i];
54 } else {
55 // Update second maximum if necessary
56 globalMax[changes][2] = Math.max(globalMax[changes][2], dp[i][changes]);
57 }
58 } else {
59 // Current value matches the tracked best value, just update maximum
60 globalMax[changes][1] = Math.max(globalMax[changes][1], dp[i][changes]);
61 }
62
63 // Update the overall maximum length
64 maxLength = Math.max(maxLength, dp[i][changes]);
65 }
66 }
67
68 return maxLength;
69}
70
Time and Space Complexity
Time Complexity: O(n × k)
The algorithm iterates through each element in the array nums
(n iterations). For each element, it performs operations across all possible values from 0 to k (k+1 iterations). Within each iteration, all operations are constant time:
- Dictionary lookups and updates in
mp[h][x]
areO(1)
on average - Array accesses and comparisons in
f[i][h]
andg[h]
areO(1)
- The max operations and conditional updates are
O(1)
Therefore, the total time complexity is O(n × (k + 1))
= O(n × k)
.
Space Complexity: O(n × k)
The space usage consists of:
f
: A 2D array of sizen × (k + 1)
, contributingO(n × k)
spacemp
: A list ofk + 1
dictionaries. In the worst case, each dictionary could store up ton
unique elements, givingO(n × k)
spaceg
: A 2D array of size(k + 1) × 3
, contributingO(k)
space- Other variables like
ans
,i
,x
,h
useO(1)
space
The dominant space complexity is O(n × k)
from the f
array and mp
dictionaries.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Initialization of the Top-Two Tracker
Pitfall: The most critical issue is initializing top_two[j][0]
to 0, which can conflict with actual values in the array that are also 0. This causes incorrect comparisons when checking if top_two[num_differences - 1][0] != current_value
.
Example of the Bug:
nums = [0, 1, 0, 2], k = 2 # When processing the first 0, top_two[0][0] is already 0 (initial value) # This incorrectly makes the algorithm think there's already a subsequence ending with 0
Solution: Initialize the value field with a sentinel value that cannot appear in the input:
# Instead of:
top_two = [[0] * 3 for _ in range(k + 1)]
# Use:
top_two = [[None, 0, 0] for _ in range(k + 1)]
# Or use a value outside the input range like -float('inf') or -1 if nums contains only non-negative integers
Then update the comparison logic:
if top_two[num_differences - 1][0] is None or top_two[num_differences - 1][0] != current_value:
dp[i][num_differences] = max(dp[i][num_differences],
top_two[num_differences - 1][1])
2. Misunderstanding the Second Maximum Update Logic
Pitfall: When updating top_two
, the code only updates the second maximum when the current value differs from the best value. However, there could be multiple distinct values achieving different lengths, and we need to track the true second maximum across all values.
Example Issue:
# Consider sequence: [1, 2, 3, 1] with k = 2 # After processing indices 0, 1, 2: # top_two[2] might be [3, 3, 2] (value 3 with length 3, second best is 2) # When processing index 3 (value 1), we might miss updating the second maximum correctly
Solution: Maintain proper second maximum tracking:
if top_two[num_differences][0] != current_value: if dp[i][num_differences] >= top_two[num_differences][1]: # New maximum found top_two[num_differences][2] = top_two[num_differences][1] top_two[num_differences][1] = dp[i][num_differences] top_two[num_differences][0] = current_value elif dp[i][num_differences] > top_two[num_differences][2]: # Not the best, but better than second best top_two[num_differences][2] = dp[i][num_differences]
3. Edge Case with k = 0
Pitfall: When k = 0
, the subsequence must have all identical elements. The transition logic for different values should never be triggered, but the code structure might still attempt it if not careful.
Solution: Ensure the condition if num_differences > 0:
properly guards the different-value transition, which the current code already does correctly.
4. Memory Optimization Opportunity Missed
Pitfall: The code maintains a full n × (k+1)
DP table, but only the current row is actually needed for the final result.
Solution for Space Optimization:
# Instead of dp[i][num_differences], use a 1D array that gets updated: current_dp = [0] * (k + 1) # Update in place for each element
However, this optimization trades off code clarity for space efficiency and should be considered based on constraints.
Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!