当前位置: 首页 > news >正文

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

1. 理论原理推导

假设有两个序列:

  • 序列 X = x_1, x_2, \dots, x_m

  • 序列 Y = y_1, y_2, \dots, y_n

子问题定义:
dp[i][j] 表示序列 X[1 \dots i] 和 Y[1 \dots j] 的最长公共子序列的长度。

递推公式:
对于任意 1 \le i \le m 和 1 \le j \le n,我们考虑两个位置上的元素 x_i​ 和 y_j​:

  1. 如果 x_i = y_j,说明这两个元素可以同时出现在公共子序列中,此时:

    dp[i][j] = dp[i-1][j-1] + 1
  2. 如果 x_i \neq y_j​,那么最长公共子序列不可能同时包含这两个元素,只能在两种可能中取最大值:

    dp[i][j] = \max\{ dp[i-1][j],\; dp[i][j-1] \}

边界条件:

  • 当 i=0 或 j=0 时,意味着其中一个序列为空,此时 dp[i][j]=0

这一推导基于最优子结构性质:最长公共子序列问题的最优解可以由其子问题的最优解构造得到。


2. 时间复杂度推导

  • 状态个数:
    动态规划表 dp 的大小为 (m+1)×(n+1) 。

  • 状态转移:
    每个状态dp[i][j]的计算只涉及常数时间的比较和加法操作。

因此,总的时间复杂度为:O(m×n)

空间复杂度也为 O(m×n) ;若通过空间优化(例如仅保留当前行和上一行),可降为 O(\min(m, n))


3. 算法步骤

下面是基于上述理论推导的动态规划求解 LCS 的具体步骤:

  1. 初始化:

    • 构建一个大小为 (m+1)×(n+1)的二维数组 dp,所有元素初始化为 0。

    • 行和列的索引从 0 开始,令dp[0][j]=0(对于所有 j)和 dp[i][0]=0(对于所有 i)。

  2. 状态转移:

    • 对于 i 从 1 到 m:

      • 对于 j 从 1 到 n:

        • x_i = y_j​,则令:

          dp[i][j] = dp[i-1][j-1] + 1
        • 否则:

          dp[i][j] = \max\{ dp[i-1][j],\; dp[i][j-1] \}
  3. 结果输出:

    • 最终,dp[m][n] 即为序列 X 和 Y 的最长公共子序列的长度。

  4. (可选)路径回溯:

    • 若需要重构出一个最长公共子序列,可以从dp[m][n]开始,逆向回溯:

      • x_i = y_j​ 时,该元素属于 LCS,记录下来,并同时减小 i 和 j;

      • dp[i-1][j] \ge dp[i][j-1] 时,移动到(i-1, j);否则移动到(i, j-1)


4. Python 代码示例

下面提供一个 Python 代码示例,既计算 LCS 的长度,也重构出一个 LCS:

def lcs_length(X, Y):
    m, n = len(X), len(Y)
    # 创建 (m+1) x (n+1) 的二维 dp 数组,并初始化为 0
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    
    # 填充 dp 表
    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])
    
    return dp

def construct_lcs(dp, X, Y):
    """
    根据 dp 表从 X 和 Y 中回溯构造最长公共子序列
    """
    i, j = len(X), len(Y)
    lcs = []
    
    while i > 0 and j > 0:
        # 如果两个元素相等,加入 LCS
        if X[i - 1] == Y[j - 1]:
            lcs.append(X[i - 1])
            i -= 1
            j -= 1
        else:
            # 否则,沿着较大值的方向回溯
            if dp[i - 1][j] >= dp[i][j - 1]:
                i -= 1
            else:
                j -= 1
    
    lcs.reverse()  # 反转得到正确顺序
    return lcs

def main():
    # 示例序列,可以根据需要替换成任意两个序列
    X = "ABCBDAB"
    Y = "BDCAB"
    
    # 计算 dp 表
    dp = lcs_length(X, Y)
    print("最长公共子序列的长度为:", dp[len(X)][len(Y)])
    
    # 重构最长公共子序列
    lcs_seq = construct_lcs(dp, X, Y)
    print("最长公共子序列为:", ''.join(lcs_seq))

if __name__ == '__main__':
    main()

相关文章:

  • 【Easylive】convertVideo2Ts 和 union 方法解析
  • UML统一建模语言
  • Mysql之事务(下)
  • 七. JAVA类和对象(二)
  • FreeCAD傻瓜教程-装配体Assembly的详细使用过程
  • java基础知识面试题总结
  • Android学习总结之算法篇三(排序)
  • git kex_exchange_identification 相关问题
  • C/C++ JSON 库综合对比及应用案例(六)
  • Redis 与 AI:从缓存到智能搜索的融合之路
  • UI设计系统:如何构建一套高效的设计规范?
  • S32K144入门笔记(二十三):FlexCAN解读(1)
  • SAP CO88根据标准价格拆分增量错误解决
  • SpringBoot实战:Excel文件上传、数据验证与存储全流程解析
  • EasyExcel导出导入excel工具类
  • 【创新项目实训个人博客】camel学习笔记(1)camel介绍
  • 【教程】如何利用bbbrisk一步一步实现评分卡
  • 设计模式(2)
  • 前端资源缓存策略全面解析:从原理到实践
  • 【Ultralytics YOLO COCO 评估脚本 | 获得COCO评价指标】
  • 人民日报任平:从“地瓜经济”理论到民营经济促进法,读懂中国经济的成长壮大之道
  • 红星控股重整期间实控人被留置后续:重整草案不会修改,涉车建兴职责已调整
  • 曾毓群说未来三年重卡新能源渗透率将突破50%,宁德时代如何打好换电这张牌
  • 梅花奖在上海|秦海璐:演了15年《四世同堂》,想演一辈子
  • 北斗系统全面进入11个国际组织的标准体系
  • 2025年上海科技节开幕,人形机器人首次登上科学红毯