0. 前言
相关:
这是一道非常非常优秀的 LIS
、LCS
问题。
1. LIS +LCS+思维+状态划分
提前需要学习[线性dp] 最长公共子序列(模板题+最长公共子序列模型)的集合划分方式,很秀的一个方式。
重点: 线性 dp
、LIS 问题、LCS 问题
思路:
- 状态定义:
f[i][j]
所有由第一个序列的前i
个字母,和第二个序列的前j
个字母构成的(在此等价LCS
状态定义),且以b[j]
结尾的(在此等价于LIS
状态定义)公共上升子序列中长度的最大值
- 状态转移:
- 分类依据:以所有的公共上升子序列是否包含
a[i]
为分类依据- 所有不包含
a[i]
的公共上升子序列,等价于f[i-1][j]
- 所有包含
a[i]
的公共上升子序列这一坨状态是f[i][j]
,没有办法直接转化,也没有办法有状态来进行表示,可以借鉴LIS
问题的求解方式来进行状态划分- 该状态先决条件
a[i] = b[j]
- 该状态的意义是:所有包含
a[i]
且以b[j]
结尾的公共子序列 - 因为
a[i]
已经固定了,所有可以枚举b[j]
的所有情况求最大值,在这里采用LIS
问题枚举方法,枚举b[j]
的倒数第二个字母k
的全部可能出现位置。例f[i][k],k = 1~j
就等价于所有以第一个序列前i
个字母且包含a[i]
(结尾就是a[i]
),和第二个序列的前k
个字母构成(结尾就是b[j]
,b[k]
是倒数第二个字母)的公共上升子序列。就完全是LIS
的划分方式,则为f[i][k]+1
,其实在这里写成f[i-1][k]+1
也是正确的,即 +1 这个操作就是吧a[i]
和b[j]
一同加上。因为,情况 1 中已经确保f[i][j] = f[i-1][j]
了,所以修改后也是正确的
- 该状态先决条件
- 所有不包含
- 分类依据:以所有的公共上升子序列是否包含
- 时间复杂度:
- O ( n 3 ) O(n^3) O(n3)
- 这个思路属于暴力
dp
的思路。当两个子序列完全相等的时候,就会取满三重循环,就超时了。但实际运行时,a[i] = b[j]
的情况就很少,退化成两重循环的时间。但是本题可以优化
TLE代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 3005;
int n;
int a[N], b[N];
int f[N][N];
int main() {
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
for (int i = 1; i <= n; ++i) cin >> b[i];
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j) {
f[i][j] = f[i - 1][j]; // 枚举所有不包含a[i]的上升公共子序列
if (a[i] == b[j]) {
f[i][j] = max(f[i][j], 1); // 更新空集
for (int k = 1; k < j; ++k)
if (b[k] < b[j])
f[i][j] = max(f[i][j], f[i][k] + 1);
}
}
int res = 0;
for (int i = 1; i <= n; ++i) res = max(res, f[n][i]);
cout << res << endl;
return 0;
}
代码优化就是对代码进行等价变形。
for (int k = 1; k < j; ++k)
if (b[k] < b[j])
f[i][j] = max(f[i][j], f[i][k] + 1);
if (b[k] < b[j])
是配合循环从 1~j-1
找到满足小于 a[i]
的,f[i][k]
最大值。所以这个条件和 j
是没有关系的,我们可以在 j
的循环中通过一个变量来记录 1~j-1
中的最小值,动态维护更新即可。这样就能够优化一维循环了。
可谓是神仙优化…tql
优化后AC代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 3005;
int n;
int a[N], b[N];
int f[N][N];
int main() {
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
for (int i = 1; i <= n; ++i) cin >> b[i];
for (int i = 1; i <= n; ++i) {
int maxv = 1; // 初始化为1,包含空集情况
for (int j = 1; j <= n; ++j) {
f[i][j] = f[i - 1][j]; // 枚举所有不包含a[i]的上升公共子序列
// 枚举所有包含a[i],且以a[j]结尾的最长公共上升子序列
// 前i个字母包含a[i],且以b[j]结尾,所有当b[j]<a[i]的过程就是从1~j-1枚举k的过程
// b[j]的序列也是单调上升的,所以b[j]<a[i]就不会让j取到i这个位置
// 所以在此是动态更新1~j-1的最小值
if (a[i] == b[j]) f[i][j] = max(f[i][j], maxv); // 顺带更新空集
if (b[j] < a[i]) maxv = max(maxv, f[i - 1][j] + 1); // a[i]已经被使用,只能从a[1~i-1]中选,符合状态定义
} // 其实在此因为b[j]<a[i],说明a[i]!=b[j],所以直接拿f[i][j]更新也是正确的
}
int res = 0;
for (int i = 1; i <= n; ++i) res = max(res, f[n][i]);
cout << res << endl;
return 0;
}