给定一个不含重复单词的列表,编写一个程序,返回给定单词列表中所有的连接词。
连接词的定义为:一个字符串完全是由至少两个给定数组中的单词组成的。
示例:
输入: ["cat","cats","catsdogcats","dog","dogcatsdog","hippopotamuses","rat","ratcatdogcat"]
输出: ["catsdogcats","dogcatsdog","ratcatdogcat"]
解释: "catsdogcats"由"cats", "dog" 和 "cats"组成;
"dogcatsdog"由"dog", "cats"和"dog"组成;
"ratcatdogcat"由"rat", "cat", "dog"和"cat"组成。
说明:
给定数组的元素总数不超过 10000。
给定数组中元素的长度总和不超过 600000。
所有输入字符串只包含小写字母。
不需要考虑答案输出的顺序。
思路分析:
请先翻阅 LeetCode 单词拆分
不难发现上一次将字符串拆分为集合中的单词,是这一题的子问题,此题我们要做的就是将集合中的某个元素字符串拆分为集合中其他的元素。
方法一:动态规划法。
class Solution {
public:
vector<string> findAllConcatenatedWordsInADict(vector<string>& words) {
if (words.size() <= 2) {
return {};
}
vector<string> result;
unordered_set<string> wordSet(words.begin(), words.end());//存储word集合
for (string word : words) {
//从集合中移除自己
wordSet.erase(word);
//下面是上一题单词拆分的思路,即降word拆分为wordSet剩余的字符串
int wordSize = word.size();
if (wordSize == 0) {
continue;
}
vector<bool> dp(wordSize + 1, false);//dp[i]表示的word串从下标[1, i]可拆分
dp[0] = true;
for (int i = 0; i <= wordSize; ++i) {
for (int j = 0; j < i; ++j) {
//只有当0 ~j可拆的前提下,拆分[j, i - j]才有意义
if (dp[j] && wordSet.count(word.substr(j, i - j))) {
dp[i] = true;
break;
}
}
}
//如果[1, wordSize]可拆
if (dp.back()){
result.push_back(word);
}
//现在需要将word重新返回到集合中
wordSet.insert(word);
}
return result;
}
};
方法二:不难发现,一个字符串如果可以拆分,那么拆分的子字符串长度必须小于原字符串,所以利用这个进行剪枝。
先将words进行长度升序排序,当在进行拆分words[i]时,只能使用words[0,i - 1]中的单词进行拼凑。
class Solution {
vector<string> results;
unordered_set<string> wordSet;
int min_len = 1;
//判断word是否可有当前wordSet中的单词进行拆分
bool isConcatenated(string const & word) {
if (wordSet.count(word)) {
//特殊情况,wordSet中直接含有word
return true;
}
//否则将word进行拆开
for (int i = min_len; i < word.size() - min_len + 1 ; ++i){
//只有当wordSet中含有word.substr(0, i)才有继续拆分下标i之后的字符段的必要
if (wordSet.count(word.substr(0, i)) > 0 && isConcatenated(word.substr(i, word.size() - i))){
return true;
}
}
return false;
}
public:
vector<string> findAllConcatenatedWordsInADict(vector<string>& words) {
//依据长度进行排序
sort(words.begin(), words.end(), [](const string &lhs, const string &rhs){return lhs.size() < rhs.size();});
min_len = max(1, words.front().size());//获取最小的长度
//根据字符串只能有更小的字符串拼接而成,所以从words的起始开始,逐渐进行查找
for (int i = 0; i < words.size();++i) {
//当前wordSet = words[0, i - 1]
//尝试使用当前wordSet中的单词进行拆分
if (words[i].size() >= min_len * 2 && isConcatenated(words[i])){
results.push_back(words[i]);
}
//现在需要将words[i]加入set中
wordSet.insert(words[i]);
}
return results;
}
};
方法三:使用字典数(前缀树)。如果不清楚该数据集结构请先翻阅 LeetCode 实现Trie(前缀树)
先将所有单词进行建树,然后进行搜索。
//前缀树的程序表示
class TrieNode {
public:
bool isWord;//当前节点为结尾是否是字符串
vector<TrieNode*> children;
TrieNode() : isWord(false), children(26, nullptr) {}
~TrieNode() {
for (TrieNode* child : children)
if (child) delete child;
}
};
class Solution {
private:
TrieNode *trieRoot;//构建的单词后缀树
//在树中插入一个单词的方法实现
void addWord(string &word) {
TrieNode *ptr = trieRoot;//扫描这棵树,将word插入
//将word的字符逐个插入
for (auto ch : word) {
if (ptr->children[ch - 'a'] == NULL) {
ptr->children[ch - 'a'] = new TrieNode();
}
ptr = ptr->children[ch - 'a'];
}
ptr->isWord = true;//标记为单词
}
//从单词word下标nowIndex开始进行寻找单词,进行匹配
bool myFind(string &word, int nowIndex, int cnt) {
int wordSize = word.size();
TrieNode *ptr = trieRoot;//扫描这棵树(顶开始)
for (int index = nowIndex; index < wordSize; ++index){
if (ptr->children[word[index] - 'a'] == NULL) {
return false;
}
ptr = ptr->children[word[index] - 'a'];
//如果prt已经在一个单词的尾端
if (ptr->isWord){
if (index == wordSize - 1){//word这个单词搜索完成
return cnt >= 1;//至少需要分成两个单词
}
if (myFind(word, index + 1, cnt + 1)){
//继续匹配index后面的部分
return true;
}
}
}
return false;
}
public:
vector<string> findAllConcatenatedWordsInADict(vector<string>& words) {
vector<string> result;
trieRoot = new TrieNode();
//首先扫描一遍,建立前缀树
for (auto &word : words) {
if (word.size()) {
addWord(word);
}
}
//逐个单词进行搜索
for (auto &word : words) {
if (myFind(word, 0, 0)) {
result.push_back(word);
}
}
return result;
}
};