原型:最长上升子序列
分析
- 状态表示:所有以a[i]结尾的严格单调上升的子序列的Max长度
- 状态划分依据:以最后一个不同的点
- 状态方程:
dp[i] = max(dp[i], dp[j] + 1)
,j
必须要小于i
for(int i = 1; i <= n; i++) {
dp[i] = 1;
for(int j = 1; j < i; j++) {
if(a[i] > a[j]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
int res = 0;
for(int i = 1; i <= n; i++) res = max(res, dp[i]);
printf("%d ", res);
寻找路径
memset(tr, -1, sizeof tr);
for(int i = 1; i <= n; i++) {
dp[i] = 1;
for(int j = 1; j < i; j++) {
if(a[i] > a[j]) {
// 有更新
if(dp[i] < dp[j] + 1) {
dp[i] = dp[j] + 1;
tr[i] = j;
}
}
}
}
int ans = 0, sign = -1;
for(int i = 1; i <= n; i++) {
if(ans < dp[i]) {
ans = dp[i];
sign = i;
}
}
// 找到最终的以sign为结尾,长度为ans的序列
printf("%d %d
", ans, sign);
int i = sign;
while(~i) {
cout << a[i] << " ";
i = tr[i];
}
变形扩展
1017. 怪盗基德的滑翔翼
- 初始方向不确定,考虑从两个方向各找一遍(从左到右、从右到左)
- 最长下降子序列可转化成逆向的最长上升子序列
AcWing 1014. 登山
- 求严格上升到某一节点之后严格下降这一整段的序列的长度最大值
- 分别从左到右、从右到左求最长上升子序列长度,遍历每个节点,以此节点作为拐点,所形成的序列长度求最大值:
res = max(res, f[i] + g[i] - 1)
AcWing 1012. 友好城市
- 以南边的城市为自变量,将其排序,与之对应的因变量即北边的城市坐标,应该满足跟随自变量的递增而单调递增,即抽象出最长上升子序列问题
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 5100;
int dp[N];
struct city {
int s, n;
}cities[N];
int main() {
int n; scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d%d", &cities[i].s, &cities[i].n);
sort(cities, cities + n + 1, [&](city a, city b) { return a.n < b.n;});
for(int i = 1; i <= n; i++) {
dp[i] = 1;
for(int j = 1; j < n; j++) {
if(cities[j].n < cities[i].n) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
int res = 0;
for(int i = 1; i <= n; i++) res = max(res, dp[i]);
printf("%d", res);
return 0;
}
1016. 最大上升子序列和
- 状态表示:以该元素为结尾的上升子序列的和
for(int i = 1; i <= n; i++) {
dp[i] = a[i];
for(int j = 1; j < i; j++) {
if(dp[i] > dp[j]) {
dp[i] = max(dp[i], dp[j] + a[i]);
}
}
}
1010. 拦截导弹 : LIS + 贪心
- 第一问LIS问题
- 第二问:求覆盖所有节点的子序列的个数的最小值
贪心思路:
- 从前往后扫描每个数,对于每个数有以下两种情况
1.现有的的所有子序列的结尾的数都小于当前这个数 -> 创建一个新的子序列
2.将当前数放到结尾大于等于这个数的最小子序列后面(别耽误别人,尽量节约 haha)
贪心法求最长上升子序列:(O(n^2))超时
数据范围:需要(O(nlogn))
(1 leq N leq 100000)
(-10^9 leq 数列中的数 leq 10^9)
-
贪心策略:尽可能使得每个子序列中的结尾元素越小越好
-
具体措施:针对每个数,将这个数接到结尾元素最大的小于此元素的子序列后
/*
数组a存储所有数据
数组q[i]表示序列长度为i的子序列的最后一个数
*/
int len = 0;
for(int i = 0; i < n; i++) {
// 找到满足小于a[i]的最大值
int l = 0, r = len;
while(l < r) {
int mid = l + r + 1 >> 1;
if(q[mid] < a[i]) l = mid;
else r = mid - 1;
}
len = max(len, r + 1);
q[r + 1] = a[i];
}
cout << len;
Dilworth定理
-
Dilworth定理:对于一个偏序集,最少链划分等于最长反链长度。
-
Dilworth定理的对偶定理:对于一个偏序集,其最少反链划分数等于其最长链的长度。
以上的铺垫用于解决拦截导弹问题:转化成求最长上升子序列长度
187. 导弹防御系统: LIS + 暴搜
最长公共子序列:原型
- 所有在第一个序列的前i个字母中出现,且在第二个序列的前j个字母中出现的子序列
- Max
- 以a[i], b[j]分别选与不选划分为四种状态
- dp[i - 1][j - 1]
- dp[i - 1][j]:不一定有b[j],因此包含前面的状态(在第一个序列的前i - 1个字母出现,且在第二个序列的前j - 1个字母中出现),但状态表示的是子序列的长度最大值,因此状态存在重复,不会影响最终结果
- dp[i][j - 1]:与dp[i - 1][j]的情况类似
- dp[i - 1][j - 1] + 1:子序列中包含a[i], a[j]
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
if(a[i] == b[j]) dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + 1);
}
}
cout << dp[n][m];
最长公共上升子序列
- 状态表示:所有第一个序列的前i个字母,和第二个序列的前j个字母构成的, 且以b[j]结尾的公共上升子序列的Max
- 状态计算
1.所有不包含a[i]的公共上升子序列: dp[i - 1][j]
2.所有包含a[i]的公共上升子序列
对于包含a[i]的公共上升子序列,将其根据倒数第二个字母是什么进行划分:
- b[1]:最大长度为1
- b[2]:最大长度为dp[i - 1][1] + 1
- ...
- b[j - 1]: dp[i - 1][j - 1] + 1
for (int i = 1; i <= n; i ++ ) {
for (int j = 1; j <= n; j ++ ) {
f[i][j] = f[i - 1][j];
if (a[i] == b[j]) {
int maxv = 1;
for (int k = 1; k < j; k ++ )
if (a[i] > b[k])
maxv = max(maxv, f[i - 1][k] + 1);
f[i][j] = max(f[i][j], maxv);
}
}
}
从中发现,每次如果找到公共子序列之后,都要计算一下dp[i][1 ~ j - 1]
for (int i = 1; i <= n; i ++ ) {
int maxv = 1;
for (int j = 1; j <= n; j ++ ) {
f[i][j] = f[i - 1][j];
if (a[i] == b[j]) f[i][j] = max(f[i][j], maxv);
if (a[i] > b[j]) maxv = max(maxv, f[i - 1][j] + 1);
}
}
int res = 0;
for (int i = 1; i <= n; i ++ ) res = max(res, f[n][i]);
printf("%d
", res);