洛谷P1439 两个排列的最长公共子序列

6 阅读5分钟

原题:P1439 两个排列的最长公共子序列

题面:

P1439 两个排列的最长公共子序列

题目描述

给出 1,2,,n1,2,\ldots,n 的两个排列 P1P_1P2P_2 ,求它们的最长公共子序列。

输入格式

第一行是一个数 nn

接下来两行,每行为 nn 个数,为自然数 1,2,,n1,2,\ldots,n 的一个排列。

输出格式

一个数,即最长公共子序列的长度。

输入输出样例 #1

输入 #1

5 
3 2 1 4 5
1 2 3 4 5

输出 #1

3

说明/提示

  • 对于 50%50\% 的数据, n103n \le 10^3
  • 对于 100%100\% 的数据, n105n \le 10^5

SolutionSolution

我们观察题面,发现一个较为重要的性质:给出的两个序列均为一个 1n1-n 的排列,即代表每个数字均只在每个序列中出现一次,且在两个序列中都会出现。

我们考虑利用这个性质将问题转化。设所给排列为 a,ba,b ,假设我们已经选定了 aa 中的一个数 aia_i ,而这个数对应的在排列 bb 中出现的位置为 bjb_j ,现在我们要选定 aa 中的另一个数 aka_k ,则这个 kk 应该满足什么性质?是不是应当有 k>ik>i ,且对于数 aka_k ,设其在 bb 中出现的位置为 ll ,则显然应当有 l>jl>j 。因为我们选取的公共子序列中的数的相对位置分别在两个排列中应当是相同的,即不能让 i<k,j>li<k,j>l ,这样选取出来的序列不符合条件。

即当我们在 bb 中选数的时候,假设上一个已经选取的数为 bjb_j ,则下一个选取的数在 aa 中出现的位置应当大于 bjb_jaa 中出现的位置。不妨将 aa 中的每一个数都对应到它的下标,并存储在 pospos 数组中。则我们选取 bj,blb_j,b_l 时,应当满足 posbj<posblpos_{b_j}<pos_{b_l} ,这个时候我们就可以看出,问题转化成了求排列 bb 的最长上升子序列,只不过比较的不是 bib_i 本身的数值,而是其映射到排列 aa 中对应的下标。

这是常见的求排列的最长公共子序列(LCS)转化为求最长上升子序列(LIS)的方法。那么回顾求LIS的方法,显然我们可以通过dp来 O(n2)O(n^2) 暴力枚举,但是对于这里 n=1e5n=1e5 的数据大小显然不可行,所以我们考虑对暴力dp进行优化。

原本我们的 dpidp_i 表示的是以 ii 为结尾的最长上升子序列的长度,然后对于任意 j<ij<i 进行状态转移。但这样会产生很多不必要的状态枚举。所以我们运用贪心的思想,设想如果对于两个同样长度的上升子序列,什么样的更可能与后面的数拼接产生更优解?是不是末尾数字更小的更容易拼接后面的数字,产生更长的长度?所以我们使用 dpidp_i 表示长度为 ii 的上升子序列最小的末尾数字。

设当前枚举到的最长长度为 lenlen ,则若有 ai>dplena_i>dp_{len} ,我们可更新 dplen+1=aidp_{len+1}=a_i,并将最长长度加一。同时用 aia_i 来更新前面的 dpdp 值,如果有 ai<dpja_i<dp_j ,就更新 dpjdp_jaia_i 。那么这样我们就可以保证对于每个长度的上升子序列,均有末尾的数字最小,从而可以及时更新最长长度。

但新的问题又出现了,如果我们只是这样枚举前面的所有 jj 来试图更新,显然这样做的复杂度同样是 O(n2)O(n^2) 的,完全没有起到优化的效果,那么我们这样来设计状态又有什么意义呢?不妨看看下面的性质:

显然我们可以看出,i<j,dpi<dpj\forall i<j,dp_i<dp_j ,因为如果有 dpidpjdp_i \ge dp_j ,显然我们可以利用 dpjdp_j 的值来更新 dpidp_i 。所以这个对应关系满足单调性。提到单调性会想起什么?自然是二分答案。

我们二分答案 dpdp ,找到位置 ii 之前第一个满足 dpjaidp_j \ge a_i 的位置 jj ,并更新位置 jjdpdp 值。为什么我们可以只用更新所有 dpjaidp_j \ge a_i 的第一个点而不用更新后面的?回想我们在什么时候会更新最长长度。是不是只有 ai>dplena_i>dp_{len} 的时候?因此当我们找到的第一个最小长度 j=lenj=len 时,我们就更新当前的最长长度 lenlen 所对应的最小末尾值,从而能够使后面出现的 aia_i 更容易满足 ai>dplena_i>dp_{len} 的条件;当第一个最小长度 j<lenj<len 的时候,这次的更新能够令后面另一次的二分更新的值更趋近于 lenlen ,所以这样做总是最优的。

最长上升子序列模板:B3637 最长上升子序列

CodingCoding

#include <iostream>
#include <cstring>
#include <iomanip>
#include <cmath>
#include <vector>
#include <algorithm>
using namespace std;

#define ll long long
#define ull unsigned long long
#define debug(x) cout << #x << "=" << x << "\n";

int n;
const int maxn = 5e3 + 10;
int a[maxn];
int dp[maxn];

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> a[i];

    int len = 0;

    for (int i = 1; i <= n; i++)
    {
        int x = a[i];
        int pos = lower_bound(dp + 1, dp + len + 1, x) - dp;
        dp[pos] = x;
        if (pos > len)
            len = pos;
    }

    cout << len;

    return 0;
}

把这个模板套到最长公共子序列中就行了,多加一个坐标映射:

#include <iostream>
#include <cstring>
#include <iomanip>
#include <cmath>
#include <vector>
#include <algorithm>
using namespace std;

#define ll long long
#define ull unsigned long long
#define debug(x) cout << #x << "=" << x << "\n";

int n;
const int maxn = 1e5 + 10;
int a[maxn], b[maxn];
int pos[maxn];
int dp[maxn];

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        pos[a[i]] = i;
    }
    for (int i = 1; i <= n; i++)
        cin >> b[i];

    int len = 0;

    for (int i = 1; i <= n; i++)
    {
        int x = pos[b[i]];
        int cur_pos = lower_bound(dp + 1, dp + len + 1, x) - dp;
        dp[cur_pos] = x;
        len = max(len, cur_pos);
    }

    cout << len;

    return 0;
}

总体时间复杂度:O(nlogn)O(nlogn) ,主要在 dpdp 的状态转移+二分查找上。