题目描述
哦,不!你不小心把一个长篇文章中的空格、标点都删掉了,并且大写也弄成了小写。像句子"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
解题思路
方法一:字典树+动态规划
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值。
- 复杂度分析
- 时间复杂度: O ( ∣ d i c t i o n a r y ∣ + n 2 ) O(|dictionary| + n^2) O(∣dictionary∣+n2), 其中 ∣ d i c t i o n a r y ∣ |dictionary| ∣dictionary∣ 代表词典中的总字符数, n = s e n t e n c e . l e n g t h n=sentence.length n=sentence.length。建字典树的时间复杂度取决于单词的总字符数,即 O ( ∣ d i c t i o n a r y ∣ ) O(|dictionary|) O(∣dictionary∣)。 d p dp dp数组一共有 n + 1 n+1 n+1个状态,每个状态转移时最坏需要 O ( n ) O(n) O(n)的时间复杂度,因此时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
- 空间复杂度: O ( ∣ d i c t i o n a r y ∣ ∗ S + n ) O(|dictionary| * S + n) O(∣dictionary∣∗S+n),S 代表字符集大小(这里为小写字母数,S=26)。 d p dp dp数组的空间代价为 O ( n ) O(n) O(n)。
代码实现
# 节点类,需要标记是否为单词结尾
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]
Tips
- 也可以使用
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 为词典中单词的个数。 - 字典树又名前缀树,Trie树,是一种存储大量字符串的树形数据结构,相比于HashMap存储,在存储单词(和语种无关,任意语言都可以)的场景上,节省了大量的内存空间。
- 很明显是用字典树去做的题目的特征:
需要大量地判断某个字符串是否是给定单词列表中的前缀/后缀。
(把单词倒着插入就变成搜后缀了。)
A u t h o r : C h i e r Author: Chier Author:Chier