最长公共子序列(离散化转化为最长上升子序列)

【模板】最长公共子序列

题目描述

给出 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 n103
  • 对于 100 % 100\% 100% 的数据, n ≤ 1 0 5 n \le 10^5 n105

题解:

对于这个问题,我暂时只能掌握两种方法。第一种是朴素的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
在这里插入图片描述

  1. 首先初始化第一行和第一列全为0,第一行全为0的意义是3,2,1,5,4和一个空序列的最长公共子序列的长度,自然全为0;第一列的意义是2,3,5,1,4和一个空序列的最长公共子序列的长度,所以也全为0
  2. 第一种情况,我们假设现在要求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的最长公共子序列的长度,因为这个时候出现了相同的元素(公共元素),所以就相当于给之前的最长公共子序列加了一个长度。
  3. 第二种情况,假设现在要求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;
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值