重要的是先说三遍:先看Edit Distance,先看Edit Distance,先看Edit Distance!
本文是加强版K Edit Distance:给定N个字符串A,问哪些字符串和Target的最小编辑距离不大于K.
编辑操作和上面一样:增加(插入)、删、替换
例:
输入:A = ["abc","abd","abcd","adc,"a"](为了简单,只考虑小写字母的字符串)
Target = "ac"
K = 1
输出:["a","abc","adc"]
问题分析:可以利用Edit Distance里,对每个字符串进行计算最小编辑距离,然后再把那些最小编辑距离不超过K的字符串输出来即可,但是这样会有很多冗余计算,时间会大大增加(相同前缀的字符串越多会该问题表现越明显)。比如,计算"abc"和"abd"时,两个字符穿都有相同前缀"ab",dp解决该问题"ab"的最小编辑距离要重复计算两次,实际上,我们计算一次就好了。为此,我们加入前缀树(Trie)来优化计算。
转移方程:设f[Sp][j]表示前缀Sp和Target里前j个字符(Target[0,...,j-1])的最小编辑距离,这里Sp表示A中字符串的前缀.。
下面p的父节点是q,Sq表示:Sq是Sp的少一个字符的前缀(例:Sp = "abc",则Sq = "ab"),情况和上面一致,
只不过把i用Sp代替,i-1用Sq代替。
f[Sp][j] = min{f[Sp][j-1]+1, f[Sq][j-1]+1, f[Sq][j]+1,f[Sq][j-1]|Sp[last] = Target[j-1]}
min{case1:在Sp后插入Target[j-1];case2:用Target[j-1]替换Sp[last];case3:删除Sp[last]即变成Sq;case4:Sp[last]和Target最后一个相等}
初始条件:
空串和长度为L的最小编辑距离是L,空串就是root:f[Sroot][j] = f[""][j] = j,(j = 0,...,n)
Sp和空串的最小编辑距离是Sp的长度:f[Sp][0] = len(Sp)
计算顺序:
初始化f[Sroot][0]...f[Sroot][n]
按照前缀树深度优先搜索计算每个f[Sp][0],...,f[Sp][n]
答案:f[Sp][n]<=K且Sp为给一个给定的单词的节点p的个数
时间复杂度:O(A中所有字符串的前缀个数*n),空间复杂度O(A中所有字符串的前缀个数*n)
代码及注释如下:
#前缀树节点结构
class TrieNode(object):
def __init__(self):
# 是否构成一个完成的单词,因为只有小写单词,所以把单词转到0-26之间
self.is_word = False
self.children = [None] * 26
self.words = ""
#前缀树的类
class Trie(object):
def __init__(self):
self.root = TrieNode()
def insert(self, s):
"""insert a string called s from root."""
p = self.root
n = len(s)
for i in range(n):
if p.children[ord(s[i]) - ord('a')] is None:
new_node = TrieNode()
if i == n - 1:
new_node.is_word = True
new_node.words = s
p.children[ord(s[i]) - ord('a')] = new_node
p = new_node
else:
p = p.children[ord(s[i]) - ord('a')]
if i == n - 1:
p.is_word = True
p.words = s
return
class solution(object):
def __init__(self,A,Target,K):
self.target = Target
self.k = K
self.n = len(Target)
self.res = []
#init Tire
self.trie = Trie()
for i in range(len(A)):
self.trie.insert(A[i])
#init f[""][0,...,n]
self.f = [i for i in range(self.n+1)]
#dfs函数:在节点p,前缀Sp,f[j]:f[Sp][j]即前缀Sp转换成Target的前j个字符的最小编辑距离。
#todo:Sp+"a",...,Sp+"z",will update :f-->nf,
def dfs(self, p,f):
nf = [0 for i in range(self.n+1)]
#从root节点开始看是否有A中字符串
if p.is_word:
#这个字符串的最小编辑距离不大于K,加入结果res里
if f[self.n]<=self.k:
self.res.append(p.words)
#继续向下找p的子节点
#next prefix's char is i in A
for i in range(26):
#儿子节点为空,跳过
if p.children[i] == None:
continue
#特殊处理nf[0]
#f[Sq][0] = 0
#现在Sq-->Sp,也就是f[Sq][0]-->f[Sp][0],前缀多了一个字符,变成Target[0]
#因为f[Sp][0] = len(Sp),现在Sp多了一个字符,只要在原来的f[0]基础上加1就可以。
nf[0] = f[0]+1
#next Target's char is self.target[j-1]
for j in range(1,self.n+1):
#case1,2,3###i:Sp-->nf i-1:Sq-->f
#f[Sp][j] = min{f[Sp][j-1]+1, f[Sq][j-1]+1, f[Sq][j]+1}
nf[j] = min(nf[j-1]+1,f[j-1]+1,f[j]+1)
#case4
#f[Sp][j] = min{f[Sp][j],f[Sq][j-1]|Sp[last] = Target[j-1]}
#把字符转成0-26之间
c = ord(self.target[j-1])-ord("a")
if i == c:
nf[j] = min(nf[j],f[j-1])
#寻找子节点
self.dfs(p.children[i],nf)
return self.res
#也就是从root节点开始深度遍历前缀树,并且更新每一轮的f值,将编辑距离小于K的字符串加到res里,最后返回res
#dfs返回result
def return_res(self):
return self.dfs(self.trie.root,self.f)
A = ["abc","abd","abcd","adc","a"]
Target = "ac"
K = 1
C = solution(A,Target,K)
C.return_res()
#输出:['a', 'abc', 'adc']
说明:可以把递归里的f直接放在TrieNode的结构里,就不用带着f在函数里递归了