《最长公共子序列》基础概念

一、算法概述

  1. 问题定义:最长公共子序列(Longest Common Subsequence,简称LCS)问题是指,给定两个序列,找到它们最长的公共子序列的长度。子序列不要求元素在原序列中连续,但需保持相对顺序。例如,序列 [1, 3, 4, 5, 6, 7, 7, 8][3, 5, 7, 4, 8, 6, 7, 8, 2] 的最长公共子序列是 [3, 5, 7, 8],长度为 4。
  2. 应用场景:该算法广泛应用于文本比较(如 diff 工具)、生物信息学(DNA 序列比对)、语音识别等领域,用于衡量两个序列的相似程度。

二、算法思路

  1. 动态规划思想:通过将原问题分解为多个子问题,利用子问题的解构建最终答案,避免重复计算。
  2. 状态定义:定义二维数组 dp[i][j] 表示序列 a 的前 i 个元素和序列 b 的前 j 个元素的最长公共子序列长度。
  3. 状态转移方程
    • a[i] 等于 b[j],则 dp[i][j] = dp[i-1][j-1] + 1,即公共子序列长度增加 1。
    • a[i] 不等于 b[j],则 dp[i][j] = max(dp[i-1][j], dp[i][j-1]),取去掉 a[i] 或去掉 b[j] 后的子问题的最优解。
  4. 路径记录:使用二维数组 path[i][j] 记录状态转移的方向,方便后续回溯得到具体的最长公共子序列。其中,path[i][j] = 0 表示 a[i] == b[j] 时的转移,path[i][j] = 1 表示从 dp[i-1][j] 转移,path[i][j] = 2 表示从 dp[i][j-1] 转移。

三、伪代码实现

算法:最长公共子序列
输入:序列 a[1..n],序列 b[1..m]
输出:a 和 b 的最长公共子序列长度

function getLCS(n, a[], m, b[]):
    创建二维数组 dp[0..n][0..m] 用于存储最长公共子序列长度
    创建二维数组 path[0..n][0..m] 用于记录状态转移路径

    // 初始化边界条件:空序列与任何序列的 LCS 长度为 0
    for i from 0 to n:
        dp[i][0] = 0
    for j from 0 to m:
        dp[0][j] = 0

    for i from 1 to n:
        for j from 1 to m:
            if a[i] == b[j]:
                dp[i][j] = dp[i-1][j-1] + 1
                path[i][j] = 0
            else if dp[i-1][j] > dp[i][j-1]:
                dp[i][j] = dp[i-1][j]
                path[i][j] = 1
            else:
                dp[i][j] = dp[i][j-1]
                path[i][j] = 2

    return dp[n][m]

// 主程序
输入 n, m
输入序列 a[1..n]
输入序列 b[1..m]
输出 getLCS(n, a, m, b)

四、算法解释

  1. 初始化阶段
    • dp[i][0]dp[0][j] 初始化为 0,表示空序列与任何序列的最长公共子序列长度为 0。
  2. 状态转移过程
    • 双重循环遍历序列 ab 的每个元素。
    • a[i] == b[j],说明找到了一个公共元素,dp[i][j]dp[i-1][j-1] 转移而来,并将 path[i][j] 标记为 0。
    • a[i] != b[j],比较 dp[i-1][j]dp[i][j-1],取较大值作为 dp[i][j],并根据取值情况标记 path[i][j] 为 1 或 2。
  3. 回溯获取子序列(未在伪代码中体现,但为完整逻辑):
    • dp[n][m] 开始,根据 path 数组的记录回溯,若 path[i][j] == 0,则 a[i](或 b[j])是公共子序列的元素,向左上方移动;若 path[i][j] == 1,向上移动;若 path[i][j] == 2,向左移动。
  4. 示例演示
    • a = [1, 3, 4, 5]b = [3, 5, 4]
    • 初始化后,dp[0][j]dp[i][0] 均为 0。
    • 遍历过程中,当 i = 2, j = 1 时,a[2] = 3 等于 b[1] = 3,则 dp[2][1] = dp[1][0] + 1 = 1path[2][1] = 0;当 i = 2, j = 2 时,a[2] != b[2]dp[2][1] = 1 大于 dp[1][2] = 0,则 dp[2][2] = dp[2][1] = 1path[2][2] = 1。最终计算得到 dp[4][3] 即为最长公共子序列长度。

五、复杂度分析

  1. 时间复杂度:算法使用两层嵌套循环遍历两个序列,因此时间复杂度为 O ( n × m ) O(n \times m) O(n×m),其中 nm 分别是两个序列的长度。
  2. 空间复杂度:使用了两个二维数组 dppath,空间复杂度为 O ( n × m ) O(n \times m) O(n×m)。在某些情况下,由于计算 dp[i][j] 只依赖于 dp[i-1][j]dp[i][j-1]dp[i-1][j-1],可以将空间优化至 O ( m i n ( n , m ) ) O(min(n, m)) O(min(n,m))
  3. 与暴力解法对比:暴力解法需枚举两个序列的所有子序列并比较,时间复杂度为指数级,而动态规划算法通过子问题复用大幅降低了时间复杂度。

  本文为作者(英雄哪里出来)在抖音的独家课程《英雄C++入门到精通》、《英雄C语言入门到精通》、《英雄Python入门到精通》三个课程的配套文字讲解,如需了解算法视频课程,请移步 作者本人 的抖音直播间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

英雄哪里出来

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值