Facebook Pixel

809. Expressive Words

Problem Description

This problem is about identifying "stretchy" words that can be transformed into a target string through character repetition.

Given a string s that represents an "expressive" word (where letters may be repeated for emphasis), and an array of query strings words, you need to determine how many words from the array can be "stretched" to match s.

A word is considered stretchy if it can be transformed into s by applying extension operations. An extension operation works as follows:

  • Choose a group of consecutive identical characters c in the word
  • Add more characters c to that group so the final group size is at least 3

For example:

  • Starting with "hello", you can extend the group "o" to get "hellooo" (group size becomes 3)
  • You cannot get "helloo" because the group "oo" has size 2, which is less than 3
  • You can also extend "ll" to "lllll" to get "helllllooo"

The key rules for a word to be stretchy:

  1. Each character group in the original word must match the corresponding character group in s
  2. If a character group in s has 3 or more characters, the corresponding group in the word can have fewer characters (as it can be extended)
  3. If a character group in s has less than 3 characters, the corresponding group in the word must have exactly the same count
  4. A word cannot have more character groups or different characters than s

The task is to count how many words from the words array are stretchy (can be transformed into s).

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that we need to compare character groups between the target string s and each query word. When we look at strings like "heeellooo" and "hello", we can see they're made up of groups of consecutive identical characters.

For "heeellooo": ["h", "eee", "ll", "ooo"] For "hello": ["h", "e", "ll", "o"]

The fundamental question is: can we extend the groups in the query word to match the groups in s?

This leads us to think about the rules for valid extensions:

  • We can only add characters to a group, never remove them
  • We can only extend a group to size 3 or more

From these constraints, we can derive the matching conditions between groups:

  1. If the target group in s has count c1 and the query group has count c2, then c1 must be >= c2 (we can't remove characters)
  2. If c1 >= 3, then any c2 <= c1 works (the group is already extended or can be extended)
  3. If c1 < 3, then c1 must equal c2 exactly (we can't extend a group to less than 3)

This naturally suggests a two-pointer approach where we:

  • Move through both strings simultaneously
  • Count consecutive identical characters in both strings
  • Check if the counts satisfy our extension rules
  • Continue until we've processed both strings completely

The solution becomes a matter of implementing this group-by-group comparison, ensuring that every group in the query word has a valid corresponding group in s, and that we've consumed both strings entirely (no extra characters in either string).

Learn more about Two Pointers patterns.

Solution Approach

The solution uses a two-pointer technique to compare character groups between the target string s and each query word t. Here's how the implementation works:

Main Function Structure: The main function iterates through each word in the words array and uses a helper function check(s, t) to determine if word t can be stretched to match s. It counts how many words return true.

The check Function Implementation:

  1. Initial Length Check:

    • First, compare lengths: if len(t) > len(s), return false immediately
    • This is because we can only add characters, never remove them
  2. Two-Pointer Traversal:

    • Initialize pointers i = 0 for string s and j = 0 for string t
    • Continue while both pointers are within bounds
  3. Character Matching:

    • At each position, check if s[i] != t[j]
    • If characters don't match, return false (different characters can't be made equal through stretching)
  4. Count Consecutive Characters:

    • For string s: Count consecutive occurrences of s[i] starting from position i, store as c1
    • For string t: Count consecutive occurrences of t[j] starting from position j, store as c2
    • Move pointers i and j forward by c1 and c2 respectively
  5. Validate Group Counts: Apply the stretching rules:

    • If c1 < c2: Return false (can't remove characters)
    • If c1 < 3 and c1 != c2: Return false (groups with less than 3 characters must match exactly)
    • Otherwise, the group is valid (either c1 >= 3 allowing stretching, or c1 == c2 for exact match)
  6. Final Verification:

    • After the loop, check if both i == len(s) and j == len(t)
    • This ensures both strings were fully consumed with no leftover characters

Time Complexity: O(n * m) where n is the length of the words array and m is the average length of strings.

Space Complexity: O(1) as we only use a constant amount of extra space for pointers and counters.

The algorithm efficiently checks each word by comparing groups of characters rather than individual characters, making it well-suited for this stretching problem.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through how the algorithm checks if "hello" can be stretched to match "heeellooo".

Target string s = "heeellooo"
Query word t = "hello"

Step 1: Initialize pointers

  • i = 0 (pointing to s[0] = 'h')
  • j = 0 (pointing to t[0] = 'h')

Step 2: First group comparison - 'h'

  • Characters match: s[0] = 'h' and t[0] = 'h' βœ“
  • Count consecutive 'h' in s: c1 = 1 (just one 'h')
  • Count consecutive 'h' in t: c2 = 1 (just one 'h')
  • Check rules:
    • c1 >= c2? Yes (1 >= 1) βœ“
    • Since c1 < 3 (1 < 3), we need c1 == c2. Yes (1 == 1) βœ“
  • Move pointers: i = 1, j = 1

Step 3: Second group comparison - 'e'

  • Characters match: s[1] = 'e' and t[1] = 'e' βœ“
  • Count consecutive 'e' in s: c1 = 3 (positions 1,2,3: "eee")
  • Count consecutive 'e' in t: c2 = 1 (just one 'e')
  • Check rules:
    • c1 >= c2? Yes (3 >= 1) βœ“
    • Since c1 >= 3, we can stretch from 1 to 3 βœ“
  • Move pointers: i = 4, j = 2

Step 4: Third group comparison - 'l'

  • Characters match: s[4] = 'l' and t[2] = 'l' βœ“
  • Count consecutive 'l' in s: c1 = 2 (positions 4,5: "ll")
  • Count consecutive 'l' in t: c2 = 2 (positions 2,3: "ll")
  • Check rules:
    • c1 >= c2? Yes (2 >= 2) βœ“
    • Since c1 < 3 (2 < 3), we need c1 == c2. Yes (2 == 2) βœ“
  • Move pointers: i = 6, j = 4

Step 5: Fourth group comparison - 'o'

  • Characters match: s[6] = 'o' and t[4] = 'o' βœ“
  • Count consecutive 'o' in s: c1 = 3 (positions 6,7,8: "ooo")
  • Count consecutive 'o' in t: c2 = 1 (just one 'o')
  • Check rules:
    • c1 >= c2? Yes (3 >= 1) βœ“
    • Since c1 >= 3, we can stretch from 1 to 3 βœ“
  • Move pointers: i = 9, j = 5

Step 6: Final verification

  • i == len(s)? Yes (9 == 9) βœ“
  • j == len(t)? Yes (5 == 5) βœ“
  • Both strings fully consumed!

Result: "hello" CAN be stretched to "heeellooo"

The word is stretchy because:

  • Group 'e' was extended from 1 to 3 characters
  • Group 'o' was extended from 1 to 3 characters
  • Groups 'h' and 'll' remained unchanged (they had less than 3 characters in the target)

Solution Implementation

1class Solution:
2    def expressiveWords(self, s: str, words: List[str]) -> int:
3        def is_stretchy(source: str, target: str) -> bool:
4            """
5            Check if target can be stretched to match source.
6          
7            Args:
8                source: The stretched string to match
9                target: The original word to check
10          
11            Returns:
12                True if target can be stretched to match source, False otherwise
13            """
14            source_len, target_len = len(source), len(target)
15          
16            # Target cannot be stretched if it's longer than source
17            if target_len > source_len:
18                return False
19          
20            source_idx = target_idx = 0
21          
22            # Process both strings character by character
23            while source_idx < source_len and target_idx < target_len:
24                # Characters must match at current positions
25                if source[source_idx] != target[target_idx]:
26                    return False
27              
28                # Count consecutive occurrences in source
29                source_group_end = source_idx
30                while source_group_end < source_len and source[source_group_end] == source[source_idx]:
31                    source_group_end += 1
32                source_group_count = source_group_end - source_idx
33              
34                # Count consecutive occurrences in target
35                target_group_end = target_idx
36                while target_group_end < target_len and target[target_group_end] == target[target_idx]:
37                    target_group_end += 1
38                target_group_count = target_group_end - target_idx
39              
40                # Move indices to next group
41                source_idx = source_group_end
42                target_idx = target_group_end
43              
44                # Check stretching rules:
45                # 1. Source must have at least as many characters as target
46                # 2. If source has fewer than 3 characters, counts must match exactly
47                if source_group_count < target_group_count or \
48                   (source_group_count < 3 and source_group_count != target_group_count):
49                    return False
50          
51            # Both strings must be fully processed
52            return source_idx == source_len and target_idx == target_len
53      
54        # Count how many words can be stretched to match s
55        return sum(is_stretchy(s, word) for word in words)
56
1class Solution {
2    /**
3     * Counts how many words from the array can be stretched to match string s
4     * @param s The target stretched string
5     * @param words Array of words to check if they can be stretched to match s
6     * @return Number of words that can be stretched to match s
7     */
8    public int expressiveWords(String s, String[] words) {
9        int count = 0;
10      
11        // Check each word to see if it can be stretched to match s
12        for (String word : words) {
13            if (check(s, word)) {
14                count++;
15            }
16        }
17      
18        return count;
19    }
20
21    /**
22     * Checks if word can be stretched to match the stretched string
23     * A character group can be extended if it contains at least 3 characters
24     * @param stretchedString The target stretched string
25     * @param word The word to check if it can be stretched
26     * @return true if word can be stretched to match stretchedString, false otherwise
27     */
28    private boolean check(String stretchedString, String word) {
29        int stretchedLength = stretchedString.length();
30        int wordLength = word.length();
31      
32        // Word cannot be stretched if it's already longer than the target
33        if (wordLength > stretchedLength) {
34            return false;
35        }
36      
37        int stretchedIndex = 0;
38        int wordIndex = 0;
39      
40        // Process both strings character by character
41        while (stretchedIndex < stretchedLength && wordIndex < wordLength) {
42            // Characters at current positions must match
43            if (stretchedString.charAt(stretchedIndex) != word.charAt(wordIndex)) {
44                return false;
45            }
46          
47            // Count consecutive occurrences of current character in stretched string
48            int stretchedGroupStart = stretchedIndex;
49            while (stretchedIndex < stretchedLength && 
50                   stretchedString.charAt(stretchedIndex) == stretchedString.charAt(stretchedGroupStart)) {
51                stretchedIndex++;
52            }
53            int stretchedGroupLength = stretchedIndex - stretchedGroupStart;
54          
55            // Count consecutive occurrences of current character in word
56            int wordGroupStart = wordIndex;
57            while (wordIndex < wordLength && 
58                   word.charAt(wordIndex) == word.charAt(wordGroupStart)) {
59                wordIndex++;
60            }
61            int wordGroupLength = wordIndex - wordGroupStart;
62          
63            // Check if the stretching is valid:
64            // 1. Stretched group must be at least as long as word group
65            // 2. If stretched group has less than 3 characters, lengths must match exactly
66            if (stretchedGroupLength < wordGroupLength || 
67                (stretchedGroupLength < 3 && stretchedGroupLength != wordGroupLength)) {
68                return false;
69            }
70        }
71      
72        // Both strings must be fully processed
73        return stretchedIndex == stretchedLength && wordIndex == wordLength;
74    }
75}
76
1class Solution {
2public:
3    int expressiveWords(string s, vector<string>& words) {
4        // Lambda function to check if word can be stretched to match string s
5        auto canStretch = [](string& stretchedWord, string& originalWord) -> int {
6            int stretchedLen = stretchedWord.size();
7            int originalLen = originalWord.size();
8          
9            // If original word is longer than stretched word, it cannot match
10            if (originalLen > stretchedLen) {
11                return 0;
12            }
13          
14            int stretchedIdx = 0;
15            int originalIdx = 0;
16          
17            // Process both strings character by character
18            while (stretchedIdx < stretchedLen && originalIdx < originalLen) {
19                // Characters must match at current positions
20                if (stretchedWord[stretchedIdx] != originalWord[originalIdx]) {
21                    return 0;
22                }
23              
24                // Count consecutive occurrences in stretched word
25                int stretchedStart = stretchedIdx;
26                while (stretchedIdx < stretchedLen && 
27                       stretchedWord[stretchedIdx] == stretchedWord[stretchedStart]) {
28                    ++stretchedIdx;
29                }
30                int stretchedCount = stretchedIdx - stretchedStart;
31              
32                // Count consecutive occurrences in original word
33                int originalStart = originalIdx;
34                while (originalIdx < originalLen && 
35                       originalWord[originalIdx] == originalWord[originalStart]) {
36                    ++originalIdx;
37                }
38                int originalCount = originalIdx - originalStart;
39              
40                // Check if the stretching is valid:
41                // 1. Stretched count must be >= original count
42                // 2. If stretched count < 3, counts must be exactly equal (no stretching)
43                if (stretchedCount < originalCount || 
44                    (stretchedCount < 3 && stretchedCount != originalCount)) {
45                    return 0;
46                }
47            }
48          
49            // Both strings must be fully processed
50            return (stretchedIdx == stretchedLen && originalIdx == originalLen) ? 1 : 0;
51        };
52      
53        // Count how many words can be stretched to match s
54        int result = 0;
55        for (string& word : words) {
56            result += canStretch(s, word);
57        }
58      
59        return result;
60    }
61};
62
1function expressiveWords(s: string, words: string[]): number {
2    // Helper function to check if a word can be stretched to match the target string
3    const canStretch = (stretchedWord: string, originalWord: string): number => {
4        const stretchedLen = stretchedWord.length;
5        const originalLen = originalWord.length;
6      
7        // If original word is longer than stretched word, it cannot match
8        if (originalLen > stretchedLen) {
9            return 0;
10        }
11      
12        let stretchedIdx = 0;
13        let originalIdx = 0;
14      
15        // Process both strings character by character
16        while (stretchedIdx < stretchedLen && originalIdx < originalLen) {
17            // Characters must match at current positions
18            if (stretchedWord[stretchedIdx] !== originalWord[originalIdx]) {
19                return 0;
20            }
21          
22            // Count consecutive occurrences of the current character in stretched word
23            const stretchedStart = stretchedIdx;
24            while (stretchedIdx < stretchedLen && 
25                   stretchedWord[stretchedIdx] === stretchedWord[stretchedStart]) {
26                stretchedIdx++;
27            }
28            const stretchedCount = stretchedIdx - stretchedStart;
29          
30            // Count consecutive occurrences of the current character in original word
31            const originalStart = originalIdx;
32            while (originalIdx < originalLen && 
33                   originalWord[originalIdx] === originalWord[originalStart]) {
34                originalIdx++;
35            }
36            const originalCount = originalIdx - originalStart;
37          
38            // Validate the stretching rules:
39            // 1. Stretched count must be >= original count
40            // 2. If stretched count < 3, counts must be exactly equal (no stretching allowed)
41            if (stretchedCount < originalCount || 
42                (stretchedCount < 3 && stretchedCount !== originalCount)) {
43                return 0;
44            }
45        }
46      
47        // Both strings must be fully processed for a valid match
48        return (stretchedIdx === stretchedLen && originalIdx === originalLen) ? 1 : 0;
49    };
50  
51    // Count how many words can be stretched to match string s
52    let result = 0;
53    for (const word of words) {
54        result += canStretch(s, word);
55    }
56  
57    return result;
58}
59

Time and Space Complexity

Time Complexity: O(m Γ— n + m Γ— wΜ„) where n is the length of string s, m is the number of words in the words array, and wΜ„ is the average length of words in the array. This can also be expressed as O(m Γ— n + Ξ£α΅’β‚Œβ‚€^(m-1) wα΅’) where wα΅’ is the length of the i-th word.

  • The outer loop iterates through all m words in the array
  • For each word, the check function is called which performs a two-pointer traversal
  • In the check function, we traverse through string s (length n) and word t (length wα΅’) simultaneously
  • Each character in both strings is visited at most twice (once in the main traversal and once in the counting loop)
  • Therefore, for each word comparison, the time is O(n + wα΅’)
  • Total time: O(m Γ— n + Ξ£α΅’β‚Œβ‚€^(m-1) wα΅’)

Space Complexity: O(1)

  • The algorithm uses only a constant amount of extra space
  • Variables i, j, k, c1, c2, m, and n are all primitive integers
  • No additional data structures are created
  • The space used does not depend on the input size

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Incorrect Group Size Validation

The Problem: A common mistake is misunderstanding when stretching is allowed. Many implementations incorrectly assume that if the source string has a group of size 3 or more, the target can have ANY smaller size. However, the target group size must still be at least 1.

Incorrect Implementation:

# Wrong: Doesn't ensure target has at least 1 character in the group
if source_group_count >= 3:
    # This would incorrectly allow target_group_count = 0
    return True

Correct Implementation:

# Must check that source >= target AND apply the size-3 rule
if source_group_count < target_group_count:
    return False
if source_group_count < 3 and source_group_count != target_group_count:
    return False

Pitfall 2: Not Fully Consuming Both Strings

The Problem: After the main loop, forgetting to verify that both strings were completely processed. This can lead to false positives where a shorter target word is incorrectly marked as stretchy.

Example Case:

  • Source: "heeello"
  • Target: "helo"
  • Without the final check, after processing "he" from both strings, the function might return True even though "llo" remains unprocessed in source.

Incorrect Implementation:

while source_idx < source_len and target_idx < target_len:
    # ... processing logic ...
  
# Wrong: Missing final verification
return True

Correct Implementation:

while source_idx < source_len and target_idx < target_len:
    # ... processing logic ...
  
# Must ensure both strings are fully consumed
return source_idx == source_len and target_idx == target_len

Pitfall 3: Off-by-One Errors in Group Counting

The Problem: When counting consecutive characters, it's easy to make off-by-one errors, especially when updating the indices after counting.

Incorrect Implementation:

# Wrong: This creates an off-by-one error
source_group_count = 1  # Start with 1
source_idx += 1
while source_idx < source_len and source[source_idx] == source[source_idx - 1]:
    source_group_count += 1
    source_idx += 1
# source_idx is now already moved, causing issues

Correct Implementation:

# Use a separate variable for counting
source_group_end = source_idx
while source_group_end < source_len and source[source_group_end] == source[source_idx]:
    source_group_end += 1
source_group_count = source_group_end - source_idx
source_idx = source_group_end  # Clean update

Pitfall 4: Mishandling Edge Cases with Single Characters

The Problem: Failing to properly handle cases where a character appears only once in either string, particularly when applying the "group of 3" rule.

Example Case:

  • Source: "abcd" (each character appears once)
  • Target: "abc"
  • Since each group in source has size 1 (less than 3), each corresponding group in target must also have exactly size 1.

Solution: The code correctly handles this with the condition:

if source_group_count < 3 and source_group_count != target_group_count:
    return False

This ensures single characters (or pairs) must match exactly and cannot be stretched.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

How many times is a tree node visited in a depth first search?


Recommended Readings

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

Load More