哦,不!你不小心把一个长篇文章中的空格、标点都删掉了,并且大写也弄成了小写。像句子"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中只包含小写字母。
定义 dp[i] 表示考虑前 i 个字符最少的未识别的字符数量,从前往后计算 dp 值。
考虑转移方程,每次转移的时候我们考虑第 j(j≤i) 个到第 i 个字符组成的子串 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])
否则没有找到的话我们可以复用 dp[i−1] 的状态再加上当前未被识别的第 i 个字符,因此此时 dp 值为
d p [ i ] = d p [ i − 1 ] + 1 dp[i]=dp[i−1]+1 dp[i]=dp[i−1]+1
最后问题化简成了转移的时候如何快速判断当前子串是否存在于词典中,可以通过字典树 Trie 来优化查找。
Trie 是一种最大程度利用多个字符串前缀信息的数据结构,它可以在 O(w) 的时间复杂度内判断一个字符串是否是一个字符串集合中某个字符串的前缀,其中 w 代表字符串的长度。
我们将词典中所有的单词「反序」插入字典树中,然后每次转移的时候我们从当前的下标 i 出发倒序遍历 i−1,i−2,⋯,0。在 Trie 上从根节点出发开始走,直到走到当前的字符 sentence[j] 在 Trie 上没有相应的位置,说明 sentence[j⋯i−1] 不存在在词典中,且它已经不是「任意一个单词的后缀」,此时我们直接跳出循环即可。否则,我们需要判断当前的子串是否是一个单词,这里我们直接在插入 Trie 的时候在单词末尾的节点打上一个 isEnd 的标记即可,这样我们在走到某个节点的时候就可以判断是否是一个单词的末尾并根据状态转移方程更新我们的 dp 值。
具体实现以及示例的图画解析可以看下面:
动态规划
def respace(self, dictionary: List[str], sentence: str) -> int:
n = len(sentence)
dp = [i for i in range(n + 1)]
for i in range(n):
for j in range(i, -1, -1):
if sentence[j:i + 1] in dictionary:
dp[i + 1] = min(dp[i + 1], dp[j])
else:
dp[i + 1] = min(dp[i + 1], dp[i] + 1)
return dp[-1]
动态规划+字典树
class Solution:
def respace(self, dictionary, sentence: str) -> int:
trie = Trie()
# 初始化字典树
for word in dictionary:
cur = trie
for i in range(len(word)-1, -1, -1):
cur = cur.next_nodes[word[i]]
cur.isEnd = True
dp = [float('inf') for _ in range(len(sentence)+1)]
dp[0] = 0
for i in range(1, len(sentence)+1):
dp[i] = dp[i - 1] + 1
j = i
cur = trie
while j >= 1 and sentence[j-1] in cur.next_nodes:
cur = cur.next_nodes[sentence[j]]
if cur.isEnd == True:
dp[i] = min(dp[i], dp[j-1])
if dp[i] == 0:
break
j -= 1
return dp[-1]
class Trie:
def __init__(self):
self.next_nodes = collections.defaultdict(Trie)
self.isEnd = False