Facebook Pixel

2135. Count Words Obtained After Adding a Letter

MediumBit ManipulationArrayHash TableStringSorting
Leetcode Link

Problem Description

You have two arrays of strings: startWords and targetWords. Each string contains only lowercase English letters, and each letter appears at most once in any string.

For each string in targetWords, you need to check if it can be obtained from any string in startWords by performing a specific conversion operation.

The conversion operation consists of two steps:

  1. Append one letter: Add exactly one lowercase letter that is not already present in the string to its end

    • For example, if you have "abc", you can add 'd', 'e', or 'y' (any letter except 'a', 'b', or 'c')
    • If you add 'd', the string becomes "abcd"
  2. Rearrange: After adding the letter, you can rearrange all the letters in any order

    • For example, "abcd" can become "acbd", "bacd", "cbda", "abcd", etc.

Your task is to count how many strings in targetWords can be obtained by applying this conversion operation to some string in startWords.

Important notes:

  • The strings in startWords don't actually change - you're just checking if the transformation is possible
  • Each target word only needs to be obtainable from at least one start word to be counted
  • The strings are 0-indexed arrays

The function should return the total count of strings in targetWords that can be obtained through the conversion operation.

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

Intuition

The key insight is that after adding a letter and rearranging, the order of letters doesn't matter - only which letters are present matters. This means we can think of each string as a set of characters rather than an ordered sequence.

Since each letter appears at most once in any string, we can represent each string as a set. The conversion operation essentially transforms a set of size n into a set of size n+1 by adding one new element.

To check if a target word can be obtained from a start word:

  • The target word must have exactly one more letter than the start word
  • The target word must contain all letters from the start word plus one additional letter

This means for each target word, we need to check if removing any one of its letters results in a set that matches some start word.

Since we're dealing with sets of lowercase letters (only 26 possible values), we can use bit manipulation for efficient representation and comparison. Each string becomes a binary number where bit i is set if the i-th letter of the alphabet is present. For example:

  • "abc" β†’ bits 0, 1, 2 are set β†’ binary 0000...0111
  • "acd" β†’ bits 0, 2, 3 are set β†’ binary 0000...1101

Using bits, checking if a start word can transform into a target word becomes:

  1. Convert all start words to their bit representations and store in a set
  2. For each target word, try removing each of its letters one by one
  3. Check if the resulting bit pattern exists in the start words set

The XOR operation with 1 << (ord(c) - 97) effectively toggles (removes) the bit corresponding to character c, allowing us to quickly check all possible "remove one letter" scenarios.

Learn more about Sorting patterns.

Solution Approach

The implementation uses hash table and bit manipulation to efficiently solve this problem.

Step 1: Convert Start Words to Bit Representations

First, we convert each string in startWords into a binary number and store them in a set s:

s = {sum(1 << (ord(c) - 97) for c in w) for w in startWords}

For each character c in a word w:

  • ord(c) - 97 gives the position of the letter (0 for 'a', 1 for 'b', etc.)
  • 1 << (ord(c) - 97) creates a bit mask with only that position set to 1
  • sum() combines all these bit masks using addition (equivalent to OR operation when bits don't overlap)

For example, "abc" becomes:

  • 'a': 1 << 0 = 001
  • 'b': 1 << 1 = 010
  • 'c': 1 << 2 = 100
  • Sum: 111 (decimal value 7)

Step 2: Check Each Target Word

For each word in targetWords, we:

  1. Convert it to its bit representation:

    x = sum(1 << (ord(c) - 97) for c in w)
  2. Try removing each letter from the target word:

    for c in w:
        if x ^ (1 << (ord(c) - 97)) in s:

    The XOR operation x ^ (1 << (ord(c) - 97)) removes the bit corresponding to character c:

    • If the bit is set (1), XOR with 1 makes it 0
    • This effectively removes that letter from the bit representation
  3. Check if the result exists in our set of start words:

    • If yes, this target word can be obtained from that start word by adding the removed letter
    • Increment the answer and break (no need to check other letters)

Example Walkthrough:

Let's say startWords = ["abc"] and targetWords = ["abcd"]:

  • Start word "abc" β†’ bit representation: 0111 (stored in set s)
  • Target word "abcd" β†’ bit representation: 1111
  • Try removing 'd': 1111 ^ 1000 = 0111 β†’ This is in s!
  • Therefore, "abcd" can be obtained from "abc" by adding 'd'

The time complexity is O(n Γ— 26 + m Γ— 26) where n is the length of startWords and m is the length of targetWords, since we iterate through at most 26 letters for each word.

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 concrete example with startWords = ["ant", "act"] and targetWords = ["tack", "note"].

Step 1: Convert Start Words to Bit Representations

For "ant":

  • 'a' β†’ position 0 β†’ 1 << 0 = 0000...0001
  • 'n' β†’ position 13 β†’ 1 << 13 = 0010000000000000
  • 't' β†’ position 19 β†’ 1 << 19 = 10000000000000000000
  • Combined: 10010000000000001 (bits 0, 13, 19 set)

For "act":

  • 'a' β†’ position 0 β†’ 1 << 0 = 0000...0001
  • 'c' β†’ position 2 β†’ 1 << 2 = 0000...0100
  • 't' β†’ position 19 β†’ 1 << 19 = 10000000000000000000
  • Combined: 10000000000000101 (bits 0, 2, 19 set)

Store both bit representations in set s = {10010000000000001, 10000000000000101}.

Step 2: Check Target Words

For "tack" (has letters 't', 'a', 'c', 'k'):

  • Convert to bits: 10000010000000101 (bits 0, 2, 10, 19 set)
  • Try removing each letter:
    • Remove 't': 10000010000000101 ^ 10000000000000000 = 00000010000000101 β†’ Not in s
    • Remove 'a': 10000010000000101 ^ 00000000000000001 = 10000010000000100 β†’ Not in s
    • Remove 'c': 10000010000000101 ^ 00000000000000100 = 10000010000000001 β†’ Not in s
    • Remove 'k': 10000010000000101 ^ 00000010000000000 = 10000000000000101 β†’ Found in s!

This matches "act"! So "tack" can be obtained from "act" by adding 'k' and rearranging. Count = 1.

For "note" (has letters 'n', 'o', 't', 'e'):

  • Convert to bits: 10011000000010000 (bits 4, 13, 14, 19 set)
  • Try removing each letter:
    • Remove 'n': 10011000000010000 ^ 00010000000000000 = 10001000000010000 β†’ Not in s
    • Remove 'o': 10011000000010000 ^ 00001000000000000 = 10010000000010000 β†’ Not in s
    • Remove 't': 10011000000010000 ^ 10000000000000000 = 00011000000010000 β†’ Not in s
    • Remove 'e': 10011000000010000 ^ 00000000000010000 = 10011000000000000 β†’ Not in s

No match found. "note" cannot be obtained from any start word.

Final Answer: 1 (only "tack" can be obtained)

Solution Implementation

1class Solution:
2    def wordCount(self, startWords: List[str], targetWords: List[str]) -> int:
3        # Convert each start word to a bitmask representation
4        # Each bit position represents a letter (a=0, b=1, ..., z=25)
5        start_bitmasks = set()
6        for word in startWords:
7            bitmask = 0
8            for char in word:
9                # Set the bit corresponding to this character
10                bit_position = ord(char) - ord('a')
11                bitmask |= (1 << bit_position)
12            start_bitmasks.add(bitmask)
13      
14        # Count how many target words can be formed
15        count = 0
16      
17        for target_word in targetWords:
18            # Convert target word to bitmask
19            target_bitmask = 0
20            for char in target_word:
21                bit_position = ord(char) - ord('a')
22                target_bitmask |= (1 << bit_position)
23          
24            # Check if we can form this target word by adding one letter to any start word
25            # This means removing one character from target word should give us a start word
26            for char in target_word:
27                bit_position = ord(char) - ord('a')
28                # Remove this character from target word's bitmask
29                modified_bitmask = target_bitmask ^ (1 << bit_position)
30              
31                # Check if this modified bitmask exists in start words
32                if modified_bitmask in start_bitmasks:
33                    count += 1
34                    break  # Found a valid transformation, move to next target word
35      
36        return count
37
1class Solution {
2    public int wordCount(String[] startWords, String[] targetWords) {
3        // Store bitmask representations of all start words
4        // Each bit position represents a letter (bit 0 = 'a', bit 1 = 'b', etc.)
5        Set<Integer> startWordBitmasks = new HashSet<>();
6      
7        // Convert each start word to its bitmask representation
8        for (String word : startWords) {
9            int bitmask = 0;
10            for (char character : word.toCharArray()) {
11                // Set the bit corresponding to this character
12                // Example: 'c' sets bit 2 (since 'c' - 'a' = 2)
13                bitmask |= 1 << (character - 'a');
14            }
15            startWordBitmasks.add(bitmask);
16        }
17      
18        int matchCount = 0;
19      
20        // Check each target word
21        for (String word : targetWords) {
22            // Convert target word to bitmask
23            int targetBitmask = 0;
24            for (char character : word.toCharArray()) {
25                targetBitmask |= 1 << (character - 'a');
26            }
27          
28            // Try removing each character from target word
29            // Check if the resulting bitmask exists in start words
30            for (char character : word.toCharArray()) {
31                // XOR with character's bit to remove it from the bitmask
32                int bitmaskWithoutChar = targetBitmask ^ (1 << (character - 'a'));
33              
34                if (startWordBitmasks.contains(bitmaskWithoutChar)) {
35                    // Found a match: target word minus one character equals a start word
36                    matchCount++;
37                    break; // Only count this target word once
38                }
39            }
40        }
41      
42        return matchCount;
43    }
44}
45
1class Solution {
2public:
3    int wordCount(vector<string>& startWords, vector<string>& targetWords) {
4        // Set to store bitmask representations of start words
5        unordered_set<int> startWordMasks;
6      
7        // Convert each start word to a bitmask representation
8        // Each bit position represents whether a character ('a' to 'z') is present
9        for (auto& word : startWords) {
10            int bitmask = 0;
11            for (char ch : word) {
12                // Set the bit at position (ch - 'a') to 1
13                bitmask |= 1 << (ch - 'a');
14            }
15            startWordMasks.insert(bitmask);
16        }
17      
18        int count = 0;
19      
20        // Check each target word
21        for (auto& word : targetWords) {
22            // Convert target word to bitmask
23            int targetMask = 0;
24            for (char ch : word) {
25                targetMask |= 1 << (ch - 'a');
26            }
27          
28            // Try removing each character from target word
29            // Check if the resulting bitmask exists in start words
30            for (char ch : word) {
31                // XOR with the bit of current character to remove it from bitmask
32                int modifiedMask = targetMask ^ (1 << (ch - 'a'));
33              
34                // If this modified mask exists in start words,
35                // it means we can form the target word by adding 'ch' to that start word
36                if (startWordMasks.contains(modifiedMask)) {
37                    ++count;
38                    break;  // Found a valid transformation, move to next target word
39                }
40            }
41        }
42      
43        return count;
44    }
45};
46
1/**
2 * Counts how many target words can be formed by adding exactly one letter to any start word
3 * @param startWords - Array of starting words
4 * @param targetWords - Array of target words to check
5 * @returns Number of target words that can be formed
6 */
7function wordCount(startWords: string[], targetWords: string[]): number {
8    // Set to store bit representations of start words
9    const startWordBitmasks = new Set<number>();
10  
11    // Convert each start word to a bitmask representation
12    // Each bit position represents presence of a letter (a=0, b=1, ..., z=25)
13    for (const word of startWords) {
14        let bitmask = 0;
15        for (const char of word) {
16            // XOR toggles the bit for this character
17            // Since words have unique characters, this sets the bit
18            const bitPosition = char.charCodeAt(0) - 97; // 'a' has ASCII 97
19            bitmask ^= 1 << bitPosition;
20        }
21        startWordBitmasks.add(bitmask);
22    }
23  
24    let validTargetCount = 0;
25  
26    // Check each target word
27    for (const word of targetWords) {
28        // Create bitmask for the target word
29        let targetBitmask = 0;
30        for (const char of word) {
31            const bitPosition = char.charCodeAt(0) - 97;
32            targetBitmask ^= 1 << bitPosition;
33        }
34      
35        // Try removing each character from target word
36        // If resulting bitmask exists in start words, target can be formed
37        for (const char of word) {
38            const bitPosition = char.charCodeAt(0) - 97;
39            // XOR with character bit to remove it from target word
40            const startWordCandidate = targetBitmask ^ (1 << bitPosition);
41          
42            if (startWordBitmasks.has(startWordCandidate)) {
43                validTargetCount++;
44                break; // Found a valid start word, no need to check other characters
45            }
46        }
47    }
48  
49    return validTargetCount;
50}
51

Time and Space Complexity

The time complexity is O(m Γ— |Ξ£| + n Γ— |Ξ£|), which can be simplified to O((m + n) Γ— |Ξ£|), where m is the length of startWords, n is the length of targetWords, and |Ξ£| = 26 represents the size of the character set (lowercase English letters).

Breaking down the time complexity:

  • Creating the set s from startWords: For each word in startWords, we iterate through its characters to compute the bitmask. This takes O(m Γ— |Ξ£|) time in the worst case.
  • Processing targetWords: For each word in targetWords, we:
    • Compute its bitmask: O(|Ξ£|) per word
    • Try removing each character from the word (iterating through all characters in w): O(|Ξ£|) operations
    • Check if the resulting bitmask exists in set s: O(1) average case
    • Total for all target words: O(n Γ— |Ξ£|)

The space complexity is O(m), where m is the length of startWords. This is because we store a set s containing at most m bitmask integers (one for each word in startWords). The additional variables (ans, x, temporary calculations) use O(1) space.

Note: The reference answer considers only n (length of targetWords) in its complexity analysis, which appears to focus on the second phase of the algorithm. The complete analysis should account for both startWords and targetWords processing.

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

Common Pitfalls

1. Attempting to Actually Modify Start Words

A common mistake is trying to modify the start words directly by appending characters and checking against target words. This approach is inefficient and misunderstands the problem.

Wrong Approach:

# Don't do this - inefficient and error-prone
for start in startWords:
    for char in 'abcdefghijklmnopqrstuvwxyz':
        if char not in start:
            modified = start + char
            sorted_modified = ''.join(sorted(modified))
            # Check against sorted target words...

Why it's problematic:

  • Creates many temporary strings (memory inefficient)
  • Requires sorting strings for comparison (O(k log k) per string where k is string length)
  • More complex logic to handle all permutations

Correct Approach: Use the bit manipulation method shown in the solution - check if removing a character from the target word yields a start word.

2. Not Handling Duplicate Characters Properly

The problem states each letter appears at most once in any string. If you don't leverage this constraint, you might write unnecessarily complex code to handle duplicates.

Inefficient consideration:

# Unnecessary complexity if trying to handle duplicates
char_count = {}
for char in word:
    char_count[char] = char_count.get(char, 0) + 1

Better approach: Since each character appears at most once, we can use bit manipulation where each bit represents the presence/absence of a character.

3. Breaking Too Early or Not Breaking At All

Wrong - Breaking too early:

for target_word in targetWords:
    for start_word in startWords:
        # Check first possible transformation and break
        break  # This only checks one start word!

Wrong - Not breaking when found:

for char in target_word:
    if modified_bitmask in start_bitmasks:
        count += 1
        # Forgot to break - might count the same target word multiple times!

Correct: Break only after finding that a target word can be formed from at least one start word:

for char in target_word:
    if modified_bitmask in start_bitmasks:
        count += 1
        break  # Move to next target word

4. Using OR Instead of XOR for Bit Removal

Wrong:

# This doesn't remove the bit, it ensures the bit is set
modified_bitmask = target_bitmask | (1 << bit_position)

Correct:

# XOR toggles the bit - since we know it's set, this removes it
modified_bitmask = target_bitmask ^ (1 << bit_position)

5. Not Using a Set for Start Words Lookup

Inefficient:

start_bitmasks = []  # Using a list
for word in startWords:
    # ... convert to bitmask
    start_bitmasks.append(bitmask)

# Later: O(n) lookup time
if modified_bitmask in start_bitmasks:  # Linear search!

Efficient:

start_bitmasks = set()  # Using a set for O(1) lookup

Using a list would make each lookup O(n), resulting in overall O(n Γ— m Γ— 26) complexity instead of O((n + m) Γ— 26).

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

What's the output of running the following function using input 56?

1KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13    def dfs(path, res):
14        if len(path) == len(digits):
15            res.append(''.join(path))
16            return
17
18        next_number = digits[len(path)]
19        for letter in KEYBOARD[next_number]:
20            path.append(letter)
21            dfs(path, res)
22            path.pop()
23
24    res = []
25    dfs([], res)
26    return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2    '2', "abc".toCharArray(),
3    '3', "def".toCharArray(),
4    '4', "ghi".toCharArray(),
5    '5', "jkl".toCharArray(),
6    '6', "mno".toCharArray(),
7    '7', "pqrs".toCharArray(),
8    '8', "tuv".toCharArray(),
9    '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13    List<String> res = new ArrayList<>();
14    dfs(new StringBuilder(), res, digits.toCharArray());
15    return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19    if (path.length() == digits.length) {
20        res.add(path.toString());
21        return;
22    }
23    char next_digit = digits[path.length()];
24    for (char letter : KEYBOARD.get(next_digit)) {
25        path.append(letter);
26        dfs(path, res, digits);
27        path.deleteCharAt(path.length() - 1);
28    }
29}
30
1const KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13    let res = [];
14    dfs(digits, [], res);
15    return res;
16}
17
18function dfs(digits, path, res) {
19    if (path.length === digits.length) {
20        res.push(path.join(''));
21        return;
22    }
23    let next_number = digits.charAt(path.length);
24    for (let letter of KEYBOARD[next_number]) {
25        path.push(letter);
26        dfs(digits, path, res);
27        path.pop();
28    }
29}
30

Recommended Readings

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

Load More