Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

809. Expressive Words - Leetcode Solution

Problem Description

The "Expressive Words" problem asks you to determine, given a "target" string S and an array of "words" words, how many words can be stretched to become the target string S.

A word is considered "stretchy" if it can be made equal to S by expanding some of its groups of characters. A group of characters in S can be stretched if it consists of three or more of the same character, and we can add extra instances of that character to the corresponding group in the word.

Constraints:

  • You may not reorder or delete characters.
  • You may only insert characters to expand groups of length at least 3 in S.
  • You must check each word independently, and count how many can be stretched to match S.
For example, if S = "heeellooo" and words = ["hello", "hi", "helo"], only "hello" and "helo" are stretchy matches.

Thought Process

At first glance, this problem suggests checking, for each word, whether you can match it to S by adding extra characters. A brute-force solution might try all possible insertions, but that's inefficient and hard to implement.

Instead, notice that both S and each word can be broken down into groups of consecutive characters. For example, "heeellooo" becomes ['h', 'e', 'l', 'o'] with counts [1, 3, 2, 3]. For a word to match, its group sequence and order must be the same, and each group must be "expandable" to match S's group.

The optimization comes from comparing these groups directly instead of manipulating strings. If a group in S is at least 3, we can stretch the corresponding group in the word to match the length in S. If it's less than 3, the group must match exactly.

Solution Approach

The core idea is to process both S and each word into sequences of character groups and counts, then compare them group by group.

  1. Group the Characters:
    • Write a helper function that, given a string, returns two lists: one with the sequence of unique consecutive characters, and one with the count of each group.
  2. Compare Groupings:
    • For each word, group it as above.
    • If the group sequence (characters and order) doesn't match S, skip it.
    • For each group, check the following:
      • If S's group count is less than 3, it must match exactly with the word's count.
      • If S's group count is 3 or more, the word's count can be less (but not more), as we can stretch it.
      • If the word's count is more than S's, it's impossible.
  3. Count Valid Words:
    • For each word that passes the above checks, increment a counter.

This approach avoids unnecessary computations and leverages the group structure for efficiency.

Example Walkthrough

Let's use S = "heeellooo" and words = ["hello", "hi", "helo"].

First, group S:
Characters: ['h', 'e', 'l', 'o'], Counts: [1, 3, 2, 3]

Now, for each word:

  • "hello":
    • Groups: ['h', 'e', 'l', 'o'], Counts: [1, 1, 2, 1]
    • Compare each group:
      • 'h': counts match (1 vs 1)
      • 'e': S has 3, word has 1; since S's group is >=3 and word's is <= S's, it's valid
      • 'l': counts match (2 vs 2)
      • 'o': S has 3, word has 1; valid for same reason as above
    • All groups valid, so "hello" is stretchy.
  • "hi":
    • Groups: ['h', 'i']
    • Group sequences do not match; skip.
  • "helo":
    • Groups: ['h', 'e', 'l', 'o'], Counts: [1, 1, 1, 1]
    • 'l': S has 2, word has 1; S's group is less than 3, so counts must match exactly. They do not, so not valid.
So only "hello" is stretchy in this case.

Time and Space Complexity

Brute-force Approach:

  • Would require generating all possible stretched versions of each word, which is exponential in the worst case.
Optimized Approach:
  • Let n be the number of words, m be the average length of S and word.
  • Grouping each string is O(m).
  • Comparing groups is O(m) per word.
  • Total complexity: O(n * m).
  • Space: O(m) for group arrays per word, but can be reused, so overall space is O(m).

This is efficient and suitable for reasonable input sizes.

Summary

The key to solving "Expressive Words" efficiently is recognizing the importance of character groups and their counts. By grouping both the target and each word, and comparing group-by-group with clear rules, we avoid brute-force string manipulation and achieve a clean, linear solution. This approach is both simple and elegant, making the problem approachable for programmers at all levels.

Code Implementation

class Solution:
    def expressiveWords(self, S: str, words: List[str]) -> int:
        def group(word):
            chars = []
            counts = []
            i = 0
            while i < len(word):
                ch = word[i]
                cnt = 1
                i += 1
                while i < len(word) and word[i] == ch:
                    cnt += 1
                    i += 1
                chars.append(ch)
                counts.append(cnt)
            return chars, counts

        s_chars, s_counts = group(S)
        res = 0
        for w in words:
            w_chars, w_counts = group(w)
            if s_chars != w_chars:
                continue
            ok = True
            for sc, wc, s_cnt, w_cnt in zip(s_chars, w_chars, s_counts, w_counts):
                if s_cnt < 3:
                    if s_cnt != w_cnt:
                        ok = False
                        break
                else:
                    if w_cnt > s_cnt or w_cnt < 1:
                        ok = False
                        break
            if ok:
                res += 1
        return res
      
class Solution {
public:
    vector<pair<char, int>> group(const string& s) {
        vector<pair<char, int>> res;
        int i = 0, n = s.size();
        while (i < n) {
            char ch = s[i];
            int cnt = 1;
            i++;
            while (i < n && s[i] == ch) {
                cnt++;
                i++;
            }
            res.push_back({ch, cnt});
        }
        return res;
    }
    
    int expressiveWords(string S, vector<string>& words) {
        auto s_group = group(S);
        int ans = 0;
        for (const string& w : words) {
            auto w_group = group(w);
            if (s_group.size() != w_group.size())
                continue;
            bool match = true;
            for (int i = 0; i < s_group.size(); ++i) {
                if (s_group[i].first != w_group[i].first)
                    match = false;
                int sc = s_group[i].second, wc = w_group[i].second;
                if (sc < 3) {
                    if (sc != wc)
                        match = false;
                } else {
                    if (wc > sc || wc < 1)
                        match = false;
                }
            }
            if (match)
                ans++;
        }
        return ans;
    }
};
      
class Solution {
    private List<Character> groupChars(String s, List<Integer> counts) {
        List<Character> chars = new ArrayList<>();
        int i = 0, n = s.length();
        while (i < n) {
            char ch = s.charAt(i);
            int cnt = 1;
            i++;
            while (i < n && s.charAt(i) == ch) {
                cnt++;
                i++;
            }
            chars.add(ch);
            counts.add(cnt);
        }
        return chars;
    }
    public int expressiveWords(String S, String[] words) {
        List<Integer> sCounts = new ArrayList<>();
        List<Character> sChars = groupChars(S, sCounts);
        int res = 0;
        for (String w : words) {
            List<Integer> wCounts = new ArrayList<>();
            List<Character> wChars = groupChars(w, wCounts);
            if (!sChars.equals(wChars)) continue;
            boolean ok = true;
            for (int i = 0; i < sChars.size(); ++i) {
                int sc = sCounts.get(i), wc = wCounts.get(i);
                if (sc < 3) {
                    if (sc != wc) {
                        ok = false;
                        break;
                    }
                } else {
                    if (wc > sc || wc < 1) {
                        ok = false;
                        break;
                    }
                }
            }
            if (ok) res++;
        }
        return res;
    }
}
      
var expressiveWords = function(S, words) {
    function group(word) {
        let chars = [], counts = [];
        let i = 0;
        while (i < word.length) {
            let ch = word[i];
            let cnt = 1;
            i++;
            while (i < word.length && word[i] === ch) {
                cnt++;
                i++;
            }
            chars.push(ch);
            counts.push(cnt);
        }
        return [chars, counts];
    }
    let [sChars, sCounts] = group(S);
    let res = 0;
    for (let w of words) {
        let [wChars, wCounts] = group(w);
        if (sChars.length !== wChars.length) continue;
        let ok = true;
        for (let i = 0; i < sChars.length; ++i) {
            if (sChars[i] !== wChars[i]) {
                ok = false;
                break;
            }
            let sc = sCounts[i], wc = wCounts[i];
            if (sc < 3) {
                if (sc !== wc) {
                    ok = false;
                    break;
                }
            } else {
                if (wc > sc || wc < 1) {
                    ok = false;
                    break;
                }
            }
        }
        if (ok) res++;
    }
    return res;
};