2901. Longest Unequal Adjacent Groups Subsequence II
Problem Description
You are given two arrays of the same length n
:
words
: an array of stringsgroups
: an array of integers
The goal is to find the longest subsequence of indices from [0, 1, ..., n-1]
that satisfies two conditions:
-
Adjacent indices must have different groups: For any two consecutive indices in the subsequence, their corresponding group values must be different. That is, if the subsequence is
[i₀, i₁, ..., iₖ₋₁]
, thengroups[iⱼ] != groups[iⱼ₊₁]
for all validj
. -
Adjacent words must have Hamming distance of 1: For any two consecutive indices in the subsequence, their corresponding words must:
- Have equal length
- Have exactly one character different between them (Hamming distance = 1)
The Hamming distance between two strings of equal length is the count of positions where the characters differ. For example, the Hamming distance between "cat" and "bat" is 1 (only the first character differs).
Return an array containing the words corresponding to the selected indices in order. If multiple valid answers exist, you may return any of them.
Important Note: The strings in the words
array may have different lengths from each other, but for two words to be adjacent in the subsequence, they must have the same length.
Example clarification: If you select indices [1, 3, 5]
as your subsequence, then:
groups[1]
must not equalgroups[3]
groups[3]
must not equalgroups[5]
words[1]
andwords[3]
must have the same length and Hamming distance of 1words[3]
andwords[5]
must have the same length and Hamming distance of 1
Intuition
This problem asks us to find the longest subsequence with specific constraints between adjacent elements. This is a classic dynamic programming pattern where we need to track the longest valid subsequence ending at each position.
Think of it this way: for each word at position i
, we want to know "What's the longest valid subsequence that can end with this word?" To answer this, we need to look at all previous positions j
(where j < i
) and check:
- Can word
j
be placed right before wordi
in our subsequence? - If yes, what's the longest subsequence ending at
j
?
For word j
to come right before word i
, three conditions must hold:
- They must belong to different groups:
groups[i] != groups[j]
- They must have the same length
- They must differ by exactly one character (Hamming distance = 1)
If all conditions are met, then the longest subsequence ending at i
could be the longest subsequence ending at j
plus one more element (word i
itself). We check all valid predecessors and pick the one that gives us the maximum length.
The key insight is that we need to track two things:
f[i]
: the length of the longest valid subsequence ending at positioni
g[i]
: the predecessor index - which word comes right before positioni
in the optimal subsequence
By storing the predecessor indices in g
, we can reconstruct the actual subsequence by backtracking from the position with the maximum length. Starting from the best ending position, we follow the chain of predecessors (g[i]
points to the previous element) until we reach the beginning (where g[i] = -1
).
This approach ensures we find the globally optimal solution by considering all possible valid subsequences and keeping track of the best one at each position.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution uses dynamic programming with path reconstruction. Let's walk through the implementation step by step:
1. Initialize DP Arrays:
f[i]
stores the length of the longest valid subsequence ending at indexi
. Initialize all values to1
since each word by itself forms a valid subsequence of length 1.g[i]
stores the predecessor index for reconstruction. Initialize all values to-1
to indicate no predecessor.mx
tracks the maximum subsequence length found so far, starting at1
.
2. Helper Function:
def check(s: str, t: str) -> bool:
return len(s) == len(t) and sum(a != b for a, b in zip(s, t)) == 1
This function checks if two strings have the same length and exactly one character difference (Hamming distance = 1).
3. Build the DP Table:
For each position i
from 0
to n-1
:
- Look at all previous positions
j
from0
toi-1
- Check if position
j
can be a valid predecessor:- Groups must be different:
groups[i] != groups[j]
- Adding word
i
afterj
would create a longer subsequence:f[i] < f[j] + 1
- Words must satisfy the Hamming distance requirement:
check(words[i], words[j])
- Groups must be different:
- If all conditions are met:
- Update
f[i] = f[j] + 1
(extend the subsequence ending atj
) - Update
g[i] = j
(rememberj
as the predecessor) - Update
mx = max(mx, f[i])
(track the global maximum)
- Update
4. Reconstruct the Path:
- Find any index
i
wheref[i] == mx
(the end of an optimal subsequence) - Starting from this index, follow the predecessor chain:
j = i while j >= 0: ans.append(words[j]) j = g[j]
- This builds the subsequence in reverse order (from end to start)
5. Return the Result:
- Reverse the collected words using
ans[::-1]
to get them in the correct order
Time Complexity: O(n² × m)
where n
is the number of words and m
is the average length of words (for Hamming distance calculation).
Space Complexity: O(n)
for the DP arrays f
and g
.
The algorithm guarantees finding one of the longest valid subsequences by systematically checking all possible ways to build subsequences and keeping track of the best option at each position.
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 a small example to illustrate the solution approach:
Input:
words = ["bab", "dab", "cab", "a", "b"]
groups = [1, 2, 2, 1, 2]
Step 1: Initialize DP arrays
f = [1, 1, 1, 1, 1]
(each word alone forms a subsequence of length 1)g = [-1, -1, -1, -1, -1]
(no predecessors initially)mx = 1
Step 2: Build DP table
For i = 0
("bab", group 1):
- No previous elements to check
f[0] = 1
,g[0] = -1
For i = 1
("dab", group 2):
- Check
j = 0
: "bab" vs "dab"- Groups different? ✓ (1 ≠ 2)
- Would extend? ✓ (f[1]=1 < f[0]+1=2)
- Hamming distance = 1? ✓ (differ only at first character: 'b' vs 'd')
- Update:
f[1] = 2
,g[1] = 0
,mx = 2
For i = 2
("cab", group 2):
- Check
j = 0
: "bab" vs "cab"- Groups different? ✓ (1 ≠ 2)
- Would extend? ✓ (f[2]=1 < f[0]+1=2)
- Hamming distance = 1? ✓ (differ only at first character: 'b' vs 'c')
- Update:
f[2] = 2
,g[2] = 0
,mx = 2
- Check
j = 1
: "dab" vs "cab"- Groups different? ✗ (2 = 2) - Skip this pair
For i = 3
("a", group 1):
- Check
j = 0
: "bab" vs "a"- Same length? ✗ (3 ≠ 1) - Skip
- Check
j = 1
: "dab" vs "a"- Same length? ✗ (3 ≠ 1) - Skip
- Check
j = 2
: "cab" vs "a"- Same length? ✗ (3 ≠ 1) - Skip
For i = 4
("b", group 2):
- Check
j = 0
: "bab" vs "b"- Same length? ✗ (3 ≠ 1) - Skip
- Check
j = 1
: "dab" vs "b"- Same length? ✗ (3 ≠ 1) - Skip
- Check
j = 2
: "cab" vs "b"- Same length? ✗ (3 ≠ 1) - Skip
- Check
j = 3
: "a" vs "b"- Groups different? ✓ (1 ≠ 2)
- Would extend? ✓ (f[4]=1 < f[3]+1=2)
- Hamming distance = 1? ✓ (differ at one position: 'a' vs 'b')
- Update:
f[4] = 2
,g[4] = 3
,mx = 2
Final DP arrays:
f = [1, 2, 2, 1, 2]
g = [-1, 0, 0, -1, 3]
mx = 2
Step 3: Reconstruct the path
Find an index where f[i] == mx = 2
. We have options at indices 1, 2, and 4.
Let's choose i = 1
:
- Start at index 1:
words[1] = "dab"
, add to result - Follow predecessor:
g[1] = 0
- At index 0:
words[0] = "bab"
, add to result - Follow predecessor:
g[0] = -1
(stop)
Result list (in reverse): ["dab", "bab"]
Reverse to get final answer: ["bab", "dab"]
This gives us a valid subsequence of length 2 where:
- "bab" (group 1) and "dab" (group 2) have different groups ✓
- "bab" and "dab" have Hamming distance 1 ✓
Solution Implementation
1class Solution:
2 def getWordsInLongestSubsequence(
3 self, words: List[str], groups: List[int]
4 ) -> List[str]:
5 def is_one_char_different(word1: str, word2: str) -> bool:
6 """Check if two words have same length and differ by exactly one character."""
7 if len(word1) != len(word2):
8 return False
9 differences = sum(char1 != char2 for char1, char2 in zip(word1, word2))
10 return differences == 1
11
12 n = len(words)
13
14 # dp[i] stores the length of longest valid subsequence ending at index i
15 dp = [1] * n
16
17 # parent[i] stores the previous index in the longest subsequence ending at i
18 parent = [-1] * n
19
20 max_length = 1
21
22 # Build the longest subsequence using dynamic programming
23 for current_idx in range(n):
24 current_group = groups[current_idx]
25
26 # Check all previous words
27 for prev_idx in range(current_idx):
28 prev_group = groups[prev_idx]
29
30 # Check if we can extend the subsequence from prev_idx to current_idx
31 if (current_group != prev_group and
32 dp[current_idx] < dp[prev_idx] + 1 and
33 is_one_char_different(words[current_idx], words[prev_idx])):
34
35 dp[current_idx] = dp[prev_idx] + 1
36 parent[current_idx] = prev_idx
37 max_length = max(max_length, dp[current_idx])
38
39 # Reconstruct the longest subsequence
40 result = []
41
42 # Find the ending index of the longest subsequence
43 for i in range(n):
44 if dp[i] == max_length:
45 # Backtrack from this index to build the subsequence
46 current = i
47 while current >= 0:
48 result.append(words[current])
49 current = parent[current]
50 break
51
52 # Reverse to get the correct order
53 return result[::-1]
54
1class Solution {
2 /**
3 * Finds the longest subsequence of words where consecutive words belong to different groups
4 * and differ by exactly one character (Hamming distance = 1).
5 *
6 * @param words Array of words
7 * @param groups Array indicating the group of each word
8 * @return List of words forming the longest valid subsequence
9 */
10 public List<String> getWordsInLongestSubsequence(String[] words, int[] groups) {
11 int n = groups.length;
12
13 // dp[i] stores the length of longest subsequence ending at index i
14 int[] dp = new int[n];
15
16 // parent[i] stores the previous index in the longest subsequence ending at i
17 int[] parent = new int[n];
18
19 // Initialize: each word can form a subsequence of length 1
20 Arrays.fill(dp, 1);
21 Arrays.fill(parent, -1);
22
23 int maxLength = 1;
24
25 // Dynamic programming: for each word, check all previous words
26 for (int i = 0; i < n; ++i) {
27 for (int j = 0; j < i; ++j) {
28 // Check if we can extend the subsequence ending at j by adding word at i
29 // Conditions:
30 // 1. Different groups
31 // 2. Adding word[i] would create a longer subsequence
32 // 3. Words differ by exactly one character
33 if (groups[i] != groups[j] &&
34 dp[i] < dp[j] + 1 &&
35 isDifferByOneChar(words[i], words[j])) {
36
37 dp[i] = dp[j] + 1;
38 parent[i] = j;
39 maxLength = Math.max(maxLength, dp[i]);
40 }
41 }
42 }
43
44 // Reconstruct the longest subsequence
45 List<String> result = new ArrayList<>();
46
47 // Find the ending index of the longest subsequence
48 for (int i = 0; i < n; ++i) {
49 if (dp[i] == maxLength) {
50 // Backtrack from this index to build the subsequence
51 for (int currentIndex = i; currentIndex >= 0; currentIndex = parent[currentIndex]) {
52 result.add(words[currentIndex]);
53 }
54 break;
55 }
56 }
57
58 // Reverse to get the correct order (we built it backwards)
59 Collections.reverse(result);
60 return result;
61 }
62
63 /**
64 * Checks if two strings differ by exactly one character (Hamming distance = 1).
65 *
66 * @param s First string
67 * @param t Second string
68 * @return true if strings have same length and differ by exactly one character
69 */
70 private boolean isDifferByOneChar(String s, String t) {
71 // Different lengths means they can't differ by exactly one character
72 if (s.length() != t.length()) {
73 return false;
74 }
75
76 int differenceCount = 0;
77
78 // Count the number of differing characters
79 for (int i = 0; i < s.length(); ++i) {
80 if (s.charAt(i) != t.charAt(i)) {
81 ++differenceCount;
82 }
83 }
84
85 return differenceCount == 1;
86 }
87}
88
1class Solution {
2public:
3 vector<string> getWordsInLongestSubsequence(vector<string>& words, vector<int>& groups) {
4 // Lambda function to check if two strings differ by exactly one character
5 auto isDifferByOne = [](const string& str1, const string& str2) {
6 // Strings must have the same length
7 if (str1.size() != str2.size()) {
8 return false;
9 }
10
11 // Count the number of differing characters
12 int diffCount = 0;
13 for (int i = 0; i < str1.size(); ++i) {
14 if (str1[i] != str2[i]) {
15 diffCount++;
16 }
17 }
18
19 // Return true if exactly one character differs
20 return diffCount == 1;
21 };
22
23 int n = groups.size();
24
25 // dp[i] stores the length of longest valid subsequence ending at index i
26 vector<int> dp(n, 1);
27
28 // parent[i] stores the previous index in the longest subsequence ending at i
29 // -1 means no previous element
30 vector<int> parent(n, -1);
31
32 // Track the maximum length found
33 int maxLength = 1;
34
35 // Dynamic programming to find the longest valid subsequence
36 for (int i = 0; i < n; ++i) {
37 for (int j = 0; j < i; ++j) {
38 // Check if we can extend the subsequence ending at j to include i
39 // Conditions:
40 // 1. Different groups
41 // 2. Adding i would create a longer subsequence
42 // 3. Words differ by exactly one character
43 if (groups[i] != groups[j] &&
44 dp[i] < dp[j] + 1 &&
45 isDifferByOne(words[i], words[j])) {
46
47 dp[i] = dp[j] + 1;
48 parent[i] = j;
49 maxLength = max(maxLength, dp[i]);
50 }
51 }
52 }
53
54 // Reconstruct the result by backtracking from the ending index
55 vector<string> result;
56
57 // Find the index where the longest subsequence ends
58 for (int i = 0; i < n; ++i) {
59 if (dp[i] == maxLength) {
60 // Backtrack through parent pointers to build the subsequence
61 for (int j = i; j != -1; j = parent[j]) {
62 result.push_back(words[j]);
63 }
64 break;
65 }
66 }
67
68 // Reverse to get the correct order (we built it backwards)
69 reverse(result.begin(), result.end());
70
71 return result;
72 }
73};
74
1/**
2 * Finds the longest subsequence of words where consecutive words:
3 * 1. Belong to different groups
4 * 2. Differ by exactly one character (hamming distance = 1)
5 *
6 * @param words - Array of words
7 * @param groups - Array of group numbers corresponding to each word
8 * @returns Array of words forming the longest valid subsequence
9 */
10function getWordsInLongestSubsequence(words: string[], groups: number[]): string[] {
11 const wordCount: number = groups.length;
12
13 // dp[i] stores the length of longest subsequence ending at index i
14 const dp: number[] = Array(wordCount).fill(1);
15
16 // parent[i] stores the previous index in the longest subsequence ending at index i
17 // -1 means no parent (start of subsequence)
18 const parent: number[] = Array(wordCount).fill(-1);
19
20 // Track the maximum subsequence length found
21 let maxLength: number = 1;
22
23 /**
24 * Checks if two strings differ by exactly one character
25 * @param firstWord - First string to compare
26 * @param secondWord - Second string to compare
27 * @returns true if strings have same length and differ by exactly one character
28 */
29 const isDifferByOneChar = (firstWord: string, secondWord: string): boolean => {
30 // Different lengths cannot have hamming distance of 1
31 if (firstWord.length !== secondWord.length) {
32 return false;
33 }
34
35 let differenceCount: number = 0;
36 for (let charIndex = 0; charIndex < firstWord.length; charIndex++) {
37 if (firstWord[charIndex] !== secondWord[charIndex]) {
38 differenceCount++;
39 }
40 }
41
42 return differenceCount === 1;
43 };
44
45 // Build longest subsequence using dynamic programming
46 for (let currentIndex = 0; currentIndex < wordCount; currentIndex++) {
47 for (let previousIndex = 0; previousIndex < currentIndex; previousIndex++) {
48 // Check if we can extend subsequence ending at previousIndex to currentIndex
49 const isDifferentGroup: boolean = groups[currentIndex] !== groups[previousIndex];
50 const canExtend: boolean = dp[currentIndex] < dp[previousIndex] + 1;
51 const isValidTransition: boolean = isDifferByOneChar(words[currentIndex], words[previousIndex]);
52
53 if (isDifferentGroup && canExtend && isValidTransition) {
54 dp[currentIndex] = dp[previousIndex] + 1;
55 parent[currentIndex] = previousIndex;
56 maxLength = Math.max(maxLength, dp[currentIndex]);
57 }
58 }
59 }
60
61 // Reconstruct the longest subsequence
62 const result: string[] = [];
63
64 // Find an index where the longest subsequence ends
65 for (let index = 0; index < wordCount; index++) {
66 if (dp[index] === maxLength) {
67 // Trace back through parent pointers to build the subsequence
68 let currentPosition: number = index;
69 while (currentPosition !== -1) {
70 result.push(words[currentPosition]);
71 currentPosition = parent[currentPosition];
72 }
73 break;
74 }
75 }
76
77 // Reverse to get correct order (we built it backwards)
78 return result.reverse();
79}
80
Time and Space Complexity
Time Complexity: O(n^2 × L)
The algorithm uses a nested loop structure where:
- The outer loop iterates through all
n
words (wheren = len(groups)
) - The inner loop iterates through all previous words up to index
i
, which is at mostn-1
iterations - For each pair of words
(i, j)
where the groups differ, thecheck
function is called - The
check
function compares two strings character by character, takingO(L)
time whereL
is the length of the words being compared
Therefore, the total time complexity is O(n × n × L) = O(n^2 × L)
.
The final reconstruction of the answer path takes O(n)
time in the worst case, but this doesn't affect the overall complexity.
Space Complexity: O(n)
The algorithm uses:
- Array
f
of sizen
to store the length of longest subsequence ending at each index:O(n)
- Array
g
of sizen
to store the parent/predecessor index for path reconstruction:O(n)
- List
ans
to store the final answer, which can contain at mostn
words:O(n)
- A few scalar variables (
mx
,i
,j
, etc.):O(1)
The total space complexity is O(n) + O(n) + O(n) + O(1) = O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Hamming Distance Calculation for Different Length Strings
The Pitfall: A common mistake is attempting to calculate Hamming distance between strings of different lengths, either by:
- Not checking lengths first and causing index out of bounds errors
- Padding shorter strings or truncating longer ones, which violates the problem requirements
- Using edit distance instead of Hamming distance
Example of Incorrect Implementation:
# WRONG - This will give incorrect results for different length strings
def is_one_char_different(word1: str, word2: str) -> bool:
differences = 0
for i in range(min(len(word1), len(word2))):
if word1[i] != word2[i]:
differences += 1
return differences == 1 # Ignores length difference!
The Solution: Always check that strings have equal length before comparing characters:
def is_one_char_different(word1: str, word2: str) -> bool:
if len(word1) != len(word2):
return False # Different lengths mean Hamming distance is undefined
differences = sum(char1 != char2 for char1, char2 in zip(word1, word2))
return differences == 1
2. Breaking Early During Path Reconstruction
The Pitfall: When multiple indices have the maximum subsequence length, breaking after finding the first one might not give the lexicographically best or most desirable answer (though any valid answer is acceptable per the problem).
Example Scenario:
If dp = [1, 3, 2, 3]
with max_length = 3
, both indices 1 and 3 could be valid ending points, potentially leading to different valid subsequences.
The Solution: Either:
- Accept that any valid answer works (as the problem states)
- Or implement additional logic to select the "best" ending point based on specific criteria:
# Option 1: Find all possible endings and choose based on criteria
ending_indices = [i for i in range(n) if dp[i] == max_length]
# Then select based on your preference (e.g., lexicographically smallest result)
# Option 2: Simply use the first valid one (current implementation)
for i in range(n):
if dp[i] == max_length:
# Use this ending
break
3. Forgetting to Initialize DP Base Cases
The Pitfall: Not initializing dp[i] = 1
for all indices, assuming that the DP will handle single-element subsequences automatically.
Why It Matters: Each word by itself forms a valid subsequence of length 1. Without proper initialization, the algorithm might miss valid subsequences or produce incorrect lengths.
The Solution: Always initialize:
dp = [1] * n # Every word is a valid subsequence of length 1 parent = [-1] * n # No predecessor for single-element subsequences
What's the relationship between a tree and a graph?
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!