2135. Count Words Obtained After Adding a Letter
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:
-
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"
- For example, if you have
-
Rearrange: After adding the letter, you can rearrange all the letters in any order
- For example,
"abcd"
can become"acbd"
,"bacd"
,"cbda"
,"abcd"
, etc.
- For example,
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.
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 β binary0000...0111
"acd"
β bits 0, 2, 3 are set β binary0000...1101
Using bits, checking if a start word can transform into a target word becomes:
- Convert all start words to their bit representations and store in a set
- For each target word, try removing each of its letters one by one
- 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 1sum()
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:
-
Convert it to its bit representation:
x = sum(1 << (ord(c) - 97) for c in w)
-
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 characterc
:- If the bit is set (1), XOR with 1 makes it 0
- This effectively removes that letter from the bit representation
-
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 sets
) - Target word
"abcd"
β bit representation:1111
- Try removing 'd':
1111 ^ 1000 = 0111
β This is ins
! - 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 EvaluatorExample 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 ins
- Remove 'a':
10000010000000101 ^ 00000000000000001
=10000010000000100
β Not ins
- Remove 'c':
10000010000000101 ^ 00000000000000100
=10000010000000001
β Not ins
- Remove 'k':
10000010000000101 ^ 00000010000000000
=10000000000000101
β Found ins
!
- Remove 't':
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 ins
- Remove 'o':
10011000000010000 ^ 00001000000000000
=10010000000010000
β Not ins
- Remove 't':
10011000000010000 ^ 10000000000000000
=00011000000010000
β Not ins
- Remove 'e':
10011000000010000 ^ 00000000000010000
=10011000000000000
β Not ins
- Remove 'n':
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
fromstartWords
: For each word instartWords
, we iterate through its characters to compute the bitmask. This takesO(m Γ |Ξ£|)
time in the worst case. - Processing
targetWords
: For each word intargetWords
, 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 Γ |Ξ£|)
- Compute its bitmask:
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).
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
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
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!