【模板】最长公共子序列
题目描述
给出 1 , 2 , … , n 1,2,\ldots,n 1,2,…,n 的两个排列 P 1 P_1 P1 和 P 2 P_2 P2 ,求它们的最长公共子序列。
输入格式
第一行是一个数 n n n。
接下来两行,每行为 n n n 个数,为自然数 1 , 2 , … , n 1,2,\ldots,n 1,2,…,n 的一个排列。
输出格式
一个数,即最长公共子序列的长度。
样例 #1
样例输入 #1
5
3 2 1 4 5
1 2 3 4 5
样例输出 #1
3
提示
- 对于 50 % 50\% 50% 的数据, n ≤ 1 0 3 n \le 10^3 n≤103;
- 对于 100 % 100\% 100% 的数据, n ≤ 1 0 5 n \le 10^5 n≤105。
题解:
对于这个问题,我暂时只能掌握两种方法。第一种是朴素的dp,第二种是通过离散化将这个问题转化为求最长上升子序列。
朴素dp
我们定义dp[ i ][ j ]为第一个序列的前i个元素和第二个序列的前j个元素的最长公共子序列的长度。
那么
- 如果第一个序列的第i个数字和第二个序列的第j个数字相等(第一种情况):
- 则dp[ i ][ j ] = dp[ i - 1][ j - 1] + 1 ;
- 说人话就是dp[ i ][ j] = 沿主对角线左上方的格子值 + 1
- 如果不相等(第二种情况):
- 则dp[ i ][ j ] = max( dp[ i-1][ j ], dp[ i ][ j-1] ) ;
- 说人话就是dp[ i ][ j] = 正上方格子和正左方格子中的最大值
举个例子
3,2,1,5,4
2,3,5,1,4
- 首先初始化第一行和第一列全为0,第一行全为0的意义是3,2,1,5,4和一个空序列的最长公共子序列的长度,自然全为0;第一列的意义是2,3,5,1,4和一个空序列的最长公共子序列的长度,所以也全为0
- 第一种情况,我们假设现在要求dp[3][4],由于左边序列的第3个数字和上边序列的第4个数字相等,判断为第一种情况。根据上文提到的,那么它的值应该就是它左斜上方的格子值加一,也就是dp[2][3]+1=2。
- 我们现在考虑dp[2][3]的意义,它代表的是第一个序列的前2个元素和第二个序列的前3个元素的公共子序列的最大长度,也就是2,3和3,2,1的公共子序列最大长度,显然长度为1;
- 而dp[3][4]代表的是2,3,5和3,2,1,5的最长公共子序列的长度,因为这个时候出现了相同的元素(公共元素),所以就相当于给之前的最长公共子序列加了一个长度。
- 第二种情况,假设现在要求dp[3][1],由于左边序列的第3个数字和上边序列的第1个数字不相等,所以判断为第二种情况。也就是说,dp[3][1]要继承正左方格子和正上方格子中的最大值。
- dp[3][0]代表2,3,5和一个空序列的公共子列的最大长度,为0
- dp[2][1]代表2,3和3的公共子列的最大长度,那这个子列就是3嘛,长度为1.
- 然后现在把左边序列的第3个数字和上边序列的第1个数字加进去,得到dp[3][1]的意义为2,3,5和3的最长公共子列长度,它的值当然是1。
- 因为产生了不同,所以取左或上的最大值实际意义就是分别试探插入第i个数字到左子列和插入第j个数字到上子列得到的结果哪个更大。
这个方法较为简单,代码实现就不贴了,空间优化可以使用滚动数组优化成一维,但是这个时间复杂度比较高,数据范围大的时候慎用!!
离散化转最长上升子序列
观察题目中给的数据,可以看到,两个序列中的数字都是由1-n组成的,且没有重复数字,换言之,两个序列数据相同,只是数据的顺序不同,所以可以考虑使用离散化,具体实现如下:
- 在读取a数组数据的时候把每个数字在a数组中对应的位置用belong数组记录下来,代码实现就是——
for (int i = 1; i <= n; i++) {
cin >> a[i];
belong[a[i]] = i;
}
- 然后再在读入b数组的时候,把b[i]的值用b[i]=belong[b[i]]转化为b数组中的数据在a数组中的位置,我们只需要保存这些位置就好,至于b数组中原来的数据到底是什么并不重要。代码实现是——
for (int i = 1; i <= n; i++) {
cin >> b[i];
b[i] = belong[b[i]];
}
离散化好了之后,下一步工作是什么呢?
首先我们先明确一下离散化的目的:
- 离散化其实就相当于一个重新编号的过程,我们通常默认顺序参照的基准都是1、2、3、4、5、… 离散化就是更换了这个基准,它把a数组中数字的顺序当成了新的基准。
- b数组现在存放的数据是第二个序列的数据在a数组中的位置(也就是新的编号),所以现在b数组的上升子列一定是a数组的子列,这不就是公共子列吗?所以求这两个序列的最长公共子列其实就转化成了求b数组的最长上升子序列。
- 举个例子:
- 现有两个序列:
2、3、1、5、4、6
1、3、5、4、2、6 - 给第一个序列重新编号为a、b、c、d、e、f
- 那么第二个序列就变成了c、b、d、e、a、f
- 很显然,c、d、e、f是最长公共子列,同时也是第二个序列的最长上升子列。上文提到的b数组的上升子列一定是a数组的子列,根据这个例子应该很容易理解——经过重新编号,a序列单调递增,所以a序列的任意子列一定也单调递增,换而言之,任何递增的子列都是a的子列。
- 现有两个序列:
明确了以上思路之后,便能进行下一步了——求b的最长上升子序列.
求最长上升子序列仍然是有两种方法的,第一种方法是dp,第二种方法是维护low数组+二分查找。
写到这我真的累了。。。。。休息一下。。。。
dp求最长上升子序列
我们定义dp[i] = 前i个数字的上升子序列的最大长度
初始化dp[ i ] = 1(默认自己是自己的上升子列),枚举第1到i-1个数字,如果b[ j ] < b[ i ],则dp[ i ]=max(dp[ i ], dp[ j ] + 1).
思路就这样,代码不说,时间复杂度高,慎用。
维护low数组+二分查找
我们定义一个low数组,用来存放各个长度的上升子列的最小的末尾数字。
可能光说很难懂,所以我举个例子:
- 假设low[3]=4,那么它的意义就是长度为3的上升子列的最后一个数字是4,假设这个上升子列是1,2,4.
- 那么如何体现最小这两个字呢?假如现在随着扫描的进行,出现了一个数字3,它可以和1、2构成一个新的长度为3的上升子列1,2,3,且这个新子列末尾数字还小于原来的4,所以就把low[3]更新为3.
然后说完这个,我就说一下解题的思路:
- 按照贪心的思想,对于某个特定长度的上升子列,它的末尾数字越小就越有利于延长这个子列。
- 定义一个变量len表示最长上升子列的长度,初始化low[++len] = b[1],也就是初始化b数组的第一个元素为一个上升子列。
- 扫描整个b数组,假如b[ i ] > low[len],那么证明可以延长上升子列的长度,则low[++len]=b[ i ]。为什么只要比较b[ i ]和low[len]就可以下判断呢?因为从1-len,low数组的数据是天然的递增有序的。
- 假如b[ i ] < low[len],那就考虑更新len以前的数据了,因为low数组递增有序,所以可以用二分查找到low数组的1-len的数据中的第一个大于b[ i ]的数字的位置,把它更新为b[ i ]。
- 至于为什么low数组递增有序,我想用倒推解释一下,举个例子,假如求得的b数组的最长上升子列是1,3,5,7,9,11,这也将会是low数组的终态。你把这个序列切割成长度为1,长度为2,长度为3…的子列,也就是【1】,【1,3】,【1,3,5】…它们的末尾数字是否是递增的呢?显然。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
const int N = 1e5 + 1;
int n; int a[N], b[N], belong[N], low[N], len = 1;
int main()
{
//freopen("P1439_3.in", "r", stdin);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i]; belong[a[i]] = i;
}
for (int i = 1; i <= n; i++) {
cin >> b[i]; b[i] = belong[b[i]];
}
low[len] = b[1];
for (int i = 2; i <= n; i++) {
if (b[i] > low[len]) {
low[++len] = b[i];
}
else {
int l = 1, r = len, mid = (l + r) / 2;
while (l < r) {
mid = (l + r) / 2;
if (b[i] <= low[mid]) {
r = mid;
}
else
l = mid + 1;
}
low[l] = b[i];
}
}
cout << len;
return 0;
}