动态规划之——莱文斯坦距离和最长公共子序列

1. 如何衡量字符串的相似性

如何量化两个字符串之间的相似性呢?我们可以用编辑距离,也就是将一个字符串通过增、删、替换字符转化成另一个字符串需要的最少编辑次数。编辑距离越小,说明两个字符串越相似。

其中,莱文斯坦距离允许增、删和替换操作,表示两个字符串差异的大小;最长公共子序列只允许增删操作,表示两个字符串相似程度的大小。下面的例子中莱文斯坦距离为 3,最长公共子序列为 4。

2. 莱文斯坦距离

对于两个字符串 s 和 t,当 s [ i ] = = t [ j ] s[i] == t[j] s[i]==t[j] 时,我们继续考察 s[i+1] 和 t[j+1];当 s [ i ] ! = t [ j ] s[i] != t[j] s[i]!=t[j] 时,我们有下述选择:

  • 删除 s[i] 或者在 t 中增加一个与 s[i] 相同的字符, 继续考察 s[i+1] 和 t[j];
  • 删除 t[j] 或者在 s 中增加一个与 t[j] 相同的字符, 继续考察 s[i] 和 t[j+1];
  • 替换 s[i] 为 t[j] 或者 t[j] 为 s[i], 继续考察 s[i+1] 和 t[j+1];

我们定义 edist[i][j] 表示子串 s[0, i] 与 t[0, j] 的最小编辑距离,那么由上面的分析可知,edist[i][j] 可以由 edist[i-1][j]、edist[i][j-1] 和 edist[i-1][j-1] 这三个状态转化而来。

当 edist[i][j] 由 edist[i-1][j] 转化而来的时候,我们只能通过删除 s[i] 或者在 t 中增加一个与 s[i] 相同的字符,那么编辑距离一定增 1。
当 edist[i][j] 由 edist[i][j-1] 转化而来的时候,我们只能通过删除 t[j] 或者在 s 中增加一个与 t[j] 相同的字符,那么编辑距离一定增 1。
当 edist[i][j] 由 edist[i-1][j-1] 转化而来的时候,如果 s [ i ] = = t [ j ] s[i] == t[j] s[i]==t[j],编辑距离保持不变;如果 s [ i ] ! = t [ j ] s[i] != t[j] s[i]!=t[j],我们只能替换 s[i] 为 t[j] 或者 t[j] 为 s[i],编辑距增 1。

最终的 edist[i][j] 取这三者中最小的一个即可,所以我们有:

e d i s t [ i ] [ j ] = { m i n ( e d i s t [ i − 1 ] [ j ] + 1 , e d i s t [ i ] [ j − 1 ] + 1 , e d i s t [ i − 1 ] [ j − 1 ] ) 如果  s [ i ] = = t [ j ] m i n ( e d i s t [ i − 1 ] [ j ] + 1 , e d i s t [ i ] [ j − 1 ] + 1 , e d i s t [ i − 1 ] [ j − 1 ] + 1 ) 如果  s [ i ] ! = t [ j ] edist[i][j] = \begin{cases} min(edist[i-1][j]+1, edist[i][j-1]+1, edist[i-1][j-1]) &\text{如果 } s[i] == t[j] \\ min(edist[i-1][j]+1, edist[i][j-1]+1, edist[i-1][j-1]+1) &\text{如果 } s[i] != t[j] \end{cases} edist[i][j]={min(edist[i1][j]+1,edist[i][j1]+1,edist[i1][j1])min(edist[i1][j]+1,edist[i][j1]+1,edist[i1][j1]+1)如果 s[i]==t[j]如果 s[i]!=t[j]

import numpy as np

s = 'mitcmu'
t = 'mtacnu'


def lwst_edit_dis(s, t):

    m = len(s)
    n = len(t)
    edist = np.ones((m, n), 'uint8')
    # 初始化 s[0] 与 t[0, i] 的编辑距离
    for i in range(0, n):
        if s[0] == t[i]:
            edist[0][i] = i
        elif i != 0:
            edist[0][i] = edist[0][i-1] + 1
        else:
            edist[0][i] = 1
    # 初始化 s[0, i] 与 t[0] 的编辑距离
    for i in range(0, m):
        if s[i] == t[0]:
            edist[i][0] = i
        elif i != 0:
            edist[i][0] = edist[i-1][0] + 1
        else:
            edist[i][0] = 1

    for i in range(1, m):
        for j in range(1, n):
            temp = min(edist[i-1][j], edist[i][j-1]) + 1
            if s[i] == t[j]:
                edist[i][j] = min(temp, edist[i-1][j-1])
            else:
                edist[i][j] = min(temp, edist[i-1][j-1]+1)

    print(edist)
    return edist[m-1][n-1]


print(lwst_edit_dis(s, t))

3. 最长公共子序列

最长公共子序列只允许增删两种操作。对于两个字符串 s 和 t,当 s [ i ] = = t [ j ] s[i] == t[j] s[i]==t[j] 时,最长公共子序列长度加 1,我们继续考察 s[i+1] 和 t[j+1];当 s [ i ] ! = t [ j ] s[i] != t[j] s[i]!=t[j] 时,最长公共子序列长度不变,我们有下述选择:

  • 删除 s[i] 或者在 t 中增加一个与 s[i] 相同的字符, 继续考察 s[i+1] 和 t[j];
  • 删除 t[j] 或者在 s 中增加一个与 t[j] 相同的字符, 继续考察 s[i] 和 t[j+1];

我们定义 lcs[i][j] 表示子串 s[0, i] 与 t[0, j] 的最长公共子序列长度,那么由上面的分析可知,lcs[i][j] 可以由 lcs[i-1][j]、lcs[i][j-1] 和 lcs[i-1][j-1] 这三个状态转化而来。

当 lcs[i][j] 由 lcs[i-1][j] 转化而来的时候,我们只能通过删除 s[i] 或者在 t 中增加一个与 s[i] 相同的字符,最长公共子序列长度保持不变。
当 lcs[i][j] 由 lcs[i][j-1] 转化而来的时候,我们只能通过删除 t[j] 或者在 s 中增加一个与 t[j] 相同的字符,最长公共子序列长度保持不变。
当 lcs[i][j] 由 lcs[i-1][j-1] 转化而来的时候,如果 s [ i ] = = t [ j ] s[i] == t[j] s[i]==t[j],最长公共子序列长度加 1;如果 s [ i ] ! = t [ j ] s[i] != t[j] s[i]!=t[j],我们只能替换 s[i] 为 t[j] 或者 t[j] 为 s[i],最长公共子序列长度保持不变。

最终的 lcs[i][j] 取这三者中最大的一个即可,所以我们有:

l c s [ i ] [ j ] = { m a x ( l c s [ i − 1 ] [ j ] , l c s [ i ] [ j − 1 ] , l c s [ i − 1 ] [ j − 1 ] + 1 ) 如果  s [ i ] = = t [ j ] m a x ( l c s [ i − 1 ] [ j ] , l c s [ i ] [ j − 1 ] , l c s [ i − 1 ] [ j − 1 ] ) 如果  s [ i ] ! = t [ j ] lcs[i][j] = \begin{cases} max(lcs[i-1][j], lcs[i][j-1], lcs[i-1][j-1]+1) &\text{如果 } s[i] == t[j] \\ max(lcs[i-1][j], lcs[i][j-1], lcs[i-1][j-1]) &\text{如果 } s[i] != t[j] \end{cases} lcs[i][j]={max(lcs[i1][j],lcs[i][j1],lcs[i1][j1]+1)max(lcs[i1][j],lcs[i][j1],lcs[i1][j1])如果 s[i]==t[j]如果 s[i]!=t[j]

import numpy as np

s = 'mitcmu'
t = 'mtacnu'


def longest_commom_subsequence(s, t):

    m = len(s)
    n = len(t)
    lcs = np.ones((m, n), 'uint8')

    for i in range(0, n):
        if s[0] == t[i]:
            lcs[0][i] = 1
        elif i != 0:
            lcs[0][i] = lcs[0][i-1]
        else:
            lcs[0][i] = 0

    for i in range(0, m):
        if s[i] == t[0]:
            lcs[i][0] = 1
        elif i != 0:
            lcs[i][0] = lcs[i-1][0]
        else:
            lcs[i][0] = 0

    for i in range(1, m):
        for j in range(1, n):
            temp = max(lcs[i-1][j], lcs[i][j-1])
            if s[i] == t[j]:
                lcs[i][j] = max(temp, lcs[i-1][j-1]+1)
            else:
                lcs[i][j] = max(temp, lcs[i-1][j-1])

    print(lcs)
    return lcs[m-1][n-1]


print(longest_commom_subsequence(s, t))

获取更多精彩,请关注「seniusen」!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值