专栏文章
求解最长公共子序列
算法·理论参与者 1已保存评论 0
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 0 条
- 当前快照
- 1 份
- 快照标识符
- @mio9fgf7
- 此快照首次捕获于
- 2025/12/02 15:31 3 个月前
- 此快照最后确认于
- 2025/12/02 15:31 3 个月前
前置芝士
声明
将最长公共子序列称为 LCS,最长上升子序列称为 LIS。
给定序列 ,,两序列的 LCS 为 。
暴力
使用 DP 枚举。
以序列的长度为阶段,枚举两个序列,我们考虑设状态 表示子序列 , 的 LCS 的长度,表示此时的 LCS。我们需要注意 与 的转移关系,以及 与 对前面转移关系的约束,因为这两个量的等量或不等关系决定了这两个元素是否在当前的 LCS 中,也是影响 取值的。
那么我们进行分类讨论!
- 若 ,那么 在 中,即 , 是 的一个 LCS。
- 若 ,那么 , 是 与 的一个 LCS。
- 若 ,那么 , 是 与 的一个 LCS。
上述结论可由反证法严格证明。
由于 行中的 是同时存在的,有
此时算法时间复杂度是 级别,对于 的数据无法全部通过。
转化
另辟蹊径。
注意条件是给定两个排列!由于 且每个元素出现且仅出现一次。
所以我们可以构造序列 , 表示元素 在 中的出现位置 ,用代码表示就是
c[b[i]] = c[a[id]] = id ,我们举个例子。例如对于 , 在 中的第 位出现,,此时 ,,也就是 是一组对应,
注意 的元素组成的子序列 ,这是一个上升子序列,观察其在 , 的对应序列,,
,完全一样!
这里提供一个不严谨证明即可。
- 判断公共序列与具体位置无关,只考虑对应元素相等,满足先后顺序。
- 考虑 中一个上升子序列的两个元素,满足 ,此时 (这里表示映射关系),即存在 ,那么对应的元素 出现在 前,元素 出现在 前,满足 中的条件。
- 推而广之,我们就将求公共序列转化为求上升序列,当然是求最长上升子序列(LIS)。
二分求 LIS
求 的 LIS,且 及时间复杂度更高的方法是不合法的。
还是考虑 DP,因为 LIS 中后续的数必须大于前一个数(请读者使用反证法自行证明),令序列 中 表示长度为 的上升子序列的最小末尾(最后一个数的最小值),序列的最后一个存在值的下标应当是 LIS 的长度(重点!)。
请先思考上升子序列需要满足哪些条件!
对于 .
我们需要不断更新这个序列(数组),我们以 ()为阶段枚举。
注意序列应当单调递增 ,我们枚举的顺序是 (以下简称性质 )。
由于 LIS 的长度未知,所以我们只关心 的存在性,说人话就是只需找到最后一个 的位置。
首先将 全部值初始化为正无穷(不严谨说法)。
我们每次只进行两个操作。
- 取出当前 ,找到 中第一个大于等于它的数(这里就是二分),设这个数位置为 ,令 。
- 更新答案,.
简要说明 :对于操作 ,我们取出 利用了二分算法,即利用了性质 ,由于性质 ,操作时天然满足当前元素 在 之前,由于 是第一个大于等于的数,所以 ,我们可以将 接入 所代表的最长公共子序列,我们保证了 具有性质 ,同时保证了 LIS 一定被更新,也就是说找出了答案。
那么我们便获取了 的一个新的不劣的值。当然这里可以分类讨论,即 有值与无值,但两种情况下只需同样的更新操作,所以该讨论是不必要的。
我们回顾一下,首先考虑我们利用了性质 ,这个性质是我们构造的,不变的,合法;那么对于性质 呢?可以看出我们只需要 当前 保持性质 ,那么 同样保持性质 , 也同理满足,那么只需保证 符合性质 即可。那么
显然只含一个元素的 必然符合单调性,那么我们也就做完了!
总结
算法时间复杂度为 ,足够通过本题。我们回顾本题,实际上该解法有一个特点,就是所设状态不在答案中,但这不妨碍我们利用问题的最优子结构做 DP,所以我认为洛谷题解区的贪心说法并不严谨。本文倾注了作者超过 小时的时间,也是个人认为对于该问题的最好解释(目前)了,希望给大家带来一些启发。
AC Code
CPP#include<bits/stdc++.h>
using namespace std;
#define int long long//保险栓
const int N = 1e5 + 5;
struct node{
int val,id;
}x[N],y[N];
int c[N],dp[N],n,ans = 0,o;
inline int read();//快读快写不必在意
inline void write(int x,bool op);
signed main(){
n = read();
for (int i=1; i<=n; i++) x[i].val = read(),x[i].id = i,c[x[i].val] = i;//构造 c 序列
for (int i=1; i<=n; i++) y[i].val = read();
memset(dp,0x77,sizeof dp);//初始化为无穷大
dp[1] = c[y[1].val];
int x,pos;
for (int i=1; i<=n; i++){
o = c[y[i].val];//取出 ci
pos = lower_bound(dp+1,dp+n+1,o) - dp;//二分找,操作 1
ans = max(ans,pos);//更新答案,操作 2
dp[pos] = o;
}
write(ans,0);
return 0;
}
//快读快写不必在意
inline int read(){
int x = 0,f = 1;
char ch = getchar();
while (ch < '0' || ch > '9'){
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9'){
x = (x << 1) + (x << 3) + ch - '0';
ch = getchar();
}
return x * f;
}
char rec[21];
inline void write(int x,bool op){
if (x == 0){
putchar('0');
return ;
}
if (x < 0){
x = -x;
putchar('-');
}
int p = 0;
while(x){
rec[p++] = x % 10 + '0';
x /= 10;
}
while(p--){
putchar(rec[p]);
}
if (op){
putchar(' ');
}
}
特别鸣谢
@2011hym(本站同名),好同学。
@Miracle_ZX 大佬,本文受到某篇启发。
完结撒花,制作不易,管理大大给个精品吧。
相关推荐
评论
共 0 条评论,欢迎与作者交流。
正在加载评论...