Count Words Obtained After Adding a Letter
MedPrompt
You are given two 0-indexed arrays of strings startWords and targetWords. Each string consists of lowercase English letters only.
For each string in targetWords, check if it is possible to choose a string from startWords and perform a conversion operation on it to be equal to that from targetWords.
The conversion operation is described in the following two steps:
- Append any lowercase letter that is not present in the string to its end.
- For example, if the string is
"abc", the letters'd','e', or'y'can be added to it, but not'a'. If'd'is added, the resulting string will be"abcd".
- For example, if the string is
- Rearrange the letters of the new string in any arbitrary order.
- For example,
"abcd"can be rearranged to"acbd","bacd","cbda", and so on. Note that it can also be rearranged to"abcd"itself.
- For example,
Return the number of strings in targetWords that can be obtained by performing the operations on any string of startWords.
Note that you will only be verifying if the string in targetWords can be obtained from a string in startWords by performing the operations. The strings in startWords do not actually change during this process.
Example 1:
Input: startWords = ["ant","act","tack"], targetWords = ["tack","act","acti"]
Output: 2
Explanation:
- In order to form targetWords[0] = "tack", we use startWords[1] = "act", append 'k' to it, and rearrange "actk" to "tack".
- There is no string in startWords that can be used to obtain targetWords[1] = "act".
Note that "act" does exist in startWords, but we must append one letter to the string before rearranging it.
- In order to form targetWords[2] = "acti", we use startWords[1] = "act", append 'i' to it, and rearrange "acti" to "acti" itself.Example 2:
Input: startWords = ["ab","a"], targetWords = ["abc","abcd"]
Output: 1
Explanation:
- In order to form targetWords[0] = "abc", we use startWords[0] = "ab", add 'c' to it, and rearrange it to "abc".
- There is no string in startWords that can be used to obtain targetWords[1] = "abcd".
Constraints:
1 <= startWords.length, targetWords.length <= 5 * 1041 <= startWords[i].length, targetWords[j].length <= 26- Each string of
startWordsandtargetWordsconsists of lowercase English letters only. - No letter occurs more than once in any string of
startWordsortargetWords.
Approaches
2 approaches with complexity analysis and trade-offs.
The core idea is that the order of characters in a word doesn't matter for this problem. "act" is equivalent to "cat". This suggests we can use a canonical representation for each word. A simple canonical form is the string with its characters sorted alphabetically. For example, both "tack" and "actk" become "ackt" when sorted.
A targetWord can be formed from a startWord if the targetWord has one extra character and the rest of the characters are the same as the startWord. In terms of our sorted canonical forms, this means if we remove one character from the sorted targetWord, the remaining string should be equal to the sorted form of some startWord.
To efficiently check for the existence of a sorted startWord, we can pre-process all startWords, convert them to their sorted form, and store them in a HashSet. This allows for quick lookups.
Algorithm
- Initialize a
HashSet<String>calledstartSet. - Iterate through each
wordinstartWords, sort its characters, and add the resulting string tostartSet. - Initialize a counter
countto 0. - For each
wordintargetWords:- Sort the characters of the
wordto getsortedTarget. - Iterate through each character of
sortedTarget. - Create a
predecessorstring by removing the character at the current position. - Check if this
predecessorstring exists instartSet. - If it exists, increment
countand break the inner loop to process the nexttargetWord.
- Sort the characters of the
- Return the final
count.
Walkthrough
This approach works by first transforming all startWords into a canonical form and storing them for efficient access. Since the problem allows rearranging letters, sorting the characters of a string provides a suitable canonical representation.
We iterate through startWords, sort each one, and store the result in a HashSet. This set, let's call it startSet, will contain all possible "base" words in their sorted form.
Then, for each targetWord, we want to check if it can be formed by adding a letter to any of the original startWords. This is equivalent to checking if removing one letter from the targetWord results in a string that matches one of the startWords. We apply the same sorting logic to the targetWord. For a given sortedTarget, we generate all possible predecessor strings by removing one character at a time. If any of these predecessors are found in our startSet, it confirms that the targetWord is reachable. We increment our count and, to be efficient, immediately move on to the next targetWord.
import java.util.Arrays;import java.util.HashSet;import java.util.Set; class Solution { public int countWords(String[] startWords, String[] targetWords) { Set<String> startSet = new HashSet<>(); for (String word : startWords) { char[] chars = word.toCharArray(); Arrays.sort(chars); startSet.add(new String(chars)); } int count = 0; for (String word : targetWords) { char[] chars = word.toCharArray(); Arrays.sort(chars); String sortedTarget = new String(chars); for (int i = 0; i < sortedTarget.length(); i++) { // Create predecessor by removing character at index i String predecessor = sortedTarget.substring(0, i) + sortedTarget.substring(i + 1); if (startSet.contains(predecessor)) { count++; break; // Found a match, move to the next targetWord } } } return count; }}Complexity
Time
O(N * L_s * log(L_s) + M * L_t^2), where N and M are the number of words in `startWords` and `targetWords`, and L_s and L_t are their maximum string lengths. Pre-processing `startWords` takes O(N * L_s * log(L_s)) for sorting. Processing `targetWords` takes O(M * (L_t * log(L_t) + L_t^2)) because for each word we sort it, then loop L_t times, with each loop involving substring creation and hashing which take O(L_t) time.
Space
O(N * L_s), where N is the number of `startWords` and L_s is the maximum length of a start word. The `HashSet` stores N strings, each of maximum length L_s.
Trade-offs
Pros
Much faster than a naive brute-force approach that compares every pair of start and target words.
Conceptually simple, leveraging standard library features like sorting and HashSets.
Cons
String sorting and manipulation can be relatively slow compared to integer operations.
Space usage can be high if
startWordscontains many long strings, as the entire sorted strings are stored.
This approach improves upon the previous one by using a more efficient canonical representation for the words. Since each word consists of unique lowercase English letters, we can represent the set of characters in a word using a 26-bit integer, also known as a bitmask. Each bit from 0 to 25 corresponds to a letter from 'a' to 'z'. If a letter is present in the word, its corresponding bit is set to 1; otherwise, it's 0.
For example, the word "act" can be represented by the integer mask (1 << ('a'-'a')) | (1 << ('c'-'a')) | (1 << ('t'-'a')).
This representation is compact and allows for very fast operations. Checking if a targetWord can be formed from a startWord now becomes a check on their integer bitmasks. Specifically, for a targetWord's mask, we can generate all possible predecessor masks by turning off one bit at a time. If any of these predecessor masks exist in a pre-computed set of startWord masks, we've found a match.
Algorithm
- Initialize a
HashSet<Integer>calledstartMasks. - For each
wordinstartWords:- Compute its integer bitmask representation.
- Add the bitmask to
startMasks.
- Initialize a counter
countto 0. - For each
wordintargetWords:- Compute its bitmask,
targetMask. - For each character
cin theword:- Calculate a
predecessorMaskby turning off the bit corresponding tocintargetMask. - Check if
predecessorMaskexists instartMasks. - If it does, increment
countand break to process the nexttargetWord.
- Calculate a
- Compute its bitmask,
- Return
count.
Walkthrough
This optimized solution leverages bitmasking to create a unique and efficient integer representation for each word. The presence of each of the 26 lowercase letters is mapped to a bit in an integer.
First, we process all startWords. For each word, we calculate its bitmask by iterating through its characters and setting the corresponding bits. These integer masks are stored in a HashSet for O(1) average time lookups.
Next, we iterate through the targetWords. For each targetWord, we also compute its bitmask. Then, to check if it can be formed from a startWord, we try removing each of its characters one by one. In the bitmask world, removing a character is equivalent to turning off its corresponding bit. We can do this efficiently using the XOR operation (^). For each potential predecessor mask generated, we check if it exists in our set of startWord masks. If a match is found, we increment our count and move to the next targetWord.
import java.util.HashSet;import java.util.Set; class Solution { public int countWords(String[] startWords, String[] targetWords) { Set<Integer> startMasks = new HashSet<>(); for (String word : startWords) { startMasks.add(getBitmask(word)); } int count = 0; for (String word : targetWords) { int targetMask = getBitmask(word); // Iterate through each character of the target word to find a predecessor for (char c : word.toCharArray()) { // To remove a character, we turn off its corresponding bit. // Using XOR is a neat trick for this since the bit is guaranteed to be set. int predecessorMask = targetMask ^ (1 << (c - 'a')); if (startMasks.contains(predecessorMask)) { count++; break; // Found a match, move to the next targetWord } } } return count; } private int getBitmask(String word) { int mask = 0; for (char c : word.toCharArray()) { mask |= (1 << (c - 'a')); } return mask; }}Complexity
Time
O(N * L_s + M * L_t), where N and M are the number of words in `startWords` and `targetWords`, and L_s and L_t are their maximum string lengths. Calculating the bitmask for a word takes time proportional to its length. The overall complexity is the sum of time to process all characters in `startWords` and all characters in `targetWords`.
Space
O(N), where N is the number of `startWords`. The `HashSet` stores at most N integers, which is very memory-efficient.
Trade-offs
Pros
Highly efficient in both time and space due to the use of bitmasking.
Integer operations and lookups are much faster than string manipulations and sorting.
Cons
The bitmasking technique is specific to problems with a small, fixed alphabet and constraints like unique characters. It's less general than the sorting approach.
Solutions
Solution
class Solution {public int wordCount(String[] startWords, String[] targetWords) { Set<Integer> s = new HashSet<>(); for (String word : startWords) { int mask = 0; for (char c : word.toCharArray()) { mask |= (1 << (c - 'a')); } s.add(mask); } int ans = 0; for (String word : targetWords) { int mask = 0; for (char c : word.toCharArray()) { mask |= (1 << (c - 'a')); } for (char c : word.toCharArray()) { int t = mask ^ (1 << (c - 'a')); if (s.contains(t)) { ++ans; break; } } } return ans; }}class Solution {public: int wordCount(vector<string> &startWords, vector<string> &targetWords) { unordered_set<int> s; for (auto &word : startWords) { int mask = 0; for (char c : word) mask |= (1 << (c - 'a')); s.insert(mask); } int ans = 0; for (auto &word : targetWords) { int mask = 0; for (char c : word) mask |= (1 << (c - 'a')); for (char c : word) { int t = mask ^ (1 << (c - 'a')); if (s.count(t)) { ++ans; break; } } } return ans; }};class Solution: def wordCount(self, startWords: List[str], targetWords: List[str]) -> int: s = set() for word in startWords: mask = 0 for c in word: mask |= 1 << (ord(c) - ord('a')) s . add(mask) ans = 0 for word in targetWords: mask = 0 for c in word: mask |= 1 << (ord(c) - ord('a')) for c in word: t = mask ^ (1 << (ord(c) - ord('a'))) if t in s: ans += 1 break return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.