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

【模板】最长公共子序列

题目描述

给出 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
    评论
树状数组可以用来解决最长公共子序列问题。下面是使用树状数组优化的最长公共子序列求解算法。 首先,我们需要将两个序列分别离散化,将每个数映射到一个连续的整数区间内,然后将它们分别存储在两个数组中。 接着,我们定义一个二维数组`dp`,其中`dp[i][j]`表示序列1中前i个数和序列2中前j个数的最长公共子序列长度。则有以下状态转移方程: ```c if (a[i] == b[j]) dp[i][j] = dp[i-1][j-1] + 1; else dp[i][j] = max(dp[i-1][j], dp[i][j-1]); ``` 其中,`a`和`b`分别是两个离散化后的序列。 时间复杂度为O(n^2)。 然后,我们可以使用树状数组来优化这个算法,将时间复杂度降至O(nlogn)。 我们需要定义一个树状数组`c`,其中`c[i]`表示序列1中前i个数中最后一个数在序列2中出现的位置。然后,我们可以用二分查找来找到序列1中第i个数在序列2中出现的最晚位置,即`c[i]`。 接着,我们可以通过遍历序列1中的每个数,用树状数组更新`c`数组,并根据`c`数组和状态转移方程来更新`dp`数组。 具体来说,对于序列1中的第i个数,我们可以用二分查找在序列2中找到它出现的最晚位置`pos`,然后用树状数组将`pos`更新为i。接着,我们可以遍历序列2中的每个数,如果它在序列1中出现过,则可以根据状态转移方程来更新`dp`数组。 时间复杂度为O(nlogn)。 以下是使用树状数组优化的最长公共子序列求解算法的完整代码实现: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_N 100000 int a[MAX_N + 10], b[MAX_N + 10]; int c[MAX_N + 10]; int dp[MAX_N + 10][2]; int n, m; int lowbit(int x) { return x & (-x); } void update(int x, int val) { while (x <= n) { c[x] = max(c[x], val); x += lowbit(x); } } int query(int x) { int res = 0; while (x) { res = max(res, c[x]); x -= lowbit(x); } return res; } int main() { scanf("%d %d", &n, &m); for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); } for (int i = 1; i <= m; i++) { scanf("%d", &b[i]); } // 离散化 int k = 1; for (int i = 1; i <= n; i++) { for (int j = k; j <= m; j++) { if (a[i] == b[j]) { a[i] = j; k = j + 1; break; } } } // 初始化 memset(c, 0, sizeof(c)); memset(dp, 0, sizeof(dp)); // 动态规划求解 for (int i = 1; i <= n; i++) { int pos = query(a[i]); dp[i][0] = dp[i-1][1]; dp[i][1] = dp[i-1][1]; if (pos > 0) { dp[i][1] = max(dp[i][1], dp[pos-1][0] + i - pos + 1); } update(a[i], i); } printf("%d\n", dp[n][1]); return 0; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值