最长公共上升子序列

最长公共上升子序列

熊大妈的奶牛在小沐沐的熏陶下开始研究信息题目。

小沐沐先让奶牛研究了最长上升子序列,再让他们研究了最长公共子序列,现在又让他们研究最长公共上升子序列了。

小沐沐说,对于两个数列 AB,如果它们都包含一段位置不一定连续的数,且数值是严格递增的,那么称这一段数是两个数列的公共上升子序列,而所有的公共上升子序列中最长的就是最长公共上升子序列了。

奶牛半懂不懂,小沐沐要你来告诉奶牛什么是最长公共上升子序列。

不过,只要告诉奶牛它的长度就可以了。

数列 AB 的长度均不超过 3000

输入格式

第一行包含一个整数 N,表示数列 AB 的长度。

第二行包含 N 个整数,表示数列 A

第三行包含 N 个整数,表示数列 B

输出格式

输出一个整数,表示最长公共上升子序列的长度。

数据范围

1N3000,序列中的数字均不超过 2311

输入样例:

4
2 2 1 3
2 1 2 3

输出样例:

2

 

解题思路

  有点像最长上升子序列与最长公共子序列问题的结合。

  首先容易想到状态定义f(i,j)表示所有由a序列的前i个数且以ai结尾,和b序列的前j个数且以bj结尾的公共上升子序列所构成的集合,属性就是公共上升子序列的最大长度。现在公共上升子序列的最后一个数已经确定了(即aibj),那么根据前一个数的选择来进行状态划分。状态转移方程就是f(i,j)=max1u<i, 1v<jau=bv, au<ai {f(u,v)}+1

  当然还有个前提条件就是ai=bj,上面的状态转移方程成立。

  很明显上面的dp做法的时间复杂度为O(n4),同时发现状态转移的部分不好进行优化,因此我们需要改变状态的定义。

  可以发现在上面的状态定义中,只有当ai=bjf(i,j)才是一个合法的状态,即如果确定以bj为结尾,那么对应的ai也就确定了。因此我们尝试只确定以bj为结尾,第一维的i只考虑a序列的前i个数是否可行(反过来考虑也可以,即确定以ai为结尾,第二维的j只考虑b序列的前j个数)。

  因此定义状态f(i,j)表示所有由a序列的前i个数,和b序列的前j个数且以bj结尾的公共上升子序列所构成的集合。此时根据公共上升子序列是否包含ai来进行状态划分。如果不包含ai,那么对应的状态集合就是f(i1,j)。如果包含ai,由于是公共上升子序列,此时应该保证ai=bj,然后在公共上升子序列的b序列中前一个数的选择bk继续划分:

  因此考虑所有满足条件的bk所对应的状态集合就是k=1,bk<bjj1f(i1,k)

  因此状态转移方程就是f(i,j)=max{f(i1,j), max1k<jbk<bj{f(i1,k)}+1}

  然而上面做法的时间复杂度为O(n3),还是会超时。但发现可以对状态转移的部分进行优化。在枚举k的时候本质就是找到满足bk<bjf(i1,k),因此对于固定的i,可以开个权值树状数组,在bj处维护f(i1,j)的最大值,每次枚举到f(i,j),那么就在树状数组中所有小于bjbk中查询得到最大的f(i1,k)。时间复杂度就降到了O(n2logn),但由于时间限制为1s,有可能会超时。事实上还是会超时,不过还是把代码贴出来:

  TLE代码如下,时间复杂度为O(n2logn)

复制代码
 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 const int N = 3010;
 5 
 6 int a[N], b[N];
 7 int f[N][N];
 8 int xs[N], sz;
 9 int tr[N];
10 
11 int lowbit(int x) {
12     return x & -x;
13 }
14 
15 void add(int x, int c) {
16     for (int i = x; i <= sz; i += lowbit(i)) {
17         tr[i] = max(tr[i], c);
18     }
19 }
20 
21 int query(int x) {
22     int ret = 0;
23     for (int i = x; i; i -= lowbit(i)) {
24         ret = max(ret, tr[i]);
25     }
26     return ret;
27 }
28 
29 int find(int x) {
30     int l = 1, r = sz;
31     while (l < r) {
32         int mid = l + r >> 1;
33         if (xs[mid] >= x) r = mid;
34         else l = mid + 1;
35     }
36     return l;
37 }
38 
39 int main() {
40     int n;
41     scanf("%d", &n);
42     for (int i = 1; i <= n; i++) {
43         scanf("%d", a + i);
44     }
45     for (int i = 1; i <= n; i++) {
46         scanf("%d", b + i);
47         xs[++sz] = b[i];
48     }
49     sort(xs + 1, xs + sz + 1);
50     sz = unique(xs + 1, xs + sz + 1) - xs - 1;
51     for (int i = 1; i <= n; i++) {
52         memset(tr, 0, sizeof(tr));
53         for (int j = 1; j <= n; j++) {
54             f[i][j] = f[i - 1][j];
55             if (a[i] == b[j]) f[i][j] = max(f[i][j], query(find(b[j]) - 1) + 1);
56             add(find(b[j]), f[i - 1][j]);
57         }
58     }
59     int ret = 0;
60     for (int i = 1; i <= n; i++) {
61         ret = max(ret, f[n][i]);
62     }
63     printf("%d", ret);
64     
65     return 0;
66 }
复制代码

  有个trick就是可以发现每次枚举j时,只有bj=ai时才考虑包含ai的那个集合并进行状态转移,然后bk<bj就等价于bk<ai,因此我们只需维护一个前缀最大值maxf=max1k<jbk<ai{f(i1,k)}就可以了,当枚举到jbj=ai,那么就会有f(i,j)=maxf+1。而如果有bj<ai则更新maxf=max{maxf, f(i1,j)}。直接把状态转移的时间复杂度降到O(1)

  AC代码如下,时间复杂度为O(n2)

复制代码
 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 const int N = 3010;
 5 
 6 int a[N], b[N];
 7 int f[N][N];
 8 
 9 int main() {
10     int n;
11     scanf("%d", &n);
12     for (int i = 1; i <= n; i++) {
13         scanf("%d", a + i);
14     }
15     for (int i = 1; i <= n; i++) {
16         scanf("%d", b + i);
17     }
18     for (int i = 1; i <= n; i++) {
19         int maxf = 0;
20         for (int j = 1; j <= n; j++) {
21             f[i][j] = f[i - 1][j];
22             if (b[j] == a[i]) f[i][j] = max(f[i][j], maxf + 1);
23             else if (b[j] < a[i]) maxf = max(maxf, f[i - 1][j]);
24         }
25     }
26     int ret = 0;
27     for (int i = 1; i <= n; i++) {
28         ret = max(ret, f[n][i]);
29     }
30     printf("%d", ret);
31     
32     return 0;
33 }
复制代码

 

参考资料

  AcWing 272. 最长公共上升子序列(算法提高课):https://www.acwing.com/video/364/

posted @   onlyblues  阅读(52)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
历史上的今天:
2022-03-21 选取数对
Web Analytics
点击右上角即可分享
微信分享提示