哦,不!你不小心把一个长篇文章中的空格、标点都删掉了,并且大写也弄成了小写。像句子"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个未识别字符。
提示:
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/re-space-lcci
d p [ i ] dp[i] dp[i] 表示考虑前 i i i 个字符最少的未识别的字符数量,从前往后计算 d p dp dp 值。
转移方程中,每次转移时考虑第 j ( j < = i ) j(j<=i) j(j<=i)个到第个字符组成的子串 s e n t e n c e [ j − 1... i − 1 ] ( 字 符 串 下 标 从 0 开 始 ) sentence[j-1...i-1](字符串下标从0开始) sentence[j−1...i−1](字符串下标从0开始)是否能在词典中找到:
如果能则 d p [ i ] = m i n ( d p [ i ] , d p [ j − 1 ] ) dp[i]=min(dp[i],dp[j-1]) dp[i]=min(dp[i],dp[j−1]);
否则可以复用 d p [ i − 1 ] dp[i-1] dp[i−1]的状态在加上当前未被识别的第 i i i 个字符,因此此时 d p dp dp 值为 d p [ i ] = d p [ i − 1 ] + 1 dp[i]=dp[i-1]+1 dp[i]=dp[i−1]+1
问题简化为了转移时如何快速判断当前子串是否存在于词典中。可以选择用字典树 T r i e Trie Trie 来优化查找, T r i e Trie Trie 是一种最大程度利用多个字符串前缀信息的数据结构,可以在 O ( w ) O(w) O(w) 的时间复杂度内判断一个字符串是否是一个字符串集合中某个字符串的前缀,其中 w w w 代表字符串的长度。(如果使用哈希表枚举的话,可能会产生很多冗余的枚举,最关键的是当前枚举的子串已经不再是词典中任何一个单词的后缀,所以可以用字典树来解决。)
将词典中的所有单词反序
插入到字典树中,每次转移时从当前的下标 i i i 出发倒序遍历 i − 1 , i − 2 , . . . , 0 i-1,i-2,...,0 i−1,i−2,...,0。在 T r i e Trie Trie上从根节点出发开始走,走到当前的字符 s e n t e n c e [ j ] sentence[j] sentence[j] 在树上没有相应的位置,说明 s e n t e n c e [ j . . . i − 1 ] sentence[j...i-1] sentence[j...i−1]不在词典中,并且不是任意一个单词的后缀
,此时跳出循环。否则,要判断当前的子串是否为一个单词,所以要在建立字典树时在单词结尾的节点做好标记 i s e n d is_end isend,这样在走到某个节点时可以判断是否为一个单词的末尾,并且根据状态转移方程来更新 d p dp dp值。
- 复杂度分析
# 节点类,需要标记是否为单词结尾
class TreeNode:
def __init__(self):
self.child = {}
self.is_end = False
class Solution:
def make_tree(self, dictionary):
for word in dictionary:
node = self.root
for s in word:
if not s in node.child:
node.child[s] = TreeNode()
node = node.child[s]
node.is_end = True
def respace(self, dictionary, sentence):
self.root = TreeNode()
self.make_tree(dictionary)
n = len(sentence)
dp = [0] * (n + 1)
for i in range(n-1, -1, -1):
dp[i] = n - i
node = self.root
for j in range(i, n):
c = sentence[j]
if c not in node.child:
dp[i] = min(dp[i], dp[j+1]+j-i+1)
break
if node.child[c].is_end:
dp[i] = min(dp[i], dp[j+1])
else:
dp[i] = min(dp[i], dp[j+1]+j-i+1)
node = node.child[c]
return dp[0]
Rabin-Karp
方法替换字典树,时间复杂度不变,空间复杂度可以优化到 O ( n + q ) O(n + q) O(n+q),其中 n n n 为 s e n t e n c e sentence sentence 中元素的个数, q q q 为词典中单词的个数。需要大量地判断某个字符串是否是给定单词列表中的前缀/后缀。
(把单词倒着插入就变成搜后缀了。)字典树参考讲解:
https://leetcode-cn.com/problems/short-encoding-of-words/solution/99-java-trie-tu-xie-gong-lue-bao-jiao-bao-hui-by-s/
A u t h o r : C h i e r Author: Chier Author:Chier