问题
问题分析
子序列是指从一个序列中删除部分元素(可以不删)后,剩余元素保持原有相对顺序形成的新序列(不要求连续)。
最长公共子序列,英文缩写为LCS(Longest Common Subsequence)。其定义是,一个序列 S ,如果分别是两个或多个已知序列的子序列,且是所有符合此条件序列中最长的,则 S 称为已知序列的最长公共子序列。
LCS 问题是:给定两个序列(如字符串、数组),找出它们之间长度最长的公共子序列。
例如:
- 序列
a = "ABCBDAB",序列b = "BDCAB" - 它们的 LCS 是
"BCAB"或"BDAB",长度为 4。
LCS 的标准解法可以通过动态规划在相对高效的时间内解决
考虑这样的一个状态:
- 用 二维数组
f[i][j],表示,序列a的前i个元素与序列b的前j个元素的最长公共子序列长度。
那么现在问题就变成了考察末尾元素 a[i],b[j] 是否在公共的子序列中
根据 a[i] 和 b[j] 是否相等,分两种情况更新
f[i][j]:
情况 1:
a[i] == b[j](当前字符相同)此时这两个字符可以作为 LCS 的一部分,因此 LCS 长度等于前
i-1和j-1个元素的 LCS 长度加 1,即:f[i][j] = f[i-1][j-1] + 1情况 2:
a[i] != b[j](当前字符不同)此时 LCS 长度取决于 “去掉
a[i]后的子序列” 和 “去掉b[j]后的子序列” 中更长的那个 LCS,即:f[i][j] = Math.max(f[i-1][j], f[i][j-1])
那么状态转移方程如下
考虑初始状态:
数组 f 初始化时所有元素为 0,因为空序列与任何序列的 LCS
长度为 0
- 当
i=0(a为空)时,f[0][j] = 0 - 当
j=0(b为空)时,f[i][0] = 0
时空复杂度分析
- 时间复杂度:
O(n*m),其中n和m是两个序列的长度。双层循环遍历了所有(n+1)*(m+1)个状态。 - 空间复杂度:
O(n*m),由二维数组f占用。(可优化为O(min(n,m)),但你的代码使用了标准解法)。
模板代码
Java
1 | import java.util.Scanner; |
Python
1 | n, m = map(int, input().split()) |
C++
1 |
|
无重复元素的 LCS 问题
问题分析
当问题中有额外的限制条件时,我们常能发现优化的可能性。
普通的 LCS(最长公共子序列)问题时间复杂度是 O(n*m)(n、m 为两个序列长度),当序列长度很大(比如 1e5)时,这个复杂度会超时。
但如果其中一个序列(比如 X)的元素 无重复,就可以利用这个特性把 LCS 转化为 LIS(最长递增子序列),时间复杂度优化到 O(m log m)(m 为另一个序列长度),效率极大提升。
具体而言,当序列 X 中的元素是唯一的,即串内的每个元素至多出现一次时,LCS 问题可转化为最长递增子序列 LIS 的问题,这样就能实现优化
假设我们有两个序列:
- X:无重复元素(上面说了 “序列 X 中的元素是唯一的”)
- Y:可以有重复元素(但我们只关心与 X 共有的元素)
我们的处理逻辑就是:
用 Y 中元素在 X 中的位置,生成一个新序列 Z,然后求 Z 的 LIS 长度,这个长度就是 X 和 Y 的 LCS 长度。
为什么能这样转化?
考虑条件,由于 X 中元素无重复,所以每个元素在 X 中的位置一定是唯一的(例如 X = [a,b,c,d],a 在 X 中的位置是 1,b 是 2, etc.)。
因此,Y 中能构成 LCS 的元素,在 X 中的位置必然是严格递增的,因为 X 中元素顺序固定。
这意味着:
X 和 Y 的 LCS 长度 = Y 中元素在 X 中位置构成的序列 Z 的 LIS 长度
为什么 Z 的 LIS 就是 LCS?
- 必要性:若某序列是 X 和 Y 的 LCS,则其元素在 X 中的位置必然递增(因 X 中元素顺序固定),因此对应 Z 的一个递增子序列,故 LCS长度 ≤ LIS 长度。
- 充分性:若 Z 的某递增子序列长度为 k,则其对应 Y 中的元素在 X 中位置递增,即这些元素在 X 中按顺序出现,因此构成一个公共子序列,故 LIS长度 ≤ LCS长度。
算法步骤
构建位置映射表
为 X 中的每个元素记录其位置(索引),由于 X 无重复,可用哈希表(如
HashMap)存储,实现 O (1) 查询。生成序列 Z
遍历 Y 中的每个元素:
- 若该元素在 X 中存在(即存在于映射表中),则将其在 X 中的位置加入 Z;
- 若不存在,则忽略(因为它不可能属于公共子序列)。
求 Z 的 LIS 长度
由于 Z 的 LIS 对应 X 和 Y 中位置递增且元素相同的最长子序列,即 LCS,因此 Z 的 LIS 长度就是答案。
模板代码
1 | import java.util.*; |
1 |
|
例题
题目
P2516 [HAOI2010] 最长公共子序列
题目描述
字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列。令给定的字符序列 X = {x0, x1, ⋯, xm − 1},序列 Y = {y0, y1, ⋯, yk − 1} 是 X 的子序列,当且仅当存在 X 的一个严格递增下标序列 {i0, i1, ⋯, ik − 1} ,使得对所有的 j = 0, 1, ⋯, k − 1 ,有 xij = yj 。例如,$X=\verb!"ABCBDAB"!$ ,$Y=\verb!"BCDB"!$ 是 X 的一个子序列。对给定的两个字符序列,求出他们最长的公共子序列长度,以及最长公共子序列个数。其中,两个子序列 i 和 j 不同,当且仅当长度不同或子序列中 $\exist k, i_k \neq j_k$。
输入格式
第一行为第一个字符序列,都是大写字母组成,以
.结束,大写字母个数不超过 5000。第二行为第二个字符序列,都是大写字母组成,以
.结束,大写字母个数不超过 5000。输出格式
第一行输出上述两个最长公共子序列的长度。
第二行输出所有可能出现的最长公共子序列个数,答案可能很大,只要将答案对 108 求余即可。
输入输出样例 #1
输入 #1
1
2 ABCBDAB.
BACBBD.输出 #1
1
2 4
7
题目分析
首先,这道题是 LCS(最长公共子序列)的进阶问题—— 不仅要计算最长公共子序列的长度,还要统计最长公共子序列的个数(去重后)
核心是在传统 LCS 动态规划的基础上,新增一个 “计数 DP 数组”,并通过容斥原理处理重复计数的问题。
求 LCS 和传统 LCS 问题一致 —— 找到两个序列中 “相对顺序一致、不要求连续” 的最长子序列长度。
例如输入样例中,ABCBDAB 和 BACBBD 的 LCS
长度是 4(如 BCBD、BCAB 等)。
但是额外的需求是要统计所有不同的最长公共子序列的数量,需注意:
- 两个子序列不同,只要下标选择不同(即使字符序列相同,也算不同);
- 答案需对
1e8取模(避免溢出)。
关键在于去重计数
直接累加可能会重复统计(比如某个子序列可能通过不同路径推导得到),因此需要用容斥原理减去重复计算的部分。
所以我们需要两个 DP 数组,分别处理 “长度” 和 “个数”:
长度 DP 数组:f[i][j]
- 定义:表示序列
a[1..i](第一个字符串前 i 个字符)和b[1..j](第二个字符串前 j 个字符)的 LCS 长度。 - 状态转移(和传统 LCS 完全一致):
- 若
a[i] == b[j]:当前字符可加入 LCS,f[i][j] = f[i-1][j-1] + 1; - 若
a[i] != b[j]:继承之前的最长长度,f[i][j] = max(f[i-1][j], f[i][j-1])。
- 若
计数 DP 数组:g[i][j]
- 定义:表示序列
a[1..i]和b[1..j]中,长度为f[i][j]的 LCS 的个数。
那么 g[i][j]的状态转移如何推导?
我们结合 a[i] 和 b[j] 是否相等,以及
f[i][j] 的来源,分情况推导 g[i][j]:
情况 1:
a[i] == b[j](当前字符匹配)此时
f[i][j] = f[i-1][j-1] + 1(LCS 长度比左上角多 1)。- 能形成当前 LCS 的子序列,必须包含
a[i]和b[j](因为匹配字符是 LCS 的一部分); - 因此,
g[i][j]首先继承g[i-1][j-1](所有以a[i-1]和b[j-1]结尾的 LCS,追加当前字符后形成新的 LCS)。
- 能形成当前 LCS 的子序列,必须包含
情况 2:
a[i] != b[j](当前字符不匹配)此时
f[i][j] = max(f[i-1][j], f[i][j-1])(LCS 长度来自上方或左方)。- 若
f[i][j] == f[i-1][j]:上方状态(i-1,j)的所有 LCS 都是当前状态的有效子序列,累加g[i-1][j]; - 若
f[i][j] == f[i-1][j]:上方状态(i-1,j)的所有 LCS 都是当前状态的有效子序列,累加g[i-1][j]; - 这其中就需要容斥定理
- 当
a[i] != b[j],且f[i-1][j] == f[i][j-1] == f[i][j]时:g[i-1][j]包含了所有以a[i-1]和b[j]结尾的 LCS;g[i][j-1]包含了所有以a[i]和b[j-1]结尾的 LCS;- 而
g[i-1][j-1]中的 LCS 同时被包含在g[i-1][j]和g[i][j-1]中,导致重复计数,因此需要减去一次。
- 例如:
a=AB,b=BA,当 i=2、j=2 时(a [2]=B,b [2]=A,不匹配):f[2][2] = max(f[1][2], f[2][1]) = max(1,1) = 1;g[1][2] = 1(来自a [1]=A和b [1..2]的 LCS 个数);g[2][1] = 1(来自a [1..2]和b [1]=B的 LCS 个数);g[1][1] = 1(重复计数的部分);- 因此
g[2][2] = 1+1-1 = 1(正确,LCS 个数为 1:要么选 A,要么选 B,但长度都是 1,且是同一个 LCS)。
- 当
- 若
考虑初始状态(边界条件)
g[i][0] = 1:任何序列和空序列的 LCS 只有 1 个(空序列本身);g[0][j] = 1:同理,空序列和任何序列的 LCS 只有 1 个。
Java 内存敏感,就到上面这么写会 MLE
由于 f[i][j] 和 g[i][j] 只依赖于
上一行(i-1) 和
同一行的前一列(j-1),可以用滚动数组将空间复杂度从
O(nm) 优化到 O(m)(n 和 m
是两个序列的长度,最大 5000,O(m) 更节省内存)。
- 用
f[0][j]和f[1][j]交替存储当前行和上一行的长度; - 用
g[0][j]和g[1][j]交替存储当前行和上一行的计数; - 用
u表示当前行的索引(0 或 1),u^1表示上一行的索引(异或运算快速切换)。
1 | import java.util.Scanner; |





