一 前言
离散序列通常是字符串序列,一个字符表示一种标签或者等级。这与定量型数据的相似性度量不同,定量型数据可以采用距离函数来度量相似度,而离散序列一般不具有数值计算的特性。故而,我们对离散序列的相似度量通常采用字符串比较的方法,本文讨论的是编辑距离和最长公共子序列。
二 编辑距离
编辑距离是将一个字符串转化为另一个字符串所使用一系列插入、删除和替换操作所需要的最小代价。 例如,将 ababab 转化成 bababa 最少需要两次:第一次删除第一个a,第二次在尾端插入一个a。假设删除和插入的成本都是1,那么 ababab 和 bababa 的编辑距离就是2。显然,编辑距离越小说明两个字符串越相似。
给定两个任意的字符序列X和Y,如何计算两者之间的编辑距离?显然,这是个动态规划的问题,需要找出它的递归模型。
设 E d i t ( i , j ) Edit(i,j) Edit(i,j)代表片段 X i X_i Xi和 Y j Y_j Yj的最小匹配代价。那么有4种可能:
- X [ i ] ! = Y [ j ] X[i]!=Y[j] X[i]!=Y[j]时, X i X_i Xi插入一个字符:插入一个字符到 X i X_i Xi的末尾,使得它和 Y j Y_j Yj的最后一个字符相等。此时匹配了一个 Y j Y_j Yj的字符, j j j指针向前移动, i i i不动。 E d i t ( i , j ) = E d i t ( i , j − 1 ) + 插 入 成 本 Edit(i,j)=Edit(i,j-1)+插入成本 Edit(i,j)=Edit(i,j−1)+插入成本。
- X [ i ] ! = Y [ j ] X[i]!=Y[j] X[i]!=Y[j]时, X i X_i Xi删除一个字符:删除 X i X_i Xi末尾的字符。此时 X [ i − 1 ] X[i-1] X[i−1]和 Y [ j ] Y[j] Y[j] 可能相等也可能不等,j不动,i指针向前移动。 E d i t ( i , j ) = E d i t ( i − 1 , j ) + 删 除 成 本 Edit(i,j)=Edit(i-1,j)+删除成本 Edit(i,j)=Edit(i−1,j)+删除成本。
- X [ i ] ! = Y [ j ] X[i]!=Y[j] X[i]!=Y[j]时, X i X_i Xi替换一个字符:用 Y j Y_j Yj的末尾字符替换掉 X i X_i Xi的末尾字符。此时 X i X_i Xi和 Y j Y_j Yj的末尾匹配, i i i和 j j j均向前移动。 E d i t ( i , j ) = E d i t ( i − 1 , j − 1 ) + 替 换 成 本 Edit(i,j)=Edit(i-1,j-1)+替换成本 Edit(i,j)=Edit(i−1,j−1)+替换成本。
- X [ i ] = = Y [ j ] X[i]==Y[j] X[i]==Y[j]时,什么都不做, i i i和 j j j均向前移动。 E d i t ( i , j ) = E d i t ( i − 1 , j − 1 ) Edit(i,j)=Edit(i-1,j-1) Edit(i,j)=Edit(i−1,j−1)。
综上所述,求解编辑距离的递归模型如下:
设 I i j 为 指 标 变 量 , X [ i ] = = Y [ j ] 时 I i j = 0 , 否 则 I i j = 1 \begin{aligned} 设I_{ij}为指标变量,X[i]==Y[j]时I_{ij}=0,否则I_{ij}=1 \end{aligned} 设Iij为指标变量,X[i]==Y[j]时Iij=0,否则Iij=1
E d i t ( i , j ) = m i n { E d i t ( i , j − 1 ) + 插 入 成 本 E d i t ( i − 1 , j ) + 删 除 成 本 E d i t ( i − 1 , j − 1 ) + I i j ∗ 替 换 成 本 \begin{aligned} Edit(i,j)=min\begin{cases}Edit(i,j-1)+插入成本 & \\Edit(i-1,j)+删除成本&\\ Edit(i-1,j-1)+I_{ij}*替换成本\end{cases} \end{aligned} Edit(i,j)=min⎩⎪⎨⎪⎧Edit(i,j−1)+插入成本Edit(i−1,j)+删除成本Edit(i−1,j−1)+Iij∗替换成本
递归模型的回溯情况:
- i = = − 1 i==-1 i==−1(X遍历完了),此时Y可能遍历完也可能没有( j > = − 1 j>=-1 j>=−1),但是剩下的Y的字符都是要插入到X的末尾,故 E d i t ( i , j ) = ( j + 1 ) ∗ 插 入 代 价 Edit(i,j)=(j+1)*插入代价 Edit(i,j)=(j+1)∗插入代价。
- j = = − 1 j==-1 j==−1(Y遍历完了),此时X可能遍历完也可能没有( i > = − 1 i>=-1 i>=−1),但是剩下的X的字符都要删除,故 E d i t ( i , j ) = ( i + 1 ) ∗ 删 除 代 价 Edit(i,j)=(i+1)*删除代价 Edit(i,j)=(i+1)∗删除代价。
重叠计算问题: 显然递归树中存在大量的重复计算的结点,引入memo备忘录来记录计算过的值,如果计算过就直接返回结果,不再递归计算下去。
代码实现:(假设插入、删除和替换的代价都是1)
#编辑距离
def editDistance(x,y):
#备忘录,用于消除重复计算
memo=dict()
#动态规划函数,i,j是x,y对应的下标
def edit(i,j):
#已经计算过了,不再重复计算
if (i,j) in memo:
return memo[(i,j)]
I = 1#指标因子
#回溯条件
#如果x遍历完了,y剩下的插入
if i==-1: return j+1
#如果y遍历完了,x剩下的删除
if j==-1: return i+1
#如果字符相等,什么都不做,指标I置0
if x[i]==y[j]:I=0
#状态转移
memo[(i,j)] =min(
edit(i,j-1)+1,#插入
edit(i-1,j)+1,#删除
edit(i-1,j-1)+I*1,#替换或者什么都不做
)
return memo[(i,j)]
return edit(len(x)-1,len(y)-1)
三 最长公共子序列
子序列是指按照原始序列的顺序依次截取字符所组成的字符串。 子序列和子串是不同的概念,因为子串一定是连续的,子序列不一定是连续的。例如,agbfcgdhei 和 afbgchdiei两个字符串,ei 是共同的字串(一定是连续的),而abcde和fghi是共同的子序列(不一定连续)。我们可以认为,最长公共子序列LCSS是一个相似度函数,因为它可以度量出两个字符串最多有多少个排列相同的字符。
给定两个任意的字符串X和Y,如何求LCSS(X,Y)?显然,这也是个动态规划的问题,我们找到其递归模型。
令 d p ( i , j ) dp(i,j) dp(i,j)表示子串 X [ 0 : i ] X[0:i] X[0:i]和子串 Y [ 0 : j ] Y[0:j] Y[0:j]最长的子序列长度。那么有三种可能:
- X [ i ] = = Y [ j ] X[i]==Y[j] X[i]==Y[j],末尾字符是相同的:那么 d p ( i , j ) dp(i,j) dp(i,j)为子串 X [ 0 : i − 1 ] X[0:i-1] X[0:i−1]和子串 Y [ 0 : j − 1 ] Y[0:j-1] Y[0:j−1]最长的子序列长度+1,即 d p ( i , j ) = d p ( i − 1 , j − 1 ) + 1 dp(i,j)=dp(i-1,j-1)+1 dp(i,j)=dp(i−1,j−1)+1
- X [ i ] ! = Y [ j ] X[i]!=Y[j] X[i]!=Y[j],尝试缩小 X i X_i Xi序列继续匹配: d p ( i , j ) = d p ( i − 1 , j ) dp(i,j)=dp(i-1,j) dp(i,j)=dp(i−1,j)
- X [ i ] ! = Y [ j ] X[i]!=Y[j] X[i]!=Y[j],尝试缩小 Y j Y_j Yj序列继续匹配: d p ( i , j ) = d p ( i , j − 1 ) dp(i,j)=dp(i,j-1) dp(i,j)=dp(i,j−1)
综上所述,递归模型为:
d p ( i , j ) = m a x { d p ( i − 1 , j − 1 ) + 1 X[i]==Y[j] d p ( i − 1 , j ) X[i]!=Y[j] d p ( i , j − 1 ) X[i]!=Y[j] \begin{aligned} dp(i,j)=max\begin{cases}dp(i-1,j-1)+1&\text{X[i]==Y[j]} \\dp(i-1,j)&\text{X[i]!=Y[j]}\\ dp(i,j-1)&\text{X[i]!=Y[j]}\end{cases} \end{aligned} dp(i,j)=max⎩⎪⎨⎪⎧dp(i−1,j−1)+1dp(i−1,j)dp(i,j−1)X[i]==Y[j]X[i]!=Y[j]X[i]!=Y[j]
递归回溯的情况:
- i = = − 1 ∣ ∣ j = = − 1 i == -1||j==-1 i==−1∣∣j==−1,此时 X i X_i Xi或 Y j Y_j Yj是空串,那么肯定不会有公共子序列, d p ( i , j ) = 0 dp(i,j)=0 dp(i,j)=0
重叠计算问题: d p ( i − 1 , j − 1 ) dp(i-1,j-1) dp(i−1,j−1)可以由先计算 d p ( i − 1 , j ) dp(i-1,j) dp(i−1,j)再计算 d p ( i , j − 1 ) dp(i,j-1) dp(i,j−1)得,也可以颠倒顺序得。这样就会有两条路径计算出同一个结果,那么递归树中会出现重复结点。消除重叠计算同样是引入memo记录计算过的值。
代码实现:
#最长公共子序列
def longestCommonSubsequence(x, y):
#消除重复计算
memo = dict()
def dp(i, j):
#计算过的不再计算
if (i, j) in memo:
return memo[(i, j)]
#回溯条件
if i == -1 or j == -1:
return 0
#状态转移
if x[i] == y[j]:
memo[(i, j)] = dp(i - 1, j - 1) + 1
else:
memo[(i, j)] = max(dp(i - 1, j), dp(i, j - 1))
return memo[(i, j)]
return dp(len(x) - 1, len(y) - 1)