题目:
题目链接: https://leetcode-cn.com/problems/short-encoding-of-words/
解题思路:
1. 基础解法,算法实现逻辑如下:
- 对单词列表进行去重
- 对去重后的列表进行排序,排序规则为:根据单词长度逆序(防止长字符串在后,短字符串在前,结果在最终字符串中保存了两份数据,比如['time', 'me']和['me', 'time']的区别)
- 对排序后的元素进行遍历,并添加变量s保存字符串,s的更新逻辑如下:
- 如果当前单词curr_s + '#'能够在s中匹配到,则不更新s
- 如果curr_s + '#'无法在s中匹配上,则更新s += curr_s + '#'
- 返回s的长度
2. 字典树
问题可以转换为,匹配当前档次,是否为其他单词的后缀
如果是,当前单次不需要记录长度
如果不是,就需要记录单词长度 + 1
根据这一点,可以对单词逆序排序后,对字母建立字典树,然后对叶子节点的高度 + 1进行求和,就是最短的字符串长度
假设单词列表为:['time', 'me', 'abe', 'el', 'bell'],对每个单词逆序后的列表为['emit', 'em', 'eba', 'le', 'lleb'], 最终建立的树如下:
代码实现:
方法一:
class Solution:
def minimumLengthEncoding(self, words: List[str]) -> int:
new_words = list(set(words))
new_words.sort(key = len, reverse = True)
s = ''
for curr_s in new_words:
if -1 == s.find(curr_s + '#'):
s += curr_s + '#'
return len(s)
方法二(借用官方题解中Lucien的评论):
一步步从最正常的写法走向Pythonic的解。
# 定义字典树中的一个节点
class Node(object):
def __init__(self):
self.children={}
class Solution:
def minimumLengthEncoding(self, words: List[str]) -> int:
words = list(set(words)) #需要去重,否则在之后计算“叶子高度”的时候会重复计算
trie=Node() #这是字典树的根
nodes=[] #这里保存着每个word对应的最后一个节点,比如对于单词time,它保存字母t对应的节点(因为是从后往前找的)
for word in words:
now=trie
for w in reversed(word):
if w in now.children:
now=now.children[w]
else:
now.children[w]=Node()
now=now.children[w]
nodes.append(now)
ans=0
for w,c in zip(words,nodes):
if len(c.children)==0: #没有children,意味着这个节点是个叶子,nodes保存着每个word对应的最后一个节点,当它是一个叶子时,我们就该累加这个word的长度+1,这就是为什么我们在最开始要去重
ans+=len(w)+1
return ans
相信以上的解答大家可以看懂,那么就从Node开始简化。原先我们把Node声明为一个类,但这个类中只有一个字典,所以我们不如就直接用一个字典来表示节点,一个空字典以为着这是一个叶子节点,否则字典中的每一个元素都是它的一个孩子,上面的代码可以简化为:
class Solution:
def minimumLengthEncoding(self, words: List[str]) -> int:
words = list(set(words)) #需要去重,否则在之后计算“叶子高度”的时候会重复计算
trie={} #这是字典树的根
nodes=[] #这里保存着每个word对应的最后一个节点,比如对于单词time,它保存字母t对应的节点(因为是从后往前找的)
for word in words:
now=trie
for w in reversed(word):
if w in now:
now=now[w]
else:
now[w]={}
now=now[w]
nodes.append(now)
ans=0
for w,c in zip(words,nodes):
if len(c)==0: #一个空字典,意味着这个节点是个叶子
ans+=len(w)+1
return ans
继续简化,我们不想在生成字典树时每次都判断“当前字典有没有这个键”,我们希望,有这个键,就返回它的值,否则返回一个空字典给我。很自然,我们需要用到defaultdict,它默认返回一个字典。但,只是返回一个普通字典吗?比如defaultdict(dict)? 不行,实际上它需要返回一个defaultdict,且这个defaultdict仍旧会递归地返回defaultdict。于是,递归地,我们定义这样一个函数,它返回一个defaultdict类型,且它的默认值是该类型本身。 Trie = lambda: collections.defaultdict(Trie) ,注意,这里的Trie是一个函数,它返回一个defaultdict实例。有了它,我们创建字典树的过程就变成了:
nodes=[]
Trie = lambda: collections.defaultdict(Trie)
trie = Trie()
for word in words:
now=trie
for w in word[::-1]:
now=now[w]
nodes.append(now)
更进一步,可以简化为
nodes=[]
Trie = lambda: collections.defaultdict(Trie)
trie = Trie()
for word in words:
nodes.append(trie[word[-1]][word[-2]].........)
它就变成了
nodes = [reduce(dict.__getitem__, word[::-1], trie)
for word in words]
先不管数组的推导式,单看数组的一项 reduce(dict.__getitem__, word[::-1], trie),reduce三个参数分别为:方法,可循环项,初始值。即它初始值是trie,按照word[::-1]的循环顺序,每次去执行方法dict.__getitem__,且将这个输出作为下次循环的输入,所以它就是trie[word[-1]][word[-2]].........的意思。
最终优化后的代码整体为:
class Solution:
def minimumLengthEncoding(self, words: List[str]) -> int:
words = list(set(words)) #remove duplicates
#Trie is a nested dictionary with nodes created
# when fetched entries are missing
Trie = lambda: collections.defaultdict(Trie)
trie = Trie()
#reduce(..., S, trie) is trie[S[0]][S[1]][S[2]][...][S[S.length - 1]]
nodes = [reduce(dict.__getitem__, word[::-1], trie)
for word in words]
#Add word to the answer if it's node has no neighbors
return sum(len(word) + 1
for i, word in enumerate(words)
if len(nodes[i]) == 0)