Facebook Pixel

2901. Longest Unequal Adjacent Groups Subsequence II

Problem Description

You are given two arrays of the same length n:

  • words: an array of strings
  • groups: an array of integers

The goal is to find the longest subsequence of indices from [0, 1, ..., n-1] that satisfies two conditions:

  1. 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ₖ₋₁], then groups[iⱼ] != groups[iⱼ₊₁] for all valid j.

  2. 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 equal groups[3]
  • groups[3] must not equal groups[5]
  • words[1] and words[3] must have the same length and Hamming distance of 1
  • words[3] and words[5] must have the same length and Hamming distance of 1
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 word i 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:

  1. They must belong to different groups: groups[i] != groups[j]
  2. They must have the same length
  3. 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 position i
  • g[i]: the predecessor index - which word comes right before position i 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 index i. Initialize all values to 1 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 at 1.

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 from 0 to i-1
  • Check if position j can be a valid predecessor:
    • Groups must be different: groups[i] != groups[j]
    • Adding word i after j would create a longer subsequence: f[i] < f[j] + 1
    • Words must satisfy the Hamming distance requirement: check(words[i], words[j])
  • If all conditions are met:
    • Update f[i] = f[j] + 1 (extend the subsequence ending at j)
    • Update g[i] = j (remember j as the predecessor)
    • Update mx = max(mx, f[i]) (track the global maximum)

4. Reconstruct the Path:

  • Find any index i where f[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 Evaluator

Example 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 (where n = len(groups))
  • The inner loop iterates through all previous words up to index i, which is at most n-1 iterations
  • For each pair of words (i, j) where the groups differ, the check function is called
  • The check function compares two strings character by character, taking O(L) time where L 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 size n to store the length of longest subsequence ending at each index: O(n)
  • Array g of size n to store the parent/predecessor index for path reconstruction: O(n)
  • List ans to store the final answer, which can contain at most n 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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What's the relationship between a tree and a graph?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More