dp基础之难点解析K Edit Distance

重要的是先说三遍:先看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在函数里递归了

你可能感兴趣的:(动态规划专解)