子序列有关问题总结
我们定义子序列为:从原序列中选取若干个元素,按原序列的顺序排列的序列。
1. 最长上升子序列问题
给定一个长为
1.1 动态规划做法
设
对于每个
这个算法的复杂度为
模板题代码:
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n);
for(int i = 0; i < n; i++) {
cin >> a[i];
}
vector<int> dp(n, 1);
int ans = 0;
for(int i = 1; i < n; i++) {
for(int j = 0; j < i; j++) {
if(a[j] < a[i]) {
dp[i] = max(dp[j] + 1, dp[i]);
}
}
ans = max(ans, dp[i]);
}
cout << ans << "\n";
return 0;
}
1.2 贪心+二分做法
设
从前往后遍历序列
要想获得最长的
这个做法的复杂度为
模板题代码:
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n);
for(int i = 0; i < n; i++) {
cin >> a[i];
}
vector<int> f{0};// f的初始值根据题目给出的数据大小决定
for(int i = 0; i < n; i++) {
if(a[i] > f.back()) {
f.push_back(a[i]);
} else {
int p = upper_bound(f.begin(), f.end(), a[i]) - f.begin();
f[p] = a[i];
}
}
cout << f.size() - 1 << "\n";
return 0;
}
1.3 树状数组优化dp
现在回过头再看之前动态规划的做法,注意到对于每个
设
有几个需要注意的地方,树状数组的大小由原序列数据的范围决定,当范围太大时记得离散化一下。本题是求最大上升子序列,要求严格递增,所以要询问
这个做法的复杂度为
模板题代码:
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
constexpr int N = 1e6 + 10;
array<int, N> fen;
void update(int x, int k) {
for(int i = x; i < N; i += i & -i) {
fen[i] = max(fen[i], k);
}
}
int query(int x) {
int res = 0;
for(int i = x; i > 0; i -= i & -i) {
res = max(res, fen[i]);
}
return res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n);
for(int i = 0; i < n; i++) {
cin >> a[i];
}
int ans = 0;
for(int i = 0; i < n; i++) {
int res = query(a[i] - 1) + 1;
ans = max(ans, res);
update(a[i], res);
}
cout << ans << "\n";
return 0;
}
1.4 其它最长子序列问题
除了最长上升子序列问题外,还有最长不下降子序列,最长下降子序列,最长不下降子序列。
若是第一种dp做法,更改一下条件即可。若是第二种二分做法,除了更改条件,还要注意是使用upper_bound还是lower_bound。
若是第三种树状数组优化dp的做法,当求下降或不上升子序列时,从后往前遍历原序列
1.5 最长子序列问题的扩展
-
对于dp做法,我们最后可以的到一个答案数组
,表示记录以 结尾的最长子序列。现给定一个定理:如果有 且 ,那么 和 一定不能形成一个符合条件的子序列。考虑反证法,如果 和 能形成一个符合条件的子序列,根据转态转态方程可知, 至少比 大1,矛盾。如果把一个序列的所有顺序对或逆序对之间连边,建图。那么根据上面的定理,我们按条件求最长子序列,得到的dp数组值相同的节点一定不相邻。
-
上面的定理反过来是不一定成立的,即:如果有
且 ,那么 和 不一定能形成一个符合条件的子序列。因为 和 并不能确定 和 的大小关系。假设我们求[10,11,1,2,3]的最长上升子序列, 和 分别为2和3,但是11和3不能形成上升子序列。 -
Dilworth定理:对偏序集
,设 中的最长链的长度为 ,那么将 中的元素分成不相交的反链,反链的个数至少是 。这个定理可以简述为:一个序列中最少不上升子序列的个数为最长上升子序列的长度。这个定理对其它最长子序列问题也适用。
2. 最长公共子序列问题
给定两个长分别为
2.1 最长公共子序列的大小
设
遍历
这个做法的复杂度为
模板题代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string a, b;
while(cin >> a >> b) {
int n = a.size(), m = b.size();
vector dp(n + 1, vector<int>(m + 1));
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
if(a[i - 1] == b[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
cout << dp[n][m] << "\n";
}
return 0;
}
2.2 求最长公共子序列
如图,假设
我们先从矩阵的右下角开始,如果有
这样我们就获得了最长公共子序列了,下图就显示了它的移动路径,并标出了加入到答案的元素。
这个做法的复杂度为
2.3 公共子序列问题的扩展
-
假设要求的序列增加到三个,求最长公共子序列的大小还是用dp的做法。设
为分别考虑三个序列的前 ,前 ,前 个元素的最长公共子序列。那么根据两个序列时的做法,有状态转移方程此外,求最长公共子序列也是类似的。如果三个位置的元素相等,则加入到答案中,否则移动到相等的数的位置。
-
如果给出的两个序列中,每个序列的中的元素都没有重复,那么它可以看成求最长上升子序列问题。
假设序列
为[3,2,1,4,5],序列 为[1,2,3,4,5]。给两个序列重新标号, 把3变成A,2变成B……于是最终两个序列变成 为[A,B,C,D,E], 为[C,B,A,D,E]。这样标号后,最长公共子序列的长度不会变,但是 变成了递增的,那么两个序列的公共子序列也是递增的。所以, 的最长上升子序列的大小,就是最长公共子序列的大小。注意:如果
中的有元素在 中没有出现,那么它对答案没有贡献,直接忽略即可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现