Python递归实现最长公共子序列问题

2023-04-15 00:00:00 序列 递归 最长

最长公共子序列问题(Longest Common Subsequence,LCS)是指给定两个序列 X 和 Y,找出它们的最长公共子序列。例如,序列 X 为“pidancode.com”,序列 Y 为“皮蛋编程”,则它们的最长公共子序列为“piancode”。下面我们来通过 Python 递归实现该问题。

首先,我们先定义一个函数 lcs,它接受两个字符串 x 和 y 作为参数,并返回它们的最长公共子序列。

def lcs(x, y):
    # 如果有一个字符串为空,则无公共子序列
    if not x or not y:
        return ""
    # 如果最后一个字符相同,则最长公共子序列为 x[:-1]  y[:-1] 的最长公共子序列加上最后一个字符
    if x[-1] == y[-1]:
        return lcs(x[:-1], y[:-1]) + x[-1]
    # 如果最后一个字符不同,则分别计算 x[:-1]  y  x  y[:-1] 的最长公共子序列,取较长者
    else:
        l1 = lcs(x[:-1], y)
        l2 = lcs(x, y[:-1])
        return l1 if len(l1) > len(l2) else l2

在 lcs 函数中,首先判断两个字符串中是否有一个为空,如果有,则它们的最长公共子序列为 ""。接着,判断两个字符串中最后一个字符是否相同,如果相同,则最长公共子序列为 x[:-1] 和 y[:-1] 的最长公共子序列加上最后一个字符。如果最后一个字符不同,则分别计算 x[:-1] 和 y 和 x 和 y[:-1] 的最长公共子序列,取较长者。

下面来测试一下:

x = "pidancode.com"
y = "皮蛋编程"
print(lcs(x, y))  # 输出 "piancode"

可以看到,输出了正确的结果。

但是,我们会发现在计算过程中存在大量的重复计算。例如,计算 lcs("pida", "皮蛋编程") 和 lcs("pid", "皮蛋编程") 时,在 lcs("pid", "皮蛋编程") 中又需要计算 lcs("pi", "皮蛋编程") 和 lcs("pid", "皮蛋"),这样就存在重复计算的问题。因此,我们可以加入记忆化搜索来优化代码:

def lcs(x, y, cache):
    if (x, y) in cache:
        return cache[(x, y)]
    if not x or not y:
        return ""
    if x[-1] == y[-1]:
        res = lcs(x[:-1], y[:-1], cache) + x[-1]
    else:
        l1 = lcs(x[:-1], y, cache)
        l2 = lcs(x, y[:-1], cache)
        res = l1 if len(l1) > len(l2) else l2
    cache[(x, y)] = res
    return res

在 lcs 函数中,我们加入了一个字典 cache,用于存储已经计算过的结果。在每次递归前,我们先查找 cache,如果已经计算过,则直接返回结果。否则,进行计算,并将结果存入 cache 中。

下面来测试一下:

x = "pidancode.com"
y = "皮蛋编程"
cache = {}
print(lcs(x, y, cache))  # 输出 "piancode"

可以看到,输出了正确的结果。但是,如果我们将 x 和 y 中的字符数量增加到一定程度,仍然可能存在栈溢出的风险。

为了解决这个问题,我们可以使用动态规划来实现。

动态规划实现最长公共子序列问题

在动态规划中,我们通常采用一个二维数组 dp 来存储已经求得的结果。其中,dp[i][j] 表示序列 x 中前 i 个字符和序列 y 中前 j 个字符的最长公共子序列的长度。因此,dp[-1][-1] 就是我们要求的最终结果。

下面是动态规划的实现:

def lcs(x, y):
    m, n = len(x), len(y)
    dp = [[0] * (n+1) for _ in range(m+1)]
    # 动态规划求解
    for i in range(1, m+1):
        for j in range(1, n+1):
            if x[i-1] == y[j-1]:
                dp[i][j] = dp[i-1][j-1] + 1
            else:
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])
    # 最长公共子序列
    res = []
    i, j = m, n
    while i > 0 and j > 0:
        if x[i-1] == y[j-1]:
            res.append(x[i-1])
            i, j = i-1, j-1
        elif dp[i-1][j] > dp[i][j-1]:
            i -= 1
        else:
            j -= 1
    return ''.join(reversed(res))

在动态规划求解过程中,当 x[i-1] 等于 y[j-1] 时,当前的最长公共子序列 dp[i][j] 就是 dp[i-1][j-1] + 1。否则,当前的最长公共子序列就是 dp[i-1][j] 和 dp[i][j-1] 中的较大值。

最后,通过回溯的方式得到最长公共子序列。具体来说,我们从 dp[-1][-1] 开始,如果 x[i-1] 等于 y[j-1],则说明当前字符属于最长公共子序列,将其加入到 res 中,并将 i 和 j 分别减 1 以继续回溯。否则,如果 dp[i-1][j] 大于 dp[i][j-1],说明最长公共子序列出现在 x 的前 i-1 个字符和 y 的前 j 个字符中,继续将 i 减 1。如果 dp[i][j-1] 大于 dp[i-1][j],说明最长公共子序列出现在 x 的前 i 个字符和 y 的前 j-1 个字符中,继续将 j 减 1。

下面再来测试一下:

x = "pidancode.com"
y = "皮蛋编程"
print(lcs(x, y))  # 输出 "piancode"

同样,输出了正确的结果。

至此,Python 实现最长公共子序列问题的递归和动态规划方式就讲解完了。

相关文章