最长公共上升子序列
最长公共上升子序列
熊大妈的奶牛在小沐沐的熏陶下开始研究信息题目。
小沐沐先让奶牛研究了最长上升子序列,再让他们研究了最长公共子序列,现在又让他们研究最长公共上升子序列了。
小沐沐说,对于两个数列 和 ,如果它们都包含一段位置不一定连续的数,且数值是严格递增的,那么称这一段数是两个数列的公共上升子序列,而所有的公共上升子序列中最长的就是最长公共上升子序列了。
奶牛半懂不懂,小沐沐要你来告诉奶牛什么是最长公共上升子序列。
不过,只要告诉奶牛它的长度就可以了。
数列 和 的长度均不超过 。
输入格式
第一行包含一个整数 ,表示数列 , 的长度。
第二行包含 个整数,表示数列 。
第三行包含 个整数,表示数列 。
输出格式
输出一个整数,表示最长公共上升子序列的长度。
数据范围
,序列中的数字均不超过 。
输入样例:
4 2 2 1 3 2 1 2 3
输出样例:
2
解题思路
有点像最长上升子序列与最长公共子序列问题的结合。
首先容易想到状态定义表示所有由序列的前个数且以结尾,和序列的前个数且以结尾的公共上升子序列所构成的集合,属性就是公共上升子序列的最大长度。现在公共上升子序列的最后一个数已经确定了(即和),那么根据前一个数的选择来进行状态划分。状态转移方程就是
当然还有个前提条件就是,上面的状态转移方程成立。
很明显上面的dp做法的时间复杂度为,同时发现状态转移的部分不好进行优化,因此我们需要改变状态的定义。
可以发现在上面的状态定义中,只有当时才是一个合法的状态,即如果确定以为结尾,那么对应的也就确定了。因此我们尝试只确定以为结尾,第一维的只考虑序列的前个数是否可行(反过来考虑也可以,即确定以为结尾,第二维的只考虑序列的前个数)。
因此定义状态表示所有由序列的前个数,和序列的前个数且以结尾的公共上升子序列所构成的集合。此时根据公共上升子序列是否包含来进行状态划分。如果不包含,那么对应的状态集合就是。如果包含,由于是公共上升子序列,此时应该保证,然后在公共上升子序列的序列中前一个数的选择继续划分:
因此考虑所有满足条件的所对应的状态集合就是。
因此状态转移方程就是
然而上面做法的时间复杂度为,还是会超时。但发现可以对状态转移的部分进行优化。在枚举的时候本质就是找到满足的,因此对于固定的,可以开个权值树状数组,在处维护的最大值,每次枚举到,那么就在树状数组中所有小于的中查询得到最大的。时间复杂度就降到了,但由于时间限制为,有可能会超时。事实上还是会超时,不过还是把代码贴出来:
TLE代码如下,时间复杂度为:
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就是可以发现每次枚举时,只有时才考虑包含的那个集合并进行状态转移,然后就等价于,因此我们只需维护一个前缀最大值就可以了,当枚举到且,那么就会有。而如果有则更新。直接把状态转移的时间复杂度降到。
AC代码如下,时间复杂度为:
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/
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/17239446.html
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
2022-03-21 选取数对