欢迎大家订阅【蓝桥杯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] = 2
,b[j] = 2
,那么 LCS 长度就是 去掉这两个元素后 的 LCS 长度加 1。
公式:dp[i][j] = dp[i-1][j-1] + 1
(左上角的值加 1)。
例子:
a = [1, 2]
,b = [2, 3]
:
当 i=2
(a[2]=2
),j=1
(b[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=1
(a[1]=1
),j=1
(b[1]=2
)时,a[i] != b[j]
,所以:
dp[1][1] = max(dp[0][1], dp[1][0]) = max(0, 0) = 0
(因为两边都是空序列的情况)。 - 当
i=2
(a[2]=3
),j=2
(b[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 表
0 | 1(b=2) | 2(b=1) | |
---|---|---|---|
0(a=空) | 0 | 0 | 0 |
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
。
填充后:
0 | 1 | 2 | |
---|---|---|---|
0 | 0 | 0 | 0 |
1 | 0 | 0 | 1 |
步骤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
。
填充后:
0 | 1 | 2 | |
---|---|---|---|
0 | 0 | 0 | 0 |
1 | 0 | 0 | 1 |
2 | 0 | 1 | 1 |
综上所述:dp[2][2] = 1
,即 LCS 长度为 1。实际公共子序列是 [1]
或 [2]
,符合预期!
为什么这样设计?用“排除法”理解
假设我们在找 a[1..i]
和 b[1..j]
的 LCS,最后一个元素有两种可能:
- 最后一个元素相等(
a[i] == b[j]
):必须包含这个元素,所以 LCS 长度等于去掉这两个元素后的 LCS 长度加 1。 - 最后一个元素不等(
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):
- a[5] == b[6]:将5加入
ans
(此时ans=[5]
),向左上移动到x=4, y=5
(a[4]=4,b[5]=4)。 - a[4] == b[5]:将4加入
ans
(ans=[5,4]
),向左上移动到x=3, y=4
(a[3]=3,b[4]=1)。 - a[3] != b[4]:比较
dp[3][4]=2
、dp[2][4]=1
(上方)、dp[3][3]=2
(左方)。- 左方值等于当前值,说明忽略b[4],向左移动到
y=3
(b[3]=2)。
- 左方值等于当前值,说明忽略b[4],向左移动到
- a[3]=3 vs b[3]=2:不等,比较
dp[3][3]=2
、dp[2][3]=1
(上方)、dp[3][2]=2
(左方)。- 左方值等于当前值,向左移动到
y=2
(b[2]=3)。
- 左方值等于当前值,向左移动到
- a[3] == b[2]:将3加入
ans
(ans=[5,4,3]
),向左上移动到x=2, y=1
(a[2]=2,b[1]=2)。 - a[2] == b[1]:将2加入
ans
(ans=[5,4,3,2]
),向左上移动到x=1, y=0
,循环结束。 - 反转
ans
:得到正确顺序[2,3,4,5]
。
【结果】
4
[2, 3, 4, 5]