3176. Find the Maximum Length of a Good Subsequence I
Problem Description
You are given an integer array nums
and a non-negative integer k
.
A sequence is called good if it has at most k
positions where consecutive elements are different. More specifically, for a sequence seq
, there can be at most k
indices i
(where 0 ≤ i ≤ seq.length - 2
) such that seq[i] != seq[i + 1]
.
Your task is to find the maximum possible length of a good subsequence that can be formed from 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
, this is a good sequence because there are exactly 2 positions where consecutive elements differ: between the second1
and first2
, and between the second2
and3
. - If
k = 0
, then the good subsequence must have all elements the same (no position where consecutive elements differ). - If
k = 1
, then the good subsequence can have at most one position where consecutive elements change.
The goal is to select elements from nums
(maintaining their relative order) to form the longest possible subsequence that satisfies the "good" condition with the given k
.
Intuition
The key insight is that we need to track two things as we build our subsequence: which element we're currently at, and how many "changes" (positions where consecutive elements differ) we've used so far.
Think of it this way: when we're deciding whether to include an element in our subsequence, we need to know:
- What was the last element we included?
- How many changes have we already made?
This naturally leads to a dynamic programming approach where we define our state as f[i][h]
= the maximum length of a good subsequence that ends at position i
and uses at most h
changes.
For each position i
, we look back at all previous positions j
(where j < i
) and consider two cases:
-
If
nums[i] == nums[j]
: Addingnums[i]
afternums[j]
doesn't create a change (consecutive elements are the same), so we can extend the subsequence ending atj
without using an additional change:f[i][h] = f[j][h] + 1
. -
If
nums[i] != nums[j]
and we have changes left (h > 0
): Addingnums[i]
afternums[j]
creates a change, so we need to use one of our allowed changes. We extend the subsequence that ended atj
withh-1
changes:f[i][h] = f[j][h-1] + 1
.
By considering all possible previous positions j
and all possible numbers of changes h
from 0 to k
, we can build up the maximum length subsequence. The answer is the maximum value among all f[i][k]
values, representing the longest good subsequence that ends at any position and uses at most k
changes.
The base case is simple: any single element forms a valid subsequence of length 1 with 0 changes, so f[i][h] = 1
initially for all valid i
and h
.
Learn more about Dynamic Programming patterns.
Solution Approach
We implement the dynamic programming solution using a 2D array f
where f[i][h]
represents the maximum length of a good subsequence ending at index i
with at most h
changes.
Initialization:
- Create a 2D array
f
of sizen × (k+1)
wheren = len(nums)
- Initialize all values to 1, since any single element forms a valid subsequence of length 1
Main Algorithm:
For each position i
from 0 to n-1
:
- For each allowed number of changes
h
from 0 tok
:- For each previous position
j
from 0 toi-1
:- Check if we can extend the subsequence ending at
j
to includenums[i]
- Check if we can extend the subsequence ending at
- For each previous position
Transition Formula: The state transition follows these rules:
-
When
nums[i] == nums[j]
: We can appendnums[i]
to the subsequence ending atj
without using any additional change, so we updatef[i][h] = max(f[i][h], f[j][h] + 1)
-
When
nums[i] != nums[j]
andh > 0
: We need to use one change to appendnums[i]
afternums[j]
, so we take the subsequence that ended atj
withh-1
changes and extend it:f[i][h] = max(f[i][h], f[j][h-1] + 1)
Finding the Answer:
After filling the DP table, the answer is the maximum value of f[i][k]
for all positions i
. This represents the longest good subsequence that can end at any position while using at most k
changes.
Time Complexity: O(n² × k)
where n
is the length of the array, as we have three nested loops.
Space Complexity: O(n × k)
for the DP table.
Ready to land your dream job?
Unlock your dream job with a 3-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 with at most 2 positions where consecutive elements differ.
Initialize DP table f[i][h]
:
f
has dimensions 5×3 (5 elements, k+1=3 columns for 0,1,2 changes)- All values start at 1 (each element alone is a valid subsequence)
Initial f: h=0 h=1 h=2 i=0: 1 1 1 i=1: 1 1 1 i=2: 1 1 1 i=3: 1 1 1 i=4: 1 1 1
Process each position:
i=0 (nums[0]=1): Base case, no previous elements to consider.
i=1 (nums[1]=2):
- Look at j=0 (nums[0]=1):
- nums[1]≠nums[0] (2≠1), so we need a change
- For h=1: f[1][1] = max(1, f[0][0]+1) = max(1, 2) = 2
- For h=2: f[1][2] = max(1, f[0][1]+1) = max(1, 2) = 2
i=2 (nums[2]=1):
- Look at j=0 (nums[0]=1):
- nums[2]=nums[0] (1=1), no change needed
- For h=0,1,2: f[2][h] = max(1, f[0][h]+1) = 2
- Look at j=1 (nums[1]=2):
- nums[2]≠nums[1] (1≠2), need a change
- For h=1: f[2][1] = max(2, f[1][0]+1) = max(2, 2) = 2
- For h=2: f[2][2] = max(2, f[1][1]+1) = max(2, 3) = 3
i=3 (nums[3]=1):
- Look at j=0 (nums[0]=1):
- nums[3]=nums[0] (1=1), no change needed
- For all h: f[3][h] = max(1, f[0][h]+1) = 2
- Look at j=1 (nums[1]=2):
- nums[3]≠nums[1] (1≠2), need a change
- For h=1: f[3][1] = max(2, f[1][0]+1) = 2
- For h=2: f[3][2] = max(2, f[1][1]+1) = 3
- Look at j=2 (nums[2]=1):
- nums[3]=nums[2] (1=1), no change needed
- For h=0: f[3][0] = max(2, f[2][0]+1) = 3
- For h=1: f[3][1] = max(2, f[2][1]+1) = 3
- For h=2: f[3][2] = max(3, f[2][2]+1) = 4
i=4 (nums[4]=3):
- Look at j=0,1,2,3 and update accordingly
- Most importantly, j=3 (nums[3]=1):
- nums[4]≠nums[3] (3≠1), need a change
- For h=1: f[4][1] = max(1, f[3][0]+1) = 4
- For h=2: f[4][2] = max(1, f[3][1]+1) = 4
Final DP table:
h=0 h=1 h=2 i=0: 1 1 1 i=1: 1 2 2 i=2: 2 2 3 i=3: 3 3 4 i=4: 1 4 4
Answer: Maximum of f[i][2] for all i = 4
The longest good subsequence is [1, 2, 1, 1] or [2, 1, 1, 3], both with length 4 and exactly 2 changes.
Solution Implementation
1class Solution:
2 def maximumLength(self, nums: List[int], k: int) -> int:
3 """
4 Find the maximum length of a subsequence where at most k pairs
5 of adjacent elements are different.
6
7 Args:
8 nums: List of integers
9 k: Maximum number of adjacent pairs that can be different
10
11 Returns:
12 Maximum length of valid subsequence
13 """
14 n = len(nums)
15
16 # dp[i][j] represents the maximum length of subsequence ending at index i
17 # with exactly j pairs of adjacent elements that are different
18 dp = [[1] * (k + 1) for _ in range(n)]
19
20 max_length = 0
21
22 # Iterate through each position as potential end of subsequence
23 for current_idx, current_val in enumerate(nums):
24 # Try all possible counts of different adjacent pairs (0 to k)
25 for diff_count in range(k + 1):
26 # Check all previous positions as potential previous element
27 for prev_idx in range(current_idx):
28 prev_val = nums[prev_idx]
29
30 if current_val == prev_val:
31 # Same values: no new difference introduced
32 dp[current_idx][diff_count] = max(
33 dp[current_idx][diff_count],
34 dp[prev_idx][diff_count] + 1
35 )
36 elif diff_count > 0:
37 # Different values: use one difference allowance
38 dp[current_idx][diff_count] = max(
39 dp[current_idx][diff_count],
40 dp[prev_idx][diff_count - 1] + 1
41 )
42
43 # Update maximum length found so far
44 max_length = max(max_length, dp[current_idx][k])
45
46 return max_length
47
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
6 // with at most j pairs of adjacent elements that are different
7 int[][] dp = new int[n][k + 1];
8 int maxLength = 0;
9
10 // Iterate through each position in the array
11 for (int i = 0; i < n; i++) {
12 // Try different number of allowed differences (0 to k)
13 for (int differences = 0; differences <= k; differences++) {
14 // Check all previous positions to build the subsequence
15 for (int j = 0; j < i; j++) {
16 if (nums[i] == nums[j]) {
17 // If current and previous elements are equal,
18 // we can extend without using an additional difference
19 dp[i][differences] = Math.max(dp[i][differences], dp[j][differences]);
20 } else if (differences > 0) {
21 // If current and previous elements are different,
22 // we need to use one difference to extend the subsequence
23 dp[i][differences] = Math.max(dp[i][differences], dp[j][differences - 1]);
24 }
25 }
26 // Include the current element in the subsequence
27 dp[i][differences]++;
28 }
29 // Update the maximum length found so far
30 maxLength = Math.max(maxLength, dp[i][k]);
31 }
32
33 return maxLength;
34 }
35}
36
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
7 // with at most j pairs of adjacent elements that are different
8 int dp[n][k + 1];
9 memset(dp, 0, sizeof(dp));
10
11 int maxLength = 0;
12
13 // Iterate through each element as potential ending position
14 for (int i = 0; i < n; ++i) {
15 // Try all possible values of allowed differences (0 to k)
16 for (int diffCount = 0; diffCount <= k; ++diffCount) {
17 // Check all previous elements to extend the subsequence
18 for (int j = 0; j < i; ++j) {
19 if (nums[i] == nums[j]) {
20 // Same elements: no additional difference needed
21 dp[i][diffCount] = max(dp[i][diffCount], dp[j][diffCount]);
22 } else if (diffCount > 0) {
23 // Different elements: use one difference allowance
24 dp[i][diffCount] = max(dp[i][diffCount], dp[j][diffCount - 1]);
25 }
26 }
27 // Include current element in the subsequence
28 ++dp[i][diffCount];
29 }
30 // Update the maximum length found so far
31 maxLength = max(maxLength, dp[i][k]);
32 }
33
34 return maxLength;
35 }
36};
37
1/**
2 * Finds the maximum length of a subsequence where at most k pairs of adjacent elements are different
3 * @param nums - The input array of numbers
4 * @param k - Maximum number of adjacent pairs that can be different
5 * @returns The maximum length of valid subsequence
6 */
7function maximumLength(nums: number[], k: number): number {
8 const arrayLength: number = nums.length;
9
10 // dp[i][j] represents the maximum length of subsequence ending at index i
11 // with exactly j pairs of different adjacent elements
12 const dp: number[][] = Array.from(
13 { length: arrayLength },
14 () => Array(k + 1).fill(0)
15 );
16
17 let maxLength: number = 0;
18
19 // Iterate through each position in the array
20 for (let currentIndex = 0; currentIndex < arrayLength; ++currentIndex) {
21 // Try all possible counts of different adjacent pairs (0 to k)
22 for (let diffCount = 0; diffCount <= k; ++diffCount) {
23 // Check all previous positions to build subsequence
24 for (let previousIndex = 0; previousIndex < currentIndex; ++previousIndex) {
25 if (nums[currentIndex] === nums[previousIndex]) {
26 // If elements are same, no additional different pair is created
27 dp[currentIndex][diffCount] = Math.max(
28 dp[currentIndex][diffCount],
29 dp[previousIndex][diffCount]
30 );
31 } else if (diffCount > 0) {
32 // If elements are different and we have budget for different pairs
33 dp[currentIndex][diffCount] = Math.max(
34 dp[currentIndex][diffCount],
35 dp[previousIndex][diffCount - 1]
36 );
37 }
38 }
39 // Include current element in the subsequence
40 ++dp[currentIndex][diffCount];
41 }
42 // Update maximum length found so far
43 maxLength = Math.max(maxLength, dp[currentIndex][k]);
44 }
45
46 return maxLength;
47}
48
Time and Space Complexity
Time Complexity: O(n^2 × k)
The code contains three nested loops:
- The outer loop iterates through all elements in
nums
, runningn
times - The middle loop iterates through all possible values of
h
from0
tok
, runningk + 1
times - The inner loop iterates through all previous elements before index
i
, which in the worst case isi
elements
For each position i
, we examine all previous positions j < i
for each value of h
. This gives us approximately n × (k + 1) × n/2
operations, which simplifies to O(n^2 × k)
.
Space Complexity: O(n × k)
The space is primarily consumed by the 2D array f
, which has dimensions n × (k + 1)
. Each cell stores a single integer value representing the maximum length of a subsequence ending at position i
with exactly h
differences from the pattern. The additional variables (n
, ans
, loop variables) use O(1)
space, so the overall space complexity is O(n × k)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Understanding of the Problem Constraint
A common misunderstanding is thinking that k
represents the total number of different elements allowed in the subsequence, rather than the number of positions where consecutive elements differ.
Wrong interpretation:
# Counting unique elements instead of transitions
def wrong_solution(nums, k):
unique_count = 0
result = []
for num in nums:
if num not in result:
if unique_count < k:
result.append(num)
unique_count += 1
else:
result.append(num)
return len(result)
Correct understanding: k
limits the number of "change points" between consecutive elements in the subsequence, not the variety of elements.
2. Off-by-One Error in DP State Definition
Many implementations mistakenly define dp[i][j]
as "subsequence ending at i with exactly j changes" when they actually need "at most j changes."
Problematic approach:
# Only tracking exact count, missing optimal solutions
for current_idx in range(n):
for exact_changes in range(k + 1): # Treating as exact count
# Missing solutions with fewer changes
...
Solution: Ensure the DP state allows for using fewer changes than the maximum:
# When same values, we can use the same number of changes
if current_val == prev_val:
dp[current_idx][diff_count] = max(
dp[current_idx][diff_count],
dp[prev_idx][diff_count] + 1
)
3. Inefficient Final Answer Extraction
A subtle pitfall is only checking dp[i][k]
for the final answer, potentially missing optimal solutions that use fewer than k
changes.
Incomplete answer extraction:
# Only checking exactly k changes at the last position return dp[n-1][k]
Correct approach:
max_length = 0
# Check all positions and all valid change counts
for i in range(n):
for j in range(k + 1):
max_length = max(max_length, dp[i][j])
return max_length
However, the provided solution correctly handles this by checking dp[current_idx][k]
which already represents "at most k changes" due to the DP transitions.
4. Memory Optimization Opportunity Missed
While not incorrect, the solution uses O(n × k) space when it could potentially be optimized using rolling arrays or hash maps for sparse states.
Space-optimized alternative using dictionary:
def maximumLength(self, nums: List[int], k: int) -> int:
n = len(nums)
# Use dictionary to store only necessary states
dp = {}
for i in range(n):
for changes in range(k + 1):
dp[(i, changes)] = 1
for j in range(i):
if nums[i] == nums[j]:
dp[(i, changes)] = max(
dp.get((i, changes), 1),
dp.get((j, changes), 1) + 1
)
elif changes > 0:
dp[(i, changes)] = max(
dp.get((i, changes), 1),
dp.get((j, changes - 1), 1) + 1
)
return max(dp[(i, k)] for i in range(n))
This approach saves memory when k is large and the actual number of states used is sparse.
Which of the following problems can be solved with backtracking (select multiple)
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!