Python递归实现最长公共子序列问题
最长公共子序列问题(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 实现最长公共子序列问题的递归和动态规划方式就讲解完了。
相关文章