[论文笔记] Simple Fast Algorithms for the Editing Distance between Trees and Related Problems 树编辑距离计算方法

[论文笔记] Simple Fast Algorithms for the Editing Distance between Trees and Related Problems 树编辑距离计算方法

1. 论文

  1. 论文:

    Zhang, K., & Shasha, D. (1989). Simple Fast Algorithms for the Editing Distance between Trees and Related Problems. SIAM Journal on Computing, 18(6), 1245–1262.

  2. 下载链接:https://sci-hub.st/10.1137/0218082

本篇博客针对论文的前半部分,对于后半部分(比如,复杂度、并行计算)不涉及

2. 引言

2.1 树编辑距离

  1. 研究对象

    结构:广义上的树,没有明确限制。因此非二叉树也可使用。

    信息:每个节点包含一个字符串label

  2. 树编辑距离是什么

    个人理解:衡量从一棵树到另一棵树,至少需要几步转换操作

    原文:The distance between two ordered trees is considered to be the weighted number of edit operations (insert, delete, and modify) to transform one tree to another.

  3. 树编辑距离能干什么

    个人理解:至少可以用来比较两棵树的相似性

  4. “转换操作”包含哪些

    假设T1转换到T2,对T1的操作包括:

    • 插入节点:插入一个T2的节点

    • 删除节点:删除一个T1的节点

    • 修改节点:将一个T1节点的label改为T2节点的label

文中详细且严谨的数学定义,而且比较好懂,这部分建议阅读原文

2.2 示例

  1. 为讨论方便,我们使用论文中假定的两棵树。(因为不指定二叉树,所以区分左右子节点也变得没有价值)

    T1
    T2
    d
    f
    e
    a
    c
    b
    c
    f
    e
    d
    a
    b
  2. 对于这两棵树,我们的目标就是计算出从T1到T2的树编辑距离

  3. 对于树的一些描述

    1. 后续遍历:描述一棵树的节点组成,通过后续遍历的顺序确定结点的序号(index)
    2. 每个节点的字母,作为该节点的label,也就是这个节点携带的信息,恰好字母的顺序就是后续

即,对于T1而言,T1由6个节点组成。T1[3]表示index为3的节点,同时T1[3]对应的label为c

注:在后续中讨论中,为方便,我将第n个节点也称为"节点+label",比如T1的第3个节点称为节点c,以此类推

2.3 关键概念

2.3.1 l(i)

作者甚至没有给这个关键概念起个名字。为了讨论方便,就还它LNI(the leafmost node index)吧

  1. 描述的是什么

    • 论文描述: l ( i ) l(i) l(i) is the number of the leftmost leaf descendant of the subtree rooted at T[i]. When T[i] is a leaf, l ( i ) = i l(i)=i l(i)=i.

    • 个人理解: l ( i ) l(i) l(i) is the index of the leftmost leaf node of the subtree rooted at T[i]. When T[i] is a leaf, l ( i ) = i l(i)=i l(i)=i.

      也就是:在以T[i]为根节点的子树中,最左叶子节点的序号为 l ( i ) l(i) l(i)

  2. 举例以及 l ( i ) l(i) l(i)的特性

  3. 例子

    在T1中, l ( 4 ) = 1 l(4)=1 l(4)=1,这也就是以第4个节点(节点d)为根节点的子树中最左叶子节点的index为1

  4. 特性:

    • 多个 l ( i ) l(i) l(i)可以指向同一个index,比如 l ( 4 ) = l ( 6 ) = 1 l(4)=l(6)=1 l(4)=l(6)=1

    • 叶子节点的 l ( i ) l(i) l(i)为自身,因为以它为根节点的子树只包含它自己,比如 l ( 2 ) = 2 l(2)=2 l(2)=2

f
d
e
a
c
b

2.3.2 LR_keyroots

  1. 是什么

    • 论文定义: L R _ k e y r o o t s ( T ) = { k ∣  there exists no  k ’ > k  such that  l ( k ) = l ( k ’ ) } . LR\_keyroots(T)= \{ k | \text{ there exists no } k’> k \text{ such that } l(k)= l(k’)\}. LR_keyroots(T)={k there exists no k>k such that l(k)=l(k)}.

    • 个人理解:前文提过多个LNI可以指向同一个index,这里的LR_keyroots中的元素是在一颗子树内,相同LNI但index最大的结点。

  2. 举例
    在T1中对所有节点计算LNI,可得到
    l ( 1 ) = l ( 4 ) = l ( 6 ) = 1 l ( 2 ) = l ( 3 ) = 2 l ( 5 ) = 5 l(1)=l(4)=l(6)=1 \\ l(2)=l(3)=2 \\ l(5)=5 l(1)=l(4)=l(6)=1l(2)=l(3)=2l(5)=5
    因此T1的LR_keyroots有三个元素,(按上面三个等式的顺序)依次是6,3,5

2.3.3 treelist(i,j)

  1. 是什么

    • 论文定义:The distance between the subtree rooted at and the subtree rooted at j is sometimes denoted treedist( i, j ).

论文表达的很清楚,以i为根节点的子树和以j为根节点的子树之间的距离

  1. 展开表示

    1. 对于一棵子树 T [ N 1 , . . . , N n ] T[N_1,...,N_n] T[N1,...,Nn],文中采用后序遍历,第一个节点 N 1 N_1 N1肯定为最左子节点,最后一个节点 N n N_n Nn肯定为根节点

      • T[1,2,3,4]中,第一个节点index为1,最后一个节点index为4

      于是子树可以表示成 T [ l ( N n ) , . . . , N n ] T[l(N_n),...,N_n] T[l(Nn),...,Nn](以下我简写为 T [ l ( N n ) : N n ] T[l(N_n):N_n] T[l(Nn):Nn],注意包含 N n N_n Nn

    2. 因此根据treelist的定义,treelist可以展开
      t r e e l i s t ( i , j ) = t r e e l i s t ( T 1 [ l ( i ) : i ] , T 2 [ l ( j ) : j ] ) treelist(i,j)=treelist(T_1[l(i):i],T_2[l(j):j]) treelist(i,j)=treelist(T1[l(i):i],T2[l(j):j])
      注意:

      • 文中将 t r e e l i s t ( i , j ) treelist(i,j) treelist(i,j)的展开记为 f o r e s t d i s t ( T 1 [ l ( i ) , . . . , i ] , T 2 [ l ( j ) , . . . , j ] ) ) forestdist(T_1[l(i),...,i],T_2[l(j),...,j])) forestdist(T1[l(i),...,i],T2[l(j),...,j]))。为简化表示,我将其记为 t r e e l i s t ( T 1 [ l ( i ) : i ] , T 2 [ l ( j ) : j ] ) treelist(T_1[l(i):i],T_2[l(j):j]) treelist(T1[l(i):i],T2[l(j):j])
      • t r e e l i s t ( i , j ) treelist(i,j) treelist(i,j)不恒等于 t r e e l i s t ( T 1 [ 1 : i ] , T 2 [ 1 : j ] ) treelist(T_1[1:i],T_2[1:j]) treelist(T1[1:i],T2[1:j]),需要看 l ( i ) = = 1 l(i)==1 l(i)==1并且 l ( j ) = = 1 l(j)==1 l(j)==1,这一点很重要

3. 算法

在明确这几个关键概念之后,我们可以讨论论文的算法了

  1. 迭代部分

    输入:两棵树T1[1:n],T2[1:m]
    输出:树编辑距离treedist(n,m)
    
    Step1: 对每棵树计算每个节点的LNI
    Step2: 计算LR_keyroots(T1), LR_keyroots(T2), 每棵树的Keyroots按节点index大小排序
    Step3: 迭代计算treelist
            for i in LR_keyroots(T1):
                for j in LR_keyroots(T2):
                    treelist(i,j) = calculate_treelist(T_1[l(i):i],T_2[l(j):j])
    

    可以看到,这是一个迭代算法,从index小的节点逐步迭代到根节点

    论文中认为,从小向大迭代可降低计算量

  2. 计算部分

    # 算法表示为python的形式,但注意不同:T[1:i]包含第i个节点
    
    def calculate_treelist(T_1[N_strart:N_end],T_2[M_strart:M_end]):
    	
        # 可分为以下4种情况为论文中引理3和引理4做支撑
    	if N_end < N_start and M_end < M_start:
            # 两棵子树不存在
            return 0
        
        if N_end >= N_start and M_end < M_start:
            # T1子树存在,T2子树不存在
            # 此时从T1子树变到T2子树,执行动作应为:删除全部T1子树节点
           return T1子树节点个数
                
        if N_end < N_start and M_end >= M_start:
            # T1子树不存在,T2子树存在
            # 此时从T1子树变到T2子树,执行动作应为:插入全部T2子树节点
           return T2子树节点个数
    
    	# 两棵子树都存在
        # 此时一定N_strart = l(i), M_strart = l(j)
        for i1 in T_1[l(i):i]:
        	for j1 in T_2[l(j):j]:
                # 可分为以下2种情况为论文中引理5做支撑
                if l(i1) == l(i) and l(j1) == l(j):
                    # i1为i最左侧后代,且j1为j最左侧后代
                    treelist(T_1[l(i):i1],T_2[l(j):j1]) = min(
                        calculate_treelist(T_1[l(i):i1-1],T_2[l(j):j1]) + 1, # 对应操作为:删除T1[i1]
                        calculate_treelist(T_1[l(i):i1],T_2[l(j):j1-1]) + 1, # 对应操作:插入T2[i1]
                        calculate_treelist(T_1[l(i):i1-1],T_2[l(j):j1-1]) + judge_label(T_1[i1],T_2[j1]) 
                        # 对应操作:判断label是否相同,相同则返回0,不同返回1
                    	)
                    
                    # 当计算到根节点时,即i1 = i时,一定有l(i1) = l(i)    
                    # 当计算到最后一次循环,即i和j为两个根节点,就是我们需要的结果    
                    if i1 == i and ji == j:
                    	return treelist(T_1[l(i):i1],T_2[l(j):j1])
                else:
                    treelist(T_1[l(i):i1],T_2[l(j):j1]) = min(
                        calculate_treelist(T_1[l(i):i1-1],T_2[l(j):j1]) + 1, # 对应操作为:删除T1[i1]
                        calculate_treelist(T_1[l(i):i1],T_2[l(j):j1-1]) + 1, # 对应操作:插入T2[i1]
                        calculate_treelist(T_1[l(i):l(i1)-1],T_2[l(j):l(j1)-1]) \
                        	+ calculate_treelist(T_1[l(i1):i1],T_2[l(ji):j1]) # 将子树分为两部分进行计算
                    	)
    
    
    def judge_label(node1,node2):
        # 比较两个节点的label是否相同
        if label(node1) == label(node2):
            # 对应操作:两节点label相同,不做处理
            return 0
        else:
            # 对应操作:将node1的label变为node2的label
            return 1
    
  3. 对引理5分为两种情况的示意图

  • 当i1 != i时
i1为j最左侧后代,l(i1) = l(i)
i1为j不为最左侧后代,l(i1) != l(i)
i1
i
...
l(i1)=l(i)
...
...
i
i1
l(i)
...
l(i1)
...

​ 因此,若i1在i为最左侧后代,在一棵树内,无需分开处理。若为不为,需要将子树分为两部分处理

  • 当i1=i时,处理方式等同最左侧后代情况

注意

  • 为了简化,算法两部分中均没有描述保存中间计算结果的步骤
  • 这些中间结果实际上是保存下来的,避免迭代中的重复计算。

4. 算法示例

讲了这么多,快找个例子讲讲吧。假设将在2.2节中的T1转换为T2:

Step 1:计算LNI

  1. T1
    l ( 1 ) = l ( 4 ) = l ( 6 ) = 1 l ( 2 ) = l ( 3 ) = 2 l ( 5 ) = 5 l(1)=l(4)=l(6)=1 \\ l(2)=l(3)=2 \\ l(5)=5 l(1)=l(4)=l(6)=1l(2)=l(3)=2l(5)=5

  2. T2
    l ( 1 ) = l ( 3 ) = l ( 4 ) = l ( 6 ) = 1 l ( 2 ) = 2 l ( 5 ) = 5 l(1)=l(3)=l(4)=l(6)=1 \\ l(2)=2 \\ l(5)=5 l(1)=l(3)=l(4)=l(6)=1l(2)=2l(5)=5

Step 2:计算LR_keyroots

  1. 根据定义可得到(按大小排序后)
    L R _ k e y r o o t s ( T 1 ) = ( 3 , 5 , 6 ) L R _ k e y r o o t s ( T 2 ) = ( 2 , 5 , 6 ) LR\_keyroots(T_1) = (3,5,6) \\ LR\_keyroots(T_2) = (2,5,6) LR_keyroots(T1)=(3,5,6)LR_keyroots(T2)=(2,5,6)

Step 3:迭代计算

  1. 依次计算:treelist(3,2),treelist(3,5),treelist(3,6),treelist(5,2),treelist(5,5),treelist(5,6),treelist(6,2),treelist(6,5),treelist(6,6)

    其中treelist(6,6)的结果就是我们所需要的最终结果

  2. 因计算过程过多,这里只展示treelist(3,2)的计算过程和treelist(6,6)的部分计算过程

treelist(3,2)

展开写应该是(这点很重要
t r e e l i s t ( T 1 [ 2 : 3 ] , T 2 [ 2 ] ) treelist(T_1[2:3],T_2[2]) treelist(T1[2:3],T2[2])

  1. 因两棵子树不为空,进入算法calculate_treelist中第4种情况,开始循环

    1. 第1次计算:treelist(T_1[2],T_2[2])
    	符合循环中的第1种情况: l(i1) == l(i) and l(j1) == l(j)
    		treelist(∅,T_2[2]) + 1 = 2
    		treelist(T_1[2],∅) + 1 = 2
    		treelist(∅,∅) + judge_label(T_1[2],T_2[2]) = 0 + 0 = 0
    	treelist(T_1[2],T_2[2]) = min(2,2,0) = 0 
    2. 第2次计算:treelist(T_1[2:3],T_2[2])
    	符合循环中的第2种情况: l(i1) == l(i) and l(j1) == l(j)
    		treelist(T_1[2],T_2[2]) + 1 = 1
    		treelist(T_1[2:3],∅) + 1 = 2 + 1 = 3
    		treelist(T_1[2],∅) + judge_label(T_1[3],T_2[2]) = 1 + 1 = 2
        treelist(T_1[2:3],T_2[2]) = min(1,3,2) = 1 
    

    因此, t r e e l i s t ( T 1 [ 2 : 3 ] , T 2 [ 2 ] ) = 1 treelist(T_1[2:3],T_2[2])=1 treelist(T1[2:3],T2[2])=1

  2. 中间结果的记录

在这里插入图片描述

图为论文中的图,线和框是我添加的,可以如此理解:

  • 两条线交点的右下方是我们的计算结果,其余地方理解为行号或列号
    • 矩阵(1,1)对应的值表示 t r e e l i s t ( T 1 [ 2 ] , T 2 [ 2 ] ) = 0 treelist(T_1[2],T_2[2])=0 treelist(T1[2],T2[2])=0
    • 矩阵(2,1)对应的值表示 t r e e l i s t ( T 1 [ 2 : 3 ] , T 2 [ 2 ] ) = 1 treelist(T_1[2:3],T_2[2])=1 treelist(T1[2:3],T2[2])=1
  • 矩阵右下角的元素使我们需要的计算结果

treelist(6,6)

展开写应该是
t r e e l i s t ( T 1 [ 1 : 6 ] , T 2 [ 1 : 6 ] ) treelist(T_1[1:6],T_2[1:6]) treelist(T1[1:6],T2[1:6])

  1. 中间计算过程过多,我们从第35次循环开始,并假设前34次结果已知

    1. 第35次计算:treelist(T_1[1:6],T_2[1:5])
    	因l(j1)=5与l(j)=6不相同,符合循环中的第2种情况
    		treelist(T_1[1:5],T_2[1:5]) + 1 = 2 + 1 = 3
    		treelist(T_1[1:6],T_2[1:4]) + 1 = 3 + 1 = 4
    		treelist(T_1[1:5],T_2[1:4]) + treelist(T_1[5],T_2[5]) = 3 + 0 = 3
        treelist(T_1[1:6],T_2[1:5]) = min(3,4,3) = 3
        
    2. 第36次计算:treelist(T_1[1:6],T_2[1:6])
    	因为i1=i,j1=j,符合循环中的第1种情况
    		treelist(T_1[1:5],T_2[1:6]) + 1 = 3 + 1 = 4
    		treelist(T_1[1:6],T_2[1:5]) + 1 = 3 + 1 = 4
    		treelist(T_1[1:5],T_2[1:5]) + judge_label(T_1[6],T_2[6]) = 2 + 0 = 2
    	treelist(T_1[1:6],T_2[1:6]) = min(4,4,2) = 2
    

    因此, t r e e l i s t ( T 1 [ 1 : 6 ] , T 2 [ 1 : 6 ] ) = 2 treelist(T_1[1:6],T_2[1:6])=2 treelist(T1[1:6],T2[1:6])=2

  2. 中间结果记录
    在这里插入图片描述

    • 矩阵(6,5)对应的值表示 t r e e l i s t ( T 1 [ 1 : 6 ] , T 2 [ 1 : 5 ] ) = 3 treelist(T_1[1:6],T_2[1:5])=3 treelist(T1[1:6],T2[1:5])=3

    • 矩阵(6,6)对应的值表示 t r e e l i s t ( T 1 [ 1 : 6 ] , T 2 [ 1 : 6 ] ) = 2 treelist(T_1[1:6],T_2[1:6])=2 treelist(T1[1:6],T2[1:6])=2

    • 可以查询之前的中间结果,比如 t r e e l i s t ( T 1 [ 1 : 5 ] , T 2 [ 1 : 4 ] ) treelist(T_1[1:5],T_2[1:4]) treelist(T1[1:5],T2[1:4])对应矩阵(5,4),值为3

    注意:矩阵(5,4)的含义是输入calculate_treelist函数的子树1的节点中的前5个和子树2的的节点中前4个,对应节点的index不一定为1:5和1:4

    比如,在计算 t r e e l i s t ( 3 , 6 ) = t r e e l i s t ( T 1 [ 2 : 3 ] , T 2 [ 1 : 2 ] ) treelist(3,6) = treelist(T_1[2:3],T_2[1:2]) treelist(3,6)=treelist(T1[2:3],T2[1:2])时,对应矩阵(2,2)的含义是 t r e e l i s t ( T 1 [ 2 : 3 ] , T 2 [ 1 : 2 ] ) = 2 treelist(T_1[2:3],T_2[1:2])=2 treelist(T1[2:3],T2[1:2])=2

5. 结语

  1. 本文提出的方法是好方法,30多年过去,还有人在学习它
  2. 但本文的对于关键概念和算法的描述以及插图,一言难尽。因此,这篇博客中补充了大量的个人理解。错误之处,还请大家指出。
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值