这篇文章主要研究的是当最长公共子序列问题中,n达到1e5或以上级别时,如何巧妙使用与LIS相似的方法解决大n场景下的LCS问题。
关于最长上升子序列的两种解法,这里不会详细讲解,请读者熟知最长上升子序列的暴力和贪心解法后再来阅读本文。
对于大n而言直接使用LCS的暴力做法是n^2级别的,会超时,所以才有了接下来的思考,我们想想为什么说LCS也可以使用类似LIS的策略。
对于两个随机序列,我们要求它们的最长公共子序列,那么这两个随机序列一定是完全一样的数字组成的不同排列,或者说至少有一部分数字是一样的,组成的全排列,不然数字完全不一样根本不符合要求公共的这一条件。
A:3、2、1、4、5
B:1、2、3、4、5
以这两个序列为例,可知AB是拥有相同元素的两个不同排列,我们考虑先将第一个序列进行完全递增排序,这样可以保证第一个序列一定递增,然后由两个序列的子序列一定是A的子序列,而A本身单调递增,因此这个子序列就单调递增,换句话说在B中找到一个递增子序列,那么它在A中一定递增,而在B中找到最长上升子序列,即是答案。
如何实现?
将A序列按照递增排序这一过程并不是真的体现在代码里,它只是一种思想,我们需要用一个map来存储A数组中各个元素出现的位置编号,然后将其映射到B数组里,用这种思维举个具体的例子
用具体例子举例:首先将AB数组按照字母来代替数字,以完成较为直观的思维验证
A:a、b、c、d、e
B:c、b、a、d、e
就是说把A数组排序前B数组里各个元素在本来的A数组中出现的位置,写在B里,然后A进行排序,而在代码体现里为了方便直接记录起A数组各个元素的位置编号,然后直接使用B的映射即可,记录就是map[a[i]] = i; 而使用就是map[b[i]];
我们只需要在写代码时候有把A数组排序的这个思想就可以了,不用真的排序,因为用不到,我们是要求B数组的最长上升子序列,只需要用B数组,和map就可以了,A数组只是辅助作用。
#include <iostream>
#include <map>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
int n, f[N], a[N], b[N];
map<int, int> m;
/*
f存的是最长上升子序列是以哪个数字结尾的,这个数字在a数组的编号是什么
map存的是a数列里中各个数字在a中的位置
我们需要拿bi元素,去这个map里去查找,某个b序列的数字在a数组中的什么位置
*/
int main()
{
cin >> n;
for (int i = 1; i <= n; ++ i )
{
cin >> a[i];
m[a[i]] = i;
}
for (int i = 1; i <= n; ++ i ) cin >> b[i];
memset(f, 0x3f, sizeof f);
f[0] = 0;
int len = 0;
for (int i = 1; i <= n; ++ i )
{
int l = 0, r = len;
/* 如果b在a数组中的位置大于此时最长上升长度len的结尾数字在a中映射
那么让len++,因为我假设a数组是递增的,如果此时b元素在a数组中位置大于f【len】
len是我们在b找到的最长上升子序列长度,而f[len]是代表len长度下以某个bi结尾的元素
在a数组中的位置,此时的bi映射在a的位置更靠后,而且a又是递增,则说明找到一个更长的
len于是进行更新。
*/
if (m[b[i]] > f[len]) f[ ++ len] = m[b[i]];
else
{
/*
f[len]是最长子序列的结尾数字在a中的编号,if没进去说明此时bi映射a中的位置没有len大
但是这个bi可能可以更新其他长度的递增子序列结尾数字(此处请联系LIS更新策略)
我们可以求出来中间某个上升子序列结尾数字在a中编号有没有可以被更新的,取最小值更新
这个更新策略符合单调队列
*/
while (l < r)
{
int mid = l + r >> 1;
if (f[mid] > m[b[i]]) r = mid;
else l = mid + 1;
}
f[r] = min(f[r], m[b[i]]);
}
}
cout << len << endl;
return 0;
}
以上代码的注释应该可以帮助读者更加清晰的理解代码的用处。