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:
- Each character group in the original word must match the corresponding character group in
s
- 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) - If a character group in
s
has less than 3 characters, the corresponding group in the word must have exactly the same count - 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
).
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:
- If the target group in
s
has countc1
and the query group has countc2
, thenc1
must be>= c2
(we can't remove characters) - If
c1 >= 3
, then anyc2 <= c1
works (the group is already extended or can be extended) - If
c1 < 3
, thenc1
must equalc2
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:
-
Initial Length Check:
- First, compare lengths: if
len(t) > len(s)
, returnfalse
immediately - This is because we can only add characters, never remove them
- First, compare lengths: if
-
Two-Pointer Traversal:
- Initialize pointers
i = 0
for strings
andj = 0
for stringt
- Continue while both pointers are within bounds
- Initialize pointers
-
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)
- At each position, check if
-
Count Consecutive Characters:
- For string
s
: Count consecutive occurrences ofs[i]
starting from positioni
, store asc1
- For string
t
: Count consecutive occurrences oft[j]
starting from positionj
, store asc2
- Move pointers
i
andj
forward byc1
andc2
respectively
- For string
-
Validate Group Counts: Apply the stretching rules:
- If
c1 < c2
: Returnfalse
(can't remove characters) - If
c1 < 3
andc1 != c2
: Returnfalse
(groups with less than 3 characters must match exactly) - Otherwise, the group is valid (either
c1 >= 3
allowing stretching, orc1 == c2
for exact match)
- If
-
Final Verification:
- After the loop, check if both
i == len(s)
andj == len(t)
- This ensures both strings were fully consumed with no leftover characters
- After the loop, check if both
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 EvaluatorExample 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 tos[0] = 'h'
)j = 0
(pointing tot[0] = 'h'
)
Step 2: First group comparison - 'h'
- Characters match:
s[0] = 'h'
andt[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 needc1 == c2
. Yes (1 == 1) β
- Move pointers:
i = 1
,j = 1
Step 3: Second group comparison - 'e'
- Characters match:
s[1] = 'e'
andt[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'
andt[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 needc1 == c2
. Yes (2 == 2) β
- Move pointers:
i = 6
,j = 4
Step 5: Fourth group comparison - 'o'
- Characters match:
s[6] = 'o'
andt[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 strings
(lengthn
) and wordt
(lengthwα΅’
) 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
, andn
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.
How many times is a tree node visited in a depth first search?
Recommended Readings
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
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!