练24:动态规划4——最长公共子序列(LCS)

欢迎大家订阅【蓝桥杯Python每日一练】 专栏,开启你的 Python数据结构与算法 学习之旅!


1 定义

最长公共子序列(Longest Common Subsequence, LCS) 是指两个序列中最长的、顺序一致但不一定连续的公共子序列。
例如:

  • 序列 a = [1, 2, 3, 4, 5]b = [2, 3, 2, 1, 4, 5] 的 LCS 是 [2, 3, 4, 5](长度为 4)。
  • 注意:子序列不要求连续(与子数组不同),但元素相对顺序必须保持一致。

2 动态规划解法

①核心思路
通过构建一个二维数组 dp[i][j],表示序列 a 的前 i 个元素和序列 b 的前 j 个元素的最长公共子序列长度。

举个超简单的例子
假设 a = [1, 2]b = [2, 3]

  • dp[2][1] 表示 a 的前2个元素 [1,2]b 的前1个元素 [2] 的 LCS 长度,答案是 1(公共子序列是 [2])。
  • dp[1][2] 表示 a 的前1个元素 [1]b 的前2个元素 [2,3] 的 LCS 长度,答案是 0(没有公共元素)。

②状态转移方程
情况1:当前元素相等(a[i] == b[j]
核心逻辑:既然当前两个元素相等,那它们一定可以成为公共子序列的一部分!
比如,a[i] = 2b[j] = 2,那么 LCS 长度就是 去掉这两个元素后 的 LCS 长度加 1。
公式dp[i][j] = dp[i-1][j-1] + 1(左上角的值加 1)。
例子
a = [1, 2]b = [2, 3]
i=2a[2]=2),j=1b[1]=2)时,a[i] == b[j],所以:
dp[2][1] = dp[1][0] + 1 = 0 + 1 = 1(因为 a 的前1个元素和 b 的前0个元素的 LCS 是 0)。

情况2:当前元素不相等(a[i] != b[j]
核心逻辑:既然这两个元素不相等,那至少要忽略其中一个元素(因为子序列可以跳过元素)。

  • 忽略 a[i]:求 a 的前 i-1 个元素和 b 的前 j 个元素的 LCS(即 dp[i-1][j])。
  • 忽略 b[j]:求 a 的前 i 个元素和 b 的前 j-1 个元素的 LCS(即 dp[i][j-1])。
  • 取两者的 最大值,因为我们要最长的公共子序列。

公式dp[i][j] = max(dp[i-1][j], dp[i][j-1])
例子
a = [1, 3]b = [2, 3]

  • i=1a[1]=1),j=1b[1]=2)时,a[i] != b[j],所以:
    dp[1][1] = max(dp[0][1], dp[1][0]) = max(0, 0) = 0(因为两边都是空序列的情况)。
  • i=2a[2]=3),j=2b[2]=3)时,a[i] == b[j],所以:
    dp[2][2] = dp[1][1] + 1 = 0 + 1 = 1(公共子序列是 [3])。

③初始化

  • dp[0][j] = 0(当 a 为空时,LCS 长度为 0)。
  • dp[i][0] = 0(当 b 为空时,LCS 长度为 0)。

a 为空(i=0
不管 b 有多长,公共子序列长度都是 0,所以 dp[0][j] = 0(整行都是 0)。
比如:a 是空序列,b[1,2,3],LCS 长度必然是 0。

b 为空(j=0
不管 a 有多长,公共子序列长度都是 0,所以 dp[i][0] = 0(整列都是 0)。
比如:b 是空序列,a[1,2,3],LCS 长度必然是 0。

【用一个超小的例子,把所有逻辑串起来】
例子
a = [1, 2]b = [2, 1](求 LCS 长度)。

步骤1:初始化 DP 表

01(b=2)2(b=1)
0(a=空)000
1(a=1)0??
2(a=2)0??

步骤2:填充第一行(i=1,a[1]=1)

  • j=1(b[1]=2):1≠2,取 max(dp[0][1]=0, dp[1][0]=0) = 0
  • j=2(b[2]=1):1==1,所以 dp[1][2] = dp[0][1] + 1 = 0 + 1 = 1
    填充后:
012
0000
1001

步骤3:填充第二行(i=2,a[2]=2)

  • j=1(b[1]=2):2==2,所以 dp[2][1] = dp[1][0] + 1 = 0 + 1 = 1
  • j=2(b[2]=1):2≠1,取 max(dp[1][2]=1, dp[2][1]=1) = 1
    填充后:
012
0000
1001
2011

综上所述:dp[2][2] = 1,即 LCS 长度为 1。实际公共子序列是 [1][2],符合预期!

为什么这样设计?用“排除法”理解
假设我们在找 a[1..i]b[1..j] 的 LCS,最后一个元素有两种可能:

  1. 最后一个元素相等(a[i] == b[j]:必须包含这个元素,所以 LCS 长度等于去掉这两个元素后的 LCS 长度加 1。
  2. 最后一个元素不等(a[i] != b[j]:LCS 要么不包含 a[i],要么不包含 b[j],取两种情况的最大值。

这就像玩拼图:每次遇到相同的元素,就把它“拼上”;遇到不同的,就试试哪种“忽略方式”能让拼图更长。

【核心逻辑】

  • 相等时:当前元素可以加入公共子序列,长度是“去掉这两个元素后的 LCS 长度 + 1”。
  • 不等时:只能忽略其中一个元素,选两种忽略方式中 LCS 更长的那个。
  • 初始化:只要有一个序列为空,LCS 长度就是 0,这是递推的起点。

3 例题分析

在这里插入图片描述
在这里插入图片描述
【示例代码】

n,m=map(int,input().split())
a=[0]+list(map(int,input().split()))
b=[0]+list(map(int,input().split()))
dp=[[0]*(m+1) for i in range(n+1)]

for i in range(1,n+1):
    for j in range(1,m+1):
        if a[i]==b[j]:
            dp[i][j]=dp[i-1][j-1]+1
        else:
            dp[i][j]=max(dp[i-1][j],dp[i][j-1])
print(dp[n][m])

# 输出最长公共子序列
ans=[]
x,y=n,m
while x!=0 and y!=0:
    if dp[x][y]==dp[x-1][y]:
        x=x-1
    elif dp[x][y]==dp[x][y-1]:
        y=y-1
    else:
        ans.append(a[x])
        x,y=x-1,y-1
print(ans[::-1])

【执行流程】

①初始化DP表
dp 是一个 6×7 的二维数组,初始全为0:

   0 1 2 3 4 5 6  (b的索引,值依次为0,2,3,2,1,4,5)
0 [0,0,0,0,0,0,0]
1 [0,0,0,0,0,0,0]  (a[1]=1)
2 [0,0,0,0,0,0,0]  (a[2]=2)
3 [0,0,0,0,0,0,0]  (a[3]=3)
4 [0,0,0,0,0,0,0]  (a[4]=4)
5 [0,0,0,0,0,0,0]  (a[5]=5)

②填充DP表(逐行逐列计算)
第1行(i=1,a[1]=1)
遍历b的每个元素(j=1到6,值为2,3,2,1,4,5):

  • 当j=4(b[4]=1)时,a[1]=b[4],所以 dp[1][4] = dp[0][3] + 1 = 0 + 1 = 1
  • 其他位置:元素不等,取上方(0)或左方(0)的最大值,仍为0。

第1行结果

[0,0,0,0,1,1,1]

第2行(i=2,a[2]=2)

  • j=1(b[1]=2):a[2]=b[1],dp[2][1] = dp[1][0] + 1 = 0 + 1 = 1
  • j=2(b[2]=3):2≠3,取 max(dp[1][2]=0, dp[2][1]=1) = 1
  • j=3(b[3]=2):a[2]=b[3],dp[2][3] = dp[1][2] + 1 = 0 + 1 = 1
  • j=4(b[4]=1):2≠1,取 max(dp[1][4]=1, dp[2][3]=1) = 1
  • j=5、6:元素不等,值仍为1。

第2行关键结果

[0,1,1,1,1,1,1]

第3行(i=3,a[3]=3):

  • j=2(b[2]=3):a[3]=b[2],dp[3][2] = dp[2][1] + 1 = 1 + 1 = 2
  • j=1(b[1]=2):3≠2,取 max(dp[2][1]=1, dp[3][0]=0) = 1
  • j=3(b[3]=2):3≠2,取 max(dp[2][3]=1, dp[3][2]=2) = 2
  • j=6(b[6]=5):3≠5,取 max(dp[2][6]=1, dp[3][5]=2) = 2

第3行关键结果

[0,1,2,2,2,2,2]

第4行(i=4,a[4]=4):

  • j=5(b[5]=4):a[4]=b[5],dp[4][5] = dp[3][4] + 1 = 2 + 1 = 3
  • j=6(b[6]=5):4≠5,取 max(dp[3][6]=2, dp[4][5]=3) = 3

第4行关键结果

[0,1,2,2,2,3,3]

第5行(i=5,a[5]=5)
j=6(b[6]=5):a[5]=b[6],dp[5][6] = dp[4][5] + 1 = 3 + 1 = 4

最终DP表右下角值dp[5][6] = 4,即LCS长度为4。

③回溯过程(找具体子序列)
x=5, y=6 开始(对应a[5]=5,b[6]=5):

  1. a[5] == b[6]:将5加入ans(此时ans=[5]),向左上移动到x=4, y=5(a[4]=4,b[5]=4)。
  2. a[4] == b[5]:将4加入ansans=[5,4]),向左上移动到x=3, y=4(a[3]=3,b[4]=1)。
  3. a[3] != b[4]:比较 dp[3][4]=2dp[2][4]=1(上方)、dp[3][3]=2(左方)。
    • 左方值等于当前值,说明忽略b[4],向左移动到y=3(b[3]=2)。
  4. a[3]=3 vs b[3]=2:不等,比较 dp[3][3]=2dp[2][3]=1(上方)、dp[3][2]=2(左方)。
    • 左方值等于当前值,向左移动到y=2(b[2]=3)。
  5. a[3] == b[2]:将3加入ansans=[5,4,3]),向左上移动到x=2, y=1(a[2]=2,b[1]=2)。
  6. a[2] == b[1]:将2加入ansans=[5,4,3,2]),向左上移动到x=1, y=0,循环结束。
  7. 反转ans:得到正确顺序 [2,3,4,5]

【结果】

4
[2, 3, 4, 5]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值