力扣刷题笔记 面试题 17.13. 恢复空格 C#

今日签到题,题目如下:

哦,不!你不小心把一个长篇文章中的空格、标点都删掉了,并且大写也弄成了小写。像句子"I reset the computer. It still didn’t boot!"已经变成了"iresetthecomputeritstilldidntboot"。在处理标点符号和大小写之前,你得先把它断成词语。当然了,你有一本厚厚的词典dictionary,不过,有些词没在词典里。假设文章用sentence表示,设计一个算法,把文章断开,要求未识别的字符最少,返回未识别的字符数。

注意:本题相对原题稍作改动,只需返回未识别的字符数

 

示例:

输入:
dictionary = ["looked","just","like","her","brother"]
sentence = "jesslookedjustliketimherbrother"
输出: 7
解释: 断句后为"jess looked just like tim her brother",共7个未识别字符。
提示:

0 <= len(sentence) <= 1000
dictionary中总字符数不超过 150000。
你可以认为dictionary和sentence中只包含小写字母。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/re-space-lcci
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

首先肯定是考虑动态规划,但是因为我在考虑遍历 sentence 的时候,一直想着从 dictionary 元素首字母开始判断,导致遍历结果的字符串长度不明确。

没有想到解决方法,所以先摸索着尝试使用暴力解题,每次在 sentence 中找到匹配字典的字符串就删除这个长度。但是这是错误的,因为可能出现如下情况:dictionary = ["ab","bcde"],sentence = "abcef"。如果使用上述方法遍历,会先将 "ab" 移出 sentence,这样导致了无法判断更优的 "bcde"。

还是先看官方题解,提供了两种解法:

  1. Trie + 动态规划

  2. 字符串哈希

这里只讨论方法一,方法二我也没仔细看。

在此之前并未了解过 Trie 使用,故先理解动态规划部分。题解中很轻易的点出如何令遍历结果的字符串长度固定,即从 dictionary 元素尾字母开始判断。即,创建 int 数组 dp,dp 长度为 n + 1,dp[0] 为空字符串长度的边界状态,dp[i] 为 sentence 前 i 个字符串的未识别字符数。遍历过程每次判断 dp[i] 时,以 sentence[i - 1](下标比长度少1)用作匹配 dictionary  元素的尾字符。如果成功匹配,由于删去了匹配的字符串长度 len,dp[i] 的状态会转移自 dp[i - len],即 dp[i] = dp[i - len]。否则,sentence[i - 1] 就只是一个新的无法识别的字符,dp[i] 的状态会转移自 dp[i-1],并且长度加 1,即 dp[i] = dp[i-1] + 1。

先使用 string.Substring(i - len,len) 截取 sentence 对应长度与字典元素进行匹配。

复杂度分析:

时间复杂度:首先遍历所有 sentence 的所有下标,嵌套遍历 dictionary 中所有元素进行匹配,由于 string 的 equal 实现时遍历 string 中的每个字符,所以时间复杂度为O(N*|dictionary|),N为 sentence 长度,|dictionary| 所有字符串长度。因为 dictionary 的最大长度为 150000,所以本方法时间复杂度较大。

空间复杂度:只使用了一个 dp 数组,故空间复杂度为 O(N)。(空间复杂度正确性我不是很确定,有路过的大佬知道的话欢迎指出)

以下为自己提交的代码:

public class Solution {
    public int Respace(string[] dictionary, string sentence) {
        int n = sentence.Length;
        int[] dp = new int[n + 1];
        dp[0] = 0;
        for (int i = 1;i < n + 1;i++)
        {
            dp[i] = dp[i-1] + 1;
            for(int j = 0;j < dictionary.Length;j++)
            {
                int len = dictionary[j].Length;
                if (i - len < 0) continue;
                string temp = sentence.Substring(i - len,len);
                if (temp == dictionary[j])
                {
                    dp[i] = Math.Min(dp[i],dp[i - len]);
                }
            }
        }
        return dp[n];
    }
}

由于这种方法的空间复杂度较高,官方题解引入了 Trie 减少时间开销。以 sentence[i-1] 为尾字符匹配 dictionary 中的元素实际相当于从 sentence[i-1] 开始逆序匹配 dictionary 中元素对应字符。由于 dictionary 中的元素只由 26 个小写字母组成,所以元素很容易出现相同的后缀,则在逆序匹配的过程中会重复的遍历到这些相同的前缀。

使用 Trie 解决该问题,首先以没有字符表达的 Trie 对象 root 作为根节点,Trie 的成员由一个长度为 26 的 Trie 数组 nextArr 指向子节点,初始化时数组中所有值为空。成员还有一个 bool 值 isEnd,用作标识一个单词是否结束。然后将 dictionary 的所有元素存入 root。存入过程将元素中的字符逆序存入,已存在的后缀会使用相同的子节点类路径,然后在不同的前一字符处节点产生分支。逆序存入的原因是 sentence[i-1] 与 dictionary 中元素的匹配过程是逆序的,匹配过程即是逆序匹配 Trie 树。

如果匹配 Trie 树的过程中,对应字符的子节点为 null,即匹配失败,开始匹配 sentence 的下一个位置。

如果对应字符的子节点的 isEnd 为True,即在该位置匹配成功,转移状态。由于还可能存在更长的匹配字符串(如 "her" 和 "brother"),所以继续匹配知道子节点为null。

初次接触 Trie,上述叙述比较混乱。

复杂度分析直接引用官方题解

力扣刷题笔记 面试题 17.13. 恢复空格 C#_第1张图片

以下为自己提交的代码:

public class Solution {
    public int Respace(string[] dictionary, string sentence) {
        int n = sentence.Length;
        Trie root = new Trie();
        for(int i = 0; i < dictionary.Length;i++)
        {
            root.Insert(dictionary[i]);
        }
        int[] dp = new int[n + 1];
        dp[0] = 0;
        for (int i = 1;i < n + 1;i++)
        {
            dp[i] = dp[i-1] + 1;
            Trie node = root;
            for(int j = i - 1;j >= 0;j--)
            {
                int index = sentence[j] - 'a';
                if (node.nextArr[index] == null)
                {
                    break;
                }
                else if (node.nextArr[index].isEnd)
                {
                    dp[i] = Math.Min(dp[i],dp[j]);
                }
                node = node.nextArr[index];
            }
        }
        return dp[n];
    }
}

public class Trie{
    public Trie[] nextArr = new Trie[26];
    public bool isEnd;
    public Trie()
    {
        isEnd = false;
    }
    public void Insert(string word)
    {
        Trie root = this;
        for(int i = word.Length - 1; i >= 0; i--)
        {
            int index = word[i] - 'a';
            if (root.nextArr[index] == null)
            {
                root.nextArr[index] = new Trie();
            }
            root = root.nextArr[index];
        }
        root.isEnd = true;
    }
}

以下为两种实现的提交结果,时间较长的为没有使用 Trie 的结果:

力扣刷题笔记 面试题 17.13. 恢复空格 C#_第2张图片

你可能感兴趣的:(基础算法)