力扣472:连接词
力扣472题:连接词
传送门:https://leetcode-cn.com/problems/concatenated-words
一、题目描述:
给你一个 不含重复 单词的字符串数组 words ,请你找出并返回 words 中的所有 连接词 。
连接词 定义为:一个完全由给定数组中的至少两个较短单词组成的字符串。
示例一:输入:words = ["cat","cats","catsdogcats","dog","dogcatsdog","hippopotamuses","rat","ratcatdogcat"]
输出:["catsdogcats","dogcatsdog","ratcatdogcat"]
解释:"catsdogcats" 由 "cats", "dog" 和 "cats" 组成;
"dogcatsdog" 由 "dog", "cats" 和 "dog" 组成;
"ratcatdogcat" 由 "rat", "cat", "dog" 和 "cat" 组成。
二、此题有多种解法。
dp+substring 、字典树+DFS、序列DP(字符串哈希优化)
先来讲第一种题解
题目要求是所有连接词,而连接词是两个以及其以上的短字符组成,所以一个连接词肯定是可以由其他较短的字符串凭借而成
-
我们使用一个Set记录所有的单词,主要是为了去重、空字符串这种特殊的case,对单词数组words进行遍历,肯定先要把自己排除掉,因为不能自己构成。
-
判断是否能由目前Set(以不包含word中的元素构成当前word,如果可以,将word并入结果集,至于具体的判断过程如下:
- 如果Set集合没有元素或者字符串为空,直接返回false
- 构成dp数组,降低时间复杂度,dp[i]表示word从[0,i)之间的字符串是否可以分解成Set集合已经包含的较短的字符串,dp[n]就表示完全分解掉
- 假设现在要计算word[0,i-1]表示是否可以行成连接词,可以分解成两部分:word[0,j-1]和word[0,i-1],i最终可以取到n,这也就对应着dp数组的两部分:word[0,j-1]对应着dp[j],word[j,i]需要判断下Set集合是否包含word[j,i],如果包含则dp[i]=true;
- 返回dp[n];
-
最后再将当前单词word加入到Set进行下一轮的判断
代码如下
public List<String> findAllConcatenatedWordsInADict(String[] words) {
//set去重,ret返回值
Set<String> set = new HashSet<>();
List<String> ret = new ArrayList<>();
for (String word : words) {
set.add(word);
}
for (String word : words) {
//测试用例中包含空String,需要判断是否为空
if ("".equals(word)) {
continue;
}
set.remove(word);
if (canBreak(word, set)) {
ret.add(word);
}
set.add(word);
}
return ret;
}
//s字符串是否可以由set集合中的分词组成
private boolean canBreak(String s, Set<String> set) {
int n = s.length();
//特殊的case
if (set.size() == 0 || n == 0) {
return false;
}
//dp数组长度为n+1,dp[i]:s中0~i-1个字符能否由set中的单词组成
boolean[] dp = new boolean[n + 1];
dp[0] = true;
for (int i = 1; i < n + 1; i++) {
for (int j = 0; j < i; j++) {
//如果dp[j]为false,则s中0到j个字符不能由set中的单词组成,s后面的j~i更不行了
if (!dp[j]) {
continue;
}
//set中是否包含s中的j到i
if (set.contains(s.substring(j, i))) {
dp[i] = true;
break;
}
}
}
//返回dp[n],是因为dp[n]表示s中下标0到n的字符是否被set包含了,如果
return dp[n];
}
第二种题解:
- 将words按照长度进行排序,长度小的在前面,因为大的字符串肯定是由小的字符串组成
- 将words进行遍历,从字典树中搜索,如果存在则添加到返回值列表中去,没有则添加到字典树中
- 此处需要了解一下Trie.Search()方法。 208. 实现 Trie (前缀树) - 力扣(LeetCode) (leetcode-cn.com)
class Solution {
private Node root = new Node();
public List<String> findAllConcatenatedWordsInADict(String[] words) {
// 字典树 + DFS
// 先使用字典树把所有单词放进去
// 然后遍历每个单词是否可以在字典树中拆成多个单词
// 为了方便,我们可以先把words按长度排个序
// 这样,我们先遍历长度短的再遍历长度长的,可以边遍历边从字典树中查找边往字典树中放
List<String> ans = new ArrayList<>();
Arrays.sort(words, (a, b) -> a.length() - b.length());
for (String word : words) {
if (!word.isEmpty()) {
// 可以分割成多个单词,放入结果集中
if (dfs(word, 0)) {
ans.add(word);
} else {
// 是连接词的单词不用插入到字典树中
// 因为一个单词是连接词,说明字典树中存在多个更短的单词
// 即使一个更长的连接词由上述的连接词构成,它也可以拆成更多个更短的单词构成
// 比如,"abcd" 由 "ab" + "cd" 构成,同时存在另一个短单词 "ef"
// 那么,"abcdef" 可以由 "ab" + "cd" + "ef" 构成,不需要把 "abcd" 放入字典树
insert(word);
}
}
}
return ans;
}
/**
* 插入单词到字典树中
* @param word
*/
private void insert(String word) {
Node node = this.root;
for (int i = 0; i < word.length(); i++) {
if (node.children[word.charAt(i) - 'a'] == null) {
node.children[word.charAt(i) - 'a'] = new Node();
}
node = node.children[word.charAt(i) - 'a'];
}
node.isEnd = true;
}
/**
* 递归遍历单词,在字典树中寻找是否存在,遇到结束符则尝试分割单词
* @param word 单词
* @param i 单词中字符的索引
* @return 可以分割为多个单词,返回 true,否则返回 false
*/
private boolean dfs(String word, int i) {
// 因为不存在重复的单词,所以,不会出现只包含一个单词的连接词
if (i == word.length()) {
return true;
}
Node node = this.root;
while (i < word.length()) {
// 如果不存在,返回false
if (node.children[word.charAt(i) - 'a'] == null) {
return false;
}
node = node.children[word.charAt(i) - 'a'];
// 如果形成了一个完整的单词,深入下一层
if (node.isEnd && dfs(word, i + 1)) {
return true;
}
i++;
}
return false;
}
class Node {
boolean isEnd;
Node[] children;
Node() {
this.isEnd = false;
this.children = new Node[26];
}
}
}

浙公网安备 33010602011771号