力扣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(字符串哈希优化)

先来讲第一种题解

题目要求是所有连接词,而连接词是两个以及其以上的短字符组成,所以一个连接词肯定是可以由其他较短的字符串凭借而成

  1. 我们使用一个Set记录所有的单词,主要是为了去重、空字符串这种特殊的case,对单词数组words进行遍历,肯定先要把自己排除掉,因为不能自己构成。

  2. 判断是否能由目前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];
  3. 最后再将当前单词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];
    }

第二种题解:

  1. words按照长度进行排序,长度小的在前面,因为大的字符串肯定是由小的字符串组成
  2. words进行遍历,从字典树中搜索,如果存在则添加到返回值列表中去,没有则添加到字典树中
  3. 此处需要了解一下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];
        }
    }

}
posted @ 2021-12-29 18:38  7Aom1  阅读(77)  评论(0)    收藏  举报